refactor: 菜单栏角色化降噪(role-based-navigation-simplification)

这个提交包含在:
2026-05-29 21:11:32 +08:00
父节点 325a7950c9
当前提交 541593a5b6
共修改 58 个文件,包含 796 行新增和 366 行删除
+1 -1
查看文件
@@ -207,7 +207,7 @@ export function hasBusinessApproverRole(roleCodes: string[] | undefined, permiss
}
export function hasFinanceRole(roleCodes: string[] | undefined, permissionCodes: string[]) {
const hasRole = (roleCodes ?? []).some((roleCode) => (FINANCE_ROLE_CODES as readonly string[]).includes(roleCode));
const hasRole = (roleCodes ?? []).some((roleCode) => (FINANCE_ROLE_CODES as readonly string[]).includes(roleCode) || roleCode === "admin");
return hasRole && hasAnyPermission(permissionCodes, [...FINANCE_PORTAL_PERMISSIONS]);
}
+87 -133
查看文件
@@ -1,4 +1,4 @@
import { NavLink } from "react-router-dom";
import { NavLink, useLocation } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { fetchSystemNavigation, type NavigationItem } from "../../api/workspace";
import { useAuth } from "../../auth/AuthContext";
@@ -19,67 +19,53 @@ export type NavItem = {
export const NAV_ITEMS: NavItem[] = [
{ to: "/portal", label: "工作台", icon: "home_app_logo" },
{ to: "/team", label: "团队", icon: "groups" },
{ to: "/leave-requests", label: "请假", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
{ to: "/team", label: "团队", icon: "groups", anyOf: ["report.dashboard.view", "project.view_all", "project.manage", "system.users"] },
{ to: "/leave-requests", label: "请假记录", icon: "event_available", anyOf: ["leave.view_own", "leave.view_all", "leave.manage"] },
{ to: "/attendance", label: "考勤", icon: "schedule" },
{ to: "/reimbursements", label: "报销", icon: "request_quote", anyOf: ["expense.view_own", "expense.view_all"] },
{ to: "/assets", label: "资产", icon: "inventory_2", anyOf: ["asset.view_own", "asset.view_all", "asset.manage", "asset.transfer", "asset.inventory"] },
{ to: "/weekly-reports", label: "周报", icon: "article" },
{ 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: "/system/workspace", label: "工作区设置", icon: "tune", anyOf: ["system.settings"] },
{ to: "/people", label: "人员管理", icon: "group", anyOf: ["system.users", "report.dashboard.view"] },
{ to: "/organization", label: "组织架构", icon: "account_tree", anyOf: ["system.users", "report.dashboard.view"] },
{ to: "/system/workflow-definitions", label: "流程定义", icon: "schema", anyOf: ["system.settings"] },
{ to: "/system/settings", label: "系统设置", icon: "settings", anyOf: ["system.settings", "system.users", "masterdata.manage", "notification.channel.manage", "integration.manage"] },
{ 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", "attendance.approve", "procurement.approve", "travel.approve"] },
{ to: "/projects", label: "项目", icon: "workspaces", anyOf: ["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"] },
{ 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"] },
{ to: "/travel-requests", label: "差旅申请", icon: "flight", anyOf: ["travel.view_all", "travel.manage", "travel.payment.manage"] },
{ to: "/budgets", label: "预算管理", icon: "account_balance", anyOf: ["budget.view", "budget.manage"] },
{ to: "/payment-batches", label: "付款批次", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/ap-operations", label: "AP 运营", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/invoice-pool", label: "票据中心", icon: "receipt_long", anyOf: ["invoice.manage"] },
{ to: "/payment-batches", label: "付款", icon: "payments", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/ap-operations", label: "应付账款", icon: "account_balance_wallet", anyOf: ["payment.batch.manage", "risk.review.manage"] },
{ to: "/invoice-pool", label: "票据", icon: "receipt_long", anyOf: ["invoice.manage"] },
{ to: "/inventory-checks", label: "资产盘点", icon: "inventory", anyOf: ["asset.inventory"] },
{ to: "/system/cutover", label: "切换控制台", icon: "swap_calls", anyOf: ["system.settings"] },
{ to: "/system/integrations/erp", label: "外部系统桥接", icon: "hub", anyOf: ["integration.manage"] },
{ to: "/dashboard", label: "仪表盘", icon: "dashboard", anyOf: ["report.dashboard.view"] },
{ to: "/settings/profile", label: "个人设置", icon: "manage_accounts" },
{ to: "/dashboard", label: "数据看板", icon: "dashboard", anyOf: ["report.dashboard.view"] },
];
const LEGACY_ACCESSIBLE_LABELS: Record<string, string> = {
const ACCESSIBLE_LABELS: Record<string, string> = {
"/reimbursements": "我的报销",
"/workflow/tasks": "待我处理",
"/assets": "资产台账",
};
const WORK_NAV_ORDER = ["/portal", "/workflow/tasks", "/reimbursements", "/todos"];
const BUSINESS_NAV_ORDER = [
"/projects",
"/contracts-registry",
"/purchase-requests",
"/assets",
"/weekly-reports",
"/team",
"/announcements",
"/leave-requests",
"/attendance",
"/wecom-approval-archive",
];
const FINANCE_NAV_ORDER = ["/payment-batches", "/ap-operations", "/invoice-pool", "/budgets", "/inventory-checks"];
const MANAGEMENT_NAV_ORDER = [
"/people",
"/organization",
const MANAGEMENT_ACTIVE_PATHS = [
"/system/settings",
"/system/workspace",
"/system/workflow-definitions",
"/system/settings",
"/system/feature-flags",
"/system/approval-rules",
"/system/expense-standards",
"/system/reimbursement-templates",
"/system/master-data",
"/system/notification-channels",
"/system/integrations/erp",
"/system/users",
"/system/departments",
"/system/cutover",
"/people",
"/people/stats",
"/organization",
"/audit-logs",
];
const MORE_NAV_ORDER = ["/travel-requests", "/dashboard", "/settings/profile", "/system/cutover", "/system/integrations/erp"];
const TEAM_NAVIGATION_ROLE_CODES = ["manager", "ceo", "finance_mgr", "hr", "admin"];
type ResolvedNavItem = {
key?: string;
@@ -88,8 +74,16 @@ type ResolvedNavItem = {
icon: string;
};
function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
const accessibleLabel = LEGACY_ACCESSIBLE_LABELS[item.href] ?? item.label;
function isGroupedActive(item: ResolvedNavItem, pathname: string) {
if (item.href === "/system/settings") {
return MANAGEMENT_ACTIVE_PATHS.some((path) => pathname === path || pathname.startsWith(`${path}/`));
}
return pathname === item.href || pathname.startsWith(`${item.href}/`);
}
function renderNavItem(item: ResolvedNavItem, pathname: string, onNavigate?: () => void) {
const accessibleLabel = ACCESSIBLE_LABELS[item.href] ?? item.label;
return (
<NavLink
@@ -99,68 +93,65 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
title={accessibleLabel}
onClick={() => onNavigate?.()}
className={({ isActive }) =>
cn(
"group nav-link-item",
isActive ? "nav-link-item--active" : "nav-link-item--idle",
)
cn("group nav-link-item", isActive || isGroupedActive(item, pathname) ? "nav-link-item--active" : "nav-link-item--idle")
}
>
{({ isActive }) => (
<>
<span
className={cn(
"nav-link-item__icon",
isActive ? "nav-link-item__icon--active" : "nav-link-item__icon--idle",
)}
>
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
</span>
<span className="min-w-0 flex-1 truncate font-medium">{item.label}</span>
</>
)}
{({ isActive }) => {
const active = isActive || isGroupedActive(item, pathname);
return (
<>
<span className={cn("nav-link-item__icon", active ? "nav-link-item__icon--active" : "nav-link-item__icon--idle")}>
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
</span>
<span className="min-w-0 flex-1 truncate font-medium">{item.label}</span>
</>
);
}}
</NavLink>
);
}
function renderSection(title: string, items: ResolvedNavItem[], onNavigate?: () => void) {
function renderPrimarySection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
if (!items.length) {
return null;
}
return (
<section className="nav-section">
<div className="nav-section__label">{title}</div>
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, onNavigate))}</div>
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
</section>
);
}
function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void) {
function renderBackupSection(items: ResolvedNavItem[], pathname: string, onNavigate?: () => void) {
if (!items.length) {
return null;
}
return (
<details className="rounded-2xl border border-slate-200 bg-slate-50/80 px-3 py-3">
<summary className="flex cursor-pointer list-none items-center justify-between gap-3 rounded-xl px-2 py-1.5 text-sm font-semibold text-[var(--color-text-primary)]">
<div>
<div>更多</div>
<div className="mt-0.5 text-xs font-normal text-[var(--color-text-secondary)]">低频入口和历史模块</div>
</div>
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)]">expand_more</span>
<details className="nav-more-section">
<summary className="nav-more-section__summary">
<span className="nav-link-item__icon nav-link-item__icon--idle">
<span className="material-symbols-outlined text-[20px]">more_horiz</span>
</span>
<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-3 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, onNavigate))}</div>
<div className="mt-1.5 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, pathname, onNavigate))}</div>
</details>
);
}
function buildFallbackItems(permissions: string[], roleCodes: string[]) {
const financeOnlyPaths = new Set(["/payment-batches", "/ap-operations", "/invoice-pool"]);
const backupOnlyPaths = new Set(BACKUP_NAV_ORDER);
const mapItems = (paths: string[]) =>
NAV_ITEMS.filter((item) => paths.includes(item.to))
.filter((item) => item.to !== "/workflow/tasks" || hasBusinessApproverRole(roleCodes, permissions))
.filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions))
.filter((item) => item.to !== "/team" || roleCodes.some((roleCode) => TEAM_NAVIGATION_ROLE_CODES.includes(roleCode)))
.filter((item) => !financeOnlyPaths.has(item.to) || hasFinanceRole(roleCodes, permissions) || roleCodes.includes("admin"))
.filter((item) => !item.anyOf || hasAnyPermission(permissions, item.anyOf))
.filter((item) => paths === BACKUP_NAV_ORDER || !backupOnlyPaths.has(item.to))
.map((item) => ({ key: item.to, href: item.to, label: item.label, icon: item.icon }));
return {
@@ -170,77 +161,43 @@ function buildFallbackItems(permissions: string[], roleCodes: string[]) {
};
}
function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) {
const allItems = [...sections.main, ...sections.admin, ...sections.backup];
function orderItems(items: ResolvedNavItem[], order: string[]) {
const byHref = new Map<string, ResolvedNavItem>();
for (const item of allItems) {
for (const item of items) {
if (!byHref.has(item.href)) {
byHref.set(item.href, item);
}
}
const used = new Set<string>();
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 ordered = order
.map((href) => {
const item = byHref.get(href);
if (item) {
used.add(href);
}
return item;
})
.filter((item): item is ResolvedNavItem => Boolean(item));
return {
main: take(MAIN_NAV_ORDER),
admin: take(ADMIN_NAV_ORDER),
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 [...ordered, ...items.filter((item) => !used.has(item.href))];
}
function regroupSections(sections: { main: ResolvedNavItem[]; admin: ResolvedNavItem[]; backup: ResolvedNavItem[] }) {
const allItems = [...sections.main, ...sections.admin, ...sections.backup];
const byHref = new Map<string, ResolvedNavItem>();
for (const item of allItems) {
if (!byHref.has(item.href)) {
byHref.set(item.href, item);
}
}
const used = new Set<string>();
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));
function toResolved(items: NavigationItem[]) {
return items.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon }));
}
function normalizeSections(sections: { main: NavigationItem[]; admin: NavigationItem[]; backup: NavigationItem[] }) {
return {
work: take(WORK_NAV_ORDER),
business: take(BUSINESS_NAV_ORDER),
finance: take(FINANCE_NAV_ORDER),
management: take(MANAGEMENT_NAV_ORDER),
more: [
...take(MORE_NAV_ORDER),
...allItems
.filter((item) => !used.has(item.href))
.map((item) => ({ key: item.key, href: item.href, label: item.label, icon: item.icon })),
],
main: orderItems(toResolved(sections.main), MAIN_NAV_ORDER),
admin: orderItems(toResolved(sections.admin), ADMIN_NAV_ORDER),
backup: orderItems(toResolved(sections.backup), BACKUP_NAV_ORDER),
};
}
export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const { me, permissions } = useAuth();
const location = useLocation();
const navigationQuery = useQuery({
queryKey: ["system-navigation"],
queryFn: fetchSystemNavigation,
@@ -250,15 +207,12 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const sections = navigationQuery.data?.sections
? normalizeSections(navigationQuery.data.sections)
: buildFallbackItems(permissions, me?.roleCodes ?? []);
const groupedSections = regroupSections(sections);
const primaryItems = [...sections.main, ...sections.admin];
return (
<nav className="space-y-5">
{renderSection("工作", groupedSections.work, onNavigate)}
{renderSection("业务", groupedSections.business, onNavigate)}
{renderSection("财务", groupedSections.finance, onNavigate)}
{renderSection("管理", groupedSections.management, onNavigate)}
{renderBackupSection(groupedSections.more, onNavigate)}
<nav className="space-y-3.5">
{renderPrimarySection(primaryItems, location.pathname, onNavigate)}
{renderBackupSection(sections.backup, location.pathname, onNavigate)}
</nav>
);
}
@@ -6,13 +6,6 @@ export function Sidebar() {
<aside className="app-shell-sidebar fixed inset-y-0 z-20 hidden w-72 overflow-hidden border-r border-slate-200 lg:flex lg:flex-col">
<div className="border-b border-slate-200 px-6 py-5">
<AppBrand />
<div className="sidebar-focus-card mt-4 text-[var(--color-text-secondary)]">
<div className="flex items-center gap-2 font-semibold text-[var(--color-text-primary)]">
<span className="size-2 rounded-full bg-primary" />
今日优先
</div>
<div className="mt-1.5 leading-6">待办、报销、项目和财务流程按优先级进入。</div>
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4 [scrollbar-width:thin]">
+1 -1
查看文件
@@ -42,7 +42,7 @@ export type MobileNavItem = {
export const MOBILE_NAV_ITEMS: MobileNavItem[] = [
{ key: "home", to: "/mobile", label: "首页", icon: "home" },
{ key: "approvals", to: "/mobile/approvals", label: "审批", icon: "fact_check" },
{ key: "approvals", to: "/mobile/approvals", label: "待办", icon: "fact_check" },
{ key: "reimbursements", to: "/mobile/reimbursements", label: "报销", icon: "request_quote", anyOf: MOBILE_REIMBURSEMENT_PERMISSIONS },
{ key: "me", to: "/mobile/me", label: "我的", icon: "person" },
];
+11 -11
查看文件
@@ -1,6 +1,6 @@
export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/team", "/leave-requests", "/reimbursements", "/assets", "/weekly-reports", "/todos", "/announcements", "/projects"];
export const ADMIN_NAV_ORDER = ["/system/workspace", "/people", "/organization", "/system/workflow-definitions", "/system/settings", "/system/feature-flags", "/audit-logs"];
export const BACKUP_NAV_ORDER = ["/attendance", "/wecom-approval-archive", "/contracts-registry", "/purchase-requests", "/travel-requests", "/budgets", "/payment-batches", "/ap-operations", "/invoice-pool", "/inventory-checks", "/system/cutover", "/system/integrations/erp", "/dashboard", "/settings/profile"];
export const MAIN_NAV_ORDER = ["/portal", "/workflow/tasks", "/reimbursements", "/payment-batches", "/ap-operations", "/invoice-pool", "/projects", "/team", "/leave-requests"];
export const ADMIN_NAV_ORDER = ["/system/settings"];
export const BACKUP_NAV_ORDER = ["/assets", "/weekly-reports", "/announcements", "/purchase-requests", "/contracts-registry", "/budgets", "/attendance", "/wecom-approval-archive", "/travel-requests", "/inventory-checks", "/dashboard"];
const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/portal", title: "工作台", group: "工作台" },
@@ -13,18 +13,18 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/purchase-requests", title: "采购申请", group: "业务" },
{ path: "/travel-requests", title: "差旅申请", group: "更多" },
{ path: "/budgets", title: "预算管理", group: "财务" },
{ path: "/payment-batches", title: "付款批次", group: "财务" },
{ path: "/ap-operations", title: "应付作业", group: "财务" },
{ path: "/invoice-pool", title: "票据中心", group: "财务" },
{ path: "/payment-batches", title: "付款", group: "财务" },
{ path: "/ap-operations", title: "应付账款", group: "财务" },
{ path: "/invoice-pool", title: "票据", group: "财务" },
{ path: "/inventory-checks", title: "盘点任务", group: "财务" },
{ path: "/weekly-reports", title: "周报", group: "业务" },
{ path: "/todos", title: "待办", group: "工作" },
{ path: "/reimbursements/", title: "报销详情", group: "工作" },
{ path: "/reimbursements", title: "报销", group: "工作" },
{ path: "/leave-requests", title: "请假", group: "业务" },
{ path: "/leave-requests", title: "请假记录", group: "业务" },
{ path: "/assets", title: "资产", group: "业务" },
{ path: "/workflow/tasks/", title: "审批详情", group: "工作" },
{ path: "/workflow/tasks", title: "审批待办", group: "工作" },
{ path: "/workflow/tasks", title: "待我处理", group: "工作" },
{ path: "/mobile/assets/scan", title: "移动资产扫描", group: "移动工作台" },
{ path: "/mobile/approvals", title: "移动审批", group: "移动工作台" },
{ path: "/mobile/wecom-approval-archive", title: "移动企微审批历史", group: "移动工作台" },
@@ -41,10 +41,10 @@ const PAGE_TITLES: Array<{ path: string; title: string; group: string }> = [
{ path: "/organization", title: "组织架构", group: "管理" },
{ path: "/system/approval-rules", title: "审批规则", group: "管理" },
{ path: "/system/workflow-definitions", title: "流程定义", group: "管理" },
{ path: "/system/settings", title: "系统设置", group: "管理" },
{ path: "/system/feature-flags", title: "功能开关", group: "管理" },
{ path: "/system/settings", title: "管理", group: "管理" },
{ path: "/system/feature-flags", title: "功能管理", group: "管理" },
{ path: "/system/cutover", title: "切换控制台", group: "更多" },
{ path: "/system/integrations/erp", title: "ERP 集成", group: "更多" },
{ path: "/system/integrations/erp", title: "外部系统", group: "更多" },
{ path: "/dashboard", title: "数据看板", group: "更多" },
{ path: "/audit-logs", title: "审计日志", group: "管理" },
];
+22
查看文件
@@ -152,6 +152,28 @@
color: var(--color-accent-primary);
}
.nav-more-section {
@apply space-y-1.5;
}
.nav-more-section__summary {
color: var(--color-text-secondary);
@apply flex cursor-pointer list-none items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm transition duration-150 hover:bg-slate-50 hover:text-slate-950;
}
.nav-more-section__summary::-webkit-details-marker {
display: none;
}
.nav-more-section__chevron {
color: var(--color-text-tertiary);
@apply text-[18px] transition-transform;
}
.nav-more-section[open] .nav-more-section__chevron {
transform: rotate(180deg);
}
.auth-shell {
background:
radial-gradient(circle at 20% 12%, rgba(0, 57, 138, 0.10), transparent 26rem),
+1 -1
查看文件
@@ -262,7 +262,7 @@ export function ErpIntegrationPage() {
return (
<div className="space-y-6">
<PageHeader
title="ERP 集成"
title="外部系统"
subtitle="主数据同步、业务队列和 ID 映射。"
actions={
tab === "master-data" ? (
+3 -3
查看文件
@@ -81,9 +81,9 @@ function formatRolloutTypeLabel(value: FeatureFlagRolloutType) {
}
function formatSectionLabel(value: FeatureFlagSection) {
if (value === "main") return "主导航";
if (value === "main") return "常用入口";
if (value === "admin") return "管理入口";
if (value === "backup") return "备用入口";
if (value === "backup") return "更多";
return value;
}
@@ -324,7 +324,7 @@ export function FeatureFlagsPage() {
return (
<ListWorkspace className={batchMode ? "pb-32" : undefined}>
<WorkspaceHeader
title="功能开关"
title="功能管理"
subtitle="外部桥接、验真、灰度和试运行边界。"
/>
<WorkspaceNotice title="安全摘要" tone="warning">
+1 -1
查看文件
@@ -1033,7 +1033,7 @@ export function MobileApprovalsPage() {
/>
) : (
<PageHeader
title={canAccessRemoteApprovals ? "审批" : "审批进度"}
title={canAccessRemoteApprovals ? "待办" : "进度"}
subtitle={canAccessRemoteApprovals ? "处理待办,查看已处理。" : "查看我发起的申请进度。"}
/>
)}
+1 -1
查看文件
@@ -128,7 +128,7 @@ export function MobileMePage() {
<MobileActionLink to="/mobile/leave" label="请假" icon="event_available" />
<MobileActionLink to="/mobile/reimbursements" label="报销" icon="request_quote" />
{canAccessPurchase ? <MobileActionLink to="/mobile/purchase-requests" label="采购" icon="shopping_cart" /> : null}
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "审批" : "审批进度"} icon="fact_check" />
<MobileActionLink to="/mobile/approvals" label={canAccessApprovals ? "待办" : "进度"} icon="fact_check" />
{canAccessFinance ? <MobileActionLink to="/mobile#finance" label="财务" description="审批与付款状态" icon="account_balance_wallet" /> : null}
{canAccessAssets ? <MobileActionLink to="/mobile/assets/scan" label="资产扫码" icon="inventory_2" /> : null}
{canAccessProjects ? <MobileActionLink to="/mobile" label="项目动态" icon="workspaces" /> : null}
@@ -75,7 +75,7 @@ const ROLE_OPTIONS = [
] as const;
const DOMAIN_SCOPE_OPTIONS = [
{ value: "workflow", label: "审批待办" },
{ value: "workflow", label: "待我处理" },
{ value: "reimbursement", label: "报销" },
{ value: "leave", label: "请假" },
{ value: "procurement", label: "采购" },
+1 -1
查看文件
@@ -16,7 +16,7 @@ const PAGE_SIZE = 20;
const NOTIFICATION_TYPE_OPTIONS = [
{ label: "全部", value: "" },
{ label: "审批待办", value: "workflow.task_assigned" },
{ label: "待我处理", value: "workflow.task_assigned" },
{ label: "报销", value: "reimbursement.approved,reimbursement.rejected,reimbursement.returned,reimbursement.paid" },
{ label: "请假", value: "leave.approved,leave.rejected" },
{ label: "采购", value: "purchase_request.approved,purchase_request.rejected" },
+5 -5
查看文件
@@ -163,8 +163,8 @@ function buildTodoAggregateItems(payload: PortalHomePayload) {
const items: TodoAggregateItem[] = [];
if (payload.pendingTasks.count > 0) {
items.push({
title: `审批待办 ${payload.pendingTasks.count} 条`,
sourceLabel: "审批",
title: `待我处理 ${payload.pendingTasks.count} 条`,
sourceLabel: "待办",
severity: "warning",
href: payload.pendingTasks.href,
icon: "fact_check",
@@ -925,9 +925,9 @@ export function PortalHomePage() {
? {
key: "approval-pending",
href: payload.pendingTasks.href,
label: "审批待办",
label: "待我处理",
value: `${payload.pendingTasks.count} 条`,
note: payload.pendingTasks.count > 0 ? "进入审批中心处理。" : "暂无待处理审批。",
note: payload.pendingTasks.count > 0 ? "进入待我处理办理。" : "暂无待处理事项。",
tone: payload.pendingTasks.count > 0 ? "warning" as const : "normal" as const,
}
: null,
@@ -1051,7 +1051,7 @@ export function PortalHomePage() {
? { label: "报销进度", to: "/reimbursements" }
: null,
payload.pendingTasks.count > 0 && approvalAction?.to
? { label: "审批中心", to: approvalAction.to }
? { label: "待我处理", to: approvalAction.to }
: null,
todoAggregateItems.length > 1
? { label: "全部待办", to: "/todos" }
+76 -27
查看文件
@@ -1,41 +1,90 @@
import { Link } from "react-router-dom";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
const ITEMS = [
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区设置", description: "导航与模块显隐。" },
{ to: "/people", icon: "badge", title: "人员管理", description: "入转调离与档案。" },
{ to: "/system/approval-rules", icon: "account_tree", title: "审批规则", description: "审批链与规则。" },
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限与管控。" },
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销模板。" },
{ to: "/system/master-data", icon: "database", title: "主数据", description: "法体、成本中心、供应商。" },
{ to: "/system/notification-channels", icon: "notifications", title: "通知渠道", description: "IM / 邮件补充通知。" },
{ to: "/system/integrations/erp", icon: "hub", title: "外部系统桥接", description: "财务出入口与桥接。" },
type ManagementItem = {
to: string;
icon: string;
title: string;
description: string;
group: "人员与组织" | "流程与规则" | "系统与审计" | "功能管理";
anyOf: string[];
};
const ITEMS: ManagementItem[] = [
{ to: "/people", icon: "badge", title: "人员", description: "员工档案、入转调离和人员状态。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
{ to: "/organization", icon: "account_tree", title: "组织", description: "组织结构、部门和汇报关系。", group: "人员与组织", anyOf: ["system.users", "report.dashboard.view"] },
{ to: "/system/users", icon: "manage_accounts", title: "账号", description: "账号、角色和登录权限。", group: "人员与组织", anyOf: ["system.users"] },
{ to: "/system/departments", icon: "account_tree", title: "部门", description: "部门基础数据维护。", group: "人员与组织", anyOf: ["system.users"] },
{ to: "/system/approval-rules", icon: "rule", title: "审批规则", description: "金额段、审批层级和处理规则。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/workflow-definitions", icon: "schema", title: "流程定义", description: "流程模板和节点治理。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/expense-standards", icon: "rule_settings", title: "费用标准", description: "费用上限、类别和管控口径。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/reimbursement-templates", icon: "receipt_long", title: "报销模板", description: "常见报销场景模板。", group: "流程与规则", anyOf: ["system.settings"] },
{ to: "/system/workspace", icon: "dashboard_customize", title: "工作区", description: "菜单入口、模块显隐和小团队基线。", group: "系统与审计", anyOf: ["system.settings"] },
{ to: "/audit-logs", icon: "manage_search", title: "审计日志", description: "关键操作留痕和排障。", group: "系统与审计", anyOf: ["system.audit.view"] },
{ to: "/system/master-data", icon: "dataset", title: "主数据", description: "法体、成本中心、供应商等基础数据。", group: "功能管理", anyOf: ["masterdata.view", "masterdata.manage"] },
{ to: "/system/notification-channels", icon: "notifications_active", title: "通知渠道", description: "企业微信、邮件等补充通知。", group: "功能管理", anyOf: ["notification.channel.manage"] },
{ to: "/system/feature-flags", icon: "toggle_on", title: "功能管理", description: "功能开关、灰度和试运行边界。", group: "功能管理", anyOf: ["system.settings"] },
{ to: "/system/integrations/erp", icon: "hub", title: "外部系统", description: "财务、ERP 和外部系统连接。", group: "功能管理", anyOf: ["integration.manage"] },
];
const GROUPS: ManagementItem["group"][] = ["人员与组织", "流程与规则", "系统与审计", "功能管理"];
export function SystemSettingsPage() {
const { permissions } = useAuth();
const visibleItems = ITEMS.filter((item) => hasAnyPermission(permissions, item.anyOf));
const visibleGroups = GROUPS.map((group) => ({
group,
items: visibleItems.filter((item) => item.group === group),
})).filter((entry) => entry.items.length > 0);
return (
<ListWorkspace>
<WorkspaceHeader title="系统设置" subtitle="后台能力入口按管理职责收拢。" />
<WorkspaceHeader title="管理" subtitle="低频配置集中到这里,侧边栏只保留一个管理入口。" />
<div className="finance-status-strip" data-testid="system-settings-summary">
<div className="finance-metric-card"><div className="finance-metric-card__label">入口总数</div><div className="finance-metric-card__value">{ITEMS.length}</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">业务配置</div><div className="finance-metric-card__value">5</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">外部桥接</div><div className="finance-metric-card__value">2</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">人员相关</div><div className="finance-metric-card__value">1</div></div>
<div className="finance-metric-card"><div className="finance-metric-card__label">模式</div><div className="finance-metric-card__value text-base">小团队</div></div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">可用入口</div>
<div className="finance-metric-card__value">{visibleItems.length}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">人员与组织</div>
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "人员与组织").length}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">流程与规则</div>
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "流程与规则").length}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">系统与审计</div>
<div className="finance-metric-card__value">{visibleItems.filter((item) => item.group === "系统与审计").length}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">模式</div>
<div className="finance-metric-card__value text-base">小团队</div>
</div>
</div>
<div className="admin-tile-grid">
{ITEMS.map((item) => (
<Link key={item.to} to={item.to} className="admin-tile-card">
<div className="flex items-start gap-3">
<span className="admin-tile-card__icon">
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
</span>
<span className="min-w-0">
<span className="block text-base font-semibold text-zinc-900">{item.title}</span>
<span className="mt-1 block text-sm leading-5 text-zinc-600">{item.description}</span>
</span>
<div className="space-y-5">
{visibleGroups.map(({ group, items }) => (
<section key={group} className="admin-command-panel space-y-3">
<h2 className="text-base font-semibold text-zinc-900">{group}</h2>
<div className="admin-tile-grid">
{items.map((item) => (
<Link key={item.to} to={item.to} className="admin-tile-card">
<div className="flex items-start gap-3">
<span className="admin-tile-card__icon">
<span className="material-symbols-outlined text-[20px]">{item.icon}</span>
</span>
<span className="min-w-0">
<span className="block text-base font-semibold text-zinc-900">{item.title}</span>
<span className="mt-1 block text-sm leading-5 text-zinc-600">{item.description}</span>
</span>
</div>
</Link>
))}
</div>
</Link>
</section>
))}
</div>
</ListWorkspace>
+16 -16
查看文件
@@ -11,7 +11,7 @@ import { toast } from "sonner";
const MODULE_LABELS: Record<string, string> = {
portal: "工作台",
team: "团队",
leaveRequests: "请假",
leaveRequests: "请假记录",
attendance: "考勤",
reimbursements: "报销",
assets: "资产",
@@ -24,31 +24,31 @@ const MODULE_LABELS: Record<string, string> = {
organization: "组织架构",
contractsRegistry: "合同台账",
workflowDefinitions: "流程定义",
systemSettings: "系统设置",
featureFlags: "功能开关",
systemSettings: "管理",
featureFlags: "功能管理",
auditLogs: "审计日志",
approvalRules: "审批规则",
userManagement: "用户管理",
departments: "部门管理",
masterData: "主数据",
notificationChannels: "通知渠道",
workflowTasks: "审批待办",
workflowTasks: "待我处理",
dashboard: "仪表盘",
purchaseRequests: "采购申请",
travelRequests: "差旅申请",
budgets: "预算管理",
paymentBatches: "付款批次",
apOperations: "AP 运营",
invoicePool: "票据中心",
paymentBatches: "付款",
apOperations: "应付账款",
invoicePool: "票据",
inventoryChecks: "资产盘点",
cutover: "切换控制台",
erpIntegrations: "ERP 集成",
erpIntegrations: "外部系统",
profile: "个人设置",
};
const VISIBILITY_OPTIONS = [
{ value: "main", label: "主导航" },
{ value: "admin", label: "管理区" },
{ value: "main", label: "常用入口" },
{ value: "admin", label: "管理与更多" },
{ value: "hidden", label: "隐藏" },
] as const;
@@ -101,7 +101,7 @@ export function WorkspaceSetupPage() {
<WorkspaceHeader
eyebrow="管理"
title="工作区设置"
subtitle="按 10 人以下小团队模式收敛主导航与默认入口。"
subtitle="按 10 人以下小团队模式收敛常用入口与低频模块。"
/>
<div className="admin-command-panel text-sm text-zinc-500">加载工作区设置中...</div>
</ListWorkspace>
@@ -113,7 +113,7 @@ export function WorkspaceSetupPage() {
<WorkspaceHeader
eyebrow="管理"
title="工作区设置"
subtitle="按小团队运营模式收敛主导航、管理入口和备用模块。"
subtitle="按小团队运营模式收敛常用入口、管理入口和低频模块。"
/>
<div className="finance-status-strip" data-testid="workspace-settings-summary">
@@ -122,11 +122,11 @@ export function WorkspaceSetupPage() {
<div className="finance-metric-card__value">{workspaceStats.total}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">主导航</div>
<div className="finance-metric-card__label">常用入口</div>
<div className="finance-metric-card__value">{workspaceStats.main}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">管理区</div>
<div className="finance-metric-card__label">管理与更多</div>
<div className="finance-metric-card__value">{workspaceStats.admin}</div>
</div>
<div className="finance-metric-card">
@@ -171,9 +171,9 @@ export function WorkspaceSetupPage() {
<section className="admin-command-panel space-y-4">
<div>
<h3 className="text-base font-semibold text-zinc-900">模块显隐</h3>
<h3 className="text-base font-semibold text-zinc-900">入口显隐</h3>
<p className="mt-1 text-sm leading-6 text-zinc-600">
主导航只保留高频入口;备用模块保留代码与数据,低频功能默认收进管理区或隐藏。
常用入口只保留高频任务;低频能力保留代码与数据,默认收进管理、更多或隐藏。
</p>
</div>