后端修改权限和折叠功能
CI / verify (pull_request) Has been cancelled

This commit is contained in:
2026-07-23 14:38:52 +08:00
parent 6479365d6a
commit 75765bf9d4
11 changed files with 375 additions and 54 deletions
+81 -3
View File
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { Layout, Menu, Typography, Button, Space } from 'antd';
import type { MenuProps } from 'antd';
@@ -18,11 +18,14 @@ import {
SettingOutlined,
AccountBookOutlined,
} from '@ant-design/icons';
import { hasAnySystemSettingsPermission } from '@dukang/shared-types';
import { clearAuth, request, type HqProfile } from '../lib/api';
import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
const { Header, Sider, Content } = Layout;
type MenuItem = NonNullable<MenuProps['items']>[number];
const MENU_ITEMS: MenuProps['items'] = [
{ key: '/', icon: <DashboardOutlined />, label: '概览' },
{ key: '/users', icon: <UserOutlined />, label: '用户' },
@@ -111,6 +114,75 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
];
function menuAllowed(key: string, permissionKeys: string[]): boolean {
const map: Record<string, string | 'system_settings_any'> = {
'/': 'dashboard',
'/users': 'users',
'/wechat-bindings': 'wechat_bindings',
'products-group': 'products',
'/products': 'products',
'/product-detail-templates': 'products',
'/orders': 'orders',
'/promo-codes': 'promo_codes',
'stores-group': 'stores',
'/stores': 'stores',
'/store-categories': 'stores',
'/store-accounts': 'stores',
'/store-media': 'stores',
'partners-group': 'partners',
'/cities': 'partners',
'/city-partners': 'partners',
'/city-warehouses': 'partners',
'/fulfillment-providers': 'partners',
'finance-group': 'finance',
'/finance/store-bills': 'finance',
'/finance/partner-bills': 'finance',
'/finance/winery-bills': 'finance',
'benefit-group': 'benefit',
'/benefit/coupons': 'benefit',
'/benefit/ledgers': 'benefit',
'/redeem-records': 'benefit',
'/redeem-pending': 'benefit',
'/redeem/debug': 'benefit',
'deliveries-group': 'deliveries',
'/deliveries': 'deliveries',
'/deliveries/xiaofeixia': 'deliveries',
'/tickets': 'tickets',
'/invoices': 'invoices',
'/resources': 'resources',
'logs-group': 'logs',
'/logs/users': 'logs',
'/logs/stores': 'logs',
'/logs/partners': 'logs',
'/logs/hq': 'logs',
'/logs/third-party': 'logs',
'/hq-permissions': 'hq_permissions',
'/system-settings': 'system_settings_any',
'/hq-accounts': 'hq_accounts',
};
const need = map[key];
if (!need) return true;
if (need === 'system_settings_any') return hasAnySystemSettingsPermission(permissionKeys);
return permissionKeys.includes(need);
}
function filterMenuItems(items: MenuProps['items'], permissionKeys: string[]): MenuProps['items'] {
if (!items) return items;
return items
.map((item) => {
if (!item || typeof item !== 'object' || !('key' in item)) return item;
const key = String(item.key);
if ('children' in item && Array.isArray(item.children)) {
if (!menuAllowed(key, permissionKeys)) return null;
const children = filterMenuItems(item.children as MenuProps['items'], permissionKeys);
if (!children?.length) return null;
return { ...item, children } as MenuItem;
}
return menuAllowed(key, permissionKeys) ? item : null;
})
.filter(Boolean) as MenuProps['items'];
}
export default function AdminLayout() {
const navigate = useNavigate();
const location = useLocation();
@@ -138,6 +210,12 @@ export default function AdminLayout() {
? '/promo-codes'
: location.pathname;
const menuItems = useMemo(() => {
if (!profile) return MENU_ITEMS;
if (profile.adminRole === 'SUPER_ADMIN') return MENU_ITEMS;
return filterMenuItems(MENU_ITEMS, profile.permissionKeys ?? []);
}, [profile]);
return (
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
<Sider
@@ -156,8 +234,8 @@ export default function AdminLayout() {
theme="dark"
mode="inline"
selectedKeys={[selectedKey]}
defaultOpenKeys={['products-group', 'stores-group', 'partners-group', 'finance-group', 'benefit-group', 'logs-group', 'deliveries-group']}
items={MENU_ITEMS}
defaultOpenKeys={[]}
items={menuItems}
onClick={({ key }) => {
if (key.startsWith('/')) navigate(key);
}}
+1
View File
@@ -7,6 +7,7 @@ export type HqProfile = {
name: string;
adminRole: string;
status: string;
permissionKeys?: string[];
};
export function getToken() {
+28 -9
View File
@@ -5,6 +5,7 @@ import {
Card,
Checkbox,
Col,
Divider,
Form,
Row,
Select,
@@ -33,6 +34,8 @@ type AccountPermRes = {
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 PermissionChecklist({
value,
onChange,
@@ -49,13 +52,24 @@ function PermissionChecklist({
disabled={disabled}
onChange={(checked) => onChange(checked as string[])}
>
<Row gutter={[8, 8]}>
{HQ_PERMISSION_CATALOG.map((item) => (
<Col key={item.key} span={8}>
<Checkbox value={item.key}>{item.label}</Checkbox>
</Col>
))}
</Row>
{CATALOG_GROUPS.map((group) => {
const items = HQ_PERMISSION_CATALOG.filter((p) => (p.group ?? '其他') === group);
return (
<div key={group} style={{ marginBottom: 16 }}>
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
{group}
</Typography.Text>
<Row gutter={[8, 8]}>
{items.map((item) => (
<Col key={item.key} span={8}>
<Checkbox value={item.key}>{item.label}</Checkbox>
</Col>
))}
</Row>
<Divider style={{ margin: '12px 0 0' }} />
</div>
);
})}
</Checkbox.Group>
);
}
@@ -159,6 +173,7 @@ export default function HqPermissionsPage() {
<Typography.Title level={4}></Typography.Title>
<Typography.Paragraph type="secondary">
=
//
</Typography.Paragraph>
<Tabs
@@ -231,7 +246,7 @@ export default function HqPermissionsPage() {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
return (
<Tag key={key} color="blue">
{item?.label || key}
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
</Tag>
);
})}
@@ -244,7 +259,11 @@ export default function HqPermissionsPage() {
<span></span>
{previewEffectiveKeys.map((key) => {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === (key as HqPermissionKey));
return <Tag key={key}>{item?.label || key}</Tag>;
return (
<Tag key={key}>
{item?.group ? `${item.group}·${item.label}` : item?.label || key}
</Tag>
);
})}
</Space>
<div style={{ marginTop: 16 }}>
@@ -17,7 +17,7 @@ import {
message,
} from 'antd';
import type { MockSmsCodeItem, SystemConfigFieldMeta, SystemConfigFormResponse } from '@dukang/shared-types';
import { request } from '../lib/api';
import { request, type HqProfile } from '../lib/api';
import { ConfigImageField, ConfigImageListField } from '../components/ConfigMediaFields';
const { TextArea } = Input;
@@ -161,6 +161,7 @@ export default function SystemSettingsPage() {
const navigate = useNavigate();
const [form] = Form.useForm<Record<string, string>>();
const [meta, setMeta] = useState<SystemConfigFormResponse | null>(null);
const [profile, setProfile] = useState<HqProfile | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [syncing, setSyncing] = useState(false);
@@ -195,6 +196,7 @@ export default function SystemSettingsPage() {
useEffect(() => {
void load();
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
}, []);
useEffect(() => {
@@ -335,10 +337,14 @@ export default function SystemSettingsPage() {
</Typography.Paragraph>
</div>
<Space>
<Button onClick={() => void onImportEnv()}></Button>
<Button loading={syncing} onClick={() => void onSyncEnv()}>
env
</Button>
{profile?.adminRole === 'SUPER_ADMIN' ? (
<>
<Button onClick={() => void onImportEnv()}></Button>
<Button loading={syncing} onClick={() => void onSyncEnv()}>
env
</Button>
</>
) : null}
</Space>
</div>
@@ -363,7 +369,7 @@ export default function SystemSettingsPage() {
<Card loading={loading}>
<Form form={form} layout="vertical" onValuesChange={() => setDirty(true)}>
<Collapse defaultActiveKey={meta?.groups.map((g) => g.key)} items={collapseItems} />
<Collapse defaultActiveKey={[]} items={collapseItems} />
</Form>
{meta?.updatedAt ? (
<Typography.Text type="secondary" style={{ display: 'block', marginTop: 16 }}>