feat: 实现系统管理用户、角色与权限配置页面

补齐 RBAC 相关页面与 UI 设计图,并接入功能与视觉回归测试,便于后续持续验证。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-26 03:08:35 +08:00
parent c4872891f8
commit 77f718965d
24 changed files with 1869 additions and 14 deletions

View File

@@ -19,10 +19,10 @@ docs/ui/
│ ├── usage/ # 用量统计
│ ├── usage-detail/ # 用量明细
│ └── quota/ # 限流与配额
└── system/ # 系统管理(待补图)
├── user/
├── role/
└── permission/
└── system/ # 系统管理
├── user/ # 用户管理(列表 / 表单 / 重置密码)
├── role/ # 角色管理(含权限配置抽屉)
└── permission/ # 权限配置(菜单 / 按钮)
```
---
@@ -43,10 +43,12 @@ docs/ui/
| ├ 用量统计 | `/llm/usage` | [llm/usage/chatone-llm-usage-ui.png](./llm/usage/chatone-llm-usage-ui.png) |
| ├ 用量明细 | `/llm/usage/details` | [llm/usage-detail/chatone-llm-usage-detail-ui.png](./llm/usage-detail/chatone-llm-usage-detail-ui.png) |
| └ 限流配额 | `/llm/quota` | [llm/quota/chatone-llm-quota-ui.png](./llm/quota/chatone-llm-quota-ui.png) |
| **系统管理** | `/system` | 待补 |
| ├ 用户管理 | `/system/user` | `system/user/` |
| ├ 角色管理 | `/system/role` | `system/role/` |
| └ 权限配置 | `/system/permission` | `system/permission/` |
| **系统管理** | `/system` | |
| ├ 用户管理 | `/system/user` | [system/user/chatone-system-user-list-ui.png](./system/user/chatone-system-user-list-ui.png) |
| │ ├ 新增/编辑 | Modal | [system/user/chatone-system-user-form-ui.png](./system/user/chatone-system-user-form-ui.png) |
| │ └ 重置密码 | Modal | [system/user/chatone-system-user-reset-pwd-ui.png](./system/user/chatone-system-user-reset-pwd-ui.png) |
| ├ 角色管理 | `/system/role` | [system/role/chatone-system-role-ui.png](./system/role/chatone-system-role-ui.png) |
| └ 权限配置 | `/system/permission` | [system/permission/chatone-system-permission-ui.png](./system/permission/chatone-system-permission-ui.png) |
---
@@ -92,6 +94,33 @@ docs/ui/
---
## 系统管理:功能清单
### 用户管理 `/system/user`
- 新增 / 编辑 / 删除用户(含批量禁用)
- 启用 / 禁用账号Switch
- 筛选:关键词(用户名 / 邮箱 / 手机)、角色、状态
- 绑定角色(多选)
- 重置密码(临时密码 + 强制下次修改)
- 展示最近登录时间、创建时间
### 角色管理 `/system/role`
- 新增 / 编辑 / 删除角色
- 角色编码(如 `ROLE_ADMIN`)、描述、启停
- 查看关联用户数
- 抽屉配置:菜单树权限 + 按钮权限码
### 权限配置 `/system/permission`
- **菜单权限**:树形维护目录 / 菜单 / 按钮;路由、图标、排序、显示开关、权限标识
- **按钮权限**:权限名称、权限码(如 `user:create`)、所属菜单;供前端 `<Access />` 使用
权限模型RBAC用户 → 角色 → 菜单/按钮权限)。
---
## 建议的菜单 Mock 结构
```ts
@@ -106,5 +135,15 @@ docs/ui/
{ name: '用量明细', path: '/llm/usage/details' },
{ name: '限流配额', path: '/llm/quota' },
],
},
{
name: '系统管理',
path: '/system',
icon: 'SettingOutlined',
children: [
{ name: '用户管理', path: '/system/user' },
{ name: '角色管理', path: '/system/role' },
{ name: '权限配置', path: '/system/permission' },
],
}
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -17,8 +17,8 @@ pnpm test:e2e:update # 确认 UI 改动后更新截图基线
- `visual/`:页面截图回归。
- `snapshots/`:由 `test:e2e:update` 生成的基线图。
模型列表、供应商管理的功能和视觉用例已启用。确认页面视觉改动符合预期后,执行
模型列表、供应商管理、用户管理、角色管理、权限配置的功能和视觉用例已启用。确认页面视觉改动符合预期后,执行
`pnpm test:e2e:update` 更新基线。
设计目标图位于 `docs/ui/llm/list/`;截图基线用于检测后续代码造成的
设计目标图位于 `docs/ui/`;截图基线用于检测后续代码造成的
视觉变化,不替代设计稿还原度审查。

View File

@@ -0,0 +1,45 @@
import { expect, test } from '../fixtures/auth';
test.describe('系统管理 / 权限配置', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/system/permission');
await page.getByText('菜单树', { exact: true }).waitFor();
});
test('展示菜单权限与详情', async ({ page }) => {
await expect(page.getByRole('tab', { name: '菜单权限' })).toBeVisible();
await expect(page.getByText('菜单树', { exact: true })).toBeVisible();
await expect(page.getByText('菜单详情', { exact: true })).toBeVisible();
await expect(page.getByLabel('菜单名称')).toHaveValue('用户管理');
await expect(page.getByLabel('权限标识')).toHaveValue('system:user:view');
});
test('可保存菜单详情', async ({ page }) => {
await page.getByLabel('菜单名称').fill('用户中心');
await page.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('菜单已保存')).toBeVisible();
await expect(page.getByRole('tree').getByText('用户中心')).toBeVisible();
});
test('可新增同级菜单', async ({ page }) => {
await page.getByRole('button', { name: '+ 新增同级' }).click();
await expect(page.getByText('已新增同级菜单')).toBeVisible();
await expect(page.getByLabel('菜单名称')).toHaveValue('新菜单');
});
test('可管理按钮权限', async ({ page }) => {
await page.getByRole('tab', { name: '按钮权限' }).click();
const buttonPanel = page.getByRole('tabpanel', { name: '按钮权限' });
await expect(buttonPanel.getByRole('button', { name: '新增按钮权限' })).toBeVisible();
await expect(buttonPanel.getByText('system:user:add')).toBeVisible();
await buttonPanel.getByRole('button', { name: '新增按钮权限' }).click();
const dialog = page.getByRole('dialog', { name: '新增按钮权限' });
await dialog.getByLabel('权限名称').fill('导入');
await dialog.getByLabel('权限码').fill('system:user:import');
await dialog.getByRole('button', { name: '保 存' }).click();
await expect(buttonPanel.getByText('system:user:import')).toBeVisible();
await expect(page.getByText('按钮权限已添加')).toBeVisible();
});
});

View File

