统一业务台账首屏布局(05-30-fix-payment-ledger-layout-consistency)
这个提交包含在:
@@ -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) => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户