RI6 完善报销票据体验与状态文案

这个提交包含在:
2026-05-12 12:02:50 +08:00
父节点 9e37528293
当前提交 21887ce28a
共修改 8 个文件,包含 278 行新增和 129 行删除
@@ -45,6 +45,12 @@ import {
import { Input } from "../components/ui/input";
import { Textarea } from "../components/ui/textarea";
import { buildPrecheckDialogMessage } from "../lib/precheck";
import {
formatInvoiceDisplayTitle,
resolveInvoiceNextStepText,
resolveInvoiceStatusLabel,
resolveInvoiceUserMessage,
} from "../lib/invoiceStatus";
import { resolveErrorMessage } from "../lib/resolveErrorMessage";
import { resolveReimbursementDisplayCode } from "../lib/reimbursementDisplay";
import { TEST_IDS } from "../lib/testIds";
@@ -86,14 +92,6 @@ const STATUS_TONE: Record<ReimbursementStatus, string> = {
cancelled: "bg-zinc-100 text-zinc-500",
};
const INVOICE_STATUS_LABEL: Record<string, string> = {
pending_ocr: "识别中",
ocr_failed: "识别失败",
available: "可用",
bound: "已添加",
archived: "已归档",
};
function todayDate() {
return new Date().toISOString().slice(0, 10);
}
@@ -134,18 +132,6 @@ function workflowActionLabel(action: string) {
return labels[action] ?? action;
}
function formatInvoiceTitle(invoice: {
invoiceCode?: string | null;
invoiceNumber?: string | null;
sellerName?: string | null;
originalName?: string | null;
}) {
if (invoice.invoiceCode || invoice.invoiceNumber) {
return `${invoice.invoiceCode || "-"}-${invoice.invoiceNumber || "-"}`;
}
return invoice.sellerName || invoice.originalName || "未识别票据";
}
function matchInvoiceSearch(
invoice: {
invoiceCode?: string | null;
@@ -282,7 +268,7 @@ export function MobileReimbursementDetailPage() {
originalName: item.originalName,
createdAt: item.createdAt,
status: "ocr_failed",
unavailableMessage: "识别失败,可删除后重新上传。",
unavailableMessage: resolveInvoiceUserMessage(item),
})),
[failedInvoicesQuery.data?.items, invoiceSearchLower],
);
@@ -463,7 +449,12 @@ export function MobileReimbursementDetailPage() {
return;
}
const message = failed.map((item) => `${item.invoiceId.slice(0, 8)}:${item.message ?? "添加失败"}`).join(";");
const message = failed
.map((item) => {
const invoice = invoiceLibraryItems.find((candidate) => candidate.id === item.invoiceId);
return `${formatInvoiceDisplayTitle(invoice)}:${item.message ?? "添加失败"}`;
})
.join(";");
setInvoiceActionError(message);
toast.error("部分发票添加失败");
},
@@ -486,7 +477,7 @@ export function MobileReimbursementDetailPage() {
const handleTrackedInvoicesUnavailable = useCallback((items: Array<{ id: string; originalName: string | null; message: string; availabilityMessage?: string | null }>) => {
const message = items
.map((item) => `${item.originalName ?? "未命名发票"}:${item.availabilityMessage ?? item.message}`)
.map((item) => `${formatInvoiceDisplayTitle(item)}:${resolveInvoiceUserMessage(item)}`)
.join(";");
setInvoiceActionError(message);
setInvoiceActionMessage("");
@@ -565,7 +556,7 @@ export function MobileReimbursementDetailPage() {
if (uploadResult.uploadAction === "blocked" || !uploadResult.invoicePoolId) {
failures.push({
file,
message: uploadResult.unavailableMessage ?? "该发票不可添加",
message: resolveInvoiceUserMessage(uploadResult),
});
} else if (uploadResult.status === "available") {
try {
@@ -577,9 +568,9 @@ export function MobileReimbursementDetailPage() {
} else if (uploadResult.status === "pending_ocr") {
pendingIds.push(uploadResult.invoicePoolId);
} else if (uploadResult.status === "ocr_failed") {
failures.push({ file, message: uploadResult.unavailableMessage ?? "发票识别失败,请重新识别或手动处理" });
failures.push({ file, message: resolveInvoiceUserMessage(uploadResult) });
} else {
failures.push({ file, message: uploadResult.unavailableMessage ?? "该发票不可添加" });
failures.push({ file, message: resolveInvoiceUserMessage(uploadResult) });
}
} catch (error) {
failures.push({ file, message: resolveErrorMessage(error) });
@@ -1013,37 +1004,41 @@ export function MobileReimbursementDetailPage() {
{detail.invoices.length ? (
<div className="mt-3 space-y-2">
{detail.invoices.map((invoice) => (
<article key={invoice.id} className="mobile-list-card">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceTitle(invoice)}</div>
<div className="mt-1 text-xs text-zinc-500">
{invoice.sellerName || "未识别销售方"} · {formatDate(invoice.invoiceDate)}
{detail.invoices.map((invoice) => {
const invoiceHint = resolveInvoiceNextStepText(invoice);
return (
<article key={invoice.id} className="mobile-list-card">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceDisplayTitle(invoice)}</div>
<div className="mt-1 text-xs text-zinc-500">
{invoice.sellerName || "未识别销售方"} · {formatDate(invoice.invoiceDate)}
</div>
<div className="mt-1 text-xs text-zinc-500">明细:{resolveLinkedItemLabel(invoice.linkedItemId)}</div>
{invoiceHint ? <div className="mt-1 text-xs text-zinc-500">{invoiceHint}</div> : null}
</div>
<div className="shrink-0 text-right">
<div className="text-sm font-semibold text-zinc-900">{formatCurrency(invoice.totalAmount)}</div>
<span className="mt-1 inline-flex rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
{resolveInvoiceStatusLabel(invoice.status)}
</span>
</div>
<div className="mt-1 text-xs text-zinc-500">明细:{resolveLinkedItemLabel(invoice.linkedItemId)}</div>
</div>
<div className="shrink-0 text-right">
<div className="text-sm font-semibold text-zinc-900">{formatCurrency(invoice.totalAmount)}</div>
<span className="mt-1 inline-flex rounded-full bg-zinc-100 px-2 py-1 text-xs text-zinc-600">
{INVOICE_STATUS_LABEL[invoice.status] ?? "未知状态"}
</span>
</div>
</div>
{canUnbindInvoices ? (
<Button
className="mt-3 w-full"
size="sm"
variant="secondary"
type="button"
disabled={unbindInvoiceMutation.isPending}
onClick={() => unbindInvoiceMutation.mutate(invoice.id)}
>
移除
</Button>
) : null}
</article>
))}
{canUnbindInvoices ? (
<Button
className="mt-3 w-full"
size="sm"
variant="secondary"
type="button"
disabled={unbindInvoiceMutation.isPending}
onClick={() => unbindInvoiceMutation.mutate(invoice.id)}
>
移除
</Button>
) : null}
</article>
);
})}
</div>
) : (
<div className="mt-3 rounded-lg border border-dashed border-zinc-200 p-5 text-center text-sm text-zinc-500">
@@ -1356,11 +1351,18 @@ export function MobileReimbursementDetailPage() {
/>
) : (
<span className="mt-0.5 inline-flex shrink-0 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-700">
识别失败
{resolveInvoiceStatusLabel(invoice.status)}
</span>
)}
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceTitle(invoice)}</div>
<div className="flex items-start justify-between gap-2">
<div className="truncate text-sm font-semibold text-zinc-900">{formatInvoiceDisplayTitle(invoice)}</div>
{isAvailable ? (
<span className="shrink-0 rounded-full bg-emerald-50 px-2 py-0.5 text-xs font-medium text-emerald-700">
{resolveInvoiceStatusLabel(invoice.status)}
</span>
) : null}
</div>
<div className="mt-1 text-xs text-zinc-500">
{invoice.sellerName || "未识别销售方"} · {formatDate(invoice.invoiceDate)} · {formatCurrency(invoice.totalAmount)}
</div>
@@ -1368,7 +1370,7 @@ export function MobileReimbursementDetailPage() {
</div>
{isFailed ? (
<div className="mt-3 rounded-md bg-red-50 px-3 py-2 text-xs text-red-700">
{invoice.unavailableMessage ?? "识别失败,可删除后重新上传。"}
{resolveInvoiceUserMessage(invoice)}
</div>
) : null}
{isAvailable && checked ? (
@@ -1474,7 +1476,7 @@ export function MobileReimbursementDetailPage() {
open={confirmDeleteReimbursement}
onOpenChange={setConfirmDeleteReimbursement}
title="删除草稿?"
description="删除后无法恢复,已添加发票会回到可用状态。"
description="删除后无法恢复,已添加发票会回到可添加状态。"
variant="destructive"
confirmLabel="删除草稿"
onConfirm={() => deleteReimbursementMutation.mutate()}