feat: 完成P6-P8前端能力闭环(V3,D2,D3,B3)

这个提交包含在:
2026-02-10 11:28:51 +08:00
父节点 6bc09f557b
当前提交 4e5a7793fe
共修改 14 个文件,包含 1151 行新增和 62 行删除
+116
查看文件
@@ -7,12 +7,15 @@ import { exportInvoicePool } from "../api/exports";
import {
archiveInvoice,
bindInvoice,
fetchInvoiceVerifications,
fetchInvoicePoolDetail,
fetchInvoicePoolList,
fetchInvoiceSignedUrl,
overrideInvoiceDuplicate,
patchInvoice,
retryInvoiceVerification,
retryInvoiceOcr,
triggerInvoiceVerification,
unbindInvoice,
uploadInvoice,
type InvoicePoolStatus,
@@ -35,6 +38,7 @@ export function InvoicePoolPage() {
const canManageInvoice = hasPermission(permissions, "invoice.manage");
const canOverrideDuplicate = hasPermission(permissions, "expense.finance.override_duplicate");
const canUploadInvoice = hasPermission(permissions, "invoice.upload");
const canVerifyInvoice = hasPermission(permissions, "invoice.verify");
const canProxySubmit = hasPermission(permissions, "expense.proxy_submit");
const isAdministration = (me?.roleCodes ?? []).includes("administration");
const showProxyOwnerSelector = canProxySubmit && isAdministration;
@@ -90,6 +94,17 @@ export function InvoicePoolPage() {
},
});
const verificationQuery = useQuery({
enabled: Boolean(detailInvoiceId) && canVerifyInvoice,
queryKey: ["invoice-pool", "verification", detailInvoiceId],
queryFn: () => fetchInvoiceVerifications(detailInvoiceId, 20),
refetchInterval: (query) => {
const items = query.state.data?.items ?? [];
const hasPending = items.some((item) => item.status === "pending");
return hasPending ? 3_000 : false;
},
});
useEffect(() => {
if (!detailQuery.data || editOpen) {
return;
@@ -251,6 +266,42 @@ export function InvoicePoolPage() {
},
});
const triggerVerificationMutation = useMutation({
mutationFn: async (invoiceId: string) => triggerInvoiceVerification(invoiceId),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["invoice-pool"] }),
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
]);
setError("");
},
onError: (err) => {
if (isApiError(err)) {
setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`);
} else {
setError("触发验真失败,请稍后重试");
}
},
});
const retryVerificationMutation = useMutation({
mutationFn: async (invoiceId: string) => retryInvoiceVerification(invoiceId),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["invoice-pool"] }),
queryClient.invalidateQueries({ queryKey: ["invoice-pool", "verification"] }),
]);
setError("");
},
onError: (err) => {
if (isApiError(err)) {
setError(`${err.code}: ${mapApiErrorMessage(err.code, err.details, err.message)}`);
} else {
setError("重试验真失败,请稍后重试");
}
},
});
const overrideDuplicateMutation = useMutation({
mutationFn: async (params: { invoiceId: string; refFormNumber: string; reason: string }) =>
overrideInvoiceDuplicate(params.invoiceId, {
@@ -557,8 +608,48 @@ export function InvoicePoolPage() {
判重例外:{detail.isDuplicateOverride ? "已例外" : "否"}{" "}
{detail.overrideRefFormNumber ? `(ref=${detail.overrideRefFormNumber})` : ""}
</div>
<div>
验真状态:
{detail.verification
? `${detail.verification.status} (${detail.verification.provider})`
: "未触发"}
</div>
{detail.verification?.resultMessage ? (
<div>验真结果:{detail.verification.resultMessage}</div>
) : null}
{detail.verification?.errorMessage ? (
<div className="text-red-600">验真错误:{detail.verification.errorMessage}</div>
) : null}
<div className="flex flex-wrap gap-2 pt-2">
{canVerifyInvoice ? (
<Button
data-testid="invoice-trigger-verification"
size="sm"
variant="secondary"
type="button"
disabled={triggerVerificationMutation.isPending || detail.status === "pending_ocr"}
onClick={() => triggerVerificationMutation.mutate(detail.id)}
>
{triggerVerificationMutation.isPending ? "触发中..." : "触发验真"}
</Button>
) : null}
{canVerifyInvoice &&
detail.verification &&
["failed", "error"].includes(detail.verification.status) ? (
<Button
data-testid="invoice-retry-verification"
size="sm"
variant="secondary"
type="button"
disabled={retryVerificationMutation.isPending}
onClick={() => retryVerificationMutation.mutate(detail.id)}
>
{retryVerificationMutation.isPending ? "重试中..." : "重试验真"}
</Button>
) : null}
{canEditDetail ? (
<Button
data-testid="invoice-edit-open"
@@ -647,6 +738,31 @@ export function InvoicePoolPage() {
) : null}
</div>
{canVerifyInvoice ? (
<div className="mt-3 rounded-lg border border-slate-200 p-3" data-testid="invoice-verification-records">
<p className="mb-2 text-sm font-medium text-slate-800">验真记录</p>
{(verificationQuery.data?.items ?? []).length ? (
<div className="space-y-2 text-xs">
{(verificationQuery.data?.items ?? []).map((record) => (
<div key={record.id} className="rounded-md border border-slate-200 px-2 py-1">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="font-mono">{record.id}</span>
<span>{record.status}</span>
</div>
<div className="text-slate-600">
provider={record.provider} attempts={record.attempts} requestedAt={record.requestedAt || "-"}
</div>
{record.resultMessage ? <div className="text-slate-700">{record.resultMessage}</div> : null}
{record.errorMessage ? <div className="text-red-600">{record.errorMessage}</div> : null}
</div>
))}
</div>
) : (
<p className="text-xs text-slate-500">暂无验真记录</p>
)}
</div>
) : null}
{editOpen ? (
<div className="mt-4 space-y-3 rounded-lg border border-slate-200 p-4">
<p className="text-sm font-medium text-slate-800">人工修正(仅必要字段)</p>