feat(web): 申请单预览编辑器增强与报销流程细节适配

- useApplicationPreviewEditor 扩展字段编辑与校验,useTravelReimbursementApplicationPreviewDateEditor 微调日期处理
- travelReimbursementExpenseQueryModel/reimbursements 服务/expenseApplicationPreview 适配工号/邮箱字段与关联动作
- useWorkbenchAiApplicationPreviewFlow/usePersonalWorkbenchAiMode 接入关联门控后的预览流转
- TravelReimbursementCreateView 调整入口,TravelReimbursementMessageItem 适配
- 新增 expense-application-fast-preview 测试,更新 attachment-association-confirmation、review-drawer-switch 测试
This commit is contained in:
caoxiaozhu
2026-06-22 15:56:06 +08:00
parent ba444a514f
commit ded8b39ccb
12 changed files with 468 additions and 43 deletions

View File

@@ -117,8 +117,12 @@ test('attachment upload association uses conversation selection instead of legac
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
'utf8'
)
const flowToolSource = readFileSync(
fileURLToPath(new URL('../src/views/scripts/travelReimbursementFlowToolModel.js', import.meta.url)),
'utf8'
)
const conversationSource = readFileSync(
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationModel.js', import.meta.url)),
fileURLToPath(new URL('../src/views/scripts/travelReimbursementConversationSessionModel.js', import.meta.url)),
'utf8'
)
@@ -140,7 +144,7 @@ test('attachment upload association uses conversation selection instead of legac
assert.match(submitComposerSource, /const appendToCurrentFlow = Boolean\(options\.appendToCurrentFlow\)/)
assert.match(submitComposerSource, /if \(!appendToCurrentFlow\) \{\s*resetFlowRun\(\)\s*\} else \{\s*clearFlowSimulationTimers\(\)/)
assert.match(flowSource, /link_to_existing_draft:\s*\{[\s\S]*key:\s*'attachment-association'/)
assert.match(flowSource, /responseMessage\.includes\('关联'\)[\s\S]*key:\s*'attachment-association'/)
assert.match(flowToolSource, /responseMessage\.includes\('关联'\)[\s\S]*key:\s*'attachment-association'/)
assert.match(flowSource, /'draft-risk-review'/)
assert.match(flowSource, /草稿风险识别/)
assert.match(conversationSource, /'attachment-association':\s*\{[\s\S]*title:\s*'票据关联草稿'/)

View File

@@ -436,6 +436,7 @@ test('application preview uses selected date range and business-specific time la
assert.equal(rows.find((row) => row.key === 'time')?.value, '2026-02-20')
assert.equal(rows.find((row) => row.key === 'time_return')?.label, '返回时间')
assert.equal(rows.find((row) => row.key === 'time_return')?.value, '2026-02-23')
assert.equal(rows.find((row) => row.key === 'time_return')?.editable, true)
assert.match(submitText, /出发时间2026-02-20/)
assert.match(submitText, /返回时间2026-02-23/)
assert.match(submitText, /事由:支撑国网仿生产环境部署/)
@@ -1610,6 +1611,52 @@ test('application preview calculates base policy estimate when transport mode is
assert.equal(staleEstimatePreview.fields.amount, '1,400元不含交通')
})
test('application preview estimate infers days from completed date range', () => {
const currentUser = { name: '\u674e\u6587\u9759', grade: 'P5', location: '\u6b66\u6c49' }
const preview = normalizeApplicationPreview({
fields: {
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
time: '2026-06-23 \u81f3 2026-06-25',
location: '\u5317\u4eac',
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
days: '',
transportMode: '',
grade: 'P5'
}
})
const request = buildApplicationPolicyEstimateRequest(preview, currentUser)
assert.equal(request.canCalculate, true)
assert.deepEqual(request.payload, {
days: 3,
location: '\u5317\u4eac',
grade: 'P5',
transport_mode: null,
origin_location: '\u6b66\u6c49',
travel_date: '2026-06-23'
})
const estimatedPreview = applyApplicationPolicyEstimateResult(preview, {
days: 3,
location: '\u5317\u4eac',
matched_city: '\u5317\u4eac',
grade: 'P5',
hotel_rate: 450,
hotel_amount: 1350,
total_allowance_rate: 100,
allowance_amount: 300,
total_amount: 1650,
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
rule_version: 'v1.0.0'
}, currentUser)
assert.equal(estimatedPreview.fields.days, '3\u5929')
assert.equal(estimatedPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
assert.equal(estimatedPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
assert.equal(estimatedPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
assert.match(estimatedPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
})
test('application preview editor refreshes transport estimate after mode change', async () => {
const preview = applyApplicationPolicyEstimateResult(
buildLocalApplicationPreview('申请 2026-05-25 至 2026-05-27 去上海出差3天服务项目部署', {
@@ -1726,3 +1773,197 @@ test('application preview editor recalculates days and subsidy after date range
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
assert.match(message.applicationPreview.fields.policyEstimate, /\u8865\u8d34 400\u5143/)
})
test('application preview editor can edit return date from table row', async () => {
const preview = normalizeApplicationPreview({
fields: {
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
time: '2026-02-20 \u81f3 2026-02-23',
location: '\u4e0a\u6d77',
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
days: '4\u5929',
transportMode: '\u706b\u8f66',
amount: '',
grade: 'P5',
applicant: '\u674e\u6587\u9759',
department: '\u6280\u672f\u90e8',
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
managerName: '\u5411\u4e07\u7ea2'
}
})
const message = {
id: 'application-preview-editor-return-date-message',
applicationPreview: preview,
text: ''
}
const requestedPayloads = []
const editor = useApplicationPreviewEditor({
persistSessionState: () => {},
toast: () => {},
currentUser: ref({ grade: 'P5' }),
calculateTravelReimbursement: async (payload) => {
requestedPayloads.push(payload)
return {
days: payload.days,
location: payload.location,
matched_city: payload.location,
grade: payload.grade,
hotel_rate: 450,
hotel_amount: 2250,
total_allowance_rate: 100,
allowance_amount: 500,
total_amount: 2750,
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
rule_version: 'v1.0.0'
}
}
})
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
assert.equal(editor.resolveApplicationPreviewEditorControl('time_return'), 'date')
assert.equal(editor.applicationPreviewEditor.value.dateMode, 'range')
assert.equal(editor.applicationPreviewEditor.value.rangeStartDate, '2026-02-20')
editor.applicationPreviewEditor.value.rangeEndDate = '2026-02-24'
const committed = await editor.commitApplicationPreviewDateEditor(message)
assert.equal(committed, true)
assert.deepEqual(requestedPayloads.at(-1), {
days: 5,
location: '\u4e0a\u6d77',
grade: 'P5',
transport_mode: '\u706b\u8f66',
origin_location: null,
travel_date: '2026-02-20'
})
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
assert.equal(message.applicationPreview.fields.days, '5\u5929')
})
test('application preview editor can edit return date from inline table input', async () => {
const preview = normalizeApplicationPreview({
fields: {
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
time: '2026-02-20 \u81f3 2026-02-23',
location: '\u4e0a\u6d77',
reason: '\u670d\u52a1\u9879\u76ee\u90e8\u7f72',
days: '4\u5929',
transportMode: '\u706b\u8f66',
amount: '',
grade: 'P5',
applicant: '\u674e\u6587\u9759',
department: '\u6280\u672f\u90e8',
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
managerName: '\u5411\u4e07\u7ea2'
}
})
const message = {
id: 'application-preview-editor-inline-return-date-message',
applicationPreview: preview,
text: ''
}
const requestedPayloads = []
const editor = useApplicationPreviewEditor({
persistSessionState: () => {},
toast: () => {},
currentUser: ref({ grade: 'P5' }),
calculateTravelReimbursement: async (payload) => {
requestedPayloads.push(payload)
return {
days: payload.days,
location: payload.location,
matched_city: payload.location,
grade: payload.grade,
hotel_rate: 450,
hotel_amount: 2250,
total_allowance_rate: 100,
allowance_amount: 500,
total_amount: 2750,
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
rule_version: 'v1.0.0'
}
}
})
editor.openApplicationPreviewEditor(message, 'time_return', '2026-02-23')
editor.applicationPreviewEditor.value.draftValue = '2026-02-24'
const committed = await editor.commitApplicationPreviewEditor(message)
assert.equal(committed, true)
assert.deepEqual(requestedPayloads.at(-1), {
days: 5,
location: '\u4e0a\u6d77',
grade: 'P5',
transport_mode: '\u706b\u8f66',
origin_location: null,
travel_date: '2026-02-20'
})
assert.equal(message.applicationPreview.fields.time, '2026-02-20 \u81f3 2026-02-24')
assert.equal(message.applicationPreview.fields.time_return, undefined)
assert.equal(message.applicationPreview.fields.days, '5\u5929')
})
test('application preview editor estimates after shorthand return date input', async () => {
const preview = normalizeApplicationPreview({
fields: {
applicationType: '\u5dee\u65c5\u8d39\u7528\u7533\u8bf7',
time: '2026-06-23',
location: '\u5317\u4eac',
reason: '\u652f\u6491\u5ba2\u6237\u73b0\u573a\u5b9e\u65bd',
days: '',
transportMode: '',
amount: '',
grade: 'P5',
applicant: '\u674e\u6587\u9759',
department: '\u6280\u672f\u90e8',
position: '\u8d22\u52a1\u667a\u80fd\u5316\u4ea7\u54c1\u7ecf\u7406',
managerName: '\u5411\u4e07\u7ea2'
}
})
const message = {
id: 'application-preview-editor-shorthand-return-date-message',
applicationPreview: preview,
text: ''
}
const requestedPayloads = []
const editor = useApplicationPreviewEditor({
persistSessionState: () => {},
toast: () => {},
currentUser: ref({ grade: 'P5', location: '\u6b66\u6c49' }),
calculateTravelReimbursement: async (payload) => {
requestedPayloads.push(payload)
return {
days: payload.days,
location: payload.location,
matched_city: payload.location,
grade: payload.grade,
hotel_rate: 450,
hotel_amount: 1350,
total_allowance_rate: 100,
allowance_amount: 300,
total_amount: 1650,
rule_name: '\u516c\u53f8\u5dee\u65c5\u8d39\u62a5\u9500\u89c4\u5219',
rule_version: 'v1.0.0'
}
}
})
editor.openApplicationPreviewEditor(message, 'time_return', '\u5f85\u8865\u5145')
editor.applicationPreviewEditor.value.draftValue = '6\u670825\u65e5'
const committed = await editor.commitApplicationPreviewEditor(message)
assert.equal(committed, true)
assert.deepEqual(requestedPayloads.at(-1), {
days: 3,
location: '\u5317\u4eac',
grade: 'P5',
transport_mode: null,
origin_location: '\u6b66\u6c49',
travel_date: '2026-06-23'
})
assert.equal(message.applicationPreview.fields.time, '2026-06-23 \u81f3 2026-06-25')
assert.equal(message.applicationPreview.fields.days, '3\u5929')
assert.equal(message.applicationPreview.fields.lodgingDailyCap, '450\u5143/\u5929')
assert.equal(message.applicationPreview.fields.subsidyDailyCap, '100\u5143/\u5929')
assert.equal(message.applicationPreview.fields.amount, '1,650\u5143\uff08\u4e0d\u542b\u4ea4\u901a\uff09')
assert.match(message.applicationPreview.fields.policyEstimate, /\u4ea4\u901a\u5f85\u8865\u5145/)
})

View File

@@ -396,7 +396,7 @@ test('submit composer scopes the side panel to intent overview, document upload,
test('expense query answers keep one clear result structure with document center jump link', () => {
assert.doesNotMatch(createViewTemplateSurface, /message\.meta\?\.length/)
assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.suggestedActions\?\.length/)
assert.match(createViewTemplateSurface, /!message\.reviewPayload && \(!message\.queryPayload \|\| message\.queryPayload\.selectionMode === 'reimbursement_application_association'\) && message\.suggestedActions\?\.length/)
assert.match(createViewTemplateSurface, /!message\.reviewPayload && !message\.queryPayload && message\.citations\?\.length/)
assert.match(createViewTemplateSurface, /message\.queryPayload\.title \|\| \(message\.queryPayload\.selectionMode === 'draft_association' \? '选择关联草稿' : '最近 5 条筛选结果'\)/)
assert.match(createViewTemplateSurface, /v-html="ui\.renderMarkdown\(ui\.buildExpenseQueryHint\(message\.queryPayload\)\)"/)