Files
YG_FT/frontend/src/router/index.ts

453 lines
15 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 { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import type { PermissionCode } from '@/types'
/** 路由切换时的全局加载态,供 App.vue 显示全屏转圈遮罩,消除懒加载时的空白卡顿感 */
export const routeLoading = ref(false)
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: () => import('@/views/login/LoginView.vue'),
meta: { title: '登录', public: true },
},
{
path: '/guide',
name: 'guide',
component: () => import('@/views/guide/GuideView.vue'),
meta: { title: '使用文档', skipPermission: true },
},
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
redirect: '/dashboard',
children: [
// 服务看板
{
path: 'dashboard',
name: 'dashboard',
component: () => import('@/views/dashboard/DashboardView.vue'),
meta: { title: '服务看板' },
},
// 平台治理
{
path: 'tenants',
name: 'tenants',
component: () => import('@/views/tenants/TenantListView.vue'),
meta: { title: '租户管理', permission: 'user-settings' },
},
{
path: 'tenants/:id',
name: 'tenant-detail',
component: () => import('@/views/tenants/TenantDetailView.vue'),
meta: { title: '租户详情', permission: 'user-settings' },
},
{
path: 'projects',
name: 'projects',
component: () => import('@/views/projects/ProjectListView.vue'),
meta: { title: '项目空间', permission: 'user-settings' },
},
{
path: 'projects/:id',
name: 'project-detail',
component: () => import('@/views/projects/ProjectDetailView.vue'),
meta: { title: '项目详情', permission: 'user-settings' },
},
{
path: 'audit-logs',
name: 'audit-logs',
component: () => import('@/views/audit/AuditLogView.vue'),
meta: { title: '审计日志', permission: 'user-settings' },
},
{
path: 'operation-logs',
name: 'operation-logs',
component: () => import('@/views/audit/OperationLogView.vue'),
meta: { title: '操作日志', permission: 'user-settings' },
},
{
path: 'approval-templates',
name: 'approval-templates',
component: () => import('@/views/approvals/ApprovalTemplateView.vue'),
meta: { title: '审批模板', permission: 'user-settings' },
},
{
path: 'approval-instances',
name: 'approval-instances',
component: () => import('@/views/approvals/ApprovalInstanceView.vue'),
meta: { title: '审批中心', permission: 'user-settings' },
},
{
path: 'resource-acl',
name: 'resource-acl',
component: () => import('@/views/governance/ResourceAclView.vue'),
meta: { title: '资源授权', permission: 'user-settings' },
},
// 模型调优
{
path: 'fine-tune',
name: 'fine-tune',
component: () => import('@/views/fine-tune/FineTuneListView.vue'),
meta: { title: '模型训练', pageSurface: 'self' },
},
{
path: 'fine-tune/create',
name: 'fine-tune-create',
component: () => import('@/views/fine-tune/FineTuneCreateView.vue'),
meta: { title: '创建训练任务' },
},
// 模型评测
{
path: 'model-eval',
name: 'model-eval',
component: () => import('@/views/eval/EvalView.vue'),
meta: { title: '模型评测', pageSurface: 'self' },
},
{
path: 'model-eval/create',
name: 'model-eval-create',
component: () => import('@/views/eval/EvalCreateView.vue'),
meta: { title: '新建评测' },
},
{
path: 'model-eval/:id',
name: 'model-eval-detail',
component: () => import('@/views/eval/EvalDetailView.vue'),
meta: { title: '评测详情' },
},
{
path: 'model-eval/dimension/create',
name: 'dimension-create',
redirect: '/model-eval/create',
},
{
path: 'model-eval/dimension/:id/edit',
name: 'dimension-edit',
component: () => import('@/views/eval/DimensionCreateView.vue'),
meta: { title: '编辑评测维度' },
},
// 模型推理
{
path: 'model-inference',
name: 'model-inference',
component: () => import('@/views/inference/InferenceListView.vue'),
meta: { title: '模型推理', pageSurface: 'self' },
},
{
path: 'model-inference/create',
name: 'model-inference-create',
component: () => import('@/views/inference/InferenceCreateView.vue'),
meta: { title: '新建推理' },
},
{
path: 'model-inference/chat/:id',
name: 'inference-chat',
component: () => import('@/views/inference/InferenceChatView.vue'),
meta: { title: '模型对话', pageSurface: 'self' },
},
// 模型对比
{
path: 'model-compare/chat/:id',
name: 'compare-chat',
component: () => import('@/views/compare/CompareChatView.vue'),
meta: { title: '模型对比' },
},
{
path: 'model-compare/result',
name: 'compare-result',
component: () => import('@/views/compare/CompareResultView.vue'),
meta: { title: '对比结果' },
},
// 模型管理
{
path: 'model-manage',
name: 'model-manage',
component: () => import('@/views/model/ModelManageView.vue'),
meta: { title: '模型管理', pageSurface: 'self' },
},
{
path: 'model-manage/create',
name: 'model-manage-create',
component: () => import('@/views/model/ModelCreateView.vue'),
meta: { title: '添加模型' },
},
{
path: 'model-manage/:id/edit',
name: 'model-manage-edit',
component: () => import('@/views/model/ModelCreateView.vue'),
meta: { title: '编辑模型' },
},
{
path: 'model-manage/merge',
name: 'merge-weights',
component: () => import('@/views/model/MergeWeightsView.vue'),
meta: { title: '合并权重' },
},
// 数据处理
{
path: 'data-process',
name: 'data-process',
component: () => import('@/views/data-process/DataProcessListView.vue'),
meta: { title: '数据处理', pageSurface: 'self' },
},
{
path: 'data-process/create',
name: 'data-process-create',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '新建数据处理任务', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id/regenerate',
name: 'data-process-regenerate',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '重新生成数据处理任务', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id/workflow',
name: 'data-process-workflow',
component: () => import('@/views/data-process/DataProcessCreateView.vue'),
meta: { title: '数据处理任务进度', pageSurface: 'self', backRouteName: 'data-process' },
},
{
path: 'data-process/:id',
name: 'data-process-detail',
component: () => import('@/views/data-process/DataProcessDetailView.vue'),
meta: { title: '数据处理详情' },
},
// 数据集管理
{
path: 'dataset',
name: 'dataset',
component: () => import('@/views/dataset/DatasetListView.vue'),
meta: { title: '数据集管理', pageSurface: 'self' },
},
{
path: 'dataset/create',
name: 'dataset-create',
component: () => import('@/views/dataset/DatasetCreateView.vue'),
meta: { title: '上传数据集' },
},
{
path: 'dataset/:id/edit',
name: 'dataset-edit',
component: () => import('@/views/dataset/DatasetCreateView.vue'),
meta: { title: '编辑数据集' },
},
{
path: 'dataset/:id/preview',
name: 'dataset-preview',
component: () => import('@/views/dataset/DatasetPreviewView.vue'),
meta: { title: '数据集预览' },
},
// 其他工具
{
path: 'tools',
name: 'tools',
component: () => import('@/views/tools/ToolsView.vue'),
meta: { title: '其他工具' },
},
{
path: 'tools/create',
name: 'tools-create',
component: () => import('@/views/tools/ToolCreateView.vue'),
meta: { title: '添加自定义工具' },
},
{
path: 'tools/:id/edit',
name: 'tools-edit',
component: () => import('@/views/tools/ToolCreateView.vue'),
meta: { title: '编辑自定义工具' },
},
// 数据类型转换
{
path: 'data-convert',
name: 'data-convert',
component: () => import('@/views/data-convert/DataConvertView.vue'),
meta: { title: '数据类型转换' },
},
{
path: 'compute',
name: 'compute',
component: () => import('@/views/compute/ComputeNodesView.vue'),
meta: { title: '算力节点', pageSurface: 'self', permission: 'compute' },
},
{
path: 'compute/gpus',
redirect: '/compute?tab=gpus',
},
{
path: 'compute/queue',
redirect: '/compute?tab=queue',
},
{
path: 'compute/nodes',
redirect: '/compute?tab=nodes',
},
// 系统设置
{
path: 'permission-denied',
name: 'permission-denied',
component: () => import('@/views/system/PermissionDeniedView.vue'),
meta: { title: '无权访问', skipPermission: true },
},
{
path: 'hardware',
name: 'hardware',
component: () => import('@/views/system/HardwareView.vue'),
meta: { title: '平台性能' },
},
{
path: 'logs',
name: 'logs',
component: () => import('@/views/system/LogsView.vue'),
meta: { title: '查看日志' },
},
{
path: 'user-settings',
name: 'user-settings',
component: () => import('@/views/system/UserSettingsView.vue'),
meta: { title: '用户设置', pageSurface: 'self', permission: 'user-settings' },
},
{
path: 'user-settings/create',
name: 'user-create',
component: () => import('@/views/system/UserCreateView.vue'),
meta: { title: '创建用户', permission: 'user-settings' },
},
{
path: 'user-settings/:id/permission',
name: 'user-permission',
component: () => import('@/views/system/UserPermissionView.vue'),
meta: { title: '权限设置', permission: 'user-settings' },
},
{
path: 'training-log/:id',
name: 'training-log',
component: () => import('@/views/system/TrainingLogView.vue'),
meta: { title: '训练日志' },
},
],
},
{
path: '/:pathMatch(.*)*',
redirect: '/dashboard',
},
]
const router = createRouter({
history: createWebHistory(),
routes,
})
const permissionBySegment: Record<string, PermissionCode> = {
dashboard: 'dashboard',
'fine-tune': 'fine-tune',
'training-log': 'fine-tune',
'model-eval': 'model-eval',
'model-inference': 'model-inference',
'model-compare': 'model-inference',
'model-manage': 'model-manage',
dataset: 'dataset',
'data-process': 'data-process',
'data-convert': 'data-convert',
compute: 'compute',
tools: 'data-convert',
hardware: 'hardware',
logs: 'logs',
'user-settings': 'user-settings',
tenants: 'user-settings',
projects: 'user-settings',
'audit-logs': 'user-settings',
'operation-logs': 'user-settings',
'approval-templates': 'user-settings',
'approval-instances': 'user-settings',
'resource-acl': 'user-settings',
}
function requiredPermission(path: string, explicit?: unknown) {
if (explicit) return explicit as PermissionCode
const segment = path.split('/')[1]
return permissionBySegment[segment]
}
// 全局守卫:登录校验
// 离开页面超时由 App.vue 的 visibilitychange 监听接管
//
// 权限控制规则(基于 governance-user-guide.md 设计):
// - admin 用户:可以访问所有页面
// - 非 admin 用户:默认可访问所有业务页面(训练、评测、推理、数据等)
// 仅以下页面限制 admin 访问user-settings、compute算力节点
router.beforeEach((to, _from, next) => {
if (!to.meta.public) routeLoading.value = true
const auth = useAuthStore()
document.title = to.meta.title ? `${to.meta.title} - 远光软件微调平台` : '远光软件微调平台'
if (to.meta.public) {
// 已登录访问登录页则跳转主页
if (to.name === 'login' && auth.isLoggedIn) {
next('/dashboard')
return
}
next()
return
}
if (!auth.isLoggedIn) {
auth.logout() // fire-and-forget无需阻塞跳转
next({ name: 'login' })
return
}
if (!to.meta.skipPermission) {
const permission = requiredPermission(to.path, to.meta.permission)
// 仅限制管理员专属页面的访问权限
// user-settings用户设置、租户管理、项目空间、审批、审计日志仅 admin 可访问
if (permission === 'user-settings' && !auth.isAdmin) {
next({ name: 'permission-denied', replace: true })
return
}
// compute算力节点/GPU 分配)仅 admin 可访问
if (permission === 'compute' && !auth.isAdmin) {
next({ name: 'permission-denied', replace: true })
return
}
// 其他所有业务页面对已登录用户开放,不再检查权限码
}
// 路由切换时记录业务模块访问(用于看板用户操作分布统计)
const ROUTE_TO_MODULE: Record<string, string> = {
'/fine-tune': 'fine-tune',
'/model-eval': 'model-eval',
'/model-inference': 'model-inference',
'/model-compare': 'model-inference',
'/data-process': 'data-process',
'/data-convert': 'data-convert',
'/model-manage': 'model-manage',
'/dataset-manage': 'dataset',
}
for (const [prefix, module] of Object.entries(ROUTE_TO_MODULE)) {
if (to.path.startsWith(prefix)) {
const key = `route-visit:${module}`
const last = Number(sessionStorage.getItem(key) || 0)
if (Date.now() - last >= 60000) {
sessionStorage.setItem(key, String(Date.now()))
import('@/api/modules/audit-visit').then(({ recordModuleVisit }) => {
recordModuleVisit(module, to.fullPath).catch(() => {})
}).catch(() => {})
}
break
}
}
next()
})
router.afterEach(() => {
routeLoading.value = false
})
export default router