feat: 细化差旅票据费用明细分类并自动计算出差补贴
将差旅费用明细拆分为火车票、机票、住宿票、乘车等细分类 型,根据票据字段自动生成行程/事由描述,结合规则引擎自 动计算出差补贴金额,前端适配费用明细编辑和差旅票据审 核交互,补充单元测试覆盖。
This commit is contained in:
@@ -1497,6 +1497,20 @@ function resolveReviewMissingSlotCards(reviewPayload) {
|
||||
: []
|
||||
}
|
||||
|
||||
function resolveReviewExtraMissingLabels(reviewPayload) {
|
||||
const labels = Array.isArray(reviewPayload?.missing_slots)
|
||||
? reviewPayload.missing_slots.map((item) => String(item || '').trim()).filter(Boolean)
|
||||
: []
|
||||
if (!labels.length) return []
|
||||
|
||||
const slotLabels = new Set(
|
||||
(Array.isArray(reviewPayload?.slot_cards) ? reviewPayload.slot_cards : [])
|
||||
.map((item) => String(item?.label || item?.key || '').trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
return labels.filter((label) => !slotLabels.has(label))
|
||||
}
|
||||
|
||||
function resolveReviewRiskBriefs(reviewPayload) {
|
||||
if (!Array.isArray(reviewPayload?.risk_briefs)) return []
|
||||
return reviewPayload.risk_briefs.filter((item) => {
|
||||
@@ -1762,7 +1776,7 @@ function buildExpenseQueryHint(queryPayload) {
|
||||
}
|
||||
|
||||
function countReviewPendingItems(reviewPayload) {
|
||||
return resolveReviewMissingSlotCards(reviewPayload).length
|
||||
return resolveReviewMissingSlotCards(reviewPayload).length + resolveReviewExtraMissingLabels(reviewPayload).length
|
||||
}
|
||||
|
||||
function countReviewRiskItems(reviewPayload) {
|
||||
@@ -1825,12 +1839,12 @@ function shouldOpenReviewDisclosure(reviewPayload) {
|
||||
}
|
||||
|
||||
function buildReviewTodoSectionTitle(reviewPayload) {
|
||||
return resolveReviewMissingSlotCards(reviewPayload).length ? '待补充内容' : '已识别信息'
|
||||
return countReviewPendingItems(reviewPayload) ? '待补充内容' : '已识别信息'
|
||||
}
|
||||
|
||||
function buildReviewTodoSectionMeta(reviewPayload) {
|
||||
const count = buildReviewTodoItems(reviewPayload).length
|
||||
if (resolveReviewMissingSlotCards(reviewPayload).length) {
|
||||
if (countReviewPendingItems(reviewPayload)) {
|
||||
return count ? `${count} 项` : '待确认'
|
||||
}
|
||||
return count ? `${count} 项` : '已齐全'
|
||||
@@ -1864,6 +1878,17 @@ function buildReviewAlertChips(reviewPayload) {
|
||||
})
|
||||
}
|
||||
|
||||
if (chips.length < 3) {
|
||||
for (const label of resolveReviewExtraMissingLabels(reviewPayload)) {
|
||||
chips.push({
|
||||
key: label,
|
||||
label,
|
||||
tone: 'warning'
|
||||
})
|
||||
if (chips.length >= 3) break
|
||||
}
|
||||
}
|
||||
|
||||
if (chips.length < 3) {
|
||||
for (const risk of resolveReviewRiskBriefs(reviewPayload)) {
|
||||
if (chips.some((item) => item.label === risk.title)) continue
|
||||
@@ -1889,8 +1914,10 @@ function buildReviewAlertChips(reviewPayload) {
|
||||
|
||||
function buildReviewTodoItems(reviewPayload) {
|
||||
const missingItems = resolveReviewMissingSlotCards(reviewPayload)
|
||||
if (missingItems.length) {
|
||||
return missingItems.map((item) => {
|
||||
const extraMissingLabels = resolveReviewExtraMissingLabels(reviewPayload)
|
||||
if (missingItems.length || extraMissingLabels.length) {
|
||||
return [
|
||||
...missingItems.map((item) => {
|
||||
const config = REVIEW_SLOT_CONFIG[item.key] || {}
|
||||
return {
|
||||
key: item.key,
|
||||
@@ -1900,7 +1927,18 @@ function buildReviewTodoItems(reviewPayload) {
|
||||
status: config.status || '待补充',
|
||||
tone: 'warning'
|
||||
}
|
||||
})
|
||||
}),
|
||||
...extraMissingLabels.map((label, index) => ({
|
||||
key: `extra-missing-${index}-${label}`,
|
||||
icon: label.includes('酒店') || label.includes('住宿') ? 'mdi mdi-bed-outline' : 'mdi mdi-file-alert-outline',
|
||||
title: label,
|
||||
hint: label.includes('必须')
|
||||
? '该票据属于当前差旅提交的必备材料,补齐后才能继续下一步。'
|
||||
: '可以继续补充该材料;如暂时没有,也可以按当前信息处理。',
|
||||
status: label.includes('必须') ? '必须补齐' : '可选补充',
|
||||
tone: 'warning'
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
return resolveReviewRecognizedSlotCards(reviewPayload)
|
||||
@@ -2571,8 +2609,18 @@ function buildLocallySyncedReviewActions(reviewPayload, canProceed) {
|
||||
return actions
|
||||
}
|
||||
|
||||
const syncedActions = actions.filter((item) => String(item?.action_type || '').trim() !== 'next_step')
|
||||
if (!syncedActions.some((item) => String(item?.action_type || '').trim() === 'save_draft')) {
|
||||
syncedActions.push({
|
||||
label: '保存为草稿',
|
||||
action_type: 'save_draft',
|
||||
description: '先暂存当前已识别信息,稍后仍可继续补充或提交。',
|
||||
emphasis: 'secondary'
|
||||
})
|
||||
}
|
||||
|
||||
return [
|
||||
...actions.filter((item) => !['save_draft', 'next_step'].includes(String(item?.action_type || '').trim())),
|
||||
...syncedActions,
|
||||
{
|
||||
label: '继续下一步',
|
||||
action_type: 'next_step',
|
||||
@@ -2607,12 +2655,17 @@ function buildLocallySyncedReviewPayload(reviewPayload, inlineState = createEmpt
|
||||
const missingSlots = nextSlotCards
|
||||
.filter((slot) => slot.required && slot.status === 'missing')
|
||||
.map((slot) => slot.label || slot.key)
|
||||
const canProceed = missingSlots.length === 0 && (Array.isArray(reviewPayload.claim_groups) ? reviewPayload.claim_groups.length > 0 : true)
|
||||
const extraMissingSlots = resolveReviewExtraMissingLabels({
|
||||
...reviewPayload,
|
||||
slot_cards: nextSlotCards
|
||||
})
|
||||
const allMissingSlots = [...missingSlots, ...extraMissingSlots]
|
||||
const canProceed = allMissingSlots.length === 0 && (Array.isArray(reviewPayload.claim_groups) ? reviewPayload.claim_groups.length > 0 : true)
|
||||
|
||||
return {
|
||||
...reviewPayload,
|
||||
can_proceed: canProceed,
|
||||
missing_slots: missingSlots,
|
||||
missing_slots: allMissingSlots,
|
||||
slot_cards: nextSlotCards,
|
||||
confirmation_actions: buildLocallySyncedReviewActions(reviewPayload, canProceed)
|
||||
}
|
||||
@@ -2821,22 +2874,24 @@ function buildReviewDocumentSummaries(reviewPayload) {
|
||||
}
|
||||
|
||||
function buildReviewDecisionHint(reviewPayload) {
|
||||
const missingSlots = resolveReviewMissingSlotCards(reviewPayload)
|
||||
const pendingCount = countReviewPendingItems(reviewPayload)
|
||||
const riskBriefs = resolveReviewRiskBriefs(reviewPayload)
|
||||
if (reviewPayload?.can_proceed) {
|
||||
if (shouldShowReviewUploadButton(reviewPayload)) {
|
||||
return '必需信息已整理好;如还有非必需票据可以继续上传,也可以直接进入下一步或保存草稿。'
|
||||
}
|
||||
return riskBriefs.length
|
||||
? `我已经把信息整理好了。你可以直接进入下一步,提交前再看一下下方 ${riskBriefs.length} 条提醒。`
|
||||
: '我已经把信息整理好了。你确认无误后,可以直接进入下一步。'
|
||||
}
|
||||
if (missingSlots.length) {
|
||||
return `我先完成了当前这轮识别,还差 ${missingSlots.length} 项关键信息。你可以继续补充;如果暂时拿不全,也可以先保存草稿。`
|
||||
if (pendingCount) {
|
||||
return `我先完成了当前这轮识别,还差 ${pendingCount} 项关键信息。你可以继续补充;如果暂时拿不全,也可以先保存草稿。`
|
||||
}
|
||||
return '如果你觉得识别结果有偏差,点“修改识别信息”直接校正,我会按新内容重新识别。'
|
||||
}
|
||||
|
||||
function buildReviewMissingHint(reviewPayload) {
|
||||
const missingSlots = resolveReviewMissingSlotCards(reviewPayload)
|
||||
if (!missingSlots.length) {
|
||||
if (!countReviewPendingItems(reviewPayload)) {
|
||||
return ''
|
||||
}
|
||||
if (reviewPayload?.can_proceed) {
|
||||
@@ -2860,8 +2915,19 @@ function buildReviewActionHint(reviewPayload) {
|
||||
return '如果现在信息还不完整,可以先保存草稿;识别错了就点“修改识别信息”。'
|
||||
}
|
||||
|
||||
function shouldShowReviewUploadButton(reviewPayload) {
|
||||
const documents = Array.isArray(reviewPayload?.document_cards) ? reviewPayload.document_cards : []
|
||||
if (!documents.length) return true
|
||||
if (countReviewPendingItems(reviewPayload)) return true
|
||||
|
||||
return resolveReviewRiskBriefs(reviewPayload).some((brief) => {
|
||||
const text = `${brief?.title || ''} ${brief?.content || ''} ${brief?.suggestion || ''}`
|
||||
return /差旅票据待补充|待上传|可继续上传|可继续提供/.test(text)
|
||||
})
|
||||
}
|
||||
|
||||
function buildReviewStatusTag(reviewPayload) {
|
||||
const missingCount = resolveReviewMissingSlotCards(reviewPayload).length
|
||||
const missingCount = countReviewPendingItems(reviewPayload)
|
||||
if (reviewPayload?.can_proceed) {
|
||||
return '可继续处理'
|
||||
}
|
||||
@@ -5607,6 +5673,7 @@ export default {
|
||||
buildReviewTodoSectionMeta,
|
||||
buildReviewAlertChips,
|
||||
buildReviewTodoItems,
|
||||
shouldShowReviewUploadButton,
|
||||
resolveReviewSubmitActions,
|
||||
resolveReviewPrimaryAction,
|
||||
resolveReviewEditAction,
|
||||
|
||||
Reference in New Issue
Block a user