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

这个提交包含在:
2026-05-30 21:44:16 +08:00
父节点 eea2af7718
当前提交 16fd9bdf52
共修改 25 个文件,包含 834 行新增和 543 行删除
+143 -125
查看文件
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useSearchParams } from "react-router-dom";
import { isApiError } from "../api/types";
import { mapApiErrorMessage } from "../api/errorMessages";
import { fetchMyDelegations } from "../api/delegations";
@@ -51,6 +52,7 @@ type UploadBatchResult = {
export function InvoicePoolPage() {
const queryClient = useQueryClient();
const [searchParams, setSearchParams] = useSearchParams();
const { permissions, me } = useAuth();
const canManageInvoice = hasPermission(permissions, "invoice.manage");
const canOverrideDuplicate = hasPermission(permissions, "expense.finance.override_duplicate");
@@ -71,7 +73,7 @@ export function InvoicePoolPage() {
const [selectedReimbursementId, setSelectedReimbursementId] = useState("");
const [selectedTravelRequestId, setSelectedTravelRequestId] = useState("");
const [selectedTravelOrderId, setSelectedTravelOrderId] = useState("");
const [detailInvoiceId, setDetailInvoiceId] = useState("");
const [detailInvoiceId, setDetailInvoiceId] = useState(searchParams.get("id") ?? "");
const [editOpen, setEditOpen] = useState(false);
const [editSetAvailable, setEditSetAvailable] = useState(false);
const [editRemarks, setEditRemarks] = useState("");
@@ -203,10 +205,25 @@ export function InvoicePoolPage() {
}, [canManageTravelPayments]);
useEffect(() => {
if (!detailInvoiceId && listQuery.data?.items?.length) {
setDetailInvoiceId(listQuery.data.items[0].id);
const searchInvoiceId = searchParams.get("id") ?? "";
if (searchInvoiceId !== detailInvoiceId) {
setDetailInvoiceId(searchInvoiceId);
}
}, [detailInvoiceId, listQuery.data?.items]);
}, [detailInvoiceId, searchParams]);
useEffect(() => {
const currentId = searchParams.get("id") ?? "";
if (detailInvoiceId === currentId) {
return;
}
const next = new URLSearchParams(searchParams);
if (detailInvoiceId) {
next.set("id", detailInvoiceId);
} else {
next.delete("id");
}
setSearchParams(next, { replace: true });
}, [detailInvoiceId, searchParams, setSearchParams]);
// --- Helper for error handling ---
@@ -453,6 +470,13 @@ export function InvoicePoolPage() {
detail.ownerEmployeeId === me?.id ||
detail.uploadedByEmployeeId === me?.id),
);
const isInvoiceDetailMode = Boolean(detailInvoiceId);
const returnToInvoiceList = () => {
setDetailInvoiceId("");
const next = new URLSearchParams(searchParams);
next.delete("id");
setSearchParams(next, { replace: true });
};
// --- Render ---
@@ -460,11 +484,18 @@ export function InvoicePoolPage() {
<ListWorkspace>
<WorkspaceHeader
title="票据中心"
subtitle="优先看识别状态、失败票据和待绑定票据;导出、全范围和财务交接进入台账工具。"
actions={
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((current) => !current)}>
台账工具
</Button>
<div className="flex flex-wrap gap-2">
{isInvoiceDetailMode ? (
<Button type="button" variant="secondary" onClick={returnToInvoiceList}>
返回列表
</Button>
) : (
<Button type="button" variant={showLedgerTools ? "default" : "secondary"} onClick={() => setShowLedgerTools((current) => !current)}>
台账工具
</Button>
)}
</div>
}
/>
@@ -474,14 +505,11 @@ export function InvoicePoolPage() {
</p>
) : null}
{showLedgerTools ? (
{!isInvoiceDetailMode && showLedgerTools ? (
<div className="finance-command-panel space-y-3" data-testid="invoice-ledger-tools">
<div className="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
<div>
<h3 className="text-base font-semibold text-zinc-900">台账工具</h3>
<p className="mt-1 text-sm leading-6 text-zinc-500">
导出按当前状态和范围生成,只用于财务复核、OCR 排障和代账交接;普通员工日常补票据仍从报销详情或移动上传进入。
</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -504,124 +532,114 @@ export function InvoicePoolPage() {
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
票据原文、预览和下载仍按详情页权限与审计控制处理,列表导出不作为敏感材料分发入口。
</div>
<InvoiceUploadCard
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
isUploading={uploadMutation.isPending}
uploadProgressLabel={
uploadProgress
? `上传中 ${uploadProgress.completed}/${uploadProgress.total}`
: "上传中..."
}
onUpload={() => uploadMutation.mutate()}
showProxyOwnerSelector={showProxyOwnerSelector}
proxyDelegationsQuery={proxyDelegationsQuery}
uploadOwnerEmployeeId={uploadOwnerEmployeeId}
setUploadOwnerEmployeeId={setUploadOwnerEmployeeId}
/>
<InvoiceBindCard
listQuery={listQuery}
reimbursementQuery={reimbursementQuery}
travelRequestQuery={travelRequestQuery}
travelRequestDetailQuery={travelRequestDetailQuery}
bindMutation={bindMutation}
bindMode={bindMode}
setBindMode={setBindMode}
canManageTravelPayments={canManageTravelPayments}
selectedInvoiceId={selectedInvoiceId}
setSelectedInvoiceId={setSelectedInvoiceId}
selectedReimbursementId={selectedReimbursementId}
setSelectedReimbursementId={setSelectedReimbursementId}
selectedTravelRequestId={selectedTravelRequestId}
setSelectedTravelRequestId={setSelectedTravelRequestId}
selectedTravelOrderId={selectedTravelOrderId}
setSelectedTravelOrderId={setSelectedTravelOrderId}
/>
</div>
) : null}
<div className="finance-status-strip" data-testid="invoice-pool-summary">
<div className="finance-metric-card">
<div className="finance-metric-card__label">票据总数</div>
<div className="finance-metric-card__value">{invoiceStats.total}</div>
{!isInvoiceDetailMode ? (
<div className="flex flex-wrap gap-2" data-testid="invoice-pool-summary">
{[
["全部", invoiceStats.total],
["识别中", invoiceStats.pending],
["识别失败", invoiceStats.failed],
["可绑定", invoiceStats.available],
["已绑定", invoiceStats.bound],
].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">{invoiceStats.pending}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">识别失败</div>
<div className="finance-metric-card__value">{invoiceStats.failed}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">可绑定</div>
<div className="finance-metric-card__value">{invoiceStats.available}</div>
</div>
<div className="finance-metric-card">
<div className="finance-metric-card__label">已绑定</div>
<div className="finance-metric-card__value">{invoiceStats.bound}</div>
</div>
</div>
) : null}
<InvoiceUploadCard
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
isUploading={uploadMutation.isPending}
uploadProgressLabel={
uploadProgress
? `上传中 ${uploadProgress.completed}/${uploadProgress.total}`
: "上传中..."
}
onUpload={() => uploadMutation.mutate()}
showProxyOwnerSelector={showProxyOwnerSelector}
proxyDelegationsQuery={proxyDelegationsQuery}
uploadOwnerEmployeeId={uploadOwnerEmployeeId}
setUploadOwnerEmployeeId={setUploadOwnerEmployeeId}
/>
<InvoiceListCard
listQuery={listQuery}
status={status}
setStatus={setStatus}
ownerScope={ownerScope}
setOwnerScope={setOwnerScope}
page={page}
setPage={setPage}
totalPages={totalPages}
detailInvoiceId={detailInvoiceId}
setDetailInvoiceId={setDetailInvoiceId}
signedUrlMutation={signedUrlMutation}
/>
<InvoiceBindCard
listQuery={listQuery}
reimbursementQuery={reimbursementQuery}
travelRequestQuery={travelRequestQuery}
travelRequestDetailQuery={travelRequestDetailQuery}
bindMutation={bindMutation}
bindMode={bindMode}
setBindMode={setBindMode}
canManageTravelPayments={canManageTravelPayments}
selectedInvoiceId={selectedInvoiceId}
setSelectedInvoiceId={setSelectedInvoiceId}
selectedReimbursementId={selectedReimbursementId}
setSelectedReimbursementId={setSelectedReimbursementId}
selectedTravelRequestId={selectedTravelRequestId}
setSelectedTravelRequestId={setSelectedTravelRequestId}
selectedTravelOrderId={selectedTravelOrderId}
setSelectedTravelOrderId={setSelectedTravelOrderId}
/>
<InvoiceDetailPanel
detail={detail}
detailQuery={detailQuery}
canEditDetail={canEditDetail}
canVerifyInvoice={canVerifyInvoice}
canUploadInvoice={canUploadInvoice}
canOverrideDuplicate={canOverrideDuplicate}
patchMutation={patchMutation}
archiveMutation={archiveMutation}
unbindMutation={unbindMutation}
retryOcrMutation={retryOcrMutation}
triggerVerificationMutation={triggerVerificationMutation}
retryVerificationMutation={retryVerificationMutation}
overrideDuplicateMutation={overrideDuplicateMutation}
verificationQuery={verificationQuery}
editOpen={editOpen}
setEditOpen={setEditOpen}
editSetAvailable={editSetAvailable}
setEditSetAvailable={setEditSetAvailable}
editRemarks={editRemarks}
setEditRemarks={setEditRemarks}
editInvoiceCode={editInvoiceCode}
setEditInvoiceCode={setEditInvoiceCode}
editInvoiceNumber={editInvoiceNumber}
setEditInvoiceNumber={setEditInvoiceNumber}
editInvoiceDate={editInvoiceDate}
setEditInvoiceDate={setEditInvoiceDate}
editSellerName={editSellerName}
setEditSellerName={setEditSellerName}
editBuyerName={editBuyerName}
setEditBuyerName={setEditBuyerName}
editAmount={editAmount}
setEditAmount={setEditAmount}
editTaxAmount={editTaxAmount}
setEditTaxAmount={setEditTaxAmount}
editTotalAmount={editTotalAmount}
setEditTotalAmount={setEditTotalAmount}
confirmAction={confirmAction}
setConfirmAction={setConfirmAction}
/>
{!isInvoiceDetailMode ? (
<InvoiceListCard
listQuery={listQuery}
status={status}
setStatus={setStatus}
ownerScope={ownerScope}
setOwnerScope={setOwnerScope}
page={page}
setPage={setPage}
totalPages={totalPages}
detailInvoiceId={detailInvoiceId}
setDetailInvoiceId={setDetailInvoiceId}
signedUrlMutation={signedUrlMutation}
/>
) : (
<InvoiceDetailPanel
detail={detail}
detailQuery={detailQuery}
canEditDetail={canEditDetail}
canVerifyInvoice={canVerifyInvoice}
canUploadInvoice={canUploadInvoice}
canOverrideDuplicate={canOverrideDuplicate}
patchMutation={patchMutation}
archiveMutation={archiveMutation}
unbindMutation={unbindMutation}
retryOcrMutation={retryOcrMutation}
triggerVerificationMutation={triggerVerificationMutation}
retryVerificationMutation={retryVerificationMutation}
overrideDuplicateMutation={overrideDuplicateMutation}
verificationQuery={verificationQuery}
editOpen={editOpen}
setEditOpen={setEditOpen}
editSetAvailable={editSetAvailable}
setEditSetAvailable={setEditSetAvailable}
editRemarks={editRemarks}
setEditRemarks={setEditRemarks}
editInvoiceCode={editInvoiceCode}
setEditInvoiceCode={setEditInvoiceCode}
editInvoiceNumber={editInvoiceNumber}
setEditInvoiceNumber={setEditInvoiceNumber}
editInvoiceDate={editInvoiceDate}
setEditInvoiceDate={setEditInvoiceDate}
editSellerName={editSellerName}
setEditSellerName={setEditSellerName}
editBuyerName={editBuyerName}
setEditBuyerName={setEditBuyerName}
editAmount={editAmount}
setEditAmount={setEditAmount}
editTaxAmount={editTaxAmount}
setEditTaxAmount={setEditTaxAmount}
editTotalAmount={editTotalAmount}
setEditTotalAmount={setEditTotalAmount}
confirmAction={confirmAction}
setConfirmAction={setConfirmAction}
/>
)}
</ListWorkspace>
);
}