收敛 PC 工作台视觉 05-27-pc-frontend-visual-consistency-wave1
这个提交包含在:
@@ -384,23 +384,6 @@ function AnnouncementsList({ announcements }: { announcements: PortalHomePayload
|
||||
);
|
||||
}
|
||||
|
||||
function RecentVisitsPanel() {
|
||||
const recentVisits = readRecentVisits();
|
||||
|
||||
return (
|
||||
<SectionPanel title="继续工作">
|
||||
<div className="space-y-3">
|
||||
{recentVisits.length ? recentVisits.map((item) => (
|
||||
<Link key={item.path} to={item.path} className="portal-list-card">
|
||||
<div className="font-medium text-zinc-900">{item.title}</div>
|
||||
<div className="mt-1 text-xs text-zinc-500">{item.path}</div>
|
||||
</Link>
|
||||
)) : <EmptyStateCard title="暂无最近访问" description="最近页面会显示在这里。" />}
|
||||
</div>
|
||||
</SectionPanel>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickLinksPanel({ items }: { items: PortalHomePayload["quickLinks"] }) {
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
|
||||
@@ -431,7 +414,7 @@ function SecondaryCardsPanel({ title, description, cards }: { title: string; des
|
||||
title={card.title}
|
||||
value={card.value}
|
||||
href={card.href}
|
||||
note="查看"
|
||||
note="查看"
|
||||
tone={severityTone(card.severity)}
|
||||
/>
|
||||
))}
|
||||
@@ -440,6 +423,75 @@ function SecondaryCardsPanel({ title, description, cards }: { title: string; des
|
||||
);
|
||||
}
|
||||
|
||||
function CompactGroup({ title, children }: { title: string; children: ReactNode }) {
|
||||
return (
|
||||
<section className="portal-compact-group">
|
||||
<h3 className="portal-compact-group__title">{title}</h3>
|
||||
<div className="portal-compact-group__body">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactSummaryLinks({ title, items }: { title: string; items: SummaryItem[] }) {
|
||||
return (
|
||||
<CompactGroup title={title}>
|
||||
{items.length ? items.map((item) => (
|
||||
<Link key={item.key} to={item.href} className="portal-compact-row">
|
||||
<span className="min-w-0 truncate">{item.label}</span>
|
||||
<span className={item.tone === "warning" ? "shrink-0 font-semibold text-amber-700" : "shrink-0 font-semibold text-slate-900"}>
|
||||
{item.value}
|
||||
</span>
|
||||
</Link>
|
||||
)) : <div className="px-2 py-1.5 text-sm text-slate-500">暂无摘要。</div>}
|
||||
</CompactGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactRecentVisits() {
|
||||
const recentVisits = readRecentVisits();
|
||||
|
||||
return (
|
||||
<CompactGroup title="继续工作">
|
||||
{recentVisits.length ? recentVisits.map((item) => (
|
||||
<Link key={item.path} to={item.path} className="portal-compact-row">
|
||||
<span className="min-w-0 truncate">{item.title}</span>
|
||||
<span className="shrink-0 text-xs text-slate-400">打开</span>
|
||||
</Link>
|
||||
)) : <div className="px-2 py-1.5 text-sm text-slate-500">暂无最近访问。</div>}
|
||||
</CompactGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactQuickLinks({ items }: { items: PortalHomePayload["quickLinks"] }) {
|
||||
const safeItems = Array.isArray(items) ? items : [];
|
||||
|
||||
return (
|
||||
<CompactGroup title="常用入口">
|
||||
{safeItems.length ? safeItems.slice(0, 6).map((item) => (
|
||||
<Link key={item.key} to={item.href} className="portal-compact-row">
|
||||
<span className="min-w-0 truncate">{item.title}</span>
|
||||
<span className="shrink-0 text-xs text-slate-400">进入</span>
|
||||
</Link>
|
||||
)) : <div className="px-2 py-1.5 text-sm text-slate-500">暂无常用入口。</div>}
|
||||
</CompactGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function CompactMetricLinks({ title, cards }: { title: string; cards: PortalHomePayload["cards"] }) {
|
||||
return (
|
||||
<CompactGroup title={title}>
|
||||
{cards.length ? cards.slice(0, 6).map((card) => (
|
||||
<Link key={card.key} to={card.href} className="portal-compact-row">
|
||||
<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}
|
||||
</span>
|
||||
</Link>
|
||||
)) : <div className="px-2 py-1.5 text-sm text-slate-500">暂无统计项。</div>}
|
||||
</CompactGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function AdoptionSummaryPanel({ adoptionSummary }: { adoptionSummary: NonNullable<PortalHomePayload["adoptionSummary"]> | null }) {
|
||||
if (!adoptionSummary) {
|
||||
return null;
|
||||
@@ -1191,21 +1243,14 @@ export function PortalHomePage() {
|
||||
</>
|
||||
) : (
|
||||
<SecondaryWorkspacePanel
|
||||
title="我的申请与资产 / 继续工作"
|
||||
description="申请、资产、最近访问"
|
||||
title="更多入口"
|
||||
description="申请、资产、最近访问和项目指标"
|
||||
>
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)]">
|
||||
<SummaryPanel title="我的申请与资产" items={employeeSummaryItems} />
|
||||
<RecentVisitsPanel />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr),minmax(0,1fr)]">
|
||||
<QuickLinksPanel items={quickLinkItems} />
|
||||
<SecondaryCardsPanel
|
||||
title="项目与协同"
|
||||
description="项目、参考指标"
|
||||
cards={secondaryCards}
|
||||
/>
|
||||
<div className="portal-secondary-compact-grid">
|
||||
<CompactSummaryLinks title="我的申请与资产" items={employeeSummaryItems} />
|
||||
<CompactRecentVisits />
|
||||
<CompactQuickLinks items={quickLinkItems} />
|
||||
<CompactMetricLinks title="项目与协同" cards={secondaryCards} />
|
||||
</div>
|
||||
</SecondaryWorkspacePanel>
|
||||
)}
|
||||
|
||||
@@ -366,30 +366,6 @@ export function ReimbursementsPage() {
|
||||
subtitle="草稿、审批和付款状态。"
|
||||
actions={
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-std"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
{canViewAll ? (
|
||||
<Button asChild variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
{canCreateExpense ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -446,33 +422,6 @@ export function ReimbursementsPage() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-csv"
|
||||
onSelect={() => setExportConfirmFormat("csv")}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-xlsx"
|
||||
onSelect={() => setExportConfirmFormat("xlsx")}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -547,6 +496,24 @@ export function ReimbursementsPage() {
|
||||
</Tabs>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{showProxyOwnerSelector ? (
|
||||
<select
|
||||
className="select-sm h-10"
|
||||
data-testid="reimbursements-owner-select"
|
||||
onChange={(event) => setProxyOwnerEmployeeId(event.target.value)}
|
||||
value={proxyOwnerEmployeeId}
|
||||
>
|
||||
<option value="">自己</option>
|
||||
{(proxyDelegationsQuery.data?.received ?? [])
|
||||
.filter((item) => item.status === "active")
|
||||
.map((item) => (
|
||||
<option key={item.id} value={item.principalEmployeeId}>
|
||||
{item.principal?.name ?? item.principalEmployeeId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : null}
|
||||
|
||||
{canViewAll ? (
|
||||
<>
|
||||
<Button
|
||||
@@ -565,9 +532,39 @@ export function ReimbursementsPage() {
|
||||
>
|
||||
全范围
|
||||
</Button>
|
||||
<Button asChild variant="secondary" type="button">
|
||||
<Link to="/reimbursements/dashboard">统计分析</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid="reimbursements-export"
|
||||
variant="secondary"
|
||||
type="button"
|
||||
disabled={exportMutation.isPending}
|
||||
>
|
||||
{exportMutation.isPending ? "导出中..." : "导出"}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-csv"
|
||||
onSelect={() => setExportConfirmFormat("csv")}
|
||||
>
|
||||
导出 CSV
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
data-testid="reimbursements-export-xlsx"
|
||||
onSelect={() => setExportConfirmFormat("xlsx")}
|
||||
>
|
||||
导出 Excel
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{canShowBatchEntry ? (
|
||||
<Button
|
||||
variant={batchMode ? "default" : "secondary"}
|
||||
|
||||
在新工单中引用
屏蔽一个用户