feat: 报销预审会话状态管理与工作台交互增强
- 新增差旅报销会话状态管理与对话模型重构 - 增强风险观测服务与运行时聊天上下文作用域 - 优化工作台图标资源、助理意图识别与摘要工具 - 完善报销创建视图样式与差旅详情页标准调整交互 - 补充风险观测、运行时聊天与报销端点测试覆盖
This commit is contained in:
@@ -612,10 +612,10 @@ export default {
|
||||
const submitConfirmDialogOpen = ref(false)
|
||||
const riskOverrideDialogOpen = ref(false)
|
||||
const riskOverrideBusy = ref(false)
|
||||
const standardAdjustmentBusy = ref(false)
|
||||
const riskOverrideIndex = ref(0)
|
||||
const highlightedRiskCardId = ref('')
|
||||
let highlightedRiskCardTimer = 0
|
||||
const riskOverrideReasons = reactive({})
|
||||
const deleteBusy = ref(false)
|
||||
const deleteDialogOpen = ref(false)
|
||||
const returnBusy = ref(false)
|
||||
@@ -653,6 +653,8 @@ export default {
|
||||
})
|
||||
const detailNoteEditor = ref('')
|
||||
const savingDetailNote = ref(false)
|
||||
let standardAdjustmentTaskSeq = 0
|
||||
let submitTaskSeq = 0
|
||||
|
||||
const request = computed(() => {
|
||||
const normalized = normalizeRequestForUi(props.request)
|
||||
@@ -901,7 +903,6 @@ export default {
|
||||
const actionBusy = computed(() =>
|
||||
Boolean(savingExpenseId.value)
|
||||
|| submitBusy.value
|
||||
|| riskOverrideBusy.value
|
||||
|| deleteBusy.value
|
||||
|| returnBusy.value
|
||||
|| approveBusy.value
|
||||
@@ -935,6 +936,10 @@ export default {
|
||||
Object.keys(expenseAttachmentMeta).forEach((key) => {
|
||||
delete expenseAttachmentMeta[key]
|
||||
})
|
||||
standardAdjustmentTaskSeq += 1
|
||||
standardAdjustmentBusy.value = false
|
||||
submitTaskSeq += 1
|
||||
submitBusy.value = false
|
||||
closeAttachmentPreview()
|
||||
}
|
||||
pendingUploadExpenseId.value = ''
|
||||
@@ -1776,17 +1781,6 @@ export default {
|
||||
return
|
||||
}
|
||||
riskOverrideIndex.value = 0
|
||||
const activeIds = new Set(warnings.map((risk) => risk.id))
|
||||
Object.keys(riskOverrideReasons).forEach((riskId) => {
|
||||
if (!activeIds.has(riskId)) {
|
||||
delete riskOverrideReasons[riskId]
|
||||
}
|
||||
})
|
||||
warnings.forEach((risk) => {
|
||||
if (typeof riskOverrideReasons[risk.id] !== 'string') {
|
||||
riskOverrideReasons[risk.id] = ''
|
||||
}
|
||||
})
|
||||
riskOverrideDialogOpen.value = true
|
||||
}
|
||||
|
||||
@@ -1824,105 +1818,43 @@ export default {
|
||||
riskOverrideIndex.value = (riskOverrideIndex.value + 1) % submitRiskWarnings.value.length
|
||||
}
|
||||
|
||||
function mergeDetailNoteWithRiskOverride(appendix) {
|
||||
const baseNote = detailNoteEditor.value.trim() || detailNoteSource.value
|
||||
return [baseNote, appendix].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
|
||||
function confirmStandardAdjustment() {
|
||||
if (riskOverrideBusy.value || standardAdjustmentBusy.value) {
|
||||
return
|
||||
}
|
||||
const claimId = String(request.value?.claimId || '').trim()
|
||||
if (!claimId) {
|
||||
toast('\u5f53\u524d\u8349\u7a3f\u7f3a\u5c11 claimId\uff0c\u6682\u65f6\u65e0\u6cd5\u6309\u804c\u7ea7\u6807\u51c6\u91cd\u7b97\u3002')
|
||||
return
|
||||
}
|
||||
|
||||
riskOverrideDialogOpen.value = false
|
||||
standardAdjustmentBusy.value = true
|
||||
const taskSeq = ++standardAdjustmentTaskSeq
|
||||
toast('\u6b63\u5728\u540e\u53f0\u6309\u804c\u7ea7\u6807\u51c6\u91cd\u65b0\u6d4b\u7b97\u8d39\u7528\u3002')
|
||||
void runStandardAdjustmentRecalculation(claimId, taskSeq)
|
||||
}
|
||||
|
||||
async function confirmRiskOverrideReasons() {
|
||||
if (riskOverrideBusy.value) {
|
||||
return
|
||||
}
|
||||
const missingIndex = submitRiskWarnings.value.findIndex((risk) => !String(riskOverrideReasons[risk.id] || '').trim())
|
||||
if (missingIndex >= 0) {
|
||||
riskOverrideIndex.value = missingIndex
|
||||
toast('请为每一条风险填写异常说明。')
|
||||
return
|
||||
}
|
||||
|
||||
const itemNoteGroups = new Map()
|
||||
const claimLevelRisks = []
|
||||
submitRiskWarnings.value.forEach((risk, index) => {
|
||||
const reason = String(riskOverrideReasons[risk.id] || '').trim()
|
||||
const item = resolveExpenseItemForRiskCard(risk)
|
||||
if (item?.id) {
|
||||
const currentGroup = itemNoteGroups.get(item.id) || { item, reasons: [] }
|
||||
currentGroup.reasons.push(reason)
|
||||
itemNoteGroups.set(item.id, currentGroup)
|
||||
} else {
|
||||
const title = String(risk.title || risk.label || '风险').trim()
|
||||
claimLevelRisks.push(`异常说明:第${index + 1}条 ${title}:${reason}`)
|
||||
}
|
||||
})
|
||||
|
||||
riskOverrideBusy.value = true
|
||||
try {
|
||||
await Promise.all(
|
||||
[...itemNoteGroups.entries()].map(([itemId, group]) => {
|
||||
const existingNote = String(group.item?.itemNote || '').trim()
|
||||
const nextNote = [
|
||||
existingNote,
|
||||
...group.reasons.filter((reason) => existingNote !== reason && !existingNote.includes(reason))
|
||||
].filter(Boolean).join('\n')
|
||||
return updateExpenseClaimItem(request.value.claimId, itemId, {
|
||||
item_note: nextNote
|
||||
})
|
||||
})
|
||||
)
|
||||
itemNoteGroups.forEach((group, itemId) => {
|
||||
const existingNote = String(group.item?.itemNote || '').trim()
|
||||
const nextNote = [
|
||||
existingNote,
|
||||
...group.reasons.filter((reason) => existingNote !== reason && !existingNote.includes(reason))
|
||||
].filter(Boolean).join('\n')
|
||||
applyLocalExpenseItemPatch(itemId, {
|
||||
itemNote: nextNote
|
||||
})
|
||||
})
|
||||
if (claimLevelRisks.length) {
|
||||
const appendix = claimLevelRisks.join('\n')
|
||||
const nextNote = mergeDetailNoteWithRiskOverride(appendix)
|
||||
if (nextNote.length > 500) {
|
||||
toast('附加说明最多 500 字,请精简风险原因后再继续提交。')
|
||||
return
|
||||
}
|
||||
await updateExpenseClaim(request.value.claimId, {
|
||||
reason: nextNote
|
||||
})
|
||||
detailNoteEditor.value = nextNote
|
||||
}
|
||||
riskOverrideDialogOpen.value = false
|
||||
submitConfirmDialogOpen.value = true
|
||||
toast('异常说明已保存,可继续提交审批。')
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || '异常说明保存失败,请稍后重试。')
|
||||
} finally {
|
||||
riskOverrideBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmStandardAdjustment() {
|
||||
if (riskOverrideBusy.value) {
|
||||
return
|
||||
}
|
||||
riskOverrideBusy.value = true
|
||||
async function runStandardAdjustmentRecalculation(claimId, taskSeq) {
|
||||
try {
|
||||
const payload = await buildStandardAdjustmentPayload()
|
||||
if (!payload.risks.length) {
|
||||
toast('当前风险暂未匹配到可重算的费用明细,请先补充异常说明。')
|
||||
toast('\u5f53\u524d\u98ce\u9669\u6682\u672a\u5339\u914d\u5230\u53ef\u91cd\u7b97\u7684\u8d39\u7528\u660e\u7ec6\uff0c\u8bf7\u5148\u8865\u5145\u5f02\u5e38\u8bf4\u660e\u3002')
|
||||
return
|
||||
}
|
||||
const response = await acceptExpenseClaimStandardAdjustment(claimId, payload)
|
||||
if (taskSeq !== standardAdjustmentTaskSeq || String(request.value?.claimId || '').trim() !== claimId) {
|
||||
return
|
||||
}
|
||||
const response = await acceptExpenseClaimStandardAdjustment(request.value.claimId, payload)
|
||||
applyStandardAdjustmentResponse(response)
|
||||
riskOverrideDialogOpen.value = false
|
||||
submitConfirmDialogOpen.value = true
|
||||
toast('已按职级最高报销标准重算实际报销金额。')
|
||||
toast('\u5df2\u6309\u804c\u7ea7\u6700\u9ad8\u62a5\u9500\u6807\u51c6\u91cd\u7b97\u5b9e\u9645\u62a5\u9500\u91d1\u989d\uff0c\u53ef\u7ee7\u7eed\u63d0\u4ea4\u5ba1\u6279\u3002')
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || '按职级标准重算失败,请稍后重试。')
|
||||
toast(error?.message || '\u6309\u804c\u7ea7\u6807\u51c6\u91cd\u7b97\u5931\u8d25\uff0c\u8bf7\u7a0d\u540e\u91cd\u8bd5\u3002')
|
||||
} finally {
|
||||
riskOverrideBusy.value = false
|
||||
if (taskSeq === standardAdjustmentTaskSeq) {
|
||||
standardAdjustmentBusy.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2375,6 +2307,11 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (standardAdjustmentBusy.value) {
|
||||
toast('费用正在按职级标准重新测算,完成后再提交审批。')
|
||||
return
|
||||
}
|
||||
|
||||
if (draftBlockingIssues.value.length) {
|
||||
toast(draftBlockingIssues.value[0] || '请先补全草稿信息,再提交审批。')
|
||||
return
|
||||
@@ -2396,7 +2333,7 @@ export default {
|
||||
submitConfirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
async function confirmSubmitRequest() {
|
||||
function confirmSubmitRequest() {
|
||||
if (!request.value.claimId) {
|
||||
toast('当前草稿缺少 claimId,暂时无法提交。')
|
||||
submitConfirmDialogOpen.value = false
|
||||
@@ -2409,34 +2346,57 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (standardAdjustmentBusy.value) {
|
||||
toast('费用正在按职级标准重新测算,完成后再提交审批。')
|
||||
submitConfirmDialogOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (draftBlockingIssues.value.length) {
|
||||
toast(draftBlockingIssues.value[0] || '请先补全草稿信息,再提交审批。')
|
||||
submitConfirmDialogOpen.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const claimId = String(request.value.claimId || '').trim()
|
||||
const documentNo = request.value.id
|
||||
const isApplication = isApplicationDocument.value
|
||||
submitBusy.value = true
|
||||
submitConfirmDialogOpen.value = false
|
||||
const taskSeq = ++submitTaskSeq
|
||||
toast('\u6b63\u5728\u540e\u53f0\u63d0\u4ea4\u5ba1\u6279\uff0c\u5b8c\u6210\u540e\u4f1a\u81ea\u52a8\u66f4\u65b0\u5355\u636e\u72b6\u6001\u3002')
|
||||
void runSubmitRequest(claimId, documentNo, isApplication, taskSeq)
|
||||
}
|
||||
|
||||
async function runSubmitRequest(claimId, documentNo, isApplication, taskSeq) {
|
||||
try {
|
||||
const payload = await submitExpenseClaim(request.value.claimId)
|
||||
const payload = await submitExpenseClaim(claimId)
|
||||
if (taskSeq !== submitTaskSeq || String(request.value?.claimId || '').trim() !== claimId) {
|
||||
return
|
||||
}
|
||||
const claimStatus = String(payload?.status || '').trim().toLowerCase()
|
||||
const approvalStage = String(payload?.approval_stage || payload?.approvalStage || '').trim()
|
||||
if (claimStatus === 'submitted') {
|
||||
toast(
|
||||
isApplicationDocument.value
|
||||
? `${request.value.id} 申请单已提交${approvalStage ? `,当前节点:${approvalStage}` : ',等待直属领导审批'}。`
|
||||
: `${request.value.id} 已提交审批${approvalStage ? `,当前节点:${approvalStage}` : '。'}`
|
||||
isApplication
|
||||
? `${documentNo} 申请单已提交${approvalStage ? `,当前节点:${approvalStage}` : ',等待直属领导审批'}。`
|
||||
: `${documentNo} 已提交审批${approvalStage ? `,当前节点:${approvalStage}` : '。'}`
|
||||
)
|
||||
} else if (claimStatus === 'supplement') {
|
||||
toast(`${request.value.id} 自动检测未通过,已转待补充。`)
|
||||
toast(`${documentNo} 自动检测未通过,已转待补充。`)
|
||||
} else {
|
||||
toast(`${request.value.id} 提交结果已更新。`)
|
||||
toast(`${documentNo} 提交结果已更新。`)
|
||||
}
|
||||
submitConfirmDialogOpen.value = false
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
emit('request-updated', { claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || '提交审批失败,请稍后重试。')
|
||||
if (taskSeq === submitTaskSeq) {
|
||||
toast(error?.message || '提交审批失败,请稍后重试。')
|
||||
}
|
||||
} finally {
|
||||
submitBusy.value = false
|
||||
if (taskSeq === submitTaskSeq) {
|
||||
submitBusy.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2664,6 +2624,10 @@ export default {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
standardAdjustmentTaskSeq += 1
|
||||
standardAdjustmentBusy.value = false
|
||||
submitTaskSeq += 1
|
||||
submitBusy.value = false
|
||||
if (highlightedRiskCardTimer) {
|
||||
window.clearTimeout(highlightedRiskCardTimer)
|
||||
highlightedRiskCardTimer = 0
|
||||
@@ -2688,7 +2652,7 @@ export default {
|
||||
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closePayConfirmDialog, closeSubmitConfirmDialog,
|
||||
closeRiskOverrideDialog, closeSmartEntryUploadDialog,
|
||||
closeReturnDialog, confirmApproveRequest, confirmDeleteRequest, confirmSubmitRequest, confirmReturnRequest,
|
||||
confirmPayRequest, confirmRiskOverrideReasons, confirmStandardAdjustment, confirmSmartEntryUpload,
|
||||
confirmPayRequest, confirmStandardAdjustment, confirmSmartEntryUpload,
|
||||
chooseSmartEntryFile, clearSmartEntryFile,
|
||||
currentAttachmentPreviewInsight, currentAttachmentPreviewRiskCards, currentProgressRingMotion,
|
||||
currentSubmitRiskWarning,
|
||||
@@ -2715,10 +2679,11 @@ export default {
|
||||
resolveRiskCardDomId, isHighlightedRiskCard,
|
||||
returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
|
||||
requiresApprovalOpinion,
|
||||
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
|
||||
saveDetailNote, savingDetailNote, savingExpenseId,
|
||||
smartEntrySelectedFileCount, smartEntrySelectedFileNames, smartEntrySelectedFileSummary,
|
||||
smartEntryRecognitionBusy, smartEntryRecognitionText,
|
||||
smartEntryUploadBusy, smartEntryUploadDialogOpen, smartEntryUploadInput,
|
||||
standardAdjustmentBusy,
|
||||
showAiAdvicePanel, showApplicationLeaderOpinion,
|
||||
showBudgetAnalysis, showStageRiskAdvice,
|
||||
showExpenseRisk, startExpenseEdit, submitActionIcon, submitActionLabel, submitBusy,
|
||||
|
||||
Reference in New Issue
Block a user