feat(management-navigation-single-company-ux): 收敛管理导航与单公司配置体验

这个提交包含在:
2026-08-03 12:43:00 +08:00
父节点 f4ddc18f12
当前提交 4b0184c64b
共修改 36 个文件,包含 1249 行新增和 178 行删除
+32 -31
查看文件
@@ -8,6 +8,10 @@ import {
BACKUP_NAV_ORDER,
MAIN_NAV_ORDER,
} from "../../features/navigation/schema";
import {
isManagementPath,
MANAGEMENT_ROOT_PERMISSIONS,
} from "../../features/navigation/managementRoutes";
import { cn } from "../../lib/utils";
export type NavItem = {
@@ -27,7 +31,7 @@ export const NAV_ITEMS: NavItem[] = [
{ to: "/weekly-reports", label: "周报", icon: "article" },
{ to: "/announcements", label: "公告", icon: "campaign" },
{ to: "/projects", label: "项目", icon: "workspaces", anyOf: ["project.view_own", "project.view_all", "project.manage"] },
{ to: "/system/settings", label: "管理", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage", "system.audit.view", "payment.account.read", "payment.account.manage"] },
{ to: "/system/settings", label: "管理", icon: "settings", anyOf: MANAGEMENT_ROOT_PERMISSIONS },
{ to: "/workflow/tasks", label: "待我处理", icon: "fact_check", anyOf: ["expense.approve", "leave.approve", "attendance.approve", "procurement.approve", "travel.approve"] },
{ to: "/contracts-registry", label: "合同", icon: "description", anyOf: ["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"] },
@@ -46,24 +50,6 @@ const ACCESSIBLE_LABELS: Record<string, string> = {
"/assets": "资产",
};
const MANAGEMENT_ACTIVE_PATHS = [
"/system/settings",
"/system/workspace",
"/system/workflow-definitions",
"/system/feature-flags",
"/system/approval-rules",
"/system/expense-standards",
"/system/reimbursement-templates",
"/system/master-data",
"/system/notification-channels",
"/system/users",
"/system/departments",
"/system/cutover",
"/people",
"/people/stats",
"/organization",
"/audit-logs",
];
const TEAM_NAVIGATION_ROLE_CODES = ["manager", "ceo", "finance_mgr", "hr", "admin"];
const PAYMENT_REVIEWER_ROLE_CODES = ["manager", "ceo"];
@@ -79,15 +65,15 @@ type ResolvedNavItem = {
icon: string;
};
function isGroupedActive(item: ResolvedNavItem, pathname: string) {
function isGroupedActive(item: ResolvedNavItem, pathname: string, permissions: string[]) {
if (item.href === "/system/settings") {
return MANAGEMENT_ACTIVE_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
return isManagementPath(pathname, permissions);
}
return pathname === item.href || pathname.startsWith(`${item.href}/`);
}
function renderNavItem(item: ResolvedNavItem, pathname: string, onNavigate?: () => void) {
function renderNavItem(item: ResolvedNavItem, pathname: string, permissions: string[], onNavigate?: () => void) {
const accessibleLabel = ACCESSIBLE_LABELS[item.href] ?? item.label;
return (
@@ -98,11 +84,11 @@ function renderNavItem(item: ResolvedNavItem, pathname: string, onNavigate?: ()
title={accessibleLabel}
onClick={() => onNavigate?.()}
className={({ isActive }) =>
cn("group nav-link-item", isActive || isGroupedActive(item, pathname) ? "nav-link-item--active" : "nav-link-item--idle")
cn("group nav-link-item", isActive || isGroupedActive(item, pathname, permissions) ? "nav-link-item--active" : "nav-link-item--idle")
}
>
{({ isActive }) => {
const active = isActive || isGroupedActive(item, pathname);
const active = isActive || isGroupedActive(item, pathname, permissions);
return (
<>
<span className={cn("nav-link-item__icon", active ? "nav-link-item__icon--active" : "nav-link-item__icon--idle")}>
@@ -116,19 +102,19 @@ function renderNavItem(item: ResolvedNavItem, pathname: string, onNavigate?: ()
);
}
function renderPrimarySection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
function renderPrimarySection(items: ResolvedNavItem[], pathname: string, permissions: string[], onNavigate?: () => void) {
if (!items.length) {
return null;
}
return (
<section className="nav-section">
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, pathname, permissions, onNavigate))}</div>
</section>
);
}
function renderBackupSection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
function renderBackupSection(items: ResolvedNavItem[], pathname: string, permissions: string[], onNavigate?: () => void) {
if (!items.length) {
return null;
}
@@ -142,7 +128,7 @@ function renderBackupSection(items: ResolvedNavItem[], pathname: string, onNavig
<span className="min-w-0 flex-1 truncate font-medium">更多</span>
<span className="material-symbols-outlined nav-more-section__chevron">expand_more</span>
</summary>
<div className="mt-1.5 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
<div className="mt-1.5 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, pathname, permissions, onNavigate))}</div>
</details>
);
}
@@ -202,6 +188,20 @@ function normalizeSections(sections: { main: NavigationItem[]; admin: Navigation
};
}
function normalizeManagementRoot(
sections: { main: ResolvedNavItem[]; admin: ResolvedNavItem[]; backup: ResolvedNavItem[] },
permissions: string[],
) {
const canAccessManagement = hasAnyPermission(permissions, MANAGEMENT_ROOT_PERMISSIONS);
const withoutManagement = sections.admin.filter((item) => item.href !== "/system/settings");
return {
...sections,
admin: canAccessManagement
? [{ key: "/system/settings", href: "/system/settings", label: "管理", icon: "settings" }, ...withoutManagement]
: withoutManagement,
};
}
export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const { me, permissions } = useAuth();
const location = useLocation();
@@ -211,15 +211,16 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
staleTime: 60_000,
});
const sections = navigationQuery.data?.sections
const baseSections = navigationQuery.data?.sections
? normalizeSections(navigationQuery.data.sections)
: buildFallbackItems(permissions, me?.roleCodes ?? []);
const sections = normalizeManagementRoot(baseSections, permissions);
const primaryItems = [...sections.main, ...sections.admin];
return (
<nav className="space-y-3.5">
{renderPrimarySection(primaryItems, location.pathname, onNavigate)}
{renderBackupSection(sections.backup, location.pathname, onNavigate)}
{renderPrimarySection(primaryItems, location.pathname, permissions, onNavigate)}
{renderBackupSection(sections.backup, location.pathname, permissions, onNavigate)}
</nav>
);
}