Files
ctms/frontend/src/components/layout/navigation.ts
T
Cheng Zhou 46f488c8ae feat(desktop): 稳定桌面端界面与文件操作反馈
- 重构 DesktopPreferences 为分栏式设置面板,整合连接、外观、通知、更新与诊断信息分区,并补充过渡动效与暗色主题样式
- DesktopLayout 侧边栏导航分组支持展开折叠,调整管理/项目区块顺序并统一图标与标题
- 新增 fileTaskFeedback 工具,统一 pickFiles/saveFile/openFile 的成功/取消提示,替换审计导出、权限日志、附件、文档、线程、项目配置等处的直接调用
- desktopUpdateManager 暴露更新状态快照与状态变更监听,区分检查中、安装中、已推迟、失败等状态
- DesktopServerSettings 增加连接诊断信息(检查时间、健康地址、耗时、HTTP 状态)
- unified-page.css 与 ProjectMilestones 引入 CSS 变量以适配暗色主题
- WebLayout 将服务器设置入口改为打开系统偏好面板,管理菜单中邮件服务归入系统设置分组
- ProfileSettings 移除已迁入偏好面板的桌面端专属区块
- 补充 Layout.desktop 布局与偏好面板契约测试
2026-07-01 17:04:23 +08:00

308 lines
9.2 KiB
TypeScript

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: ["settings"],
children: [
{
label: "邮件服务",
path: "/admin/email-settings",
group: "系统设置",
icon: "settings",
keywords: ["email", "settings"],
},
],
},
);
}
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.children?.length ? item.children : [item]);