@@ -0,0 +1,60 @@
import { expect, test } from '../fixtures/auth';
test.describe('系统管理 / 角色管理', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/system/role');
await page.getByRole('table').waitFor();
});
test('展示角色列表并支持筛选', async ({ page }) => {
await expect(page.getByRole('button', { name: '新增角色' })).toBeVisible();
await expect(page.getByText('超级管理员', { exact: true })).toBeVisible();
await expect(page.getByText('ROLE_ADMIN')).toBeVisible();
await page.getByPlaceholder('搜索角色名称').fill('只读');
await expect(page.getByText('只读', { exact: true })).toBeVisible();
await expect(page.getByText('超级管理员', { exact: true })).toHaveCount(0);
});
test('可新增角色', async ({ page }) => {
await page.getByRole('button', { name: '新增角色' }).click();
const dialog = page.getByRole('dialog', { name: '新增角色' });
await dialog.getByLabel('角色名称').fill('审计');
await dialog.getByLabel('角色编码').fill('ROLE_AUDIT');
await dialog.getByLabel('描述').fill('审计只读权限');
await dialog.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('审计', { exact: true })).toBeVisible();
await expect(page.getByText('角色已添加')).toBeVisible();
});
test('可切换状态并配置权限', async ({ page }) => {
const statusSwitch = page.getByRole('switch', { name: '运营状态' });
await expect(statusSwitch).toBeChecked();
await statusSwitch.click();
await expect(statusSwitch).not.toBeChecked();
const row = page.getByRole('row').filter({ hasText: '运营' });
await row.getByRole('button', { name: '权限配置' }).click();
await expect(page.getByText('配置权限 - 运营')).toBeVisible();
await expect(page.getByText('菜单权限')).toBeVisible();
await expect(page.getByText('按钮权限')).toBeVisible();
await page.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('权限已保存')).toBeVisible();
});
test('可编辑并删除角色', async ({ page }) => {
const row = page.getByRole('row').filter({ hasText: '只读' });
await row.getByRole('button', { name: '编辑' }).click();
const dialog = page.getByRole('dialog', { name: '编辑角色' });
await dialog.getByLabel('描述').fill('只读访问,不可修改');
await dialog.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('只读访问,不可修改')).toBeVisible();
await row.getByRole('button', { name: '删除' }).click();
await page.getByRole('button', { name: '确 定' }).click();
await expect(page.getByText('ROLE_VIEWER')).toHaveCount(0);
});
});

View File

@@ -0,0 +1,72 @@
import { expect, test } from '../fixtures/auth';
test.describe('系统管理 / 用户管理', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/system/user');
await page.getByRole('table').waitFor();
});
test('展示用户列表并支持筛选', async ({ page }) => {
await expect(page.getByRole('button', { name: '新增用户' })).toBeVisible();
await expect(page.getByText('admin', { exact: true })).toBeVisible();
await page.getByPlaceholder('搜索用户名 / 邮箱 / 手机号').fill('viewer');
await expect(page.getByText('viewer', { exact: true })).toBeVisible();
await expect(page.getByText('admin', { exact: true })).toHaveCount(0);
});
test('可新增用户', async ({ page }) => {
await page.getByRole('button', { name: '新增用户' }).click();
const dialog = page.getByRole('dialog', { name: '新增用户' });
await dialog.getByLabel('用户名').fill('tester');
await dialog.getByLabel('昵称').fill('测试用户');
await dialog.getByLabel('邮箱').fill('tester@chatone.ai');
await dialog.getByLabel('手机号').fill('13800000009');
await dialog.getByLabel('初始密码').fill('Test123456');
await dialog.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('tester', { exact: true })).toBeVisible();
await expect(page.getByText('用户已添加')).toBeVisible();
});
test('可切换状态并重置密码', async ({ page }) => {
const statusSwitch = page.getByRole('switch', { name: 'operator状态' });
await expect(statusSwitch).toBeChecked();
await statusSwitch.click();
await expect(statusSwitch).not.toBeChecked();
const row = page.getByRole('row').filter({ hasText: 'operator' });
await row.getByRole('button', { name: '重置密码' }).click();
const dialog = page.getByRole('dialog', { name: '重置密码 - operator' });
await dialog.getByLabel('新密码').fill('NewPass123');
await dialog.getByLabel('确认密码').fill('NewPass123');
await dialog.getByRole('button', { name: '确认重置' }).click();
await expect(page.getByText('密码已重置')).toBeVisible();
});
test('可编辑并删除用户', async ({ page }) => {
const operatorRow = page.getByRole('row').filter({ hasText: 'operator' });
await operatorRow.getByRole('button', { name: '编辑' }).click();
const dialog = page.getByRole('dialog', { name: '编辑用户' });
await dialog.getByLabel('昵称').fill('新运营');
await dialog.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('新运营', { exact: true })).toBeVisible();
const viewerRow = page.getByRole('row').filter({ hasText: 'viewer' });
await viewerRow.getByRole('button', { name: '删除' }).click();
await page.getByRole('button', { name: '确 定' }).click();
await expect(page.getByText('viewer', { exact: true })).toHaveCount(0);
});
test('可批量禁用用户', async ({ page }) => {
const row = page.getByRole('row').filter({ hasText: 'operator' });
await row.getByRole('checkbox').check();
await page.getByRole('button', { name: '批量禁用' }).click();
await page.getByRole('button', { name: '确 定' }).click();
await expect(page.getByRole('switch', { name: 'operator状态' })).not.toBeChecked();
await expect(page.getByText('已禁用 1 个用户')).toBeVisible();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

View File

@@ -0,0 +1,12 @@
import { expect, test } from '../fixtures/auth';
test('权限配置视觉基线', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/system/permission');
await page.getByText('菜单树', { exact: true }).waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('system-permission.png', {
fullPage: true,
});
});

View File

@@ -0,0 +1,12 @@
import { expect, test } from '../fixtures/auth';
test('角色管理视觉基线', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/system/role');
await page.getByRole('table').waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('system-roles-list.png', {
fullPage: true,
});
});

View File

@@ -0,0 +1,12 @@
import { expect, test } from '../fixtures/auth';
test('用户管理视觉基线', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/system/user');
await page.getByRole('table').waitFor();
await page.evaluate(() => document.fonts.ready);
await expect(page).toHaveScreenshot('system-users-list.png', {
fullPage: true,
});
});

View File

@@ -71,6 +71,7 @@ const mockMenus: MenuItem[] = [
id: '3-3',
name: '权限配置',
path: '/system/permission',
icon: 'SafetyCertificateOutlined',
},
],
},

View File

