菜单路由
# 1. 路由
项目基于 unibest (opens new window) 框架,使用约定式路由,页面文件放在 src/pages 或 src/pages-xxx 目录下,会自动生成路由配置。
官方文档
详细的路由配置和使用方式,请参考:《unibest 官方文档 —— uni 插件》 (opens new window)
# 1.1 路由配置
使用 definePage 宏配置页面信息:
<script lang="ts" setup>
definePage({
style: {
navigationStyle: 'custom', // 自定义导航栏
navigationBarTitleText: '', // 页面标题
},
excludeLoginPath: false, // 是否需要登录,默认为 false(一般情况下,不用添加)
})
</script>
# 1.2 路由跳转
① uniapp 提供了多种路由跳转方式,可见 《uniapp 官方文档 —— 页面和路由》 (opens new window) 文档。
② 项目封装了增强的返回方法 navigateBackPlus,当不存在上一页时会跳转到首页:
import { navigateBackPlus } from '@/utils'
// 返回上一页,如果不存在则跳转到首页
navigateBackPlus()
// 返回上一页,如果不存在则跳转到指定页面
navigateBackPlus('/pages-system/user/index')
# 1.3 登录拦截
项目默认在 src/router/config.ts (opens new window) 开启登录拦截,未登录时访问需要登录的页面,会自动跳转到登录页。
可通过以下两种方式,配置页面不需要登录拦截:
① 方式一:在 definePage 中配置
<script lang="ts" setup>
definePage({
excludeLoginPath: true, // 排除登录拦截
})
</script>
② 方式二:在 EXCLUDE_LOGIN_PATH_LIST 中配置
// src/router/config.ts
export const EXCLUDE_LOGIN_PATH_LIST = [
'/pages/xxx/index',
'/pages-sub/xxx/index',
]
# 2. 首页菜单
首页(工作台)的菜单数据,配置在 src/pages/index/menu.json (opens new window) 文件中,由 src/pages/index/index.ts (opens new window) 负责加载(权限过滤、onlyPc 解析、排序等)。

