Files
Migu2.0/frontends/apps/simple-platform-vue/src/router/index.ts
T

112 lines
7.3 KiB
TypeScript
Raw Normal View History

import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
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', name: 'admin-config-vehicle', component: () => import('@/views/admin/config/VehicleMaintenanceView.vue'), meta: { title: '车辆维护策略' } },
{ 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/fleet', name: 'admin-config-fleet', component: () => import('@/views/admin/config/FleetLifecycleView.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: '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' }
]
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。
try {
if (to.path.startsWith('/admin') && auth.scope !== 'Platform') {
await auth.switchScope('Platform')
} else if (to.path.startsWith('/monitor') && auth.scope !== 'RCSMonitor') {
await auth.switchScope('RCSMonitor')
}
} catch (_) { /* 服务端拒绝切换:维持原 scope,UI 会按现有 perms 渲染(多为 readonly */ }
return true
})
router.afterEach((to) => {
const base = '迷毂'
document.title = to.meta.title ? `${to.meta.title} · ${base}` : base
})
export default router