加班记录: - 加班人默认回显当前用户且不可改 - 列表加创建日期列 - 移除编辑功能(创建即提交,仅可删除) - 提交时校验结束>开始 版本列表: - 移除顶部 9 个状态 tab + 表格状态列 - 仅保留搜索、项目、优先级筛选 项目列表: - 行内显示进度未达 100% 的版本(与版本页同源算法) - 新增 lib/version-progress.ts 抽出公共进度计算 角色权限: - RoleItem 加 permissions 字段 - 新建 lib/permissions.ts: 14 组 37 个权限点 + 默认 5 角色映射 + hasPermission - 角色表单内嵌权限矩阵(主模块 4 件套 + Tab 二档 + Bug Tab 4 档)+ 全选/反选/仅查看快捷 - 新建 components/auth/Guard.tsx: RouteGuard + PermissionGuard + useHasPermission + AccessDenied - Sidebar 菜单按 view 权限过滤 - 7 个主路由(products/projects/versions/requirements/overtime/admin/members/roles)包 RouteGuard - 版本详情 Tab 按 view 权限过滤,自动跳转到第一个有权限的 Tab - 默认未登录/无角色按只读最严格 - 超管 role-admin: ['*'] 通配符,permissions 不可改 通用: - 新建 components/FieldError.tsx 统一表单错误文案 - PlanTab 提交时校验结束>开始 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
10 KiB
角色权限设计
Context
当前系统有 5 个默认角色(超管/产品经理/开发/测试/设计师),但 RoleItem 没有 permissions 字段,新增角色表单也只能填名字+描述。所有页面/按钮都没做权限拦截,任何登录用户都能进所有模块、做所有操作。
本期目标:给角色加权限字段、为默认 5 角色按职能分配权限、新增角色 UI 支持勾选权限、并且把权限拦截一并接入到 7 个主模块 + 版本详情 7 个 Tab + 各按钮。
决策摘要
| 项 | 决策 |
|---|---|
| 权限粒度 | 细粒度(view/create/edit/delete + 加班特殊操作 export) |
| 模块清单 | 7 主模块 + 7 个版本详情 Tab;与我相关不纳入 |
| 数据结构 | RoleItem.permissions: string[],module:action 形式(推荐方案 A) |
| 超管标记 | permissions: ['*'] 通配符 |
| Bug Tab 粒度 | 单独拆 4 档(view/create/edit/delete),其他 Tab 二档 |
| 默认 5 角色映射 | 按职能划分(详见下方) |
| 新增角色默认值 | 全部不勾选(最小授权) |
| 新增角色 UI | 现有表单内嵌权限矩阵 + 顶部"全选/反选/仅查看"快捷 |
| 拦截接入范围 | 本期一并接入:路由级 + 按钮级 + Sidebar 菜单 + Tab 级 |
| 未登录/无角色 | 默认按只读(最严格) |
| 权限点总数 | 37 个 |
| localStorage 迁移 | fetchMembers 启动检测旧 RoleItem 无 permissions 字段则补默认值 |
1. 权限点字典(37 个)
主模块(4 件套,加班加 export):
| 模块 | 权限点 |
|---|---|
| 产品 product | view / create / edit / delete |
| 项目 project | view / create / edit / delete |
| 版本 version | view / create / edit / delete |
| 需求池 requirement | view / create / edit / delete |
| 加班记录 overtime | view / create / delete / export |
| 成员 member | view / create / edit / delete |
| 角色 role | view / create / edit / delete |
版本详情 Tab(其他二档,Bug 单独 4 档):
| Tab | 权限点 |
|---|---|
需求 Tab version.req |
view / manage |
调研 Tab version.research |
view / manage |
产品方案 Tab version.product_plan |
view / manage |
UI 设计 Tab version.ui_plan |
view / manage |
开发任务 Tab version.devtask |
view / manage |
测试用例 Tab version.testcase |
view / manage |
Bug Tab version.bug |
view / create / edit / delete |
说明:Tab 二档因为 Tab 内部操作高度耦合(manage = 该 Tab 内全部增删改)。Bug 单独拆 4 档因为开发与测试在 Bug 流程上职能不同:测试创建/关闭,开发改状态/修复,测试验证。
2. 默认 5 角色权限映射
(1) 超级管理员 role-admin — permissions: ['*']
(2) 产品经理 role-pm:
- 全权:product / project / version / requirement / version.req / version.product_plan
- 仅查看:overtime(view)/ member(view)/ role(view)/ version.research / version.ui_plan / version.devtask / version.testcase / version.bug
(3) 开发工程师 role-dev:
- 全权:version.devtask
- Bug Tab:view + edit
- 加班:view + create
- 仅查看:product / project / version / requirement / version.req / version.research / version.product_plan / version.ui_plan / version.testcase
- 不可:member / role / overtime export+delete / 主模块的 create/edit/delete
(4) 测试工程师 role-test:
- 全权:version.testcase / version.bug(4 档全权)
- 加班:view + create
- 仅查看:product / project / version / requirement / version.req / version.research / version.product_plan / version.ui_plan / version.devtask
- 不可:同开发
(5) 设计师 role-design:
- 全权:version.ui_plan
- 加班:view + create
- 仅查看:product / project / version / requirement / version.req / version.research / version.product_plan / version.devtask / version.testcase / version.bug
- 不可:同开发
3. 数据结构 + Store
apps/web/lib/members.ts:RoleItem 加 permissions: string[](必填,默认 [])
apps/web/lib/permissions.ts 新增:
export type Permission = string;
export interface ActionDef {
action: 'view' | 'create' | 'edit' | 'delete' | 'manage' | 'export';
label: string;
permission: string;
}
export interface PermissionGroup {
module: string;
moduleLabel: string;
category: 'main' | 'version_tab';
actions: ActionDef[];
}
export const PERMISSION_GROUPS: PermissionGroup[] = [/* 14 组 */];
export const ALL_PERMISSIONS: string[] = /* 37 个展开 */;
export const DEFAULT_ROLE_PERMISSIONS: Record<string, string[]> = {
'role-admin': ['*'],
'role-pm': [...],
'role-dev': [...],
'role-test': [...],
'role-design': [...],
};
export function hasPermission(role: RoleItem | undefined, permission: string): boolean {
if (!role) return false;
if (role.permissions.includes('*')) return true;
return role.permissions.includes(permission);
}
apps/web/stores/useMemberStore.ts:
PRESET_ROLES5 条加permissionsfetchMembers加迁移:旧 RoleItem 无 permissions 字段时补默认值createRole/updateRole接收permissions: string[]- 超管
role-admin不可改 permissions(updateRole拦截)
4. UI 改动
(A) 角色表单(apps/web/app/admin/roles/page.tsx)
RoleModal 加权限区块(在 description 下方)。Modal 宽度 max-w-xs → max-w-2xl。
布局:
[全选] [反选] [仅查看] ← 顶部快捷按钮
主模块
─────────────────────────
产品 ☐查看 ☐创建 ☐编辑 ☐删除
项目 ☐查看 ☐创建 ☐编辑 ☐删除
版本 ☐查看 ☐创建 ☐编辑 ☐删除
需求池 ☐查看 ☐创建 ☐编辑 ☐删除
加班记录 ☐查看 ☐创建 ☐删除 ☐导出
成员 ☐查看 ☐创建 ☐编辑 ☐删除
角色 ☐查看 ☐创建 ☐编辑 ☐删除
版本详情 Tab
─────────────────────────
需求 Tab ☐查看 ☐管理
调研 Tab ☐查看 ☐管理
产品方案 Tab ☐查看 ☐管理
UI 设计 Tab ☐查看 ☐管理
开发任务 Tab ☐查看 ☐管理
测试用例 Tab ☐查看 ☐管理
Bug Tab ☐查看 ☐创建 ☐编辑 ☐删除
- 系统角色(
isSystem)表单顶部黄色提示"超管权限不可修改",所有 checkbox 设disabled - 编辑态:
initial.permissions默认勾选
(B) 全局拦截层(apps/web/components/auth/Guard.tsx 新增)
// 路由级
<RouteGuard permission="product:view">
<ProductsPage />
</RouteGuard>
// 按钮级
<PermissionGuard permission="product:create" fallback={null}>
<button>新建产品</button>
</PermissionGuard>
// hook
const canEdit = useHasPermission('product:edit');
useHasPermission 实现:从 useAuthStore 取 user.roleId,从 useMemberStore 查 role.permissions,调 hasPermission。
(C) 接入清单
路由级:
/productsproduct:view/projectsproject:view/versionsversion:view/requirementsrequirement:view/overtimeovertime:view/admin/membersmember:view/admin/rolesrole:view- 无权限:
<AccessDenied />占位组件(不跳转,避免循环) /workspace不拦截
按钮级:
- 各页面"新建/编辑/删除/导出"按钮包
PermissionGuard OvertimeModal内的"删除原因"按钮要overtime:delete
Sidebar 菜单过滤:
- 7 个主菜单按
<module>:view过滤——无权限的菜单不显示(不灰显) - 工作区组永远显示(与我相关)
版本详情 Tab 过滤(apps/web/app/versions/[id]/page.tsx):
- Tab 列表渲染时按
version.<tab>:view过滤——无权限的 Tab 不渲染 - Tab 内"新建/管理"按钮按对应 manage / 4 档权限拦截
(D) 默认状态处理
- mock 用户已登录但 role 找不到 / 用户未登录:当前组件
useHasPermission返回false,所有页面/菜单/按钮都显示无权限——最严格只读 - 现有 mock 数据
m-8 陈十是role-admin,登录该用户即超管全权 - 切换其他 mock 用户(PM/开发/测试/设计)测试不同视图
数据流
useAuthStore.user
│
└─ user.roleId
│
└─ useMemberStore.roles.find(r => r.id === user.roleId)
│
└─ role.permissions: string[]
│
├─ useHasPermission(perm) ─→ 按钮/Guard
├─ Sidebar 菜单过滤 ─→ 顶级菜单
└─ Tab 过滤 ─→ 版本详情
实施清单
新增:
apps/web/lib/permissions.ts— 权限字典 + DEFAULT_ROLE_PERMISSIONS + hasPermissionapps/web/components/auth/Guard.tsx— RouteGuard + PermissionGuard + useHasPermission + AccessDenied
修改:
apps/web/lib/members.ts— RoleItem 加 permissionsapps/web/stores/useMemberStore.ts— PRESET_ROLES 补 permissions + 迁移逻辑 + 超管保护apps/web/app/admin/roles/page.tsx— RoleModal 加权限矩阵 + 顶部快捷apps/web/components/layout/Sidebar.tsx— 菜单按 view 过滤apps/web/app/products/page.tsx— RouteGuard + 按钮拦截apps/web/app/projects/page.tsx— 同上apps/web/app/versions/page.tsx— 同上apps/web/app/requirements/page.tsx— 同上apps/web/app/overtime/page.tsx— 同上 + export 按钮拦截apps/web/app/admin/members/page.tsx— 同上apps/web/app/versions/[id]/page.tsx— Tab 过滤 + Tab 内按钮拦截
验证
pnpm type-check0 错误pnpm build14 个页面成功- 角色页:超管显示"系统不可修改"+ checkbox disabled;其他 4 个角色显示已勾选权限
- 新建角色:勾选"产品-查看 + 加班-查看"保存,应能在新角色下看到对应菜单
- 切换 mock 用户为开发工程师(如李四 m-2):Sidebar 看不到成员/角色菜单;进版本详情看不到测试用例/Bug 的 manage 按钮(但能看 view)
- 切换设计师(孙七 m-5):UI 设计 Tab 全权,开发任务 Tab 只读
- localStorage 旧 RoleItem 无 permissions 字段:启动后自动补默认值,UI 正常渲染