@@ -0,0 +1,491 @@
import { PlusOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import {
App,
Button,
Col,
Form,
Input,
InputNumber,
Modal,
Popconfirm,
Radio,
Row,
Select,
Space,
Switch,
Table,
Tabs,
Tree,
Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { DataNode } from 'antd/es/tree';
import { useEffect, useMemo, useState } from 'react';
import {
iconOptions,
initialButtonPermissions,
initialMenuTree,
type ButtonPermissionItem,
type MenuNodeType,
type MenuPermissionNode,
} from './mock';
interface MenuFormValues {
title: string;
path: string;
icon: string;
order: number;
type: MenuNodeType;
visible: boolean;
permission: string;
remark?: string;
}
interface ButtonFormValues {
name: string;
code: string;
menuKey: string;
}
const findNode = (nodes: MenuPermissionNode[], key: string): MenuPermissionNode | undefined => {
for (const node of nodes) {
if (node.key === key) return node;
if (node.children) {
const found = findNode(node.children, key);
if (found) return found;
}
}
return undefined;
};
const findParent = (
nodes: MenuPermissionNode[],
key: string,
parent?: MenuPermissionNode,
): MenuPermissionNode | undefined => {
for (const node of nodes) {
if (node.key === key) return parent;
if (node.children?.length) {
const found = findParent(node.children, key, node);
if (found !== undefined || node.children.some(child => child.key === key)) {
return found ?? node;
}
}
}
return undefined;
};
const updateNode = (
nodes: MenuPermissionNode[],
key: string,
updater: (node: MenuPermissionNode) => MenuPermissionNode,
): MenuPermissionNode[] =>
nodes.map(node => {
if (node.key === key) return updater(node);
if (!node.children) return node;
return { ...node, children: updateNode(node.children, key, updater) };
});
const removeNode = (nodes: MenuPermissionNode[], key: string): MenuPermissionNode[] =>
nodes
.filter(node => node.key !== key)
.map(node => (node.children ? { ...node, children: removeNode(node.children, key) } : node));
const toTreeData = (nodes: MenuPermissionNode[]): DataNode[] =>
nodes.map(node => ({
key: node.key,
title: node.title,
children: node.children ? toTreeData(node.children) : undefined,
}));
const flattenMenus = (nodes: MenuPermissionNode[], parentTitle = ''): { key: string; label: string }[] => {
const result: { key: string; label: string }[] = [];
nodes.forEach(node => {
const label = parentTitle ? `${parentTitle} / ${node.title}` : node.title;
if (node.type === '菜单' || node.type === '目录') {
result.push({ key: node.key, label });
}
if (node.children) result.push(...flattenMenus(node.children, label));
});
return result;
};
export default function PermissionManagement() {
const { message } = App.useApp();
const [menuForm] = Form.useForm<MenuFormValues>();
const [buttonForm] = Form.useForm<ButtonFormValues>();
const [menuTree, setMenuTree] = useState(initialMenuTree);
const [buttons, setButtons] = useState(initialButtonPermissions);
const [selectedKey, setSelectedKey] = useState('3-1');
const [buttonOpen, setButtonOpen] = useState(false);
const [editingButton, setEditingButton] = useState<ButtonPermissionItem>();
const [activeTab, setActiveTab] = useState('menu');
const selectedNode = useMemo(() => findNode(menuTree, selectedKey), [menuTree, selectedKey]);
const menuOptions = useMemo(() => flattenMenus(menuTree), [menuTree]);
const relatedButtons = useMemo(() => buttons.filter(item => item.menuKey === selectedKey), [buttons, selectedKey]);
const syncForm = (node?: MenuPermissionNode) => {
if (!node) return;
menuForm.setFieldsValue({
title: node.title,
path: node.path,
icon: node.icon,
order: node.order,
type: node.type,
visible: node.visible,
permission: node.permission,
remark: node.remark ?? '',
});
};
useEffect(() => {
syncForm(selectedNode);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedNode]);
const selectMenu = (key: string) => {
setSelectedKey(key);
};
const addSibling = () => {
if (!selectedNode) {
message.warning('请先选择菜单');
return;
}
const parent = findParent(menuTree, selectedKey);
const newNode: MenuPermissionNode = {
key: `menu-${Date.now()}`,
title: '新菜单',
path: '/new',
icon: 'AppstoreOutlined',
order: 99,
type: '菜单',
visible: true,
permission: 'new:view',
};
if (!parent) {
setMenuTree(current => [...current, newNode]);
} else {
setMenuTree(current =>
updateNode(current, parent.key, node => ({
...node,
children: [...(node.children ?? []), newNode],
})),
);
}
setSelectedKey(newNode.key);
syncForm(newNode);
message.success('已新增同级菜单');
};
const addChild = () => {
if (!selectedNode) {
message.warning('请先选择菜单');
return;
}
const newNode: MenuPermissionNode = {
key: `menu-${Date.now()}`,
title: '新子菜单',
path: `${selectedNode.path}/child`,
icon: 'AppstoreOutlined',
order: (selectedNode.children?.length ?? 0) + 1,
type: '菜单',
visible: true,
permission: `${selectedNode.permission}:child`,
};
setMenuTree(current =>
updateNode(current, selectedKey, node => ({
...node,
type: node.type === '菜单' ? '目录' : node.type,
children: [...(node.children ?? []), newNode],
})),
);
setSelectedKey(newNode.key);
syncForm(newNode);
message.success('已新增子级菜单');
};
const deleteMenu = () => {
if (!selectedNode) return;
const nextTree = removeNode(menuTree, selectedKey);
setMenuTree(nextTree);
const fallback = nextTree[0]?.key ?? nextTree[0]?.children?.[0]?.key;
if (fallback) {
setSelectedKey(fallback);
syncForm(findNode(nextTree, fallback));
} else {
setSelectedKey('');
menuForm.resetFields();
}
message.success('菜单已删除');
};
const saveMenu = async () => {
if (!selectedKey) return;
const values = await menuForm.validateFields();
setMenuTree(current =>
updateNode(current, selectedKey, node => ({
...node,
...values,
})),
);
message.success('菜单已保存');
};
const openCreateButton = () => {
setEditingButton(undefined);
buttonForm.setFieldsValue({
name: '',
code: '',
menuKey: selectedKey || menuOptions[0]?.key,
});
setButtonOpen(true);
};
const openEditButton = (record: ButtonPermissionItem) => {
setEditingButton(record);
buttonForm.setFieldsValue({
name: record.name,
code: record.code,
menuKey: record.menuKey,
});
setButtonOpen(true);
};
const saveButton = async () => {
const values = await buttonForm.validateFields();
const menuLabel = menuOptions.find(item => item.key === values.menuKey)?.label ?? '';
if (editingButton) {
setButtons(current =>
current.map(item =>
item.id === editingButton.id
? { ...item, name: values.name, code: values.code, menuKey: values.menuKey, menuPath: menuLabel }
: item,
),
);
message.success('按钮权限已更新');
} else {
setButtons(current => [
...current,
{
id: `btn-${Date.now()}`,
name: values.name,
code: values.code,
menuKey: values.menuKey,
menuPath: menuLabel,
},
]);
message.success('按钮权限已添加');
}
setButtonOpen(false);
buttonForm.resetFields();
};
const deleteButton = (record: ButtonPermissionItem) => {
setButtons(current => current.filter(item => item.id !== record.id));
message.success('按钮权限已删除');
};
const buttonColumns: ColumnsType<ButtonPermissionItem> = [
{ title: '权限名称', dataIndex: 'name', width: 120 },
{
title: '权限码',
dataIndex: 'code',
width: 220,
render: (value: string) => <Typography.Text code>{value}</Typography.Text>,
},
{ title: '所属菜单', dataIndex: 'menuPath', ellipsis: true },
{
title: '操作',
key: 'actions',
width: 140,
render: (_, record) => (
<Space size={2}>
<Button type="link" size="small" onClick={() => openEditButton(record)}>
</Button>
<Popconfirm title={`确定删除权限 ${record.code}`} onConfirm={() => deleteButton(record)}>
<Button type="link" danger size="small">
</Button>
</Popconfirm>
</Space>
),
},
];
const menuTab = (
<Row gutter={16}>
<Col xs={24} lg={8}>
<ProCard
title="菜单树"
size="small"
variant="outlined"
extra={
<Space size={4}>
<Button size="small" type="link" onClick={addSibling}>
+
</Button>
<Button size="small" type="link" onClick={addChild}>
+
</Button>
<Popconfirm title="确定删除选中菜单?" disabled={!selectedKey} onConfirm={deleteMenu}>
<Button size="small" type="link" danger disabled={!selectedKey}>
</Button>
</Popconfirm>
</Space>
}
>
<Tree
defaultExpandAll
selectedKeys={selectedKey ? [selectedKey] : []}
treeData={toTreeData(menuTree)}
onSelect={keys => {
if (keys[0]) selectMenu(String(keys[0]));
}}
/>
</ProCard>
</Col>
<Col xs={24} lg={16}>
<ProCard title="菜单详情" size="small" variant="outlined">
<Form form={menuForm} layout="vertical" requiredMark="optional" disabled={!selectedKey}>
<Row gutter={16}>
<Col span={12}>
<Form.Item label="菜单名称" name="title" rules={[{ required: true, message: '请输入菜单名称' }]}>
<Input placeholder="例如 用户管理" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="路由路径" name="path" rules={[{ required: true, message: '请输入路由路径' }]}>
<Input placeholder="/system/user" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="图标" name="icon">
<Select options={iconOptions.map(value => ({ label: value, value }))} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="排序" name="order" rules={[{ required: true }]}>
<InputNumber min={1} style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="类型" name="type" rules={[{ required: true }]}>
<Radio.Group
options={[
{ label: '目录', value: '目录' },
{ label: '菜单', value: '菜单' },
{ label: '按钮', value: '按钮' },
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="是否显示" name="visible" valuePropName="checked">
<Switch checkedChildren="显示" unCheckedChildren="隐藏" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="权限标识" name="permission" rules={[{ required: true, message: '请输入权限标识' }]}>
<Input placeholder="system:user:view" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="备注" name="remark">
<Input.TextArea rows={2} maxLength={200} showCount placeholder="请输入备注" />
</Form.Item>
</Col>
</Row>
<Button type="primary" onClick={saveMenu} disabled={!selectedKey}>
</Button>
</Form>
</ProCard>
<ProCard title="按钮权限预览" size="small" variant="outlined" style={{ marginTop: 16 }}>
<Typography.Paragraph type="secondary" style={{ marginTop: 0 }}>
</Typography.Paragraph>
<Table<ButtonPermissionItem>
rowKey="id"
size="small"
columns={buttonColumns}
dataSource={relatedButtons}
pagination={false}
/>
</ProCard>
</Col>
</Row>
);
const buttonTab = (
<ProCard variant="outlined">
<Space vertical size={16} style={{ width: '100%' }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }}>
<Typography.Text type="secondary"> Access 使</Typography.Text>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateButton}>
</Button>
</Space>
<Table<ButtonPermissionItem>
rowKey="id"
columns={buttonColumns}
dataSource={buttons}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: total => `${total}` }}
/>
</Space>
</ProCard>
);
return (
<>
<ProCard variant="outlined">
<Typography.Title level={5} style={{ marginTop: 0 }}>
</Typography.Title>
<Typography.Paragraph type="secondary">
RBAC
</Typography.Paragraph>
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={[
{ key: 'menu', label: '菜单权限', children: menuTab },
{ key: 'button', label: '按钮权限', children: buttonTab },
]}
/>
</ProCard>
<Modal
open={buttonOpen}
title={editingButton ? '编辑按钮权限' : '新增按钮权限'}
okText="保存"
cancelText="取消"
onOk={saveButton}
onCancel={() => {
setButtonOpen(false);
buttonForm.resetFields();
}}
>
<Form form={buttonForm} layout="vertical" requiredMark="optional">
<Form.Item label="权限名称" name="name" rules={[{ required: true, message: '请输入权限名称' }]}>
<Input placeholder="例如 新增" />
</Form.Item>
<Form.Item label="权限码" name="code" rules={[{ required: true, message: '请输入权限码' }]}>
<Input placeholder="例如 system:user:add" />
</Form.Item>
<Form.Item label="所属菜单" name="menuKey" rules={[{ required: true, message: '请选择所属菜单' }]}>
<Select options={menuOptions.map(item => ({ label: item.label, value: item.key }))} />
</Form.Item>
</Form>
</Modal>
</>
);
}

