feat: 增强风险规则生成引擎与预算中心页面

后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块,
优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强
报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图
组件,重构审计页面和风险规则测试对话框交互,完善文档中心
和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-26 09:15:14 +08:00
parent d0e946cf47
commit 0e861d8fa6
150 changed files with 14953 additions and 4099 deletions

View File

@@ -20,11 +20,14 @@ import {
} from '../../services/reimbursements.js'
import {
canApproveLeaderExpenseClaims,
canDeleteArchivedExpenseClaims,
canManageExpenseClaims,
canReturnExpenseClaims,
isFinanceUser
} from '../../utils/accessControl.js'
import { normalizeRequestForUi } from '../../utils/requestViewModel.js'
import { buildLeaderApprovalInfo, resolveGeneratedDraftClaimNo } from '../../utils/applicationApproval.js'
import { buildApplicationDetailFactItems } from '../../utils/expenseApplicationDetail.js'
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
import {
buildAiAdviceViewModel,
buildAttachmentInsightViewModel,
@@ -460,7 +463,13 @@ export default {
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
const canOpenAiEntry = computed(() => isEditableRequest.value)
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
const canDeleteRequest = computed(() => isEditableRequest.value || canManageCurrentClaim.value)
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
const canDeleteRequest = computed(() => {
if (isArchivedRequest.value) {
return canDeleteArchivedExpenseClaims(currentUser.value)
}
return isEditableRequest.value || canManageCurrentClaim.value
})
const isDirectManagerApprovalStage = computed(() => {
const node = String(request.value.node || request.value.approvalStage || '').trim()
return node === '直属领导审批'
@@ -475,7 +484,7 @@ export default {
&& Boolean(request.value.claimId)
)
const canApproveRequest = computed(() =>
Boolean(props.approvalMode)
(Boolean(props.approvalMode) || isApplicationDocument.value)
&& request.value.approvalKey === 'in_progress'
&& Boolean(request.value.claimId)
&& (
@@ -490,7 +499,37 @@ export default {
)
)
)
const showLeaderApprovalPanel = computed(() => canApproveRequest.value)
const showApplicationLeaderOpinionInput = computed(() => (
isApplicationDocument.value
&& canApproveRequest.value
&& isDirectManagerApprovalStage.value
))
const leaderApprovalInfo = computed(() => buildLeaderApprovalInfo(request.value))
const leaderApprovalReadonlyText = computed(() => {
if (leaderApprovalInfo.value.opinion) {
return leaderApprovalInfo.value.opinion
}
return isApplicationDocument.value ? '待直属领导填写审批意见。' : ''
})
const leaderApprovalReadonlyMeta = computed(() => {
const pieces = [
leaderApprovalInfo.value.operator ? `${leaderApprovalInfo.value.operator}确认` : '',
leaderApprovalInfo.value.time
].filter(Boolean)
if (leaderApprovalInfo.value.generatedDraftClaimNo) {
pieces.push(`已生成报销草稿 ${leaderApprovalInfo.value.generatedDraftClaimNo}`)
}
return pieces.join(' · ')
})
const showApplicationLeaderOpinion = computed(() => (
isApplicationDocument.value
&& (
showApplicationLeaderOpinionInput.value
|| leaderApprovalReadonlyText.value
)
))
const showLeaderApprovalPanel = computed(() => canApproveRequest.value && !showApplicationLeaderOpinionInput.value)
const requiresApprovalOpinion = computed(() => isDirectManagerApprovalStage.value)
const approvalOpinionTitle = computed(() => (isFinanceApprovalStage.value ? '财务意见' : '领导意见'))
const approvalOpinionPlaceholder = computed(() => {
if (isFinanceApprovalStage.value) {
@@ -505,7 +544,7 @@ export default {
if (isFinanceApprovalStage.value) {
return '审核通过后将进入归档入账。'
}
return isApplicationDocument.value ? '审批通过后申请流程完成。' : '审批通过后将流转至财务审批。'
return isApplicationDocument.value ? '领导意见为必填,确认后会生成报销草稿。' : '领导意见为必填,审批通过后将流转至财务审批。'
})
const approvalConfirmBadge = computed(() => (isFinanceApprovalStage.value ? '财务终审' : '领导审批'))
const approvalConfirmDescription = computed(() => {
@@ -513,7 +552,7 @@ export default {
return '确认后该报销单会完成财务终审并进入归档入账,请确认票据、金额与财务意见无误。'
}
if (isApplicationDocument.value) {
return '确认后该申请单会完成直属领导审批,请确认申请信息与领导意见无误。'
return '确认后该申请单会完成直属领导审批,并自动进入申请人的报销草稿中。'
}
return '确认后该报销单会从直属领导审批流转到财务审批,请确认申请信息与领导意见无误。'
})
@@ -521,14 +560,29 @@ export default {
if (isFinanceApprovalStage.value) {
return '归档入账'
}
return isApplicationDocument.value ? '审批完成' : '财务审批'
return isApplicationDocument.value ? '报销草稿' : '财务审批'
})
const approveActionLabel = computed(() => (isApplicationDocument.value ? '确认审核' : '审批通过'))
const approveBusyLabel = computed(() => (isApplicationDocument.value ? '确认中' : '通过中'))
const approveConfirmTitle = computed(() => (
isApplicationDocument.value ? `确认审核 ${request.value.id} 吗?` : `确认通过 ${request.value.id} 吗?`
))
const approveConfirmText = computed(() => (isApplicationDocument.value ? '确认审核' : '确认通过'))
const approveBusyText = computed(() => (isApplicationDocument.value ? '确认中...' : '通过中...'))
const returnDialogDescription = computed(() => (
isApplicationDocument.value
? '退回后该申请单会回到待提交状态,申请人需要调整后重新提交。'
: '退回后该单据会回到待提交状态,申请人需要调整后重新提交并再次经过 AI 预审。'
))
const approvalConfirmSummaryLabel = computed(() => (
isApplicationDocument.value ? '生成结果' : '下一节点'
))
const approvalSuccessToast = computed(() => {
if (isFinanceApprovalStage.value) {
return `${request.value.id} 已完成财务终审,进入归档入账。`
}
return isApplicationDocument.value
? `${request.value.id} 申请已审批通过`
? `${request.value.id} 已确认审核,正在生成报销草稿`
: `${request.value.id} 已审批通过,流转至财务审批。`
})
const deleteActionLabel = computed(() => (isDraftRequest.value ? '删除草稿' : '删除单据'))
@@ -613,13 +667,6 @@ export default {
value: request.value.typeLabel,
icon: '',
valueClass: ''
},
{
key: 'status',
label: '当前状态',
value: request.value.node,
icon: '',
valueClass: 'status'
}
])
@@ -652,6 +699,7 @@ export default {
const total = expenseItems.value.reduce((sum, item) => sum + Number(item.itemAmount || 0), 0)
return formatCurrency(total)
})
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
const uploadedExpenseCount = computed(() => expenseItems.value.filter((item) => item.attachments.length).length)
const expenseTableColumnCount = computed(
@@ -1582,7 +1630,11 @@ export default {
}
if (!canDeleteRequest.value) {
toast('当前单据已进入流程,只有财务人员或高级管理人员可以删除。')
toast(
isArchivedRequest.value
? '已归档单据不能删除,只有高级管理员可以执行删除。'
: '当前单据已进入流程,只有高级管理人员可以删除。'
)
return
}
@@ -1668,6 +1720,11 @@ export default {
return
}
if (requiresApprovalOpinion.value && !leaderOpinion.value.trim()) {
toast('请先填写领导意见,填写后才能确认审核。')
return
}
approveConfirmDialogOpen.value = true
}
@@ -1692,14 +1749,25 @@ export default {
return
}
if (requiresApprovalOpinion.value && !leaderOpinion.value.trim()) {
toast('请先填写领导意见,填写后才能确认审核。')
approveConfirmDialogOpen.value = false
return
}
approveBusy.value = true
try {
await approveExpenseClaim(request.value.claimId, {
const responsePayload = await approveExpenseClaim(request.value.claimId, {
opinion: leaderOpinion.value.trim()
})
const generatedDraftClaimNo = resolveGeneratedDraftClaimNo(responsePayload)
approveConfirmDialogOpen.value = false
leaderOpinion.value = ''
toast(approvalSuccessToast.value)
toast(
isApplicationDocument.value && generatedDraftClaimNo
? `${request.value.id} 已确认审核,报销草稿 ${generatedDraftClaimNo} 已生成。`
: approvalSuccessToast.value
)
emit('request-updated', { claimId: request.value.claimId })
} catch (error) {
toast(error?.message || '审批通过失败,请稍后重试。')
@@ -1736,8 +1804,11 @@ export default {
emit, actionBusy, aiAdvice, aiAdviceHint, aiAdviceTitle, attachmentPreviewError, attachmentPreviewIndexLabel,
attachmentPreviewLoading, attachmentPreviewMediaType, attachmentPreviewName, attachmentPreviewOpen,
attachmentPreviewUrl, approveBusy, approveConfirmDialogOpen, approvalConfirmBadge,
approvalConfirmDescription, approvalNextStage, approvalOpinionHint, approvalOpinionPlaceholder,
approvalOpinionTitle, canDeleteRequest, canManageCurrentClaim, canNavigateAttachmentPreview,
approvalConfirmDescription, approvalConfirmSummaryLabel, approvalNextStage, approvalOpinionHint,
approvalOpinionPlaceholder, approvalOpinionTitle, approveActionLabel, approveBusyLabel,
applicationDetailFactItems,
approveBusyText, approveConfirmText, approveConfirmTitle, canDeleteRequest, canManageCurrentClaim,
canNavigateAttachmentPreview,
canOpenAiEntry, canApproveRequest, canReturnRequest, canSubmit, canPreviewAttachment,
closeApproveConfirmDialog, closeDeleteDialog, closeAttachmentPreview, closeSubmitConfirmDialog,
closeRiskOverrideDialog,
@@ -1756,12 +1827,15 @@ export default {
isMajorExpenseRisk,
openAiEntry, openAttachmentPreview, goToNextAttachmentPreview, goToPreviousAttachmentPreview,
profile, progressSteps, request, leaderOpinion, removeExpenseAttachment, removeExpenseItem,
leaderApprovalReadonlyMeta, leaderApprovalReadonlyText,
resolveExpenseRiskIndicatorTitle,
resetDetailNote, resolveAttachmentDisplayName, resolveAttachmentPreviewTitle, resolveAttachmentRecognition,
resolveExpenseReasonHelper, resolveExpenseReasonPlaceholder, resolveExpenseRiskState, resolveExpenseIssues,
returnBusy, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
returnBusy, returnDialogDescription, returnDialogOpen, riskOverrideBusy, riskOverrideDialogOpen, riskOverrideIndexLabel,
requiresApprovalOpinion,
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
showAiAdvicePanel, showLeaderApprovalPanel, showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
showAiAdvicePanel, showApplicationLeaderOpinion, showApplicationLeaderOpinionInput,
showLeaderApprovalPanel, showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
submitRiskWarnings,
triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
}