release(main): 同步 dev 最新候选改动
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (push) Has been cancelled
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled
Client Quality Gates / Shared client and Web (pull_request) Has been cancelled
Client Quality Gates / macOS Desktop (pull_request) Has been cancelled
Storage Persistence Guard / storage-persistence-audit (pull_request) Has been cancelled
This commit is contained in:
+186
-74
@@ -7,59 +7,76 @@ import { findFirstAccessibleProjectPath, getProjectRoutePermission, hasProjectPe
|
||||
import { fetchStudies, fetchStudyDetail } from "../api/studies";
|
||||
import { fetchApiEndpointPermissions } from "../api/projectPermissions";
|
||||
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 DesktopServerSettings from "../views/DesktopServerSettings.vue";
|
||||
import StudyHome from "../views/StudyHome.vue";
|
||||
import FaqDetail from "../views/FaqDetail.vue";
|
||||
import AuditLogs from "../views/admin/AuditLogs.vue";
|
||||
import AdminUsers from "../views/admin/Users.vue";
|
||||
import AdminProjects from "../views/admin/Projects.vue";
|
||||
import AdminSites from "../views/admin/Sites.vue";
|
||||
import PermissionManagement from "../views/admin/PermissionManagement.vue";
|
||||
import SystemMonitoringPage from "../views/admin/SystemMonitoringPage.vue";
|
||||
import EmailSettings from "../views/admin/EmailSettings.vue";
|
||||
import ProjectDetail from "../views/admin/ProjectDetail.vue";
|
||||
import ProfileSettings from "../views/ProfileSettings.vue";
|
||||
import ProjectOverview from "../views/ia/ProjectOverview.vue";
|
||||
import ProjectMilestones from "../views/ia/ProjectMilestones.vue";
|
||||
import FeeContracts from "../views/fees/ContractFees.vue";
|
||||
import FeeContractDetail from "../views/fees/ContractFeeDetail.vue";
|
||||
import DrugShipments from "../views/ia/DrugShipments.vue";
|
||||
import ShipmentDetail from "../views/drug/ShipmentDetail.vue";
|
||||
import MaterialEquipment from "../views/ia/MaterialEquipment.vue";
|
||||
import MaterialEquipmentDetail from "../views/materials/MaterialEquipmentDetail.vue";
|
||||
import FileVersionManagement from "../views/ia/FileVersionManagement.vue";
|
||||
import DocumentList from "../views/documents/DocumentList.vue";
|
||||
import DocumentDetail from "../views/documents/DocumentDetail.vue";
|
||||
import StartupFeasibilityEthics from "../views/ia/StartupFeasibilityEthics.vue";
|
||||
import StartupMeetingAuth from "../views/ia/StartupMeetingAuth.vue";
|
||||
import SubjectManagement from "../views/ia/SubjectManagement.vue";
|
||||
import RiskIssueSae from "../views/ia/RiskIssueSae.vue";
|
||||
import RiskIssuePd from "../views/ia/RiskIssuePd.vue";
|
||||
import RiskIssueMonitoringVisits from "../views/ia/RiskIssueMonitoringVisits.vue";
|
||||
import EtmfPlaceholder from "../views/ia/EtmfPlaceholder.vue";
|
||||
import KnowledgeMedicalConsult from "../views/ia/KnowledgeMedicalConsult.vue";
|
||||
import Precautions from "../views/ia/Precautions.vue";
|
||||
import KnowledgeSupportFiles from "../views/ia/KnowledgeSupportFiles.vue";
|
||||
import KnowledgeInstructionFiles from "../views/ia/KnowledgeInstructionFiles.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 TrainingDetail from "../views/startup/TrainingDetail.vue";
|
||||
import PrecautionForm from "../views/knowledge/PrecautionForm.vue";
|
||||
import PrecautionDetail from "../views/knowledge/PrecautionDetail.vue";
|
||||
import SubjectForm from "../views/subjects/SubjectForm.vue";
|
||||
import SubjectDetail from "../views/subjects/SubjectDetail.vue";
|
||||
import { TEXT } from "../locales";
|
||||
import { resolveDesktopRouteRedirect } from "./desktopGuard";
|
||||
import { isTauriRuntime } from "../runtime";
|
||||
import { DESKTOP_SESSION_RESTORE_PATH, shouldPreserveDesktopSessionOnAuthCheckFailure } from "../session/authRecovery";
|
||||
|
||||
const Login = () => import("../views/Login.vue");
|
||||
const Register = () => import("../views/Register.vue");
|
||||
const ForgotPassword = () => import("../views/ForgotPassword.vue");
|
||||
const DesktopServerSettings = () => import("../views/DesktopServerSettings.vue");
|
||||
const DesktopProjectEntry = () => import("../views/DesktopProjectEntry.vue");
|
||||
const DesktopSessionRestore = () => import("../views/DesktopSessionRestore.vue");
|
||||
const WebWorkbenchEntry = () => import("../views/WebWorkbenchEntry.vue");
|
||||
const FaqDetail = () => import("../views/FaqDetail.vue");
|
||||
const AuditLogs = () => import("../views/admin/AuditLogs.vue");
|
||||
const AdminUsers = () => import("../views/admin/Users.vue");
|
||||
const AdminProjects = () => import("../views/admin/Projects.vue");
|
||||
const AdminSites = () => import("../views/admin/Sites.vue");
|
||||
const PermissionManagement = () => import("../views/admin/PermissionManagement.vue");
|
||||
const SystemMonitoringPage = () => import("../views/admin/SystemMonitoringPage.vue");
|
||||
const EmailSettings = () => import("../views/admin/EmailSettings.vue");
|
||||
const ProjectDetail = () => import("../views/admin/ProjectDetail.vue");
|
||||
const ProjectOverview = () => import("../views/ia/ProjectOverview.vue");
|
||||
const ProjectMilestones = () => import("../views/ia/ProjectMilestones.vue");
|
||||
const ProjectNotifications = () => import("../views/ProjectNotifications.vue");
|
||||
const FeeContracts = () => import("../views/fees/ContractFees.vue");
|
||||
const FeeContractDetail = () => import("../views/fees/ContractFeeDetail.vue");
|
||||
const DrugShipments = () => import("../views/ia/DrugShipments.vue");
|
||||
const ShipmentDetail = () => import("../views/drug/ShipmentDetail.vue");
|
||||
const MaterialEquipment = () => import("../views/ia/MaterialEquipment.vue");
|
||||
const MaterialEquipmentDetail = () => import("../views/materials/MaterialEquipmentDetail.vue");
|
||||
const DocumentList = () => import("../views/documents/DocumentList.vue");
|
||||
const DocumentDetail = () => import("../views/documents/DocumentDetail.vue");
|
||||
const OfficePreviewWorkspace = () => import("../views/OfficePreviewWorkspace.vue");
|
||||
const StartupFeasibilityEthics = () => import("../views/ia/StartupFeasibilityEthics.vue");
|
||||
const StartupMeetingAuth = () => import("../views/ia/StartupMeetingAuth.vue");
|
||||
const SubjectManagement = () => import("../views/ia/SubjectManagement.vue");
|
||||
const RiskIssueSae = () => import("../views/ia/RiskIssueSae.vue");
|
||||
const RiskIssuePd = () => import("../views/ia/RiskIssuePd.vue");
|
||||
const RiskIssueMonitoringVisits = () => import("../views/ia/RiskIssueMonitoringVisits.vue");
|
||||
const EtmfPlaceholder = () => import("../views/ia/EtmfPlaceholder.vue");
|
||||
const KnowledgeMedicalConsult = () => import("../views/ia/KnowledgeMedicalConsult.vue");
|
||||
const Precautions = () => import("../views/ia/Precautions.vue");
|
||||
const KnowledgeSupportFiles = () => import("../views/ia/KnowledgeSupportFiles.vue");
|
||||
const KnowledgeInstructionFiles = () => import("../views/ia/KnowledgeInstructionFiles.vue");
|
||||
const CollaborationLibrary = () => import("../views/knowledge/CollaborationLibrary.vue");
|
||||
const CollaborationWorkspace = () => import("../views/knowledge/CollaborationWorkspace.vue");
|
||||
const CollaborationShareWorkspace = () => import("../views/knowledge/CollaborationShareWorkspace.vue");
|
||||
const FeasibilityForm = () => import("../views/startup/FeasibilityForm.vue");
|
||||
const FeasibilityDetail = () => import("../views/startup/FeasibilityDetail.vue");
|
||||
const EthicsForm = () => import("../views/startup/EthicsForm.vue");
|
||||
const EthicsDetail = () => import("../views/startup/EthicsDetail.vue");
|
||||
const KickoffForm = () => import("../views/startup/KickoffForm.vue");
|
||||
const KickoffDetail = () => import("../views/startup/KickoffDetail.vue");
|
||||
const TrainingDetail = () => import("../views/startup/TrainingDetail.vue");
|
||||
const PrecautionForm = () => import("../views/knowledge/PrecautionForm.vue");
|
||||
const PrecautionDetail = () => import("../views/knowledge/PrecautionDetail.vue");
|
||||
const SubjectForm = () => import("../views/subjects/SubjectForm.vue");
|
||||
const SubjectDetail = () => import("../views/subjects/SubjectDetail.vue");
|
||||
|
||||
const SYSTEM_PERMISSION_READ = "system:permissions:read";
|
||||
const SYSTEM_PERMISSION_PROJECT_CONFIG = "system:permissions:project_config";
|
||||
const WEB_WORKBENCH_ENTRY_PATH = "/workbench";
|
||||
const DESKTOP_PROJECT_ENTRY_PATH = "/desktop/project-entry";
|
||||
|
||||
const resolveWorkbenchEntryPath = (isDesktopRuntime: boolean) =>
|
||||
isDesktopRuntime ? DESKTOP_PROJECT_ENTRY_PATH : WEB_WORKBENCH_ENTRY_PATH;
|
||||
|
||||
const isWorkbenchEntryPath = (path: string) =>
|
||||
path === WEB_WORKBENCH_ENTRY_PATH || path === DESKTOP_PROJECT_ENTRY_PATH;
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -80,12 +97,36 @@ const routes: RouteRecordRaw[] = [
|
||||
component: ForgotPassword,
|
||||
meta: { public: true, title: TEXT.modules.auth.forgotTitle },
|
||||
},
|
||||
{
|
||||
path: "/collaboration/share",
|
||||
name: "CollaborationPublicShare",
|
||||
component: CollaborationShareWorkspace,
|
||||
meta: { public: true, title: "共享协作文件" },
|
||||
},
|
||||
{
|
||||
path: "/desktop/server-settings",
|
||||
name: "DesktopServerSettings",
|
||||
component: DesktopServerSettings,
|
||||
meta: { public: true, title: "服务器设置" },
|
||||
},
|
||||
{
|
||||
path: DESKTOP_SESSION_RESTORE_PATH,
|
||||
name: "DesktopSessionRestore",
|
||||
component: DesktopSessionRestore,
|
||||
meta: { public: true, title: "恢复登录状态" },
|
||||
},
|
||||
{
|
||||
path: WEB_WORKBENCH_ENTRY_PATH,
|
||||
name: "WorkbenchEntry",
|
||||
component: WebWorkbenchEntry,
|
||||
meta: { title: "工作台入口" },
|
||||
},
|
||||
{
|
||||
path: DESKTOP_PROJECT_ENTRY_PATH,
|
||||
name: "DesktopProjectEntry",
|
||||
component: DesktopProjectEntry,
|
||||
meta: { title: "选择工作入口" },
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: Layout,
|
||||
@@ -109,6 +150,12 @@ const routes: RouteRecordRaw[] = [
|
||||
component: ProjectMilestones,
|
||||
meta: { title: TEXT.menu.projectMilestones, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "project/notifications",
|
||||
name: "ProjectNotifications",
|
||||
component: ProjectNotifications,
|
||||
meta: { title: "提醒中心", requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "fees/contracts",
|
||||
name: "FeeContracts",
|
||||
@@ -148,7 +195,7 @@ const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "file-versions",
|
||||
name: "FileVersionManagement",
|
||||
component: FileVersionManagement,
|
||||
component: DocumentList,
|
||||
meta: { title: TEXT.menu.fileVersionManagement, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
@@ -163,6 +210,24 @@ const routes: RouteRecordRaw[] = [
|
||||
component: DocumentDetail,
|
||||
meta: { title: TEXT.menu.fileVersionManagement, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "office-preview/attachment/:id",
|
||||
name: "OfficeAttachmentPreview",
|
||||
component: OfficePreviewWorkspace,
|
||||
meta: { title: "Office 文件预览", requiresStudy: true, fullBleed: true, transientWorkspaceTask: true },
|
||||
},
|
||||
{
|
||||
path: "office-preview/version/:id",
|
||||
name: "OfficeVersionPreview",
|
||||
component: OfficePreviewWorkspace,
|
||||
meta: { title: "Office 文件预览", requiresStudy: true, fullBleed: true, transientWorkspaceTask: true },
|
||||
},
|
||||
{
|
||||
path: "office-preview/collaboration/:fileId/revision/:id",
|
||||
name: "OfficeCollaborationRevisionPreview",
|
||||
component: OfficePreviewWorkspace,
|
||||
meta: { title: "协作历史版本预览", requiresStudy: true, fullBleed: true, transientWorkspaceTask: true },
|
||||
},
|
||||
{
|
||||
path: "startup/feasibility-ethics",
|
||||
name: "StartupFeasibilityEthics",
|
||||
@@ -329,6 +394,25 @@ const routes: RouteRecordRaw[] = [
|
||||
component: KnowledgeInstructionFiles,
|
||||
meta: { title: TEXT.menu.knowledgeInstructionFiles, requiresStudy: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/collaboration",
|
||||
name: "KnowledgeCollaboration",
|
||||
component: CollaborationLibrary,
|
||||
meta: { title: TEXT.menu.knowledgeCollaboration, requiresStudy: true, fullBleed: true },
|
||||
},
|
||||
{
|
||||
path: "knowledge/collaboration/:fileId",
|
||||
name: "KnowledgeCollaborationWorkspace",
|
||||
component: CollaborationWorkspace,
|
||||
meta: {
|
||||
title: TEXT.menu.knowledgeCollaboration,
|
||||
requiresStudy: true,
|
||||
fullBleed: true,
|
||||
immersiveWorkspace: true,
|
||||
transientWorkspaceTask: true,
|
||||
workspaceTaskGroup: TEXT.menu.knowledgeCollaboration,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -408,7 +492,7 @@ const routes: RouteRecordRaw[] = [
|
||||
path: "permissions/project",
|
||||
name: "AdminPermissionsProject",
|
||||
component: PermissionManagement,
|
||||
meta: { title: "项目权限配置", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG },
|
||||
meta: { title: "项目访问控制", systemPermission: SYSTEM_PERMISSION_PROJECT_CONFIG },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -512,20 +596,34 @@ const ensureProjectPermissionAccess = async (
|
||||
};
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const isDesktopRuntime = isTauriRuntime();
|
||||
const desktopRedirect = resolveDesktopRouteRedirect(to);
|
||||
if (desktopRedirect) {
|
||||
next({ path: desktopRedirect });
|
||||
return;
|
||||
}
|
||||
if (isDesktopRuntime && to.path === WEB_WORKBENCH_ENTRY_PATH) {
|
||||
next({ path: DESKTOP_PROJECT_ENTRY_PATH });
|
||||
return;
|
||||
}
|
||||
if (!isDesktopRuntime && to.path === DESKTOP_PROJECT_ENTRY_PATH) {
|
||||
next({ path: WEB_WORKBENCH_ENTRY_PATH });
|
||||
return;
|
||||
}
|
||||
|
||||
const auth = useAuthStore();
|
||||
const studyStore = useStudyStore();
|
||||
const getToken = () => auth.token;
|
||||
let token = getToken();
|
||||
if (!auth.user && getToken()) {
|
||||
const isDesktopSessionRestoreRoute = to.path === DESKTOP_SESSION_RESTORE_PATH;
|
||||
if (!to.meta.public && !auth.user && getToken() && !isDesktopSessionRestoreRoute) {
|
||||
try {
|
||||
await auth.fetchMe();
|
||||
} catch {
|
||||
await auth.fetchMe({ disableNetworkRetry: true, suppressErrorMessage: true });
|
||||
} catch (error) {
|
||||
if (isDesktopRuntime && shouldPreserveDesktopSessionOnAuthCheckFailure(error)) {
|
||||
next({ path: DESKTOP_SESSION_RESTORE_PATH, query: { redirect: to.fullPath } });
|
||||
return;
|
||||
}
|
||||
// 有 token 但无法获取用户,强制回登录,避免进入“无用户上下文”页面
|
||||
await auth.logout();
|
||||
next({ path: "/login" });
|
||||
@@ -534,38 +632,52 @@ router.beforeEach(async (to, _from, next) => {
|
||||
}
|
||||
token = getToken();
|
||||
const isAdmin = isSystemAdmin(auth.user);
|
||||
if (token && !studyStore.currentStudy) {
|
||||
if (isAdmin) {
|
||||
await studyStore.ensureDefaultActiveStudy();
|
||||
} else {
|
||||
await studyStore.ensureDefaultStudy();
|
||||
}
|
||||
}
|
||||
if (token && studyStore.currentStudy && auth.user?.email) {
|
||||
studyStore.rememberCurrentStudyForUser(auth.user.email);
|
||||
}
|
||||
if (!to.meta.public && !token) {
|
||||
next({ path: "/login" });
|
||||
return;
|
||||
}
|
||||
if (isAdmin && to.path === "/") {
|
||||
next({ path: studyStore.currentStudy ? "/project/overview" : "/admin/users" });
|
||||
const workbenchEntryPath = resolveWorkbenchEntryPath(isDesktopRuntime);
|
||||
if (token && studyStore.currentStudy && auth.user?.email) {
|
||||
studyStore.rememberCurrentStudyForUser(auth.user.email);
|
||||
}
|
||||
|
||||
if (token && isWorkbenchEntryPath(to.path) && studyStore.currentStudy) {
|
||||
studyStore.clearCurrentStudy();
|
||||
}
|
||||
|
||||
if (isDesktopRuntime && token && !isAdmin && to.path.startsWith("/admin")) {
|
||||
const hasPmProject = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM";
|
||||
if (!hasPmProject) await studyStore.ensureDefaultPmStudy().catch(() => {});
|
||||
const canAccessProjectManagement = studyStore.currentStudyRole === "PM" || (studyStore.currentStudy as any)?.role_in_study === "PM";
|
||||
if (!canAccessProjectManagement) {
|
||||
next({ path: DESKTOP_PROJECT_ENTRY_PATH });
|
||||
return;
|
||||
}
|
||||
if (!studyStore.currentPermissions) await studyStore.loadCurrentStudyPermissions().catch(() => {});
|
||||
}
|
||||
|
||||
if (isDesktopRuntime && token && isAdmin && to.path.startsWith("/admin") && studyStore.currentStudy) {
|
||||
studyStore.clearCurrentStudy();
|
||||
}
|
||||
|
||||
if (isDesktopRuntime && token && to.path === "/") {
|
||||
next({ path: studyStore.currentStudy ? "/project/overview" : DESKTOP_PROJECT_ENTRY_PATH });
|
||||
return;
|
||||
}
|
||||
if (token && to.path === "/") {
|
||||
next({ path: studyStore.currentStudy ? "/project/overview" : workbenchEntryPath });
|
||||
return;
|
||||
}
|
||||
if ((to.path === "/login" || to.path === "/register") && token) {
|
||||
if (!auth.forceLogin) {
|
||||
next({
|
||||
path: isAdmin
|
||||
? studyStore.currentStudy
|
||||
? "/project/overview"
|
||||
: "/admin/users"
|
||||
: studyStore.currentStudy
|
||||
? "/project/overview"
|
||||
: "/admin/projects",
|
||||
});
|
||||
next({ path: workbenchEntryPath });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (to.name === "FileVersionManagement" && studyStore.currentStudy?.id) {
|
||||
next({ name: "DocumentList", params: { trialId: studyStore.currentStudy.id }, replace: true });
|
||||
return;
|
||||
}
|
||||
if (to.meta.adminProjectPermission) {
|
||||
const allowed = await ensureAdminProjectAccess(to, isAdmin).catch(() => false);
|
||||
if (!allowed) {
|
||||
@@ -621,7 +733,7 @@ router.beforeEach(async (to, _from, next) => {
|
||||
}
|
||||
}
|
||||
if (to.meta.requiresStudy && !studyStore.currentStudy) {
|
||||
next({ path: isAdmin ? "/admin/users" : "/admin/projects" });
|
||||
next({ path: workbenchEntryPath });
|
||||
return;
|
||||
}
|
||||
if (to.meta.requiresStudy && studyStore.currentStudy) {
|
||||
|
||||
Reference in New Issue
Block a user