feat: 扩展风险规则体系、审批动态路由与预算中心列表化改造
- 新增 25+ 条风险规则(预算/报销/申请/通用类),完善风险规则模拟与反馈发布机制 - 引入费用审批动态路由、平台风险分级、预审与风险阶段管理 - 预算中心列表化改造,优化票据夹仪表盘与数字员工工作看板 - 新增 Hermes 风险线索收集器、Agent 链路追踪中心 - 扩展数字员工能力库(18 个领域 Skill)与交通费用自动预估 - 完善报销申请快速预览、权限控制与前端测试覆盖
This commit is contained in:
@@ -7,7 +7,7 @@ import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
||||
import TravelRequestApprovalDialog from '../../components/travel/TravelRequestApprovalDialog.vue'
|
||||
import TravelRequestBudgetAnalysis from '../../components/travel/TravelRequestBudgetAnalysis.vue'
|
||||
import TravelRequestDeleteDialog from '../../components/travel/TravelRequestDeleteDialog.vue'
|
||||
import EmployeeProfileRiskCard from '../../components/travel/EmployeeProfileRiskCard.vue'
|
||||
import StageRiskAdviceCard from '../../components/travel/StageRiskAdviceCard.vue'
|
||||
import RiskObservationEvidenceCard from '../../components/travel/RiskObservationEvidenceCard.vue'
|
||||
import TravelRequestReturnDialog from '../../components/travel/TravelRequestReturnDialog.vue'
|
||||
import {
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
deleteExpenseClaimItem,
|
||||
deleteExpenseClaimItemAttachment,
|
||||
deleteExpenseClaim,
|
||||
fetchEmployeeLatestProfile,
|
||||
fetchExpenseClaimItemAttachmentMeta,
|
||||
fetchExpenseClaimItemAttachmentPreview,
|
||||
preReviewExpenseClaim,
|
||||
returnExpenseClaim,
|
||||
submitExpenseClaim,
|
||||
uploadExpenseClaimItemAttachment,
|
||||
@@ -35,6 +35,10 @@ import {
|
||||
isCurrentRequestApplicant,
|
||||
isFinanceUser
|
||||
} from '../../utils/accessControl.js'
|
||||
import {
|
||||
buildRiskViewerContext,
|
||||
filterRiskCardsForVisibility
|
||||
} from '../../utils/riskVisibility.js'
|
||||
import {
|
||||
buildLeaderApprovalEvents,
|
||||
buildLeaderApprovalInfo,
|
||||
@@ -52,6 +56,7 @@ import {
|
||||
buildClaimSummaryRiskCards,
|
||||
buildItemClaimRiskState,
|
||||
extractRiskTagsFromText,
|
||||
filterRiskCardsByBusinessStage,
|
||||
normalizeRiskTone,
|
||||
resolveRiskTags
|
||||
} from './travelRequestDetailInsights.js'
|
||||
@@ -78,6 +83,17 @@ import {
|
||||
resolveExpenseReasonPlaceholder,
|
||||
resolveExpenseUploadHint
|
||||
} from './travelRequestDetailExpenseModel.js'
|
||||
import {
|
||||
buildAiPreReviewSnapshot,
|
||||
findLatestAiPreReviewEvent,
|
||||
isAiPreReviewFlag,
|
||||
isAiPreReviewPassed,
|
||||
resolveAiPreReviewToast,
|
||||
resolveSubmitActionIcon,
|
||||
resolveSubmitActionLabel,
|
||||
resolveSubmitConfirmDescription,
|
||||
resolveSubmitConfirmText
|
||||
} from './travelRequestDetailPreReviewModel.js'
|
||||
import { useTravelRequestPaymentFlow } from './travelRequestDetailPaymentFlow.js'
|
||||
|
||||
/*
|
||||
@@ -377,7 +393,7 @@ export default {
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
EnterpriseSelect,
|
||||
EmployeeProfileRiskCard,
|
||||
StageRiskAdviceCard,
|
||||
RiskObservationEvidenceCard,
|
||||
TravelRequestApprovalDialog,
|
||||
TravelRequestBudgetAnalysis,
|
||||
@@ -410,6 +426,8 @@ export default {
|
||||
const deletingExpenseId = ref('')
|
||||
const pendingUploadExpenseId = ref('')
|
||||
const submitBusy = ref(false)
|
||||
const aiPreReviewSnapshot = ref(null)
|
||||
const riskFlagPreviewSnapshot = ref(null)
|
||||
const submitConfirmDialogOpen = ref(false)
|
||||
const riskOverrideDialogOpen = ref(false)
|
||||
const riskOverrideBusy = ref(false)
|
||||
@@ -441,10 +459,6 @@ export default {
|
||||
})
|
||||
const detailNoteEditor = ref('')
|
||||
const savingDetailNote = ref(false)
|
||||
const employeeRiskProfile = ref(null)
|
||||
const employeeRiskProfileLoading = ref(false)
|
||||
const employeeRiskProfileError = ref('')
|
||||
let employeeRiskProfileLoadSeq = 0
|
||||
|
||||
const request = computed(() => {
|
||||
const normalized = normalizeRequestForUi(props.request)
|
||||
@@ -496,7 +510,10 @@ export default {
|
||||
if (isArchivedRequest.value) {
|
||||
return canDeleteArchivedExpenseClaims(currentUser.value)
|
||||
}
|
||||
return isEditableRequest.value || canManageCurrentClaim.value
|
||||
if (canManageCurrentClaim.value) {
|
||||
return true
|
||||
}
|
||||
return isEditableRequest.value && isCurrentApplicant.value
|
||||
})
|
||||
const isDirectManagerApprovalStage = computed(() => {
|
||||
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
||||
@@ -533,29 +550,6 @@ export default {
|
||||
&& canApproveBudgetExpenseApplications(currentUser.value, request.value)
|
||||
&& !isCurrentApplicant.value
|
||||
))
|
||||
const employeeProfileId = computed(() =>
|
||||
String(
|
||||
request.value.employeeId
|
||||
|| request.value.employee_id
|
||||
|| request.value.profileEmployeeId
|
||||
|| ''
|
||||
).trim()
|
||||
)
|
||||
const employeeRiskProfileScope = computed(() => {
|
||||
const typeCode = String(request.value.typeCode || request.value.expense_type || '').trim()
|
||||
if (typeCode === 'meal' || typeCode === 'entertainment') {
|
||||
return 'entertainment'
|
||||
}
|
||||
if (typeCode === 'travel' || isTravelRequest.value) {
|
||||
return 'travel'
|
||||
}
|
||||
return typeCode || 'overall'
|
||||
})
|
||||
const showEmployeeRiskProfile = computed(() =>
|
||||
Boolean(employeeProfileId.value)
|
||||
&& Boolean(request.value.claimId)
|
||||
&& !isDraftRequest.value
|
||||
)
|
||||
const canReturnRequest = computed(() => {
|
||||
if (request.value.approvalKey !== 'in_progress' || !request.value.claimId || !canReturnExpenseClaims(currentUser.value)) {
|
||||
return false
|
||||
@@ -581,6 +575,25 @@ export default {
|
||||
|| canProcessBudgetApprovalStage.value
|
||||
)
|
||||
)
|
||||
const canViewApprovalRiskAdvice = computed(() => (
|
||||
Boolean(request.value.claimId)
|
||||
&& !isDraftRequest.value
|
||||
&& !isCurrentApplicant.value
|
||||
&& (canReturnRequest.value || canApproveRequest.value)
|
||||
))
|
||||
const showStageRiskAdvice = computed(() => canViewApprovalRiskAdvice.value)
|
||||
const riskViewerContext = computed(() => buildRiskViewerContext({
|
||||
request: request.value,
|
||||
currentUser: currentUser.value,
|
||||
businessStage: isApplicationDocument.value ? 'expense_application' : 'reimbursement',
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
isCurrentApplicant: isCurrentApplicant.value,
|
||||
isBudgetReviewer: canProcessBudgetApprovalStage.value,
|
||||
isDirectManagerReviewer: isCurrentDirectManagerApprover.value,
|
||||
isFinanceReviewer: canProcessFinanceApprovalStage.value,
|
||||
isAdminViewer: canManageCurrentClaim.value,
|
||||
canViewApprovalRiskAdvice: canViewApprovalRiskAdvice.value
|
||||
}))
|
||||
const {
|
||||
canPayRequest,
|
||||
closePayConfirmDialog,
|
||||
@@ -628,7 +641,7 @@ export default {
|
||||
if (isBudgetApprovalStage.value) {
|
||||
return '不填写附加意见则默认同意,确认后会归档申请单并生成报销草稿。'
|
||||
}
|
||||
return isApplicationDocument.value ? '不填写附加意见则默认同意,确认后会流转至预算管理者审批。' : '不填写附加意见则默认同意,审批通过后将流转至财务审批。'
|
||||
return isApplicationDocument.value ? '不填写附加意见则默认同意,确认后系统会按预算与风险结果决定下一步:无风险且预算充足将直接完成申请,否则进入预算管理者审批。' : '不填写附加意见则默认同意,审批通过后将流转至财务审批。'
|
||||
})
|
||||
const approvalConfirmBadge = computed(() => {
|
||||
if (isFinanceApprovalStage.value) {
|
||||
@@ -643,7 +656,7 @@ export default {
|
||||
if (isApplicationDocument.value) {
|
||||
return isBudgetApprovalStage.value
|
||||
? '确认后该申请单会完成预算审核,归档申请单,并自动进入申请人的报销草稿中。'
|
||||
: '确认后该申请单会完成直属领导审批,并流转给预算管理者进一步审核。'
|
||||
: '确认后该申请单会完成直属领导审批,系统将按预算余额、当前风险和历史风险判断是否需要预算管理者复核;无风险且预算充足会直接完成申请并生成报销草稿。'
|
||||
}
|
||||
return '确认后该报销单会从直属领导审批流转到财务审批,请确认申请信息与领导意见无误。'
|
||||
})
|
||||
@@ -666,7 +679,7 @@ export default {
|
||||
return isApplicationDocument.value
|
||||
? isBudgetApprovalStage.value
|
||||
? `${request.value.id} 已完成预算审核,正在生成报销草稿。`
|
||||
: `${request.value.id} 已确认审核,已流转至预算管理者审批。`
|
||||
: `${request.value.id} 已确认审核,系统已按预算与风险结果更新流程。`
|
||||
: `${request.value.id} 已审批通过,流转至财务审批。`
|
||||
})
|
||||
const deleteActionLabel = computed(() => (isDraftRequest.value ? '删除草稿' : '删除单据'))
|
||||
@@ -713,6 +726,7 @@ export default {
|
||||
Object.keys(expenseAttachmentMeta).forEach((key) => {
|
||||
delete expenseAttachmentMeta[key]
|
||||
})
|
||||
aiPreReviewSnapshot.value = null
|
||||
closeAttachmentPreview()
|
||||
}
|
||||
pendingUploadExpenseId.value = ''
|
||||
@@ -724,19 +738,6 @@ export default {
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [
|
||||
employeeProfileId.value,
|
||||
request.value.claimId,
|
||||
employeeRiskProfileScope.value,
|
||||
showEmployeeRiskProfile.value
|
||||
],
|
||||
() => {
|
||||
void loadEmployeeRiskProfile()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const heroFactItems = computed(() => [
|
||||
{
|
||||
key: 'document',
|
||||
@@ -846,6 +847,12 @@ export default {
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(
|
||||
() => request.value.claimId,
|
||||
() => {
|
||||
riskFlagPreviewSnapshot.value = null
|
||||
}
|
||||
)
|
||||
const draftBlockingIssues = computed(() =>
|
||||
isEditableRequest.value ? buildDraftBlockingIssues(request.value, expenseItems.value) : []
|
||||
)
|
||||
@@ -907,7 +914,25 @@ export default {
|
||||
|
||||
function resolveClaimRiskFlags() {
|
||||
const flags = request.value?.riskFlags || request.value?.risk_flags_json || []
|
||||
return Array.isArray(flags) ? flags : []
|
||||
let requestFlags = Array.isArray(flags) ? flags : []
|
||||
const previewSnapshot = riskFlagPreviewSnapshot.value
|
||||
if (
|
||||
previewSnapshot
|
||||
&& previewSnapshot.claimId === request.value?.claimId
|
||||
&& Array.isArray(previewSnapshot.riskFlags)
|
||||
) {
|
||||
requestFlags = previewSnapshot.riskFlags
|
||||
}
|
||||
const snapshot = aiPreReviewSnapshot.value
|
||||
if (
|
||||
snapshot
|
||||
&& snapshot.claimId === request.value?.claimId
|
||||
&& Array.isArray(snapshot.riskFlags)
|
||||
&& !requestFlags.some(isAiPreReviewFlag)
|
||||
) {
|
||||
return snapshot.riskFlags
|
||||
}
|
||||
return requestFlags
|
||||
}
|
||||
|
||||
function resolveAttachmentDisplayName(item) {
|
||||
@@ -953,38 +978,6 @@ export default {
|
||||
return payload
|
||||
}
|
||||
|
||||
async function loadEmployeeRiskProfile() {
|
||||
const sequence = ++employeeRiskProfileLoadSeq
|
||||
employeeRiskProfileError.value = ''
|
||||
if (!showEmployeeRiskProfile.value) {
|
||||
employeeRiskProfile.value = null
|
||||
employeeRiskProfileLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
employeeRiskProfileLoading.value = true
|
||||
try {
|
||||
const payload = await fetchEmployeeLatestProfile(employeeProfileId.value, {
|
||||
scene: 'approval',
|
||||
claim_id: request.value.claimId,
|
||||
window_days: 90,
|
||||
expense_type_scope: employeeRiskProfileScope.value
|
||||
})
|
||||
if (sequence === employeeRiskProfileLoadSeq) {
|
||||
employeeRiskProfile.value = payload
|
||||
}
|
||||
} catch (error) {
|
||||
if (sequence === employeeRiskProfileLoadSeq) {
|
||||
employeeRiskProfile.value = null
|
||||
employeeRiskProfileError.value = error?.message || '画像读取失败,请稍后重试。'
|
||||
}
|
||||
} finally {
|
||||
if (sequence === employeeRiskProfileLoadSeq) {
|
||||
employeeRiskProfileLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function canPreviewAttachment(item) {
|
||||
if (!item?.invoiceId) {
|
||||
return false
|
||||
@@ -1100,23 +1093,66 @@ export default {
|
||||
return summary ? `重大风险警示:${summary}` : '重大风险警示'
|
||||
}
|
||||
|
||||
function applyAiPreReviewPayload(payload) {
|
||||
aiPreReviewSnapshot.value = buildAiPreReviewSnapshot(payload, request.value.claimId)
|
||||
}
|
||||
|
||||
function applyClaimRiskFlagsPayload(payload) {
|
||||
const flags = Array.isArray(payload?.claim_risk_flags)
|
||||
? payload.claim_risk_flags
|
||||
: Array.isArray(payload?.claimRiskFlags)
|
||||
? payload.claimRiskFlags
|
||||
: null
|
||||
if (!flags) {
|
||||
return
|
||||
}
|
||||
riskFlagPreviewSnapshot.value = {
|
||||
claimId: request.value.claimId,
|
||||
riskFlags: flags
|
||||
}
|
||||
}
|
||||
|
||||
const requiresAiPreReview = computed(() => isEditableRequest.value && !isApplicationDocument.value)
|
||||
const aiPreReviewEvent = computed(() => findLatestAiPreReviewEvent(resolveClaimRiskFlags()))
|
||||
const hasAiPreReviewResult = computed(() => !requiresAiPreReview.value || Boolean(aiPreReviewEvent.value))
|
||||
const aiPreReviewPassed = computed(() =>
|
||||
isAiPreReviewPassed(aiPreReviewEvent.value, requiresAiPreReview.value)
|
||||
)
|
||||
|
||||
const aiAdvice = computed(() => {
|
||||
const completionItems = isEditableRequest.value
|
||||
? draftBlockingIssues.value.map(mapIssueToAdvice).filter(Boolean)
|
||||
: []
|
||||
const directRiskCards = buildAttachmentRiskCards({
|
||||
expenseItems: expenseItems.value,
|
||||
attachmentMetaByItemId: expenseAttachmentMeta,
|
||||
claimRiskFlags: resolveClaimRiskFlags()
|
||||
})
|
||||
const currentBusinessStage = isApplicationDocument.value ? 'expense_application' : 'reimbursement'
|
||||
const directRiskCards = filterRiskCardsByBusinessStage(
|
||||
buildAttachmentRiskCards({
|
||||
expenseItems: expenseItems.value,
|
||||
attachmentMetaByItemId: expenseAttachmentMeta,
|
||||
claimRiskFlags: resolveClaimRiskFlags(),
|
||||
businessStage: currentBusinessStage
|
||||
}),
|
||||
currentBusinessStage
|
||||
)
|
||||
const hasActionableRiskCards = directRiskCards.some(
|
||||
(card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone))
|
||||
)
|
||||
const riskCards = [
|
||||
...(hasActionableRiskCards ? [] : buildClaimSummaryRiskCards(request.value)),
|
||||
const summaryRiskCards = filterRiskCardsByBusinessStage(
|
||||
buildClaimSummaryRiskCards({
|
||||
...(request.value || {}),
|
||||
businessStage: currentBusinessStage
|
||||
}),
|
||||
currentBusinessStage
|
||||
)
|
||||
const optionalRiskCards = filterRiskCardsByBusinessStage(
|
||||
buildOptionalTravelReceiptRiskCards(request.value, expenseItems.value),
|
||||
currentBusinessStage
|
||||
)
|
||||
const scopedRiskCards = [
|
||||
...(hasActionableRiskCards ? [] : summaryRiskCards),
|
||||
...directRiskCards,
|
||||
...buildOptionalTravelReceiptRiskCards(request.value, expenseItems.value)
|
||||
...optionalRiskCards
|
||||
]
|
||||
const riskCards = filterRiskCardsForVisibility(scopedRiskCards, riskViewerContext.value)
|
||||
|
||||
return buildAiAdviceViewModel({
|
||||
completionItems,
|
||||
@@ -1124,13 +1160,54 @@ 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 hasVisibleRiskCards = computed(() =>
|
||||
aiAdvice.value.riskCards.some((card) => ['medium', 'high'].includes(normalizeRiskTone(card?.tone)))
|
||||
)
|
||||
const hasAdviceSections = computed(() => aiAdvice.value.sections.length > 0)
|
||||
const showAiAdvicePanel = computed(() => (
|
||||
(
|
||||
isEditableRequest.value
|
||||
&& (
|
||||
(requiresAiPreReview.value && hasAiPreReviewResult.value)
|
||||
|| hasAdviceSections.value
|
||||
)
|
||||
)
|
||||
|| (!isEditableRequest.value && canViewApprovalRiskAdvice.value && aiAdvice.value.riskCards.length > 0)
|
||||
|| (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value && hasVisibleRiskCards.value)
|
||||
))
|
||||
const aiAdviceTitle = computed(() => {
|
||||
if (!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value) {
|
||||
return '报销风险提示'
|
||||
}
|
||||
if (isEditableRequest.value && isApplicationDocument.value) {
|
||||
return '表单自查提示'
|
||||
}
|
||||
return isEditableRequest.value ? 'AI建议' : 'AI提示'
|
||||
})
|
||||
const aiAdviceHint = computed(() => (
|
||||
!isEditableRequest.value && isCurrentApplicant.value && !isApplicationDocument.value
|
||||
? '展示票据、行程、金额等可自行修正的风险点,便于提交人先整改,减少后续退单。'
|
||||
: isEditableRequest.value
|
||||
? (isApplicationDocument.value ? '仅提示申请表单本身需要补充的内容,不展示预算治理细节。' : 'AI预审已完成,请按风险提示补充原因或进入下一步。')
|
||||
: '展示系统已识别的风险点,便于审批和后续整改。'
|
||||
))
|
||||
|
||||
const submitActionLabel = computed(() => {
|
||||
return resolveSubmitActionLabel({
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
hasAiPreReviewResult: hasAiPreReviewResult.value,
|
||||
submitBusy: submitBusy.value
|
||||
})
|
||||
})
|
||||
const submitActionIcon = computed(() => resolveSubmitActionIcon({
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
hasAiPreReviewResult: hasAiPreReviewResult.value
|
||||
}))
|
||||
const submitConfirmDescription = computed(() => resolveSubmitConfirmDescription({
|
||||
isApplicationDocument: isApplicationDocument.value,
|
||||
aiPreReviewPassed: aiPreReviewPassed.value
|
||||
}))
|
||||
const submitConfirmText = computed(() => resolveSubmitConfirmText(isApplicationDocument.value))
|
||||
|
||||
const submitRiskWarnings = computed(() =>
|
||||
aiAdvice.value.riskCards
|
||||
@@ -1470,6 +1547,7 @@ export default {
|
||||
|
||||
try {
|
||||
const payload = await uploadExpenseClaimItemAttachment(request.value.claimId, item.id, file)
|
||||
applyClaimRiskFlagsPayload(payload)
|
||||
expenseAttachmentMeta[item.id] = payload?.attachment || null
|
||||
const recognizedItemAmount = Number(payload?.item_amount ?? payload?.itemAmount)
|
||||
const recognizedItemDate = normalizeIsoDateValue(payload?.item_date ?? payload?.itemDate)
|
||||
@@ -1519,6 +1597,7 @@ export default {
|
||||
deletingAttachmentId.value = item.id
|
||||
try {
|
||||
const payload = await deleteExpenseClaimItemAttachment(request.value.claimId, item.id)
|
||||
applyClaimRiskFlagsPayload(payload)
|
||||
delete expenseAttachmentMeta[item.id]
|
||||
applyLocalExpenseItemPatch(item.id, {
|
||||
invoiceId: '',
|
||||
@@ -1672,7 +1751,22 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
async function runAiPreReview() {
|
||||
submitBusy.value = true
|
||||
try {
|
||||
const payload = await preReviewExpenseClaim(request.value.claimId)
|
||||
applyAiPreReviewPayload(payload)
|
||||
const event = findLatestAiPreReviewEvent(payload?.risk_flags_json || [])
|
||||
toast(resolveAiPreReviewToast(event))
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
} catch (error) {
|
||||
toast(error?.message || 'AI预审失败,请稍后重试。')
|
||||
} finally {
|
||||
submitBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!request.value.claimId) {
|
||||
toast('当前草稿缺少 claimId,暂时无法提交。')
|
||||
return
|
||||
@@ -1688,6 +1782,11 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresAiPreReview.value && !hasAiPreReviewResult.value) {
|
||||
await runAiPreReview()
|
||||
return
|
||||
}
|
||||
|
||||
if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
|
||||
openRiskOverrideDialog()
|
||||
return
|
||||
@@ -1723,6 +1822,12 @@ export default {
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresAiPreReview.value && !hasAiPreReviewResult.value) {
|
||||
submitConfirmDialogOpen.value = false
|
||||
await runAiPreReview()
|
||||
return
|
||||
}
|
||||
|
||||
if (submitRiskWarnings.value.length && !hasRiskOverrideExplanation.value) {
|
||||
submitConfirmDialogOpen.value = false
|
||||
openRiskOverrideDialog()
|
||||
@@ -1862,6 +1967,14 @@ export default {
|
||||
approveConfirmDialogOpen.value = false
|
||||
}
|
||||
|
||||
function resolveApproveErrorMessage(error) {
|
||||
const message = String(error?.message || '').trim()
|
||||
if (message.includes('未找到同部门 P8 预算审批人')) {
|
||||
return '当前部门未配置 P8 预算审批人,请联系管理员配置后再审批。'
|
||||
}
|
||||
return message || '审批通过失败,请稍后重试。'
|
||||
}
|
||||
|
||||
async function confirmApproveRequest() {
|
||||
if (!request.value.claimId) {
|
||||
toast('当前单据缺少 claimId,暂时无法审批通过。')
|
||||
@@ -1889,8 +2002,9 @@ export default {
|
||||
: approvalSuccessToast.value
|
||||
)
|
||||
emit('request-updated', { claimId: request.value.claimId })
|
||||
emit('backToRequests')
|
||||
} catch (error) {
|
||||
toast(error?.message || '审批通过失败,请稍后重试。')
|
||||
toast(resolveApproveErrorMessage(error))
|
||||
} finally {
|
||||
approveBusy.value = false
|
||||
}
|
||||
@@ -1939,7 +2053,6 @@ export default {
|
||||
canEditDetailNote, deleteActionLabel, deleteBusy, deleteDialogDescription, deleteDialogOpen,
|
||||
deleteDialogTitle, deletingAttachmentId, deletingExpenseId, detailNote, detailNoteDirty,
|
||||
detailNoteEditor, detailNoteEditorView, detailNoteTags, draftBlockingIssues, editingExpenseId, creatingExpense, expenseEditor,
|
||||
employeeRiskProfile, employeeRiskProfileError, employeeRiskProfileLoading,
|
||||
expenseItems, expenseTableColumnCount, expenseTotal, expenseUploadInput,
|
||||
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
||||
goToNextSubmitRisk, goToPreviousSubmitRisk,
|
||||
@@ -1957,9 +2070,9 @@ export default {
|
||||
requiresApprovalOpinion,
|
||||
riskOverrideReasons, saveDetailNote, savingDetailNote, savingExpenseId,
|
||||
showAiAdvicePanel, showApplicationLeaderOpinion,
|
||||
showBudgetAnalysis, showEmployeeRiskProfile,
|
||||
showExpenseRisk, startExpenseEdit, submitBusy, submitConfirmDialogOpen,
|
||||
submitRiskWarnings,
|
||||
showBudgetAnalysis, showStageRiskAdvice,
|
||||
showExpenseRisk, startExpenseEdit, submitActionIcon, submitActionLabel, submitBusy,
|
||||
submitConfirmDescription, submitConfirmDialogOpen, submitConfirmText, submitRiskWarnings,
|
||||
triggerExpenseUpload, uploadedExpenseCount, uploadingExpenseId, saveExpenseEdit
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user