R73-A3 优化轻交互 OCR 前端处理
这个提交包含在:
@@ -1,3 +1,5 @@
|
||||
import { formatInvoiceOcrFieldLabels } from "../lib/invoiceOcr";
|
||||
|
||||
function readStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
@@ -189,6 +191,14 @@ export function mapApiErrorMessage(
|
||||
return "当前环境未开启 OCR,请联系管理员或切换到 mock/tencent。";
|
||||
}
|
||||
|
||||
if (details?.ocr === "missing_required_fields") {
|
||||
const labels = formatInvoiceOcrFieldLabels(readStringList(details?.missingFields));
|
||||
if (labels.length) {
|
||||
return `票据仍缺少关键字段:${labels.join("、")},补齐后才能标记为可用。`;
|
||||
}
|
||||
return "票据仍缺少关键字段,补齐后才能标记为可用。";
|
||||
}
|
||||
|
||||
|
||||
if (details?.verification === "disabled") {
|
||||
return "当前环境未开启发票验真。";
|
||||
|
||||
@@ -30,7 +30,7 @@ export const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-lg -tranzinc-x-1/2 -tranzinc-y-1/2 rounded-xl border border-zinc-200 bg-white p-6 shadow-lg focus-visible:outline-none",
|
||||
"fixed left-1/2 top-1/2 z-50 w-[92vw] max-w-lg -translate-x-1/2 -translate-y-1/2 rounded-xl border border-zinc-200 bg-white p-6 shadow-lg focus-visible:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -73,4 +73,3 @@ export const DialogDescription = React.forwardRef<
|
||||
));
|
||||
|
||||
DialogDescription.displayName = "DialogDescription";
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const OCR_FIELD_LABELS: Record<string, string> = {
|
||||
invoiceCode: "发票代码",
|
||||
invoiceNumber: "发票号码",
|
||||
invoiceDate: "开票日期",
|
||||
totalAmount: "总金额",
|
||||
sellerName: "销售方",
|
||||
};
|
||||
|
||||
export function parseMissingInvoiceOcrFields(rawError: string | null | undefined) {
|
||||
if (typeof rawError !== "string") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matched = rawError.match(/^missing_required_fields:(.+)$/);
|
||||
if (!matched?.[1]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return matched[1]
|
||||
.split(",")
|
||||
.map((field) => field.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function formatInvoiceOcrFieldLabels(fields: string[]) {
|
||||
return fields.map((field) => OCR_FIELD_LABELS[field] ?? field);
|
||||
}
|
||||
|
||||
export function formatInvoiceOcrErrorMessage(rawError: string | null | undefined) {
|
||||
const missingFields = parseMissingInvoiceOcrFields(rawError);
|
||||
if (missingFields.length) {
|
||||
return `OCR 缺少关键字段:${formatInvoiceOcrFieldLabels(missingFields).join("、")}。`;
|
||||
}
|
||||
|
||||
if (rawError === "duplicate_invoice_code_number") {
|
||||
return "OCR 识别到的发票代码/号码与已使用票据重复,请核对后再处理。";
|
||||
}
|
||||
|
||||
if (rawError === "mock_ocr_failed") {
|
||||
return "OCR 识别失败,请重试或人工补齐关键信息。";
|
||||
}
|
||||
|
||||
if (!rawError) {
|
||||
return "OCR 识别失败,请稍后重试。";
|
||||
}
|
||||
|
||||
return rawError;
|
||||
}
|
||||
@@ -41,6 +41,13 @@ export const TEST_IDS = {
|
||||
erpExport: "reimbursement-erp-export",
|
||||
overview: "reimbursement-overview",
|
||||
workflowProgress: "reimbursement-workflow-progress",
|
||||
invoices: "reimbursement-invoices",
|
||||
invoiceAdd: "reimbursement-invoice-add",
|
||||
invoiceDialog: "reimbursement-invoice-dialog",
|
||||
invoiceUploadFile: "reimbursement-invoice-upload-file",
|
||||
invoiceUploadSubmit: "reimbursement-invoice-upload-submit",
|
||||
invoicePendingHint: "reimbursement-invoice-pending-hint",
|
||||
invoiceActionError: "reimbursement-invoice-action-error",
|
||||
},
|
||||
weeklyReport: {
|
||||
save: "weekly-report-save",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { isApiError } from "../api/types";
|
||||
import { mapApiErrorMessage } from "../api/errorMessages";
|
||||
@@ -26,6 +26,7 @@ import { useAuth } from "../auth/AuthContext";
|
||||
import { hasPermission } from "../auth/permissions";
|
||||
import { toast } from "sonner";
|
||||
import { downloadBlob } from "../lib/download";
|
||||
import { formatInvoiceOcrErrorMessage } from "../lib/invoiceOcr";
|
||||
import { PageHeader } from "../components/common/PageHeader";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../components/ui/dropdown-menu";
|
||||
@@ -64,9 +65,14 @@ export function InvoicePoolPage() {
|
||||
const [editInvoiceCode, setEditInvoiceCode] = useState("");
|
||||
const [editInvoiceNumber, setEditInvoiceNumber] = useState("");
|
||||
const [editInvoiceDate, setEditInvoiceDate] = useState("");
|
||||
const [editSellerName, setEditSellerName] = useState("");
|
||||
const [editBuyerName, setEditBuyerName] = useState("");
|
||||
const [editAmount, setEditAmount] = useState("");
|
||||
const [editTaxAmount, setEditTaxAmount] = useState("");
|
||||
const [editTotalAmount, setEditTotalAmount] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [confirmAction, setConfirmAction] = useState<{ type: string; invoiceId: string } | null>(null);
|
||||
const previousDetailStatusRef = useRef<InvoicePoolStatus | null>(null);
|
||||
|
||||
// --- Queries ---
|
||||
|
||||
@@ -140,9 +146,29 @@ export function InvoicePoolPage() {
|
||||
setEditInvoiceCode(detailQuery.data.invoiceCode ?? "");
|
||||
setEditInvoiceNumber(detailQuery.data.invoiceNumber ?? "");
|
||||
setEditInvoiceDate(detailQuery.data.invoiceDate ?? "");
|
||||
setEditSellerName(detailQuery.data.sellerName ?? "");
|
||||
setEditBuyerName(detailQuery.data.buyerName ?? "");
|
||||
setEditAmount(detailQuery.data.amount ?? "");
|
||||
setEditTaxAmount(detailQuery.data.taxAmount ?? "");
|
||||
setEditTotalAmount(detailQuery.data.totalAmount ?? "");
|
||||
}, [detailQuery.data, editOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentStatus = detailQuery.data?.status ?? null;
|
||||
const previousStatus = previousDetailStatusRef.current;
|
||||
|
||||
if (previousStatus === "pending_ocr" && currentStatus && currentStatus !== "pending_ocr") {
|
||||
void queryClient.invalidateQueries({ queryKey: ["invoice-pool"] });
|
||||
if (currentStatus === "available") {
|
||||
toast.success("票据 OCR 已完成,可继续绑定或验真");
|
||||
} else if (currentStatus === "ocr_failed") {
|
||||
toast.error(`票据 OCR 未完成:${formatInvoiceOcrErrorMessage(detailQuery.data?.ocrLastError)}`);
|
||||
}
|
||||
}
|
||||
|
||||
previousDetailStatusRef.current = currentStatus;
|
||||
}, [detailQuery.data?.ocrLastError, detailQuery.data?.status, queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canManageInvoice && ownerScope === "all") {
|
||||
setOwnerScope("own");
|
||||
@@ -188,7 +214,7 @@ export function InvoicePoolPage() {
|
||||
setDetailInvoiceId(data.invoicePoolId);
|
||||
setSelectedFile(null);
|
||||
setError("");
|
||||
toast.success("上传成功");
|
||||
toast.success(data.status === "pending_ocr" ? "上传成功,OCR 识别中" : "上传成功,可直接处理");
|
||||
},
|
||||
onError: (err) => handleError(err, "上传失败,请稍后重试"),
|
||||
});
|
||||
@@ -246,6 +272,10 @@ export function InvoicePoolPage() {
|
||||
invoiceCode: editInvoiceCode.trim() ? editInvoiceCode.trim() : null,
|
||||
invoiceNumber: editInvoiceNumber.trim() ? editInvoiceNumber.trim() : null,
|
||||
invoiceDate: editInvoiceDate ? editInvoiceDate : null,
|
||||
sellerName: editSellerName.trim() ? editSellerName.trim() : null,
|
||||
buyerName: editBuyerName.trim() ? editBuyerName.trim() : null,
|
||||
amount: editAmount.trim() ? editAmount.trim() : null,
|
||||
taxAmount: editTaxAmount.trim() ? editTaxAmount.trim() : null,
|
||||
totalAmount: editTotalAmount.trim() ? editTotalAmount.trim() : null,
|
||||
});
|
||||
},
|
||||
@@ -461,6 +491,14 @@ export function InvoicePoolPage() {
|
||||
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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { UseMutationResult, UseQueryResult } from "@tanstack/react-query";
|
||||
import type { InvoicePoolDetail, InvoiceVerificationRecord } from "../../api/invoicePool";
|
||||
import { ConfirmDialog } from "../../components/common/ConfirmDialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
|
||||
|
||||
export type InvoiceDetailPanelProps = {
|
||||
detail: InvoicePoolDetail | undefined;
|
||||
@@ -30,6 +31,14 @@ export type InvoiceDetailPanelProps = {
|
||||
setEditInvoiceNumber: (value: string) => void;
|
||||
editInvoiceDate: string;
|
||||
setEditInvoiceDate: (value: string) => void;
|
||||
editSellerName: string;
|
||||
setEditSellerName: (value: string) => void;
|
||||
editBuyerName: string;
|
||||
setEditBuyerName: (value: string) => void;
|
||||
editAmount: string;
|
||||
setEditAmount: (value: string) => void;
|
||||
editTaxAmount: string;
|
||||
setEditTaxAmount: (value: string) => void;
|
||||
editTotalAmount: string;
|
||||
setEditTotalAmount: (value: string) => void;
|
||||
confirmAction: { type: string; invoiceId: string } | null;
|
||||
@@ -62,6 +71,14 @@ export function InvoiceDetailPanel({
|
||||
setEditInvoiceNumber,
|
||||
editInvoiceDate,
|
||||
setEditInvoiceDate,
|
||||
editSellerName,
|
||||
setEditSellerName,
|
||||
editBuyerName,
|
||||
setEditBuyerName,
|
||||
editAmount,
|
||||
setEditAmount,
|
||||
editTaxAmount,
|
||||
setEditTaxAmount,
|
||||
editTotalAmount,
|
||||
setEditTotalAmount,
|
||||
confirmAction,
|
||||
@@ -80,6 +97,8 @@ export function InvoiceDetailPanel({
|
||||
<span className="text-zinc-500">发票号:</span>
|
||||
{detail.invoiceCode || "-"}-{detail.invoiceNumber || "-"}
|
||||
</div>
|
||||
<div><span className="text-zinc-500">销售方:</span>{detail.sellerName || "-"}</div>
|
||||
<div><span className="text-zinc-500">购买方:</span>{detail.buyerName || "-"}</div>
|
||||
<div className="col-span-2">
|
||||
<span className="text-zinc-500">总金额:</span>
|
||||
{detail.totalAmount ? new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY" }).format(Number(detail.totalAmount)) : "-"}
|
||||
@@ -101,7 +120,7 @@ export function InvoiceDetailPanel({
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{detail.ocrLastError ? <div className="col-span-2 text-red-600"><span className="text-zinc-500">OCR错误:</span>{detail.ocrLastError}</div> : null}
|
||||
{detail.ocrLastError ? <div className="col-span-2 text-red-600"><span className="text-zinc-500">OCR错误:</span>{formatInvoiceOcrErrorMessage(detail.ocrLastError)}</div> : null}
|
||||
{detail.remarks ? <div className="col-span-2"><span className="text-zinc-500">备注:</span>{detail.remarks}</div> : null}
|
||||
<div className="col-span-2">
|
||||
<span className="text-zinc-500">判重例外:</span>
|
||||
@@ -109,6 +128,18 @@ export function InvoiceDetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{detail.status === "pending_ocr" ? (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700" data-testid="invoice-ocr-status-banner">
|
||||
OCR 正在处理中,当前页面会自动轮询;完成后可直接继续绑定、验真或查看详情。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{detail.status === "ocr_failed" ? (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" data-testid="invoice-ocr-status-banner">
|
||||
{formatInvoiceOcrErrorMessage(detail.ocrLastError)} 可在下方直接重试 OCR,或补齐关键字段后标记为可用。
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap gap-2 pt-4 border-t border-zinc-100 mt-4">
|
||||
{canVerifyInvoice ? (
|
||||
<Button
|
||||
@@ -294,6 +325,54 @@ export function InvoiceDetailPanel({
|
||||
value={editInvoiceDate}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-zinc-600" htmlFor="invoiceSellerName">
|
||||
销售方
|
||||
</label>
|
||||
<input
|
||||
className="input-std"
|
||||
data-testid="invoice-edit-seller-name"
|
||||
id="invoiceSellerName"
|
||||
onChange={(event) => setEditSellerName(event.target.value)}
|
||||
value={editSellerName}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-zinc-600" htmlFor="invoiceBuyerName">
|
||||
购买方
|
||||
</label>
|
||||
<input
|
||||
className="input-std"
|
||||
data-testid="invoice-edit-buyer-name"
|
||||
id="invoiceBuyerName"
|
||||
onChange={(event) => setEditBuyerName(event.target.value)}
|
||||
value={editBuyerName}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-zinc-600" htmlFor="invoiceAmount">
|
||||
不含税金额
|
||||
</label>
|
||||
<input
|
||||
className="input-std"
|
||||
data-testid="invoice-edit-amount"
|
||||
id="invoiceAmount"
|
||||
onChange={(event) => setEditAmount(event.target.value)}
|
||||
value={editAmount}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-zinc-600" htmlFor="invoiceTaxAmount">
|
||||
税额
|
||||
</label>
|
||||
<input
|
||||
className="input-std"
|
||||
data-testid="invoice-edit-tax-amount"
|
||||
id="invoiceTaxAmount"
|
||||
onChange={(event) => setEditTaxAmount(event.target.value)}
|
||||
value={editTaxAmount}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-medium text-zinc-600" htmlFor="invoiceTotalAmount">
|
||||
总金额
|
||||
|
||||
@@ -4,6 +4,7 @@ import { StatusBadge } from "../../components/common/StatusBadge";
|
||||
import { TableSkeleton } from "../../components/common/TableSkeleton";
|
||||
import { Pagination } from "../../components/common/Pagination";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../../components/ui/dropdown-menu";
|
||||
import { formatInvoiceOcrErrorMessage } from "../../lib/invoiceOcr";
|
||||
|
||||
export type InvoiceListCardProps = {
|
||||
listQuery: UseQueryResult<InvoicePoolListResponse, Error>;
|
||||
@@ -102,7 +103,7 @@ export function InvoiceListCard({
|
||||
) : item.status === "ocr_failed" ? (
|
||||
<span className="flex items-center gap-1.5 text-red-600">
|
||||
<span className="size-1.5 rounded-full bg-red-500" />
|
||||
识别失败 <span className="text-zinc-400 text-xs">({item.ocrLastError || "未知错误"})</span>
|
||||
识别失败 <span className="text-zinc-400 text-xs">({formatInvoiceOcrErrorMessage(item.ocrLastError)})</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-zinc-400 text-xs">解析成功</span>
|
||||
|
||||
在新工单中引用
屏蔽一个用户