Files
Nexus/frontend/src/router/index.ts
T
Nexus Agent 2a6f526bc9 feat: 内置 Web 浏览器页与服务器站点入口
侧栏浏览器页 iframe 预览 http(s) 站点;服务器列表「站点」由 domain 跳转,禁止嵌入时可新标签打开。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 18:30:44 +08:00

74 lines
3.5 KiB
TypeScript

import { createRouter, createWebHashHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const routes = [
{ path: '/', name: 'Dashboard', component: () => import('@/pages/DashboardPage.vue') },
{ path: '/servers', name: 'Servers', component: () => import('@/pages/ServersPage.vue') },
{ path: '/terminal', name: 'Terminal', component: () => import('@/pages/TerminalPage.vue') },
{ path: '/browser', name: 'Browser', component: () => import('@/pages/BrowserPage.vue') },
{ path: '/files', name: 'Files', component: () => import('@/pages/FilesPage.vue') },
{ path: '/push', name: 'Push', component: () => import('@/pages/PushPage.vue') },
{ path: '/scripts', name: 'Scripts', component: () => import('@/pages/ScriptsPage.vue') },
{ path: '/script-runs', name: 'ScriptRuns', component: () => import('@/pages/ScriptRunsPage.vue') },
{ path: '/script-runs/:id', name: 'ScriptRunDetail', component: () => import('@/pages/ScriptRunsPage.vue') },
{ path: '/executions', name: 'Executions', component: () => import('@/pages/ScriptRunsPage.vue') },
{ path: '/executions/:id', name: 'ExecutionDetail', component: () => import('@/pages/ScriptRunsPage.vue') },
{ path: '/credentials', name: 'Credentials',component: () => import('@/pages/CredentialsPage.vue') },
{ path: '/schedules', name: 'Schedules', component: () => import('@/pages/SchedulesPage.vue') },
{ path: '/retries', name: 'Retries', component: () => import('@/pages/RetriesPage.vue') },
{ path: '/commands', name: 'Commands', component: () => import('@/pages/CommandsPage.vue') },
{ path: '/alerts', name: 'Alerts', component: () => import('@/pages/AlertsPage.vue') },
{ path: '/audit', name: 'Audit', component: () => import('@/pages/AuditPage.vue') },
{ path: '/settings', name: 'Settings', component: () => import('@/pages/SettingsPage.vue') },
{ path: '/login', name: 'Login', component: () => import('@/pages/LoginPage.vue'), meta: { public: true } },
]
const router = createRouter({ history: createWebHashHistory(), routes })
const CHUNK_RELOAD_FLAG = 'nexus_chunk_reload_v1'
function isChunkLoadError(message: string): boolean {
return (
message.includes('Failed to fetch dynamically imported module')
|| message.includes('Importing a module script failed')
|| message.includes('error loading dynamically imported module')
)
}
/** 懒加载 chunk 缺失时静默 reload 一次(部署后旧标签页兜底);登录由 refresh cookie 恢复 */
router.onError((err, to) => {
const msg = err?.message || ''
if (!isChunkLoadError(msg)) return
const w = window as Window & { $snackbar?: (text: string, color?: string) => void }
if (!sessionStorage.getItem(CHUNK_RELOAD_FLAG)) {
sessionStorage.setItem(CHUNK_RELOAD_FLAG, '1')
window.location.reload()
return
}
w.$snackbar?.(
`页面「${String(to.name || to.path)}」资源已更新,请关闭此标签后重新打开后台`,
'error',
)
})
router.afterEach(() => {
sessionStorage.removeItem(CHUNK_RELOAD_FLAG)
})
// Auth guard — restore session from HttpOnly refresh cookie when needed
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (!auth.bootstrapped) {
await auth.tryRestoreSession()
}
if (!to.meta.public && !auth.isLoggedIn) {
return { name: 'Login', query: { redirect: to.fullPath } }
}
if (to.name === 'Login' && auth.isLoggedIn) {
return { name: 'Dashboard' }
}
})
export default router