feat: 完善差旅票据行程提取与费用明细回填逻辑

增强文档智能识别的票据场景关键词和字段提取能力,优化
会话关联草稿报销单的解析路径,修复费用明细合并和票据
去重边界问题,前端改进报销创建和审批详情交互,补充单
元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-21 14:24:51 +08:00
parent b183b0bd5e
commit f28d7e6d16
24 changed files with 1565 additions and 433 deletions

View File

@@ -1296,6 +1296,65 @@ function buildReviewFormValues(fields) {
}, {})
}
function buildBusinessTimeContextFromReviewValues(values = {}) {
const timeText = String(values.time_range || values.business_time || values.occurred_date || '').trim()
if (!timeText) {
return null
}
const matchedDates = timeText.match(/\d{4}-\d{2}-\d{2}/g) || []
if (!matchedDates.length) {
return null
}
const startDate = matchedDates[0]
const endDate = matchedDates[matchedDates.length - 1] || startDate
if (!isValidIsoDateString(startDate) || !isValidIsoDateString(endDate) || startDate > endDate) {
return null
}
const displayValue = startDate === endDate ? startDate : `${startDate}${endDate}`
return {
mode: startDate === endDate ? 'single' : 'range',
start_date: startDate,
end_date: endDate,
display_value: displayValue
}
}
function buildReviewFormContextFromPayload(reviewPayload, inlineState = null) {
if (!reviewPayload || typeof reviewPayload !== 'object') {
return {}
}
const fallbackState = buildInlineReviewState(reviewPayload)
const candidateState = inlineState || fallbackState
const hasCandidateValue = Object.values(candidateState || {}).some((value) => {
if (typeof value === 'number') return value > 0
return Boolean(String(value || '').trim())
})
const state = hasCandidateValue ? candidateState : fallbackState
const fields = mergeInlineReviewFields(reviewPayload.edit_fields || [], state)
const values = buildReviewFormValues(fields)
const slotMap = buildReviewSlotMap(reviewPayload)
const inheritedTimeRange = String(
slotMap.time_range?.normalized_value ||
slotMap.time_range?.value ||
values.time_range ||
values.business_time ||
values.occurred_date ||
''
).trim()
if (inheritedTimeRange) {
values.time_range = values.time_range || inheritedTimeRange
values.business_time = values.business_time || inheritedTimeRange
}
const businessTimeContext = buildBusinessTimeContextFromReviewValues(values)
return {
review_form_values: values,
...(businessTimeContext ? { business_time_context: businessTimeContext } : {})
}
}
function buildReviewCorrectionMessage(fields) {
const lines = ['请按以下核对后的报销信息更新当前识别结果:']
for (const item of cloneReviewEditFields(fields)) {
@@ -4956,7 +5015,13 @@ export default {
}
function saveInlineReviewChanges() {
if (!activeReviewPayload.value || !reviewHasUnsavedChanges.value || reviewActionBusy.value) return
if (
!activeReviewPayload.value
|| !reviewHasUnsavedChanges.value
|| submitting.value
|| reviewActionBusy.value
|| sessionSwitchBusy.value
) return
if (reviewInlineEditorKey.value && !commitInlineReviewEditor()) {
return
@@ -5043,7 +5108,7 @@ export default {
}
async function submitComposer(options = {}) {
if (sessionSwitchBusy.value) return null
if (submitting.value || sessionSwitchBusy.value) return null
const rawText = resolveComposerSubmitText(options.rawText).trim()
const systemGenerated = Boolean(options.systemGenerated)
@@ -5160,6 +5225,21 @@ export default {
if (resolvedUploadDisposition === 'continue_existing') {
extraContext.review_action = 'link_to_existing_draft'
const inheritedReviewContext = buildReviewFormContextFromPayload(
activeReviewPayload.value,
reviewInlineForm.value
)
if (inheritedReviewContext.review_form_values) {
extraContext.review_form_values = {
...inheritedReviewContext.review_form_values,
...(extraContext.review_form_values && typeof extraContext.review_form_values === 'object'
? extraContext.review_form_values
: {})
}
}
if (inheritedReviewContext.business_time_context && !extraContext.business_time_context) {
extraContext.business_time_context = inheritedReviewContext.business_time_context
}
effectiveFileNames = mergeUploadAttachmentNames(reviewAttachmentNames, fileNames)
effectiveOcrDocuments = mergeUploadOcrDocuments(
buildOcrDocumentsFromReviewPayload(activeReviewPayload.value),
@@ -5362,7 +5442,7 @@ export default {
async function handleReviewAction(message, action) {
const actionType = String(action?.action_type || '').trim()
if (!actionType || reviewActionBusy.value) return
if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return
if (actionType === 'cancel_review') {
openCancelReviewDialog(message)