feat(web): AI 文档详情引用解析与查询卡片增强

- 新增 aiDocumentDetailReference,统一解析 #ai-open-document-detail / #ai-open-application-detail 引用,兼容 A/R/D 短格式与 AP-/RE-/AD- 旧格式单号,提供 isBusinessDocumentReference 判定
- aiDocumentQueryModel 文档卡片接入详情引用,按申请单/报销单生成对应 href,HTML 渲染器识别单据记录表格并生成卡片链接
- PersonalWorkbenchAiMode 处理文档详情点击跳转,卡片样式重构为结构化布局并更新背景资源
- expenseApplicationPreview 补充事由字段,同步新增/更新 ai-document-detail-reference、document-query-model、html-renderer、workbench-ai-mode 等测试
- 更新公司通信费报销规则表
This commit is contained in:
caoxiaozhu
2026-06-21 22:49:53 +08:00
parent 3b74a330a3
commit 8b3495455b
15 changed files with 832 additions and 318 deletions

View File

@@ -0,0 +1,141 @@
import { isApplicationDocumentNo } from './documentClassification.js'
export const AI_DOCUMENT_DETAIL_HREF_PREFIX = '#ai-open-document-detail:'
export const AI_APPLICATION_DETAIL_HREF_PREFIX = '#ai-open-application-detail:'
const SHORT_BUSINESS_DOCUMENT_NO_PATTERN = /^(?:A|R|D)[A-HJ-NP-Z2-9]{8}$/
const LEGACY_BUSINESS_DOCUMENT_NO_PATTERN = /^(?:AP|APP|RE|AD|EXP|CL|BX)-/
function normalizeText(value = '') {
return String(value ?? '').trim()
}
export function isBusinessDocumentReference(value = '') {
const text = normalizeText(value)
return Boolean(
text &&
(
SHORT_BUSINESS_DOCUMENT_NO_PATTERN.test(text) ||
LEGACY_BUSINESS_DOCUMENT_NO_PATTERN.test(text)
)
)
}
function parseDetailReferencePayload(reference = '', options = {}) {
const text = normalizeText(reference)
if (!text) {
return null
}
const params = new URLSearchParams(text)
const claimId = normalizeText(params.get('claim_id') || params.get('claimId'))
const claimNo = normalizeText(
params.get('claim_no') ||
params.get('claimNo') ||
params.get('document_no') ||
params.get('documentNo')
)
const documentType = normalizeText(options.documentType)
if (claimId || claimNo) {
return {
reference: claimNo || claimId,
claimId,
claimNo,
...(documentType ? { documentType } : {})
}
}
return {
reference: text,
...(documentType ? { documentType } : {})
}
}
function parseAiDetailHref(href = '', prefix = '', options = {}) {
const value = normalizeText(href)
if (!value.startsWith(prefix)) {
return null
}
const encodedReference = value.slice(prefix.length)
if (!encodedReference) {
return null
}
try {
return parseDetailReferencePayload(decodeURIComponent(encodedReference), options)
} catch {
return parseDetailReferencePayload(encodedReference, options)
}
}
export function parseAiDocumentDetailHref(href = '') {
return parseAiDetailHref(href, AI_DOCUMENT_DETAIL_HREF_PREFIX)
}
export function parseAiApplicationDetailHref(href = '') {
return parseAiDetailHref(href, AI_APPLICATION_DETAIL_HREF_PREFIX, { documentType: 'application' })
}
function resolveExplicitClaimId(source = {}) {
const claimNo = normalizeText(source.claimNo || source.claim_no || source.documentNo || source.document_no)
const rawClaimId = normalizeText(source.claimId || source.claim_id || source.id)
if (!rawClaimId || rawClaimId === claimNo || isBusinessDocumentReference(rawClaimId)) {
return ''
}
return rawClaimId
}
export function buildAiDocumentDetailHref(source = {}, options = {}) {
const prefix = normalizeText(options.prefix) || AI_DOCUMENT_DETAIL_HREF_PREFIX
const claimId = resolveExplicitClaimId(source)
const claimNo = normalizeText(source.claimNo || source.claim_no || source.documentNo || source.document_no)
const fallback = normalizeText(source.reference)
if (claimId || claimNo) {
const params = new URLSearchParams()
if (claimId) {
params.set('claim_id', claimId)
}
if (claimNo) {
params.set('claim_no', claimNo)
}
return `${prefix}${encodeURIComponent(params.toString())}`
}
return fallback ? `${prefix}${encodeURIComponent(fallback)}` : ''
}
export function buildAiDocumentDetailRequest(detailReference = {}) {
const reference = normalizeText(detailReference.reference)
const explicitClaimId = resolveExplicitClaimId(detailReference)
const explicitClaimNo = normalizeText(
detailReference.claimNo ||
detailReference.claim_no ||
detailReference.documentNo ||
detailReference.document_no
)
const referenceIsBusinessNo = isBusinessDocumentReference(reference)
const claimId = explicitClaimId || (!referenceIsBusinessNo ? reference : '')
const claimNo = explicitClaimNo || (referenceIsBusinessNo ? reference : '')
const lookupReference = claimId || claimNo || reference
const displayReference = claimNo || reference || lookupReference
const documentTypeCode = normalizeText(
detailReference.documentTypeCode ||
detailReference.document_type_code ||
detailReference.documentType ||
detailReference.document_type
).toLowerCase()
const isApplication = documentTypeCode === 'application' || isApplicationDocumentNo(displayReference)
return {
id: lookupReference,
claimId,
claimNo,
documentNo: displayReference,
documentType: isApplication ? 'application' : 'reimbursement',
documentTypeCode: isApplication ? 'application' : 'reimbursement',
detailLookupOnly: true,
source: 'workbench',
returnTo: 'workbench'
}
}