完善桌面端界面、发布检查与邮箱域名同步
Client Quality Gates / Shared client and Web (push) Has been cancelled
Client Quality Gates / macOS Desktop (push) Has been cancelled

This commit is contained in:
Cheng Zhou
2026-07-01 10:49:31 +08:00
parent c923f887a0
commit 9cac75e85c
32 changed files with 5392 additions and 2684 deletions
File diff suppressed because it is too large Load Diff
+17 -5
View File
@@ -3,23 +3,35 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
const readLayoutSource = () => readFileSync(resolve(__dirname, "./Layout.vue"), "utf8");
const readDesktopLayoutSource = () => readFileSync(resolve(__dirname, "./DesktopLayout.vue"), "utf8");
const readWebLayoutSource = () => readFileSync(resolve(__dirname, "./WebLayout.vue"), "utf8");
const readNavigationSource = () => readFileSync(resolve(__dirname, "./layout/navigation.ts"), "utf8");
describe("desktop layout shell", () => {
it("keeps desktop-only controls behind the Tauri runtime flag", () => {
it("routes desktop and web shells through the runtime flag", () => {
const source = readLayoutSource();
expect(source).toContain("const isDesktop = isTauriRuntime()");
expect(source).toContain('v-if="isDesktop"');
expect(source).toContain("DesktopCommandPalette");
expect(source).toContain("DesktopPreferences");
expect(source).toContain("<DesktopLayout v-if=\"isDesktop\" />");
expect(source).toContain("<WebLayout v-else />");
});
it("uses route-only desktop preference storage", () => {
const source = readLayoutSource();
const source = readDesktopLayoutSource();
expect(source).toContain("readDesktopRecentRoutes");
expect(source).toContain("readDesktopFavoriteRoutes");
expect(source).toContain("recordDesktopRecentRoute");
expect(source).toContain("currentDesktopRoutePreference");
});
it("shares active route mapping between web and desktop shells", () => {
const webLayout = readWebLayoutSource();
const desktopLayout = readDesktopLayoutSource();
const navigation = readNavigationSource();
expect(webLayout).toContain("getActiveLayoutPath(route.path)");
expect(desktopLayout).toContain("getActiveLayoutPath(route.path)");
expect(navigation).toContain("export const getActiveLayoutPath");
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,298 @@
import { TEXT } from "../../locales";
export type LayoutNavigationIcon =
| "audit"
| "calendar"
| "check"
| "coin"
| "dashboard"
| "document"
| "flag"
| "folder"
| "key"
| "monitor"
| "notebook"
| "project"
| "settings"
| "subject"
| "users";
export type LayoutNavigationItem = {
label: string;
path: string;
group: string;
icon: LayoutNavigationIcon;
keywords?: string[];
children?: LayoutNavigationItem[];
};
export const getActiveLayoutPath = (path: string) => {
if (path.startsWith("/project/milestones")) return "/project/milestones";
if (path.startsWith("/project/")) return "/project/overview";
if (path.startsWith("/fees/contracts")) return "/fees/contracts";
if (path.startsWith("/drug/shipments")) return "/drug/shipments";
if (path.startsWith("/materials/equipment")) return "/materials/equipment";
if (path.startsWith("/file-versions") || path.startsWith("/trial/") || path.startsWith("/documents/")) return "/file-versions";
if (path.startsWith("/startup/feasibility") || path.startsWith("/startup/ethics")) return "/startup/feasibility-ethics";
if (path.startsWith("/startup/meeting-auth") || path.startsWith("/startup/kickoff") || path.startsWith("/startup/training")) {
return "/startup/meeting-auth";
}
if (path.startsWith("/subjects")) return "/subjects";
if (path.startsWith("/risk-issues/sae")) return "/risk-issues/sae";
if (path.startsWith("/risk-issues/pd")) return "/risk-issues/pd";
if (path.startsWith("/risk-issues/monitoring-visits")) return "/risk-issues/monitoring-visits";
if (path.startsWith("/risk-issues")) return "/risk-issues/sae";
if (path.startsWith("/etmf")) return "/etmf";
if (path.startsWith("/knowledge/medical-consult")) return "/knowledge/medical-consult";
if (path.startsWith("/knowledge/precautions")) return "/knowledge/precautions";
if (path.startsWith("/knowledge/support-files")) return "/knowledge/support-files";
if (path.startsWith("/knowledge/instruction-files")) return "/knowledge/instruction-files";
if (path.startsWith("/projects/")) return "/admin/projects";
if (path.startsWith("/admin/projects/")) return "/admin/projects";
if (path.startsWith("/admin/system-monitoring") || path.startsWith("/admin/permission-monitoring")) return "/admin/system-monitoring";
if (path.startsWith("/admin/permissions/")) return path;
return path;
};
export const buildAdminNavigationItems = (options: {
hasUser: boolean;
isAdmin: boolean;
canAccessAdminPermissions: boolean;
}): LayoutNavigationItem[] => {
if (!options.hasUser) return [];
const items: LayoutNavigationItem[] = [];
if (options.isAdmin) {
items.push({
label: TEXT.menu.accountManagement,
path: "/admin/users",
group: TEXT.menu.admin,
icon: "users",
keywords: ["user", "account"],
});
}
items.push({
label: TEXT.menu.projectManagement,
path: "/admin/projects",
group: TEXT.menu.admin,
icon: "project",
keywords: ["project"],
});
if (options.isAdmin) {
items.push(
{
label: TEXT.menu.auditLogs,
path: "/admin/audit-logs",
group: TEXT.menu.admin,
icon: "audit",
keywords: ["audit"],
},
{
label: TEXT.menu.systemMonitoring,
path: "/admin/system-monitoring",
group: TEXT.menu.admin,
icon: "monitor",
keywords: ["monitoring"],
},
{
label: "邮件服务",
path: "/admin/email-settings",
group: TEXT.menu.admin,
icon: "settings",
keywords: ["email"],
},
);
}
if (options.canAccessAdminPermissions) {
items.push({
label: TEXT.menu.permissionManagement,
path: "/admin/permissions/system",
group: TEXT.menu.admin,
icon: "key",
keywords: ["permission"],
children: [
{
label: "系统级权限",
path: "/admin/permissions/system",
group: TEXT.menu.permissionManagement,
icon: "key",
keywords: ["permission", "system"],
},
{
label: "项目权限配置",
path: "/admin/permissions/project",
group: TEXT.menu.permissionManagement,
icon: "key",
keywords: ["permission", "project"],
},
],
});
}
return items;
};
export const buildProjectNavigationItems = (options: {
hasCurrentStudy: boolean;
hasAnyProjectModuleAccess: boolean;
canAccessProjectPath: (path: string) => boolean;
}): LayoutNavigationItem[] => {
if (!options.hasCurrentStudy || !options.hasAnyProjectModuleAccess) return [];
const items: LayoutNavigationItem[] = [
{
label: TEXT.menu.projectOverview,
path: "/project/overview",
group: TEXT.menu.currentProject,
icon: "dashboard",
keywords: ["overview"],
},
{
label: TEXT.menu.projectMilestones,
path: "/project/milestones",
group: TEXT.menu.currentProject,
icon: "calendar",
keywords: ["milestone"],
},
{
label: TEXT.menu.feeContracts,
path: "/fees/contracts",
group: TEXT.menu.currentProject,
icon: "coin",
keywords: ["fee", "contract"],
},
{
label: TEXT.menu.materialManagement,
path: "/drug/shipments",
group: TEXT.menu.currentProject,
icon: "folder",
children: [
{
label: TEXT.menu.drugShipments,
path: "/drug/shipments",
group: TEXT.menu.materialManagement,
icon: "folder",
keywords: ["drug", "shipment"],
},
{
label: TEXT.menu.materialEquipment,
path: "/materials/equipment",
group: TEXT.menu.materialManagement,
icon: "folder",
keywords: ["material", "equipment"],
},
],
},
{
label: TEXT.menu.fileVersionManagement,
path: "/file-versions",
group: TEXT.menu.currentProject,
icon: "document",
keywords: ["document", "file", "version"],
},
{
label: TEXT.menu.startupFeasibilityEthics,
path: "/startup/feasibility-ethics",
group: TEXT.menu.currentProject,
icon: "calendar",
keywords: ["startup", "ethics"],
},
{
label: TEXT.menu.startupMeetingAuth,
path: "/startup/meeting-auth",
group: TEXT.menu.currentProject,
icon: "check",
keywords: ["meeting", "training"],
},
{
label: TEXT.menu.subjects,
path: "/subjects",
group: TEXT.menu.currentProject,
icon: "subject",
keywords: ["subject"],
},
{
label: TEXT.menu.riskIssues,
path: "/risk-issues/sae",
group: TEXT.menu.currentProject,
icon: "flag",
children: [
{
label: TEXT.menu.riskIssueSae,
path: "/risk-issues/sae",
group: TEXT.menu.riskIssues,
icon: "flag",
keywords: ["sae", "risk"],
},
{
label: TEXT.menu.riskIssuePd,
path: "/risk-issues/pd",
group: TEXT.menu.riskIssues,
icon: "flag",
keywords: ["pd", "risk"],
},
{
label: TEXT.menu.riskIssueMonitoringVisits,
path: "/risk-issues/monitoring-visits",
group: TEXT.menu.riskIssues,
icon: "flag",
keywords: ["monitoring", "visit"],
},
],
},
{
label: TEXT.menu.etmf,
path: "/etmf",
group: TEXT.menu.currentProject,
icon: "folder",
keywords: ["etmf"],
},
{
label: TEXT.menu.sharedLibrary,
path: "/knowledge/medical-consult",
group: TEXT.menu.currentProject,
icon: "notebook",
children: [
{
label: TEXT.menu.knowledgeMedicalConsult,
path: "/knowledge/medical-consult",
group: TEXT.menu.sharedLibrary,
icon: "notebook",
keywords: ["knowledge", "medical"],
},
{
label: TEXT.menu.knowledgeNotes,
path: "/knowledge/precautions",
group: TEXT.menu.sharedLibrary,
icon: "notebook",
keywords: ["knowledge", "note"],
},
{
label: TEXT.menu.knowledgeSupportFiles,
path: "/knowledge/support-files",
group: TEXT.menu.sharedLibrary,
icon: "notebook",
keywords: ["knowledge", "support"],
},
{
label: TEXT.menu.knowledgeInstructionFiles,
path: "/knowledge/instruction-files",
group: TEXT.menu.sharedLibrary,
icon: "notebook",
keywords: ["knowledge", "instruction"],
},
],
},
];
return items
.map((item) => {
if (!item.children) return options.canAccessProjectPath(item.path) ? item : null;
const children = item.children.filter((child) => options.canAccessProjectPath(child.path));
if (!children.length) return null;
return { ...item, path: children[0].path, children };
})
.filter(Boolean) as LayoutNavigationItem[];
};
export const flattenLayoutNavigationItems = (items: LayoutNavigationItem[]) =>
items.flatMap((item) => [item, ...(item.children || [])]);