完善桌面端界面、发布检查与邮箱域名同步
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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 || [])]);
|
||||
Reference in New Issue
Block a user