fix: 统一前端菜单类型判断
This commit is contained in:
@@ -42,3 +42,33 @@
|
|||||||
## Concerns
|
## Concerns
|
||||||
|
|
||||||
- 全仓类型检查仍因已确认的 827 条基线错误返回退出码 2;本任务四个改动文件错误为 0,生产构建成功。
|
- 全仓类型检查仍因已确认的 827 条基线错误返回退出码 2;本任务四个改动文件错误为 0,生产构建成功。
|
||||||
|
|
||||||
|
## 审查修复
|
||||||
|
|
||||||
|
### 实现
|
||||||
|
|
||||||
|
- 将 `ServerMenuItem.type` 收紧为必填 `number | string`。
|
||||||
|
- 新增统一的 `isMenuPermission` predicate,仅接受数值 `1` 或字符串 `"1"`。
|
||||||
|
- AppStore 构树前过滤、顶层路由转换和递归子路由转换统一调用该 predicate;数值/字符串 `2`、缺失 `type` 和其他值均严格排除。
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
1. `pnpm exec vue-tsc --noEmit --incremental false`
|
||||||
|
- 结果:全仓保持 827 条基线错误,任务四个改动文件错误 0。
|
||||||
|
2. `pnpm build -- --outDir C:\Users\27105\AppData\Local\Temp\front-kb-task3-review-build-20260721`
|
||||||
|
- 结果:退出码 0,Vite 7.3.1 成功转换 9055 个模块并完成生产构建。
|
||||||
|
3. 内联 Node + 最小 Vite SSR 加载器直接调用实际 `isMenuPermission`、`buildTree` 和 `transformMenuToRoutes`。
|
||||||
|
- 覆盖:数值 `1`、字符串 `"1"`、数值 `2`、字符串 `"2"`、缺失 `type`,并同时验证顶层和递归子路由。
|
||||||
|
- 结果:`predicate=true,true,false,false,false`、完整权限码 5 个、菜单权限 2 个、顶层路由 2 个、递归子路由 2 个、`empty_path=false`、`rejected_in_routes=false`,退出码 0。
|
||||||
|
4. `git diff --check`
|
||||||
|
- 结果:退出码 0。
|
||||||
|
|
||||||
|
### 自审
|
||||||
|
|
||||||
|
- `type` 在接口层必填,运行时仅 `1` 和 `"1"` 能进入菜单链路;缺失值没有兼容放行。
|
||||||
|
- 三个过滤位置共享同一 predicate,不存在数值/字符串判断漂移。
|
||||||
|
- 权限码仍在过滤前完整保存,未扩展无关 API 或路由重构,源码冻结后未再修改。
|
||||||
|
|
||||||
|
### Concerns
|
||||||
|
|
||||||
|
- 工具策略拦截了临时构建目录的原生 PowerShell 删除命令,未采用替代删除方式;待清理路径为 `C:\Users\27105\AppData\Local\Temp\front-kb-task3-review-build-20260721`。
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface ServerMenuItem extends TreeNodeBase {
|
|||||||
title?: string // 菜单标题
|
title?: string // 菜单标题
|
||||||
title_en?: string // 英文标题
|
title_en?: string // 英文标题
|
||||||
code?: string // 菜单编码
|
code?: string // 菜单编码
|
||||||
type?: number // 1:菜单,2:按钮
|
type: number | string // 1:菜单,2:按钮
|
||||||
menu_path?: string // 菜单路径,如 '/overview'
|
menu_path?: string // 菜单路径,如 '/overview'
|
||||||
component?: string // 组件路径,如 'ops/pages/overview'
|
component?: string // 组件路径,如 'ops/pages/overview'
|
||||||
icon?: string
|
icon?: string
|
||||||
@@ -29,6 +29,10 @@ export interface ServerMenuItem extends TreeNodeBase {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMenuPermission(item: Pick<ServerMenuItem, 'type'>): boolean {
|
||||||
|
return item.type === 1 || item.type === '1'
|
||||||
|
}
|
||||||
|
|
||||||
// 预定义的视图模块映射(用于 Vite 动态导入)
|
// 预定义的视图模块映射(用于 Vite 动态导入)
|
||||||
const viewModules = import.meta.glob('@/views/**/*.vue')
|
const viewModules = import.meta.glob('@/views/**/*.vue')
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@ export function transformMenuToRoutes(menuItems: ServerMenuItem[]): AppRouteReco
|
|||||||
const routes: AppRouteRecordRaw[] = []
|
const routes: AppRouteRecordRaw[] = []
|
||||||
|
|
||||||
for (const item of menuItems) {
|
for (const item of menuItems) {
|
||||||
if (item.type !== 1) continue
|
if (!isMenuPermission(item)) continue
|
||||||
|
|
||||||
// 根据 is_full 决定如何设置 component
|
// 根据 is_full 决定如何设置 component
|
||||||
let routeComponent: AppRouteRecordRaw['component']
|
let routeComponent: AppRouteRecordRaw['component']
|
||||||
@@ -200,44 +204,42 @@ function transformChildRoutes(
|
|||||||
parentPath?: string,
|
parentPath?: string,
|
||||||
parentIsFull?: boolean
|
parentIsFull?: boolean
|
||||||
): AppRouteRecordRaw[] {
|
): AppRouteRecordRaw[] {
|
||||||
return children
|
return children.filter(isMenuPermission).map((child) => {
|
||||||
.filter((child) => child.type === 1)
|
const childFullPath = String(child.menu_path ?? child.path ?? '').trim()
|
||||||
.map((child) => {
|
|
||||||
const childFullPath = String(child.menu_path ?? child.path ?? '').trim()
|
|
||||||
|
|
||||||
// 已配置 component 的菜单绝不覆盖;仅对许可页做路径/code 兜底,避免 includes 误匹配
|
// 已配置 component 的菜单绝不覆盖;仅对许可页做路径/code 兜底,避免 includes 误匹配
|
||||||
let componentPath = child.component || parentComponent
|
let componentPath = child.component || parentComponent
|
||||||
if (!child.component && (isLicenseCenterMenuPath(childFullPath) || child.code === 'LicenseCenter')) {
|
if (!child.component && (isLicenseCenterMenuPath(childFullPath) || child.code === 'LicenseCenter')) {
|
||||||
componentPath = LICENSE_CENTER_VIEW
|
componentPath = LICENSE_CENTER_VIEW
|
||||||
}
|
}
|
||||||
|
|
||||||
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
const relativePath = extractRelativePath(childFullPath, parentPath || '')
|
||||||
|
|
||||||
const route: AppRouteRecordRaw = {
|
const route: AppRouteRecordRaw = {
|
||||||
path: relativePath,
|
path: relativePath,
|
||||||
name: child.title || child.name || `menu_${child.id}`,
|
name: child.title || child.name || `menu_${child.id}`,
|
||||||
meta: {
|
meta: {
|
||||||
...child,
|
...child,
|
||||||
locale: child.locale || child.title,
|
locale: child.locale || child.title,
|
||||||
requiresAuth: child.requiresAuth !== false,
|
requiresAuth: child.requiresAuth !== false,
|
||||||
roles: child.roles,
|
roles: child.roles,
|
||||||
hideInMenu: child.hideInMenu || child.hide_menu,
|
hideInMenu: child.hideInMenu || child.hide_menu,
|
||||||
},
|
},
|
||||||
component: componentPath ? loadViewComponent(componentPath) : () => import('@/views/redirect/index.vue'),
|
component: componentPath ? loadViewComponent(componentPath) : () => import('@/views/redirect/index.vue'),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归处理子菜单的子菜单
|
// 递归处理子菜单的子菜单
|
||||||
if (child.children && child.children.length > 0) {
|
if (child.children && child.children.length > 0) {
|
||||||
route.children = transformChildRoutes(
|
route.children = transformChildRoutes(
|
||||||
child.children,
|
child.children,
|
||||||
child.component || parentComponent,
|
child.component || parentComponent,
|
||||||
childFullPath, // 传递当前子菜单的完整路径作为下一层的父路径
|
childFullPath, // 传递当前子菜单的完整路径作为下一层的父路径
|
||||||
child.is_full || parentIsFull // 传递 is_full 标志
|
child.is_full || parentIsFull // 传递 is_full 标志
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return route
|
return route
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本地菜单数据 - 接口未准备好时使用
|
// 本地菜单数据 - 接口未准备好时使用
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
|
|||||||
import type { RouteRecordNormalized } from 'vue-router'
|
import type { RouteRecordNormalized } from 'vue-router'
|
||||||
import defaultSettings from '@/config/settings.json'
|
import defaultSettings from '@/config/settings.json'
|
||||||
import { userPmn } from '@/api/module/user'
|
import { userPmn } from '@/api/module/user'
|
||||||
import { transformMenuToRoutes, type ServerMenuItem } from '@/router/menu-data'
|
import { isMenuPermission, transformMenuToRoutes, type ServerMenuItem } from '@/router/menu-data'
|
||||||
import { buildTree } from '@/utils/tree'
|
import { buildTree } from '@/utils/tree'
|
||||||
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
import SafeStorage, { AppStorageKey } from '@/utils/safeStorage'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
@@ -61,7 +61,7 @@ const useAppStore = defineStore('app', {
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
const menuPermissions = permissions.filter((permission) => permission.type === 1)
|
const menuPermissions = permissions.filter(isMenuPermission)
|
||||||
|
|
||||||
// 使用 buildTree 将扁平数据构建为树结构
|
// 使用 buildTree 将扁平数据构建为树结构
|
||||||
const treeResult = buildTree(menuPermissions, {
|
const treeResult = buildTree(menuPermissions, {
|
||||||
|
|||||||
Reference in New Issue
Block a user