refactor(frontend): split large reimbursement and audit modules

This commit is contained in:
caoxiaozhu
2026-05-21 23:53:03 +08:00
parent 2908dda024
commit f6f787ff38
53 changed files with 15637 additions and 14179 deletions

View File

@@ -6,8 +6,14 @@ import { fileURLToPath } from 'node:url'
import {
buildAiAdviceViewModel,
buildAttachmentInsightViewModel,
buildAttachmentRiskCards
buildAttachmentRiskCards,
extractRiskTagsFromText,
resolveRiskTags,
resolveRiskTagTone
} from '../src/views/scripts/travelRequestDetailInsights.js'
import {
buildDraftBlockingIssues
} from '../src/views/scripts/travelRequestDetailExpenseModel.js'
const detailViewTemplate = readFileSync(
fileURLToPath(new URL('../src/views/TravelRequestDetailView.vue', import.meta.url)),
@@ -101,6 +107,24 @@ test('AI advice card splits every attachment risk point with basis and suggestio
assert.ok(advice.riskCards.every((card) => card.suggestion.includes('费用项目调整为交通费')))
})
test('risk cards carry severity and domain tags for statistics', () => {
const hotelRisk = {
tone: 'high',
title: '住宿超标待说明',
risk: '住宿标准:北京酒店 800 元/晚超出报销标准。'
}
const trafficRisk = {
tone: 'medium',
title: '交通票据提醒',
risk: '火车票说明格式待调整。'
}
assert.deepEqual(resolveRiskTags(hotelRisk), ['#high_risk', '#hotel'])
assert.deepEqual(resolveRiskTags(trafficRisk), ['#middle_risk', '#traffic'])
assert.equal(resolveRiskTagTone('#hotel'), 'hotel')
assert.deepEqual(extractRiskTagsFromText('超标说明:#high_risk #hotel 原因'), ['#high_risk', '#hotel'])
})
test('AI advice splits claim attachment risk flags into specific points', () => {
const riskCards = buildAttachmentRiskCards({
claimRiskFlags: [
@@ -196,13 +220,23 @@ test('AI advice template renders grouped section titles with completion before r
test('AI advice risk section uses compact card styling hooks', () => {
assert.match(detailViewTemplate, /class="\['risk-advice-card', card\.tone\]"/)
assert.match(detailViewTemplate, /v-if="card\.tags\?\.length" class="risk-card-tag-list"/)
assert.match(detailViewStyle, /\.validation-card \{\s*border: 1px solid #e5e7eb;/)
assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-card \{\s*display: grid;\s*gap: 8px;\s*padding: 12px 12px 11px;/)
assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-card\.low/)
assert.match(detailViewStyle, /\.risk-advice-card\.low/)
assert.match(detailViewStyle, /\.risk-note-tag\.high/)
assert.match(detailViewStyle, /\.risk-note-tag\.hotel/)
assert.match(detailViewStyle, /\.validation-section--risk \.risk-advice-meta ul,\s*\.validation-section--risk \.risk-advice-meta p \{\s*margin: 0;/)
})
test('expense rows show a major-risk warning icon before time', () => {
assert.match(detailViewTemplate, /'has-major-risk': isMajorExpenseRisk\(item\)/)
assert.match(detailViewTemplate, /class="mdi mdi-alert expense-risk-indicator"/)
assert.match(detailViewStyle, /\.expense-risk-indicator \{/)
assert.match(detailViewScript, /function isMajorExpenseRisk\(item\)/)
})
test('AI advice shows only the latest manual return while preserving return count context', () => {
const riskCards = buildAttachmentRiskCards({
claimRiskFlags: [
@@ -238,6 +272,10 @@ test('AI advice shows only the latest manual return while preserving return coun
test('expense attachment actions keep preview as the only recognition entry point', () => {
assert.match(detailViewTemplate, /:aria-label="resolveAttachmentPreviewTitle\(item\)"/)
assert.match(detailViewScript, /return fileName \? `预览附件:\$\{fileName\}` : '预览附件'/)
assert.match(detailViewScript, /\.filter\(\(item\) => canPreviewAttachment\(item\)\)/)
assert.match(detailViewScript, /function hasStoredAttachmentReference\(item\) \{[\s\S]*return String\(item\?\.invoiceId \|\| ''\)\.includes\('\/'\)/)
assert.match(detailViewScript, /if \(metadata\) \{[\s\S]*return metadata\.previewable !== false[\s\S]*return true/)
assert.match(detailViewScript, /原件尚未保存到单据中,请重新上传后预览/)
assert.doesNotMatch(detailViewTemplate, /aria-label="识别附件"/)
assert.doesNotMatch(detailViewTemplate, /点击识别按钮/)
assert.doesNotMatch(detailViewScript, /recognizeExpenseAttachment/)
@@ -358,6 +396,8 @@ test('travel detail AI advice adds low risk reminders for optional receipts', ()
test('expense detail save is blocked while attachment recognition is running', () => {
assert.match(detailViewScript, /const uploadingExpenseId = ref\(''\)/)
assert.match(detailViewScript, /const actionBusy = computed\(\(\) =>[\s\S]*Boolean\(uploadingExpenseId\.value\)/)
assert.match(detailViewScript, /const canSubmit = computed\(\(\) => isEditableRequest\.value && !actionBusy\.value\)/)
assert.match(detailViewScript, /if \(draftBlockingIssues\.value\.length\) \{[\s\S]*请先补全草稿信息,再提交审批。/)
assert.match(
detailViewTemplate,
/@click="saveExpenseEdit\(item\)"[\s\S]*:disabled="actionBusy"/
@@ -368,6 +408,36 @@ test('expense detail save is blocked while attachment recognition is running', (
)
})
test('draft submit validation uses expense detail date and amount when claim summary is stale', () => {
const issues = buildDraftBlockingIssues(
{
profileName: '张三',
typeLabel: '待补充',
typeCode: 'office',
reason: '待补充',
location: '待补充',
occurredDisplay: '待补充',
amountValue: 0
},
[
{
id: 'item-1',
itemDate: '2026-05-21',
itemType: 'office',
itemReason: '采购办公用品',
itemLocation: '',
itemAmount: 88,
invoiceId: 'claim-1/item-1/office-note.png'
}
]
)
assert.ok(!issues.some((issue) => issue.includes('发生时间未完善')))
assert.ok(!issues.some((issue) => issue.includes('报销金额未完善')))
assert.ok(!issues.some((issue) => issue.includes('报销类型未完善')))
assert.ok(!issues.some((issue) => issue.includes('报销事由未完善')))
})
test('transport ticket descriptions use route format and invalid format becomes risk advice', () => {
assert.match(detailViewScript, /const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set\(\['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket'\]\)/)
assert.match(detailViewScript, /const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set\(\['hotel_ticket'\]\)/)