feat: 优化差旅报销预审流程与个人工作台 UI 体系

- 完善 user_agent_application 申请差旅报销预审槽位与消息组装
- 增强预算助理报告与风险建议卡片交互
- 重构登录页视觉样式与移动端响应式适配
- 优化个人工作台、文档中心、政策中心、员工管理等页面布局
- 拆分 travelRequestDetailPreReviewModel 为 advice/submit 模型
- 补充报销草稿、风险复核、Item Sync 与模板执行器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-02 14:01:51 +08:00
parent 92444e7eae
commit ca691f3ee0
107 changed files with 5663 additions and 1542 deletions

View File

@@ -22,10 +22,22 @@ const createViewScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/TravelReimbursementCreateView.js', import.meta.url)),
'utf8'
)
const messageItemTemplate = readFileSync(
fileURLToPath(new URL('../src/components/travel/TravelReimbursementMessageItem.vue', import.meta.url)),
'utf8'
)
const insightPanelTemplate = readFileSync(
fileURLToPath(new URL('../src/components/travel/TravelReimbursementInsightPanel.vue', import.meta.url)),
'utf8'
)
const reimbursementService = readFileSync(
fileURLToPath(new URL('../src/services/reimbursements.js', import.meta.url)),
'utf8'
)
const reimbursementFlowScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementFlow.js', import.meta.url)),
'utf8'
)
const reviewActionsScript = readFileSync(
fileURLToPath(new URL('../src/views/scripts/useTravelReimbursementReviewActions.js', import.meta.url)),
'utf8'
@@ -62,6 +74,10 @@ const createViewPart4Styles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/views/travel-reimbursement-create-view-part4.css', import.meta.url)),
'utf8'
)
const insightPanelStyles = readFileSync(
fileURLToPath(new URL('../src/assets/styles/components/travel-reimbursement-insight-panel.css', import.meta.url)),
'utf8'
)
test('review drawer tools expose the default review tab before conditional document and risk tabs', () => {
assert.match(createViewTemplate, /v-if="activeReviewPayload && reviewOverviewDrawerAvailable"[\s\S]*title="报销识别核对"[\s\S]*@click="switchToReviewOverviewDrawer"/)
@@ -101,6 +117,14 @@ test('document review drawer fills sidebar height and preview dialog is centered
assert.match(createViewPart4Styles, /\.review-preview-modal\s*\{[\s\S]*margin:\s*auto;[\s\S]*flex:\s*none;/)
})
test('document review OCR result card header keeps copy and navigation separated', () => {
assert.match(insightPanelTemplate, /class="review-side-head-copy"[\s\S]*票据识别结果卡片[\s\S]*逐张查看 OCR 结果/)
assert.match(insightPanelStyles, /\.review-document-switch-head\s*\{[\s\S]*display:\s*grid;[\s\S]*grid-template-columns:\s*minmax\(0,\s*1fr\) auto;/)
assert.match(insightPanelStyles, /\.review-side-head-copy\s*\{[\s\S]*min-width:\s*0;[\s\S]*display:\s*grid;/)
assert.match(insightPanelStyles, /\.review-side-head-copy p\s*\{[\s\S]*overflow-wrap:\s*anywhere;/)
assert.match(insightPanelStyles, /\.review-document-nav\s*\{[\s\S]*flex:\s*0 0 auto;[\s\S]*flex-wrap:\s*nowrap;[\s\S]*white-space:\s*nowrap;/)
})
test('document preview avoids restored stale object urls', () => {
assert.match(createViewTemplate, /v-if="documentPreviewDialog\.kind === 'image'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
assert.match(createViewTemplate, /v-else-if="documentPreviewDialog\.kind === 'pdf'"[\s\S]*:key="documentPreviewDialog\.renderKey"/)
@@ -406,6 +430,15 @@ test('review drawer save action is disabled while receipt recognition is submitt
)
})
test('flow run detail refresh has timeout so composer submit is not held open', () => {
assert.match(reimbursementFlowScript, /FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS\s*=\s*3000/)
assert.match(
reimbursementFlowScript,
/await Promise\.race\(\[[\s\S]*fetchAgentRunDetail\(flowRunId\.value\)[\s\S]*globalThis\.setTimeout\(\(\) => resolve\(null\), FLOW_RUN_DETAIL_REFRESH_TIMEOUT_MS\)/
)
assert.match(reimbursementFlowScript, /if \(!run\) \{\s*return null\s*}/)
})
test('draft creation keeps detail-scoped attachment persistence alive before close', () => {
assert.match(
submitComposerScript,
@@ -529,11 +562,29 @@ test('saved draft review messages stop showing the save-draft prompt', () => {
}
const followup = buildReviewPlainFollowupCopy(reviewPayload, { savedDraft: true })
assert.equal(followup.lead, '补充信息')
assert.match(followup.summary, /草稿/)
assert.match(followup.summary, /关联|补充|提交/)
assert.equal(followup.lead, '后续处理')
assert.match(followup.summary, /自动检测/)
assert.match(followup.summary, /继续上传/)
assert.equal(followup.items.length, 0)
assert.doesNotMatch(followup.summary, /当前草稿待完善|必须/)
assert.doesNotMatch(followup.summary, /点击|点“草稿”|保存为草稿|临时保存|暂存/)
assert.match(createViewTemplate, /buildReviewPlainFollowupForMessage\(message\)/)
assert.match(messageItemTemplate, /buildReviewPlainFollowupForMessage\(message\)/)
assert.match(createViewScript, /function isDraftSavedReviewMessage\(message\)/)
assert.match(createViewScript, /function canUseInlineSaveDraft\(message\)[\s\S]*isDraftSavedReviewMessage\(message\)/)
})
test('guided save draft emits refresh and exposes reimbursement draft detail card', () => {
assert.match(
createViewScript,
/emitDraftSaved:\s*\(payload\)\s*=>\s*emit\('draft-saved', payload\)/
)
assert.match(submitComposerScript, /function emitSavedDraftRefresh\(draftPayload\)/)
assert.match(
submitComposerScript,
/emitSavedDraftRefresh\(payload\?\.result\?\.draft_payload \|\| null\)/
)
assert.match(createViewScript, /function shouldShowDraftSavedCard\(message\)/)
assert.match(createViewScript, /function resolveReimbursementDraftClaimNo\(draftPayload\)/)
assert.doesNotMatch(createViewScript, /function buildReimbursementDraftSummaryItems\(draftPayload\)/)
assert.match(messageItemTemplate, /class="reimbursement-draft-link"/)
})