加班记录: - 加班人默认回显当前用户且不可改 - 列表加创建日期列 - 移除编辑功能(创建即提交,仅可删除) - 提交时校验结束>开始 版本列表: - 移除顶部 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>
159 lines
8.0 KiB
TypeScript
159 lines
8.0 KiB
TypeScript
'use client';
|
||
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';
|
||
|
||
const PRESET_DEPARTMENTS: Department[] = [
|
||
{ id: 'dept-1', name: '产品部', order: 1, createdAt: '2024-01-01' },
|
||
{ id: 'dept-2', name: '技术部', order: 2, createdAt: '2024-01-01' },
|
||
{ id: 'dept-2-1', name: '前端组', parentId: 'dept-2', order: 1, createdAt: '2024-01-01' },
|
||
{ id: 'dept-2-2', name: '后端组', parentId: 'dept-2', order: 2, createdAt: '2024-01-01' },
|
||
{ id: 'dept-2-3', name: '测试组', parentId: 'dept-2', order: 3, createdAt: '2024-01-01' },
|
||
{ id: 'dept-3', name: '设计部', order: 3, createdAt: '2024-01-01' },
|
||
{ id: 'dept-4', name: '运营部', order: 4, createdAt: '2024-01-01' },
|
||
];
|
||
|
||
const PRESET_ROLES: RoleItem[] = [
|
||
{ 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[] = [
|
||
{ id: 'm-1', name: '张三', departmentId: 'dept-1', roleId: 'role-pm', phone: '13800138001', email: 'zhangsan@company.com', password: 'Ftb@2024', createdAt: '2024-01-10' },
|
||
{ id: 'm-2', name: '李四', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13900139002', email: 'lisi@company.com', password: 'Ftb@2024', createdAt: '2024-01-12' },
|
||
{ id: 'm-3', name: '王五', departmentId: 'dept-2-2', roleId: 'role-dev', phone: '13700137003', email: 'wangwu@company.com', password: 'Ftb@2024', createdAt: '2024-01-15' },
|
||
{ id: 'm-4', name: '赵六', departmentId: 'dept-2-3', roleId: 'role-test', phone: '13600136004', email: 'zhaoliu@company.com', password: 'Ftb@2024', createdAt: '2024-02-01' },
|
||
{ id: 'm-5', name: '孙七', departmentId: 'dept-3', roleId: 'role-design', phone: '13500135005', email: 'sunqi@company.com', password: 'Ftb@2024', createdAt: '2024-02-10' },
|
||
{ id: 'm-6', name: '周八', departmentId: 'dept-4', roleId: 'role-pm', phone: '13400134006', email: 'zhouba@company.com', password: 'Ftb@2024', createdAt: '2024-02-15' },
|
||
{ id: 'm-7', name: '吴九', departmentId: 'dept-2-1', roleId: 'role-dev', phone: '13300133007', email: 'wujiu@company.com', password: 'Ftb@2024', createdAt: '2024-03-01' },
|
||
{ id: 'm-8', name: '陈十', departmentId: 'dept-1', roleId: 'role-admin', phone: '13200132008', email: 'chenshi@company.com', password: 'Ftb@2024', createdAt: '2024-01-01' },
|
||
];
|
||
|
||
interface MemberState {
|
||
departments: Department[];
|
||
members: Member[];
|
||
roles: RoleItem[];
|
||
passwordRule: PasswordRule;
|
||
loading: boolean;
|
||
fetchMembers: () => void;
|
||
updatePasswordRule: (rule: PasswordRule) => void;
|
||
createDepartment: (data: Omit<Department, 'id' | 'createdAt'>) => void;
|
||
updateDepartment: (id: string, data: Partial<Department>) => void;
|
||
deleteDepartment: (id: string) => void;
|
||
createMember: (data: Omit<Member, 'id' | 'createdAt'>) => void;
|
||
updateMember: (id: string, data: Partial<Member>) => void;
|
||
deleteMember: (id: string) => void;
|
||
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
|
||
updateRole: (id: string, data: Partial<RoleItem>) => void;
|
||
deleteRole: (id: string) => void;
|
||
}
|
||
|
||
function saveLocal(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
|
||
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch {}
|
||
}
|
||
|
||
function loadLocal(): { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
if (raw) return JSON.parse(raw);
|
||
} catch {}
|
||
return null;
|
||
}
|
||
|
||
export const useMemberStore = create<MemberState>((set, get) => ({
|
||
departments: PRESET_DEPARTMENTS,
|
||
members: MOCK_MEMBERS,
|
||
roles: PRESET_ROLES,
|
||
passwordRule: DEFAULT_PASSWORD_RULE,
|
||
loading: false,
|
||
|
||
fetchMembers: () => {
|
||
const cached = loadLocal();
|
||
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) => {
|
||
set({ passwordRule: rule });
|
||
saveLocal({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule });
|
||
},
|
||
|
||
createDepartment: (data) => {
|
||
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||
const departments = [...get().departments, dept];
|
||
set({ departments });
|
||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
updateDepartment: (id, data) => {
|
||
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
|
||
set({ departments });
|
||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
deleteDepartment: (id) => {
|
||
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
|
||
set({ departments });
|
||
saveLocal({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
|
||
createMember: (data) => {
|
||
const member: Member = { ...data, id: `m-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
|
||
const members = [...get().members, member];
|
||
set({ members });
|
||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
updateMember: (id, data) => {
|
||
const members = get().members.map((m) => m.id === id ? { ...m, ...data } : m);
|
||
set({ members });
|
||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
deleteMember: (id) => {
|
||
const members = get().members.filter((m) => m.id !== id);
|
||
set({ members });
|
||
saveLocal({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule });
|
||
},
|
||
|
||
createRole: (data) => {
|
||
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) => {
|
||
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 });
|
||
},
|
||
deleteRole: (id) => {
|
||
const roles = get().roles.filter((r) => r.id !== id);
|
||
set({ roles });
|
||
saveLocal({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule });
|
||
},
|
||
}));
|