View File

@@ -0,0 +1,164 @@
export type MenuNodeType = '目录' | '菜单' | '按钮';
export interface MenuPermissionNode {
key: string;
title: string;
path: string;
icon: string;
order: number;
type: MenuNodeType;
visible: boolean;
permission: string;
remark?: string;
children?: MenuPermissionNode[];
}
export interface ButtonPermissionItem {
id: string;
name: string;
code: string;
menuPath: string;
menuKey: string;
}
export const iconOptions = [
'DashboardOutlined',
'BlockOutlined',
'AppstoreOutlined',
'CloudServerOutlined',
'LineChartOutlined',
'UnorderedListOutlined',
'ControlOutlined',
'SettingOutlined',
'UserOutlined',
'TeamOutlined',
'SafetyCertificateOutlined',
];
export const initialMenuTree: MenuPermissionNode[] = [
{
key: '1',
title: '总览',
path: '/dashboard',
icon: 'DashboardOutlined',
order: 1,
type: '菜单',
visible: true,
permission: 'dashboard:view',
},
{
key: '2',
title: '大模型管理',
path: '/llm',
icon: 'BlockOutlined',
order: 2,
type: '目录',
visible: true,
permission: 'llm:view',
children: [
{
key: '2-1',
title: '模型列表',
path: '/llm/models',
icon: 'AppstoreOutlined',
order: 1,
type: '菜单',
visible: true,
permission: 'llm:models:view',
},
{
key: '2-2',
title: '供应商管理',
path: '/llm/providers',
icon: 'CloudServerOutlined',
order: 2,
type: '菜单',
visible: true,
permission: 'llm:providers:view',
},
{
key: '2-3',
title: '用量统计',
path: '/llm/usage',
icon: 'LineChartOutlined',
order: 3,
type: '菜单',
visible: true,
permission: 'llm:usage:view',
},
{
key: '2-4',
title: '用量明细',
path: '/llm/usage/details',
icon: 'UnorderedListOutlined',
order: 4,
type: '菜单',
visible: true,
permission: 'llm:usage:details:view',
},
{
key: '2-5',
title: '限流配额',
path: '/llm/quota',
icon: 'ControlOutlined',
order: 5,
type: '菜单',
visible: true,
permission: 'llm:quota:view',
},
],
},
{
key: '3',
title: '系统管理',
path: '/system',
icon: 'SettingOutlined',
order: 3,
type: '目录',
visible: true,
permission: 'system:view',
children: [
{
key: '3-1',
title: '用户管理',
path: '/system/user',
icon: 'UserOutlined',
order: 1,
type: '菜单',
visible: true,
permission: 'system:user:view',
},
{
key: '3-2',
title: '角色管理',
path: '/system/role',
icon: 'TeamOutlined',
order: 2,
type: '菜单',
visible: true,
permission: 'system:role:view',
},
{
key: '3-3',
title: '权限配置',
path: '/system/permission',
icon: 'SafetyCertificateOutlined',
order: 3,
type: '菜单',
visible: true,
permission: 'system:permission:view',
},
],
},
];
export const initialButtonPermissions: ButtonPermissionItem[] = [
{ id: 'btn-1', name: '新增', code: 'system:user:add', menuPath: '系统管理 / 用户管理', menuKey: '3-1' },
{ id: 'btn-2', name: '编辑', code: 'system:user:edit', menuPath: '系统管理 / 用户管理', menuKey: '3-1' },
{ id: 'btn-3', name: '删除', code: 'system:user:delete', menuPath: '系统管理 / 用户管理', menuKey: '3-1' },
{ id: 'btn-4', name: '导出', code: 'system:user:export', menuPath: '系统管理 / 用户管理', menuKey: '3-1' },
{ id: 'btn-5', name: '新增', code: 'system:role:add', menuPath: '系统管理 / 角色管理', menuKey: '3-2' },
{ id: 'btn-6', name: '编辑', code: 'system:role:edit', menuPath: '系统管理 / 角色管理', menuKey: '3-2' },
{ id: 'btn-7', name: '删除', code: 'system:role:delete', menuPath: '系统管理 / 角色管理', menuKey: '3-2' },
{ id: 'btn-8', name: '新增模型', code: 'llm:models:add', menuPath: '大模型管理 / 模型列表', menuKey: '2-1' },
];

