Files
ftb-project-management/apps/web/stores/useMemberStore.ts
2026-07-03 19:36:18 +08:00

250 lines
10 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, ensureMemberUsernames, generatePassword, generateUsernameFromName } from '@/lib/members';
import {
SYSTEM_ADMIN_MEMBER,
SYSTEM_ADMIN_ROLE_ID,
canDeleteSystemMember,
ensureSystemAdminMember,
sanitizeSystemAdminPatch,
} from '@/lib/member-system';
import { scheduleSaveWithOptimisticRollback } from '@/lib/optimistic-persistence';
import { DEFAULT_ROLE_PERMISSIONS, DEFAULT_ROLE_PRESETS } from '@/lib/permissions';
import { mergePresetRolePermissions } from '@/lib/role-permission-migration';
import { loadServerData, saveServerData, SERVER_DATA_CACHE_MS } from '@/lib/server-data';
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[] = DEFAULT_ROLE_PRESETS;
const MOCK_MEMBERS: Member[] = [
SYSTEM_ADMIN_MEMBER,
];
let lastMembersFetchAt = 0;
interface MemberState {
departments: Department[];
members: Member[];
roles: RoleItem[];
passwordRule: PasswordRule;
loading: boolean;
loaded: boolean;
fetchMembers: (options?: { force?: boolean }) => Promise<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>) => Promise<void>;
deleteMember: (id: string) => void;
createRole: (data: Omit<RoleItem, 'id' | 'createdAt'>) => void;
updateRole: (id: string, data: Partial<RoleItem>) => void;
deleteRole: (id: string) => void;
}
async function saveStored(state: { departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }) {
await saveServerData('members', state);
}
async function loadStored(): Promise<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule } | null> {
try {
return await loadServerData<{ departments: Department[]; members: Member[]; roles: RoleItem[]; passwordRule: PasswordRule }>('members');
} 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,
loaded: false,
fetchMembers: async (options) => {
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
const cached = await loadStored();
if (!options?.force && get().loaded && Date.now() - lastMembersFetchAt < SERVER_DATA_CACHE_MS) return;
lastMembersFetchAt = Date.now();
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;
});
const permissionMerged = mergePresetRolePermissions(migratedRoles);
const rolesWithNewDefaults = permissionMerged.roles;
const adminRole = PRESET_ROLES.find((r) => r.id === SYSTEM_ADMIN_ROLE_ID)!;
const roles = rolesWithNewDefaults.some((r) => r.id === SYSTEM_ADMIN_ROLE_ID)
? rolesWithNewDefaults.map((r) => r.id === SYSTEM_ADMIN_ROLE_ID ? { ...adminRole, ...r, isSystem: true, permissions: DEFAULT_ROLE_PERMISSIONS[SYSTEM_ADMIN_ROLE_ID] } : r)
: [adminRole, ...rolesWithNewDefaults];
const rolesChanged =
permissionMerged.changed ||
roles.length !== rolesWithNewDefaults.length ||
roles.some((role, index) => JSON.stringify(role) !== JSON.stringify(rolesWithNewDefaults[index]));
const ensured = ensureSystemAdminMember(cached.members);
const usernameEnsured = ensureMemberUsernames(ensured.members);
const next = {
departments: cached.departments,
members: usernameEnsured.members,
roles,
passwordRule: cached.passwordRule || DEFAULT_PASSWORD_RULE,
};
set({ ...next, loaded: true });
if (ensured.changed || usernameEnsured.changed || rolesChanged) void saveStored(next).catch(() => {});
} else {
set({ loaded: true });
}
},
updatePasswordRule: (rule) => {
const previous = get().passwordRule;
set({ passwordRule: rule });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule }),
expected: rule,
getCurrent: () => get().passwordRule,
rollback: () => set({ passwordRule: previous }),
});
},
createDepartment: (data) => {
const previous = get().departments;
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
const departments = [...previous, dept];
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
updateDepartment: (id, data) => {
const previous = get().departments;
const departments = previous.map((d) => d.id === id ? { ...d, ...data } : d);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
deleteDepartment: (id) => {
const previous = get().departments;
const departments = previous.filter((d) => d.id !== id && d.parentId !== id);
set({ departments });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }),
expected: departments,
getCurrent: () => get().departments,
rollback: () => set({ departments: previous }),
});
},
createMember: (data) => {
const previous = get().members;
const member: Member = {
...data,
username: data.username || generateUsernameFromName(data.name, previous.map((m) => m.username ?? '')),
phone: data.phone ?? '',
id: `m-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
};
const members = [...previous, member];
set({ members });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }),
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),
});
},
updateMember: async (id, data) => {
const previousMembers = get().members;
const safeData = sanitizeSystemAdminPatch(id, data);
const updatedMembers = get().members.map((m) => m.id === id ? { ...m, ...safeData } : m);
const ensured = ensureSystemAdminMember(updatedMembers);
set({ members: ensured.members });
try {
await saveStored({ departments: get().departments, members: ensured.members, roles: get().roles, passwordRule: get().passwordRule });
} catch (error) {
set({ members: previousMembers });
throw error;
}
},
deleteMember: (id) => {
if (!canDeleteSystemMember(id)) return;
const previous = get().members;
const members = previous.filter((m) => m.id !== id);
set({ members });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }),
expected: members,
getCurrent: () => get().members,
rollback: () => set({ members: previous }),
});
},
createRole: (data) => {
const previous = get().roles;
const role: RoleItem = {
...data,
id: `role-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
permissions: data.permissions ?? [],
};
const roles = [...previous, role];
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
updateRole: (id, data) => {
const previous = get().roles;
const roles = previous.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 });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
deleteRole: (id) => {
if (id === SYSTEM_ADMIN_ROLE_ID) return;
const previous = get().roles;
const roles = previous.filter((r) => r.id !== id);
set({ roles });
scheduleSaveWithOptimisticRollback({
save: () => saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }),
expected: roles,
getCurrent: () => get().roles,
rollback: () => set({ roles: previous }),
});
},
}));