统一业务列表 Ant Design 风格(05-31-unify-business-list-ant-design)

这个提交包含在:
2026-05-31 11:39:46 +08:00
父节点 59c942da54
当前提交 40c23c5cf3
共修改 19 个文件,包含 905 行新增和 578 行删除
+65 -44
查看文件
@@ -22,6 +22,10 @@ import { mapApiErrorMessage } from "../api/errorMessages";
import { useAuth } from "../auth/AuthContext";
import { hasPermission } from "../auth/permissions";
import { ListWorkspace, WorkspaceHeader } from "../components/patterns/ListWorkspace";
import {
BusinessMobileList,
BusinessTable,
} from "../components/patterns/BusinessListTemplate";
import { ActionConfirmDialog } from "../components/common/ActionConfirmDialog";
import { AttachmentGovernanceBadges } from "../components/common/AttachmentGovernanceBadges";
import { useAttachmentDownloadReasonDialog } from "../components/common/useAttachmentDownloadReasonDialog";
@@ -130,23 +134,6 @@ export function PaymentBatchesPage() {
const selectedBatch = useMemo<PaymentBatch | null>(() => {
return batches.find((item) => item.id === selectedBatchId) ?? null;
}, [batches, selectedBatchId]);
const batchStats = useMemo(() => {
return {
total: batches.length,
needsReview: batches.filter((item) => item.reviewRequirement.nextStep !== "ready_for_execute" && ["draft", "reviewed"].includes(item.status)).length,
readyToPay: batches.filter((item) => item.reviewRequirement.nextStep === "ready_for_execute" && ["reviewed", "failed"].includes(item.status)).length,
needsReceipt: batches.filter((item) =>
["reviewed", "failed"].includes(item.status) &&
(item.receiptAttachmentCount ?? 0) === 0 &&
item.items.some((entry) =>
["pending", "failed"].includes(entry.status) &&
(entry.paymentChannelOverride ?? item.defaultPaymentChannel) === "bank",
),
).length,
exceptions: batches.filter((item) => item.highRisk || item.status === "failed").length,
};
}, [batches]);
const receiptAttachmentsQuery = useQuery({
queryKey: ["payment-batch-receipts", selectedBatch?.id],
enabled: Boolean(selectedBatch?.id),
@@ -379,22 +366,6 @@ export function PaymentBatchesPage() {
{error ? <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">{error}</div> : null}
{!selectedBatchId ? (
<div className="flex flex-wrap gap-2" data-testid="payment-batches-summary">
{[
["全部", batchStats.total],
["待复核", batchStats.needsReview],
["待付款", batchStats.readyToPay],
["待补回单", batchStats.needsReceipt],
["异常", batchStats.exceptions],
].map(([label, value]) => (
<span key={label} className="rounded-full border border-zinc-200 bg-white px-3 py-1 text-sm text-zinc-700">
{label} <span className="font-semibold text-zinc-900">{value}</span>
</span>
))}
</div>
) : null}
{!selectedBatchId && showBatchTools ? (
<div className="finance-command-panel space-y-4" data-testid="payment-batch-create-panel">
<div>
@@ -691,17 +662,13 @@ export function PaymentBatchesPage() {
<div className="finance-command-panel text-sm text-zinc-500">未找到付款批次</div>
)
) : (
<div className="finance-command-panel" data-testid="payment-batches-list-panel">
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="text-base font-semibold text-zinc-900">批次列表</h3>
<span className="text-sm text-zinc-500">共 {batches.length} 条</span>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<>
<BusinessMobileList testId="payment-batches-mobile-list">
{batches.map((item) => (
<button
key={item.id}
type="button"
className="finance-list-card"
className="w-full rounded-lg border border-zinc-200 bg-white p-3 text-left"
onClick={() => {
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
@@ -718,12 +685,66 @@ export function PaymentBatchesPage() {
{item.highRisk ? " · 高风险" : ""}
{` · 复核 ${item.reviewRequirement.completedReviewCount}/${item.reviewRequirement.requiredReviewCount}`}
</div>
<div className="mt-3 text-sm font-medium text-primary">查看详情</div>
</button>
))}
{!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无付款批次</div> : null}
</div>
</div>
{!listQuery.data?.items?.length ? <div className="rounded-lg border border-zinc-200 bg-white p-4 text-sm text-zinc-500">暂无付款批次</div> : null}
</BusinessMobileList>
<BusinessTable testId="payment-batches-table">
<thead className="table-header">
<tr>
<th className="table-th">批次</th>
<th className="table-th">实体</th>
<th className="table-th">金额</th>
<th className="table-th">复核</th>
<th className="table-th">状态</th>
<th className="table-th w-28">操作</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-50">
{batches.length ? (
batches.map((item) => (
<tr className="table-row" key={item.id}>
<td className="table-td">
<div className="font-semibold text-zinc-900">{item.batchNo}</div>
<div className="text-xs text-zinc-500">{item.name}</div>
</td>
<td className="table-td">{entityTypeLabel(item.entityType)}</td>
<td className="table-td-strong">{item.totalAmount}</td>
<td className="table-td">
{item.reviewRequirement.completedReviewCount}/{item.reviewRequirement.requiredReviewCount}
</td>
<td className="table-td">
<div className="flex flex-wrap items-center gap-2">
<StatusBadge status={item.status} label={batchStatusLabel(item.status)} />
{item.highRisk ? <span className="badge-warning">高风险</span> : null}
</div>
</td>
<td className="table-td">
<Button
type="button"
size="sm"
variant="secondary"
onClick={() => {
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
setSearchParams(next, { replace: true });
}}
>
查看详情
</Button>
</td>
</tr>
))
) : (
<tr>
<td className="py-8 text-center text-sm text-zinc-500" colSpan={6}>
暂无付款批次
</td>
</tr>
)}
</tbody>
</BusinessTable>
</>
)}
<ActionConfirmDialog
open={Boolean(confirmPaymentAction)}