task-252(admin.html观感对齐): 菜单管理列表列对齐(ID/菜单类型/上级菜单/创建时间)+ 类型中文映射

This commit is contained in:
2026-09-05 21:18:59 +08:00
parent 07fda4cf22
commit 8b4955d9b5
3 changed files with 105 additions and 5 deletions
@@ -8,6 +8,7 @@ import {
menuDeleteReason,
menuFormFromNode,
menuParentChangeReason,
menuTypeLabel,
nextSiblingSort,
siblingOrderAfterMove,
validateMenuManageForm,
@@ -23,6 +24,17 @@ const editingNode = ref<MenuManageNode | null>(null)
const menuForm = reactive<MenuManageForm>(createMenuForm(null, 0))
const parentOptions = computed(() => flattenMenuNodes(rows.value))
const dialogTitle = computed(() => (editingNode.value ? '编辑菜单' : '新增菜单'))
/** 节点 id -> 名称,供「上级菜单」列展示。 */
const nameById = computed(() => {
const map = new Map<number, string>()
for (const node of flattenMenuNodes(rows.value)) map.set(node.id, node.name)
return map
})
function parentNameOf(node: MenuManageNode): string {
if (node.parentId == null) return '—'
return nameById.value.get(node.parentId) ?? `#${node.parentId}`
}
async function loadMenus() {
loading.value = true
@@ -124,17 +136,29 @@ onMounted(loadMenus)
</div>
<el-card shadow="never">
<el-table v-loading="loading" :data="rows" row-key="id" default-expand-all stripe>
<el-table-column prop="name" label="菜单名称" min-width="220" />
<el-table-column prop="columnKey" label="权限标识" min-width="240" />
<el-table-column prop="routePath" label="路由" min-width="260" />
<el-table-column prop="sortOrder" label="排序" width="100" />
<el-table-column prop="id" label="ID" width="70" />
<el-table-column prop="name" label="菜单名称" min-width="200" />
<el-table-column label="菜单类型" width="110">
<template #default="{ row }">
<el-tag size="small" :type="row.menuType === 'app' ? 'info' : 'primary'">{{ menuTypeLabel(row.menuType) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="上级菜单" width="150">
<template #default="{ row }">
{{ parentNameOf(row) }}
</template>
</el-table-column>
<el-table-column prop="columnKey" label="权限标识" min-width="220" />
<el-table-column prop="routePath" label="路由" min-width="240" />
<el-table-column prop="createdAt" label="创建时间" min-width="170" />
<el-table-column prop="sortOrder" label="排序" width="90" />
<el-table-column label="移动" width="120">
<template #default="{ row }">
<el-button link type="primary" @click="moveMenuRow(row, -1)">上移</el-button>
<el-button link type="primary" @click="moveMenuRow(row, 1)">下移</el-button>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" :disabled="menuDeleteReason(row) !== undefined" @click="removeMenu(row)">删除</el-button>
@@ -14,9 +14,15 @@ export interface MenuManageNode {
menuType: string
routePath: string
sortOrder: number
createdAt?: string
children?: MenuManageNode[]
}
/** 菜单类型展示文案(对齐 admin 菜单类型语义)。 */
export function menuTypeLabel(type: string | null | undefined): string {
return (type || '').toLowerCase() === 'app' ? '应用菜单' : '后台菜单'
}
export interface MenuManageForm {
name: string
columnKey: string
@@ -75,6 +81,8 @@ export function parseMenuManageItem(raw: unknown): MenuManageNode | null {
}
const rootColumnKey = text(record.root_column_key)
if (rootColumnKey) node.rootColumnKey = rootColumnKey
const createdAt = text(record.created_at)
if (createdAt) node.createdAt = createdAt
return node
}
+68
View File
@@ -0,0 +1,68 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import { menuTypeLabel } from '../src/pages/account/menu-manage-model.ts'
// module 13 task 252:菜单管理页面对齐 admin panel-columns(列表列/类型/上级/创建时间/删除确认/排序)。
test('test_task_252_menus_list_normal_primary_path', () => {
const page = readSource('src/pages/account/MenusPage.vue')
assert.match(page, /菜单类型/, '需有“菜单类型”列')
assert.match(page, /menuTypeLabel/, '菜单类型展示走中文映射')
assert.match(page, /上级菜单/, '需有“上级菜单”列')
})
test('test_task_252_menus_list_normal_variant_input', () => {
// 正常变体:保留权限标识/路由/排序列,便于运维;删除确认文案含菜单名。
const page = readSource('src/pages/account/MenusPage.vue')
assert.match(page, /权限标识/, '需有权限标识列')
assert.match(page, /routePath/, '需有路由列')
assert.match(page, /确定删除菜单/, '删除确认需含“确定删除菜单”')
assert.match(page, /menuDeleteReason/, '删除需过前置校验')
})
test('test_task_252_menus_list_normal_repeated_operation_is_idempotent', () => {
// 类型映射幂等。
assert.equal(menuTypeLabel('admin'), '后台菜单')
assert.equal(menuTypeLabel('APP'), '应用菜单')
assert.equal(menuTypeLabel('app'), menuTypeLabel('app'))
})
test('test_task_252_menus_list_boundary_empty_input', () => {
// 边界:未知类型归后台菜单;创建/编辑标题区分。
assert.equal(menuTypeLabel(null), '后台菜单')
assert.equal(menuTypeLabel(undefined), '后台菜单')
const page = readSource('src/pages/account/MenusPage.vue')
assert.match(page, /新增菜单/, '页头新增菜单入口')
assert.match(page, /editingNode.*编辑菜单|编辑菜单/, '编辑态标题')
})
test('test_task_252_menus_list_boundary_single_item', () => {
// 边界:父菜单列对根节点给占位,不崩。
const page = readSource('src/pages/account/MenusPage.vue')
assert.match(page, /parentNameOf/, '上级菜单列经 parentNameOf 映射')
})
test('test_task_252_menus_list_boundary_limit_or_missing_field', () => {
// 边界上限:创建/编辑表单含名称/标识/父菜单/路由/排序。
const page = readSource('src/pages/account/MenusPage.vue')
for (const field of ['menuForm.name', 'menuForm.columnKey', 'menuForm.parentId', 'menuForm.routePath', 'menuForm.sortOrder']) {
assert.ok(page.includes(field), `表单需含 ${field}`)
}
})
test('test_task_252_menus_list_invalid_input_rejected', () => {
// 异常:有子节点禁删、父级环校验文案在模型内。
const model = readSource('src/pages/account/menu-manage-model.ts')
assert.match(model, /请先删除该菜单下的子菜单/)
assert.match(model, /不能选择自己的子菜单作为父菜单/)
assert.match(model, /created_at/, '节点解析需含创建时间')
})
test('test_task_252_menus_list_dependency_failure_returns_actionable_message', () => {
// 依赖:排序提交走 /column/reorder,失败有「排序保存失败」提示。
const api = readSource('src/pages/account/menu-manage-api.ts')
assert.match(api, /column\/reorder/, '排序走 column/reorder')
const page = readSource('src/pages/account/MenusPage.vue')
assert.match(page, /排序保存失败/, '排序失败提示“排序保存失败”')
})