Files
Migu2.0/frontends/apps/simple-platform-vue/src/router/index.ts
T
zhaowei.huangandCursor e2269e430d feat(platform): 合并车辆运维页并优化 3D 画布嵌入体验
将维护策略与车队生命周期并入车辆运维 Tab,补充旧路由重定向与 RBAC 页面 key 迁移;改进 Workspace3D 就绪探测与 wwwroot 静态资源。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:39:50 +08:00

164 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('@/views/LoginView.vue'),
meta: { layout: 'blank', public: true, title: '登录' }
},
{
path: '/status',
name: 'status',
component: () => import('@/views/ServiceStatusView.vue'),
meta: { layout: 'blank', public: true, title: '服务状态' }
},
{
path: '/admin',
component: () => import('@/layouts/AppShell.vue'),
meta: { scope: 'Platform' },
redirect: '/admin/dashboard',
children: [
{ path: 'dashboard', name: 'admin-dashboard', component: () => import('@/views/admin/DashboardView.vue'), meta: { title: '总览' } },
{ path: 'map-monitor', name: 'admin-map-monitor', component: () => import('@/views/admin/MapMonitorView.vue'), meta: { title: '地图监控' } },
{ path: 'map-editor', name: 'admin-map-editor', component: () => import('@/views/admin/MapEditorView.vue'), meta: { title: '地图编辑' } },
{ path: 'tracks', name: 'admin-tracks', component: () => import('@/views/admin/TrackTableView.vue'), meta: { title: '场景管理' } },
{ path: 'cars', name: 'admin-cars', component: () => import('@/views/admin/CarPanelView.vue'), meta: { title: '车辆管理' } },
{ path: 'processes', name: 'admin-processes', component: () => import('@/views/admin/ProcessPanelView.vue'), meta: { title: '进程管理' } },
{ path: 'scripts', name: 'admin-scripts', component: () => import('@/views/admin/ScriptPanelView.vue'), meta: { title: '脚本管理' } },
{ path: 'missions', name: 'admin-missions', component: () => import('@/views/admin/MissionEditorView.vue'), meta: { title: '任务编排' } },
{ path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } },
{ path: 'playback', name: 'admin-playback', component: () => import('@/views/admin/PlaybackView.vue'), meta: { title: '调度回放' } },
{ path: 'config/system', name: 'admin-config-system', component: () => import('@/views/admin/config/SystemConfigView.vue'), meta: { title: '系统级配置' } },
{ path: 'config/integrations', name: 'admin-config-integrations', component: () => import('@/views/admin/config/ExternalIntegrationView.vue'), meta: { title: '外部系统对接' } },
{ path: 'config/routing', name: 'admin-config-routing', component: () => import('@/views/admin/config/RoutingPolicyView.vue'), meta: { title: '路径规划策略' } },
{ path: 'config/vehicle-hub', name: 'admin-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
{ path: 'vehicle-hub', redirect: '/admin/config/vehicle-hub' },
// 旧独立配置页已合并进车辆运维 Tab,保留深链接兼容。
{ path: 'config/vehicle', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'maintenance' } } },
{ path: 'config/fleet', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'fleet' } } },
{ path: 'config/charge', name: 'admin-config-charge', component: () => import('@/views/admin/config/ChargePolicyView.vue'), meta: { title: '充电逻辑' } },
{ path: 'config/task', name: 'admin-config-task', component: () => import('@/views/admin/config/TaskAllocationView.vue'), meta: { title: '任务分配' } },
{ path: 'config/traffic', name: 'admin-config-traffic', component: () => import('@/views/admin/config/TrafficRuleView.vue'), meta: { title: '交通管制' } },
{ path: 'config/auth', name: 'admin-config-auth', component: () => import('@/views/admin/config/AuthRoleView.vue'), meta: { title: '权限角色' } },
{ path: 'config/device', name: 'admin-config-device', component: () => import('@/views/admin/config/DeviceHubView.vue'), meta: { title: '设备接入' } },
{ path: 'config/scenario', name: 'admin-config-scenario', component: () => import('@/views/admin/config/ScenarioTemplateView.vue'), meta: { title: '场景模板' } },
{ path: 'config/location', name: 'admin-config-location', component: () => import('@/views/admin/config/LocationView.vue'), meta: { title: '库位管理' } },
{ path: 'config/ops', name: 'admin-config-ops', component: () => import('@/views/admin/config/OpsConfigView.vue'), meta: { title: '运营维护' } },
{ path: 'config/widget', name: 'admin-config-widget', component: () => import('@/views/admin/config/CustomWidgetView.vue'), meta: { title: '自定义控件' } },
{ path: 'config/map-monitor', name: 'admin-config-map-monitor', component: () => import('@/views/admin/config/MapMonitorConfigView.vue'), meta: { title: '地图监控配置' } }
]
},
{
path: '/monitor',
component: () => import('@/layouts/AppShell.vue'),
meta: { scope: 'RCSMonitor' },
redirect: '/monitor/map',
children: [
{ path: 'dashboard', name: 'monitor-dashboard', component: () => import('@/views/monitor/MonitorDashboardView.vue'), meta: { title: '运营总览' } },
{ path: 'vehicle-hub', name: 'monitor-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
{ path: 'map', name: 'monitor-map', component: () => import('@/views/monitor/MonitorMapView.vue'), meta: { title: '地图监控' } },
{ path: 'ops', name: 'monitor-ops', component: () => import('@/views/monitor/OpsActionPanelView.vue'), meta: { title: '运维操作' } },
{ path: 'notes', name: 'monitor-notes', component: () => import('@/views/monitor/AnnotationView.vue'), meta: { title: '运营备注' } }
]
},
{ path: '/', redirect: '/login' },
{ path: '/:pathMatch(.*)*', redirect: '/login' }
]
/**
* RBAC 页面权限:route.name 即「权限页面 Key」(与后端 PageCatalog / EffectivePermissions.allowedPages 对齐)。
* 这里从路由表派生出各 scope 的有序页面列表,供守卫做权限校验与「跳到首个可访问页面」的回退。
*/
function childrenPages(parentPath: string): Array<{ name: string; path: string }> {
const parent = routes.find((r) => r.path === parentPath)
return ((parent?.children ?? []) as RouteRecordRaw[])
.filter((c) => typeof c.name === 'string')
.map((c) => ({ name: String(c.name), path: `${parentPath}/${(c as { path: string }).path}` }))
}
const ADMIN_PAGES = childrenPages('/admin')
const MONITOR_PAGES = childrenPages('/monitor')
const MANAGED_PAGE_NAMES = new Set([...ADMIN_PAGES, ...MONITOR_PAGES].map((p) => p.name))
const router = createRouter({
history: createWebHistory(),
routes
})
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (to.meta.public) return true
if (!auth.isAuthed) {
return { path: '/login', query: { redirect: to.fullPath } }
}
// 关键守卫:localStorage 里只要有 token 字符串 isAuthed 就为 true,但服务端可能已经
// 不认这个 token(最常见:Platform.Server 用占位 Jwt:Secret 启动,每次重启 secret 重生 →
// 历史 token 全部失效)。如果进入受保护路由后页面恰好没立刻发 API,旧 401 拦截路径不会
// 触发,用户就能「看到」框架页 → 看起来是「跳过了登录页」。
//
// 解决:本浏览器会话内首次进入受保护路由前 await 一次 validateGET /api/auth/me),
// 由后端 [Authorize] 判定 token 是否仍有效。失败 → store 已清空、跳 /login 并带 redirect。
if (!auth.validated) {
try {
await auth.validate()
} catch {
return { path: '/login', query: { redirect: to.fullPath } }
}
}
// 会话 45 AR-6switchScope 改为后端发起 ——
// 必须 await 完成后再放行,否则页面用旧 scope 的 perms 渲染一帧后才被纠正。
// 失败(如 ops 账号尝试切 Platform 被 403)则维持原 scope,路由仍放行让用户看到 readonly UI。
const needScope: 'Platform' | 'RCSMonitor' | null =
to.path.startsWith('/admin') ? 'Platform'
: to.path.startsWith('/monitor') ? 'RCSMonitor'
: null
if (needScope && auth.scope !== needScope) {
try {
await auth.switchScope(needScope)
} catch (_) {
// 偶发失败(网络/超时)重试一次:避免带着「另一个域」的 effectivePermissions 渲染当前页,
// 否则配置页会被 PermissionGuard 误判为隐藏 → 整页空白(刷新后重新 switch 才恢复)。
try { await auth.switchScope(needScope) } catch (_2) { /* 仍失败:维持原 scopeUI 多为 readonly */ }
}
}
// RBAC 页面级权限:scope 已切换到目标域,allowedPages 已刷新。
// 若目标页面不在当前账号的可访问页面集合内,跳到该 scope 下首个可访问页面(菜单顺序)。
const name = typeof to.name === 'string' ? to.name : ''
if (name && MANAGED_PAGE_NAMES.has(name) && !auth.hasPage(name)) {
const list = auth.scope === 'RCSMonitor' ? MONITOR_PAGES : ADMIN_PAGES
const fallback = list.find((p) => auth.hasPage(p.name))?.path
if (fallback && fallback !== to.path) {
ElMessage.warning('无权访问该页面,已跳转到可访问页面')
return fallback
}
if (!fallback) {
ElMessage.error('当前账号在该区域没有任何可访问页面,请联系管理员分配权限')
return { path: '/status' }
}
}
return true
})
router.afterEach((to) => {
const base = '迷毂'
document.title = to.meta.title ? `${to.meta.title} · ${base}` : base
})
// 懒加载 chunk 失败自愈:重新部署后产物 hash 变化、或网络抖动会让动态 import() 失败,
// 表现为点击导航后白屏、刷新才好。这里捕获该类错误并整页重载到目标路由,
// 让浏览器重新拉取最新的 index.html 与对应 chunk 引用。
router.onError((error, to) => {
const m = (error as Error)?.message ?? ''
if (/dynamically imported module|Loading (chunk|CSS chunk)|module script failed|Failed to fetch/i.test(m)) {
if (to?.fullPath) window.location.assign(to.fullPath)
else window.location.reload()
}
})
export default router