fix: 修复系统管理审查发现的权限一致性与数据同步问题

统一角色与权限配置的标识体系,并修正菜单树勾选、菜单级联清理与用户备注持久化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-26 03:27:23 +08:00
parent 77f718965d
commit 8a03781577
8 changed files with 74 additions and 60 deletions

View File

@@ -19,6 +19,12 @@ test.describe('系统管理 / 权限配置', () => {
await page.getByRole('button', { name: '保 存' }).click(); await page.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('菜单已保存')).toBeVisible(); await expect(page.getByText('菜单已保存')).toBeVisible();
await expect(page.getByRole('tree').getByText('用户中心')).toBeVisible(); await expect(page.getByRole('tree').getByText('用户中心')).toBeVisible();
await page.getByRole('tab', { name: '按钮权限' }).click();
const buttonPanel = page.getByRole('tabpanel', { name: '按钮权限' });
await expect(buttonPanel.getByRole('row').filter({ hasText: 'system:user:add' })).toContainText(
'系统管理 / 用户中心',
);
}); });
test('可新增同级菜单', async ({ page }) => { test('可新增同级菜单', async ({ page }) => {
@@ -42,4 +48,15 @@ test.describe('系统管理 / 权限配置', () => {
await expect(buttonPanel.getByText('system:user:import')).toBeVisible(); await expect(buttonPanel.getByText('system:user:import')).toBeVisible();
await expect(page.getByText('按钮权限已添加')).toBeVisible(); await expect(page.getByText('按钮权限已添加')).toBeVisible();
}); });
test('删除菜单时同步清理按钮权限', async ({ page }) => {
await page.getByRole('button', { name: '删除' }).first().click();
await page.getByRole('button', { name: '确 定' }).click();
await expect(page.getByText('菜单已删除')).toBeVisible();
await page.getByRole('tab', { name: '按钮权限' }).click();
const buttonPanel = page.getByRole('tabpanel', { name: '按钮权限' });
await expect(buttonPanel.getByText('system:user:add')).toHaveCount(0);
await expect(buttonPanel.getByText('system:user:edit')).toHaveCount(0);
});
}); });

View File

