feat(system): 完成前端系统管理(布局框架、三个管理页面、e2e)
- BasicLayout:侧边栏/顶栏/面包屑/移动端抽屉;菜单从路由表派生; 路由守卫新增 requiresAdmin 层(菜单隐藏 + 直连 URL 弹回) - 用户/角色/日志三个页面:分页请求序号防竞态、确认动作流抽 useConfirmAction、 时间范围配 default-time、日志详情按需懒加载并缓存 - 空 catch 统一改 handledError 显式声明;词表/选项常量集中(与后端对齐), 「停用」文案全站统一为「禁用」 - 品牌色提升为全局 token,Element Plus 主色指向赭金 - e2e:公共工具抽 common.py;系统管理 13 项(含详情懒加载脱敏断言), 管理员密码改 E2E_ADMIN_PASSWORD 环境变量;login_flow 8 项回归通过 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
c83287819a
commit
8355e0cc7d
26 files changed
+2785
-180
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { ROLE_ADMIN } from '@/utils/constants'
|
||||
|
||||
/** 菜单节点:从路由表派生,避免菜单标题与页面标题两处维护漂移。 */
|
||||
export interface MenuNode {
|
||||
path: string
|
||||
title: string
|
||||
order: number
|
||||
group?: string
|
||||
}
|
||||
|
||||
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 isAdmin = computed(() => userStore.roles.includes(ROLE_ADMIN))
|
||||
|
||||
const menuNodes = computed<MenuNode[]>(() =>
|
||||
router
|
||||
.getRoutes()
|
||||
.filter((record) => Boolean(record.meta.title) && record.meta.menu !== false)
|
||||
.filter((record) => !record.meta.requiresAdmin || isAdmin.value)
|
||||
.map((record) => ({
|
||||
path: record.path,
|
||||
title: record.meta.title ?? '',
|
||||
order: record.meta.order ?? 99,
|
||||
group: record.meta.group,
|
||||
}))
|
||||
.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 }
|
||||
}
|
||||
Reference in new issue
Block a user