feat(rbac-web): 前端页面级权限与「权限与角色」管理页

- 新增 rbac 的 api/types/mock,对接后端 catalog/roles/users;mock 模式与后端 PageCatalog 完全对齐
- EffectivePermissions 增加 allowedPages,auth store 新增 hasPage 判定
- 路由守卫按 route.name = 页面 Key 做页面级放行,无权时跳转到 scope 内首个可访问页面;AppShell 菜单按 hasPage 过滤
- 路由守卫 switchScope 失败自动重试一次;新增懒加载 chunk 失败整页自愈 onError,修复切页白屏
- PermissionGuard hidden 态改为友好空态提示;AuthRoleView 重写为完整的用户/角色/权限编辑界面
This commit is contained in:
zhaowei.huang
2026-05-29 23:51:57 +08:00
parent 37680a0ae7
commit 00cfcf2897
11 changed files with 976 additions and 106 deletions
@@ -89,10 +89,9 @@
</el-header>
<el-main class="main mg-content">
<router-view v-slot="{ Component }">
<transition name="fade-up" mode="out-in">
<component :is="Component" />
</transition>
<!-- 不用 mode="out-in"懒加载子路由切换时易出现离场后新组件未挂载 主区全白刷新才恢复 -->
<router-view v-slot="{ Component, route: rv }">
<component :is="Component" v-if="Component" :key="rv.fullPath" class="route-page" />
</router-view>
</el-main>
@@ -137,53 +136,68 @@ const runModeLabel = computed(() => {
}
})
interface MenuItem { path: string; label: string; icon?: unknown; children?: MenuItem[] }
// key 即「权限页面 Key」(= vue-router route.name),用于按 auth.allowedPages 过滤菜单。
// 分组节点(无 key)只要还有可见子项就保留。
interface MenuItem { path: string; label: string; icon?: unknown; key?: string; children?: MenuItem[] }
const ADMIN_MENU: MenuItem[] = [
{ path: '/admin/dashboard', label: '总览', icon: Histogram },
{ path: '/admin/map-monitor', label: '地图监控', icon: MapLocation },
{ path: '/admin/dashboard', label: '总览', icon: Histogram, key: 'admin-dashboard' },
{ path: '/admin/map-monitor', label: '地图监控', icon: MapLocation, key: 'admin-map-monitor' },
{
path: '/admin/design', label: '设计与编排', icon: Tools,
children: [
{ path: '/admin/map-editor', label: '地图编辑' },
{ path: '/admin/project-properties', label: '项目属性' },
{ path: '/admin/tracks', label: '场景管理' },
{ path: '/admin/cars', label: '车辆管理' },
{ path: '/admin/processes', label: '进程管理' },
{ path: '/admin/scripts', label: '脚本管理' },
{ path: '/admin/missions', label: '任务编排' }
{ path: '/admin/map-editor', label: '地图编辑', key: 'admin-map-editor' },
{ path: '/admin/project-properties', label: '项目属性', key: 'admin-project-properties' },
{ path: '/admin/tracks', label: '场景管理', key: 'admin-tracks' },
{ path: '/admin/cars', label: '车辆管理', key: 'admin-cars' },
{ path: '/admin/processes', label: '进程管理', key: 'admin-processes' },
{ path: '/admin/scripts', label: '脚本管理', key: 'admin-scripts' },
{ path: '/admin/missions', label: '任务编排', key: 'admin-missions' }
]
},
{ path: '/admin/playback', label: '调度回放', icon: Notebook },
{ path: '/admin/playback', label: '调度回放', icon: Notebook, key: 'admin-playback' },
{
path: '/admin/config', label: '平台配置中心', icon: Setting,
children: [
{ path: '/admin/config/system', label: '系统级配置' },
{ path: '/admin/config/integrations', label: '外部系统对接' },
{ path: '/admin/config/routing', label: '路径规划' },
{ path: '/admin/config/vehicle', label: '车辆维护' },
{ path: '/admin/config/charge', label: '充电策略' },
{ path: '/admin/config/task', label: '任务分配' },
{ path: '/admin/config/traffic', label: '交通管制' },
{ path: '/admin/config/auth', label: '权限与角色' },
{ path: '/admin/config/device', label: '设备接入' },
{ path: '/admin/config/fleet', label: '车队生命周期' },
{ path: '/admin/config/scenario', label: '场景模板' },
{ path: '/admin/config/location', label: '库位管理' },
{ path: '/admin/config/ops', label: '运营维护' },
{ path: '/admin/config/widget', label: '自定义控件' }
{ path: '/admin/config/system', label: '系统级配置', key: 'admin-config-system' },
{ path: '/admin/config/integrations', label: '外部系统对接', key: 'admin-config-integrations' },
{ path: '/admin/config/routing', label: '路径规划', key: 'admin-config-routing' },
{ path: '/admin/config/vehicle', label: '车辆维护', key: 'admin-config-vehicle' },
{ path: '/admin/config/charge', label: '充电策略', key: 'admin-config-charge' },
{ path: '/admin/config/task', label: '任务分配', key: 'admin-config-task' },
{ path: '/admin/config/traffic', label: '交通管制', key: 'admin-config-traffic' },
{ path: '/admin/config/auth', label: '权限与角色', key: 'admin-config-auth' },
{ path: '/admin/config/device', label: '设备接入', key: 'admin-config-device' },
{ path: '/admin/config/fleet', label: '车队生命周期', key: 'admin-config-fleet' },
{ path: '/admin/config/scenario', label: '场景模板', key: 'admin-config-scenario' },
{ path: '/admin/config/location', label: '库位管理', key: 'admin-config-location' },
{ path: '/admin/config/ops', label: '运营维护', key: 'admin-config-ops' },
{ path: '/admin/config/widget', label: '自定义控件', key: 'admin-config-widget' }
]
}
]
const MONITOR_MENU: MenuItem[] = [
{ path: '/monitor/dashboard', label: '运营总览', icon: Monitor },
{ path: '/monitor/map', label: '地图监控', icon: MapLocation },
{ path: '/monitor/ops', label: '运维操作', icon: Promotion },
{ path: '/monitor/notes', label: '运营备注', icon: Notebook }
{ path: '/monitor/dashboard', label: '运营总览', icon: Monitor, key: 'monitor-dashboard' },
{ path: '/monitor/map', label: '地图监控', icon: MapLocation, key: 'monitor-map' },
{ path: '/monitor/ops', label: '运维操作', icon: Promotion, key: 'monitor-ops' },
{ path: '/monitor/notes', label: '运营备注', icon: Notebook, key: 'monitor-notes' }
]
const currentMenu = computed<MenuItem[]>(() => (auth.scope === 'Platform' ? ADMIN_MENU : MONITOR_MENU))
function filterMenu(items: MenuItem[]): MenuItem[] {
const out: MenuItem[] = []
for (const m of items) {
if (m.children) {
const kids = m.children.filter((c) => !c.key || auth.hasPage(c.key))
if (kids.length) out.push({ ...m, children: kids })
} else if (!m.key || auth.hasPage(m.key)) {
out.push(m)
}
}
return out
}
const currentMenu = computed<MenuItem[]>(() => filterMenu(auth.scope === 'Platform' ? ADMIN_MENU : MONITOR_MENU))
const activePath = computed(() => {
const path = route.path
@@ -447,7 +461,13 @@ function onUserCommand(cmd: string) {
}
/* ─────────────── Header ─────────────── */
.container-right { background: transparent; }
.container-right {
background: transparent;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.header {
position: relative;
display: flex; justify-content: space-between; align-items: center;
@@ -544,6 +564,11 @@ function onUserCommand(cmd: string) {
/* ─────────────── Main / Footer ─────────────── */
.main {
flex: 1;
min-height: 0;
overflow: auto;
display: flex;
flex-direction: column;
padding: 24px 24px;
background: transparent;
color: #fff;
@@ -562,11 +587,10 @@ function onUserCommand(cmd: string) {
height: 32px !important; line-height: 32px;
}
/* 路由过渡动画 */
.fade-up-enter-active,
.fade-up-leave-active {
transition: opacity .25s cubic-bezier(.25, .8, .25, 1), transform .25s cubic-bezier(.25, .8, .25, 1);
.route-page {
min-height: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.fade-up-enter-from { opacity: 0; transform: translateY(8px); }
.fade-up-leave-to { opacity: 0; transform: translateY(-8px); }
</style>