task-54(账号/权限页面): 实现后台菜单列表加载

menu-manage-model.ts 增 buildMenuManageTree 按 parentId 组装展示树(同级按 sort+name);
menu-manage-api.ts GET /api/admin/permission-menus(menuType=admin) 扁平拉取并建树;
MenusPage 去内联 http,改走 adapter 展示默认展开树形菜单。8 用例全过,395 单测 + build 绿。
This commit is contained in:
2026-09-05 15:16:25 +08:00
parent 5d9019a40d
commit ffb16bea9a
4 changed files with 183 additions and 0 deletions
@@ -0,0 +1,47 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { loadMenuManageTree } from './menu-manage-api'
import type { MenuManageNode } from './menu-manage-model'
const loading = ref(false)
const rows = ref<MenuManageNode[]>([])
async function loadMenus() {
loading.value = true
try {
rows.value = await loadMenuManageTree()
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '菜单列表加载失败')
} finally {
loading.value = false
}
}
onMounted(loadMenus)
</script>
<template>
<div class="page-stack">
<div class="page-heading">
<div>
<h2>菜单管理</h2>
<p>维护后台菜单树和页面路由权限粒度仅到菜单/页面</p>
</div>
<el-button type="primary">新增菜单</el-button>
</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 label="操作" width="140">
<template #default>
<el-button link type="primary">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
@@ -0,0 +1,21 @@
/** 菜单管理列表加载适配(任务 54):GET /api/admin/permission-menus(admin) → 展示树。 */
import { http } from '@/api/http'
import {
ADMIN_MENU_TYPE,
buildMenuManageTree,
parseMenuManageList,
type MenuManageNode,
} from './menu-manage-model'
export const MENUS_ENDPOINT = '/api/admin/permission-menus'
/** 拉取扁平可编辑菜单节点。 */
export async function fetchMenuManageNodes(): Promise<MenuManageNode[]> {
const { data } = await http.get<unknown>(MENUS_ENDPOINT, { params: { menuType: ADMIN_MENU_TYPE } })
return parseMenuManageList(data)
}
/** 拉取并按 parentId 组装成展示树。 */
export async function loadMenuManageTree(): Promise<MenuManageNode[]> {
return buildMenuManageTree(await fetchMenuManageNodes())
}
@@ -14,6 +14,7 @@ export interface MenuManageNode {
menuType: string
routePath: string
sortOrder: number
children?: MenuManageNode[]
}
export interface MenuManageForm {
@@ -86,6 +87,37 @@ export function parseMenuManageList(payload: unknown): MenuManageNode[] {
.filter((node): node is MenuManageNode => node !== null)
}
/** 按 parentId 组装展示树;同级按 (sortOrder,name) 升序,父缺失按根处理。 */
export function buildMenuManageTree(items: MenuManageNode[]): MenuManageNode[] {
const nodes = new Map<number, MenuManageNode>()
for (const item of items) {
nodes.set(item.id, { ...item, children: [] })
}
const roots: MenuManageNode[] = []
for (const item of items) {
const node = nodes.get(item.id)
if (!node) continue
const parent = item.parentId === null ? undefined : nodes.get(item.parentId)
if (parent) {
parent.children!.push(node)
} else {
roots.push(node)
}
}
const order = (a: MenuManageNode, b: MenuManageNode) =>
a.sortOrder - b.sortOrder || a.name.localeCompare(b.name)
const sortRecursive = (nodes: MenuManageNode[]) => {
nodes.sort(order)
for (const node of nodes) node.children?.length && sortRecursive(node.children)
}
sortRecursive(roots)
for (const item of items) {
const node = nodes.get(item.id)
if (node && !node.children?.length) delete node.children
}
return roots
}
/** 新建/编辑表单空值工厂:默认菜单类型为 admin、排序 0、根父节点。 */
export function emptyMenuManageForm(): MenuManageForm {
return { name: '', columnKey: '', parentId: null, menuType: ADMIN_MENU_TYPE, routePath: '', sortOrder: 0 }