实现系统菜单树管理第一版
This commit is contained in:
@@ -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([
|
||||
|
||||
248
client/src/tests/systemMenusView.spec.ts
Normal file
248
client/src/tests/systemMenusView.spec.ts
Normal 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('树结构有未保存调整')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user