统一业务台账首屏布局(05-30-fix-payment-ledger-layout-consistency)

这个提交包含在:
2026-05-30 21:44:16 +08:00
父节点 eea2af7718
当前提交 16fd9bdf52
共修改 25 个文件,包含 834 行新增和 543 行删除
+75 -80
查看文件
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import {
@@ -128,7 +128,7 @@ export function PaymentBatchesPage() {
const selectedBatchId = searchParams.get("id") ?? "";
const batches = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]);
const selectedBatch = useMemo<PaymentBatch | null>(() => {
return batches.find((item) => item.id === selectedBatchId) ?? batches[0] ?? null;
return batches.find((item) => item.id === selectedBatchId) ?? null;
}, [batches, selectedBatchId]);
const batchStats = useMemo(() => {
return {
@@ -152,17 +152,6 @@ export function PaymentBatchesPage() {
enabled: Boolean(selectedBatch?.id),
queryFn: () => fetchPaymentBatchReceiptAttachments(selectedBatch!.id),
});
useEffect(() => {
if (!selectedBatch) {
return;
}
if (selectedBatch.id === selectedBatchId) {
return;
}
const next = new URLSearchParams(searchParams);
next.set("id", selectedBatch.id);
setSearchParams(next, { replace: true });
}, [searchParams, selectedBatch, selectedBatchId, setSearchParams]);
const canEditSelectedBatch =
canManageBatch &&
@@ -195,6 +184,12 @@ export function PaymentBatchesPage() {
await queryClient.invalidateQueries({ queryKey: ["ap-operations"] });
};
const returnToList = () => {
const next = new URLSearchParams(searchParams);
next.delete("id");
setSearchParams(next, { replace: true });
};
const createMutation = useMutation({
mutationFn: async () =>
createPaymentBatch({
@@ -363,51 +358,47 @@ export function PaymentBatchesPage() {
{downloadReasonDialog}
<WorkspaceHeader
title="付款"
subtitle="先核对待复核、待付款和异常批次;真实付款在线下完成,OA 只记录复核、回单和付款状态。"
actions={
<>
<div className="flex flex-wrap gap-2">
{selectedBatchId ? (
<Button type="button" variant="secondary" onClick={returnToList}>
返回列表
</Button>
) : null}
<Button type="button" variant="secondary" onClick={() => void refresh()}>
刷新
</Button>
<Button type="button" variant={showBatchTools ? "default" : "secondary"} onClick={() => setShowBatchTools((current) => !current)}>
台账工具
</Button>
</>
{!selectedBatchId ? (
<Button type="button" variant={showBatchTools ? "default" : "secondary"} onClick={() => setShowBatchTools((current) => !current)}>
台账工具
</Button>
) : null}
</div>
}
/>
{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="finance-status-strip" data-testid="payment-batches-summary">
<div className="finance-metric-card">
<div className="finance-metric-card__label">待复核</div>
<div className="finance-metric-card__value">{batchStats.needsReview}</div>
{!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>
<div className="finance-metric-card">
<div className="finance-metric-card__label">待付款标记</div>
<div className="finance-metric-card__value">{batchStats.readyToPay}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">待补回单</div>
<div className="finance-metric-card__value">{batchStats.needsReceipt}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">异常批次</div>
<div className="finance-metric-card__value">{batchStats.exceptions}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">批次数</div>
<div className="finance-metric-card__value">{batchStats.total}</div>
</div>
</div>
) : null}
{showBatchTools ? (
{!selectedBatchId && showBatchTools ? (
<div className="finance-command-panel space-y-4" data-testid="payment-batch-create-panel">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具 · 新建批次</h3>
<p className="mt-1 text-sm text-zinc-500">
仅把已确认待付款的单据归入批次。真实付款仍在银行或既有财务流程完成,OA 记录复核、回单和状态。
</p>
<h3 className="text-base font-semibold text-zinc-900">新建批次</h3>
</div>
{!canManageBatch ? (
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
@@ -490,39 +481,8 @@ export function PaymentBatchesPage() {
</div>
) : null}
<div className="finance-workspace-grid">
<div className="grid gap-5 xl:col-span-2 xl:grid-cols-[340px,minmax(0,1fr)]">
<div className="finance-command-panel">
<h3 className="mb-3 text-base font-semibold text-zinc-900">批次列表</h3>
<div className="space-y-2">
{batches.map((item) => (
<button
key={item.id}
type="button"
className={selectedBatch?.id === item.id ? "finance-list-card finance-list-card--active" : "finance-list-card"}
onClick={() => {
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
setSearchParams(next, { replace: true });
}}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.batchNo}</div>
<StatusBadge status={item.status} label={batchStatusLabel(item.status)} />
</div>
<div className="mt-1 text-sm text-zinc-600">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500">
金额 {item.totalAmount}
{item.highRisk ? " · 高风险" : ""}
{` · 复核 ${item.reviewRequirement.completedReviewCount}/${item.reviewRequirement.requiredReviewCount}`}
</div>
</button>
))}
{!listQuery.data?.items?.length ? <div className="text-sm text-zinc-500">暂无付款批次</div> : null}
</div>
</div>
{selectedBatch ? (
{selectedBatchId ? (
selectedBatch ? (
<div className="space-y-5">
<div className="finance-command-panel space-y-3" data-testid="payment-batch-detail-panel">
<div className="flex flex-wrap items-center justify-between gap-3">
@@ -727,9 +687,44 @@ export function PaymentBatchesPage() {
</div>
</div>
</div>
) : null}
) : (
<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">
{batches.map((item) => (
<button
key={item.id}
type="button"
className="finance-list-card"
onClick={() => {
const next = new URLSearchParams(searchParams);
next.set("id", item.id);
setSearchParams(next, { replace: true });
}}
>
<div className="flex items-center justify-between gap-3">
<div className="font-medium text-zinc-900">{item.batchNo}</div>
<StatusBadge status={item.status} label={batchStatusLabel(item.status)} />
</div>
<div className="mt-1 text-sm text-zinc-600">{item.name}</div>
<div className="mt-1 text-xs text-zinc-500">
金额 {item.totalAmount}
{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>
</div>
)}
<ActionConfirmDialog
open={Boolean(confirmPaymentAction)}
onOpenChange={(open) => {