fix: 修复系统管理审查发现的权限一致性与数据同步问题
统一角色与权限配置的标识体系,并修正菜单树勾选、菜单级联清理与用户备注持久化。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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('菜单已保存');
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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: [],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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: '',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
Reference in New Issue
Block a user