Files
ftb-project-management/apps/web/stores/useMemberStore.ts
Script Generator c2f2fed82c 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>
2026-06-16 19:15:28 +08:00

159 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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 });
},
}));