R73-A3 优化轻交互 OCR 前端处理

这个提交包含在:
2026-04-28 21:14:00 +08:00
父节点 b7c018922c
当前提交 03f384fdfe
共修改 8 个文件,包含 227 行新增和 12 行删除
+39 -6
查看文件
@@ -48,6 +48,7 @@ import { DetailSideMeta } from "../components/patterns/DetailSideMeta";
import { EntityOverviewHero } from "../components/patterns/EntityOverviewHero";
import { PageScaffold } from "../components/patterns/PageScaffold";
import { SectionPanel } from "../components/patterns/SectionPanel";
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
import { TEST_IDS } from "../lib/testIds";
import { ReimbursementActions, type PayPayload } from "./reimbursement-detail/ReimbursementActions";
import {
@@ -437,6 +438,12 @@ export function ReimbursementDetailPage() {
if (!failedResults.length) {
setInvoiceActionError("");
setIsInvoiceDialogOpen(false);
setInvoiceSearchInput("");
setInvoiceSearch("");
setSelectedInvoices({});
setSelectedInvoiceFile(null);
setPendingOcrInvoiceId(null);
toast.success(`已绑定 ${succeededIds.length} 张发票`);
return;
}
@@ -482,13 +489,27 @@ export function ReimbursementDetailPage() {
if (!ownerEmployeeId) {
throw new Error("missing ownerEmployeeId");
}
return uploadInvoice(file, undefined, ownerEmployeeId);
return uploadInvoice(file, "vat", ownerEmployeeId);
},
onSuccess: async (result) => {
setInvoiceActionError("");
setSelectedInvoiceFile(null);
setInvoiceSearchInput("");
setInvoiceSearch("");
if (result.status === "available") {
const refreshed = await bindableInvoicesQuery.refetch();
if (refreshed.data?.items.some((item) => item.id === result.invoicePoolId)) {
setSelectedInvoices((previous) => ({
...previous,
[result.invoicePoolId]: previous[result.invoicePoolId] ?? "",
}));
}
setPendingOcrInvoiceId(null);
toast.success("发票上传成功,可直接绑定");
return;
}
setPendingOcrInvoiceId(result.invoicePoolId);
toast.success("发票上传成功,OCR 识别中");
@@ -512,7 +533,9 @@ export function ReimbursementDetailPage() {
if (invoice.status === "ocr_failed") {
setPendingOcrInvoiceId(null);
toast.error("发票 OCR 识别失败,请前往票据中心处理");
const message = `发票 OCR 未完成:${formatInvoiceOcrErrorMessage(invoice.ocrLastError)}可前往票据中心继续修正或重试。`;
setInvoiceActionError(message);
toast.error("发票 OCR 识别失败,请在当前提示或票据中心继续处理");
return;
}
} catch {
@@ -522,6 +545,7 @@ export function ReimbursementDetailPage() {
setPendingOcrInvoiceId(null);
await queryClient.invalidateQueries({ queryKey: ["invoice-pool", "bindable", reimbursementId] });
setInvoiceActionError("发票仍在 OCR 处理中,请稍后刷新列表;若长时间未完成,可前往票据中心查看详情。");
toast("发票仍在 OCR 处理中,请稍后手动刷新候选列表");
},
onError: (error) => {
@@ -1002,7 +1026,7 @@ export function ReimbursementDetailPage() {
) : null}
</div>
<div className="space-y-4 text-sm" data-testid="reimbursement-invoices">
<div className="space-y-4 text-sm" data-testid={TEST_IDS.reimbursement.invoices}>
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="space-y-1">
<h3 className="text-base font-semibold text-zinc-900">关联票据</h3>
@@ -1010,6 +1034,7 @@ export function ReimbursementDetailPage() {
</div>
{canOpenInvoiceBindDialog ? (
<Button
data-testid={TEST_IDS.reimbursement.invoiceAdd}
type="button"
variant="secondary"
onClick={() => handleInvoiceDialogOpenChange(true)}
@@ -1022,7 +1047,10 @@ export function ReimbursementDetailPage() {
</div>
{invoiceActionError ? (
<p className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
<p
className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600"
data-testid={TEST_IDS.reimbursement.invoiceActionError}
>
{invoiceActionError}
</p>
) : null}
@@ -1231,7 +1259,10 @@ export function ReimbursementDetailPage() {
</div>
<Dialog open={isInvoiceDialogOpen} onOpenChange={handleInvoiceDialogOpenChange}>
<DialogContent className="max-h-[85vh] max-w-3xl overflow-y-auto">
<DialogContent
className="max-h-[85vh] max-w-3xl overflow-y-auto"
data-testid={TEST_IDS.reimbursement.invoiceDialog}
>
<DialogHeader>
<DialogTitle>添加发票</DialogTitle>
<DialogDescription>选择当前报销人可绑定的可用发票,或上传新发票等待 OCR 完成后直接绑定。</DialogDescription>
@@ -1350,11 +1381,13 @@ export function ReimbursementDetailPage() {
<input
id="reimbursement-inline-invoice-upload"
className="hidden"
data-testid={TEST_IDS.reimbursement.invoiceUploadFile}
type="file"
accept="application/pdf,image/jpeg,image/png,image/webp"
onChange={(event) => setSelectedInvoiceFile(event.target.files?.[0] ?? null)}
/>
<Button
data-testid={TEST_IDS.reimbursement.invoiceUploadSubmit}
type="button"
disabled={!selectedInvoiceFile || uploadInvoiceMutation.isPending}
onClick={() => selectedInvoiceFile && uploadInvoiceMutation.mutate(selectedInvoiceFile)}
@@ -1364,7 +1397,7 @@ export function ReimbursementDetailPage() {
</div>
{pendingOcrInvoiceId ? (
<p className="mt-3 text-xs text-zinc-500">
<p className="mt-3 text-xs text-zinc-500" data-testid={TEST_IDS.reimbursement.invoicePendingHint}>
发票 {pendingOcrInvoiceId.slice(0, 8)} 正在 OCR 识别中,系统会在可绑定后自动刷新列表。
</p>
) : null}