feat: 实现系统管理用户、角色与权限配置页面
补齐 RBAC 相关页面与 UI 设计图,并接入功能与视觉回归测试,便于后续持续验证。 Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -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' },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
BIN
docs/ui/system/permission/chatone-system-permission-ui.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/ui/system/role/chatone-system-role-ui.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/ui/system/user/chatone-system-user-form-ui.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/ui/system/user/chatone-system-user-list-ui.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/ui/system/user/chatone-system-user-reset-pwd-ui.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
@@ -17,8 +17,8 @@ pnpm test:e2e:update # 确认 UI 改动后更新截图基线
|
||||
- `visual/`:页面截图回归。
|
||||
- `snapshots/`:由 `test:e2e:update` 生成的基线图。
|
||||
|
||||
模型列表、供应商管理的功能和视觉用例已启用。确认页面视觉改动符合预期后,执行
|
||||
模型列表、供应商管理、用户管理、角色管理、权限配置的功能和视觉用例已启用。确认页面视觉改动符合预期后,执行
|
||||
`pnpm test:e2e:update` 更新基线。
|
||||
|
||||
设计目标图位于 `docs/ui/llm/list/`;截图基线用于检测后续代码造成的
|
||||
设计目标图位于 `docs/ui/`;截图基线用于检测后续代码造成的
|
||||
视觉变化,不替代设计稿还原度审查。
|
||||
|
||||
45
e2e/pages/system-permission.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
60
e2e/pages/system-roles.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
72
e2e/pages/system-users.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 103 KiB |
12
e2e/visual/system-permission.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
12
e2e/visual/system-roles.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
12
e2e/visual/system-users.spec.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
@@ -71,6 +71,7 @@ const mockMenus: MenuItem[] = [
|
||||
id: '3-3',
|
||||
name: '权限配置',
|
||||
path: '/system/permission',
|
||||
icon: 'SafetyCertificateOutlined',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
491
src/pages/system/permission/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
164
src/pages/system/permission/mock.ts
Normal 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' },
|
||||
];
|
||||
313
src/pages/system/role/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
106
src/pages/system/role/mock.ts
Normal 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: [],
|
||||
},
|
||||
];
|
||||
462
src/pages/system/user/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
62
src/pages/system/user/mock.ts
Normal 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',
|
||||
},
|
||||
];
|
||||
@@ -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 /> },
|
||||
],
|
||||
},
|
||||
|
||||