管理后台左侧列表调整

This commit is contained in:
2026-07-07 18:49:47 +08:00
parent 98e9652865
commit 1ac72e9a69
14 changed files with 780 additions and 40 deletions
+2
View File
@@ -30,6 +30,7 @@ import ThirdPartyLogsPage from './pages/ThirdPartyLogsPage';
import StoreLogsPage from './pages/StoreLogsPage';
import PartnerLogsPage from './pages/PartnerLogsPage';
import WechatBindingsPage from './pages/WechatBindingsPage';
import HqPermissionsPage from './pages/HqPermissionsPage';
function RequireAuth({ children }: { children: React.ReactNode }) {
if (!getToken()) return <Navigate to="/login" replace />;
@@ -75,6 +76,7 @@ export default function App() {
<Route path="/logs/third-party" element={<ThirdPartyLogsPage />} />
<Route path="/deliveries" element={<DeliveriesPage />} />
<Route path="/deliveries/xiaofeixia" element={<XiaofeixiaTestPage />} />
<Route path="/hq-permissions" element={<HqPermissionsPage />} />
<Route path="/hq-accounts" element={<HqAccountsPage />} />
</Route>
<Route path="*" element={<Navigate to="/" replace />} />
+13 -11
View File
@@ -14,6 +14,7 @@ import {
LogoutOutlined,
CloudUploadOutlined,
FileTextOutlined,
LockOutlined,
} from '@ant-design/icons';
import { clearAuth, request, type HqProfile } from '../lib/api';
import { bindAdminEllipsisTitle } from '../lib/ellipsis-title';
@@ -34,7 +35,6 @@ const MENU_ITEMS: MenuProps['items'] = [
],
},
{ key: '/orders', icon: <ShoppingOutlined />, label: '订单' },
{ key: '/resources', icon: <CloudUploadOutlined />, label: 'OSS 资源库' },
{
key: 'stores-group',
icon: <ShopOutlined />,
@@ -54,6 +54,7 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/partners', label: '开城合伙人' },
{ key: '/cities', label: '开城城市' },
{ key: '/partner-accounts', label: '开城合伙人账户' },
{ key: '/partner-bills', label: '合伙人结算' },
],
},
{
@@ -67,8 +68,17 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/redeem/debug', label: '核销调试' },
],
},
{ key: '/partner-bills', icon: <TeamOutlined />, label: '合伙人结算' },
{
key: 'deliveries-group',
icon: <CarOutlined />,
label: '配送单',
children: [
{ key: '/deliveries', label: '配送单列表' },
{ key: '/deliveries/xiaofeixia', label: '小飞侠联调' },
],
},
{ key: '/tickets', icon: <CarOutlined />, label: '工单中心' },
{ key: '/resources', icon: <CloudUploadOutlined />, label: 'OSS 资源库' },
{
key: 'logs-group',
icon: <FileTextOutlined />,
@@ -81,15 +91,7 @@ const MENU_ITEMS: MenuProps['items'] = [
{ key: '/logs/third-party', label: '第三方日志' },
],
},
{
key: 'deliveries-group',
icon: <CarOutlined />,
label: '配送单',
children: [
{ key: '/deliveries', label: '配送单列表' },
{ key: '/deliveries/xiaofeixia', label: '小飞侠联调' },
],
},
{ key: '/hq-permissions', icon: <LockOutlined />, label: '权限分配' },
{ key: '/hq-accounts', icon: <SafetyOutlined />, label: 'HQ账户' },
];
+2 -1
View File
@@ -6,7 +6,8 @@ export const HQ_OPERATION_ACTION_OPTIONS = [
{ value: 'PARTNER_ACCOUNT_CREATE', label: '新增合伙人账户' },
{ value: 'PARTNER_ACCOUNT_UPDATE', label: '编辑合伙人账户' },
{ value: 'HQ_ACCOUNT_CREATE', label: '新增 HQ 管理员' },
{ value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员/权限' },
{ value: 'HQ_ACCOUNT_UPDATE', label: '编辑 HQ 管理员' },
{ value: 'HQ_PERMISSION_UPDATE', label: '配置 HQ 权限' },
{ value: 'USER_DELETE', label: '删除用户' },
{ value: 'USER_BATCH_DELETE', label: '批量删除用户' },
{ value: 'ORDER_SHIP', label: '订单发货' },
+85 -16
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import {
Button, Drawer, Form, Input, Modal, Select, Space, Table, Tag, Typography, message,
Button, Drawer, Form, Input, Modal, Radio, Select, Space, Table, Tag, Typography, message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { request, type HqProfile } from '../lib/api';
@@ -8,7 +8,15 @@ import { ACCOUNT_STATUS_LABELS, fmtTime } from '../lib/constants';
import { useAdminList } from '../lib/useAdminList';
type Row = {
id: string; phone: string; name: string; adminRole: string; status: string; lastLoginAt: string | null; createdAt: string;
id: string;
phone: string;
loginName: string | null;
hasPassword: boolean;
name: string;
adminRole: string;
status: string;
lastLoginAt: string | null;
createdAt: string;
};
const ROLE_LABELS: Record<string, string> = {
@@ -38,6 +46,7 @@ export default function HqAccountsPage() {
const [detail, setDetail] = useState<Row | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const credentialType = Form.useWatch('credentialType', createForm) ?? 'phone';
const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN';
useEffect(() => {
@@ -46,7 +55,18 @@ export default function HqAccountsPage() {
const columns: ColumnsType<Row> = [
{ title: '姓名', dataIndex: 'name' },
{ title: '用户名', dataIndex: 'loginName', width: 120, render: (v) => v || '—' },
{ title: '手机', dataIndex: 'phone', width: 130 },
{
title: '登录方式',
width: 110,
render: (_, r) => (
<Space size={4}>
{r.hasPassword ? <Tag color="blue"></Tag> : null}
<Tag></Tag>
</Space>
),
},
{ title: '角色', dataIndex: 'adminRole', width: 110, render: (r) => ROLE_LABELS[r] || r },
{ title: '状态', dataIndex: 'status', width: 90, render: (s) => <Tag>{ACCOUNT_STATUS_LABELS[s] || s}</Tag> },
{ title: '最后登录', dataIndex: 'lastLoginAt', width: 160, render: fmtTime },
@@ -55,7 +75,12 @@ export default function HqAccountsPage() {
render: (_, row) => (
<Button type="link" size="small" disabled={!isSuperAdmin} onClick={() => {
setDetail(row);
editForm.setFieldsValue(row);
editForm.setFieldsValue({
name: row.name,
loginName: row.loginName,
adminRole: row.adminRole,
status: row.status,
});
setDrawerOpen(true);
}}></Button>
),
@@ -66,7 +91,18 @@ export default function HqAccountsPage() {
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}>HQ </Typography.Title>
{isSuperAdmin && <Button type="primary" onClick={() => setCreateOpen(true)}></Button>}
{isSuperAdmin && (
<Button
type="primary"
onClick={() => {
createForm.resetFields();
createForm.setFieldsValue({ credentialType: 'phone', adminRole: 'OPS' });
setCreateOpen(true);
}}
>
</Button>
)}
</Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
<Form.Item name="phone" label="手机"><Input allowClear /></Form.Item>
@@ -82,7 +118,9 @@ export default function HqAccountsPage() {
<Button type="primary" onClick={async () => {
if (!detail) return;
const v = await editForm.validateFields();
await request(`/admin/hq-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
const payload = { ...v };
if (!payload.password) delete payload.password;
await request(`/admin/hq-accounts/${detail.id}`, { method: 'PUT', body: JSON.stringify(payload) });
message.success('已保存');
setDrawerOpen(false);
void reload();
@@ -90,6 +128,10 @@ export default function HqAccountsPage() {
}>
<Form form={editForm} layout="vertical">
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="loginName" label="用户名"><Input placeholder="用于密码登录" /></Form.Item>
<Form.Item name="password" label="新密码" extra="留空则不修改">
<Input.Password placeholder="至少 6 位" />
</Form.Item>
<Form.Item name="adminRole" label="角色">
<Select options={Object.entries(ROLE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
@@ -98,20 +140,47 @@ export default function HqAccountsPage() {
</Form.Item>
</Form>
</Drawer>
<Modal title="新建 HQ 账户" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={async () => {
const v = await createForm.validateFields();
await request('/admin/hq-accounts', { method: 'POST', body: JSON.stringify(v) });
message.success('已创建');
setCreateOpen(false);
createForm.resetFields();
void reload();
}}>
<Form form={createForm} layout="vertical">
<Form.Item name="phone" label="手机" rules={[{ required: true }]}><Input /></Form.Item>
<Modal
title="新建 HQ 账户"
open={createOpen}
onCancel={() => setCreateOpen(false)}
onOk={async () => {
const v = await createForm.validateFields();
await request('/admin/hq-accounts', { method: 'POST', body: JSON.stringify(v) });
message.success('已创建');
setCreateOpen(false);
createForm.resetFields();
void reload();
}}
>
<Form form={createForm} layout="vertical" initialValues={{ credentialType: 'phone', adminRole: 'OPS' }}>
<Form.Item name="credentialType" label="创建方式">
<Radio.Group>
<Radio value="phone"></Radio>
<Radio value="password"></Radio>
</Radio.Group>
</Form.Item>
<Form.Item name="name" label="姓名" rules={[{ required: true }]}><Input /></Form.Item>
<Form.Item name="adminRole" label="角色" initialValue="OPS">
<Form.Item name="adminRole" label="角色">
<Select options={Object.entries(ROLE_LABELS).map(([value, label]) => ({ value, label }))} />
</Form.Item>
{credentialType === 'phone' ? (
<Form.Item name="phone" label="手机号" rules={[{ required: true, message: '请输入手机号' }]}>
<Input maxLength={11} />
</Form.Item>
) : (
<>
<Form.Item name="loginName" label="用户名" rules={[{ required: true, message: '请输入用户名' }]}>
<Input autoComplete="off" />
</Form.Item>
<Form.Item name="password" label="密码" rules={[{ required: true, message: '请输入密码' }, { min: 6, message: '至少 6 位' }]}>
<Input.Password autoComplete="new-password" />
</Form.Item>
<Form.Item name="phone" label="手机号(可选)" extra="不填则自动生成占位手机号,用于满足账号唯一约束">
<Input maxLength={11} />
</Form.Item>
</>
)}
</Form>
</Modal>
</div>
@@ -0,0 +1,264 @@
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]));
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[])}
>
<Row gutter={[8, 8]}>
{HQ_PERMISSION_CATALOG.map((item) => (
<Col key={item.key} span={8}>
<Checkbox value={item.key}>{item.label}</Checkbox>
</Col>
))}
</Row>
</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 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
.filter((a) => a.adminRole !== 'SUPER_ADMIN')
.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 账户" />
) : (
<>
<Space wrap style={{ marginBottom: 12 }}>
<span></span>
{roleInheritedKeys.map((key) => {
const item = HQ_PERMISSION_CATALOG.find((p) => p.key === key);
return (
<Tag key={key} color="blue">
{item?.label || key}
</Tag>
);
})}
</Space>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<PermissionChecklist value={accountKeys} onChange={setAccountKeys} />
<Space wrap style={{ marginTop: 12 }}>
<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>;
})}
</Space>
<div style={{ marginTop: 16 }}>
<Button type="primary" loading={accountSaving} onClick={() => void saveAccountPermissions()}>
</Button>
</div>
</>
)}
</Card>
),
},
]}
/>
</div>
);
}