DG9 统一全站 UI 布局样板迁移

关联 Issue: DG9-0..DG9-12
这个提交包含在:
2026-05-14 00:52:59 +08:00
父节点 3ac5c785b1
当前提交 4b14c925b6
共修改 30 个文件,包含 743 行新增和 437 行删除
+6 -6
查看文件
@@ -41,7 +41,7 @@ import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import {
Dialog,
@@ -605,12 +605,12 @@ export function ApOperationsPage() {
};
if (summaryQuery.isLoading && paymentBatchesQuery.isLoading && riskFlagsQuery.isLoading && financeLedgerQuery.isLoading) {
return <div className="card-std p-6 text-sm text-zinc-500">加载财务工作台...</div>;
return <div className="card-std p-4 text-sm text-zinc-500 md:p-5">加载财务工作台...</div>;
}
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="财务付款与对账"
subtitle="集中处理待付款批次、异常付款、风险标记和对账异常。"
actions={
@@ -671,7 +671,7 @@ export function ApOperationsPage() {
/>
</div>
<div className="card-std p-6">
<div className="card-std p-4 md:p-5">
<Tabs value={activeTab} onValueChange={(value) => setActiveTab(value as OperationsTab)}>
<TabsList className="flex w-full flex-wrap justify-start gap-1">
<TabsTrigger value="ledger">财务台账 {financeLedgerQuery.data?.total ?? 0}</TabsTrigger>
@@ -1630,6 +1630,6 @@ export function ApOperationsPage() {
await executeMutation.mutateAsync(retryTarget.batchId);
}}
/>
</div>
</ListWorkspace>
);
}
+8 -9
查看文件
@@ -10,7 +10,7 @@ import {
type ApprovalRoleCode,
type ApprovalRule,
} from "../api/approvalRules";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../components/ui/dialog";
import { Input } from "../components/ui/input";
@@ -144,8 +144,8 @@ export function ApprovalRulesPage() {
}, [editOpen, editRule]);
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="审批规则"
subtitle="维护金额段与审批层级(仅影响新提交的报销单,历史流程不回写)。"
actions={
@@ -161,7 +161,7 @@ export function ApprovalRulesPage() {
</p>
) : null}
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建审批规则</h3>
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
@@ -286,7 +286,7 @@ export function ApprovalRulesPage() {
</div>
</div>
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">规则列表</h3>
{listQuery.isLoading ? <TableSkeleton columns={6} rows={5} /> : null}
@@ -304,7 +304,7 @@ export function ApprovalRulesPage() {
) : null}
{!listQuery.isLoading && !listQuery.isError ? (
<div className="rounded-lg border border-zinc-200">
<PrimaryTable>
<Table data-testid="approval-rules-table">
<TableHeader>
<tr>
@@ -371,7 +371,7 @@ export function ApprovalRulesPage() {
)}
</TableBody>
</Table>
</div>
</PrimaryTable>
) : null}
</div>
@@ -516,7 +516,6 @@ export function ApprovalRulesPage() {
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</ListWorkspace>
);
}
+12 -12
查看文件
@@ -27,10 +27,10 @@ import { fetchCostCenters, fetchDocumentCategories, fetchLegalEntities } from ".
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission, hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
@@ -578,8 +578,8 @@ export function AssetRegistryPage() {
};
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="资产台账"
subtitle="维护资产分类与资产卡片,支持法体、成本中心和采购来源引用。"
actions={
@@ -597,7 +597,7 @@ export function AssetRegistryPage() {
/>
<div className="grid gap-6 xl:grid-cols-[380px_minmax(0,1fr)]">
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-zinc-900">资产分类</h3>
{canManage ? (
@@ -669,7 +669,7 @@ export function AssetRegistryPage() {
<div className="rounded-lg border border-zinc-200 p-3 text-sm text-zinc-500">暂无资产分类</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<PrimaryTable className="hidden md:block">
<Table>
<TableHeader>
<tr>
@@ -691,13 +691,13 @@ export function AssetRegistryPage() {
))}
</TableBody>
</Table>
</div>
</PrimaryTable>
</>
) : null}
</div>
<div className="space-y-6">
<div className="card-std space-y-4 p-6">
<FilterBar className="space-y-4">
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-6">
<select className="select-std" value={statusFilter} onChange={(event) => setStatusFilter(event.target.value as AssetStatus | "") }>
{assetStatusOptions().map((item) => (
@@ -796,7 +796,7 @@ export function AssetRegistryPage() {
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无资产卡片</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<PrimaryTable className="hidden md:block">
<Table>
<TableHeader>
<tr>
@@ -830,12 +830,12 @@ export function AssetRegistryPage() {
)}
</TableBody>
</Table>
</div>
</PrimaryTable>
</>
) : null}
</div>
</FilterBar>
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
{!selectedAssetId && !isCreatingAsset ? (
<div className="rounded-lg border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500">
请先从列表选择资产卡片,或点击“新建资产卡片”。
@@ -1128,6 +1128,6 @@ export function AssetRegistryPage() {
</div>
</div>
</div>
</div>
</ListWorkspace>
);
}
+4 -4
查看文件
@@ -28,7 +28,7 @@ import { hasPermission } from "../auth/permissions";
import { toast } from "sonner";
import { downloadBlob } from "../lib/download";
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
import { InvoiceUploadCard } from "./invoice-pool/InvoiceUploadCard";
@@ -446,8 +446,8 @@ export function InvoicePoolPage() {
// --- Render ---
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="票据中心"
subtitle="上传、识别、绑定。"
actions={
@@ -569,6 +569,6 @@ export function InvoicePoolPage() {
confirmAction={confirmAction}
setConfirmAction={setConfirmAction}
/>
</div>
</ListWorkspace>
);
}
+4 -5
查看文件
@@ -20,11 +20,10 @@ import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { CardSkeleton } from "../components/common/CardSkeleton";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { PageHeader } from "../components/common/PageHeader";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace";
import { ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
@@ -780,12 +779,12 @@ export function LeaveRequestPage() {
return (
<ListWorkspace>
<PageHeader
<WorkspaceHeader
title="请假申请"
subtitle="支持假种选择、半天粒度请假、额度冻结与撤回释放。"
/>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatStrip columns={4}>
<MetricCard title="可用额度" value={balanceSummary.available.toFixed(1)} note="按当前启用假种汇总" />
<MetricCard title="已冻结" value={balanceSummary.reserved.toFixed(1)} note="审批中申请会占用额度" tone={balanceSummary.reserved > 0 ? "warning" : "neutral"} />
<MetricCard title="已使用" value={balanceSummary.used.toFixed(1)} note="通过后计入已使用" />
@@ -795,7 +794,7 @@ export function LeaveRequestPage() {
note="创建草稿前可先预览天数"
tone={effectiveRequestedDaysPreview ? "warning" : "neutral"}
/>
</section>
</StatStrip>
<div className="page-shell-grid">
<SectionPanel title={formTitle} description={formDescription}>
+24 -20
查看文件
@@ -22,9 +22,9 @@ import { hasAnyPermission, hasPermission } from "../auth/permissions";
import { ConfirmDialog } from "../components/common/ConfirmDialog";
import { HolidayCalendarTab } from "../components/master-data/HolidayCalendarTab";
import { SupplierTab } from "../components/master-data/SupplierTab";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -292,16 +292,16 @@ export function MasterDataPage() {
if (!canView) {
return (
<div className="space-y-4">
<PageHeader title="主数据" subtitle="你当前没有主数据权限,请联系管理员开通。" />
<div className="card-std p-6 text-sm text-zinc-600">所需权限:masterdata.view 或 masterdata.manage</div>
</div>
<ListWorkspace>
<WorkspaceHeader title="主数据" subtitle="你当前没有主数据权限,请联系管理员开通。" />
<div className="card-std p-4 text-sm text-zinc-600 md:p-5">所需权限:masterdata.view 或 masterdata.manage</div>
</ListWorkspace>
);
}
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="主数据"
subtitle="集中维护法体、成本中心、单据分类、日历与供应商,为预算、采购、请假与资产扩展提供统一骨架。"
actions={
@@ -325,7 +325,7 @@ export function MasterDataPage() {
</TabsList>
<TabsContent value="legal-entities" className="space-y-6">
<div className="card-std p-6 space-y-4">
<FilterBar className="space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">法体筛选</h3>
<select
@@ -338,10 +338,10 @@ export function MasterDataPage() {
<option value="false">仅停用</option>
</select>
</div>
</div>
</FilterBar>
{canManage ? (
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建法体</h3>
<div className="grid gap-3 md:grid-cols-3">
<div className="space-y-1">
@@ -377,10 +377,11 @@ export function MasterDataPage() {
</div>
) : null}
<div className="card-std p-6 space-y-3">
<div className="card-std space-y-3 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">法体列表</h3>
{legalEntitiesQuery.isLoading ? <TableSkeleton columns={6} rows={4} /> : null}
{!legalEntitiesQuery.isLoading ? (
<PrimaryTable>
<Table>
<TableHeader>
<TableRow>
@@ -431,12 +432,13 @@ export function MasterDataPage() {
)}
</TableBody>
</Table>
</PrimaryTable>
) : null}
</div>
</TabsContent>
<TabsContent value="cost-centers" className="space-y-6">
<div className="card-std p-6 space-y-4">
<FilterBar className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<select
className="select-std"
@@ -462,10 +464,10 @@ export function MasterDataPage() {
))}
</select>
</div>
</div>
</FilterBar>
{canManage ? (
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建成本中心</h3>
<div className="grid gap-3 md:grid-cols-4">
<div className="space-y-1 md:col-span-2">
@@ -532,10 +534,11 @@ export function MasterDataPage() {
</div>
) : null}
<div className="card-std p-6 space-y-3">
<div className="card-std space-y-3 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">成本中心列表</h3>
{costCentersQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
{!costCentersQuery.isLoading ? (
<PrimaryTable>
<Table>
<TableHeader>
<TableRow>
@@ -588,12 +591,13 @@ export function MasterDataPage() {
)}
</TableBody>
</Table>
</PrimaryTable>
) : null}
</div>
</TabsContent>
<TabsContent value="document-categories" className="space-y-6">
<div className="card-std p-6 space-y-4">
<FilterBar className="space-y-4">
<div className="grid gap-3 md:grid-cols-3">
<select
className="select-std"
@@ -619,10 +623,10 @@ export function MasterDataPage() {
))}
</select>
</div>
</div>
</FilterBar>
{canManage ? (
<div className="card-std p-6 space-y-4">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建单据分类</h3>
<div className="grid gap-3 md:grid-cols-4">
<div className="space-y-1">
@@ -693,7 +697,7 @@ export function MasterDataPage() {
</div>
) : null}
<div className="card-std p-6 space-y-3">
<div className="card-std space-y-3 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">单据分类列表</h3>
{documentCategoriesQuery.isLoading ? <TableSkeleton columns={7} rows={4} /> : null}
{!documentCategoriesQuery.isLoading ? (
@@ -790,6 +794,6 @@ export function MasterDataPage() {
}
}}
/>
</div>
</ListWorkspace>
);
}
+70 -48
查看文件
@@ -999,11 +999,11 @@ export function MobileApprovalsPage() {
) : null}
{!isPushDetail && !canAccessRemoteApprovals ? (
<section className="mobile-card space-y-3" data-testid="mobile-approvals-progress">
<div className="flex items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">我发起的进度</h3>
<p className="mt-1 text-sm text-zinc-500">报销、请假、采购进度</p>
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-progress">
<div className="mobile-section-header">
<div className="min-w-0">
<h3 className="mobile-section-title">我发起的进度</h3>
<p className="mobile-list-card__subtitle mt-1">报销、请假、采购进度</p>
</div>
<span className="badge-neutral">{employeeProgressCards.length} 条</span>
</div>
@@ -1028,7 +1028,7 @@ export function MobileApprovalsPage() {
<div className="mobile-empty-state">暂无申请进度</div>
) : null}
<div className="space-y-2">
<div className="mobile-page-stack">
{employeeProgressCards.map((item) => (
<Link
className="mobile-list-card block"
@@ -1037,12 +1037,14 @@ export function MobileApprovalsPage() {
to={item.to}
state={{ from: currentMobileSource }}
>
<div className="flex items-start justify-between gap-3">
<div className="mobile-list-card__header">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{item.title}</div>
{item.subtitle ? <div className="mt-1 truncate text-xs text-zinc-500">{item.subtitle}</div> : null}
<div className="mobile-list-card__title">{item.title}</div>
{item.subtitle ? <div className="mobile-list-card__subtitle mt-1">{item.subtitle}</div> : null}
</div>
<StatusBadge status={item.status} />
<span className="mobile-status-slot">
<StatusBadge status={item.status} />
</span>
</div>
</Link>
))}
@@ -1051,13 +1053,13 @@ export function MobileApprovalsPage() {
) : null}
{!isPushDetail && canAccessRemoteApprovals ? (
<section className="mobile-card space-y-4" data-testid="mobile-approvals-list">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h3 className="text-base font-semibold text-zinc-900">{listTitle}</h3>
<p className="mt-1 text-sm text-zinc-500">{filteredTaskCards.length} 条</p>
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-list">
<div className="mobile-section-header">
<div className="min-w-0">
<h3 className="mobile-section-title">{listTitle}</h3>
<p className="mobile-list-card__subtitle mt-1">{filteredTaskCards.length} 条</p>
</div>
<span className={`text-sm ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
<span className={`mobile-section-meta ${isOnline ? "text-emerald-600" : "text-orange-600"}`}>{isOnline ? "在线" : "离线"}</span>
</div>
<Tabs value={taskTab} onValueChange={handleTaskTabChange}>
@@ -1195,7 +1197,7 @@ export function MobileApprovalsPage() {
</p>
) : null}
<div className="space-y-2">
<div className="mobile-page-stack">
{filteredTaskCards.map((task) => {
const isSelected = selectedTaskId === task.id;
const isBatchSelected = selectedBatchTaskIds.includes(task.id);
@@ -1204,7 +1206,7 @@ export function MobileApprovalsPage() {
return (
<div
key={task.id}
className={`mobile-list-card flex gap-3 ${
className={`mobile-list-card grid grid-cols-[auto_minmax(0,1fr)] gap-3 ${
isBatchSelected ? "border-primary bg-primary-50" : "border-zinc-200 bg-white"
}`}
>
@@ -1221,12 +1223,14 @@ export function MobileApprovalsPage() {
data-testid={`mobile-approvals-task-${task.id}`}
onClick={() => toggleBatchTask(task.id)}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{task.title}</div>
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
<div className="mobile-list-card__header">
<div className="mobile-list-card__title">{task.title}</div>
<span className="mobile-status-slot">
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
</span>
</div>
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
<div className="mt-1 text-xs text-zinc-500">
<div className="mobile-list-card__subtitle mt-1">{task.subtitle}</div>
<div className="mt-1 min-w-0 truncate text-xs text-zinc-500">
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
</div>
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
@@ -1245,12 +1249,14 @@ export function MobileApprovalsPage() {
onClick={() => handleTaskSelection(task.id)}
type="button"
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{task.title}</div>
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
<div className="mobile-list-card__header">
<div className="mobile-list-card__title">{task.title}</div>
<span className="mobile-status-slot">
<StatusBadge status={task.status} label={workflowStatusLabel(task.status)} />
</span>
</div>
<div className="mt-1 text-xs text-zinc-500">{task.subtitle}</div>
<div className="mt-1 text-xs text-zinc-500">
<div className="mobile-list-card__subtitle mt-1">{task.subtitle}</div>
<div className="mt-1 min-w-0 truncate text-xs text-zinc-500">
{joinBusinessMeta([task.applicantName ? `申请人:${task.applicantName}` : null, task.metric ? `金额/天数:${task.metric}` : null]) || "暂无摘要"}
</div>
{task.isCached ? <div className="mt-1 text-xs text-zinc-500">来源:离线缓存</div> : null}
@@ -1262,8 +1268,8 @@ export function MobileApprovalsPage() {
) : null}
{isPushDetail ? (
<section className="mobile-card scroll-mt-4 space-y-3" data-testid="mobile-approvals-detail">
<h3 className="text-base font-semibold text-zinc-900">处理</h3>
<section className="mobile-card mobile-page-stack scroll-mt-4" data-testid="mobile-approvals-detail">
<h3 className="mobile-section-title">处理</h3>
{!selectedTaskId ? <p className="text-sm text-zinc-500">请先选择任务。</p> : null}
@@ -1286,12 +1292,14 @@ export function MobileApprovalsPage() {
{selectedTask ? (
<>
<div className="mobile-list-card text-sm">
<div className="flex items-start justify-between gap-3">
<div className="mobile-list-card__header">
<div className="min-w-0">
<div className="truncate font-semibold text-zinc-900">{selectedDetailTitle}</div>
<div className="mt-1 text-xs text-zinc-500">{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}</div>
<div className="mobile-list-card__title">{selectedDetailTitle}</div>
<div className="mobile-list-card__subtitle mt-1">{joinBusinessMeta([`节点:${selectedTask.task.stepName}`, `创建:${formatDateTime(selectedTask.task.createdAt)}`])}</div>
</div>
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
<span className="mobile-status-slot">
<StatusBadge status={selectedTask.task.status} label={workflowStatusLabel(selectedTask.task.status)} />
</span>
</div>
<div className="mobile-field-list mt-3">
<div className="mobile-field-row">
@@ -1343,10 +1351,10 @@ export function MobileApprovalsPage() {
) : null}
</div>
<div className="space-y-2" data-testid="mobile-approvals-history">
<div className="flex items-center justify-between gap-3">
<h4 className="text-sm font-semibold text-zinc-900">处理历史</h4>
<span className="text-xs text-zinc-500">{selectedTask.history.length} 条</span>
<div className="mobile-page-stack" data-testid="mobile-approvals-history">
<div className="mobile-section-header">
<h4 className="mobile-section-title">处理历史</h4>
<span className="mobile-section-meta">{selectedTask.history.length} 条</span>
</div>
{selectedTask.history.length ? (
selectedTask.history.map((item) => (
@@ -1381,7 +1389,7 @@ export function MobileApprovalsPage() {
<p className="text-xs text-zinc-500">离线仅保存通过 / 驳回草稿,联网同步成功后才会正式提交。</p>
) : null}
<div className="grid gap-2 min-[390px]:grid-cols-3">
<div className="mobile-sticky-action-grid">
<Button
className="min-h-12 w-full"
data-testid="mobile-approvals-approve"
@@ -1470,10 +1478,10 @@ export function MobileApprovalsPage() {
) : null}
{!isPushDetail && shouldShowDraftQueue ? (
<section className="mobile-card space-y-3" data-testid="mobile-approvals-queue">
<div className="flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">离线草稿</h3>
<span className="text-sm text-zinc-500">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
<section className="mobile-card mobile-page-stack" data-testid="mobile-approvals-queue">
<div className="mobile-section-header">
<h3 className="mobile-section-title">离线草稿</h3>
<span className="mobile-section-meta">{currentQueueItems.length ? `待同步 ${currentQueueItems.length}` : "无草稿"}</span>
</div>
{currentQueueItems.length ? (
@@ -1495,13 +1503,27 @@ export function MobileApprovalsPage() {
{currentQueueItems.map((item) => (
<div key={item.id} className="mobile-list-card text-sm">
<div className="font-medium text-zinc-900">
<div className="mobile-list-card__title">
{publicReference(item.taskSnapshot.reference) ?? entityLabel(item.taskSnapshot.entityType)} · {item.action === "approve" ? "通过" : "驳回"}
</div>
<div className="text-zinc-500">业务:{entityLabel(item.taskSnapshot.entityType)} · 步骤:{item.taskSnapshot.stepName ?? "-"}</div>
<div className="text-zinc-500">状态:{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
<div className="text-zinc-500">意见:{item.comment ?? "-"}</div>
<div className="text-zinc-500">最近错误:{item.lastError ?? "-"}</div>
<div className="mobile-field-list mt-2">
<div className="mobile-field-row">
<div className="mobile-field-label">业务</div>
<div className="mobile-field-value">{entityLabel(item.taskSnapshot.entityType)} · 步骤:{item.taskSnapshot.stepName ?? "-"}</div>
</div>
<div className="mobile-field-row">
<div className="mobile-field-label">状态</div>
<div className="mobile-field-value">{queueStatusLabel(item.status)} · 更新时间:{formatDateTime(item.updatedAt)}</div>
</div>
<div className="mobile-field-row">
<div className="mobile-field-label">意见</div>
<div className="mobile-field-value">{item.comment ?? "-"}</div>
</div>
<div className="mobile-field-row">
<div className="mobile-field-label">最近错误</div>
<div className="mobile-field-value">{item.lastError ?? "-"}</div>
</div>
</div>
<div className="mt-2 grid gap-2 sm:flex sm:flex-wrap">
<Button
type="button"
+53 -43
查看文件
@@ -119,25 +119,25 @@ export function MobileNotificationsPage() {
return (
<PageScaffold className="pb-4">
<div className="space-y-3" data-testid="mobile-notifications-page">
<PageHeader
title="通知"
meta={<span className="badge-neutral">未读 {listQuery.data?.unreadCount ?? 0}</span>}
actions={
<Button
type="button"
variant="secondary"
data-testid="mobile-notifications-mark-all-read"
disabled={(listQuery.data?.unreadCount ?? 0) === 0 || markAllReadMutation.isPending}
onClick={() => markAllReadMutation.mutate()}
>
全部已读
</Button>
}
/>
<div className="mobile-page-stack" data-testid="mobile-notifications-page">
<PageHeader
title="通知"
meta={<span className="badge-neutral">未读 {listQuery.data?.unreadCount ?? 0}</span>}
actions={
<Button
type="button"
variant="secondary"
data-testid="mobile-notifications-mark-all-read"
disabled={(listQuery.data?.unreadCount ?? 0) === 0 || markAllReadMutation.isPending}
onClick={() => markAllReadMutation.mutate()}
>
全部已读
</Button>
}
/>
<section className="mobile-card space-y-4" data-testid="mobile-notifications-list">
<div className="space-y-3">
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications-list">
<div className="mobile-page-stack">
<Tabs
data-testid="mobile-notifications-filter-tabs"
value={unreadOnly ? "unread" : "all"}
@@ -158,7 +158,7 @@ export function MobileNotificationsPage() {
</TabsList>
</Tabs>
<label className="flex items-center gap-2 text-sm text-zinc-600">
<label className="mobile-field-row text-sm text-zinc-600">
<span className="shrink-0">类型</span>
<select
data-testid="mobile-notifications-type-filter"
@@ -194,32 +194,42 @@ export function MobileNotificationsPage() {
<div className="mobile-empty-state">暂无通知</div>
) : null}
{notificationItems.map((item) => {
const targetLink = resolveMobileNotificationTargetLink(item);
return (
<button
data-testid="mobile-notification-card"
key={item.id}
className="mobile-list-card w-full text-left"
type="button"
onClick={() => void openNotificationTarget(item, targetLink)}
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
<div className="text-sm font-semibold text-zinc-900">{item.title}</div>
{item.body ? <div className="mt-1 text-xs leading-5 text-zinc-500">{item.body}</div> : null}
<div className="mobile-page-stack">
{notificationItems.map((item) => {
const targetLink = resolveMobileNotificationTargetLink(item);
return (
<button
data-testid="mobile-notification-card"
key={item.id}
className="mobile-list-card w-full text-left"
type="button"
onClick={() => void openNotificationTarget(item, targetLink)}
>
<div className="mobile-list-card__header">
<div className="min-w-0">
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
<div className="mobile-list-card__title mt-0.5">{item.title}</div>
{item.body ? <div className="mt-1 line-clamp-2 text-xs leading-5 text-zinc-500">{item.body}</div> : null}
</div>
<span className={item.readAt ? "badge-neutral mobile-status-slot" : "badge-warning mobile-status-slot"}>
{item.readAt ? "已读" : "未读"}
</span>
</div>
<span className={item.readAt ? "badge-neutral shrink-0" : "badge-warning shrink-0"}>
{item.readAt ? "已读" : "未读"}
</span>
</div>
<div className="mt-3 text-xs text-zinc-500">{formatTime(item.createdAt)}</div>
<div className="mt-3 text-xs text-primary">{targetLink ? "直达详情" : "暂无直达详情"}</div>
</button>
);
})}
<div className="mobile-field-list mt-3">
<div className="mobile-field-row">
<div className="mobile-field-label">时间</div>
<div className="mobile-field-value">{formatTime(item.createdAt)}</div>
</div>
<div className="mobile-field-row">
<div className="mobile-field-label">去向</div>
<div className="mobile-field-value text-primary">{targetLink ? "直达详情" : "暂无直达详情"}</div>
</div>
</div>
</button>
);
})}
</div>
<Pagination
page={page}
+33 -33
查看文件
@@ -325,15 +325,15 @@ export function MobilePortalPage() {
].filter((item): item is MobileAction => Boolean(item)).slice(0, 4);
return (
<PageScaffold className="space-y-4">
<PageScaffold className="mobile-page-stack">
<PageHeader title="今天" />
{canAccessApprovals && pendingApprovalCount > 0 ? (
<section className="mobile-card" data-testid="mobile-pending-summary">
<div className="mb-3 flex items-start justify-between gap-3">
<div>
<div className="text-base font-semibold text-zinc-900">待处理</div>
<div className="mt-1 text-xs text-zinc-500">待我审批</div>
<section className="mobile-card mobile-page-stack" data-testid="mobile-pending-summary">
<div className="mobile-section-header">
<div className="min-w-0">
<div className="mobile-section-title">待处理</div>
<div className="mobile-list-card__subtitle mt-1">待我审批</div>
</div>
<div className="text-right">
<div className="text-3xl font-semibold leading-none text-zinc-900" data-testid="mobile-pending-summary-count">
@@ -345,7 +345,7 @@ export function MobilePortalPage() {
</div>
</div>
{urgentPendingTask ? (
<div className="space-y-2">
<div className="mobile-page-stack">
{(() => {
const task = urgentPendingTask;
const entityType = task.workflowInstance?.entityType;
@@ -355,16 +355,16 @@ export function MobilePortalPage() {
key={task.id}
to={`/mobile/approvals/${encodeURIComponent(task.id)}`}
state={{ from: currentMobileSource }}
className="block rounded-lg border border-zinc-200 px-3 py-2 text-sm"
className="mobile-list-card block text-sm"
data-testid="mobile-pending-summary-primary"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 truncate font-medium text-zinc-900">
{reference ? `${businessEntityLabel(entityType, { noun: true })} ${reference}` : businessEntityLabel(entityType, { noun: true })}
<div className="mobile-list-card__header">
<div className="mobile-list-card__title">
{reference ? `${businessEntityLabel(entityType, { noun: true })} ${reference}` : businessEntityLabel(entityType, { noun: true })}
</div>
</div>
</div>
<div className="mt-1 truncate text-xs text-zinc-500">
{joinBusinessMeta([task.businessSummary?.applicantName ? `申请人:${task.businessSummary.applicantName}` : null, task.businessSummary?.amount ?? task.businessSummary?.duration, `节点:${task.stepName}`])}
<div className="mobile-list-card__subtitle mt-1">
{joinBusinessMeta([task.businessSummary?.applicantName ? `申请人:${task.businessSummary.applicantName}` : null, task.businessSummary?.amount ?? task.businessSummary?.duration, `节点:${task.stepName}`])}
</div>
</Link>
);
@@ -378,9 +378,9 @@ export function MobilePortalPage() {
) : null}
{canUseFinance ? (
<section className="mobile-card" data-testid="mobile-finance-summary">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-base font-semibold text-zinc-900">财务</div>
<section className="mobile-card mobile-page-stack" data-testid="mobile-finance-summary">
<div className="mobile-section-header">
<div className="mobile-section-title">财务</div>
<span className="badge-neutral">复核与付款</span>
</div>
{portalHomeQuery.isLoading ? <p className="text-sm text-zinc-500">读取中...</p> : null}
@@ -408,9 +408,9 @@ export function MobilePortalPage() {
</section>
) : null}
<section className="mobile-card" data-testid="mobile-today-focus">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-base font-semibold text-zinc-900">常用</div>
<section className="mobile-card mobile-page-stack" data-testid="mobile-today-focus">
<div className="mobile-section-header">
<div className="mobile-section-title">常用</div>
</div>
<div className="mobile-icon-grid" data-testid="mobile-action-grid">
{actions.map((action) => (
@@ -420,9 +420,9 @@ export function MobilePortalPage() {
</section>
{!attendanceFeatureDisabled ? (
<section className="mobile-card" data-testid="mobile-attendance-card">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-base font-semibold text-zinc-900">打卡</div>
<section className="mobile-card mobile-page-stack" data-testid="mobile-attendance-card">
<div className="mobile-section-header">
<div className="mobile-section-title">打卡</div>
<span className="badge-neutral">{attendanceToday?.workDate ?? "今日"}</span>
</div>
@@ -464,9 +464,9 @@ export function MobilePortalPage() {
</section>
) : null}
<section className="mobile-card" data-testid="mobile-notifications">
<div className="mb-3 flex items-center justify-between gap-3">
<div className="text-base font-semibold text-zinc-900">通知</div>
<section className="mobile-card mobile-page-stack" data-testid="mobile-notifications">
<div className="mobile-section-header">
<div className="mobile-section-title">通知</div>
<Button asChild size="sm" type="button" variant="secondary">
<Link to="/mobile/notifications">全部</Link>
</Button>
@@ -482,23 +482,23 @@ export function MobilePortalPage() {
<p className="text-sm text-zinc-500">暂无通知。</p>
) : null}
<div className="space-y-2">
<div className="mobile-page-stack">
{homeNotifications.map((item) => (
<button
key={item.id}
type="button"
className="block w-full rounded-lg border border-zinc-200 px-3 py-2 text-left text-sm"
className="mobile-list-card w-full text-left text-sm"
onClick={() => void openHomeNotification(item)}
data-testid="mobile-home-notification-card"
>
<div className="flex items-center justify-between gap-3">
<div className="mobile-list-card__header">
<div className="min-w-0">
<div className="text-xs text-zinc-500">{notificationTypeLabel(item.type)}</div>
<div className="mt-0.5 truncate font-medium text-zinc-900">{item.title}</div>
<div className="text-xs font-medium text-primary">{notificationTypeLabel(item.type)}</div>
<div className="mobile-list-card__title mt-0.5">{item.title}</div>
</div>
{!item.readAt ? <span className="badge-warning shrink-0">新</span> : null}
{!item.readAt ? <span className="badge-warning mobile-status-slot">新</span> : null}
</div>
<div className="mt-1 text-xs text-zinc-500">{formatDateTime(item.createdAt)}</div>
<div className="mobile-list-card__subtitle mt-1">{formatDateTime(item.createdAt)}</div>
</button>
))}
</div>
@@ -1173,7 +1173,7 @@ export function MobileReimbursementDetailPage() {
className="mobile-sticky-action-surface fixed inset-x-0 bottom-0 z-50 border-t border-zinc-200 bg-white/95 shadow-[0_-10px_24px_rgba(15,23,42,0.10)] backdrop-blur"
data-testid="mobile-reimbursement-sticky-actions"
>
<div className={cn(canOpenInvoiceDialog && showSubmitAction ? "mobile-two-action-grid" : "grid grid-cols-1 gap-2")}>
<div className="mobile-sticky-action-grid">
{canOpenInvoiceDialog ? (
<Button
className="min-h-11 min-w-0 px-2 text-sm whitespace-nowrap"
+8 -8
查看文件
@@ -16,7 +16,7 @@ import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -199,13 +199,13 @@ export function PaymentBatchesPage() {
});
return (
<div className="space-y-6">
<PageHeader title="支付批次" subtitle="财务批量付款、复核与执行;高风险批次达到 5 万元后需完成二次复核。" />
<ListWorkspace>
<WorkspaceHeader title="支付批次" subtitle="财务批量付款、复核与执行;高风险批次达到 5 万元后需完成二次复核。" />
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
{!canManageBatch ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
@@ -281,7 +281,7 @@ export function PaymentBatchesPage() {
</div>
<div className="space-y-6">
<div className="card-std p-6">
<div className="card-std p-4 md:p-5">
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
<div className="space-y-2">
{(listQuery.data?.items ?? []).map((item) => (
@@ -313,7 +313,7 @@ export function PaymentBatchesPage() {
{selectedBatch ? (
<div className="space-y-6">
<div className="card-std space-y-3 p-6">
<div className="card-std space-y-3 p-4 md:p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<div className="text-xs text-zinc-500">{selectedBatch.batchNo}</div>
@@ -372,7 +372,7 @@ export function PaymentBatchesPage() {
</div>
</div>
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">批次项</h3>
<div className="space-y-4">
{selectedBatch.items.map((item) => (
@@ -417,6 +417,6 @@ export function PaymentBatchesPage() {
) : null}
</div>
</div>
</div>
</ListWorkspace>
);
}
+4 -5
查看文件
@@ -12,9 +12,8 @@ import {
import { isApiError } from "../api/types";
import { useAuth } from "../auth/AuthContext";
import { hasAnyPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { StatusBadge } from "../components/common/StatusBadge";
import { FilterBar, ListWorkspace } from "../components/patterns/ListWorkspace";
import { FilterBar, ListWorkspace, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
@@ -161,7 +160,7 @@ export function ProjectsPage() {
return (
<ListWorkspace>
<PageHeader
<WorkspaceHeader
title="项目"
subtitle="轻项目主档、成员、里程碑、任务与风险统一在 OA 内跟进。"
actions={
@@ -173,12 +172,12 @@ export function ProjectsPage() {
}
/>
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatStrip columns={4}>
<MetricCard title="当前项目" value={projectStats.total} note={mine ? "已切换到我的项目" : "当前筛选结果"} />
<MetricCard title="进行中" value={projectStats.active} note="优先跟进活跃项目" />
<MetricCard title="逾期里程碑" value={projectStats.overdueMilestones} note="延期事项待处理" tone={projectStats.overdueMilestones > 0 ? "warning" : "neutral"} />
<MetricCard title="未关闭风险" value={projectStats.openRisks} note="问题留痕建议绑定责任人" tone={projectStats.openRisks > 0 ? "danger" : "neutral"} />
</section>
</StatStrip>
<div className="page-shell-grid">
<div className="space-y-6">
+21 -18
查看文件
@@ -36,7 +36,6 @@ import { fetchProjects } from "../api/projects";
import { remindWorkflowTask } from "../api/workflow";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { PageHeader } from "../components/common/PageHeader";
import { RiskFlagsCard } from "../components/common/RiskFlagsCard";
import { AiSuggestionCard } from "../components/ai/AiSuggestionCard";
import { Pagination } from "../components/common/Pagination";
@@ -46,6 +45,7 @@ import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Textarea } from "../components/ui/textarea";
import { FilterBar, ListWorkspace, PrimaryTable, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { buildPrecheckDialogMessage } from "../lib/precheck";
import {
@@ -720,6 +720,9 @@ export function PurchaseRequestsPage() {
const detail = detailQuery.data;
const archiveOrder = archiveOrderQuery.data?.archiveOrder ?? detail?.archiveOrder ?? null;
const detailAttachments = detail?.attachments ?? [];
const detailApprovalHistory = detail?.approvalHistory ?? [];
const detailRiskFlags = detail?.riskFlags ?? [];
const handleSubmitAction = async () => {
if (!selectedId) {
@@ -769,8 +772,8 @@ export function PurchaseRequestsPage() {
);
return (
<div className="space-y-6">
<PageHeader
<ListWorkspace>
<WorkspaceHeader
title="采购申请"
subtitle="草稿、附件、审批。"
actions={
@@ -793,7 +796,7 @@ export function PurchaseRequestsPage() {
</p>
) : null}
<div className="card-std space-y-4 p-6">
<FilterBar className="space-y-4">
<div className="grid gap-3 md:grid-cols-5">
<select
className="select-std"
@@ -887,10 +890,10 @@ export function PurchaseRequestsPage() {
</div>
) : null}
</div>
</div>
</FilterBar>
<div className="grid gap-6 xl:grid-cols-[420px_minmax(0,1fr)]">
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
<div className="flex items-center justify-between">
<h3 className="text-base font-semibold text-zinc-900">采购单列表</h3>
<span className="text-sm text-zinc-500">共 {listQuery.data?.total ?? 0} 条</span>
@@ -952,7 +955,7 @@ export function PurchaseRequestsPage() {
<div className="rounded-lg border border-zinc-200 p-4 text-sm text-zinc-500">暂无采购申请</div>
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<PrimaryTable className="hidden md:block">
<Table data-testid="purchase-requests-table">
<TableHeader>
<tr>
@@ -1002,7 +1005,7 @@ export function PurchaseRequestsPage() {
)}
</TableBody>
</Table>
</div>
</PrimaryTable>
</>
) : null}
@@ -1014,7 +1017,7 @@ export function PurchaseRequestsPage() {
/>
</div>
<div className="card-std space-y-5 p-6">
<div className="card-std space-y-5 p-4 md:p-5">
{!selectedId ? (
<div className="rounded-lg border border-dashed border-zinc-300 p-8 text-center text-sm text-zinc-500">
请先在左侧选择采购单,或新建一张采购草稿。
@@ -1096,7 +1099,7 @@ export function PurchaseRequestsPage() {
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<div className="text-zinc-500">附件</div>
<div className="mt-2 font-medium text-zinc-900">{detail.attachments.length} 个</div>
<div className="mt-2 font-medium text-zinc-900">{detailAttachments.length} 个</div>
</div>
</div>
@@ -1116,12 +1119,12 @@ export function PurchaseRequestsPage() {
</div>
</div>
<RiskFlagsCard items={detail.riskFlags} title="待处理风险项" />
<RiskFlagsCard items={detailRiskFlags} title="待处理风险项" />
<SectionPanel title="审批记录" description="展示最近审批动作、退回或驳回说明,便于申请人按意见修改。">
{detail.approvalHistory.length ? (
{detailApprovalHistory.length ? (
<div className="space-y-2" data-testid="purchase-approval-history">
{detail.approvalHistory.map((item) => (
{detailApprovalHistory.map((item) => (
<div key={item.id} className="rounded-lg border border-zinc-200 px-3 py-2 text-sm">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-medium text-zinc-900">
@@ -1515,8 +1518,8 @@ export function PurchaseRequestsPage() {
</div>
</div>
<div className="space-y-3 md:hidden">
{detail.attachments.length ? (
detail.attachments.map((attachment) => (
{detailAttachments.length ? (
detailAttachments.map((attachment) => (
<div key={attachment.id} className="rounded-lg border border-zinc-200 bg-white p-4">
<div className="font-medium text-zinc-900">{attachment.originalName}</div>
<div className="mt-1 text-xs text-zinc-500">{attachment.mimeType} / {attachment.fileSize} bytes</div>
@@ -1563,8 +1566,8 @@ export function PurchaseRequestsPage() {
</tr>
</TableHeader>
<TableBody>
{detail.attachments.length ? (
detail.attachments.map((attachment) => (
{detailAttachments.length ? (
detailAttachments.map((attachment) => (
<TableRow key={attachment.id}>
<TableCell>
<div className="font-medium text-zinc-900">{attachment.originalName}</div>
@@ -1754,6 +1757,6 @@ export function PurchaseRequestsPage() {
) : null}
</div>
</div>
</div>
</ListWorkspace>
);
}
+18 -33
查看文件
@@ -22,7 +22,6 @@ import {
withdrawReimbursement,
linkReimbursementProject,
type ReimbursementItem,
type ReimbursementStatus,
} from "../api/reimbursements";
import { remindWorkflowTask } from "../api/workflow";
import {
@@ -51,6 +50,8 @@ import { Input } from "../components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
import { StatStrip } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import {
@@ -73,15 +74,6 @@ import { buildPrecheckDialogMessage } from "../lib/precheck";
type ItemEditorState = { mode: "create" } | { mode: "edit"; itemId: string } | null;
type SelectedInvoiceMap = Record<string, string>;
const REIMBURSEMENT_STATUS_LABEL: Record<ReimbursementStatus, string> = {
draft: "草稿",
reviewing: "审批中",
approved: "已通过",
paid: "已支付",
rejected: "已驳回",
cancelled: "已取消",
};
function formatDateTime(value: string | null) {
if (!value) return "-";
return value.slice(0, 19).replace("T", " ");
@@ -99,10 +91,6 @@ function buildActionErrorMessage(error: unknown, fallbackMessage: string) {
return fallbackMessage;
}
function reimbursementStatusLabel(status: ReimbursementStatus) {
return REIMBURSEMENT_STATUS_LABEL[status] ?? "未知状态";
}
function workflowActionLabel(action: string) {
const labels: Record<string, string> = {
start: "提交",
@@ -1009,27 +997,24 @@ export function ReimbursementDetailPage() {
<div className="space-y-6">
<SectionPanel title="概览与关联">
<div className="space-y-6" data-testid="reimbursement-overview">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">当前状态</div>
<div className="mt-2"><StatusBadge status={detail.status} /></div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">关联项目</div>
<div className="mt-2 font-medium text-zinc-900">{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}</div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">明细数</div>
<div className="mt-2 font-medium text-zinc-900">{detail.items.length}</div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">发票</div>
<div className="mt-2 font-medium text-zinc-900">{detail.invoices.length}</div>
</div>
</div>
<StatStrip columns={4}>
<MetricCard
title="关联项目"
value={
<span className="text-base">
{detail.project ? `${detail.project.projectNo} · ${detail.project.name}` : "未关联"}
</span>
}
/>
<MetricCard title="明细数" value={detail.items.length} />
<MetricCard title="发票" value={detail.invoices.length} />
<MetricCard
title="付款状态"
value={<span className="text-base">{isPaidReimbursement ? "已付款" : "未付款"}</span>}
/>
</StatStrip>
<div className="rounded-lg border border-zinc-200 p-4 text-sm" data-testid="reimbursement-summary">
<div>状态:{reimbursementStatusLabel(detail.status)}</div>
<div>报销编号:{displayCode}</div>
<div>报销日期:{detail.reimbursementDate ?? "-"}</div>
<div>金额:{detail.totalAmount ?? "-"}</div>
+4 -5
查看文件
@@ -25,11 +25,10 @@ import { hasPermission } from "../auth/permissions";
import { toast } from "sonner";
import { downloadBlob } from "../lib/download";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { PageHeader } from "../components/common/PageHeader";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import { FilterBar, ListWorkspace, PrimaryTable } from "../components/patterns/ListWorkspace";
import { FilterBar, ListWorkspace, PrimaryTable, StatStrip, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { Button } from "../components/ui/button";
import {
@@ -362,7 +361,7 @@ export function ReimbursementsPage() {
return (
<ListWorkspace>
<PageHeader
<WorkspaceHeader
title="我的报销"
subtitle="草稿、审批、支付。"
actions={
@@ -513,12 +512,12 @@ export function ReimbursementsPage() {
</p>
) : null}
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<StatStrip columns={4}>
<MetricCard title="当前列表" value={stats.currentPageCount} note={`共 ${listQuery.data?.total ?? 0} 条`} />
<MetricCard title="当前金额" value={formatCurrency(stats.currentPageAmount)} note="当前页" />
<MetricCard title="审批中" value={stats.reviewing} note="待推进" tone={stats.reviewing > 0 ? "warning" : "neutral"} />
<MetricCard title="已支付" value={stats.paid} note={scope === "all" ? "全范围" : "仅自己"} />
</section>
</StatStrip>
<FilterBar className="space-y-4">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
+5 -5
查看文件
@@ -1,5 +1,5 @@
import { Link } from "react-router-dom";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
const ITEMS = [
{ to: "/system/workspace", title: "工作区设置", description: "调整小团队模式的导航与模块显隐。" },
@@ -14,17 +14,17 @@ const ITEMS = [
export function SystemSettingsPage() {
return (
<div className="space-y-6">
<PageHeader title="系统设置" subtitle="小团队模式下的后台设置总入口。" />
<ListWorkspace>
<WorkspaceHeader title="系统设置" subtitle="小团队模式下的后台设置总入口。" />
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{ITEMS.map((item) => (
<Link key={item.to} to={item.to} className="card-std p-6 hover:bg-zinc-50">
<Link key={item.to} to={item.to} className="card-std p-4 hover:bg-zinc-50 md:p-5">
<div className="text-base font-semibold text-zinc-900">{item.title}</div>
<div className="mt-2 text-sm text-zinc-600">{item.description}</div>
</Link>
))}
</div>
</div>
</ListWorkspace>
);
}
+7 -7
查看文件
@@ -8,7 +8,7 @@ import {
updateWorkflowDefinition,
type WorkflowDefinitionEditorModel,
} from "../api/workflowDefinitions";
import { PageHeader } from "../components/common/PageHeader";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
@@ -79,10 +79,10 @@ export function WorkflowDefinitionsPage() {
});
return (
<div className="space-y-6">
<PageHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
<ListWorkspace>
<WorkspaceHeader title="流程定义" subtitle="线性流程的版本化治理;报销继续由审批规则投影。" />
<div className="grid gap-6 xl:grid-cols-[220px,280px,1fr]">
<div className="card-std space-y-3 p-6">
<div className="card-std space-y-3 p-4 md:p-5">
<Label>业务域</Label>
<select className="select-std" value={entityType} onChange={(event) => setEntityType(event.target.value as WorkflowDefinitionEditorModel["entityType"])}>
{ENTITY_TYPES.map((item) => <option key={item} value={item}>{item}</option>)}
@@ -90,7 +90,7 @@ export function WorkflowDefinitionsPage() {
{entityType !== "reimbursement" ? <Button type="button" onClick={() => createMutation.mutate()}>创建草稿</Button> : null}
</div>
<div className="card-std space-y-3 p-6">
<div className="card-std space-y-3 p-4 md:p-5">
<h3 className="text-base font-semibold text-zinc-900">版本列表</h3>
<div className="space-y-2">
{scopedItems.map((item) => (
@@ -103,7 +103,7 @@ export function WorkflowDefinitionsPage() {
</div>
</div>
<div className="card-std space-y-4 p-6">
<div className="card-std space-y-4 p-4 md:p-5">
{selected ? (
<>
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -163,6 +163,6 @@ export function WorkflowDefinitionsPage() {
)}
</div>
</div>
</div>
</ListWorkspace>
);
}
+64 -78
查看文件
@@ -1,3 +1,4 @@
import type { ReactNode } from "react";
import { useMemo, useState } from "react";
import { Link, Navigate, useNavigate, useParams } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -19,6 +20,8 @@ import { CardSkeleton } from "../components/common/CardSkeleton";
import { StatusBadge } from "../components/common/StatusBadge";
import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
import { StatStrip } from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { Button } from "../components/ui/button";
@@ -48,22 +51,6 @@ function formatDateTime(value: string | null | undefined) {
return value.slice(0, 19).replace("T", " ");
}
function workflowStatusLabel(status: string | undefined) {
if (!status) {
return "-";
}
const labels: Record<string, string> = {
pending: "待处理",
completed: "已处理",
rejected: "已驳回",
cancelled: "已取消",
skipped: "已跳过",
};
return labels[status] ?? status;
}
function workflowInstanceStatusLabel(status: string | undefined | null) {
if (!status) {
return "-";
@@ -103,6 +90,15 @@ function workflowActionLabel(action: string | undefined | null) {
return labels[action] ?? action;
}
function DetailField({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3 text-sm">
<dt className="text-xs font-medium text-zinc-500">{label}</dt>
<dd className="mt-1 break-words font-medium text-zinc-900">{value || "-"}</dd>
</div>
);
}
function isAlreadyProcessedTaskError(error: unknown) {
return isApiError(error) && error.details?.task === "already_processed";
}
@@ -240,7 +236,6 @@ export function WorkflowTaskDetailPage() {
meta={(
<>
<StatusBadge status={detail.task.status} />
<span className="info-chip">对象类型:{entityTitle(detail.workflowInstance?.entityType)}</span>
<span className="info-chip">业务状态:{businessStatusLabel}</span>
</>
)}
@@ -255,87 +250,78 @@ export function WorkflowTaskDetailPage() {
<div className="space-y-6">
<SectionPanel title="审批对象摘要">
<div className="space-y-4" data-testid="workflow-task-overview">
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">当前状态</div>
<div className="mt-2"><StatusBadge status={detail.task.status} /></div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">当前节点</div>
<div className="mt-2 font-medium text-zinc-900">{detail.task.stepName}</div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">对象类型</div>
<div className="mt-2 font-medium text-zinc-900">{entityTitle(detail.workflowInstance?.entityType)}</div>
</div>
<div className="rounded-lg border border-zinc-200 px-4 py-3 text-sm">
<div className="text-zinc-500">可执行动作</div>
<div className="mt-2 font-medium text-zinc-900">{allowedActionLabels || "-"}</div>
</div>
</div>
<StatStrip columns={4}>
<MetricCard title="当前节点" value={<span className="text-base">{detail.task.stepName}</span>} />
<MetricCard title="业务类型" value={<span className="text-base">{entityTitle(detail.workflowInstance?.entityType)}</span>} />
<MetricCard title="可执行动作" value={<span className="text-base">{allowedActionLabels || "-"}</span>} />
<MetricCard title="创建时间" value={<span className="text-base">{formatDateTime(detail.task.createdAt)}</span>} />
</StatStrip>
<div className="rounded-lg border border-zinc-200 p-4 text-sm" data-testid="workflow-task-summary">
<div>状态:{workflowStatusLabel(detail.task.status)}</div>
<div>业务状态:{businessStatusLabel}</div>
<div>对象类型:{entityTitle(detail.workflowInstance?.entityType)}</div>
<dl className="grid gap-3 md:grid-cols-2" data-testid="workflow-task-summary">
{detail.reimbursement ? (
<>
<div>报销单号:{detail.reimbursement.formNumber ?? "-"}</div>
<div>报销金额:{detail.reimbursement.totalAmount ?? "-"}</div>
<div>提交人:{detail.reimbursement.submitter?.name ?? "-"}</div>
<div>报销人:{detail.reimbursement.owner?.name ?? "-"}</div>
<DetailField label="报销单号" value={detail.reimbursement.formNumber ?? "-"} />
<DetailField label="报销金额" value={detail.reimbursement.totalAmount ?? "-"} />
<DetailField label="提交人" value={detail.reimbursement.submitter?.name ?? "-"} />
<DetailField label="报销人" value={detail.reimbursement.owner?.name ?? "-"} />
</>
) : null}
{detail.leaveRequest ? (
<>
<div>假期类型:{detail.leaveRequest.leaveType?.name ?? "-"}</div>
<div>申请人:{detail.leaveRequest.owner?.name ?? "-"}</div>
<div>
时间段:
<DetailField label="假期类型" value={detail.leaveRequest.leaveType?.name ?? "-"} />
<DetailField label="申请人" value={detail.leaveRequest.owner?.name ?? "-"} />
<DetailField
label="时间段"
value={
<>
{detail.leaveRequest.startDate ?? "-"} {detail.leaveRequest.startSession === "am" ? "上午" : "下午"}
{" → "}
{detail.leaveRequest.endDate ?? "-"} {detail.leaveRequest.endSession === "am" ? "上午" : "下午"}
</div>
<div>请假天数:{detail.leaveRequest.requestedDays}</div>
<div>说明:{detail.leaveRequest.reason ?? "-"}</div>
<div>附件:{detail.leaveRequest.attachment?.originalName ?? "-"}</div>
</>
}
/>
<DetailField label="请假天数" value={detail.leaveRequest.requestedDays} />
<DetailField label="说明" value={detail.leaveRequest.reason ?? "-"} />
<DetailField label="附件" value={detail.leaveRequest.attachment?.originalName ?? "-"} />
</>
) : null}
{detail.purchaseRequest ? (
<>
<div>采购单号:{resolvePurchaseDisplayCode(detail.purchaseRequest)}</div>
<div>标题:{detail.purchaseRequest.title || "-"}</div>
<div>申请人:{detail.purchaseRequest.applicant?.name ?? "-"}</div>
<div>供应商:{detail.purchaseRequest.supplier?.name ?? "-"}</div>
<div>法体:{detail.purchaseRequest.legalEntity?.name ?? "-"}</div>
<div>成本中心:{detail.purchaseRequest.costCenter?.name ?? "-"}</div>
<div>单据分类:{detail.purchaseRequest.documentCategory?.name ?? "-"}</div>
<div>金额:{detail.purchaseRequest.totalAmount ?? "-"} {detail.purchaseRequest.currencyCode}</div>
<div>预计到货:{detail.purchaseRequest.expectedDeliveryDate ?? "-"}</div>
<div>说明:{detail.purchaseRequest.reason ?? "-"}</div>
<DetailField label="采购单号" value={resolvePurchaseDisplayCode(detail.purchaseRequest)} />
<DetailField label="标题" value={detail.purchaseRequest.title || "-"} />
<DetailField label="申请人" value={detail.purchaseRequest.applicant?.name ?? "-"} />
<DetailField label="供应商" value={detail.purchaseRequest.supplier?.name ?? "-"} />
<DetailField label="法体" value={detail.purchaseRequest.legalEntity?.name ?? "-"} />
<DetailField label="成本中心" value={detail.purchaseRequest.costCenter?.name ?? "-"} />
<DetailField label="单据分类" value={detail.purchaseRequest.documentCategory?.name ?? "-"} />
<DetailField label="金额" value={`${detail.purchaseRequest.totalAmount ?? "-"} ${detail.purchaseRequest.currencyCode}`} />
<DetailField label="预计到货" value={detail.purchaseRequest.expectedDeliveryDate ?? "-"} />
<DetailField label="说明" value={detail.purchaseRequest.reason ?? "-"} />
</>
) : null}
{detail.travelRequest ? (
<>
<div>差旅单号:{detail.travelRequest.requestNo ?? "暂未生成"}</div>
<div>申请人:{detail.travelRequest.applicant?.name ?? "-"}</div>
<div>出差类型:{detail.travelRequest.tripType === "international" ? "国际" : "国内"}</div>
<div>法体:{detail.travelRequest.legalEntity?.name ?? "-"}</div>
<div>成本中心:{detail.travelRequest.costCenter?.name ?? "-"}</div>
<div>起止日期:{detail.travelRequest.startDate ?? "-"} → {detail.travelRequest.endDate ?? "-"}</div>
<div>预估金额:{detail.travelRequest.estimatedAmount ?? "-"}</div>
<div>出差目的:{detail.travelRequest.purpose || "-"}</div>
<div>
行程段:
{detail.travelRequest.segments.length
? detail.travelRequest.segments
.map((item) => `${item.fromCity}→${item.toCity}(${item.segmentRole})`)
.join(";")
: "-"}
</div>
<DetailField label="差旅单号" value={detail.travelRequest.requestNo ?? "暂未生成"} />
<DetailField label="申请人" value={detail.travelRequest.applicant?.name ?? "-"} />
<DetailField label="出差类型" value={detail.travelRequest.tripType === "international" ? "国际" : "国内"} />
<DetailField label="法体" value={detail.travelRequest.legalEntity?.name ?? "-"} />
<DetailField label="成本中心" value={detail.travelRequest.costCenter?.name ?? "-"} />
<DetailField label="起止日期" value={`${detail.travelRequest.startDate ?? "-"} → ${detail.travelRequest.endDate ?? "-"}`} />
<DetailField label="预估金额" value={detail.travelRequest.estimatedAmount ?? "-"} />
<DetailField label="出差目的" value={detail.travelRequest.purpose || "-"} />
<DetailField
label="行程段"
value={
detail.travelRequest.segments.length
? detail.travelRequest.segments
.map((item) => `${item.fromCity}→${item.toCity}(${item.segmentRole})`)
.join(";")
: "-"
}
/>
</>
) : null}
</div>
</dl>
</div>
</SectionPanel>
+67 -49
查看文件
@@ -9,10 +9,19 @@ import {
fetchWorkflowTaskSummary,
type WorkflowTaskSummary,
} from "../api/workflow";
import { PageHeader } from "../components/common/PageHeader";
import { Pagination } from "../components/common/Pagination";
import { StatusBadge } from "../components/common/StatusBadge";
import { TableSkeleton } from "../components/common/TableSkeleton";
import {
ActionToolbar,
FilterBar,
ListWorkspace,
PrimaryTable,
StatStrip,
WorkspaceEmptyState,
WorkspaceHeader,
} from "../components/patterns/ListWorkspace";
import { MetricCard } from "../components/patterns/MetricCard";
import { Button } from "../components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../components/ui/table";
import { Tabs, TabsList, TabsTrigger } from "../components/ui/tabs";
@@ -187,35 +196,34 @@ export function WorkflowTasksPage() {
const taskItems = query.data?.items ?? [];
return (
<div className="space-y-4 md:space-y-6">
<PageHeader title="审批中心" subtitle="集中处理待办、查看已办和业务审批历史。" />
<ListWorkspace>
<WorkspaceHeader
eyebrow="审批"
title="审批中心"
subtitle="集中处理待办、查看已办和业务审批历史。"
/>
<div className="grid gap-3 md:grid-cols-4" data-testid="workflow-task-summary">
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
<div className="text-xs text-zinc-500">待办总数</div>
<div className="mt-2 text-2xl font-semibold text-zinc-900">{summaryQuery.data?.pendingTotal ?? "-"}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
<div className="text-xs text-zinc-500">超时待办</div>
<div className="mt-2 text-2xl font-semibold text-amber-700">{summaryQuery.data?.overdueTotal ?? "-"}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
<div className="text-xs text-zinc-500">最早待处理</div>
<div className="mt-2 text-sm font-medium text-zinc-900">{formatSummaryTime(summaryQuery.data?.earliestPendingAt)}</div>
</div>
<div className="rounded-lg border border-zinc-200 bg-white px-4 py-3">
<div className="text-xs text-zinc-500">业务分布</div>
<div className="mt-2 truncate text-sm font-medium text-zinc-900">
{(summaryQuery.data?.byEntityType ?? [])
.filter((item) => item.pendingTotal > 0)
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
.join(" / ") || "-"}
</div>
</div>
<div data-testid="workflow-task-summary">
<StatStrip columns={4}>
<MetricCard title="待办总数" value={summaryQuery.data?.pendingTotal ?? "-"} />
<MetricCard title="超时待办" value={summaryQuery.data?.overdueTotal ?? "-"} tone="warning" />
<MetricCard title="最早待处理" value={formatSummaryTime(summaryQuery.data?.earliestPendingAt)} />
<MetricCard
title="业务分布"
value={
<span className="block truncate text-base">
{(summaryQuery.data?.byEntityType ?? [])
.filter((item) => item.pendingTotal > 0)
.map((item) => `${entityLabel(item.key)} ${item.pendingTotal}`)
.join(" / ") || "-"}
</span>
}
/>
</StatStrip>
</div>
<div className="card-std space-y-4 p-4 md:p-5">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<FilterBar>
<ActionToolbar className="items-start">
<Tabs
data-testid="workflow-tasks-status-tabs"
value={status}
@@ -234,13 +242,13 @@ export function WorkflowTasksPage() {
</TabsList>
</Tabs>
<div className="flex items-center gap-2">
<div className="flex w-full items-center gap-2 sm:w-auto">
<label className="text-sm font-medium text-zinc-700" htmlFor="workflow-entity-type-filter">
业务类型
</label>
<select
id="workflow-entity-type-filter"
className="select-std min-w-36"
className="select-std min-w-36 flex-1 sm:flex-none"
data-testid="workflow-tasks-entity-filter"
value={entityType}
onChange={(event) => updateSearchParams({ entityType: event.target.value as WorkflowEntityFilter, page: 1 })}
@@ -252,7 +260,8 @@ export function WorkflowTasksPage() {
))}
</select>
</div>
</div>
</ActionToolbar>
</FilterBar>
{query.isLoading ? <TableSkeleton columns={7} rows={5} /> : null}
@@ -270,24 +279,36 @@ export function WorkflowTasksPage() {
{!query.isLoading && !query.isError ? (
<>
<div className="space-y-3 md:hidden" data-testid="workflow-tasks-mobile-list">
<div className="mobile-page-stack md:hidden" data-testid="workflow-tasks-mobile-list">
{taskItems.length ? (
taskItems.map((task) => (
<article className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm" key={task.id}>
<div className="flex items-start justify-between gap-3">
<article className="mobile-list-card" key={task.id}>
<div className="mobile-list-card__header">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{displayTitle(task)}</div>
<div className="mt-1 text-xs text-zinc-500">
<div className="mobile-list-card__title">{displayTitle(task)}</div>
<div className="mobile-list-card__subtitle">
{entityLabel(task.workflowInstance?.entityType)} · {displayReference(task)}
</div>
</div>
<StatusBadge className="shrink-0" status={task.status} />
<StatusBadge className="mobile-status-slot" status={task.status} />
</div>
<div className="mt-3 grid gap-1 rounded-lg bg-zinc-50 px-3 py-2 text-xs text-zinc-600">
<div>申请人:{displayApplicant(task)}</div>
<div>金额/天数:{displayMetric(task)}</div>
<div>当前节点:{task.stepName}</div>
<div>业务状态:{businessStatusLabel(task.businessSummary?.businessStatus)}</div>
<div className="mobile-field-list mt-3 rounded-lg bg-zinc-50 px-3 py-2">
<div className="mobile-field-row">
<span className="mobile-field-label">申请人</span>
<span className="mobile-field-value">{displayApplicant(task)}</span>
</div>
<div className="mobile-field-row">
<span className="mobile-field-label">金额/天数</span>
<span className="mobile-field-value">{displayMetric(task)}</span>
</div>
<div className="mobile-field-row">
<span className="mobile-field-label">当前节点</span>
<span className="mobile-field-value">{task.stepName}</span>
</div>
<div className="mobile-field-row">
<span className="mobile-field-label">业务状态</span>
<span className="mobile-field-value">{businessStatusLabel(task.businessSummary?.businessStatus)}</span>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-2">
{status === "pending" && (task.allowedActions ?? []).includes("approve") ? (
@@ -317,13 +338,11 @@ export function WorkflowTasksPage() {
</article>
))
) : (
<div className="rounded-xl border border-dashed border-zinc-300 px-4 py-8 text-center text-sm text-zinc-500">
暂无数据
</div>
<WorkspaceEmptyState />
)}
</div>
<div className="hidden rounded-lg border border-zinc-200 md:block">
<PrimaryTable className="hidden md:block">
<Table data-testid="workflow-tasks-table">
<TableHeader>
<tr>
@@ -381,14 +400,14 @@ export function WorkflowTasksPage() {
))
) : (
<TableRow>
<TableCell className="py-3 text-zinc-500" colSpan={8}>
<TableCell className="px-6 py-8 text-center text-zinc-500" colSpan={8}>
暂无数据
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</PrimaryTable>
</>
) : null}
@@ -398,7 +417,6 @@ export function WorkflowTasksPage() {
onPageChange={(p) => updateSearchParams({ page: p })}
testIdPrefix="workflow-tasks"
/>
</div>
</div>
</ListWorkspace>
);
}