重构前端 Wave0-2 体验基线(05-28-frontend-ux-redesign)

这个提交包含在:
2026-05-28 23:03:40 +08:00
父节点 605dddf633
当前提交 8f38687186
共修改 23 个文件,包含 711 行新增和 146 行删除
+76 -20
查看文件
@@ -56,6 +56,31 @@ const LEGACY_ACCESSIBLE_LABELS: Record<string, string> = {
"/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",
"/system/workspace",
"/system/workflow-definitions",
"/system/settings",
"/system/feature-flags",
"/audit-logs",
];
const MORE_NAV_ORDER = ["/travel-requests", "/dashboard", "/settings/profile", "/system/cutover", "/system/integrations/erp"];
type ResolvedNavItem = {
key?: string;
href: string;
@@ -75,10 +100,8 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
onClick={() => onNavigate?.()}
className={({ isActive }) =>
cn(
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition duration-150",
isActive
? "bg-primary/10 text-primary ring-1 ring-primary/10"
: "text-[var(--color-text-secondary)] hover:bg-slate-50 hover:text-[var(--color-text-primary)]",
"group nav-link-item",
isActive ? "nav-link-item--active" : "nav-link-item--idle",
)
}
>
@@ -86,10 +109,8 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
<>
<span
className={cn(
"flex size-9 items-center justify-center rounded-lg border text-[20px] transition-colors",
isActive
? "border-primary/15 bg-white text-primary"
: "border-slate-200 bg-white text-[var(--color-text-tertiary)] group-hover:border-slate-300 group-hover:text-primary",
"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>
@@ -101,17 +122,14 @@ function renderNavItem(item: ResolvedNavItem, onNavigate?: () => void) {
);
}
function renderSection(title: string, description: string, items: ResolvedNavItem[], onNavigate?: () => void) {
function renderSection(title: string, items: ResolvedNavItem[], onNavigate?: () => void) {
if (!items.length) {
return null;
}
return (
<section className="space-y-2">
<div className="px-3">
<div className="text-[11px] font-bold uppercase tracking-[0.2em] text-primary/70">{title}</div>
<div className="mt-1 text-xs leading-5 text-[var(--color-text-tertiary)]">{description}</div>
</div>
<section className="nav-section">
<div className="nav-section__label">{title}</div>
<div className="space-y-1.5">{items.map((item) => renderNavItem(item, onNavigate))}</div>
</section>
);
@@ -126,10 +144,10 @@ function renderBackupSection(items: ResolvedNavItem[], onNavigate?: () => void)
<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>更多</div>
<div className="mt-0.5 text-xs font-normal text-[var(--color-text-secondary)]">低频入口和历史模块</div>
</div>
<span className="text-xs font-medium text-[var(--color-text-tertiary)]">按需展开</span>
<span className="material-symbols-outlined text-[18px] text-[var(--color-text-tertiary)]">expand_more</span>
</summary>
<div className="mt-3 space-y-1.5 pb-1">{items.map((item) => renderNavItem(item, onNavigate))}</div>
</details>
@@ -186,6 +204,41 @@ function normalizeSections(sections: { main: NavigationItem[]; admin: Navigation
};
}
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));
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 })),
],
};
}
export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const { me, permissions } = useAuth();
const navigationQuery = useQuery({
@@ -197,12 +250,15 @@ export function NavLinks({ onNavigate }: { onNavigate?: () => void }) {
const sections = navigationQuery.data?.sections
? normalizeSections(navigationQuery.data.sections)
: buildFallbackItems(permissions, me?.roleCodes ?? []);
const groupedSections = regroupSections(sections);
return (
<nav className="space-y-5">
{renderSection("主导航", "今日工作与高频协同。", sections.main, onNavigate)}
{renderSection("管理入口", "仅管理员与负责人使用。", sections.admin, onNavigate)}
{renderBackupSection(sections.backup, onNavigate)}
{renderSection("工作", groupedSections.work, onNavigate)}
{renderSection("业务", groupedSections.business, onNavigate)}
{renderSection("财务", groupedSections.finance, onNavigate)}
{renderSection("管理", groupedSections.management, onNavigate)}
{renderBackupSection(groupedSections.more, onNavigate)}
</nav>
);
}