R107/A1-A6 修复网页视觉巡检问题
这个提交包含在:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户