实现系统菜单树管理第一版

This commit is contained in:
andy
2026-07-16 12:47:21 +07:00
parent 5390b8f71b
commit ed37d5f955
9 changed files with 1410 additions and 316 deletions

View File

@@ -10,6 +10,7 @@ import {
createAdminUser,
fetchAdminAudits,
fetchAdminHotels,
fetchAdminMenuTree,
fetchAdminMenus,
fetchAdminPermissions,
fetchAdminRoleDetail,
@@ -20,6 +21,7 @@ import {
updateAdminHotel,
updateAdminHotelStatus,
updateAdminMenu,
updateAdminMenuTreeOrder,
updateAdminRole,
updateAdminUser,
} from '@/services/systemAdminService'
@@ -107,6 +109,69 @@ describe('systemAdminService', () => {
)
})
it('fetches and saves the admin menu tree endpoints', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse({
items: [
{
id: '40001',
parent_id: null,
menu_code: 'SYSTEM_SETTINGS',
menu_name: '系统设置',
menu_type: 'GROUP',
route_path: '/system',
component_key: 'system',
icon_key: 'pi pi-cog',
permission_code: 'SYSTEM_ADMIN_CONSOLE_ACCESS',
sort_order: 900,
visible: true,
menu_status: 'ACTIVE',
known_route: true,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
children: [],
},
],
warnings: ['菜单 49999 的 parent_id 不存在,已按根级异常节点返回。'],
}),
)
const tree = await fetchAdminMenuTree()
await updateAdminMenuTreeOrder({
items: [
{
menu_id: '40002',
parent_id: '40001',
sort_order: 100,
},
],
})
expect(tree.items[0]?.children).toEqual([])
expect(tree.warnings[0]).toContain('parent_id')
expect(fetchMock).toHaveBeenNthCalledWith(
1,
'/api/admin/menus/tree',
expect.objectContaining({ method: 'GET' }),
)
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'/api/admin/menus/tree-order',
expect.objectContaining({
method: 'PUT',
body: JSON.stringify({
items: [
{
menu_id: '40002',
parent_id: '40001',
sort_order: 100,
},
],
}),
}),
)
})
it('fetches the readonly permission list', async () => {
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
mockJsonResponse([

View File

@@ -0,0 +1,248 @@
import { flushPromises, mount } from '@vue/test-utils'
import { createI18n } from 'vue-i18n'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import zhCN from '@/i18n/locales/zh-CN'
import SystemMenusView from '@/views/system/SystemMenusView.vue'
vi.mock('@/services/systemAdminService', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/services/systemAdminService')>()
return {
...actual,
createAdminMenu: vi.fn(),
fetchAdminMenuTree: vi.fn(),
fetchAdminPermissions: vi.fn(),
updateAdminMenu: vi.fn(),
updateAdminMenuTreeOrder: vi.fn(),
}
})
const systemAdminService = await import('@/services/systemAdminService')
function createMenuTree(options: { warnings?: string[] } = {}) {
return {
items: [
{
id: '40001',
parent_id: null,
menu_code: 'SYSTEM_SETTINGS',
menu_name: '系统设置',
menu_type: 'GROUP',
route_path: '/system',
component_key: 'system',
icon_key: 'pi pi-cog',
permission_code: 'SYSTEM_ADMIN_CONSOLE_ACCESS',
sort_order: 900,
visible: true,
menu_status: 'ACTIVE',
known_route: true,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
children: [
{
id: '40002',
parent_id: '40001',
menu_code: 'SYSTEM_USERS',
menu_name: '用户管理',
menu_type: 'PAGE',
route_path: '/system/users',
component_key: 'systemUsers',
icon_key: 'pi pi-users',
permission_code: 'SYSTEM_USER_MANAGE',
sort_order: 100,
visible: true,
menu_status: 'ACTIVE',
known_route: true,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
children: [],
},
{
id: '40003',
parent_id: '40001',
menu_code: 'SYSTEM_ROLES',
menu_name: '角色权限',
menu_type: 'PAGE',
route_path: '/system/roles',
component_key: 'systemRoles',
icon_key: 'pi pi-shield',
permission_code: 'SYSTEM_ROLE_MANAGE',
sort_order: 200,
visible: true,
menu_status: 'ACTIVE',
known_route: true,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
children: [],
},
],
},
{
id: '49999',
parent_id: '48888',
menu_code: 'BROKEN_MENU',
menu_name: '孤儿菜单',
menu_type: 'PAGE',
route_path: '/ghost',
component_key: 'ghost',
icon_key: 'pi pi-question',
permission_code: null,
sort_order: 999,
visible: true,
menu_status: 'ACTIVE',
known_route: false,
created_at: '2026-07-16T00:00:00Z',
updated_at: '2026-07-16T00:00:00Z',
children: [],
},
],
warnings: options.warnings ?? [],
}
}
function mountSystemMenusView() {
const i18n = createI18n({
legacy: false,
locale: 'zh-CN',
messages: {
'zh-CN': zhCN,
},
})
return mount(SystemMenusView, {
global: {
plugins: [i18n],
},
})
}
describe('SystemMenusView', () => {
beforeEach(() => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockReset()
vi.mocked(systemAdminService.fetchAdminPermissions).mockReset()
vi.mocked(systemAdminService.createAdminMenu).mockReset()
vi.mocked(systemAdminService.updateAdminMenu).mockReset()
vi.mocked(systemAdminService.updateAdminMenuTreeOrder).mockReset()
vi.mocked(systemAdminService.fetchAdminPermissions).mockResolvedValue([])
})
it('renders the backend menu tree with warning state', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(
createMenuTree({
warnings: ['菜单 49999 的 parent_id=48888 不存在,已按根级异常节点返回。'],
}),
)
const wrapper = mountSystemMenusView()
await flushPromises()
expect(systemAdminService.fetchAdminMenuTree).toHaveBeenCalledTimes(1)
expect(wrapper.text()).toContain('系统设置')
expect(wrapper.text()).toContain('用户管理')
expect(wrapper.text()).toContain('孤儿菜单')
expect(wrapper.text()).toContain('菜单树存在异常配置')
expect(wrapper.find('[data-testid="menu-tree"]').exists()).toBe(true)
})
it('saves reordered sibling nodes through tree-order endpoint', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(createMenuTree())
vi.mocked(systemAdminService.updateAdminMenuTreeOrder).mockResolvedValue({
items: createMenuTree().items,
warnings: [],
})
const wrapper = mountSystemMenusView()
await flushPromises()
const rolesMoveUp = wrapper.find('[data-testid="move-up-40003"]')
expect(rolesMoveUp.exists()).toBe(true)
await rolesMoveUp.trigger('click')
expect(wrapper.text()).toContain('树结构有未保存调整')
await wrapper.find('[data-testid="save-tree-order"]').trigger('click')
await flushPromises()
expect(systemAdminService.updateAdminMenuTreeOrder).toHaveBeenCalledWith({
items: expect.arrayContaining([
expect.objectContaining({
menu_id: '40003',
parent_id: '40001',
sort_order: 100,
}),
expect.objectContaining({
menu_id: '40002',
parent_id: '40001',
sort_order: 200,
}),
]),
})
})
it('blocks tree refresh and menu config save while tree order has unsaved changes', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(createMenuTree())
const wrapper = mountSystemMenusView()
await flushPromises()
await wrapper.find('[data-testid="move-up-40003"]').trigger('click')
expect(wrapper.text()).toContain('树结构有未保存调整')
expect(wrapper.find('[data-testid="refresh-menu-tree"]').attributes('disabled')).toBeDefined()
expect(wrapper.find('[data-testid="update-menu-config"]').attributes('disabled')).toBeDefined()
})
it('keeps the loaded tree visible when tree-order save fails', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(createMenuTree())
vi.mocked(systemAdminService.updateAdminMenuTreeOrder).mockRejectedValue(new Error('保存树结构失败'))
const wrapper = mountSystemMenusView()
await flushPromises()
await wrapper.find('[data-testid="move-up-40003"]').trigger('click')
await wrapper.find('[data-testid="save-tree-order"]').trigger('click')
await flushPromises()
expect(wrapper.find('[data-testid="menu-tree"]').exists()).toBe(true)
expect(wrapper.text()).toContain('保存树结构失败')
expect(wrapper.text()).toContain('用户管理')
})
it('shows permission load failure and disables menu config submit', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(createMenuTree())
vi.mocked(systemAdminService.fetchAdminPermissions).mockRejectedValue(new Error('权限接口异常'))
const wrapper = mountSystemMenusView()
await flushPromises()
expect(wrapper.text()).toContain('权限列表加载失败')
expect(wrapper.find('[data-testid="update-menu-config"]').attributes('disabled')).toBeDefined()
})
it('locks tree ordering when backend returns tree warnings', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(
createMenuTree({
warnings: ['菜单 49999 的 parent_id=48888 不存在,已按根级异常节点返回。'],
}),
)
const wrapper = mountSystemMenusView()
await flushPromises()
expect(wrapper.find('[data-testid="save-tree-order"]').attributes('disabled')).toBeDefined()
expect(wrapper.find('[data-testid="move-up-40003"]').attributes('disabled')).toBeDefined()
expect(wrapper.text()).toContain('请先处理菜单树异常')
})
it('clears dragged menu state on dragend before a later tree drop', async () => {
vi.mocked(systemAdminService.fetchAdminMenuTree).mockResolvedValue(createMenuTree())
const wrapper = mountSystemMenusView()
await flushPromises()
await wrapper.find('[data-testid="menu-row-40002"]').trigger('dragstart')
await wrapper.find('[data-testid="menu-row-40002"]').trigger('dragend')
await wrapper.find('[data-testid="menu-tree"]').trigger('drop')
expect(wrapper.text()).not.toContain('树结构有未保存调整')
})
})