View File

@@ -0,0 +1,313 @@
import { PlusOutlined, SearchOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import {
App,
Button,
Checkbox,
Drawer,
Form,
Input,
Modal,
Popconfirm,
Select,
Space,
Switch,
Table,
Tree,
Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useMemo, useState } from 'react';
import { buttonPermissionOptions, initialRoles, menuPermissionTree, type RoleItem } from './mock';
interface RoleFormValues {
name: string;
code: string;
description: string;
enabled: boolean;
}
export default function RoleManagement() {
const { message } = App.useApp();
const [form] = Form.useForm<RoleFormValues>();
const [roles, setRoles] = useState(initialRoles);
const [keyword, setKeyword] = useState('');
const [enabled, setEnabled] = useState<boolean>();
const [formOpen, setFormOpen] = useState(false);
const [editing, setEditing] = useState<RoleItem>();
const [permissionRole, setPermissionRole] = useState<RoleItem>();
const [menuKeys, setMenuKeys] = useState<string[]>([]);
const [buttonKeys, setButtonKeys] = useState<string[]>([]);
const filteredRoles = useMemo(() => {
const normalized = keyword.trim().toLowerCase();
return roles.filter(role => {
const matchesKeyword =
!normalized || role.name.toLowerCase().includes(normalized) || role.code.toLowerCase().includes(normalized);
return matchesKeyword && (enabled === undefined || role.enabled === enabled);
});
}, [enabled, keyword, roles]);
const openCreate = () => {
setEditing(undefined);
form.setFieldsValue({ name: '', code: '', description: '', enabled: true });
setFormOpen(true);
};
const openEdit = (role: RoleItem) => {
setEditing(role);
form.setFieldsValue({
name: role.name,
code: role.code,
description: role.description,
enabled: role.enabled,
});
setFormOpen(true);
};
const closeForm = () => {
setFormOpen(false);
setEditing(undefined);
form.resetFields();
};
const saveRole = async () => {
const values = await form.validateFields();
if (editing) {
setRoles(current =>
current.map(role =>
role.id === editing.id
? {
...role,
...values,
updatedAt: '2026-07-26 02:30',
}
: role,
),
);
message.success('角色已更新');
} else {
setRoles(current => [
...current,
{
id: `role-${Date.now()}`,
name: values.name,
code: values.code,
description: values.description,
enabled: values.enabled,
userCount: 0,
updatedAt: '2026-07-26 02:30',
menuKeys: ['dashboard'],
buttonKeys: [],
},
]);
message.success('角色已添加');
}
closeForm();
};
const deleteRole = (role: RoleItem) => {
setRoles(current => current.filter(item => item.id !== role.id));
message.success('角色已删除');
};
const openPermission = (role: RoleItem) => {
setPermissionRole(role);
setMenuKeys(role.menuKeys);
setButtonKeys(role.buttonKeys);
};
const savePermission = () => {
if (!permissionRole) return;
setRoles(current =>
current.map(role =>
role.id === permissionRole.id ? { ...role, menuKeys, buttonKeys, updatedAt: '2026-07-26 02:30' } : role,
),
);
message.success('权限已保存');
setPermissionRole(undefined);
};
const columns: ColumnsType<RoleItem> = [
{ title: '角色名称', dataIndex: 'name', width: 140 },
{
title: '角色编码',
dataIndex: 'code',
width: 160,
render: (value: string) => <Typography.Text code>{value}</Typography.Text>,
},
{
title: '用户数',
dataIndex: 'userCount',
width: 90,
align: 'right',
render: (value: number) => `${value}`,
},
{
title: '状态',
dataIndex: 'enabled',
width: 100,
render: (value: boolean, record) => (
<Switch
size="small"
checked={value}
checkedChildren="启用"
unCheckedChildren="禁用"
aria-label={`${record.name}状态`}
onChange={checked => {
setRoles(current => current.map(item => (item.id === record.id ? { ...item, enabled: checked } : item)));
message.success(checked ? '角色已启用' : '角色已禁用');
}}
/>
),
},
{ title: '描述', dataIndex: 'description', ellipsis: true },
{ title: '更新时间', dataIndex: 'updatedAt', width: 160 },
{
title: '操作',
key: 'actions',
width: 220,
fixed: 'right',
render: (_, record) => (
<Space size={2}>
<Button type="link" size="small" onClick={() => openPermission(record)}>
</Button>
<Button type="link" size="small" onClick={() => openEdit(record)}>
</Button>
<Popconfirm title={`确定删除角色 ${record.name}`} onConfirm={() => deleteRole(record)}>
<Button type="link" danger size="small">
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<>
<ProCard variant="outlined">
<Space vertical size={16} style={{ width: '100%' }}>
<Space style={{ width: '100%', justifyContent: 'space-between' }} align="start">
<div>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary">访</Typography.Text>
</div>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</Space>
<Space wrap>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="搜索角色名称"
style={{ width: 240 }}
value={keyword}
onChange={event => setKeyword(event.target.value)}
/>
<Select
allowClear
placeholder="全部状态"
style={{ width: 130 }}
options={[
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
]}
value={enabled}
onChange={setEnabled}
/>
</Space>
<Table<RoleItem>
rowKey="id"
columns={columns}
dataSource={filteredRoles}
scroll={{ x: 1000 }}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: total => `${total}` }}
/>
</Space>
</ProCard>
<Modal
open={formOpen}
title={editing ? '编辑角色' : '新增角色'}
okText="保存"
cancelText="取消"
onOk={saveRole}
onCancel={closeForm}
>
<Form<RoleFormValues> form={form} layout="vertical" requiredMark="optional">
<Form.Item label="角色名称" name="name" rules={[{ required: true, message: '请输入角色名称' }]}>
<Input placeholder="例如 运营" />
</Form.Item>
<Form.Item
label="角色编码"
name="code"
rules={[
{ required: true, message: '请输入角色编码' },
{ pattern: /^ROLE_[A-Z0-9_]+$/, message: '格式如 ROLE_OPERATOR' },
]}
>
<Input disabled={Boolean(editing)} placeholder="例如 ROLE_OPERATOR" />
</Form.Item>
<Form.Item label="描述" name="description">
<Input.TextArea rows={2} placeholder="请输入角色描述" />
</Form.Item>
<Form.Item label="状态" name="enabled" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Form>
</Modal>
<Drawer
open={Boolean(permissionRole)}
title={`配置权限 - ${permissionRole?.name ?? ''}`}
size={420}
onClose={() => setPermissionRole(undefined)}
footer={
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => setPermissionRole(undefined)}></Button>
<Button type="primary" onClick={savePermission}>
</Button>
</Space>
}
>
<Space vertical size={20} style={{ width: '100%' }}>
<div>
<Typography.Title level={5} style={{ marginTop: 0 }}>
</Typography.Title>
<Tree
checkable
defaultExpandAll
checkedKeys={menuKeys}
treeData={menuPermissionTree}
fieldNames={{ title: 'title', key: 'key', children: 'children' }}
onCheck={checked => {
const keys = Array.isArray(checked) ? checked : checked.checked;
setMenuKeys(keys.map(String));
}}
/>
</div>
<div>
<Typography.Title level={5}></Typography.Title>
<Checkbox.Group
options={buttonPermissionOptions}
value={buttonKeys}
onChange={values => setButtonKeys(values.map(String))}
style={{ display: 'flex', flexDirection: 'column', gap: 8 }}
/>
</div>
</Space>
</Drawer>
</>
);
}

