feat: 新增归档中心页面并完善知识库与报销查询能力

新增前端归档中心视图及相关工具函数,扩充知识库文档分类和
提取器支持多种格式,增强编排器报销查询的多维度检索,优
化本体规则和用户代理审核消息,前端完善报销创建和审批详
情交互细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-22 16:00:19 +08:00
parent 1f15699013
commit 88ff04bef8
120 changed files with 6236 additions and 643 deletions

View File

@@ -29,10 +29,11 @@ import {
buildAiAdviceViewModel,
buildAttachmentInsightViewModel,
buildAttachmentRiskCards,
buildClaimSummaryRiskCards,
buildItemClaimRiskState,
extractRiskTagsFromText,
normalizeRiskTone,
resolveRiskTags,
resolveRiskTagTone
resolveRiskTags
} from './travelRequestDetailInsights.js'
import {
EXPENSE_TYPE_OPTIONS,
@@ -95,6 +96,26 @@ function normalizeDetailNoteDraftValue(value) {
return isPlaceholderValue(text) ? '' : text
}
function stripRiskTagsForDisplay(value) {
return String(value || '')
.split('\n')
.map((line) =>
line
.replace(/(?:^|\s)#[A-Za-z_]+(?=\s|$)/g, ' ')
.replace(/[ \t]{2,}/g, ' ')
.replace(/\s+第/g, ':第')
.trim()
)
.join('\n')
.trim()
}
function mergeVisibleNoteWithHiddenTags(visibleText, rawText) {
const cleanText = normalizeDetailNoteDraftValue(visibleText)
const tags = extractRiskTagsFromText(rawText).join(' ')
return [cleanText, tags].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
}
function buildTravelTimeLabelMap(items, requestModel) {
const travelItems = items
.map((item, index) => {
@@ -612,13 +633,36 @@ export default {
() => 6 + (isEditableRequest.value ? 1 : 0)
)
const canEditDetailNote = computed(() => isDraftRequest.value)
const stripDetailNoteRiskTags = (value) =>
String(value || '')
.split('\n')
.map((line) =>
line
.replace(/(?:^|\s)#[A-Za-z_]+(?=\s|$)/g, ' ')
.replace(/[ \t]{2,}/g, ' ')
.replace(/:\s+第/g, ':第')
.trim()
)
.join('\n')
.trim()
const mergeDetailNoteVisibleTextWithTags = (visibleText, rawText) => {
const cleanText = normalizeDetailNoteDraftValue(visibleText)
const tags = extractRiskTagsFromText(rawText).join(' ')
return [cleanText, tags].map((item) => String(item || '').trim()).filter(Boolean).join('\n')
}
const detailNoteSource = computed(() => normalizeDetailNoteDraftValue(request.value.note))
const detailNote = computed(() => {
if (detailNoteSource.value) {
return detailNoteSource.value
return stripDetailNoteRiskTags(detailNoteSource.value)
}
return '暂无附加说明。请补充本次出差或办事事由,例如“去北京客户现场出差,拜访 XX 客户并处理项目验收事项”。'
})
const detailNoteEditorView = computed({
get: () => stripDetailNoteRiskTags(detailNoteEditor.value),
set: (value) => {
detailNoteEditor.value = mergeDetailNoteVisibleTextWithTags(value, detailNoteEditor.value)
}
})
const detailNoteDirty = computed(() => detailNoteEditor.value.trim() !== detailNoteSource.value)
const detailNoteTags = computed(() =>
extractRiskTagsFromText(canEditDetailNote.value ? detailNoteEditor.value : detailNoteSource.value)
@@ -689,6 +733,11 @@ export default {
return expenseAttachmentMeta[item.id] || null
}
function resolveClaimRiskFlags() {
const flags = request.value?.riskFlags || request.value?.risk_flags_json || []
return Array.isArray(flags) ? flags : []
}
function resolveAttachmentDisplayName(item) {
const metadata = resolveAttachmentMeta(item)
return String(metadata?.file_name || item.attachmentHint || '').trim()
@@ -790,10 +839,6 @@ export default {
}
function resolveExpenseRiskState(item) {
if (!item.invoiceId) {
return null
}
if (uploadingExpenseId.value === item.id) {
return {
label: 'AI识别中',
@@ -818,6 +863,15 @@ export default {
}
}
const claimRiskState = buildItemClaimRiskState(item, resolveClaimRiskFlags())
if (claimRiskState) {
return claimRiskState
}
if (!item.invoiceId) {
return null
}
return {
label: '已上传',
tone: 'low',
@@ -843,13 +897,20 @@ export default {
}
const aiAdvice = computed(() => {
const completionItems = draftBlockingIssues.value.map(mapIssueToAdvice).filter(Boolean)
const completionItems = isEditableRequest.value
? draftBlockingIssues.value.map(mapIssueToAdvice).filter(Boolean)
: []
const directRiskCards = buildAttachmentRiskCards({
expenseItems: expenseItems.value,
attachmentMetaByItemId: expenseAttachmentMeta,
claimRiskFlags: resolveClaimRiskFlags()
})
const hasActionableRiskCards = directRiskCards.some(
(card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone))
)
const riskCards = [
...buildAttachmentRiskCards({
expenseItems: expenseItems.value,
attachmentMetaByItemId: expenseAttachmentMeta,
claimRiskFlags: request.value.riskFlags || request.value.risk_flags_json || []
}),
...(hasActionableRiskCards ? [] : buildClaimSummaryRiskCards(request.value)),
...directRiskCards,
...buildOptionalTravelReceiptRiskCards(request.value, expenseItems.value)
]
@@ -859,6 +920,14 @@ export default {
})
})
const showAiAdvicePanel = computed(() => isEditableRequest.value || aiAdvice.value.riskCards.length > 0)
const aiAdviceTitle = computed(() => (isEditableRequest.value ? 'AI建议' : 'AI提示'))
const aiAdviceHint = computed(() => (
isEditableRequest.value
? '按建议顺序补齐信息或处理风险后,再发起审批。'
: '展示系统已识别的风险点,便于审批和后续整改。'
))
const submitRiskWarnings = computed(() =>
aiAdvice.value.riskCards
.filter((card) => normalizeRiskTone(card?.tone) === 'high')
@@ -904,10 +973,6 @@ export default {
}
}
function resolveRiskTagClass(tag) {
return resolveRiskTagTone(tag)
}
function openRiskOverrideDialog() {
const warnings = submitRiskWarnings.value
if (!warnings.length) {
@@ -1619,11 +1684,18 @@ export default {
return
}
const claimId = String(request.value?.claimId || '').trim()
emit('openAssistant', {
source: 'detail',
prompt: '',
request: request.value,
restoreLatestConversation: true
restoreLatestConversation: false,
scope: claimId
? {
type: 'claim',
claimId
}
: null
})
}
@@ -1632,7 +1704,7 @@ export default {
})
return {
emit, actionBusy, aiAdvice, attachmentPreviewError, attachmentPreviewIndexLabel,
emit, actionBusy, aiAdvice, aiAdviceHint, aiAdviceTitle, attachmentPreviewError, attachmentPreviewIndexLabel,
attachmentPreviewLoading, attachmentPreviewMediaType, attachmentPreviewName, attachmentPreviewOpen,
attachmentPreviewUrl, approveBusy, approveConfirmDialogOpen, approvalConfirmBadge,
approvalConfirmDescription, approvalNextStage, approvalOpinionHint, approvalOpinionPlaceholder,
@@ -1646,7 +1718,7 @@ export default {
currentSubmitRiskWarning,
canEditDetailNote, deleteActionLabel, deleteBusy, deleteDialogDescription, deleteDialogOpen,
deleteDialogTitle, deletingAttachmentId, deletingExpenseId, detailNote, detailNoteDirty,
detailNoteEditor, detailNoteTags, draftBlockingIssues, editingExpenseId, creatingExpense, expenseEditor,
detailNoteEditor, detailNoteEditorView, detailNoteTags, draftBlockingIssues, editingExpenseId, creatingExpense, expenseEditor,
expenseItems, expenseTableColumnCount, expenseTotal, expenseUploadInput,
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
goToNextSubmitRisk, goToPreviousSubmitRisk,
@@ -1655,12 +1727,12 @@ export default {
isMajorExpenseRisk,
openAiEntry, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
resolveExpenseRiskIndicatorTitle, resolveRiskTagClass,
resolveExpenseRiskIndicatorTitle,
resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
returnBusy, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
showLeaderApprovalPanel, showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
showAiAdvicePanel, showLeaderApprovalPanel, showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
submitRiskWarnings,
triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
}