Files
dukang/apps/admin-web/src/pages/HqPermissionsPage.tsx
T
jacy 0cb2b2cebb
CI / verify (pull_request) Has been cancelled
feat(admin): store detail edit with login phone sync; tighten HQ UI
HQ store update now syncs store_account.phone for shop login; expand editable store fields. System version only for SUPER_ADMIN; permissions checklist layout polish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 15:10:14 +08:00

344 lines
12 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.
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,
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[];
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,
}: {
value: string[];
onChange: (keys: string[]) => void;
disabled?: boolean;
}) {
return (
<Checkbox.Group
style={{ width: '100%' }}
value={value}
disabled={disabled}
onChange={(checked) => 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 (
<div
key={group}
style={{
marginBottom: 16,
padding: '12px 16px',
background: '#fafafa',
borderRadius: 8,
border: '1px solid #f0f0f0',
}}
>
<Typography.Text strong style={{ display: 'block', marginBottom: 10 }}>
{group}
</Typography.Text>
{compact ? (
<Space size={[24, 8]} wrap>
{items.map((item) => (
<Checkbox key={item.key} value={item.key}>
{item.label}
</Checkbox>
))}
</Space>
) : (
<Row gutter={[12, 10]}>
{items.map((item) => (
<Col key={item.key} xs={24} sm={12} md={span}>
<Checkbox value={item.key}>{item.label}</Checkbox>
</Col>
))}
</Row>
)}
</div>
);
})}
</Checkbox.Group>
);
}
export default function HqPermissionsPage() {
const [profile, setProfile] = useState<HqProfile | null>(null);
const [role, setRole] = useState<string>('OPS');
const [roleKeys, setRoleKeys] = useState<string[]>([]);
const [roleLoading, setRoleLoading] = useState(false);
const [roleSaving, setRoleSaving] = useState(false);
const [accounts, setAccounts] = useState<AccountOption[]>([]);
const [accountId, setAccountId] = useState<string>();
const [accountKeys, setAccountKeys] = useState<string[]>([]);
const [roleInheritedKeys, setRoleInheritedKeys] = useState<string[]>([]);
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(
() => [...new Set([...roleInheritedKeys, ...accountKeys])],
[roleInheritedKeys, accountKeys],
);
const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
useEffect(() => {
request<HqProfile>('/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<RolePermRes>(`/admin/hq-permissions/roles/${role}`)
.then((res) => setRoleKeys(res.permissionKeys))
.finally(() => setRoleLoading(false));
}, [isSuperAdmin, role]);
useEffect(() => {
if (!isSuperAdmin || !accountId) return;
setAccountLoading(true);
request<AccountPermRes>(`/admin/hq-permissions/accounts/${accountId}`)
.then((res) => {
setAccountKeys(res.userPermissionKeys);
setRoleInheritedKeys(res.rolePermissionKeys);
})
.finally(() => setAccountLoading(false));
}, [isSuperAdmin, accountId]);
async function saveRolePermissions() {
setRoleSaving(true);
try {
const res = await request<RolePermRes>(`/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<AccountPermRes>(`/admin/hq-permissions/accounts/${accountId}`, {
method: 'PUT',
body: JSON.stringify({ permissionKeys: accountKeys }),
});
setAccountKeys(res.userPermissionKeys);
setRoleInheritedKeys(res.rolePermissionKeys);
message.success('用户权限已保存');
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败');
} finally {
setAccountSaving(false);
}
}
if (!isSuperAdmin) {
return (
<div>
<Typography.Title level={4}>权限分配</Typography.Title>
<Alert type="warning" showIcon message="仅超级管理员可配置权限" />
</div>
);
}
return (
<div>
<Typography.Title level={4}>权限分配</Typography.Title>
<Typography.Paragraph type="secondary">
按角色配置基础权限;按用户可追加专属权限。最终生效权限 = 角色权限 用户权限。
超级管理员默认拥有除「危险操作」外的全部权限;删除用户/订单/城市需在「按用户分配」中单独勾选(默认均无)。
「系统设置」已拆分为各配置分组;「财务」对应门店/合伙人/酒厂账单。
</Typography.Paragraph>
<Tabs
items={[
{
key: 'role',
label: '按角色分配',
children: (
<Card loading={roleLoading}>
<Form layout="inline" style={{ marginBottom: 16 }}>
<Form.Item label="角色">
<Select
style={{ width: 180 }}
value={role}
onChange={setRole}
options={HQ_ADMIN_ROLES.map((r) => ({
value: r.value,
label: r.label,
disabled: r.value === 'SUPER_ADMIN',
}))}
/>
</Form.Item>
</Form>
{role === 'SUPER_ADMIN' ? (
<Alert
type="info"
showIcon
message="超级管理员基础权限固定(不含危险操作)。删除用户/订单/城市请到「按用户分配」为具体账号勾选。"
/>
) : (
<>
<PermissionChecklist value={roleKeys} onChange={setRoleKeys} />
<div style={{ marginTop: 16 }}>
<Button type="primary" loading={roleSaving} onClick={() => void saveRolePermissions()}>
保存角色权限
</Button>
</div>
</>
)}
</Card>
),
},
{
key: 'user',
label: '按用户分配',
children: (
<Card loading={accountLoading}>
<Form layout="inline" style={{ marginBottom: 16 }}>
<Form.Item label="HQ 账户">
<Select
showSearch
allowClear
placeholder="选择账户"
style={{ width: 320 }}
value={accountId}
onChange={setAccountId}
optionFilterProp="label"
options={accounts.map((a) => ({
value: a.id,
label: `${a.name} · ${a.loginName || a.phone} · ${ROLE_LABELS[a.adminRole] || a.adminRole}`,
}))}
/>
</Form.Item>
</Form>
{!accountId ? (
<Alert type="info" showIcon message="请先选择要配置的 HQ 账户" />
) : (
<>
<div style={{ marginBottom: 12 }}>
<span style={{ marginRight: 8 }}>角色继承:</span>
{selectedIsSuperAdmin ? (
<Tag color="blue">超级管理员基础权限(不含危险操作)</Tag>
) : (
<Space wrap size={[4, 4]}>
{roleInheritedKeys.map((key) => {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
return (
<Tag key={key} color="blue">
{item?.label || key}
</Tag>
);
})}
{!roleInheritedKeys.length ? (
<Typography.Text type="secondary"></Typography.Text>
) : null}
</Space>
)}
</div>
<Typography.Paragraph type="secondary">
下方勾选为用户专属追加权限(保存后与角色权限合并生效)。危险操作(删除用户/订单/城市)默认不授予,需在此勾选。
</Typography.Paragraph>
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
<div style={{ marginTop: 12 }}>
<span style={{ marginRight: 8 }}>合并生效:</span>
{selectedIsSuperAdmin ? (
<Space wrap size={[4, 4]}>
<Tag>基础权限(全部)</Tag>
{accountKeys.map((key) => {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
return (
<Tag key={key} color="orange">
{item?.label || key}
</Tag>
);
})}
</Space>
) : (
<Space wrap size={[4, 4]}>
{previewEffectiveKeys.map((key) => {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
return (
<Tag key={key}>
{item?.label || key}
</Tag>
);
})}
</Space>
)}
</div>
<div style={{ marginTop: 16 }}>
<Button type="primary" loading={accountSaving} onClick={() => void saveAccountPermissions()}>
保存用户权限
</Button>
</div>
</>
)}
</Card>
),
},
]}
/>
</div>
);
}