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

195 lines
8.5 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 { 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) => {
set({ passwordRule: rule });
void saveStored({ departments: get().departments, members: get().members, roles: get().roles, passwordRule: rule }).catch(() => {});
},
createDepartment: (data) => {
const dept: Department = { ...data, id: `dept-${Date.now()}`, createdAt: new Date().toISOString().slice(0, 10) };
const departments = [...get().departments, dept];
set({ departments });
void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {});
},
updateDepartment: (id, data) => {
const departments = get().departments.map((d) => d.id === id ? { ...d, ...data } : d);
set({ departments });
void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {});
},
deleteDepartment: (id) => {
const departments = get().departments.filter((d) => d.id !== id && d.parentId !== id);
set({ departments });
void saveStored({ departments, members: get().members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {});
},
createMember: (data) => {
const member: Member = {
...data,
username: data.username || generateUsernameFromName(data.name, get().members.map((m) => m.username ?? '')),
phone: data.phone ?? '',
id: `m-${Date.now()}`,
createdAt: new Date().toISOString().slice(0, 10),
};
const members = [...get().members, member];
set({ members });
void saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {});
},
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 members = get().members.filter((m) => m.id !== id);
set({ members });
void saveStored({ departments: get().departments, members, roles: get().roles, passwordRule: get().passwordRule }).catch(() => {});
},
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 });
void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {});
},
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 });
void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {});
},
deleteRole: (id) => {
if (id === SYSTEM_ADMIN_ROLE_ID) return;
const roles = get().roles.filter((r) => r.id !== id);
set({ roles });
void saveStored({ departments: get().departments, members: get().members, roles, passwordRule: get().passwordRule }).catch(() => {});
},
}));