@@ -40,6 +40,9 @@ test.describe('系统管理 / 角色管理', () => {
await expect(page.getByText('配置权限 - 运营')).toBeVisible(); await expect(page.getByText('配置权限 - 运营')).toBeVisible();
await expect(page.getByText('菜单权限')).toBeVisible(); await expect(page.getByText('菜单权限')).toBeVisible();
await expect(page.getByText('按钮权限')).toBeVisible(); await expect(page.getByText('按钮权限')).toBeVisible();
await expect(page.getByRole('treeitem', { name: '用量明细' })).toHaveAttribute('aria-checked', 'false');
await expect(page.getByRole('treeitem', { name: '模型列表' })).toHaveAttribute('aria-checked', 'true');
await expect(page.getByRole('checkbox', { name: 'system:user:add' })).toBeChecked();
await page.getByRole('button', { name: '保 存' }).click(); await page.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('权限已保存')).toBeVisible(); await expect(page.getByText('权限已保存')).toBeVisible();

View File

@@ -51,9 +51,14 @@ test.describe('系统管理 / 用户管理', () => {
await operatorRow.getByRole('button', { name: '编辑' }).click(); await operatorRow.getByRole('button', { name: '编辑' }).click();
const dialog = page.getByRole('dialog', { name: '编辑用户' }); const dialog = page.getByRole('dialog', { name: '编辑用户' });
await dialog.getByLabel('昵称').fill('新运营'); await dialog.getByLabel('昵称').fill('新运营');
await dialog.getByLabel('备注').fill('负责日常运营');
await dialog.getByRole('button', { name: '保 存' }).click(); await dialog.getByRole('button', { name: '保 存' }).click();
await expect(page.getByText('新运营', { exact: true })).toBeVisible(); await expect(page.getByText('新运营', { exact: true })).toBeVisible();
await operatorRow.getByRole('button', { name: '编辑' }).click();
await expect(page.getByRole('dialog', { name: '编辑用户' }).getByLabel('备注')).toHaveValue('负责日常运营');
await page.getByRole('dialog', { name: '编辑用户' }).getByRole('button', { name: '取 消' }).click();
const viewerRow = page.getByRole('row').filter({ hasText: 'viewer' }); const viewerRow = page.getByRole('row').filter({ hasText: 'viewer' });
await viewerRow.getByRole('button', { name: '删除' }).click(); await viewerRow.getByRole('button', { name: '删除' }).click();
await page.getByRole('button', { name: '确 定' }).click(); await page.getByRole('button', { name: '确 定' }).click();

View File

@@ -93,6 +93,11 @@ const removeNode = (nodes: MenuPermissionNode[], key: string): MenuPermissionNod
.filter(node => node.key !== key) .filter(node => node.key !== key)
.map(node => (node.children ? { ...node, children: removeNode(node.children, key) } : node)); .map(node => (node.children ? { ...node, children: removeNode(node.children, key) } : node));
const collectNodeKeys = (node: MenuPermissionNode): string[] => [
node.key,
...(node.children?.flatMap(collectNodeKeys) ?? []),
];
const toTreeData = (nodes: MenuPermissionNode[]): DataNode[] => const toTreeData = (nodes: MenuPermissionNode[]): DataNode[] =>
nodes.map(node => ({ nodes.map(node => ({
key: node.key, key: node.key,
@@ -212,8 +217,10 @@ export default function PermissionManagement() {
const deleteMenu = () => { const deleteMenu = () => {
if (!selectedNode) return; if (!selectedNode) return;
const removedKeys = new Set(collectNodeKeys(selectedNode));
const nextTree = removeNode(menuTree, selectedKey); const nextTree = removeNode(menuTree, selectedKey);
setMenuTree(nextTree); setMenuTree(nextTree);
setButtons(current => current.filter(item => !removedKeys.has(item.menuKey)));
const fallback = nextTree[0]?.key ?? nextTree[0]?.children?.[0]?.key; const fallback = nextTree[0]?.key ?? nextTree[0]?.children?.[0]?.key;
if (fallback) { if (fallback) {
setSelectedKey(fallback); setSelectedKey(fallback);
@@ -228,10 +235,17 @@ export default function PermissionManagement() {
const saveMenu = async () => { const saveMenu = async () => {
if (!selectedKey) return; if (!selectedKey) return;
const values = await menuForm.validateFields(); const values = await menuForm.validateFields();
setMenuTree(current => const nextTree = updateNode(menuTree, selectedKey, node => ({
updateNode(current, selectedKey, node => ({ ...node,
...node, ...values,
...values, }));
const menuPaths = new Map(flattenMenus(nextTree).map(item => [item.key, item.label]));
setMenuTree(nextTree);
setButtons(current =>
current.map(item => ({
...item,
menuPath: menuPaths.get(item.menuKey) ?? item.menuPath,
})), })),
); );
message.success('菜单已保存'); message.success('菜单已保存');

View File

@@ -287,6 +287,7 @@ export default function RoleManagement() {
</Typography.Title> </Typography.Title>
<Tree <Tree
checkable checkable
checkStrictly
defaultExpandAll defaultExpandAll
checkedKeys={menuKeys} checkedKeys={menuKeys}
treeData={menuPermissionTree} treeData={menuPermissionTree}

View File

@@ -1,3 +1,9 @@
import {
initialButtonPermissions,
initialMenuTree,
type MenuPermissionNode as PermissionMenuNode,
} from '../permission/mock';
export interface RoleItem { export interface RoleItem {
id: string; id: string;
name: string; name: string;
@@ -16,36 +22,19 @@ export interface MenuPermissionNode {
children?: MenuPermissionNode[]; children?: MenuPermissionNode[];
} }
export const menuPermissionTree: MenuPermissionNode[] = [ const toRoleMenuTree = (nodes: PermissionMenuNode[]): MenuPermissionNode[] =>
{ key: 'dashboard', title: '总览' }, nodes.map(node => ({
{ key: node.key,
key: 'llm', title: node.title,
title: '大模型管理', children: node.children ? toRoleMenuTree(node.children) : undefined,
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 = [ export const menuPermissionTree = toRoleMenuTree(initialMenuTree);
{ label: 'user:create', value: 'user:create' },
{ label: 'user:edit', value: 'user:edit' }, export const buttonPermissionOptions = initialButtonPermissions.map(item => ({
{ label: 'user:delete', value: 'user:delete' }, label: item.code,
{ label: 'llm:create', value: 'llm:create' }, value: item.code,
]; }));
export const initialRoles: RoleItem[] = [ export const initialRoles: RoleItem[] = [
{ {
@@ -56,20 +45,8 @@ export const initialRoles: RoleItem[] = [
enabled: true, enabled: true,
description: '拥有系统全部权限', description: '拥有系统全部权限',
updatedAt: '2026-07-20 10:00', updatedAt: '2026-07-20 10:00',
menuKeys: [ menuKeys: ['1', '2', '2-1', '2-2', '2-3', '2-4', '2-5', '3', '3-1', '3-2', '3-3'],
'dashboard', buttonKeys: initialButtonPermissions.map(item => item.code),
'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', id: 'role-2',
@@ -79,18 +56,8 @@ export const initialRoles: RoleItem[] = [
enabled: true, enabled: true,
description: '负责模型与用户日常运营', description: '负责模型与用户日常运营',
updatedAt: '2026-07-22 15:30', updatedAt: '2026-07-22 15:30',
menuKeys: [ menuKeys: ['1', '2', '2-1', '2-2', '2-3', '2-5', '3', '3-1', '3-2'],
'dashboard', buttonKeys: ['system:user:add', 'system:user:edit', 'llm:models:add'],
'llm',
'llm-models',
'llm-providers',
'llm-usage',
'llm-quota',
'system',
'system-user',
'system-role',
],
buttonKeys: ['user:create', 'user:edit', 'llm:create'],
}, },
{ {
id: 'role-3', id: 'role-3',
@@ -100,7 +67,7 @@ export const initialRoles: RoleItem[] = [
enabled: true, enabled: true,
description: '仅可查看数据,不可修改', description: '仅可查看数据,不可修改',
updatedAt: '2026-07-18 09:12', updatedAt: '2026-07-18 09:12',
menuKeys: ['dashboard', 'llm', 'llm-models', 'llm-usage'], menuKeys: ['1', '2', '2-1', '2-3'],
buttonKeys: [], buttonKeys: [],
}, },
]; ];

View File

@@ -118,6 +118,7 @@ export default function UserManagement() {
phone: values.phone, phone: values.phone,
roles: values.roles, roles: values.roles,
enabled: values.enabled, enabled: values.enabled,
remark: values.remark,
} }
: user, : user,
), ),
@@ -134,6 +135,7 @@ export default function UserManagement() {
phone: values.phone, phone: values.phone,
roles: values.roles, roles: values.roles,
enabled: values.enabled, enabled: values.enabled,
remark: values.remark,
lastLoginAt: '从未登录', lastLoginAt: '从未登录',
createdAt: '2026-07-26', createdAt: '2026-07-26',
}, },

View File

@@ -10,6 +10,7 @@ export interface UserItem {
enabled: boolean; enabled: boolean;
lastLoginAt: string; lastLoginAt: string;
createdAt: string; createdAt: string;
remark?: string;
} }
export const roleOptions: UserRole[] = ['超级管理员', '运营', '只读']; export const roleOptions: UserRole[] = ['超级管理员', '运营', '只读'];
@@ -25,6 +26,7 @@ export const initialUsers: UserItem[] = [
enabled: true, enabled: true,
lastLoginAt: '2026-07-25 22:10', lastLoginAt: '2026-07-25 22:10',
createdAt: '2025-01-01', createdAt: '2025-01-01',
remark: '系统内置管理员',
}, },
{ {
id: 'user-2', id: 'user-2',
@@ -36,6 +38,7 @@ export const initialUsers: UserItem[] = [
enabled: true, enabled: true,
lastLoginAt: '2026-07-24 18:32', lastLoginAt: '2026-07-24 18:32',
createdAt: '2025-02-15', createdAt: '2025-02-15',
remark: '',
}, },
{ {
id: 'user-3', id: 'user-3',
@@ -47,6 +50,7 @@ export const initialUsers: UserItem[] = [
enabled: false, enabled: false,
lastLoginAt: '2026-07-20 09:15', lastLoginAt: '2026-07-20 09:15',
createdAt: '2025-03-10', createdAt: '2025-03-10',
remark: '',
}, },
{ {
id: 'user-4', id: 'user-4',
@@ -58,5 +62,6 @@ export const initialUsers: UserItem[] = [
enabled: true, enabled: true,
lastLoginAt: '2026-07-25 16:08', lastLoginAt: '2026-07-25 16:08',
createdAt: '2025-05-21', createdAt: '2025-05-21',
remark: '',
}, },
]; ];