feat(web): AI 工作台文件预览/附件关联任务与草稿分支

- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览
- 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示
- 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿
- PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善
- DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配
- 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
This commit is contained in:
caoxiaozhu
2026-06-24 10:42:50 +08:00
parent 0264a4b5b4
commit ee730aa31c
73 changed files with 2528 additions and 379 deletions

View File

@@ -244,9 +244,10 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeSurface, /buildFileIdentity,[\s\S]*collectReceiptFiles[\s\S]*travelReimbursementAttachmentModel\.js/)
assert.match(aiModeSurface, /MAX_ATTACHMENTS,[\s\S]*mergeFilesWithLimit[\s\S]*travelReimbursementAttachmentModel\.js/)
assert.match(aiModeSurface, /import \* as aiAttachmentAssociationModel from '\.\.\/\.\.\/utils\/aiAttachmentAssociationModel\.js'/)
assert.match(aiModeSurface, /aiAttachmentAssociationModel\.resolveAiAttachmentAssociationMatch/)
assert.match(aiModeSurface, /aiAttachmentAssociationModel\.buildAiAttachmentAssociationResultMessage/)
assert.match(aiModeSurface, /syncExpenseClaimFilesToDraft/)
assert.match(aiModeSurface, /createAttachmentAssociationJob[\s\S]*services\/attachmentAssociationJobs\.js/)
assert.match(aiModeSurface, /fetchAttachmentAssociationJob[\s\S]*services\/attachmentAssociationJobs\.js/)
assert.match(aiModeSurface, /import \{ recognizeOcrFiles \} from '\.\.\/\.\.\/services\/ocr\.js'/)
assert.match(aiModeSurface, /const AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION = 'confirm_ai_attachment_association'/)
assert.match(aiModeSurface, /const AI_ATTACHMENT_OCR_DETAIL_ACTION = 'show_ai_attachment_ocr_details'/)
@@ -269,10 +270,16 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeSurface, /resolveAiAttachmentAssociationClaimNo\(actionPayload\)/)
assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_OCR_DETAIL_ACTION\)/)
assert.match(aiModeSurface, /const collected = await collectAiModeReceiptContext\(files\)/)
assert.match(aiModeSurface, /const claims = extractExpenseClaimItems\(claimsPayload\)/)
assert.match(aiModeSurface, /aiAttachmentAssociationModel\.resolveAiAttachmentAssociationMatch\(claims, collected\.ocrDocuments\)/)
assert.match(aiModeSurface, /aiAttachmentAssociationRuntime\.set\(associationId/)
assert.match(aiModeSurface, /attachmentOcrDetails,\s*[\s\S]*includeOcrDetails: Boolean\(attachmentOcrDetails\)/)
assert.match(aiModeSurface, /function extractReceiptIdsFromOcrDocuments\(documents = \[\]\)/)
assert.match(aiModeSurface, /const receiptIds = attachmentJobFlow\.extractReceiptIdsFromOcrDocuments\(collected\.ocrDocuments\)/)
assert.match(aiModeSurface, /await createAttachmentAssociationJob\(\{[\s\S]*receipt_ids: receiptIds,[\s\S]*conversation_id: conversationId\?\.value/)
assert.match(aiModeSurface, /attachmentAssociationJob: job/)
assert.match(aiModeSurface, /async function pollJob\(/)
assert.match(aiModeSurface, /fetchAttachmentAssociationJob\(normalizedJobId\)/)
assert.match(aiModeSurface, /function resumePendingJobs\(\)/)
assert.match(aiModeSurface, /resumePendingAiAttachmentAssociationJobs: attachmentJobFlow\.resumePendingJobs/)
assert.match(aiModeSurface, /attachmentFlow\.resumePendingAiAttachmentAssociationJobs\(\)/)
assert.match(aiModeSurface, /attachmentAssociationJob: normalizeInlineAttachmentAssociationJob/)
assert.match(aiModeSurface, /async function confirmAiAttachmentAssociation\(actionPayload = \{\}, sourceMessage = null\)/)
assert.match(aiModeSurface, /syncExpenseClaimFilesToDraft\(\{[\s\S]*fetchExpenseClaimDetail,[\s\S]*createExpenseClaimItem,[\s\S]*uploadExpenseClaimItemAttachment/)
assert.match(aiModeSurface, /if \(actionType === AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION\)/)
@@ -388,7 +395,8 @@ test('AI mode screen follows the approved reference structure', () => {
assert.match(aiModeSurface, /const hasServerStreamedContent = Boolean\(String\(pendingMessage\.content \|\| ''\)\.trim\(\)\)/)
assert.match(aiModeSurface, /if \(!hasServerStreamedContent\) \{[\s\S]*await streamInlineAssistantContent\(pendingMessage\.id, finalMessageText\)[\s\S]*\}/)
assert.match(aiModeSurface, /if \(actionType === AI_APPLICATION_ACTION_SUBMIT\) \{[\s\S]*buildInlineApplicationResultTable\(draftPayload/)
assert.match(aiModeSurface, /需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。/)
assert.match(aiModeSurface, /\| 单据类型 \| 单据编号 \| 单据状态 \| 当前节点 \| 日期 \| 地点 \| 事由 \| 金额 \|/)
assert.doesNotMatch(aiModeSurface, /需要查看完整详情时,请点击卡片“操作”行的“查看”进入单据详情。/)
assert.match(aiModeSurface, /function buildInlineApplicationActionFailureText\(error, isSubmit\)/)
assert.match(aiModeSurface, /我已保留当前申请核对表/)
assert.match(aiModeSurface, /applicationPreview:\s*targetMessage\.applicationPreview/)
@@ -549,12 +557,25 @@ test('AI mode normal assistant requests include OCR context for uploaded receipt
assert.match(aiModeSurface, /const aiModeReceiptContextCache = new Map\(\)/)
assert.match(aiModeSurface, /const aiModeReceiptRecognitionState = reactive\(\{\}\)/)
assert.match(aiModeSurface, /function resolveAiModeReceiptRecognitionState\(file\)/)
assert.match(aiModeSurface, /function hasPendingAiModeReceiptRecognition\(files = \[\]\)/)
assert.match(aiModeSurface, /function hasFailedAiModeReceiptRecognition\(files = \[\]\)/)
assert.match(aiModeSurface, /resolveAiModeReceiptRecognitionState\(selectedFiles\.value\[index\]\)/)
assert.match(aiModeSurface, /status:\s*'recognizing'[\s\S]*label:\s*'智能录入识别中'/)
assert.match(aiModeSurface, /status:\s*'recognized'[\s\S]*label:\s*detail \? `已识别票据/)
assert.match(aiModeSurface, /status:\s*'recognized'[\s\S]*label:\s*detail \? `当前会话已识别/)
assert.match(aiModeSurface, /本状态不代表票据夹已有记录/)
assert.match(aiModeSurface, /status:\s*'failed'[\s\S]*label:\s*'识别失败'/)
assert.match(aiModeSurface, /const isAiModeReceiptRecognitionPending = computed\(\(\) => attachmentFlow\.hasPendingAiModeReceiptRecognition\(selectedFiles\.value\)\)/)
assert.match(aiModeSurface, /const hasAiModeReceiptRecognitionFailure = computed\(\(\) => attachmentFlow\.hasFailedAiModeReceiptRecognition\(selectedFiles\.value\)\)/)
assert.match(aiModeSurface, /const isAiModeInputLocked = computed\(\(\) => applicationPreviewEstimatePending\.value \|\| isAiModeReceiptRecognitionPending\.value\)/)
assert.match(aiModeSurface, /!hasAiModeReceiptRecognitionFailure\.value[\s\S]*Boolean\(assistantDraft\.value\.trim\(\)\)/)
assert.match(aiModeSurface, /function resolveAiModeInputLockMessage\(\) \{[\s\S]*附件识别中,请稍等/)
assert.match(aiModeSurface, /hasAiModeReceiptRecognitionFailure\.value[\s\S]*请先移除识别失败的附件或重新上传/)
assert.match(aiModeSurface, /:placeholder="runtime\.isAiModeInputLocked \? runtime\.aiModeInputLockMessage : placeholder"/)
assert.match(aiModeSurface, /function primeAiModeReceiptContext\(files = \[\]\)/)
assert.match(aiModeSurface, /function startAiModeReceiptRecognition\(files = \[\]\)/)
assert.match(aiModeSurface, /function startAiModeReceiptRecognition\(files = \[\], options = \{\}\)/)
assert.match(aiModeSurface, /const forceRefresh = Boolean\(options\.forceRefresh\)/)
assert.match(aiModeSurface, /if \(!forceRefresh && cached\?\.status === 'resolved'\) \{/)
assert.match(aiModeSurface, /startAiModeReceiptRecognition\(files, \{ forceRefresh: true \}\)/)
assert.match(aiModeSurface, /function buildAiModeReceiptContextCacheKey\(ocrFiles = \[\]\)/)
assert.match(aiModeSurface, /applyAiModeReceiptRecognitionResult\(ocrFiles, context\)/)
assert.match(aiModeSurface, /buildFileIdentity\(file\)/)