R107/A1-A6 修复网页视觉巡检问题

这个提交包含在:
2026-05-07 20:05:48 +08:00
父节点 78b07ad895
当前提交 230d8f53db
共修改 8 个文件,包含 77 行新增和 101 行删除
+4 -3
查看文件
@@ -1034,7 +1034,7 @@ export function ReimbursementDetailPage() {
<TableHead>发生日期</TableHead>
<TableHead>金额</TableHead>
<TableHead>说明</TableHead>
<TableHead className="w-32">操作</TableHead>
<TableHead className="w-44">操作</TableHead>
</tr>
</TableHeader>
<TableBody>
@@ -1087,9 +1087,10 @@ export function ReimbursementDetailPage() {
</TableCell>
<TableCell>
{canEditItems ? (
<div className="flex flex-wrap gap-2">
<div className="flex min-w-36 items-center gap-2">
<Button
data-testid="reimbursement-item-edit"
className="min-w-16"
size="sm"
type="button"
variant="secondary"
@@ -1099,6 +1100,7 @@ export function ReimbursementDetailPage() {
</Button>
<Button
data-testid="reimbursement-item-delete"
className="min-w-16"
size="sm"
type="button"
variant="secondary"
@@ -1142,7 +1144,6 @@ export function ReimbursementDetailPage() {
<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>
<p className="text-xs text-zinc-500">可在当前页面内直接绑定已有发票,或上传新发票后等待 OCR 完成。</p>
</div>
{canOpenInvoiceBindDialog ? (
<Button
+1 -1
查看文件
@@ -1072,7 +1072,7 @@ function TravelRequestForm(props: TravelRequestFormProps) {
</Button>
) : null}
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label>角色</Label>
<select
@@ -45,6 +45,18 @@ export type InvoiceDetailPanelProps = {
setConfirmAction: (action: { type: string; invoiceId: string } | null) => void;
};
function invoiceStatusLabel(status: string) {
const labels: Record<string, string> = {
pending_ocr: "识别中",
available: "可用",
bound: "已绑定",
archived: "已归档",
ocr_failed: "识别失败",
duplicate_suspected: "疑似重复",
};
return labels[status] ?? status;
}
export function InvoiceDetailPanel({
detail,
canEditDetail,
@@ -84,6 +96,17 @@ export function InvoiceDetailPanel({
confirmAction,
setConfirmAction,
}: InvoiceDetailPanelProps) {
const invoiceNo = detail
? [detail.invoiceCode, detail.invoiceNumber].filter(Boolean).join("-") || detail.originalName || "待识别"
: "";
const bindingStatus = detail?.linkedReimbursementId
? "已绑定报销单"
: detail?.linkedTravelOrderId
? "已绑定差旅订单"
: detail?.linkedItemId
? "已绑定明细"
: "未绑定";
return (
<>
<div className="card-std space-y-4 p-4 md:p-5" data-testid="invoice-detail-panel">
@@ -91,12 +114,12 @@ export function InvoiceDetailPanel({
{detail ? (
<>
<div className="grid grid-cols-2 gap-y-3 pt-2 text-sm text-zinc-700">
<div><span className="text-zinc-500">票据 ID:</span>{detail.id}</div>
<div><span className="text-zinc-500">状态:</span>{detail.status}</div>
<div className="col-span-2">
<span className="text-zinc-500">发票号:</span>
{detail.invoiceCode || "-"}-{detail.invoiceNumber || "-"}
<span className="text-zinc-500">发票号码:</span>
{invoiceNo}
</div>
<div><span className="text-zinc-500">状态:</span>{invoiceStatusLabel(detail.status)}</div>
<div><span className="text-zinc-500">绑定状态:</span>{bindingStatus}</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">
@@ -104,7 +127,7 @@ export function InvoiceDetailPanel({
{detail.totalAmount ? new Intl.NumberFormat("zh-CN", { style: "currency", currency: "CNY" }).format(Number(detail.totalAmount)) : "-"}
</div>
<div><span className="text-zinc-500">开票日期:</span>{detail.invoiceDate || "-"}</div>
<div><span className="text-zinc-500">关联报销:</span>{detail.linkedReimbursementId || "-"}</div>
<div><span className="text-zinc-500">文件名:</span>{detail.originalName || "-"}</div>
<div className="col-span-2 flex flex-col gap-1">
<div>
<span className="text-zinc-500">验真状态:</span>
@@ -144,7 +167,7 @@ export function InvoiceDetailPanel({
{canVerifyInvoice ? (
<Button
data-testid="invoice-trigger-verification"
className="btn-secondary"
variant="secondary"
type="button"
disabled={triggerVerificationMutation.isPending || detail.status === "pending_ocr"}
onClick={() => triggerVerificationMutation.mutate(detail.id)}
@@ -158,7 +181,7 @@ export function InvoiceDetailPanel({
["failed", "error"].includes(detail.verification.status) ? (
<Button
data-testid="invoice-retry-verification"
className="btn-secondary"
variant="secondary"
type="button"
disabled={retryVerificationMutation.isPending}
onClick={() => retryVerificationMutation.mutate(detail.id)}
@@ -170,7 +193,7 @@ export function InvoiceDetailPanel({
{canEditDetail ? (
<Button
data-testid="invoice-edit-open"
className="btn-secondary"
variant="secondary"
type="button"
disabled={patchMutation.isPending}
onClick={() => setEditOpen(!editOpen)}
@@ -182,7 +205,7 @@ export function InvoiceDetailPanel({
{canEditDetail && detail.status === "ocr_failed" ? (
<Button
data-testid="invoice-retry-ocr"
className="btn-secondary"
variant="secondary"
type="button"
disabled={retryOcrMutation.isPending}
onClick={() => setConfirmAction({ type: "retryOcr", invoiceId: detail.id })}
@@ -194,7 +217,7 @@ export function InvoiceDetailPanel({
{canEditDetail && canUploadInvoice && detail.status === "bound" ? (
<Button
data-testid="invoice-unbind"
className="btn-secondary"
variant="secondary"
type="button"
disabled={unbindMutation.isPending}
onClick={() => setConfirmAction({ type: "unbind", invoiceId: detail.id })}
@@ -206,7 +229,7 @@ export function InvoiceDetailPanel({
{canEditDetail && detail.status !== "bound" && detail.status !== "archived" ? (
<Button
data-testid="invoice-archive"
className="btn-secondary text-red-600 hover:text-red-700 hover:bg-red-50"
variant="destructive"
type="button"
disabled={archiveMutation.isPending}
onClick={() => setConfirmAction({ type: "archive", invoiceId: detail.id })}
@@ -220,7 +243,7 @@ export function InvoiceDetailPanel({
["available", "ocr_failed"].includes(detail.status) ? (
<Button
data-testid="invoice-override-duplicate"
className="btn-secondary"
variant="secondary"
type="button"
disabled={overrideDuplicateMutation.isPending}
onClick={() => {
@@ -246,10 +269,10 @@ export function InvoiceDetailPanel({
<p className="mb-3 text-sm font-semibold text-zinc-900 border-b border-zinc-200/60 pb-2">验真记录</p>
{(verificationQuery.data?.items ?? []).length ? (
<div className="space-y-3 text-xs">
{(verificationQuery.data?.items ?? []).map((record) => (
{(verificationQuery.data?.items ?? []).map((record, index) => (
<div key={record.id} className="rounded-md border border-zinc-200 bg-white px-3 py-2 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-2 mb-1">
<span className="font-mono text-zinc-500">{record.id}</span>
<span className="font-medium text-zinc-700">记录 {index + 1}</span>
<span className={`font-semibold ${record.status === "failed" || record.status === "error" ? "text-red-600" : "text-emerald-600"}`}>
{record.status}
</span>
@@ -403,7 +426,6 @@ export function InvoiceDetailPanel({
<div className="flex flex-wrap gap-2">
<Button
data-testid="invoice-edit-save"
className="btn-primary"
disabled={patchMutation.isPending}
onClick={() => patchMutation.mutate()}
type="button"
@@ -412,7 +434,7 @@ export function InvoiceDetailPanel({
</Button>
<Button
data-testid="invoice-edit-cancel"
className="btn-secondary"
variant="secondary"
disabled={patchMutation.isPending}
onClick={() => setEditOpen(false)}
type="button"
@@ -21,6 +21,11 @@ export type InvoiceListCardProps = {
signedUrlMutation: UseMutationResult<{ signedUrl: string; expiresAt: string }, Error, string>;
};
function invoiceDisplayName(item: InvoicePoolListResponse["items"][number]) {
const invoiceNo = [item.invoiceCode, item.invoiceNumber].filter(Boolean).join("-");
return item.originalName || invoiceNo || "待识别票据";
}
export function InvoiceListCard({
listQuery,
status,
@@ -79,8 +84,8 @@ export function InvoiceListCard({
<article className="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm" data-testid="invoice-mobile-row" key={item.id}>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-900">{item.originalName || item.id}</div>
<div className="mt-1 font-mono text-xs text-zinc-500">{item.id.slice(0, 8)}</div>
<div className="truncate text-sm font-semibold text-zinc-900">{invoiceDisplayName(item)}</div>
<div className="mt-1 text-xs text-zinc-500">{item.invoiceDate || "待识别"}</div>
</div>
<StatusBadge className="shrink-0" status={item.status} />
</div>
@@ -141,7 +146,7 @@ export function InvoiceListCard({
<table className="w-full text-left text-sm text-zinc-600" data-testid="invoice-pool-table">
<thead className="bg-zinc-50/50">
<tr>
<th className="table-th w-[280px]">票据流水号</th>
<th className="table-th w-[280px]">票据名称</th>
<th className="table-th w-[120px]">状态</th>
<th className="table-th w-[200px]">发票号码</th>
<th className="table-th">识别状态</th>
@@ -153,7 +158,8 @@ export function InvoiceListCard({
listQuery.data?.items.map((item) => (
<tr className="table-row" data-testid="invoice-row" key={item.id}>
<td className="table-td">
<div className="font-mono text-xs text-zinc-500">{item.id}</div>
<div className="max-w-[260px] truncate font-medium text-zinc-900">{invoiceDisplayName(item)}</div>
<div className="mt-1 text-xs text-zinc-500">{item.invoiceDate || "待识别"}</div>
</td>
<td className="table-td"><StatusBadge status={item.status} /></td>
<td className="table-td font-medium text-zinc-900">
@@ -566,6 +566,10 @@ test.describe.serial("R106 自动视觉巡检", () => {
await adminMobileContext.close();
writeReport(seedData);
if (process.env.R106_VISUAL_FAIL_ON_FINDINGS === "1") {
const summary = findings.map((finding) => `${finding.severity} ${finding.page} / ${finding.category}: ${finding.title}`).join("\n");
expect(findings, summary || "R106 visual findings should be empty").toEqual([]);
}
await expect.poll(() => findings.length, { message: "R106 report generated" }).toBeGreaterThanOrEqual(0);
});
});