# 2.1 菜单数据结构
菜单采用 三层结构:分组(group)▸ 二级分组(subGroup)▸ 菜单项(menu)。对应的 TypeScript 类型(定义在 src/pages/index/index.ts):
/** 菜单项 */
export interface MenuItem {
key: string // 菜单唯一标识
name: string // 菜单名称
icon: string // 菜单图标(Wot UI 图标名)
url?: string // 跳转路径
iconColor?: string // 图标颜色
permission?: string // 权限标识
onlyPc?: boolean // 是否仅 PC 端可用
}
/** 二级分组 */
export interface MenuSubGroup {
key: string // 二级分组唯一标识
name: string // 二级分组名称(空字符串表示不展示小标题,直接平铺)
menus: MenuItem[] // 二级分组下的菜单列表
}
/** 分组 */
export interface MenuGroup {
key: string // 分组唯一标识
name: string // 分组名称
subGroups: MenuSubGroup[] // 二级分组列表
menus: MenuItem[] // 由二级分组拍平得到(便于搜索 / 常用等场景)
}
# 2.2 菜单配置示例
菜单数据写在 menu.json 里(只放数据,布局/渲染参数不进 JSON);分组、二级分组、菜单项的展示顺序,以数组顺序为准。
{
"groups": [
{
"key": "system",
"name": "系统管理",
"subGroups": [
{
"key": "permission",
"name": "权限管理",
"menus": [
{
"key": "user",
"name": "用户管理",
"icon": "user",
"iconColor": "#1890ff",
"url": "/pages-system/user/index",
"permission": "system:user:list"
}
]
}
]
}
]
}
菜单名 / 分组,尽量和管理后台对齐
menu.json 的菜单名、二级分组、顺序,已尽量对齐 PC 管理后台 [系统管理 -> 菜单管理](即后端 system_menu),方便大家理解。
疑问:首页菜单,是否支持管理后台配置?
短期来看,暂时需求量不大。
目前先通过 PC 端的管理后台 [系统管理 -> 菜单管理] 功能,里面的 permission 字段,来控制「移动端 - 首页菜单」的显示和隐藏即可。
# 2.3 仅 PC 端页面
部分功能(如代码生成、表单构建等)仅支持 PC 端访问,配置 onlyPc 为 true(无需配 url):
{
"key": "codegen",
"name": "代码生成",
"icon": "code",
"iconColor": "#52c41a",
"onlyPc": true
}
加载时会自动把这类菜单的 url 解析为「仅 PC 端访问」提示页(ONLY_PC_PAGE 常量,对应 src/pages-core/error/pc-only.vue),点击后跳转提示。
# 2.4 工作台布局
首页支持 4 种布局,可在「编辑工作台」页切换(开关本地持久化);4 种布局共用同一份 menu.json 数据,仅渲染外壳不同:
| 布局 | 说明 |
|---|---|
| 小标题 | 平铺,二级分组用灰色小标题分隔 |
| 折叠 | 二级分组默认折叠成一行,点开才展开网格 |
| 纵向分类 | 左侧模块栏 + 右侧二级分组(默认) |
| 横向分类 | 顶部横向 Tab + 下方二级分组 |
# 2.5 我的常用 / 最近使用 / 搜索
- 我的常用:手动收藏的菜单,可在「编辑工作台」增删、上下排序。
- 最近使用:点击菜单后自动记录(去重置顶,最多 12 个)。
- 菜单搜索:首页右上角的搜索入口,按 菜单名 / 模块名 / 二级分组 / 英文 key 模糊匹配(含子序列),点击结果直接跳转。
# 3. 权限控制
前端通过权限控制,隐藏用户没有权限的菜单和按钮,实现功能级别的权限。
友情提示
前端的权限控制,主要是提升用户体验,避免操作后发现没有权限。最终在请求到后端时,还是会进行一次权限的校验。
# 3.1 useAccess Hook
项目提供了 useAccess (opens new window) Hook 进行权限判断:
import { useAccess } from '@/hooks/useAccess'
const { hasAccessByCodes, hasAccessByRoles } = useAccess()
// 基于权限码判断
if (hasAccessByCodes(['system:user:create'])) {
// 有新增用户权限
}
// 基于角色判断
if (hasAccessByRoles(['admin'])) {
// 是管理员角色
}
# 3.2 权限数据来源
用户的权限数据在登录成功后,通过 /admin-api/system/auth/get-permission-info (opens new window) 接口获取,存储在 userStore 中:
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
// 用户角色列表
console.log(userStore.roles) // ['admin', 'common']
// 用户权限列表
console.log(userStore.permissions) // ['system:user:list', 'system:user:create', ...]
# 3.3 菜单权限过滤
首页菜单会自动根据用户权限进行过滤,没有权限的菜单项不会显示。
实现代码:src/pages/index/index.ts (opens new window) 的 getMenuGroups 方法:
// src/pages/index/index.ts
export function getMenuGroups(): MenuGroup[] {
const { hasAccessByCodes } = useAccess()
const result: MenuGroup[] = []
for (const group of groupsData) {
const subGroups: MenuSubGroup[] = []
for (const sub of group.subGroups) {
// 过滤掉没有权限的菜单项(没配权限的默认展示);onlyPc 解析为「仅 PC」提示页
const menus = sub.menus
.filter(menu => !menu.permission || hasAccessByCodes([menu.permission]))
.map(resolveItem)
if (menus.length > 0) {
subGroups.push({ key: sub.key, name: sub.name, menus })
}
}
// 二级分组全为空 → 整个分组不展示
if (subGroups.length === 0) {
continue
}
result.push({
key: group.key,
name: group.name,
subGroups,
menus: subGroups.flatMap(sub => sub.menus), // 拍平,兼容搜索 / 常用
})
}
return result
}
# 3.4 按钮权限控制
在页面中可以使用 v-if 结合 hasAccessByCodes 或 hasAccessByRoles 控制按钮显示。
实战案例:
- 列表页的新增按钮:
src/pages-system/user/index.vue(opens new window) - 详情页的编辑、删除按钮
src/pages-system/user/detail/index.vue(opens new window)
<template>
<wd-button
v-if="hasAccessByCodes(['system:user:create'])"
type="primary"
@click="handleCreate"
>
新增用户
</wd-button>
</template>
<script lang="ts" setup>
import { useAccess } from '@/hooks/useAccess'
const { hasAccessByCodes } = useAccess()
</script>