refactor(travel): split reimbursement create workflow
完整修改内容: - 拆分 TravelReimbursementCreateView:提取审核面板纯模型、消息操作、建议动作处理、生命周期 watcher/UI 映射、小财管家运行时、续办流程和运行时文本模型,减少主组件继续堆叠业务分支。 - 调整申请预览链路:新增本地申请意图 gate,完善复杂差旅申请的大模型复核判断、交通方式缺失/候选识别、规则中心交通费用预估合并和申请冲突处理。 - 优化小财管家流程:抽出 steward typewriter 分段策略,避免 Markdown 表格逐字闪烁;补齐跨助手 carry、字段补齐续办、文本确认提交和行程规划推荐动作。 - 调整消息与样式:移除申请预览日期 chip 样式,收敛申请卡片/报销草稿消息的展示与复制、朗读、反馈入口逻辑。 - 更新测试:将源码锚点迁移到新模块,覆盖申请预览、提交确认、小财管家续办、引导流和审核抽屉相关断言。 验证: - node --check web/src/views/scripts/TravelReimbursementCreateView.js 及新增拆分模块 - npm --prefix web run build - node --test web/tests/expense-application-fast-preview.test.mjs web/tests/expense-application-submit-rich-confirm.test.mjs web/tests/travel-reimbursement-guided-flow.test.mjs 说明: - 后端/规则/容器配置/Audit 页面等工作区已有改动未纳入本提交。 - 容器内后端定向 pytest 曾运行 timeout 180s /tmp/x-financial-server-venv/bin/pytest -q <相关后端测试>,180 秒超时且超时前已有失败标记,未作为通过项记录。 - TravelReimbursementCreateView 当前仍超过 800 行,后续仍需继续拆分;本提交先把新增职责模块控制在 800 行内,阻止主类/主模块继续膨胀。
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
import { nextTick, onBeforeUnmount, onMounted, watch } from 'vue'
|
||||
import {
|
||||
buildLocalApplicationPreviewMessage,
|
||||
normalizeApplicationPreview
|
||||
} from '../../utils/expenseApplicationPreview.js'
|
||||
import {
|
||||
MAX_ATTACHMENTS,
|
||||
VISIBLE_ATTACHMENT_CHIPS,
|
||||
buildReviewFilePreviewsFromReviewPayload
|
||||
} from './travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
SESSION_TYPE_EXPENSE,
|
||||
createMessage,
|
||||
buildWelcomeInsight
|
||||
} from './travelReimbursementConversationModel.js'
|
||||
|
||||
export function useTravelReimbursementCreateViewLifecycle({
|
||||
activeFlowSteps,
|
||||
activeReviewPanelScope,
|
||||
activeReviewPayload,
|
||||
activeSessionType,
|
||||
adjustComposerTextareaHeight,
|
||||
attachedFiles,
|
||||
clearExpenseSessionForDeletedClaim,
|
||||
clearStewardThinkingTimers,
|
||||
closeAfterBusy,
|
||||
composerDraft,
|
||||
composerFilesExpanded,
|
||||
composerUploadIntent,
|
||||
conversationId,
|
||||
currentInsight,
|
||||
currentUser,
|
||||
draftClaimId,
|
||||
guidedFlowState,
|
||||
handleComposerDatePickerOutside,
|
||||
hasInsightPanelContent,
|
||||
insightPanelCollapsed,
|
||||
linkedRequest,
|
||||
maybeFinalizeDeferredClose,
|
||||
mergeFilesWithLimit,
|
||||
messages,
|
||||
persistSessionState,
|
||||
props,
|
||||
rememberFilePreviews,
|
||||
resetReviewDrawerFromPayload,
|
||||
resolveActiveClaimId,
|
||||
restorePersistedDraftAttachmentPreviews,
|
||||
reviewDocumentDrawerAvailable,
|
||||
reviewDrawerMode,
|
||||
reviewFilePreviews,
|
||||
reviewFlowDrawerAvailable,
|
||||
reviewRiskDrawerAvailable,
|
||||
scrollToBottom,
|
||||
startFlowTick,
|
||||
stopAttachmentRuntime,
|
||||
stopFlowRuntime,
|
||||
submitComposer,
|
||||
toast,
|
||||
workbenchVisible,
|
||||
REVIEW_DRAWER_MODE_DOCUMENTS,
|
||||
REVIEW_DRAWER_MODE_FLOW,
|
||||
REVIEW_DRAWER_MODE_REVIEW,
|
||||
REVIEW_DRAWER_MODE_RISK,
|
||||
SESSION_TYPE_EXPENSE: sessionTypeExpense = SESSION_TYPE_EXPENSE
|
||||
}) {
|
||||
watch(
|
||||
() => [activeReviewPayload.value, activeReviewPanelScope.value],
|
||||
([payload]) => {
|
||||
rememberFilePreviews(buildReviewFilePreviewsFromReviewPayload(payload))
|
||||
const shouldKeepFlowDrawer = reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW && activeFlowSteps.value.length > 0
|
||||
resetReviewDrawerFromPayload(payload)
|
||||
if (shouldKeepFlowDrawer) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => hasInsightPanelContent.value,
|
||||
(available) => {
|
||||
if (!available) {
|
||||
insightPanelCollapsed.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reviewDocumentDrawerAvailable.value,
|
||||
(available) => {
|
||||
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reviewRiskDrawerAvailable.value,
|
||||
(available) => {
|
||||
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => reviewFlowDrawerAvailable.value,
|
||||
(available) => {
|
||||
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [activeSessionType.value, activeFlowSteps.value.length],
|
||||
([, activeCount], [, previousActiveCount] = []) => {
|
||||
if (activeCount <= 0 || previousActiveCount > 0) {
|
||||
return
|
||||
}
|
||||
reviewDrawerMode.value = REVIEW_DRAWER_MODE_FLOW
|
||||
insightPanelCollapsed.value = false
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => composerDraft.value,
|
||||
() => {
|
||||
nextTick(adjustComposerTextareaHeight)
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
sessionType: activeSessionType.value,
|
||||
conversationId: conversationId.value,
|
||||
draftClaimId: draftClaimId.value,
|
||||
messages: messages.value,
|
||||
currentInsight: currentInsight.value,
|
||||
reviewFilePreviews: reviewFilePreviews.value,
|
||||
composerDraft: composerDraft.value,
|
||||
composerUploadIntent: composerUploadIntent.value,
|
||||
guidedFlowState: guidedFlowState.value,
|
||||
insightPanelCollapsed: insightPanelCollapsed.value
|
||||
}),
|
||||
() => {
|
||||
persistSessionState()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [activeSessionType.value, resolveActiveClaimId()],
|
||||
([sessionType, claimId]) => {
|
||||
if (sessionType !== sessionTypeExpense || !claimId) {
|
||||
return
|
||||
}
|
||||
void restorePersistedDraftAttachmentPreviews(claimId)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.invalidatedDraftClaimId,
|
||||
(claimId) => {
|
||||
clearExpenseSessionForDeletedClaim(claimId)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => workbenchVisible.value,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
scrollToBottom()
|
||||
} else {
|
||||
maybeFinalizeDeferredClose()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.reopenToken,
|
||||
(token, previousToken) => {
|
||||
if (token === previousToken) {
|
||||
return
|
||||
}
|
||||
closeAfterBusy.value = false
|
||||
workbenchVisible.value = true
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
scrollToBottom()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleComposerDatePickerOutside)
|
||||
startFlowTick()
|
||||
nextTick(() => {
|
||||
workbenchVisible.value = true
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
scrollToBottom()
|
||||
})
|
||||
})
|
||||
currentInsight.value =
|
||||
currentInsight.value
|
||||
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
||||
if (props.initialApplicationPreview && typeof props.initialApplicationPreview === 'object') {
|
||||
const applicationPreview = normalizeApplicationPreview(props.initialApplicationPreview)
|
||||
messages.value.push(createMessage('assistant', buildLocalApplicationPreviewMessage(applicationPreview), [], {
|
||||
meta: ['修改申请'],
|
||||
applicationPreview
|
||||
}))
|
||||
persistSessionState()
|
||||
}
|
||||
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
||||
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
||||
composerDraft.value = props.initialPrompt.trim()
|
||||
attachedFiles.value = initialMerge.files
|
||||
composerFilesExpanded.value = initialMerge.files.length > VISIBLE_ATTACHMENT_CHIPS
|
||||
if (initialMerge.overflowCount > 0) {
|
||||
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
||||
}
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
})
|
||||
if (props.initialPromptAutoSubmit !== false) {
|
||||
submitComposer()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleComposerDatePickerOutside)
|
||||
clearStewardThinkingTimers()
|
||||
stopFlowRuntime()
|
||||
stopAttachmentRuntime()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user