重构 PC 首页与报销列表降噪(frontend-simplification-refactor)
这个提交包含在:
+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} />
|
||||
|
||||
在新工单中引用
屏蔽一个用户