feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user