View File

@@ -0,0 +1,106 @@
export interface RoleItem {
id: string;
name: string;
code: string;
userCount: number;
enabled: boolean;
description: string;
updatedAt: string;
menuKeys: string[];
buttonKeys: string[];
}
export interface MenuPermissionNode {
key: string;
title: string;
children?: MenuPermissionNode[];
}
export const menuPermissionTree: MenuPermissionNode[] = [
{ key: 'dashboard', title: '总览' },
{
key: 'llm',
title: '大模型管理',
children: [
{ key: 'llm-models', title: '模型列表' },
{ key: 'llm-providers', title: '供应商管理' },
{ key: 'llm-usage', title: '用量统计' },
{ key: 'llm-usage-details', title: '用量明细' },
{ key: 'llm-quota', title: '限流配额' },
],
},
{
key: 'system',
title: '系统管理',
children: [
{ key: 'system-user', title: '用户管理' },
{ key: 'system-role', title: '角色管理' },
{ key: 'system-permission', title: '权限配置' },
],
},
];
export const buttonPermissionOptions = [
{ label: 'user:create', value: 'user:create' },
{ label: 'user:edit', value: 'user:edit' },
{ label: 'user:delete', value: 'user:delete' },
{ label: 'llm:create', value: 'llm:create' },
];
export const initialRoles: RoleItem[] = [
{
id: 'role-1',
name: '超级管理员',
code: 'ROLE_ADMIN',
userCount: 3,
enabled: true,
description: '拥有系统全部权限',
updatedAt: '2026-07-20 10:00',
menuKeys: [
'dashboard',
'llm',
'llm-models',
'llm-providers',
'llm-usage',
'llm-usage-details',
'llm-quota',
'system',
'system-user',
'system-role',
'system-permission',
],
buttonKeys: ['user:create', 'user:edit', 'user:delete', 'llm:create'],
},
{
id: 'role-2',
name: '运营',
code: 'ROLE_OPERATOR',
userCount: 5,
enabled: true,
description: '负责模型与用户日常运营',
updatedAt: '2026-07-22 15:30',
menuKeys: [
'dashboard',
'llm',
'llm-models',
'llm-providers',
'llm-usage',
'llm-quota',
'system',
'system-user',
'system-role',
],
buttonKeys: ['user:create', 'user:edit', 'llm:create'],
},
{
id: 'role-3',
name: '只读',
code: 'ROLE_VIEWER',
userCount: 4,
enabled: true,
description: '仅可查看数据,不可修改',
updatedAt: '2026-07-18 09:12',
menuKeys: ['dashboard', 'llm', 'llm-models', 'llm-usage'],
buttonKeys: [],
},
];

View File

