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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user