feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
buildLocallySyncedReviewPayload,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
isTravelReviewPayload,
|
||||
resolveReviewFooterActions
|
||||
} from '../src/views/scripts/travelReimbursementReviewModel.js'
|
||||
import { useTravelReimbursementAttachments } from '../src/views/scripts/useTravelReimbursementAttachments.js'
|
||||
import { renderMarkdown } from '../src/utils/markdown.js'
|
||||
|
||||
const createViewTemplate = readFileSync(
|
||||
@@ -407,7 +409,7 @@ test('review drawer save action is disabled while receipt recognition is submitt
|
||||
test('draft creation starts composer attachment persistence after response rendering', () => {
|
||||
assert.match(
|
||||
submitComposerScript,
|
||||
/void syncComposerFilesToDraft\(resolvedDraftClaimId, files\)\s*\.then\(\(\) => \{\s*persistSessionState\(\)\s*\}\)\s*\.catch\(\(error\) => \{/s
|
||||
/void syncComposerFilesToDraft\(resolvedDraftClaimId, files\)\s*\.then\(\(syncResult\) => \{[\s\S]*persistSessionState\(\)[\s\S]*emitRequestUpdated\?\.\(\{/s
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
submitComposerScript,
|
||||
@@ -422,10 +424,84 @@ test('draft creation starts composer attachment persistence after response rende
|
||||
assert.match(attachmentsScript, /const normalizedMatchBuckets = new Map\(\)/)
|
||||
assert.match(
|
||||
attachmentsScript,
|
||||
/const targetItem = nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch/
|
||||
/nextExactMatch \|\| nextNormalizedMatch \|\| fallbackMatch \|\| emptyFallbackMatch/
|
||||
)
|
||||
})
|
||||
|
||||
test('detail smart-entry receipt sync uploads files to existing empty items and creates a row when needed', async () => {
|
||||
const uploadCalls = []
|
||||
let createCount = 0
|
||||
const claimSnapshots = [
|
||||
{
|
||||
items: [
|
||||
{ id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
|
||||
{ id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
|
||||
]
|
||||
},
|
||||
{
|
||||
items: [
|
||||
{ id: 'item-empty-1', item_type: 'hotel_ticket', invoice_id: '' },
|
||||
{ id: 'item-persisted', item_type: 'train_ticket', invoice_id: 'claim-1/item-persisted/old.pdf' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const attachments = useTravelReimbursementAttachments({
|
||||
isKnowledgeSession: ref(false),
|
||||
reviewFilePreviews: ref([]),
|
||||
linkedRequest: ref({}),
|
||||
draftClaimId: ref('claim-1'),
|
||||
activeReviewPayload: ref(null),
|
||||
reviewInlinePendingFiles: ref([]),
|
||||
reviewInlineForm: ref({}),
|
||||
reviewInlineEditorKey: ref(''),
|
||||
composerUploadIntent: ref(''),
|
||||
submitting: ref(false),
|
||||
reviewActionBusy: ref(false),
|
||||
toast: () => {},
|
||||
fileInputRef: ref(null),
|
||||
createExpenseClaimItem: async () => {
|
||||
createCount += 1
|
||||
return {
|
||||
items: [
|
||||
{ id: 'item-created-1', item_type: 'taxi_receipt', invoice_id: '' }
|
||||
]
|
||||
}
|
||||
},
|
||||
fetchExpenseClaimDetail: async () => claimSnapshots.shift() || { items: [] },
|
||||
fetchExpenseClaimItemAttachmentMeta: async () => null,
|
||||
fetchExpenseClaimAttachmentAsset: async () => new Blob(['preview']),
|
||||
uploadExpenseClaimItemAttachment: async (claimId, itemId, file) => {
|
||||
uploadCalls.push({ claimId, itemId, fileName: file.name })
|
||||
return {}
|
||||
},
|
||||
extractReviewAttachmentNames: () => [],
|
||||
mergeFilesWithLimit: (existing, incoming) => ({ files: [...existing, ...incoming], overflowCount: 0 }),
|
||||
mergeFilePreviews: (existing, incoming) => [...existing, ...incoming],
|
||||
isTemporaryPreviewUrl: () => false,
|
||||
resolveAttachmentPreviewKind: () => '',
|
||||
resolveDocumentPreview: () => null,
|
||||
buildFilePreviews: () => [],
|
||||
buildFileIdentity: (file) => file.name,
|
||||
MAX_ATTACHMENTS: 5,
|
||||
VISIBLE_ATTACHMENT_CHIPS: 3,
|
||||
clearInlineReviewFieldError: () => {}
|
||||
})
|
||||
|
||||
const result = await attachments.syncComposerFilesToDraft('claim-1', [
|
||||
{ name: 'hotel.pdf' },
|
||||
{ name: 'taxi.pdf' }
|
||||
])
|
||||
|
||||
assert.deepEqual(uploadCalls, [
|
||||
{ claimId: 'claim-1', itemId: 'item-empty-1', fileName: 'hotel.pdf' },
|
||||
{ claimId: 'claim-1', itemId: 'item-created-1', fileName: 'taxi.pdf' }
|
||||
])
|
||||
assert.equal(createCount, 1)
|
||||
assert.equal(result.uploadedCount, 2)
|
||||
assert.equal(result.skippedCount, 0)
|
||||
})
|
||||
|
||||
test('review summary renders markdown and save draft relies on backend response only', () => {
|
||||
assert.match(
|
||||
createViewTemplate,
|
||||
|
||||
Reference in New Issue
Block a user