1. 删除大量旧的示例页面、组件示例和静态菜单配置 2. 新增菜单扁平数组转树形结构工具函数 3. 重构菜单加载逻辑,改为从后端动态获取并格式化 4. 新增全局权限检查方法和自定义权限指令 5. 优化侧边栏菜单路由跳转逻辑,自动跳转第一个有权限的子页面 6. 移除路由中对旧demo模块的引用
19 KiB
Menu 权限迁移方案
参照项目:
D:\code\company\SCTMES_MES_V5\vue-app(以下简称"源项目")
一、源项目(SCTMES)Menu 权限架构
1.1 后端接口
GET system_settings/menu_configuration/menu/all
API 文件:src/api/system_settings/menu_configuration/menu.js
返回当前用户可见的扁平数组,字段如下:
| 字段 | 类型 | 说明 |
|---|---|---|
menu_id |
number | 主键,用于构建父子关系 |
parent_id |
number | 父节点 ID,0 表示顶栏根节点 |
url |
string | 前端路由路径,如 /system_settings/user_management/user |
name |
string | 菜单显示名称 |
icon |
string | 图标名(FontAwesome) |
is_navi |
number | 是否作为导航菜单展示(1=是) |
status |
number | 启用/禁用 |
type |
string | 菜单类型 |
params |
string | 额外参数 |
sort |
number | 排序 |
remark |
string | 备注 |
1.2 菜单数据全链路
登录成功 → account.login() → dispatch('load')
│
▼
account.load()
│
...加载 user/theme/transition/page/menu/size/color...
│
▼
dispatch('d2admin/menu/sourceDataLoad')
│
┌────────────┴────────────┐
│ │
localStorage localStorage 为空
有缓存 且有 token
│ │
▼ ▼
直接读缓存 GET /menu/all 后端接口
│
▼
写入 localStorage 缓存
│
└────────────┬────────────┘
│
▼
menu.install(this, sourceData)
Store 文件:src/store/modules/d2admin/modules/menu.js
state: {
header: [],
aside: [],
asideCollapse: setting.menu.asideCollapse,
asideTransition: setting.menu.asideTransition,
authKey: {}, // ← 权限字典 { '/path': '菜单名', ... }
sourceData: [] // ← 菜单源数据(来自后端或本地缓存)
}
1.3 sourceDataLoad action 核心逻辑
async sourceDataLoad({ state, dispatch }) {
// 1. 优先从 localStorage 读取缓存
state.sourceData = await dispatch('d2admin/db/get', {
dbName: 'database',
path: '$menu.sourceData',
defaultValue: [],
user: true
}, { root: true })
// 2. 缓存为空且已登录 → 调后端获取
if (!state.sourceData.length && util.cookies.get('token')) {
const res = await getMenuAll(null)
state.sourceData = res.data || []
await dispatch('d2admin/db/set', {
dbName: 'database',
path: '$menu.sourceData',
value: state.sourceData,
user: true
}, { root: true })
}
// 3. 处理为 header/aside/authKey
menu.install(this, state.sourceData)
}
1.4 menu.install() — 扁平数据 → 菜单树 + 权限字典
export default {
install(vm, source) {
// 1. 构建权限字典 { '/path': '菜单名' }
vm.commit('d2admin/menu/headerAuth', source)
// 2. 扁平数据转为 {header: [], aside: []} 树结构
const { header, aside } = getMenuData(source)
// 3. 写入 store → 触发菜单渲染
vm.commit('d2admin/menu/headerSet', header)
vm.commit('d2admin/menu/asideSet', aside)
}
}
getMenuData() 核心流程:
source (扁平数组)
│
├── 过滤: is_navi !== 1 的跳过
│
├── parent_id === 0 → 推入 header[]
│
└── 全部节点 → 推入 aside[]
│
▼
util.formatDataToTree(aside) { menu_id ↔ parent_id }
│
▼
树形结构 aside
headerAuth mutation:遍历 source,将每个有 url 的节点写入 authKey[url] = name:
headerAuth(state, source) {
let auth = {}
source.forEach(value => {
if (value.url) {
auth[value.url] = value.name
}
})
state.authKey = auth
}
1.5 权限控制系统
$permission() 方法 — 定义在 src/plugin/d2admin/index.js:
Vue.prototype.$permission = (value, type = 'menu') => {
let path = ''
const auth = store.state.d2admin.menu.authKey
switch (type) {
case 'menu': // 直接用 URL 查
path = value
break
case 'router': // 路由 name → URL
path = value.name.replace(/-/g, '/')
path.slice(0, 1) !== '/' && (path = '/' + path)
break
}
return !!(path && Object.prototype.hasOwnProperty.call(auth, path))
}
v-permission 指令 — 定义在 src/main.js:
Vue.directive('permission', {
bind: (el, binding) => {
if (!Vue.prototype.$permission(binding.value)) {
el.parentNode ? el.parentNode.removeChild(el) : el.style.display = 'none'
}
}
})
使用示例:
<el-button v-permission="'/system_settings/user_management/user'">新增用户</el-button>
1.6 菜单导航权限过滤
Menu Mixin — src/layout/header-aside/components/mixin/menu.js:
点击顶栏一级菜单时,不直接跳转,而是查找第一个有权限的子路由:
getRouterAuthPath(index, indexPath) {
// 子路由直接访问
if (index === '/index' || !indexPath || indexPath.length > 1) {
this.$router.push({ path: index })
return
}
// 查找一级路由下第一个有权限的子路由
let router = null
for (const value of frameInRoutes) {
if (value.path === index) {
router = value.children
break
}
}
if (!router) {
this.$router.push({ path: index })
return
}
for (const value of router) {
const newPath = index + '/' + value.path
if (this.$permission(newPath)) {
this.$router.push({ path: newPath })
break
}
}
}
1.7 路由 — 全部静态注册
路由在 src/router/routes.js 中全部静态定义,不使用 addRoute。
路由守卫(src/router/index.js)仅判断 token 是否存在,不校验具体路由权限。权限控制在菜单渲染和菜单点击时完成。
1.8 main.js 启动流程
created() {
// 仅初始化页面池,不再设置静态菜单
this.$store.commit('d2admin/page/init', frameInRoutes)
// 静态菜单设置已注释:
// this.$store.commit('d2admin/menu/headerSet', menuHeader)
// this.$store.commit('d2admin/search/init', menuHeader)
},
mounted() {
this.$store.dispatch('d2admin/account/load') // → sourceDataLoad → 获取菜单
},
watch: {
// $route.matched 切换侧边栏的 watch 已注释
}
二、mes-ui 当前现状 vs 目标
| 模块 | mes-ui 现状 | 源项目(目标) |
|---|---|---|
| 菜单数据来源 | src/menu/modules/*.js 静态硬编码 |
后端 GET /menu/all + localStorage 缓存 |
sourceDataLoad |
仅 localStorage 读取,不参与菜单构建 | localStorage 兜底 + 调后端 → menu.install() |
| 权限字典 | 无 | authKey: { '/path': 'name' } |
| 菜单树构建 | 编译时 supplementPath() |
运行时 getMenuData() + formatDataToTree() |
| 权限检查 | 无 | $permission() + v-permission 指令 |
| 菜单点击 | 直接 $router.push |
getRouterAuthPath() 查找首个有权限子路由 |
| main.js 菜单初始化 | created 中 headerSet(menuHeader) |
注释掉静态菜单,由 mounted 中的 load() 触发 |
| 路由注册 | 全部静态 | 全部静态(一致) |
| 路由守卫 | 仅 token 检查 | 仅 token 检查(一致) |
三、迁移计划
阶段 1️⃣:约定后端接口格式
后端接口:GET /api/menu/all(或复用源项目的 system_settings/menu_configuration/menu/all)
返回格式(扁平数组):
{
"code": 0,
"data": [
{ "menu_id": 1, "parent_id": 0, "url": "/index", "name": "首页", "icon": "home", "is_navi": 1 },
{ "menu_id": 2, "parent_id": 0, "url": "/production_master_data", "name": "生产主数据", "icon": "cogs", "is_navi": 1 },
{ "menu_id": 3, "parent_id": 2, "url": "/production_master_data/factory_model/factory_area", "name": "工厂区域", "icon": "map-marker", "is_navi": 1 }
]
}
阶段 2️⃣:新增/改造 API 层
新增 src/api/menu.js(参照源项目 menu.js):
import { request } from '@/api/_service'
export function getMenuAll(data) {
return request({
url: 'menu/all',
method: 'get',
params: {
method: 'system_settings_menu_configuration_menu_all',
platform: 'background',
...data
}
})
}
阶段 3️⃣:改造 menu.js Store
改造 src/store/modules/d2admin/modules/menu.js
| 改动 | 内容 |
|---|---|
| state | 新增 authKey: {} |
sourceDataLoad action |
改为:localStorage 兜底 → 调后端 → menu.install() |
| mutations | 新增 headerAuth(state, source) → 构建 authKey 字典 |
// 关键变更 — sourceDataLoad:
async sourceDataLoad({ state, commit, dispatch }) {
// 1. 先读 localStorage 缓存
state.sourceData = await dispatch('d2admin/db/get', {
dbName: 'database',
path: '$menu.sourceData',
defaultValue: [],
user: true
}, { root: true })
// 2. 缓存为空且已登录 → 调后端
if (!state.sourceData.length && util.cookies.get('token')) {
const res = await getMenuAll()
state.sourceData = res || []
await dispatch('d2admin/db/set', {
dbName: 'database',
path: '$menu.sourceData',
value: state.sourceData,
user: true
}, { root: true })
}
// 3. 构建菜单树 + 权限字典
menuUtil.install(this, state.sourceData)
}
阶段 4️⃣:改造 src/menu/index.js — 从静态导出变为工具模块
将 src/menu/index.js 从"导出静态菜单配置"改为"菜单处理工具"(参照源项目):
import util from '@/libs/util'
function getMenuData(arr) {
let tree = { header: [], aside: [] }
arr.forEach(value => {
if (!value.is_navi) return // 过滤非导航菜单
const menuItem = {
path: value.url,
title: value.name,
icon: value.icon,
type: value.type,
params: value.params
}
if (value.parent_id === 0) {
tree.header.push({ ...menuItem })
}
menuItem.menu_id = value.menu_id
menuItem.parent_id = value.parent_id
tree.aside.push(menuItem)
})
// 扁平数组转树
tree.aside = util.formatDataToTree(tree.aside)
return tree
}
export default {
install(vm, source) {
vm.commit('d2admin/menu/headerAuth', source)
const { header, aside } = getMenuData(source)
vm.commit('d2admin/menu/headerSet', header)
vm.commit('d2admin/menu/asideSet', aside)
}
}
原有的静态菜单模块(src/menu/modules/demo-*.js)可保留但不再被引用,后续按需清理。
阶段 5️⃣:增加权限控制基础设施
5.1 $permission 方法
改造 src/plugin/d2admin/index.js,新增:
Vue.prototype.$permission = (value, type = 'menu') => {
let path = ''
const auth = store.state.d2admin.menu.authKey
switch (type) {
case 'menu':
path = value
break
case 'router':
path = value.name.replace(/-/g, '/')
path.slice(0, 1) !== '/' && (path = '/' + path)
break
}
return !!(path && Object.prototype.hasOwnProperty.call(auth, path))
}
5.2 v-permission 指令
改造 src/main.js,新增:
Vue.directive('permission', {
bind: (el, binding) => {
if (!Vue.prototype.$permission(binding.value)) {
el.parentNode ? el.parentNode.removeChild(el) : el.style.display = 'none'
}
}
})
5.3 菜单点击权限过滤
改造 src/layout/header-aside/components/mixin/menu.js,增加 getRouterAuthPath 方法:
import { frameInRoutes } from '@/router/routes'
methods: {
handleMenuSelect(index, indexPath) {
if (/^d2-menu-empty-\d+$/.test(index) || index === undefined) {
this.$message.warning('临时菜单')
} else if (/^https:\/\/|http:\/\//.test(index)) {
util.open(index)
} else {
this.getRouterAuthPath(index, indexPath)
}
},
getRouterAuthPath(index, indexPath) {
if (index === '/index' || !indexPath || indexPath.length > 1) {
this.$router.push({ path: index })
return
}
let router = null
for (const value of frameInRoutes) {
if (value.path === index) {
router = value.children
break
}
}
if (!router) {
this.$router.push({ path: index })
return
}
for (const value of router) {
const newPath = index + '/' + value.path
if (this.$permission(newPath)) {
this.$router.push({ path: newPath })
break
}
}
}
}
阶段 6️⃣:改造 main.js 启动流程
改造 src/main.js:
created() {
this.$store.commit('d2admin/page/init', frameInRoutes)
// 注释掉静态菜单设置:
// this.$store.commit('d2admin/menu/headerSet', menuHeader)
// this.$store.commit('d2admin/search/init', menuHeader)
},
mounted() {
this.$store.commit('d2admin/releases/versionShow')
this.$store.dispatch('d2admin/account/load') // → sourceDataLoad → 获取后端菜单
this.$store.commit('d2admin/ua/get')
this.$store.dispatch('d2admin/fullscreen/listen')
},
watch: {
// 注释掉 $route.matched 侧边栏切换(由 store.aside 直接驱动)
// '$route.matched': { ... }
}
阶段 7️⃣:增加 util.formatDataToTree 工具函数
改造 src/libs/util.js,新增:
util.formatDataToTree = (data, key = 'menu_id', pid = 'parent_id') => {
if (!data || data.length <= 0) return []
let map = {}
data.forEach(value => { map[value[key]] = { ...value } })
let tree = []
data.forEach(item => {
const id = item[key]
if (map[id][pid] && map[map[id][pid]]) {
if (!map[map[id][pid]].children) {
map[map[id][pid]].children = []
}
map[map[id][pid]].children.push(map[id])
} else {
tree.push(map[id])
}
})
return tree
}
四、影响的文件清单
| 文件 | 改动类型 | 说明 |
|---|---|---|
src/api/menu.js |
新增 | 菜单 API(getMenuAll) |
src/store/modules/d2admin/modules/menu.js |
改造 | sourceDataLoad 加后端调用 + headerAuth mutation + authKey state |
src/menu/index.js |
重写 | 从静态配置改为 install() 工具模块 |
src/plugin/d2admin/index.js |
改造 | 新增 $permission 方法 |
src/main.js |
改造 | 注释静态菜单设置;新增 v-permission 指令 |
src/layout/header-aside/components/mixin/menu.js |
改造 | 增加 getRouterAuthPath 权限过滤 |
src/libs/util.js |
改造 | 新增 formatDataToTree 工具函数 |
src/menu/modules/demo-*.js |
后续可删 | 静态菜单数据不再使用,单独清理 |
五、完整时序图
用户登录
│
▼
login/index.vue → dispatch('d2admin/account/login')
│
▼
account.js login action
├── loginAdminUser() → 后端登录接口
├── util.cookies.set() → 存 token/uuid
├── dispatch('d2admin/user/set') → 存用户信息
└── dispatch('load')
│
├── d2admin/user/load → 加载用户名
├── d2admin/theme/load → 加载主题
├── d2admin/transition/load → 加载过渡效果
├── d2admin/page/openedLoad → 加载标签页
├── d2admin/menu/asideLoad → 加载侧边栏收起设置
├── d2admin/size/load → 加载全局尺寸
├── d2admin/color/load → 加载颜色设置
└── d2admin/menu/sourceDataLoad
│
├── 先读 localStorage 缓存
├── 缓存空 → GET /api/menu/all 后端接口
├── 写入 localStorage 缓存
└── menu.install(this, sourceData)
├── headerAuth → 构建 authKey 权限字典
├── getMenuData → 扁平 → {header, aside} 树
├── headerSet → 顶栏菜单渲染
└── asideSet → 侧边栏菜单渲染
│
▼
跳转到首页 → 菜单已按权限渲染
六、注意事项
- 路由仍是静态注册的:mes-ui 不需要改造成动态
addRoute,路由全部在router/routes.js中静态定义即可,权限通过菜单显示/隐藏 +getRouterAuthPath控制。 - 不要删除源项目代码:源项目在
D:\code\company\SCTMES_MES_V5\vue-app,仅作为参照,不对其做任何修改。 - localStorage 缓存是离线兜底:首次登录调后端,后续从缓存读取;注销时清空缓存。
blockcookie:已在登录页mounted中设为'false',注销时设为'true',用于控制错误日志是否弹出提示。