0cb2b2cebb
CI / verify (pull_request) Has been cancelled
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>
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
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>
|
||
);
|
||
}
|