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

This commit is contained in:
Cheng Zhou
2026-07-16 17:15:50 +08:00
parent 32167fba02
commit d5279b124f
393 changed files with 51630 additions and 9711 deletions
+186 -74
View File
@@ -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) {