实现系统菜单树管理第一版
This commit is contained in:
@@ -56,6 +56,9 @@ export default {
|
||||
nextPage: 'Next',
|
||||
pageStatus: 'Page {current} / {total}',
|
||||
pageSize: 'Page size',
|
||||
refresh: 'Refresh',
|
||||
saving: 'Saving',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
notFound: {
|
||||
title: 'Page unavailable',
|
||||
@@ -130,16 +133,33 @@ export default {
|
||||
},
|
||||
menus: {
|
||||
title: 'Menu filters',
|
||||
managementTitle: 'Menu management',
|
||||
managementHint: 'Maintain sidebar menu hierarchy, order, visibility, and permission entry points.',
|
||||
createTitle: 'Create menu',
|
||||
editTitle: 'Edit menu',
|
||||
tableTitle: 'Menus',
|
||||
totalSuffix: 'menus',
|
||||
keywordPlaceholder: 'Menu code / name / route',
|
||||
menuStatus: 'Menu status',
|
||||
visibleOnly: 'Visible only',
|
||||
treeWarningTitle: 'Menu tree has configuration warnings',
|
||||
treeWarningHint: 'The backend returned the menu tree, but these items need administrator review.',
|
||||
treeTitle: 'Menu tree',
|
||||
treeDirty: 'Tree changes are not saved',
|
||||
treeDirtyActionBlocked: 'Save the menu tree before continuing.',
|
||||
treeWarningSaveBlocked: 'Resolve the menu tree warnings and confirm parent relationships before saving the tree.',
|
||||
saveTreeOrder: 'Save tree',
|
||||
menuCode: 'Menu code',
|
||||
menuName: 'Menu name',
|
||||
parentMenu: 'Parent menu',
|
||||
rootMenu: 'Root menu',
|
||||
componentKey: 'Component key',
|
||||
iconKey: 'Icon key',
|
||||
unknownRoute: 'Unknown route',
|
||||
unknownRouteHint: 'This route is not recognized by the frontend and may open an unavailable page.',
|
||||
permissionLoadFailed: 'Permission list failed to load. Permission selection and menu config saving are temporarily unavailable.',
|
||||
moveUp: 'Move up',
|
||||
moveDown: 'Move down',
|
||||
columns: {
|
||||
menu: 'Menu',
|
||||
route: 'Route',
|
||||
|
||||
@@ -56,6 +56,9 @@ export default {
|
||||
nextPage: 'หน้าถัดไป',
|
||||
pageStatus: 'หน้า {current} / {total}',
|
||||
pageSize: 'ต่อหน้า',
|
||||
refresh: 'รีเฟรช',
|
||||
saving: 'กำลังบันทึก',
|
||||
cancel: 'ยกเลิก',
|
||||
},
|
||||
notFound: {
|
||||
title: 'หน้านี้ยังไม่พร้อม',
|
||||
@@ -130,16 +133,33 @@ export default {
|
||||
},
|
||||
menus: {
|
||||
title: 'ตัวกรองเมนู',
|
||||
managementTitle: 'จัดการเมนู',
|
||||
managementHint: 'ดูแลลำดับชั้น ลำดับการแสดง การมองเห็น และสิทธิ์ของเมนูด้านข้าง',
|
||||
createTitle: 'สร้างเมนู',
|
||||
editTitle: 'แก้ไขเมนู',
|
||||
tableTitle: 'ข้อมูลเมนู',
|
||||
totalSuffix: 'เมนู',
|
||||
keywordPlaceholder: 'รหัสเมนู / ชื่อ / เส้นทาง',
|
||||
menuStatus: 'สถานะเมนู',
|
||||
visibleOnly: 'ดูเฉพาะเมนูที่แสดง',
|
||||
treeWarningTitle: 'โครงสร้างเมนูมีการตั้งค่าที่ต้องตรวจสอบ',
|
||||
treeWarningHint: 'หลังบ้านส่งโครงสร้างเมนูกลับมาแล้ว แต่รายการต่อไปนี้ต้องให้ผู้ดูแลตรวจสอบ',
|
||||
treeTitle: 'โครงสร้างเมนู',
|
||||
treeDirty: 'มีการปรับโครงสร้างที่ยังไม่ได้บันทึก',
|
||||
treeDirtyActionBlocked: 'โปรดบันทึกโครงสร้างเมนูก่อนดำเนินการต่อ',
|
||||
treeWarningSaveBlocked: 'โปรดแก้ไขคำเตือนของโครงสร้างเมนูและยืนยันเมนูหลักให้ถูกต้องก่อนบันทึก',
|
||||
saveTreeOrder: 'บันทึกโครงสร้าง',
|
||||
menuCode: 'รหัสเมนู',
|
||||
menuName: 'ชื่อเมนู',
|
||||
parentMenu: 'เมนูหลัก',
|
||||
rootMenu: 'เมนูราก',
|
||||
componentKey: 'รหัสคอมโพเนนต์',
|
||||
iconKey: 'รหัสไอคอน',
|
||||
unknownRoute: 'เส้นทางไม่รู้จัก',
|
||||
unknownRouteHint: 'เส้นทางนี้ยังไม่ถูกรู้จักโดยหน้าเว็บ อาจเปิดไปยังหน้าที่ยังไม่พร้อม',
|
||||
permissionLoadFailed: 'โหลดรายการสิทธิ์ไม่สำเร็จ จึงยังเลือกสิทธิ์และบันทึกการตั้งค่าเมนูไม่ได้',
|
||||
moveUp: 'เลื่อนขึ้น',
|
||||
moveDown: 'เลื่อนลง',
|
||||
columns: {
|
||||
menu: 'เมนู',
|
||||
route: 'เส้นทาง',
|
||||
|
||||
@@ -56,6 +56,9 @@ export default {
|
||||
nextPage: '下一页',
|
||||
pageStatus: '第 {current} / {total} 页',
|
||||
pageSize: '每页',
|
||||
refresh: '刷新',
|
||||
saving: '保存中',
|
||||
cancel: '取消',
|
||||
},
|
||||
notFound: {
|
||||
title: '页面不可用',
|
||||
@@ -130,16 +133,33 @@ export default {
|
||||
},
|
||||
menus: {
|
||||
title: '菜单筛选',
|
||||
managementTitle: '菜单管理',
|
||||
managementHint: '维护侧边栏菜单的父子层级、排序、可见性和权限入口。',
|
||||
createTitle: '新增菜单',
|
||||
editTitle: '编辑菜单',
|
||||
tableTitle: '菜单数据',
|
||||
totalSuffix: '个菜单',
|
||||
keywordPlaceholder: '菜单代码 / 名称 / 路由',
|
||||
menuStatus: '菜单状态',
|
||||
visibleOnly: '只看可见菜单',
|
||||
treeWarningTitle: '菜单树存在异常配置',
|
||||
treeWarningHint: '后端已返回菜单树,但以下配置需要管理员确认。',
|
||||
treeTitle: '菜单树',
|
||||
treeDirty: '树结构有未保存调整',
|
||||
treeDirtyActionBlocked: '请先保存树结构后再继续操作。',
|
||||
treeWarningSaveBlocked: '请先处理菜单树异常,确认父级关系正确后再保存树结构。',
|
||||
saveTreeOrder: '保存树结构',
|
||||
menuCode: '菜单代码',
|
||||
menuName: '菜单名称',
|
||||
parentMenu: '上级菜单',
|
||||
rootMenu: '根菜单',
|
||||
componentKey: '组件标识',
|
||||
iconKey: '图标标识',
|
||||
unknownRoute: '未知路由',
|
||||
unknownRouteHint: '当前路由未被前端识别,菜单可能会进入未接入页面。',
|
||||
permissionLoadFailed: '权限列表加载失败,权限选择和菜单配置保存暂不可用。',
|
||||
moveUp: '上移',
|
||||
moveDown: '下移',
|
||||
columns: {
|
||||
menu: '菜单',
|
||||
route: '路由',
|
||||
|
||||
@@ -10,6 +10,8 @@ import type {
|
||||
AdminMenuCreateRequest,
|
||||
AdminMenuListFilters,
|
||||
AdminMenuResult,
|
||||
AdminMenuTreeOrderUpdateRequest,
|
||||
AdminMenuTreeResult,
|
||||
AdminMenuUpdateRequest,
|
||||
AdminPermissionResult,
|
||||
AdminRoleCreateRequest,
|
||||
@@ -118,6 +120,10 @@ export async function fetchAdminMenus(
|
||||
return getJson<PlatformPageResult<AdminMenuResult>>(withQuery('/api/admin/menus', filters))
|
||||
}
|
||||
|
||||
export async function fetchAdminMenuTree(): Promise<AdminMenuTreeResult> {
|
||||
return getJson<AdminMenuTreeResult>('/api/admin/menus/tree')
|
||||
}
|
||||
|
||||
export async function createAdminMenu(request: AdminMenuCreateRequest): Promise<AdminMenuResult> {
|
||||
return sendJson<AdminMenuResult>('/api/admin/menus', 'POST', request)
|
||||
}
|
||||
@@ -126,6 +132,12 @@ export async function updateAdminMenu(menuId: string, request: AdminMenuUpdateRe
|
||||
return sendJson<AdminMenuResult>(`/api/admin/menus/${encodeURIComponent(menuId)}`, 'PUT', request)
|
||||
}
|
||||
|
||||
export async function updateAdminMenuTreeOrder(
|
||||
request: AdminMenuTreeOrderUpdateRequest,
|
||||
): Promise<AdminMenuTreeResult> {
|
||||
return sendJson<AdminMenuTreeResult>('/api/admin/menus/tree-order', 'PUT', request)
|
||||
}
|
||||
|
||||
export async function fetchAdminHotels(
|
||||
filters: AdminHotelListFilters = {},
|
||||
): Promise<PlatformPageResult<AdminHotelResult>> {
|
||||
|
||||
@@ -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('树结构有未保存调整')
|
||||
})
|
||||
})
|
||||
@@ -107,6 +107,15 @@ export interface AdminMenuResult {
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface AdminMenuTreeNodeResult extends AdminMenuResult {
|
||||
children: AdminMenuTreeNodeResult[]
|
||||
}
|
||||
|
||||
export interface AdminMenuTreeResult {
|
||||
items: AdminMenuTreeNodeResult[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export interface AdminHotelResult {
|
||||
id: string
|
||||
hotel_id: string
|
||||
@@ -199,6 +208,16 @@ export interface AdminMenuUpdateRequest {
|
||||
menu_status: string
|
||||
}
|
||||
|
||||
export interface AdminMenuTreeOrderItemRequest {
|
||||
menu_id: string
|
||||
parent_id?: string | null
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export interface AdminMenuTreeOrderUpdateRequest {
|
||||
items: AdminMenuTreeOrderItemRequest[]
|
||||
}
|
||||
|
||||
export interface AdminHotelCreateRequest {
|
||||
hotel_id: string
|
||||
hotel_name: string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,6 +90,194 @@
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.system-admin-menu-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(360px, 0.95fr) minmax(360px, 1.05fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.section-actions,
|
||||
.form-actions,
|
||||
.menu-tree-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-actions,
|
||||
.form-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.filter-grid .checkbox-field {
|
||||
min-height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
align-self: end;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.filter-grid .checkbox-field input {
|
||||
min-height: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.menu-warning-panel {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
border-color: rgba(245, 158, 11, 0.35);
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.menu-warning-panel ul {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0 0 0 18px;
|
||||
color: #92400e;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.menu-operation-panel {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-color: rgba(245, 158, 11, 0.28);
|
||||
background: var(--th-color-white);
|
||||
}
|
||||
|
||||
.menu-tree-section,
|
||||
.menu-detail-panel {
|
||||
align-self: start;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.menu-tree {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 14px 20px 20px;
|
||||
}
|
||||
|
||||
.menu-tree-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 54px;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
padding: 8px 10px;
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
.menu-tree-row:hover {
|
||||
border-color: rgba(37, 99, 235, 0.36);
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.07);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.menu-tree-row--selected {
|
||||
border-color: var(--th-color-blue-600);
|
||||
box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.menu-tree-row--muted {
|
||||
background: var(--th-color-slate-50);
|
||||
}
|
||||
|
||||
.menu-tree-row--warning {
|
||||
border-color: rgba(245, 158, 11, 0.45);
|
||||
}
|
||||
|
||||
.menu-tree-main {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, max-content) minmax(80px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.menu-tree-branch {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: 2px solid var(--th-color-blue-600);
|
||||
border-radius: 50%;
|
||||
background: var(--th-color-blue-50);
|
||||
}
|
||||
|
||||
.menu-tree-name {
|
||||
overflow: hidden;
|
||||
color: var(--th-color-slate-900);
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-tree-meta {
|
||||
color: var(--th-color-slate-500);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.status-pill--active {
|
||||
background: #dcfce7;
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.code-chip--warning {
|
||||
background: #ffedd5;
|
||||
color: #c2410c;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--th-color-slate-200);
|
||||
border-radius: var(--th-radius-sm);
|
||||
background: var(--th-color-white);
|
||||
color: var(--th-color-slate-700);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-button:hover:not(:disabled) {
|
||||
border-color: var(--th-color-blue-600);
|
||||
color: var(--th-color-blue-600);
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.menu-config-form {
|
||||
padding: 16px 20px 20px;
|
||||
}
|
||||
|
||||
.form-message--warning {
|
||||
color: #b45309;
|
||||
}
|
||||
|
||||
.form-message--error {
|
||||
color: var(--th-color-danger);
|
||||
}
|
||||
|
||||
.text-button,
|
||||
.row-action {
|
||||
border: 0;
|
||||
@@ -102,6 +290,12 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.text-button:disabled,
|
||||
.row-action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
min-height: 220px;
|
||||
display: grid;
|
||||
@@ -288,7 +482,8 @@ td small {
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.system-admin-grid {
|
||||
.system-admin-grid,
|
||||
.system-admin-menu-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -306,4 +501,18 @@ td small {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-actions,
|
||||
.form-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-tree-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.menu-tree-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user