diff --git a/client/src/components/navigation/NavLinks.tsx b/client/src/components/navigation/NavLinks.tsx index 513fa4f3..dc5bd484 100644 --- a/client/src/components/navigation/NavLinks.tsx +++ b/client/src/components/navigation/NavLinks.tsx @@ -1,6 +1,6 @@ import { NavLink } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; -import { fetchSystemNavigation } from "../../api/workspace"; +import { fetchSystemNavigation, type NavigationItem } from "../../api/workspace"; import { useAuth } from "../../auth/AuthContext"; import { hasAnyPermission } from "../../auth/permissions"; import { cn } from "../../lib/utils"; @@ -22,7 +22,6 @@ export const NAV_ITEMS: NavItem[] = [ { to: "/todos", label: "待办", icon: "check_circle" }, { to: "/announcements", label: "公告", icon: "campaign" }, { to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_own", "project.view_all", "project.manage"] }, - { to: "/contracts-registry", label: "合同台账", icon: "description", anyOf: ["project.view_own", "project.view_all", "project.manage", "system.settings"] }, { to: "/system/workspace", label: "工作区设置", icon: "tune", anyOf: ["system.settings"] }, { to: "/people", label: "人员管理", icon: "group", anyOf: ["system.users", "report.dashboard.view"] }, { to: "/system/workflow-definitions", label: "流程定义", icon: "schema", anyOf: ["system.settings"] }, @@ -30,6 +29,7 @@ export const NAV_ITEMS: NavItem[] = [ { to: "/system/feature-flags", label: "功能开关", icon: "toggle_on", anyOf: ["system.settings"] }, { to: "/audit-logs", label: "审计日志", icon: "manage_search", anyOf: ["system.audit.view"] }, { to: "/workflow/tasks", label: "审批待办", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "procurement.approve", "travel.approve"] }, + { to: "/contracts-registry", label: "合同台账", icon: "description", anyOf: ["project.view_own", "project.view_all", "project.manage", "system.settings"] }, { to: "/purchase-requests", label: "采购申请", icon: "shopping_cart", anyOf: ["procurement.create", "procurement.view_own", "procurement.view_all", "procurement.manage"] }, { to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.create", "travel.view_own", "travel.view_all", "travel.manage", "travel.payment.manage", "integration.manage"] }, { to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] }, @@ -43,17 +43,47 @@ export const NAV_ITEMS: NavItem[] = [ { to: "/settings/profile", label: "个人设置", icon: "manage_accounts" }, ]; +const MAIN_NAV_ORDER = ["/portal", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"]; +const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"]; +const BACKUP_NAV_ORDER = ["/workflow/tasks", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"]; + const LEGACY_ACCESSIBLE_LABELS: Record = { "/reimbursements": "我的报销", "/workflow/tasks": "待我处理", "/assets": "资产台账", }; -function renderSection( - title: string, - items: Array<{ key?: string; href: string; label: string; icon: string }>, - onNavigate?: () => void, -) { +type ResolvedNavItem = { + key?: string; + href: string; + label: string; + icon: string; +}; + +function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) { + return ( + onNavigate?.()} + className={({ isActive }) => + cn( + "flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors", + isActive + ? "bg-zinc-100 font-bold text-zinc-900" + : "font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900", + ) + } + > + {item.icon} + {item.label} + + ); +} + +function renderSection(title: string, items: ResolvedNavItem[], onNavigate?: () => void) { if (!items.length) { return null; } @@ -61,30 +91,29 @@ function renderSection( return (
{title}
- {items.map((item) => ( - onNavigate?.()} - className={({ isActive }) => - cn( - "flex items-center gap-3 px-3 py-2 my-1 rounded-lg text-sm transition-colors", - isActive - ? "bg-zinc-100 font-bold text-zinc-900" - : "font-medium text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900", - ) - } - > - {item.icon} - {item.label} - - ))} + {items.map((item) => renderNavItem(item, onNavigate))}
); } +function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void) { + if (!items.length) { + return null; + } + + return ( +
+ + 备用模块 + 按需展开 + +
+ {items.map((item) => renderNavItem(item, onNavigate))} +
+
+ ); +} + function buildFallbackItems(permissions: string[]) { const mapItems = (paths: string[]) => NAV_ITEMS.filter((item) => paths.includes(item.to)) @@ -92,23 +121,55 @@ function buildFallbackItems(permissions: string[]) { .map((item) => ({ key: item.to, href: item.to, label: item.label, icon: item.icon })); return { - main: mapItems(["/portal", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"]), - admin: mapItems(["/system/workspace", "/people", "/contracts-registry", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"]), - backup: mapItems(["/workflow/tasks", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard"]), + main: mapItems(MAIN_NAV_ORDER), + admin: mapItems(ADMIN_NAV_ORDER), + backup: mapItems(BACKUP_NAV_ORDER), }; } +function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) { + const allItems = [...sections.main, ...sections.admin, ...sections.backup]; + const byHref = new Map(); + for (const item of allItems) { + if (!byHref.has(item.href)) { + byHref.set(item.href, item); + } + } + + const used = new Set(); + const take = (hrefs: string[]) => hrefs + .map((href) => { + const item = byHref.get(href); + if (item) { + used.add(href); + } + return item; + }) + .filter((item): item is ResolvedNavItem => Boolean(item)); + + const main = take(MAIN_NAV_ORDER); + const admin = take(ADMIN_NAV_ORDER); + const backup = [ + ...take(BACKUP_NAV_ORDER), + ...allItems.filter((item) => !used.has(item.href)).map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon })), + ]; + + return { main, admin, backup }; +} + export function NavLinks({ onNavigate }: { onNavigate?: () => void }) { const { permissions } = useAuth(); const navigationQuery = useQuery({ queryKey: ["system-navigation"], queryFn: fetchSystemNavigation, staleTime: 60_000 }); - const sections = navigationQuery.data?.sections ?? buildFallbackItems(permissions); + const sections = navigationQuery.data?.sections + ? normalizeSections(navigationQuery.data.sections) + : buildFallbackItems(permissions); return ( -