import { useEffect, useMemo, useState } from 'react'; import { Alert, Button, Card, Checkbox, Col, Form, Row, Select, Space, Tabs, Tag, Typography, message, } from 'antd'; import { HQ_ADMIN_ROLES, HQ_PERMISSION_CATALOG, computeHqEffectivePermissionKeys, type HqPermissionKey, } from '@dukang/shared-types'; import { request, type HqProfile } from '../lib/api'; type RolePermRes = { role: string; permissionKeys: string[] }; type AccountOption = { id: string; name: string; phone: string; loginName: string | null; adminRole: string }; type AccountPermRes = { account: AccountOption; permissionKeys: string[]; rolePermissionKeys: string[]; grantKeys?: string[]; denyKeys?: string[]; userPermissionKeys: string[]; effectivePermissionKeys: string[]; }; const ROLE_LABELS = Object.fromEntries(HQ_ADMIN_ROLES.map((r) => [r.value, r.label])); const CATALOG_GROUPS = [...new Set(HQ_PERMISSION_CATALOG.map((p) => p.group ?? '其他'))]; function groupColSpan(itemCount: number): number { if (itemCount <= 2) return 12; if (itemCount === 3) return 8; if (itemCount === 4) return 6; return 8; } function PermissionChecklist({ value, onChange, disabled, roleKeys, grantKeys, }: { value: string[]; onChange: (keys: string[]) => void; disabled?: boolean; roleKeys?: string[]; grantKeys?: string[]; }) { const roleSet = new Set(roleKeys ?? []); const grantSet = new Set(grantKeys ?? []); return ( onChange(checked as string[])} > {CATALOG_GROUPS.map((group) => { const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group); const span = groupColSpan(items.length); const compact = items.length <= 3; return (
{group} {compact ? ( {items.map((item) => ( {item.label} {roleSet.has(item.key) && !value.includes(item.key) ? ( 已撤销 ) : grantSet.has(item.key) ? ( 追加 ) : roleSet.has(item.key) ? ( 角色 ) : null} ))} ) : ( {items.map((item) => ( {item.label} {roleSet.has(item.key) && !value.includes(item.key) ? ( 已撤销 ) : grantSet.has(item.key) ? ( 追加 ) : roleSet.has(item.key) ? ( 角色 ) : null} ))} )}
); })}
); } function splitAccountOverrides(roleKeys: string[], effectiveKeys: string[]) { const roleSet = new Set(roleKeys); const effectiveSet = new Set(effectiveKeys); const grantKeys = effectiveKeys.filter((k) => !roleSet.has(k)); const denyKeys = roleKeys.filter((k) => !effectiveSet.has(k)); return { grantKeys, denyKeys }; } export default function HqPermissionsPage() { const [profile, setProfile] = useState(null); const [role, setRole] = useState('OPS'); const [roleKeys, setRoleKeys] = useState([]); const [roleLoading, setRoleLoading] = useState(false); const [roleSaving, setRoleSaving] = useState(false); const [accounts, setAccounts] = useState([]); const [accountId, setAccountId] = useState(); const [accountKeys, setAccountKeys] = useState([]); const [roleInheritedKeys, setRoleInheritedKeys] = useState([]); const [grantKeys, setGrantKeys] = useState([]); const [denyKeys, setDenyKeys] = useState([]); const [accountLoading, setAccountLoading] = useState(false); const [accountSaving, setAccountSaving] = useState(false); const selectedAccount = useMemo( () => accounts.find((a) => a.id === accountId), [accounts, accountId], ); const selectedIsSuperAdmin = selectedAccount?.adminRole === 'SUPER_ADMIN'; const previewEffectiveKeys = useMemo( () => computeHqEffectivePermissionKeys(roleInheritedKeys, grantKeys, denyKeys), [roleInheritedKeys, grantKeys, denyKeys], ); const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN'; useEffect(() => { request('/admin/auth/me').then(setProfile).catch(() => {}); }, []); useEffect(() => { if (!isSuperAdmin) return; request<{ items: AccountOption[] }>('/admin/hq-accounts?page=1&pageSize=100') .then((res) => setAccounts(res.items)) .catch(() => {}); }, [isSuperAdmin]); useEffect(() => { if (!isSuperAdmin || !role) return; setRoleLoading(true); request(`/admin/hq-permissions/roles/${role}`) .then((res) => setRoleKeys(res.permissionKeys)) .finally(() => setRoleLoading(false)); }, [isSuperAdmin, role]); useEffect(() => { if (!isSuperAdmin || !accountId) return; setAccountLoading(true); request(`/admin/hq-permissions/accounts/${accountId}`) .then((res) => { setGrantKeys(res.grantKeys ?? res.userPermissionKeys ?? []); setDenyKeys(res.denyKeys ?? []); setRoleInheritedKeys(res.rolePermissionKeys); setAccountKeys(res.effectivePermissionKeys); }) .finally(() => setAccountLoading(false)); }, [isSuperAdmin, accountId]); async function saveRolePermissions() { setRoleSaving(true); try { const res = await request(`/admin/hq-permissions/roles/${role}`, { method: 'PUT', body: JSON.stringify({ permissionKeys: roleKeys }), }); setRoleKeys(res.permissionKeys); message.success('角色权限已保存'); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setRoleSaving(false); } } async function saveAccountPermissions() { if (!accountId) return; setAccountSaving(true); try { const res = await request(`/admin/hq-permissions/accounts/${accountId}`, { method: 'PUT', body: JSON.stringify({ grantKeys, denyKeys }), }); setGrantKeys(res.grantKeys ?? res.userPermissionKeys ?? []); setDenyKeys(res.denyKeys ?? []); setRoleInheritedKeys(res.rolePermissionKeys); setAccountKeys(res.effectivePermissionKeys); message.success('用户权限已保存'); } catch (e) { message.error(e instanceof Error ? e.message : '保存失败'); } finally { setAccountSaving(false); } } if (!isSuperAdmin) { return (
权限分配
); } return (
权限分配 按角色配置基础权限;按用户可追加或撤销。最终生效权限 =(角色权限 ∪ 追加)− 撤销。 超级管理员默认拥有除「危险操作」外的全部权限;删除用户/订单/城市需在「按用户分配」中单独勾选(默认均无)。 运营/财务默认可删除门店分类;城市门店服务可新增分类,不可删除。 「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
({ value: a.id, label: `${a.name} · ${a.loginName || a.phone} · ${ROLE_LABELS[a.adminRole] || a.adminRole}`, }))} />
{!accountId ? ( ) : ( <> 勾选表示该账号最终拥有该权限。取消角色已有项即为撤销;勾选角色没有的项即为追加。 切换角色会清空账号级追加/撤销。 { if (selectedIsSuperAdmin) return; const next = splitAccountOverrides(roleInheritedKeys, nextEffective); setGrantKeys(next.grantKeys); setDenyKeys(next.denyKeys); setAccountKeys(nextEffective); }} /> {selectedIsSuperAdmin ? ( ) : null}
合并生效: {previewEffectiveKeys.map((key) => { const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey)); return ( {item?.label || key} ); })}
)} ), }, ]} />
); }