Files
ctms/frontend/src/router/index.ts
T
2026-01-12 11:14:04 +08:00

428 lines
15 KiB
TypeScript

import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { useAuthStore } from "../store/auth";
import { useStudyStore } from "../store/study";
import Layout from "../components/Layout.vue";
import Login from "../views/Login.vue";
import Register from "../views/Register.vue";
import ForgotPassword from "../views/ForgotPassword.vue";
import StudyHome from "../views/StudyHome.vue";
import MyWorkbench from "../views/workbench/MyWorkbench.vue";
import FaqDetail from "../views/FaqDetail.vue";
import AuditLogs from "../views/admin/AuditLogs.vue";
import AdminUsers from "../views/admin/Users.vue";
import AdminUserApproval from "../views/admin/AdminUserApproval.vue";
import AdminProjects from "../views/admin/Projects.vue";
import ProjectMembers from "../views/admin/ProjectMembers.vue";
import AdminSites from "../views/admin/Sites.vue";
import ProjectDetail from "../views/admin/ProjectDetail.vue";
import ProfileSettings from "../views/ProfileSettings.vue";
import ProjectOverview from "../views/ia/ProjectOverview.vue";
import FinanceContracts from "../views/ia/FinanceContracts.vue";
import FinanceSpecial from "../views/ia/FinanceSpecial.vue";
import DrugShipments from "../views/ia/DrugShipments.vue";
import StartupFeasibilityEthics from "../views/ia/StartupFeasibilityEthics.vue";
import StartupMeetingAuth from "../views/ia/StartupMeetingAuth.vue";
import SubjectManagement from "../views/ia/SubjectManagement.vue";
import MonitoringPlaceholder from "../views/ia/MonitoringPlaceholder.vue";
import AuditPlaceholder from "../views/ia/AuditPlaceholder.vue";
import KnowledgeMedicalConsult from "../views/ia/KnowledgeMedicalConsult.vue";
import KnowledgeNotes from "../views/ia/KnowledgeNotes.vue";
import ContractForm from "../views/finance/ContractForm.vue";
import ContractDetail from "../views/finance/ContractDetail.vue";
import SpecialForm from "../views/finance/SpecialForm.vue";
import SpecialDetail from "../views/finance/SpecialDetail.vue";
import ShipmentForm from "../views/drug/ShipmentForm.vue";
import ShipmentDetail from "../views/drug/ShipmentDetail.vue";
import FeasibilityForm from "../views/startup/FeasibilityForm.vue";
import FeasibilityDetail from "../views/startup/FeasibilityDetail.vue";
import EthicsForm from "../views/startup/EthicsForm.vue";
import EthicsDetail from "../views/startup/EthicsDetail.vue";
import KickoffForm from "../views/startup/KickoffForm.vue";
import KickoffDetail from "../views/startup/KickoffDetail.vue";
import TrainingForm from "../views/startup/TrainingForm.vue";
import TrainingDetail from "../views/startup/TrainingDetail.vue";
import NoteForm from "../views/knowledge/NoteForm.vue";
import NoteDetail from "../views/knowledge/NoteDetail.vue";
import SubjectForm from "../views/subjects/SubjectForm.vue";
import SubjectDetail from "../views/subjects/SubjectDetail.vue";
import { TEXT } from "../locales";
const routes: RouteRecordRaw[] = [
{
path: "/login",
name: "Login",
component: Login,
meta: { public: true, title: TEXT.common.actions.login },
},
{
path: "/register",
name: "Register",
component: Register,
meta: { public: true, title: TEXT.common.actions.register },
},
{
path: "/forgot-password",
name: "ForgotPassword",
component: ForgotPassword,
meta: { public: true, title: TEXT.modules.auth.forgotTitle },
},
{
path: "/",
component: Layout,
redirect: "/workbench",
children: [
{
path: "workbench",
name: "MyWorkbench",
component: MyWorkbench,
meta: { title: TEXT.menu.workbench },
},
{
path: "profile",
name: "ProfileSettings",
component: ProfileSettings,
meta: { title: TEXT.menu.profile },
},
{
path: "project/overview",
name: "ProjectOverview",
component: ProjectOverview,
meta: { title: TEXT.menu.projectOverview, requiresStudy: true },
},
{
path: "finance/contracts",
name: "FinanceContracts",
component: FinanceContracts,
meta: { title: TEXT.menu.financeContracts, requiresStudy: true },
},
{
path: "finance/contracts/new",
name: "FinanceContractNew",
component: ContractForm,
meta: { title: TEXT.common.actions.add + TEXT.menu.financeContracts, requiresStudy: true },
},
{
path: "finance/contracts/:contractId",
name: "FinanceContractDetail",
component: ContractDetail,
meta: { title: TEXT.modules.financeContracts.detailTitle, requiresStudy: true },
},
{
path: "finance/contracts/:contractId/edit",
name: "FinanceContractEdit",
component: ContractForm,
meta: { title: TEXT.common.actions.edit + TEXT.menu.financeContracts, requiresStudy: true },
},
{
path: "finance/special",
name: "FinanceSpecial",
component: FinanceSpecial,
meta: { title: TEXT.menu.financeSpecials, requiresStudy: true },
},
{
path: "finance/special/new",
name: "FinanceSpecialNew",
component: SpecialForm,
meta: { title: TEXT.common.actions.add + TEXT.menu.financeSpecials, requiresStudy: true },
},
{
path: "finance/special/:specialId",
name: "FinanceSpecialDetail",
component: SpecialDetail,
meta: { title: TEXT.modules.financeSpecials.detailTitle, requiresStudy: true },
},
{
path: "finance/special/:specialId/edit",
name: "FinanceSpecialEdit",
component: SpecialForm,
meta: { title: TEXT.common.actions.edit + TEXT.menu.financeSpecials, requiresStudy: true },
},
{
path: "drug/shipments",
name: "DrugShipments",
component: DrugShipments,
meta: { title: TEXT.menu.drugShipments, requiresStudy: true },
},
{
path: "drug/shipments/new",
name: "DrugShipmentNew",
component: ShipmentForm,
meta: { title: TEXT.common.actions.add + TEXT.menu.drugShipments, requiresStudy: true },
},
{
path: "drug/shipments/:shipmentId",
name: "DrugShipmentDetail",
component: ShipmentDetail,
meta: { title: TEXT.modules.drugShipments.detailTitle, requiresStudy: true },
},
{
path: "drug/shipments/:shipmentId/edit",
name: "DrugShipmentEdit",
component: ShipmentForm,
meta: { title: TEXT.common.actions.edit + TEXT.menu.drugShipments, requiresStudy: true },
},
{
path: "startup/feasibility-ethics",
name: "StartupFeasibilityEthics",
component: StartupFeasibilityEthics,
meta: { title: TEXT.menu.startupFeasibilityEthics, requiresStudy: true },
},
{
path: "startup/feasibility/new",
name: "StartupFeasibilityNew",
component: FeasibilityForm,
meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true },
},
{
path: "startup/feasibility/:recordId",
name: "StartupFeasibilityDetail",
component: FeasibilityDetail,
meta: { title: TEXT.modules.startupFeasibilityEthics.feasibilityDetailTitle, requiresStudy: true },
},
{
path: "startup/feasibility/:recordId/edit",
name: "StartupFeasibilityEdit",
component: FeasibilityForm,
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.feasibilityTab, requiresStudy: true },
},
{
path: "startup/ethics/new",
name: "StartupEthicsNew",
component: EthicsForm,
meta: { title: TEXT.common.actions.add + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true },
},
{
path: "startup/ethics/:recordId",
name: "StartupEthicsDetail",
component: EthicsDetail,
meta: { title: TEXT.modules.startupFeasibilityEthics.ethicsDetailTitle, requiresStudy: true },
},
{
path: "startup/ethics/:recordId/edit",
name: "StartupEthicsEdit",
component: EthicsForm,
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupFeasibilityEthics.ethicsTab, requiresStudy: true },
},
{
path: "startup/meeting-auth",
name: "StartupMeetingAuth",
component: StartupMeetingAuth,
meta: { title: TEXT.menu.startupMeetingAuth, requiresStudy: true },
},
{
path: "startup/kickoff/new",
name: "StartupKickoffNew",
component: KickoffForm,
meta: { title: TEXT.common.actions.add + TEXT.modules.startupMeetingAuth.kickoffTab, requiresStudy: true },
},
{
path: "startup/kickoff/:meetingId",
name: "StartupKickoffDetail",
component: KickoffDetail,
meta: { title: TEXT.modules.startupMeetingAuth.kickoffDetailTitle, requiresStudy: true },
},
{
path: "startup/kickoff/:meetingId/edit",
name: "StartupKickoffEdit",
component: KickoffForm,
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupMeetingAuth.kickoffTab, requiresStudy: true },
},
{
path: "startup/training/new",
name: "StartupTrainingNew",
component: TrainingForm,
meta: { title: TEXT.common.actions.add + TEXT.common.fields.person, requiresStudy: true },
},
{
path: "startup/training/:recordId",
name: "StartupTrainingDetail",
component: TrainingDetail,
meta: { title: TEXT.modules.startupMeetingAuth.trainingDetailTitle, requiresStudy: true },
},
{
path: "startup/training/:recordId/edit",
name: "StartupTrainingEdit",
component: TrainingForm,
meta: { title: TEXT.common.actions.edit + TEXT.modules.startupMeetingAuth.trainingTab, requiresStudy: true },
},
{
path: "subjects",
name: "SubjectManagement",
component: SubjectManagement,
meta: { title: TEXT.menu.subjects, requiresStudy: true },
},
{
path: "subjects/new",
name: "SubjectNew",
component: SubjectForm,
meta: { title: TEXT.common.actions.add + TEXT.menu.subjects, requiresStudy: true },
},
{
path: "subjects/:subjectId",
name: "SubjectDetail",
component: SubjectDetail,
meta: { title: TEXT.modules.subjectDetail.title, requiresStudy: true },
},
{
path: "subjects/:subjectId/edit",
name: "SubjectEdit",
component: SubjectForm,
meta: { title: TEXT.common.actions.edit + TEXT.menu.subjects, requiresStudy: true },
},
{
path: "monitoring",
name: "MonitoringPlaceholder",
component: MonitoringPlaceholder,
meta: { title: TEXT.menu.monitoring, requiresStudy: true },
},
{
path: "audit",
name: "AuditPlaceholder",
component: AuditPlaceholder,
meta: { title: TEXT.menu.audit, requiresStudy: true },
},
{
path: "knowledge/medical-consult",
name: "KnowledgeMedicalConsult",
component: KnowledgeMedicalConsult,
meta: { title: TEXT.menu.knowledgeMedicalConsult, requiresStudy: true },
},
{
path: "knowledge/medical-consult/:itemId",
name: "KnowledgeMedicalConsultDetail",
component: FaqDetail,
meta: { title: TEXT.modules.knowledgeMedicalConsult.detailTitle, requiresStudy: true },
},
{
path: "knowledge/notes",
name: "KnowledgeNotes",
component: KnowledgeNotes,
meta: { title: TEXT.menu.knowledgeNotes, requiresStudy: true },
},
{
path: "knowledge/notes/new",
name: "KnowledgeNoteNew",
component: NoteForm,
meta: { title: TEXT.common.actions.add + TEXT.menu.knowledgeNotes, requiresStudy: true },
},
{
path: "knowledge/notes/:noteId",
name: "KnowledgeNoteDetail",
component: NoteDetail,
meta: { title: TEXT.modules.knowledgeNotes.detailTitle, requiresStudy: true },
},
{
path: "knowledge/notes/:noteId/edit",
name: "KnowledgeNoteEdit",
component: NoteForm,
meta: { title: TEXT.common.actions.edit + TEXT.menu.knowledgeNotes, requiresStudy: true },
},
],
},
{
path: "/admin",
component: Layout,
redirect: "/admin/users",
children: [
{
path: "users",
name: "AdminUsers",
component: AdminUsers,
meta: { title: TEXT.menu.accountManagement, requiresAdmin: true },
},
{
path: "user-approval",
name: "AdminUserApproval",
component: AdminUserApproval,
meta: { title: TEXT.modules.adminUserApproval.title, requiresAdmin: true },
},
{
path: "projects",
name: "AdminProjects",
component: AdminProjects,
meta: { title: TEXT.menu.projectManagement, requiresAdmin: true },
},
{
path: "projects/:projectId/members",
name: "AdminProjectMembers",
component: ProjectMembers,
meta: { title: TEXT.modules.adminProjectMembers.memberLabel, requiresAdmin: true },
},
{
path: "projects/:projectId/sites",
name: "AdminSites",
component: AdminSites,
meta: { title: TEXT.modules.adminSites.title, requiresAdmin: true },
},
{
path: "audit-logs",
name: "AdminAuditLogs",
component: AuditLogs,
meta: { title: TEXT.menu.auditLogs, requiresAdmin: true },
},
],
},
{
path: "/projects/:projectId",
name: "ProjectDetail",
component: ProjectDetail,
meta: { title: TEXT.modules.adminProjects.detailTitle, requiresAdmin: true },
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
router.beforeEach(async (to, _from, next) => {
const auth = useAuthStore();
const studyStore = useStudyStore();
const getToken = () => auth.token;
if (!auth.user && getToken()) {
try {
await auth.fetchMe();
} catch {
// token 失效或用户不可用时立即登出,避免重复 401
auth.logout();
}
}
const token = getToken();
if (token && !studyStore.currentStudy) {
await studyStore.ensureDefaultStudy();
}
if (!to.meta.public && !token) {
next({ path: "/login" });
return;
}
if ((to.path === "/login" || to.path === "/register") && token) {
if (!auth.forceLogin) {
next({ path: studyStore.currentStudy ? "/project/overview" : "/workbench" });
return;
}
}
if (to.meta.requiresAdmin && auth.user?.role !== "ADMIN") {
next({ path: "/workbench" });
return;
}
if (to.name === "AuditLogs") {
const role = auth.user?.role;
const studyRole = studyStore.currentStudyRole;
if (!(role === "ADMIN" || role === "PM" || studyRole === "PM")) {
next({ path: "/project/overview" });
return;
}
}
if (to.meta.requiresStudy && !studyStore.currentStudy) {
next({ path: "/workbench" });
return;
}
if (to.path === "/workbench" && studyStore.currentStudy) {
next({ path: "/project/overview" });
return;
}
next();
});
export default router;