feat(management-navigation-single-company-ux): 收敛管理导航与单公司配置体验
这个提交包含在:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户