- 引入 @element-plus/icons-vue 官方图标库,菜单项按模块配图标,消除 纯文字导航的辨识成本(新增依赖已评估:Element Plus 官方同源维护) - AppMenu/BasicLayout/router 同步:图标渲染、菜单结构与路由 meta 对齐 - index.css 视觉系统化:品牌色、语义三色(ok/warn/danger 及其 wash 层次)、 形体与材质(圆角、缓动、阴影、等宽字体)统一为 CSS 变量,并覆盖 Element Plus 主色 - 登录页、业务页、结算页、工作台、审批页等视图随新变量适配 验证:pnpm type-check 与 pnpm build 均通过;后端接口契约未变。 Co-Authored-By: Claude Code <noreply@anthropic.com>
71 lines
2.2 KiB
TypeScript
71 lines
2.2 KiB
TypeScript
import { computed } from 'vue'
|
||
import { useRouter } from 'vue-router'
|
||
import type { Component } from 'vue'
|
||
|
||
import { menuIcon } from '@/components/layout/menuIcons'
|
||
import { canAccessRoute, isMenuRecord } from '@/router/access'
|
||
import { useUserStore } from '@/store/user'
|
||
|
||
/** 菜单节点:从路由表派生,避免菜单标题与页面标题两处维护漂移。 */
|
||
export interface MenuNode {
|
||
path: string
|
||
title: string
|
||
order: number
|
||
group?: string
|
||
/** 图标组件(由 meta.icon 语义 key 解析;未配置则为 undefined) */
|
||
icon?: Component
|
||
}
|
||
|
||
export interface MenuGroup {
|
||
name: string
|
||
items: MenuNode[]
|
||
}
|
||
|
||
/**
|
||
* 侧边菜单数据。
|
||
*
|
||
* 规则:
|
||
* - 取路由表中带 `meta.title` 且未被 `meta.menu === false` 排除的记录
|
||
* - `meta.requiresAdmin` 的项仅管理员可见(与路由守卫双保险,隐藏入口在前)
|
||
* - `meta.group` 相同的项聚合为一个子菜单;无 group 的为顶层菜单项
|
||
* - 按 `meta.order` 排序
|
||
*/
|
||
export function useAppMenu() {
|
||
const router = useRouter()
|
||
const userStore = useUserStore()
|
||
|
||
const userRoles = computed(() => userStore.roles)
|
||
|
||
const menuNodes = computed<MenuNode[]>(() =>
|
||
router
|
||
.getRoutes()
|
||
// 准入规则与路由守卫、落地页解析共用,避免三处漂移
|
||
.filter((record) => isMenuRecord(record) && canAccessRoute(record, userRoles.value))
|
||
.map((record) => ({
|
||
path: record.path,
|
||
title: record.meta.title ?? '',
|
||
order: record.meta.order ?? 99,
|
||
group: record.meta.group,
|
||
icon: menuIcon(record.meta.icon),
|
||
}))
|
||
.sort((a, b) => a.order - b.order),
|
||
)
|
||
|
||
/** 无分组的顶层菜单项 */
|
||
const topLevelMenus = computed(() => menuNodes.value.filter((node) => !node.group))
|
||
|
||
/** 分组菜单(按 group 名聚合,组内保持 order 顺序) */
|
||
const groupedMenus = computed<MenuGroup[]>(() => {
|
||
const groups = new Map<string, MenuNode[]>()
|
||
for (const node of menuNodes.value) {
|
||
if (!node.group) continue
|
||
const items = groups.get(node.group) ?? []
|
||
items.push(node)
|
||
groups.set(node.group, items)
|
||
}
|
||
return [...groups.entries()].map(([name, items]) => ({ name, items }))
|
||
})
|
||
|
||
return { topLevelMenus, groupedMenus }
|
||
}
|