@@ -0,0 +1,462 @@
import { PlusOutlined, SearchOutlined, UserOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import {
App,
Avatar,
Button,
Checkbox,
Col,
Form,
Input,
Modal,
Popconfirm,
Row,
Select,
Space,
Switch,
Table,
Tag,
Typography,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useMemo, useState } from 'react';
import { initialUsers, roleOptions, type UserItem, type UserRole } from './mock';
interface UserFormValues {
username: string;
nickname: string;
email: string;
phone: string;
roles: UserRole[];
password?: string;
enabled: boolean;
remark?: string;
}
interface PasswordFormValues {
password: string;
confirmPassword: string;
forceChange: boolean;
}
const roleColors: Record<UserRole, string> = {
: 'blue',
: 'cyan',
: 'default',
};
const maskPhone = (phone: string) => phone.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
export default function UserManagement() {
const { message } = App.useApp();
const [userForm] = Form.useForm<UserFormValues>();
const [passwordForm] = Form.useForm<PasswordFormValues>();
const [users, setUsers] = useState(initialUsers);
const [keyword, setKeyword] = useState('');
const [role, setRole] = useState<UserRole>();
const [enabled, setEnabled] = useState<boolean>();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [editingUser, setEditingUser] = useState<UserItem>();
const [formOpen, setFormOpen] = useState(false);
const [resetUser, setResetUser] = useState<UserItem>();
const filteredUsers = useMemo(() => {
const normalizedKeyword = keyword.trim().toLowerCase();
return users.filter(user => {
const matchesKeyword =
!normalizedKeyword ||
user.username.toLowerCase().includes(normalizedKeyword) ||
user.nickname.toLowerCase().includes(normalizedKeyword) ||
user.email.toLowerCase().includes(normalizedKeyword) ||
user.phone.includes(normalizedKeyword);
return (
matchesKeyword && (!role || user.roles.includes(role)) && (enabled === undefined || user.enabled === enabled)
);
});
}, [enabled, keyword, role, users]);
const closeUserForm = () => {
setFormOpen(false);
setEditingUser(undefined);
userForm.resetFields();
};
const openCreate = () => {
setEditingUser(undefined);
userForm.setFieldsValue({
username: '',
nickname: '',
email: '',
phone: '',
roles: ['只读'],
password: '',
enabled: true,
remark: '',
});
setFormOpen(true);
};
const openEdit = (user: UserItem) => {
setEditingUser(user);
userForm.setFieldsValue({ ...user, password: '' });
setFormOpen(true);
};
const saveUser = async () => {
const values = await userForm.validateFields();
if (editingUser) {
setUsers(current =>
current.map(user =>
user.id === editingUser.id
? {
...user,
nickname: values.nickname,
email: values.email,
phone: values.phone,
roles: values.roles,
enabled: values.enabled,
}
: user,
),
);
message.success('用户已更新');
} else {
setUsers(current => [
...current,
{
id: `user-${Date.now()}`,
username: values.username,
nickname: values.nickname,
email: values.email,
phone: values.phone,
roles: values.roles,
enabled: values.enabled,
lastLoginAt: '从未登录',
createdAt: '2026-07-26',
},
]);
message.success('用户已添加');
}
closeUserForm();
};
const deleteUser = (user: UserItem) => {
setUsers(current => current.filter(item => item.id !== user.id));
setSelectedKeys(current => current.filter(key => key !== user.id));
message.success('用户已删除');
};
const batchDisable = () => {
setUsers(current => current.map(user => (selectedKeys.includes(user.id) ? { ...user, enabled: false } : user)));
message.success(`已禁用 ${selectedKeys.length} 个用户`);
setSelectedKeys([]);
};
const openResetPassword = (user: UserItem) => {
setResetUser(user);
passwordForm.setFieldsValue({ password: '', confirmPassword: '', forceChange: true });
};
const resetPassword = async () => {
await passwordForm.validateFields();
message.success('密码已重置');
setResetUser(undefined);
passwordForm.resetFields();
};
const columns: ColumnsType<UserItem> = [
{
title: '用户名',
dataIndex: 'username',
width: 120,
render: (value: string) => (
<Space>
<Avatar size="small" icon={<UserOutlined />} />
<Typography.Text strong style={{ whiteSpace: 'nowrap' }}>
{value}
</Typography.Text>
</Space>
),
},
{ title: '昵称', dataIndex: 'nickname', width: 70, ellipsis: true },
{ title: '邮箱', dataIndex: 'email', width: 150, ellipsis: true },
{
title: '手机号',
dataIndex: 'phone',
width: 110,
render: (value: string) => <span style={{ whiteSpace: 'nowrap' }}>{maskPhone(value)}</span>,
},
{
title: '角色',
dataIndex: 'roles',
width: 100,
render: (roles: UserRole[]) => (
<Space size={[0, 4]} wrap>
{roles.map(item => (
<Tag key={item} color={roleColors[item]}>
{item}
</Tag>
))}
</Space>
),
},
{
title: '状态',
dataIndex: 'enabled',
width: 65,
render: (value: boolean, user) => (
<Switch
size="small"
checked={value}
checkedChildren="启用"
unCheckedChildren="禁用"
aria-label={`${user.username}状态`}
onChange={checked => {
setUsers(current => current.map(item => (item.id === user.id ? { ...item, enabled: checked } : item)));
message.success(checked ? '用户已启用' : '用户已禁用');
}}
/>
),
},
{
title: '最近登录',
dataIndex: 'lastLoginAt',
width: 150,
render: (value: string) => <span style={{ whiteSpace: 'nowrap' }}>{value}</span>,
},
{
title: '创建时间',
dataIndex: 'createdAt',
width: 100,
render: (value: string) => <span style={{ whiteSpace: 'nowrap' }}>{value}</span>,
},
{
title: '操作',
key: 'actions',
width: 180,
fixed: 'right',
render: (_, user) => (
<Space size={2}>
<Button type="link" size="small" onClick={() => openEdit(user)}>
</Button>
<Button type="link" size="small" onClick={() => openResetPassword(user)}>
</Button>
<Popconfirm title={`确定删除用户 ${user.username}`} onConfirm={() => deleteUser(user)}>
<Button type="link" danger size="small">
</Button>
</Popconfirm>
</Space>
),
},
];
return (
<>
<ProCard variant="outlined">
<Space vertical size={16} style={{ width: '100%' }}>
<div>
<Typography.Title level={5} style={{ margin: 0 }}>
</Typography.Title>
<Typography.Text type="secondary"></Typography.Text>
</div>
<Row gutter={[12, 12]} justify="space-between">
<Col flex="auto">
<Space wrap>
<Input
allowClear
prefix={<SearchOutlined />}
placeholder="搜索用户名 / 邮箱 / 手机号"
style={{ width: 260 }}
value={keyword}
onChange={event => setKeyword(event.target.value)}
/>
<Select
allowClear
placeholder="全部角色"
style={{ width: 140 }}
options={roleOptions.map(value => ({ label: value, value }))}
value={role}
onChange={setRole}
/>
<Select
allowClear
placeholder="全部状态"
style={{ width: 130 }}
options={[
{ label: '已启用', value: true },
{ label: '已禁用', value: false },
]}
value={enabled}
onChange={setEnabled}
/>
</Space>
</Col>
<Col>
<Space>
<Popconfirm
title={`确定禁用选中的 ${selectedKeys.length} 个用户?`}
disabled={!selectedKeys.length}
onConfirm={batchDisable}
>
<Button disabled={!selectedKeys.length}></Button>
</Popconfirm>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
</Button>
</Space>
</Col>
</Row>
<Table<UserItem>
rowKey="id"
columns={columns}
dataSource={filteredUsers}
rowSelection={{ selectedRowKeys: selectedKeys, onChange: setSelectedKeys }}
scroll={{ x: 1045 }}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: total => `${total}` }}
/>
</Space>
</ProCard>
<Modal
open={formOpen}
title={editingUser ? '编辑用户' : '新增用户'}
okText="保存"
cancelText="取消"
width={640}
onOk={saveUser}
onCancel={closeUserForm}
>
<Form<UserFormValues> form={userForm} layout="vertical" requiredMark="optional">
<Row gutter={16}>
<Col span={12}>
<Form.Item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名' }]}>
<Input disabled={Boolean(editingUser)} placeholder="登录账号,创建后不可修改" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="昵称" name="nickname" rules={[{ required: true, message: '请输入昵称' }]}>
<Input placeholder="请输入昵称" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="邮箱"
name="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '请输入有效邮箱' },
]}
>
<Input placeholder="name@example.com" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="手机号"
name="phone"
rules={[
{ required: true, message: '请输入手机号' },
{ pattern: /^1\d{10}$/, message: '请输入 11 位手机号' },
]}
>
<Input placeholder="请输入手机号" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item label="角色" name="roles" rules={[{ required: true, message: '请选择角色' }]}>
<Select
mode="multiple"
placeholder="请选择角色"
options={roleOptions.map(value => ({ label: value, value }))}
/>
</Form.Item>
</Col>
{!editingUser && (
<Col span={12}>
<Form.Item
label="初始密码"
name="password"
extra="首次登录后建议修改"
rules={[
{ required: true, message: '请输入初始密码' },
{ min: 8, message: '密码至少 8 位' },
]}
>
<Input.Password placeholder="请输入初始密码" />
</Form.Item>
</Col>
)}
<Col span={12}>
<Form.Item label="状态" name="enabled" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
</Col>
<Col span={24}>
<Form.Item label="备注" name="remark">
<Input.TextArea rows={2} placeholder="请输入备注" />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
<Modal
open={Boolean(resetUser)}
title={`重置密码 - ${resetUser?.username ?? ''}`}
okText="确认重置"
cancelText="取消"
onOk={resetPassword}
onCancel={() => {
setResetUser(undefined);
passwordForm.resetFields();
}}
>
<Typography.Paragraph type="secondary">
</Typography.Paragraph>
<Form<PasswordFormValues> form={passwordForm} layout="vertical">
<Form.Item
label="新密码"
name="password"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 8, message: '密码至少 8 位' },
]}
>
<Input.Password placeholder="请输入新密码" />
</Form.Item>
<Form.Item
label="确认密码"
name="confirmPassword"
dependencies={['password']}
rules={[
{ required: true, message: '请再次输入密码' },
({ getFieldValue }) => ({
validator(_, value) {
return !value || getFieldValue('password') === value
? Promise.resolve()
: Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password placeholder="请再次输入密码" />
</Form.Item>
<Form.Item name="forceChange" valuePropName="checked">
<Checkbox></Checkbox>
</Form.Item>
</Form>
</Modal>
</>
);
}

View File

@@ -0,0 +1,62 @@
export type UserRole = '超级管理员' | '运营' | '只读';
export interface UserItem {
id: string;
username: string;
nickname: string;
email: string;
phone: string;
roles: UserRole[];
enabled: boolean;
lastLoginAt: string;
createdAt: string;
}
export const roleOptions: UserRole[] = ['超级管理员', '运营', '只读'];
export const initialUsers: UserItem[] = [
{
id: 'user-1',
username: 'admin',
nickname: '管理员',
email: 'admin@chatone.ai',
phone: '13800000001',
roles: ['超级管理员'],
enabled: true,
lastLoginAt: '2026-07-25 22:10',
createdAt: '2025-01-01',
},
{
id: 'user-2',
username: 'operator',
nickname: '小王',
email: 'wang@chatone.ai',
phone: '13800000002',
roles: ['运营'],
enabled: true,
lastLoginAt: '2026-07-24 18:32',
createdAt: '2025-02-15',
},
{
id: 'user-3',
username: 'viewer',
nickname: '小李',
email: 'li@chatone.ai',
phone: '13800000003',
roles: ['只读'],
enabled: false,
lastLoginAt: '2026-07-20 09:15',
createdAt: '2025-03-10',
},
{
id: 'user-4',
username: 'model_ops',
nickname: '模型运营',
email: 'model.ops@chatone.ai',
phone: '13900000004',
roles: ['运营', '只读'],
enabled: true,
lastLoginAt: '2026-07-25 16:08',
createdAt: '2025-05-21',
},
];

View File

@@ -11,6 +11,9 @@ const Login = lazy(() => import('@/pages/login'));
const Dashboard = lazy(() => import('@/pages/dashboard'));
const LLM = lazy(() => import('@/pages/llm'));
const LLMProviders = lazy(() => import('@/pages/llm/providers'));
const UserManagement = lazy(() => import('@/pages/system/user'));
const RoleManagement = lazy(() => import('@/pages/system/role'));
const PermissionManagement = lazy(() => import('@/pages/system/permission'));
const NotFound = lazy(() => import('@/pages/exception/404'));
const Forbidden = lazy(() => import('@/pages/exception/403'));
@@ -45,10 +48,11 @@ export const routes: RouteObject[] = [
{ path: 'llm/usage', element: <div></div> },
{ path: 'llm/usage/details', element: <div></div> },
{ path: 'llm/quota', element: <div></div> },
{ path: 'system/user', element: <div></div> },
{ path: 'system/role', element: <div></div> },
{ path: 'system/permission/menu', element: <div></div> },
{ path: 'system/permission/button', element: <div></div> },
{ path: 'system/user', element: <UserManagement /> },
{ path: 'system/role', element: <RoleManagement /> },
{ path: 'system/permission', element: <PermissionManagement /> },
{ path: 'system/permission/menu', element: <Navigate to="/system/permission" replace /> },
{ path: 'system/permission/button', element: <Navigate to="/system/permission" replace /> },
{ path: '*', element: <NotFound /> },
],
},