Files
Migu2.0/frontends/apps/simple-platform-vue/src/router/index.ts
T
zhaowei.huangandCursor 6986370e9d 新增任务管理与报警管理前端,并接入车队健康数据。
运营菜单下挂地图监控/任务/报警入口,车辆卡片与任务列表同步展示运行态。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 11:31:52 +08:00

192 lines
12 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: '服务状态' }
},
{
// 部署配置向导:首次部署强制完成平台选型(导航方式 / 模块 / 功能)。需登录,但不属于 admin/monitor scope。
path: '/wizard',
name: 'wizard',
component: () => import('@/views/WizardView.vue'),
meta: { layout: 'blank', 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: 'tasks', name: 'admin-tasks', component: () => import('@/views/admin/TaskManagementView.vue'), meta: { title: '任务管理' } },
{ path: 'alarms', name: 'admin-alarms', component: () => import('@/views/admin/AlarmManagementView.vue'), meta: { title: '报警管理' } },
{ path: 'maps', name: 'admin-maps', component: () => import('@/views/admin/MapManagementView.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: 'task-templates', name: 'admin-task-templates', component: () => import('@/views/admin/TaskTemplateView.vue'), meta: { title: '任务编排' } },
{ path: 'simple-fields', name: 'admin-simple-fields', component: () => import('@/views/admin/SimpleFieldManagementView.vue'), meta: { title: '字段管理' } },
{ path: 'project-properties', name: 'admin-project-properties', component: () => import('@/views/admin/ProjectPropertiesView.vue'), meta: { title: '项目属性' } },
// ── 平台配置中心:聚合页 + 独立业务页(page key = route.name,对齐后端 PageCatalog)。 ──
{ path: 'config/strategy', name: 'admin-config-strategy', component: () => import('@/views/admin/config/StrategyConfigView.vue'), meta: { title: '调度策略' } },
{ path: 'config/vehicle-hub', name: 'admin-vehicle-hub', component: () => import('@/views/shared/VehicleHubView.vue'), meta: { title: '车辆运维' } },
{ path: 'config/facility', name: 'admin-config-facility', component: () => import('@/views/admin/config/FacilityConfigView.vue'), meta: { title: '设备接入' } },
{ path: 'config/warehouse', name: 'admin-config-warehouse', component: () => import('@/views/admin/WarehouseManagementView.vue'), meta: { title: '仓储管理' } },
{ path: 'config/business', name: 'admin-config-business', component: () => import('@/views/admin/config/BusinessConfigView.vue'), meta: { title: '业务与集成' } },
{ path: 'config/ops-center', name: 'admin-config-ops-center', component: () => import('@/views/admin/config/OpsCenterView.vue'), meta: { title: '运维与回放' } },
{ path: 'config/system-center', name: 'admin-config-system-center', component: () => import('@/views/admin/config/SystemCenterView.vue'), meta: { title: '系统与权限' } },
// ── 旧路径深链接兼容:redirect 到聚合页对应 tab(无 name → 不计入受权限管理的页面)。 ──
{ path: 'playback', redirect: { path: '/admin/config/ops-center', query: { tab: 'playback' } } },
{ path: 'vehicle-hub', redirect: '/admin/config/vehicle-hub' },
{ path: 'config/vehicle', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'ota' } } },
{ path: 'config/fleet', redirect: { path: '/admin/config/vehicle-hub', query: { tab: 'ota' } } },
{ path: 'config/routing', redirect: { path: '/admin/config/strategy', query: { tab: 'routing' } } },
{ path: 'config/task', redirect: { path: '/admin/config/strategy', query: { tab: 'task' } } },
{ path: 'config/traffic', redirect: { path: '/admin/config/strategy', query: { tab: 'traffic' } } },
{ path: 'config/charge', redirect: { path: '/admin/config/strategy', query: { tab: 'charge' } } },
{ path: 'config/device', redirect: '/admin/config/facility' },
{ path: 'config/location', redirect: '/admin/config/warehouse' },
{ path: 'config/integrations', redirect: { path: '/admin/config/business', query: { tab: 'integrations' } } },
{ path: 'config/scenario', redirect: { path: '/admin/config/business', query: { tab: 'scenario' } } },
{ path: 'config/widget', redirect: { path: '/admin/config/business', query: { tab: 'widget' } } },
{ path: 'config/ops', redirect: { path: '/admin/config/ops-center', query: { tab: 'ops' } } },
{ path: 'config/logs', redirect: { path: '/admin/config/ops-center', query: { tab: 'logs' } } },
{ path: 'config/map-monitor', redirect: { path: '/admin/config/ops-center', query: { tab: 'map-monitor' } } },
{ path: 'config/system', redirect: { path: '/admin/config/system-center', query: { tab: 'system' } } },
{ path: 'config/auth', redirect: { path: '/admin/config/system-center', query: { tab: 'auth' } } }
]
},
{
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 } }
}
}
// 部署配置向导:首次部署(deployment.Configured=false)时,强制先完成平台选型再进入业务页。
// 已在 /wizard 则放行,避免自跳死循环;保存成功后 store.markWizardDone() 解除拦截。
if (auth.needsWizard && to.name !== 'wizard') {
return { name: 'wizard' }
}
// 会话 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 : ''
const pageKey = typeof to.meta.pageKey === 'string' ? to.meta.pageKey : name
if (pageKey && MANAGED_PAGE_NAMES.has(pageKey) && !auth.hasPage(pageKey)) {
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