修复报销票据门禁提示与 OCR 降级(08-30-b2-submit-error-ocr-fallback)

这个提交包含在:
2026-08-31 20:20:20 +08:00
父节点 8b19538584
当前提交 f57e2236f2
共修改 21 个文件,包含 1829 行新增和 49 行删除
+148
查看文件
@@ -0,0 +1,148 @@
import { isApiError } from "../api/types";
export type InvoiceGuardAmountUnavailableItem = {
itemId: string;
invoiceIds: string[];
};
export type InvoiceGuardAmountExceededItem = {
itemId: string;
declaredAmount: string;
invoiceTotal: string;
};
export type ReimbursementInvoiceGuardIssues = {
unlinkedInvoiceCount: number;
unlinkedInvoiceIds: string[];
missingInvoiceItemIds: string[];
amountUnavailableItems: InvoiceGuardAmountUnavailableItem[];
amountExceededItems: InvoiceGuardAmountExceededItem[];
};
function readStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.filter((item): item is string => typeof item === "string" && item.length > 0)
: [];
}
function readNonNegativeInteger(value: unknown): number {
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
}
export function parseReimbursementInvoiceGuardIssues(
error: unknown,
): ReimbursementInvoiceGuardIssues | null {
if (!isApiError(error) || error.details?.items !== "invoice_item_guard_failed") {
return null;
}
const amountUnavailableItems = Array.isArray(error.details.amountUnavailableItems)
? error.details.amountUnavailableItems.flatMap((value) => {
if (!value || typeof value !== "object") return [];
const itemId = "itemId" in value && typeof value.itemId === "string" ? value.itemId : "";
if (!itemId) return [];
return [{
itemId,
invoiceIds: readStringArray("invoiceIds" in value ? value.invoiceIds : undefined),
}];
})
: [];
const amountExceededItems = Array.isArray(error.details.amountExceededItems)
? error.details.amountExceededItems.flatMap((value) => {
if (!value || typeof value !== "object") return [];
const itemId = "itemId" in value && typeof value.itemId === "string" ? value.itemId : "";
const declaredAmount = "declaredAmount" in value && typeof value.declaredAmount === "string"
? value.declaredAmount
: "";
const invoiceTotal = "invoiceTotal" in value && typeof value.invoiceTotal === "string"
? value.invoiceTotal
: "";
if (!itemId || !declaredAmount || !invoiceTotal) return [];
return [{ itemId, declaredAmount, invoiceTotal }];
})
: [];
return {
unlinkedInvoiceCount: readNonNegativeInteger(error.details.unlinkedInvoiceCount),
unlinkedInvoiceIds: readStringArray(error.details.unlinkedInvoiceIds),
missingInvoiceItemIds: readStringArray(error.details.missingInvoiceItemIds),
amountUnavailableItems,
amountExceededItems,
};
}
export function invoiceGuardIssueCount(issues: ReimbursementInvoiceGuardIssues): number {
return issues.unlinkedInvoiceCount
+ issues.missingInvoiceItemIds.length
+ issues.amountUnavailableItems.length
+ issues.amountExceededItems.length;
}
export function resolveInvoiceGuardIssuesAfterLink(
issues: ReimbursementInvoiceGuardIssues | null,
invoiceId: string,
linkedItemId: string,
): ReimbursementInvoiceGuardIssues | null {
if (!issues) return null;
const locatedInvoice = issues.unlinkedInvoiceIds.includes(invoiceId);
const unlinkedInvoiceIds = issues.unlinkedInvoiceIds.filter((id) => id !== invoiceId);
const shouldDecreaseCount = locatedInvoice || issues.unlinkedInvoiceIds.length === 0;
const next = {
...issues,
unlinkedInvoiceCount: shouldDecreaseCount
? Math.max(0, issues.unlinkedInvoiceCount - 1)
: issues.unlinkedInvoiceCount,
unlinkedInvoiceIds,
missingInvoiceItemIds: issues.missingInvoiceItemIds.filter((id) => id !== linkedItemId),
};
return invoiceGuardIssueCount(next) > 0 ? next : null;
}
export function invoiceGuardItemMessages(
issues: ReimbursementInvoiceGuardIssues | null,
itemId: string,
): string[] {
if (!issues) return [];
const messages: string[] = [];
if (issues.missingInvoiceItemIds.includes(itemId)) {
messages.push("尚未关联正式发票,请到“报销材料”添加发票并选择本明细。");
}
if (issues.amountUnavailableItems.some((item) => item.itemId === itemId)) {
messages.push("关联发票的票面金额未识别,请重试识别或联系财务处理。");
}
const exceeded = issues.amountExceededItems.find((item) => item.itemId === itemId);
if (exceeded) {
messages.push(
`申报金额 ¥${exceeded.declaredAmount} 高于关联发票合计 ¥${exceeded.invoiceTotal},请补充发票或调整申报金额。`,
);
}
return messages;
}
export function invoiceGuardInvoiceMessages(
issues: ReimbursementInvoiceGuardIssues | null,
invoiceId: string,
linkedItemId: string | null,
): string[] {
if (!issues) return [];
const messages: string[] = [];
const isUnlinked = issues.unlinkedInvoiceIds.includes(invoiceId)
|| (issues.unlinkedInvoiceIds.length === 0 && issues.unlinkedInvoiceCount > 0 && !linkedItemId);
if (isUnlinked) {
messages.push("这张发票还没有选择报销明细,请在下方完成关联。");
}
if (issues.amountUnavailableItems.some((item) => item.invoiceIds.includes(invoiceId))) {
messages.push("这张发票未识别出票面金额,请重试识别或联系财务处理。");
}
return messages;
}
export function focusFirstInvoiceGuardIssue() {
window.requestAnimationFrame(() => {
const target = document.querySelector<HTMLElement>('[data-reimbursement-guard-issue="true"]');
if (!target) return;
target.focus({ preventScroll: true });
target.scrollIntoView({ behavior: "smooth", block: "center" });
});
}