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:
@@ -23,6 +23,10 @@ const createViewScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const reviewPanelModelScript = readFileSync(
|
||||
fileURLToPath(new URL('../src/views/scripts/travelReimbursementReviewPanelModel.js', import.meta.url)),
|
||||
'utf8'
|
||||
)
|
||||
const messageItemTemplate = readFileSync(
|
||||
fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
|
||||
'utf8'
|
||||
@@ -173,9 +177,9 @@ test('local transport review no longer uses the travel hotel template', () => {
|
||||
}
|
||||
|
||||
assert.equal(isTravelReviewPayload(reviewPayload, { expense_type: '交通费' }), false)
|
||||
assert.match(createViewScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/)
|
||||
assert.match(reviewPanelModelScript, /shouldShowReviewFactCard\(reviewPayload, 'customer_name'/)
|
||||
assert.doesNotMatch(
|
||||
createViewScript,
|
||||
reviewPanelModelScript,
|
||||
/key:\s*'customer_name'[\s\S]{0,220}placeholder:\s*'请输入客户名称'[\s\S]{0,80}\},\s*\{[\s\S]{0,80}key:\s*'attachments'/
|
||||
)
|
||||
assert.match(createViewTemplate, /placeholder="例如:出租车\/网约车票据 \/ 火车\/高铁票"/)
|
||||
@@ -279,7 +283,7 @@ test('next step action uses rich text guidance and confirm dialog instead of foo
|
||||
})
|
||||
|
||||
test('review risk drawer lists risk briefs without score and posts details into the conversation', () => {
|
||||
const riskItemsBlock = createViewScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nfunction buildReviewRiskConversationText/)
|
||||
const riskItemsBlock = reviewPanelModelScript.match(/function buildReviewRiskItems\(reviewPayload\) \{[\s\S]*?\n\}\n\nexport function buildReviewRiskConversationText/)
|
||||
assert.ok(riskItemsBlock, 'risk item builder should be present')
|
||||
|
||||
assert.doesNotMatch(createViewTemplate, /review-side-risk-score/)
|
||||
@@ -288,9 +292,9 @@ test('review risk drawer lists risk briefs without score and posts details into
|
||||
assert.doesNotMatch(createViewScript, /function buildReviewRiskScore/)
|
||||
assert.doesNotMatch(createViewScript, /const reviewRiskScore/)
|
||||
assert.doesNotMatch(riskItemsBlock[0], /\.slice\(0,\s*6\)/)
|
||||
assert.match(createViewScript, /const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = \[[\s\S]*'历史报销画像'[\s\S]*'制度注意事项'/)
|
||||
assert.match(reviewPanelModelScript, /const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = \[[\s\S]*'历史报销画像'[\s\S]*'制度注意事项'/)
|
||||
assert.match(
|
||||
createViewScript,
|
||||
reviewPanelModelScript,
|
||||
/function resolveReviewRiskBriefs\(reviewPayload\) \{[\s\S]*DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS\.some/
|
||||
)
|
||||
|
||||
@@ -300,17 +304,17 @@ test('review risk drawer lists risk briefs without score and posts details into
|
||||
)
|
||||
assert.doesNotMatch(createViewTemplate, /\{\{\s*item\.levelLabel\s*\}\}/)
|
||||
assert.match(createViewTemplate, /class="review-side-risk-icon" :title="item\.levelLabel"/)
|
||||
assert.match(createViewScript, /info:\s*\{[\s\S]*label:\s*'提示'/)
|
||||
assert.match(createViewScript, /medium:\s*\{[\s\S]*label:\s*'中风险'/)
|
||||
assert.match(createViewScript, /low:\s*\{[\s\S]*label:\s*'低风险'/)
|
||||
assert.match(createViewScript, /const isInfo = String\(item\?\.level \|\| ''\)\.trim\(\) === 'info'/)
|
||||
assert.match(createViewScript, /\$\{isInfo \? '提示内容' : '风险点'\}:\$\{summary\}/)
|
||||
assert.match(createViewScript, /\$\{isInfo \? '处理建议' : '修改建议'\}:\$\{suggestion\}/)
|
||||
assert.match(createViewScript, /function normalizeReviewRiskTitle/)
|
||||
assert.match(createViewScript, /\.replace\(\/AI\\s\*预审/)
|
||||
assert.match(createViewScript, /\.replace\(\/\(高风险\|中风险\|低风险\)\/g,\s*''\)/)
|
||||
assert.match(createViewScript, /sourceLabel:\s*meta\.label/)
|
||||
assert.doesNotMatch(createViewScript, /normalizedTitle\.includes\('AI预审'\)/)
|
||||
assert.match(reviewPanelModelScript, /info:\s*\{[\s\S]*label:\s*'提示'/)
|
||||
assert.match(reviewPanelModelScript, /medium:\s*\{[\s\S]*label:\s*'中风险'/)
|
||||
assert.match(reviewPanelModelScript, /low:\s*\{[\s\S]*label:\s*'低风险'/)
|
||||
assert.match(reviewPanelModelScript, /const isInfo = String\(item\?\.level \|\| ''\)\.trim\(\) === 'info'/)
|
||||
assert.match(reviewPanelModelScript, /\$\{isInfo \? '提示内容' : '风险点'\}:\$\{summary\}/)
|
||||
assert.match(reviewPanelModelScript, /\$\{isInfo \? '处理建议' : '修改建议'\}:\$\{suggestion\}/)
|
||||
assert.match(reviewPanelModelScript, /function normalizeReviewRiskTitle/)
|
||||
assert.match(reviewPanelModelScript, /\.replace\(\/AI\\s\*预审/)
|
||||
assert.match(reviewPanelModelScript, /\.replace\(\/\(高风险\|中风险\|低风险\)\/g,\s*''\)/)
|
||||
assert.match(reviewPanelModelScript, /sourceLabel:\s*meta\.label/)
|
||||
assert.doesNotMatch(reviewPanelModelScript, /normalizedTitle\.includes\('AI预审'\)/)
|
||||
assert.match(createViewScript, /metaTone:\s*item\.level \|\| 'low'/)
|
||||
assert.doesNotMatch(createViewTemplate, /@click="openReviewRiskDetail\(item\)"/)
|
||||
assert.doesNotMatch(createViewTemplate, /review-risk-detail-modal/)
|
||||
@@ -321,7 +325,7 @@ test('review risk drawer lists risk briefs without score and posts details into
|
||||
createViewScript,
|
||||
/function appendReviewRiskBriefToConversation\(item\) \{[\s\S]*messages\.value\.push\(createMessage\('assistant'/
|
||||
)
|
||||
assert.match(createViewScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/)
|
||||
assert.match(reviewPanelModelScript, /function buildReviewRiskConversationText\(item, detailTarget = \{\}\)/)
|
||||
assert.match(createViewScript, /function resolveReviewDetailTarget\(message = null\) \{[\s\S]*router\.resolve\(\{[\s\S]*name: 'app-document-detail'/)
|
||||
assert.match(createViewScript, /function resolveReviewRiskDetailTarget\(\) \{[\s\S]*return resolveReviewDetailTarget\(\)/)
|
||||
assert.match(createViewScript, /进入 \$\{claimNo\} 详情重新填写/)
|
||||
@@ -335,14 +339,14 @@ test('review drawer default mode is scoped by the current action and travel over
|
||||
assert.match(reviewDrawerScript, /scope === 'documents' && hasDocuments[\s\S]*REVIEW_DRAWER_MODE_DOCUMENTS/)
|
||||
assert.match(reviewDrawerScript, /scope === 'risk' && hasRisks[\s\S]*REVIEW_DRAWER_MODE_RISK/)
|
||||
assert.match(reviewDrawerScript, /scope === 'overview'[\s\S]*REVIEW_DRAWER_MODE_REVIEW/)
|
||||
assert.match(createViewScript, /function normalizeReviewPanelScope\(scope\)/)
|
||||
assert.match(reviewPanelModelScript, /function normalizeReviewPanelScope\(scope\)/)
|
||||
assert.match(createViewScript, /canExposeReviewPanelScope\(item\.reviewPanelScope\)/)
|
||||
assert.match(createViewScript, /currentInsight\.value\.intent === 'agent' && agent[\s\S]*return null/)
|
||||
assert.match(createViewScript, /function isTravelReviewPayload\(reviewPayload/)
|
||||
assert.match(createViewScript, /function resolveReviewTravelTransportType\(reviewPayload/)
|
||||
assert.match(createViewScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/)
|
||||
assert.match(createViewScript, /label: '酒店名称'[\s\S]*modelKey: 'merchant_name'/)
|
||||
assert.match(createViewScript, /label: '出差事宜'[\s\S]*editor: 'textarea'[\s\S]*wide: true/)
|
||||
assert.match(reviewPanelModelScript, /function isTravelReviewPayload\(reviewPayload/)
|
||||
assert.match(reviewPanelModelScript, /function resolveReviewTravelTransportType\(reviewPayload/)
|
||||
assert.match(reviewPanelModelScript, /label: '交通类型'[\s\S]*modelKey: 'transport_type'/)
|
||||
assert.match(reviewPanelModelScript, /label: '酒店名称'[\s\S]*modelKey: 'merchant_name'/)
|
||||
assert.match(reviewPanelModelScript, /label: '出差事宜'[\s\S]*editor: 'textarea'[\s\S]*wide: true/)
|
||||
assert.match(createViewTemplate, /item\.editor === 'textarea'[\s\S]*<textarea/)
|
||||
assert.match(createViewTemplate, /wide: item\.wide/)
|
||||
})
|
||||
@@ -414,8 +418,8 @@ test('composer exposes travel calculator and posts spreadsheet-backed result int
|
||||
})
|
||||
|
||||
test('continuing receipt upload preserves prior review form context', () => {
|
||||
assert.match(createViewScript, /function buildReviewFormContextFromPayload\(reviewPayload, inlineState = null\)/)
|
||||
assert.match(createViewScript, /function buildBusinessTimeContextFromReviewValues\(values = \{\}\)/)
|
||||
assert.match(reviewPanelModelScript, /function buildReviewFormContextFromPayload\(reviewPayload, inlineState = null\)/)
|
||||
assert.match(reviewPanelModelScript, /function buildBusinessTimeContextFromReviewValues\(values = \{\}\)/)
|
||||
assert.match(
|
||||
createViewScript,
|
||||
/resolvedUploadDisposition === 'continue_existing'[\s\S]*buildReviewFormContextFromPayload\([\s\S]*activeReviewPayload\.value[\s\S]*reviewInlineForm\.value[\s\S]*extraContext\.review_form_values/s
|
||||
|
||||
Reference in New Issue
Block a user