feat: 加班/版本/项目调整 + 角色权限设计

加班记录:
- 加班人默认回显当前用户且不可改
- 列表加创建日期列
- 移除编辑功能(创建即提交,仅可删除)
- 提交时校验结束>开始

版本列表:
- 移除顶部 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>
This commit is contained in:
Script Generator
2026-06-16 19:15:28 +08:00
parent 1f5bed0c79
commit c2f2fed82c
18 changed files with 1023 additions and 231 deletions

View File

@@ -2,6 +2,7 @@
import { create } from 'zustand';
import type { Department, Member, RoleItem, PasswordRule } from '@/lib/members';
import { DEFAULT_PASSWORD_RULE, generatePassword } from '@/lib/members';
import { DEFAULT_ROLE_PERMISSIONS } from '@/lib/permissions';
const STORAGE_KEY = 'ftb_members_v1';
@@ -16,11 +17,11 @@ const PRESET_DEPARTMENTS: Department[] = [
];
const PRESET_ROLES: RoleItem[] = [
{ id: 'role-admin', name: '超级管理员', description: '拥有系统全部权限', createdAt: '2024-01-01', isSystem: true },
{ id: 'role-pm', name: '产品经理', description: '管理产品和需求', createdAt: '2024-01-01' },
{ id: 'role-dev', name: '开发工程师', description: '负责开发任务', createdAt: '2024-01-01' },
{ id: 'role-test', name: '测试工程师', description: '负责测试任务', createdAt: '2024-01-01' },
{ id: 'role-design', name: '设计师', description: '负责UI/UX设计', createdAt: '2024-01-01' },
{ id: 'role-admin', name: '超级管理员', description: '拥有系统全部权限', createdAt: '2024-01-01', isSystem: true, permissions: DEFAULT_ROLE_PERMISSIONS['role-admin'] },
{ id: 'role-pm', name: '产品经理', description: '管理产品和需求', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-pm'] },
{ id: 'role-dev', name: '开发工程师', description: '负责开发任务', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-dev'] },
{ id: 'role-test', name: '测试工程师', description: '负责测试任务', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-test'] },
{ id: 'role-design', name: '设计师', description: '负责UI/UX设计', createdAt: '2024-01-01', permissions: DEFAULT_ROLE_PERMISSIONS['role-design'] },
];
const MOCK_MEMBERS: Member[] = [
@@ -74,7 +75,16 @@ export const useMemberStore = create<MemberState>((set, get) => ({
fetchMembers: () => {
const cached = loadLocal();
if (cached) set({ departments: cached.departments, members: cached.members, roles: cached.roles, passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE });
if (cached) {
// 迁移:旧 RoleItem 没 permissions 字段,补默认值
const migratedRoles = cached.roles.map((r) => {
if (!('permissions' in r) || !Array.isArray((r as any).permissions)) {
return { ...r, permissions: DEFAULT_ROLE_PERMISSIONS[r.id] ?? [] };
}
return r;
});
set({ departments: cached.departments, members: cached.members, roles: migratedRoles, passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE });
}
},
updatePasswordRule: (rule) => {
@@ -117,13 +127,26 @@ export const useMemberStore = create<MemberState>((set, get) => ({
},
createRole: (data) => {
const role: RoleItem = { ...data, id: `role-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
const role: RoleItem = {
...data,
id: `role-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
permissions: data.permissions ?? [],
};
const roles = [...get().roles, role];
set({ roles });
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
},
updateRole: (id, data) => {
const roles = get().roles.map((r) => r.id === id ? { ...r, ...data } : r);
const roles = get().roles.map((r) => {
if (r.id !== id) return r;
// 超管不允许改 permissions防误操作锁死系统
if (r.id === 'role-admin' && data.permissions) {
const { permissions, ...rest } = data;
return { ...r, ...rest };
}
return { ...r, ...data };
});
set({ roles });
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
},