重构 PC 首页与报销列表降噪(frontend-simplification-refactor)

这个提交包含在:
2026-05-29 11:25:45 +08:00
父节点 a5549bdce6
当前提交 03f31958fa
共修改 18 个文件,包含 689 行新增和 296 行删除
+147 -92
查看文件
@@ -67,6 +67,16 @@ type QuickActionConfig = {
disabled?: boolean;
};
type PortalFocusAction = {
label: string;
to: string;
};
type PortalFocusLink = {
label: string;
to: string;
};
type GettingStartedChecklistItem = {
key: string;
title: string;
@@ -129,11 +139,6 @@ function compactCards(cards: PortalHomePayload["cards"], excludedKeys: string[])
return cards.filter((card) => !excludedKeys.includes(card.key));
}
function formatWeeklyStatus(payload: PortalHomePayload["weeklyReport"]) {
if (!payload) return "未启用";
return payload.status === "submitted" ? "已提交" : "待填写";
}
function formatRelativeTime(value: string | null) {
if (!value) {
return "-";
@@ -411,6 +416,7 @@ function SecondaryCardsPanel({ title, cards }: { title: string; cards: PortalHom
{cards.map((card) => (
<MetricCard
key={card.key}
ariaLabel={`portal-card-${card.key}`}
title={card.title}
value={card.value}
href={card.href}
@@ -481,7 +487,7 @@ function CompactMetricLinks({ title, cards }: { title: string; cards: PortalHome
return (
<CompactGroup title={title}>
{cards.length ? cards.slice(0, 6).map((card) => (
<Link key={card.key} to={card.href} className="portal-compact-row">
<Link key={card.key} to={card.href} className="portal-compact-row" aria-label={`portal-card-${card.key}`}>
<span className="min-w-0 truncate">{card.title}</span>
<span className={card.severity === "high" || card.severity === "warning" ? "shrink-0 font-semibold text-amber-700" : "shrink-0 font-semibold text-slate-900"}>
{card.value}
@@ -713,47 +719,77 @@ function QuickActionsPanel({
);
}
function TodoAggregatePanel({ items }: { items: TodoAggregateItem[] }) {
const visibleItems = items.slice(0, 5);
const hiddenCount = Math.max(items.length - visibleItems.length, 0);
function TodayTaskPanel({
primaryAction,
secondaryLinks,
focusItem,
hiddenFocusCount,
}: {
primaryAction: PortalFocusAction | null;
secondaryLinks: PortalFocusLink[];
focusItem: TodoAggregateItem | null;
hiddenFocusCount: number;
}) {
return (
<SectionPanel
title="今日待办焦点"
description="审批、TODO 和异常提醒集中在这里。"
className="portal-panel-fill"
>
<div className="space-y-3">
{visibleItems.length ? visibleItems.map((item) => (
<Link key={`${item.title}-${item.href}-${item.sourceLabel}`} to={item.href} className="portal-list-card">
<div className="flex items-start gap-3">
<div className={item.severity === "high"
? "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-red-50 text-red-600"
: item.severity === "warning"
? "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-amber-50 text-amber-600"
: "mt-0.5 flex size-9 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600"}
>
<span className="material-symbols-outlined text-[18px]">{item.icon}</span>
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2">
<div className="font-medium text-zinc-900">{item.title}</div>
<span className="badge-neutral">{item.sourceLabel}</span>
{item.severity === "high" ? <span className="badge-error">高优先</span> : null}
{item.severity === "warning" ? <span className="badge-warning">关注</span> : null}
<SectionPanel title="今日任务" className="portal-panel-fill">
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.45fr),minmax(280px,0.72fr)] xl:items-stretch">
<div className="rounded-lg border border-slate-200 bg-white/80 px-3.5 py-3">
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-semibold text-zinc-900">当前焦点</div>
{hiddenFocusCount > 0 ? <div className="text-xs text-zinc-500">另有 {hiddenFocusCount} 项</div> : null}
</div>
{focusItem ? (
<Link
to={focusItem.href}
className="mt-3 block rounded-lg border border-slate-200 bg-slate-50/70 px-3 py-2.5 transition-colors hover:border-slate-300 hover:bg-white"
>
<div className="flex items-center gap-3">
<div className={focusItem.severity === "high"
? "flex size-9 shrink-0 items-center justify-center rounded-lg bg-red-50 text-red-600"
: focusItem.severity === "warning"
? "flex size-9 shrink-0 items-center justify-center rounded-lg bg-amber-50 text-amber-600"
: "flex size-9 shrink-0 items-center justify-center rounded-lg bg-zinc-100 text-zinc-600"}
>
<span className="material-symbols-outlined text-[18px]">{focusItem.icon}</span>
</div>
<div className="mt-1 text-sm text-primary">进入</div>
<div className="min-w-0 flex-1">
<div className="truncate font-medium text-zinc-900">{focusItem.title}</div>
<div className="mt-1 text-xs text-zinc-500">{focusItem.sourceLabel}</div>
</div>
<span className="material-symbols-outlined text-zinc-400">arrow_forward</span>
</div>
</Link>
) : (
<div className="mt-3 rounded-lg border border-dashed border-slate-300 px-3 py-4 text-sm text-zinc-500">
暂无必须立即处理的事项。
</div>
</Link>
)) : <EmptyStateCard title="暂无待办" description="没有需要立刻处理的事。" />}
</div>
{hiddenCount > 0 ? (
<div className="portal-panel-footer">
<span>另有 {hiddenCount} 项。</span>
<Link to="/todos" className="font-medium text-primary hover:underline">查看全部待办</Link>
)}
</div>
) : null}
<div className="rounded-lg border border-slate-200 bg-white/90 px-3.5 py-3">
<div className="text-sm font-semibold text-zinc-900">下一步</div>
{primaryAction ? (
<Button asChild className="mt-3 w-full" type="button">
<Link to={primaryAction.to}>{primaryAction.label}</Link>
</Button>
) : (
<Button className="mt-3 w-full" disabled type="button">
暂无主动作
</Button>
)}
{secondaryLinks.length ? (
<div className="mt-3 grid gap-2 sm:grid-cols-2 xl:grid-cols-1">
{secondaryLinks.map((item) => (
<Button key={`${item.label}-${item.to}`} asChild variant="secondary" type="button">
<Link to={item.to}>{item.label}</Link>
</Button>
))}
</div>
) : null}
</div>
</div>
</SectionPanel>
);
}
@@ -1112,6 +1148,33 @@ export function PortalHomePage() {
});
}
const approvalAction = quickActions.find((item) => item.key === "approval");
const expenseAction = quickActions.find((item) => item.key === "expense");
const fallbackAction = quickActions.find((item): item is QuickActionConfig & { to: string } => Boolean(item.to));
const primaryAction: PortalFocusAction | null =
payload.pendingTasks.count > 0 && approvalAction?.to
? { label: "处理待办", to: approvalAction.to }
: expenseAction?.to
? { label: "新建报销", to: expenseAction.to }
: fallbackAction
? { label: fallbackAction.label, to: fallbackAction.to }
: null;
const secondaryLinkCandidates: Array<PortalFocusLink | null> = [
hasAnyPermission(permissions, EXPENSE_PORTAL_PERMISSIONS)
? { label: "报销进度", to: "/reimbursements" }
: null,
payload.pendingTasks.count > 0 && approvalAction?.to
? { label: "审批中心", to: approvalAction.to }
: null,
todoAggregateItems.length > 1
? { label: "全部待办", to: "/todos" }
: null,
];
const secondaryLinks = secondaryLinkCandidates
.filter((item): item is PortalFocusLink => item !== null)
.filter((item) => item.to !== primaryAction?.to)
.slice(0, 2);
return (
<PageScaffold>
<ErrorBoundary fallback={null}>
@@ -1123,12 +1186,12 @@ export function PortalHomePage() {
title={`你好,${payload.greeting.name}`}
className="workbench-hero--compact"
subtitle={roleCapabilities.canUseFinance
? "付款、风险、审批。"
? "先处理今天的付款、风险和审批。"
: roleCapabilities.canManageSystem
? "先看待处理,再进入配置、审计和通知。"
? "先处理待办,再进入管理入口。"
: isManagerView
? "审批待办、团队状态和周报缺口集中查看。"
: "先看待办,再提交申请、补材料或查看状态。"}
? "先处理待办,再看团队状态。"
: "先处理待办,再发起申请。"}
actions={
!payload.workspace.persisted && canSetupWorkspace ? (
<Button asChild variant="secondary" type="button">
@@ -1138,25 +1201,12 @@ export function PortalHomePage() {
}
/>
<div className="portal-metric-strip ops-status-strip">
{primaryCards.map((card) => (
<MetricCard
key={card.key}
ariaLabel={`portal-card-${card.key}`}
href={card.href}
note={card.key === "my_weekly_report"
? formatWeeklyStatus(payload.weeklyReport)
: card.key === "today_presence"
? payload.todayPresence?.note ?? "更新今日状态"
: card.key === "team_presence"
? payload.teamPresence ? `请假 ${payload.teamPresence.leave},未更新 ${payload.teamPresence.unspecified}` : "团队状态板待启用"
: undefined}
title={card.title}
tone={severityTone(card.severity)}
value={card.value}
/>
))}
</div>
<TodayTaskPanel
primaryAction={primaryAction}
secondaryLinks={secondaryLinks}
focusItem={todoAggregateItems[0] ?? null}
hiddenFocusCount={Math.max(todoAggregateItems.length - 1, 0)}
/>
{showGettingStarted ? (
<ErrorBoundary
@@ -1175,33 +1225,33 @@ export function PortalHomePage() {
</ErrorBoundary>
) : null}
<div className="portal-home-grid">
<TodoAggregatePanel items={todoAggregateItems} />
<QuickActionsPanel items={quickActions} layout="rail" />
</div>
<div className="portal-home-grid portal-home-grid--collaboration">
<ErrorBoundary
fallback={(
<PortalSectionFallback
title="团队全员状态"
description="今日团队状态。"
/>
)}
>
<TeamStatusBoardPanel items={teamStatusBoard} />
</ErrorBoundary>
<ErrorBoundary
fallback={(
<PortalSectionFallback
title="近期动态流"
description="最近业务动作。"
/>
)}
>
<ActivityAnnouncementsPanel activities={recentActivities} announcements={announcementItems} />
</ErrorBoundary>
</div>
<SecondaryWorkspacePanel
title="团队与通知"
description="团队状态、动态和公告"
>
<div className="portal-home-grid portal-home-grid--collaboration">
<ErrorBoundary
fallback={(
<PortalSectionFallback
title="团队全员状态"
description="今日团队状态。"
/>
)}
>
<TeamStatusBoardPanel items={teamStatusBoard} />
</ErrorBoundary>
<ErrorBoundary
fallback={(
<PortalSectionFallback
title="近期动态流"
description="最近业务动作。"
/>
)}
>
<ActivityAnnouncementsPanel activities={recentActivities} announcements={announcementItems} />
</ErrorBoundary>
</div>
</SecondaryWorkspacePanel>
{isManagerView ? (
<>
@@ -1210,6 +1260,10 @@ export function PortalHomePage() {
description="入口"
>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)] 2xl:grid-cols-[minmax(0,1fr),minmax(0,1fr),340px]">
{primaryCards.length ? (
<SecondaryCardsPanel title="状态摘要" cards={primaryCards} />
) : null}
<QuickActionsPanel items={quickActions} />
{roleCapabilities.canApproveTasks ? (
<SummaryPanel title="审批处理" items={approvalSummaryItems} />
) : null}
@@ -1248,6 +1302,7 @@ export function PortalHomePage() {
description="申请、资产、最近访问"
>
<div className="portal-secondary-compact-grid">
<CompactMetricLinks title="状态摘要" cards={primaryCards} />
<CompactSummaryLinks title="我的申请与资产" items={employeeSummaryItems} />
<CompactRecentVisits />
<CompactQuickLinks items={quickLinkItems} />