feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和 提取器支持多种格式,增强编排器报销查询的多维度检索,优 化本体规则和用户代理审核消息,前端完善报销创建和审批详 情交互细节,补充单元测试覆盖。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user