2026-06-22 11:58:53 +08:00
|
|
|
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
2026-06-26 22:42:29 +08:00
|
|
|
import {
|
|
|
|
|
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
|
|
|
|
runAiApplicationPreviewAction
|
|
|
|
|
} from '../../services/aiApplicationPreviewActions.js'
|
|
|
|
|
import { calculateTravelReimbursement } from '../../services/reimbursements.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
import {
|
|
|
|
|
canApproveBudgetExpenseApplications,
|
|
|
|
|
canApproveLeaderExpenseClaims,
|
|
|
|
|
canManageExpenseClaims,
|
|
|
|
|
canReturnExpenseClaims,
|
|
|
|
|
isCurrentDirectManagerForRequest,
|
|
|
|
|
isCurrentRequestApplicant,
|
|
|
|
|
isFinanceUser,
|
|
|
|
|
isPlatformAdminUser
|
|
|
|
|
} from '../../utils/accessControl.js'
|
|
|
|
|
import {
|
|
|
|
|
buildLeaderApprovalEvents,
|
|
|
|
|
buildLeaderApprovalInfo
|
|
|
|
|
} from '../../utils/applicationApproval.js'
|
|
|
|
|
import {
|
|
|
|
|
buildApplicationDetailFactItems,
|
|
|
|
|
buildRelatedApplicationFactItems
|
|
|
|
|
} from '../../utils/expenseApplicationDetail.js'
|
|
|
|
|
import { buildRiskViewerContext } from '../../utils/riskVisibility.js'
|
|
|
|
|
import { resolveProgressStepsForViewer } from '../../utils/requestProgressViewer.js'
|
|
|
|
|
import { isArchivedRequestView, normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
2026-06-26 22:42:29 +08:00
|
|
|
import {
|
|
|
|
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
|
|
|
|
applyApplicationPolicyEstimateError,
|
|
|
|
|
applyApplicationPolicyEstimateResult,
|
|
|
|
|
buildApplicationPolicyEstimateRequest,
|
|
|
|
|
normalizeApplicationPreview
|
|
|
|
|
} from '../../utils/expenseApplicationPreview.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
import {
|
|
|
|
|
EXPENSE_TYPE_OPTIONS,
|
|
|
|
|
buildFallbackExpenseItems,
|
|
|
|
|
buildFallbackProgressSteps,
|
|
|
|
|
isApplicationDocumentRequest,
|
|
|
|
|
rebuildExpenseItems,
|
|
|
|
|
resolveExpenseReasonHelper,
|
|
|
|
|
resolveExpenseReasonPlaceholder
|
|
|
|
|
} from './travelRequestDetailExpenseModel.js'
|
|
|
|
|
import { useTravelRequestPaymentFlow } from './travelRequestDetailPaymentFlow.js'
|
|
|
|
|
import { useTravelRequestDetailApprovalFlow } from './useTravelRequestDetailApprovalFlow.js'
|
|
|
|
|
import { useTravelRequestDetailAttachmentPreview } from './useTravelRequestDetailAttachmentPreview.js'
|
|
|
|
|
import { useTravelRequestDetailExpenseEditor } from './useTravelRequestDetailExpenseEditor.js'
|
|
|
|
|
import { useTravelRequestDetailRiskSubmit } from './useTravelRequestDetailRiskSubmit.js'
|
|
|
|
|
|
|
|
|
|
export function useTravelRequestDetailSetup(props, { emit }) {
|
|
|
|
|
const { toast } = useToast()
|
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
const expenseItems = ref([])
|
|
|
|
|
const expenseAttachmentMeta = reactive({})
|
2026-06-26 22:42:29 +08:00
|
|
|
const applicationDetailEditor = reactive({
|
|
|
|
|
fieldKey: '',
|
|
|
|
|
draftValue: '',
|
|
|
|
|
saving: false
|
|
|
|
|
})
|
2026-06-22 11:58:53 +08:00
|
|
|
const riskFlagPreviewSnapshot = ref(null)
|
|
|
|
|
let actionBusy = { value: false }
|
|
|
|
|
const getActionBusy = () => Boolean(actionBusy?.value)
|
|
|
|
|
|
|
|
|
|
const request = computed(() => {
|
|
|
|
|
const normalized = normalizeRequestForUi(props.request)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
normalized || {
|
|
|
|
|
id: 'EXP-202605-000',
|
|
|
|
|
claimId: '',
|
|
|
|
|
reason: '待补充报销事由',
|
|
|
|
|
typeLabel: '其他费用',
|
|
|
|
|
typeCode: 'other',
|
|
|
|
|
detailVariant: 'general',
|
|
|
|
|
sceneTarget: '待补充',
|
|
|
|
|
location: '待补充',
|
|
|
|
|
occurredDisplay: '待补充',
|
|
|
|
|
applyTime: '待补充',
|
|
|
|
|
amountDisplay: '¥0',
|
|
|
|
|
amountValue: 0,
|
|
|
|
|
node: '待提交',
|
|
|
|
|
approval: '草稿',
|
|
|
|
|
approvalKey: 'draft',
|
|
|
|
|
approvalTone: 'draft',
|
|
|
|
|
secondaryStatusLabel: '票据状态',
|
|
|
|
|
secondaryStatusValue: '待补充',
|
|
|
|
|
secondaryStatusTone: 'warning',
|
|
|
|
|
relatedCustomer: '待补充',
|
|
|
|
|
attachmentSummary: '待补充',
|
|
|
|
|
riskSummary: '待补充',
|
|
|
|
|
note: '',
|
|
|
|
|
profileIdentity: '员工',
|
|
|
|
|
profilePosition: '待补充',
|
|
|
|
|
profileGrade: '待补充',
|
|
|
|
|
profileManager: '待补充',
|
|
|
|
|
profileName: '当前申请人',
|
|
|
|
|
profileDepartment: '待补充部门',
|
|
|
|
|
profileAvatar: '申'
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const isApplicationDocument = computed(() => isApplicationDocumentRequest(request.value))
|
|
|
|
|
const isTravelRequest = computed(() => request.value.detailVariant === 'travel' && !isApplicationDocument.value)
|
|
|
|
|
const isDraftRequest = computed(() => request.value.approvalKey === 'draft')
|
|
|
|
|
const isEditableRequest = computed(() => ['draft', 'supplement'].includes(request.value.approvalKey))
|
|
|
|
|
const canOpenAiEntry = computed(() => isEditableRequest.value)
|
|
|
|
|
const isCurrentApplicant = computed(() => isCurrentRequestApplicant(request.value, currentUser.value))
|
2026-06-26 22:42:29 +08:00
|
|
|
const canModifyApplication = computed(() => (
|
2026-06-22 11:58:53 +08:00
|
|
|
isApplicationDocument.value
|
|
|
|
|
&& isEditableRequest.value
|
|
|
|
|
&& isCurrentApplicant.value
|
|
|
|
|
))
|
|
|
|
|
const canManageCurrentClaim = computed(() => canManageExpenseClaims(currentUser.value))
|
|
|
|
|
const isArchivedRequest = computed(() => isArchivedRequestView(request.value))
|
|
|
|
|
const isApplicantDeletableRequest = computed(() => {
|
|
|
|
|
if (!isCurrentApplicant.value) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
const status = String(request.value.status || request.value.approvalKey || '').trim().toLowerCase()
|
|
|
|
|
return ['draft', 'supplement', 'returned'].includes(status)
|
|
|
|
|
})
|
|
|
|
|
const canDeleteRequest = computed(() => {
|
|
|
|
|
if (isPlatformAdminUser(currentUser.value)) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
return isApplicantDeletableRequest.value
|
|
|
|
|
})
|
|
|
|
|
const isDirectManagerApprovalStage = computed(() => {
|
|
|
|
|
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
|
|
|
|
return node === '直属领导审批'
|
|
|
|
|
})
|
|
|
|
|
const isFinanceApprovalStage = computed(() => {
|
|
|
|
|
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
|
|
|
|
return node === '财务审批'
|
|
|
|
|
})
|
|
|
|
|
const isBudgetApprovalStage = computed(() => {
|
|
|
|
|
const node = String(request.value.node || request.value.approvalStage || '').trim()
|
|
|
|
|
return node === '预算管理者审批'
|
|
|
|
|
})
|
|
|
|
|
const isCurrentDirectManagerApprover = computed(() => (
|
|
|
|
|
canApproveLeaderExpenseClaims(currentUser.value)
|
|
|
|
|
&& isCurrentDirectManagerForRequest(request.value, currentUser.value)
|
|
|
|
|
))
|
|
|
|
|
const canProcessFinanceApprovalStage = computed(() => (
|
|
|
|
|
!isApplicationDocument.value
|
|
|
|
|
&& isFinanceApprovalStage.value
|
|
|
|
|
&& isFinanceUser(currentUser.value)
|
|
|
|
|
&& !isCurrentApplicant.value
|
|
|
|
|
))
|
|
|
|
|
const canProcessBudgetApprovalStage = computed(() => (
|
|
|
|
|
isApplicationDocument.value
|
|
|
|
|
&& isBudgetApprovalStage.value
|
|
|
|
|
&& canApproveBudgetExpenseApplications(currentUser.value, request.value)
|
|
|
|
|
&& !isCurrentApplicant.value
|
|
|
|
|
))
|
|
|
|
|
const showBudgetAnalysis = computed(() => (
|
|
|
|
|
isApplicationDocument.value
|
|
|
|
|
&& isBudgetApprovalStage.value
|
|
|
|
|
&& canApproveBudgetExpenseApplications(currentUser.value, request.value)
|
|
|
|
|
&& !isCurrentApplicant.value
|
|
|
|
|
))
|
|
|
|
|
const canProcessCurrentApprovalStage = computed(() => {
|
|
|
|
|
if (isDirectManagerApprovalStage.value) {
|
|
|
|
|
return isCurrentDirectManagerApprover.value
|
|
|
|
|
}
|
|
|
|
|
if (isBudgetApprovalStage.value) {
|
|
|
|
|
return canProcessBudgetApprovalStage.value
|
|
|
|
|
}
|
|
|
|
|
return canProcessFinanceApprovalStage.value
|
|
|
|
|
})
|
|
|
|
|
const canReturnRequest = computed(() => {
|
|
|
|
|
if (request.value.approvalKey !== 'in_progress' || !request.value.claimId || !canReturnExpenseClaims(currentUser.value)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return canProcessCurrentApprovalStage.value
|
|
|
|
|
})
|
|
|
|
|
const canApproveRequest = computed(() =>
|
|
|
|
|
request.value.approvalKey === 'in_progress'
|
|
|
|
|
&& Boolean(request.value.claimId)
|
|
|
|
|
&& canProcessCurrentApprovalStage.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 paymentFlow = useTravelRequestPaymentFlow({
|
|
|
|
|
request,
|
|
|
|
|
currentUser,
|
|
|
|
|
isApplicationDocument,
|
|
|
|
|
isCurrentApplicant,
|
|
|
|
|
toast,
|
|
|
|
|
emit
|
|
|
|
|
})
|
|
|
|
|
const attachmentPreview = useTravelRequestDetailAttachmentPreview({
|
|
|
|
|
request,
|
|
|
|
|
expenseItems,
|
|
|
|
|
expenseAttachmentMeta
|
|
|
|
|
})
|
|
|
|
|
const riskSubmit = useTravelRequestDetailRiskSubmit({
|
|
|
|
|
request,
|
|
|
|
|
expenseItems,
|
|
|
|
|
expenseAttachmentMeta,
|
|
|
|
|
riskFlagPreviewSnapshot,
|
|
|
|
|
isApplicationDocument,
|
|
|
|
|
isEditableRequest,
|
|
|
|
|
isDraftRequest,
|
|
|
|
|
isCurrentApplicant,
|
|
|
|
|
canViewApprovalRiskAdvice,
|
|
|
|
|
riskViewerContext,
|
|
|
|
|
getActionBusy,
|
|
|
|
|
toast,
|
|
|
|
|
emit
|
|
|
|
|
})
|
|
|
|
|
const approvalFlow = useTravelRequestDetailApprovalFlow({
|
|
|
|
|
request,
|
|
|
|
|
isApplicationDocument,
|
|
|
|
|
isDraftRequest,
|
|
|
|
|
isArchivedRequest,
|
|
|
|
|
isFinanceApprovalStage,
|
|
|
|
|
isBudgetApprovalStage,
|
|
|
|
|
canDeleteRequest,
|
|
|
|
|
canReturnRequest,
|
|
|
|
|
canApproveRequest,
|
|
|
|
|
approvalRiskConfirmItems: riskSubmit.approvalRiskConfirmItems,
|
|
|
|
|
canViewApprovalRiskAdvice,
|
|
|
|
|
toast,
|
|
|
|
|
emit
|
|
|
|
|
})
|
|
|
|
|
const expenseEditor = useTravelRequestDetailExpenseEditor({
|
|
|
|
|
request,
|
|
|
|
|
expenseItems,
|
|
|
|
|
expenseAttachmentMeta,
|
|
|
|
|
isEditableRequest,
|
|
|
|
|
getActionBusy,
|
|
|
|
|
toast,
|
|
|
|
|
emit,
|
|
|
|
|
attachmentPreviewOpen: attachmentPreview.attachmentPreviewOpen,
|
|
|
|
|
buildAttachmentRiskNotice: attachmentPreview.buildAttachmentRiskNotice,
|
|
|
|
|
closeAttachmentPreview: attachmentPreview.closeAttachmentPreview,
|
|
|
|
|
refreshExpenseAttachmentMeta: attachmentPreview.refreshExpenseAttachmentMeta,
|
|
|
|
|
resolveAttachmentMeta: attachmentPreview.resolveAttachmentMeta,
|
|
|
|
|
resolveClaimRiskFlags: riskSubmit.resolveClaimRiskFlags,
|
|
|
|
|
applyClaimRiskFlagsPayload: riskSubmit.applyClaimRiskFlagsPayload
|
|
|
|
|
})
|
|
|
|
|
const {
|
|
|
|
|
deletingAttachmentId,
|
|
|
|
|
deletingExpenseId,
|
|
|
|
|
savingExpenseId,
|
|
|
|
|
smartEntryRecognitionBusy,
|
|
|
|
|
uploadingExpenseId
|
|
|
|
|
} = expenseEditor
|
|
|
|
|
|
|
|
|
|
actionBusy = computed(() =>
|
|
|
|
|
Boolean(savingExpenseId.value)
|
|
|
|
|
|| riskSubmit.submitBusy.value
|
|
|
|
|
|| approvalFlow.deleteBusy.value
|
|
|
|
|
|| approvalFlow.returnBusy.value
|
|
|
|
|
|| approvalFlow.approveBusy.value
|
|
|
|
|
|| paymentFlow.payBusy.value
|
2026-06-26 22:42:29 +08:00
|
|
|
|| applicationDetailEditor.saving
|
2026-06-22 11:58:53 +08:00
|
|
|
|| smartEntryRecognitionBusy.value
|
|
|
|
|
|| Boolean(uploadingExpenseId.value)
|
|
|
|
|
|| Boolean(deletingAttachmentId.value)
|
|
|
|
|
|| Boolean(deletingExpenseId.value)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const profile = computed(() => ({
|
|
|
|
|
name: request.value.profileName,
|
|
|
|
|
identity: request.value.profileIdentity,
|
|
|
|
|
position: request.value.profilePosition,
|
|
|
|
|
department: request.value.profileDepartment,
|
|
|
|
|
grade: request.value.profileGrade,
|
|
|
|
|
manager: request.value.profileManager,
|
|
|
|
|
avatar: request.value.profileAvatar
|
|
|
|
|
}))
|
|
|
|
|
const leaderApprovalInfo = computed(() => buildLeaderApprovalInfo(request.value))
|
|
|
|
|
const leaderApprovalEvents = computed(() => buildLeaderApprovalEvents(request.value))
|
|
|
|
|
const hasLeaderApprovalEvents = computed(() => leaderApprovalEvents.value.length > 0)
|
|
|
|
|
const hasSingleLeaderApprovalEvent = computed(() => leaderApprovalEvents.value.length === 1)
|
|
|
|
|
const leaderApprovalReadonlyMeta = computed(() => {
|
|
|
|
|
const pieces = hasLeaderApprovalEvents.value ? [`${leaderApprovalEvents.value.length} 条批复记录`] : []
|
|
|
|
|
if (leaderApprovalInfo.value.generatedDraftClaimNo) {
|
|
|
|
|
pieces.push(`已生成报销草稿 ${leaderApprovalInfo.value.generatedDraftClaimNo}`)
|
|
|
|
|
}
|
|
|
|
|
return pieces.join(' · ')
|
|
|
|
|
})
|
|
|
|
|
const showApplicationLeaderOpinion = computed(() => (
|
|
|
|
|
isApplicationDocument.value
|
|
|
|
|
&& hasLeaderApprovalEvents.value
|
|
|
|
|
))
|
|
|
|
|
const heroFactItems = computed(() => [
|
|
|
|
|
{
|
|
|
|
|
key: 'document',
|
|
|
|
|
label: isApplicationDocument.value ? '申请单号' : '报销单号',
|
|
|
|
|
value: request.value.documentNo || request.value.id,
|
|
|
|
|
icon: 'mdi mdi-camera-outline',
|
|
|
|
|
valueClass: ''
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'date',
|
|
|
|
|
label: '单据申请日期',
|
|
|
|
|
value: request.value.applyTime || request.value.occurredDisplay,
|
|
|
|
|
icon: 'mdi mdi-calendar-month-outline',
|
|
|
|
|
valueClass: ''
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'amount',
|
|
|
|
|
label: isApplicationDocument.value ? '预计金额' : '报销金额',
|
|
|
|
|
value: request.value.amountDisplay,
|
|
|
|
|
icon: '',
|
|
|
|
|
valueClass: 'amount'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'type',
|
|
|
|
|
label: isApplicationDocument.value ? '申请类型' : isTravelRequest.value ? '差旅类型' : '报销类型',
|
|
|
|
|
value: request.value.typeLabel,
|
|
|
|
|
icon: '',
|
|
|
|
|
valueClass: ''
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
const progressSteps = computed(() => {
|
|
|
|
|
const sourceSteps = Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
|
|
|
|
|
? request.value.progressSteps
|
|
|
|
|
: buildFallbackProgressSteps(request.value)
|
|
|
|
|
return resolveProgressStepsForViewer(sourceSteps, {
|
|
|
|
|
isApplicationDocument: isApplicationDocument.value,
|
|
|
|
|
isCurrentDirectManagerApprover: isCurrentDirectManagerApprover.value
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
const currentProgressRingMotion = {
|
|
|
|
|
initial: { scale: 1, opacity: 0.34 },
|
|
|
|
|
enter: {
|
|
|
|
|
scale: [1, 1.42, 1.78],
|
|
|
|
|
opacity: [0.34, 0.16, 0],
|
|
|
|
|
transition: {
|
|
|
|
|
duration: 3.2,
|
|
|
|
|
repeat: Infinity,
|
|
|
|
|
repeatType: 'loop',
|
|
|
|
|
repeatDelay: 0.85,
|
|
|
|
|
ease: 'easeOut',
|
|
|
|
|
times: [0, 0.5, 1]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const submitConfirmAmountDisplay = computed(() =>
|
|
|
|
|
isApplicationDocument.value ? (request.value.amountDisplay || expenseEditor.expenseTotal.value) : expenseEditor.expenseTotal.value
|
|
|
|
|
)
|
|
|
|
|
const applicationDetailFactItems = computed(() => buildApplicationDetailFactItems(request.value))
|
|
|
|
|
const relatedApplicationFactItems = computed(() => buildRelatedApplicationFactItems(request.value))
|
2026-06-26 22:42:29 +08:00
|
|
|
const applicationEditEditableFields = ['reason', 'time', 'location', 'transportMode']
|
|
|
|
|
const applicationDetailEditableFactKeys = new Set([
|
|
|
|
|
'reason',
|
|
|
|
|
'location',
|
|
|
|
|
'transport_mode',
|
|
|
|
|
'trip_start_time',
|
|
|
|
|
'trip_return_time',
|
|
|
|
|
'time'
|
|
|
|
|
])
|
2026-06-22 11:58:53 +08:00
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
request,
|
|
|
|
|
(nextRequest, previousRequest) => {
|
|
|
|
|
expenseItems.value =
|
|
|
|
|
Array.isArray(nextRequest.expenseItems)
|
|
|
|
|
? rebuildExpenseItems(nextRequest.expenseItems, nextRequest)
|
|
|
|
|
: buildFallbackExpenseItems(nextRequest)
|
|
|
|
|
if (nextRequest.claimId !== previousRequest?.claimId) {
|
|
|
|
|
Object.keys(expenseAttachmentMeta).forEach((key) => {
|
|
|
|
|
delete expenseAttachmentMeta[key]
|
|
|
|
|
})
|
|
|
|
|
riskSubmit.resetSubmitWorkState()
|
|
|
|
|
attachmentPreview.closeAttachmentPreview()
|
|
|
|
|
}
|
|
|
|
|
expenseEditor.resetExpenseWorkState()
|
2026-06-26 22:42:29 +08:00
|
|
|
cancelApplicationDetailEditor()
|
2026-06-22 11:58:53 +08:00
|
|
|
void attachmentPreview.syncExpenseAttachmentMeta()
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
)
|
|
|
|
|
watch(
|
|
|
|
|
() => request.value.claimId,
|
|
|
|
|
() => {
|
|
|
|
|
riskSubmit.clearRiskFlagPreviewSnapshot()
|
|
|
|
|
expenseEditor.resetSmartEntryRecognitionApplications()
|
|
|
|
|
expenseEditor.bindSmartEntryRecognitionTask()
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
function buildApplicationEditPreview() {
|
|
|
|
|
const factEntries = applicationDetailFactItems.value
|
|
|
|
|
.map((item) => [String(item?.label || '').trim(), String(item?.value || '').trim()])
|
|
|
|
|
.filter(([label, value]) => label && value)
|
|
|
|
|
const facts = new Map(factEntries)
|
|
|
|
|
const pickFact = (...labels) => {
|
|
|
|
|
for (const label of labels) {
|
|
|
|
|
const value = facts.get(label)
|
|
|
|
|
if (value) {
|
|
|
|
|
return value
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
const tripStart = pickFact('出发时间')
|
|
|
|
|
const tripReturn = pickFact('返回时间')
|
|
|
|
|
const time = tripStart && tripReturn && tripStart !== tripReturn
|
|
|
|
|
? `${tripStart} 至 ${tripReturn}`
|
|
|
|
|
: pickFact('行程时间', '申请时间', '招待时间', '发生时间') || tripStart
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
sourceText: '修改申请',
|
|
|
|
|
modelReviewStatus: 'template',
|
2026-06-26 22:42:29 +08:00
|
|
|
applicationEditMode: true,
|
|
|
|
|
editableFields: applicationEditEditableFields,
|
2026-06-22 11:58:53 +08:00
|
|
|
fields: {
|
|
|
|
|
applicationType: pickFact('申请类型') || request.value.typeLabel || '费用申请',
|
|
|
|
|
applicant: request.value.profileName || request.value.person || request.value.applicant || '',
|
|
|
|
|
grade: pickFact('职级') || request.value.profileGrade || '',
|
|
|
|
|
department: request.value.profileDepartment || request.value.departmentName || request.value.department || '',
|
|
|
|
|
position: request.value.profilePosition || request.value.employeePosition || request.value.position || '',
|
|
|
|
|
managerName: request.value.profileManager || request.value.managerName || request.value.manager || '',
|
|
|
|
|
time,
|
|
|
|
|
location: pickFact('地点') || request.value.location || request.value.city || '',
|
|
|
|
|
reason: pickFact('事由') || request.value.reason || '',
|
|
|
|
|
days: pickFact('天数'),
|
|
|
|
|
transportMode: pickFact('出行方式'),
|
|
|
|
|
lodgingDailyCap: pickFact('住宿上限/天'),
|
|
|
|
|
subsidyDailyCap: pickFact('补贴标准/天'),
|
|
|
|
|
transportPolicy: pickFact('交通费用口径'),
|
|
|
|
|
policyEstimate: pickFact('规则测算参考'),
|
|
|
|
|
amount: pickFact('系统预估费用', '用户预估费用', '预计金额') || request.value.amountDisplay || request.value.amount || ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:29 +08:00
|
|
|
function buildApplicationEditDraftPayload() {
|
|
|
|
|
const claimId = String(request.value?.claimId || '').trim()
|
|
|
|
|
const claimNo = String(request.value?.claimNo || request.value?.documentNo || request.value?.id || '').trim()
|
|
|
|
|
return {
|
|
|
|
|
draft_type: 'expense_application',
|
|
|
|
|
document_type: 'expense_application',
|
|
|
|
|
claim_id: claimId,
|
|
|
|
|
claim_no: claimNo,
|
|
|
|
|
status: String(request.value?.status || request.value?.approvalKey || '').trim(),
|
|
|
|
|
approval_stage: String(request.value?.node || request.value?.approvalStage || '待提交').trim(),
|
|
|
|
|
title: String(request.value?.typeLabel || '费用申请').trim(),
|
|
|
|
|
application_edit_mode: true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeApplicationDetailEditorValue(value = '') {
|
|
|
|
|
const text = String(value || '').trim()
|
|
|
|
|
return text === '待补充' ? '' : text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveApplicationDetailFactValue(key = '') {
|
|
|
|
|
const targetKey = String(key || '').trim()
|
|
|
|
|
return String(applicationDetailFactItems.value.find((item) => item?.key === targetKey)?.value || '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildApplicationDetailDateRange(startDate = '', endDate = '') {
|
|
|
|
|
const start = String(startDate || '').trim()
|
|
|
|
|
const end = String(endDate || '').trim()
|
|
|
|
|
if (!start && !end) return ''
|
|
|
|
|
if (!start) return end
|
|
|
|
|
if (!end || end === start) return start
|
|
|
|
|
return `${start} 至 ${end}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveApplicationDetailDays(startDate = '', endDate = '') {
|
|
|
|
|
const start = String(startDate || '').trim()
|
|
|
|
|
const end = String(endDate || '').trim()
|
|
|
|
|
if (!start || !end) return ''
|
|
|
|
|
const startTime = new Date(`${start}T00:00:00`).getTime()
|
|
|
|
|
const endTime = new Date(`${end}T00:00:00`).getTime()
|
|
|
|
|
if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
return `${Math.round((endTime - startTime) / 86400000) + 1}天`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function canEditApplicationDetailItem(item = {}) {
|
|
|
|
|
return (
|
|
|
|
|
canModifyApplication.value
|
|
|
|
|
&& applicationDetailEditableFactKeys.has(String(item?.key || '').trim())
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isApplicationDetailEditing(item = {}) {
|
|
|
|
|
return String(applicationDetailEditor.fieldKey || '') === String(item?.key || '')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveApplicationDetailEditorControl(item = {}) {
|
|
|
|
|
const key = String(item?.key || '').trim()
|
|
|
|
|
if (['trip_start_time', 'trip_return_time', 'time'].includes(key)) {
|
|
|
|
|
return 'date'
|
|
|
|
|
}
|
|
|
|
|
if (key === 'transport_mode') {
|
|
|
|
|
return 'select'
|
|
|
|
|
}
|
|
|
|
|
return 'text'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function openApplicationDetailEditor(item = {}) {
|
|
|
|
|
if (!canEditApplicationDetailItem(item) || applicationDetailEditor.saving) {
|
2026-06-22 11:58:53 +08:00
|
|
|
return
|
|
|
|
|
}
|
2026-06-26 22:42:29 +08:00
|
|
|
applicationDetailEditor.fieldKey = String(item.key || '').trim()
|
|
|
|
|
applicationDetailEditor.draftValue = normalizeApplicationDetailEditorValue(item.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cancelApplicationDetailEditor() {
|
|
|
|
|
applicationDetailEditor.fieldKey = ''
|
|
|
|
|
applicationDetailEditor.draftValue = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildEditedApplicationPreview(item = {}) {
|
|
|
|
|
const key = String(item?.key || '').trim()
|
|
|
|
|
const nextValue = normalizeApplicationDetailEditorValue(applicationDetailEditor.draftValue)
|
|
|
|
|
const preview = buildApplicationEditPreview()
|
|
|
|
|
const fields = { ...(preview.fields || {}) }
|
|
|
|
|
|
|
|
|
|
if (key === 'reason') {
|
|
|
|
|
fields.reason = nextValue
|
|
|
|
|
} else if (key === 'location') {
|
|
|
|
|
fields.location = nextValue
|
|
|
|
|
} else if (key === 'transport_mode') {
|
|
|
|
|
fields.transportMode = nextValue
|
|
|
|
|
} else if (key === 'time') {
|
|
|
|
|
fields.time = nextValue
|
|
|
|
|
} else if (key === 'trip_start_time' || key === 'trip_return_time') {
|
|
|
|
|
const startDate = key === 'trip_start_time'
|
|
|
|
|
? nextValue
|
|
|
|
|
: resolveApplicationDetailFactValue('trip_start_time')
|
|
|
|
|
const endDate = key === 'trip_return_time'
|
|
|
|
|
? nextValue
|
|
|
|
|
: resolveApplicationDetailFactValue('trip_return_time')
|
|
|
|
|
fields.time = buildApplicationDetailDateRange(startDate, endDate)
|
|
|
|
|
fields.days = resolveApplicationDetailDays(startDate, endDate) || fields.days
|
|
|
|
|
}
|
2026-06-22 11:58:53 +08:00
|
|
|
|
2026-06-26 22:42:29 +08:00
|
|
|
return normalizeApplicationPreview({
|
|
|
|
|
...preview,
|
|
|
|
|
fields
|
2026-06-22 11:58:53 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 22:42:29 +08:00
|
|
|
async function refreshEditedApplicationPreviewEstimate(preview = {}) {
|
|
|
|
|
const estimateRequest = buildApplicationPolicyEstimateRequest(preview, currentUser.value || {})
|
|
|
|
|
if (!estimateRequest.canCalculate) {
|
|
|
|
|
return preview
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
const result = await calculateTravelReimbursement(estimateRequest.payload)
|
|
|
|
|
return applyApplicationPolicyEstimateResult(preview, result, currentUser.value || {})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return applyApplicationPolicyEstimateError(preview, error, currentUser.value || {})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveApplicationDetailEdit(item = {}) {
|
|
|
|
|
if (!isApplicationDetailEditing(item) || applicationDetailEditor.saving) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (!String(request.value?.claimId || '').trim()) {
|
|
|
|
|
toast('当前申请缺少单据标识,暂不能修改。')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applicationDetailEditor.saving = true
|
|
|
|
|
try {
|
|
|
|
|
const preview = await refreshEditedApplicationPreviewEstimate(buildEditedApplicationPreview(item))
|
|
|
|
|
const payload = await runAiApplicationPreviewAction({
|
|
|
|
|
actionType: AI_APPLICATION_ACTION_SAVE_DRAFT,
|
|
|
|
|
applicationPreview: preview,
|
|
|
|
|
currentUser: currentUser.value || {},
|
|
|
|
|
draftPayload: buildApplicationEditDraftPayload()
|
|
|
|
|
})
|
|
|
|
|
const draftPayload = payload?.result?.draft_payload || payload?.draft_payload || {}
|
|
|
|
|
emit('request-updated', {
|
|
|
|
|
claimId: String(draftPayload.claim_id || request.value.claimId || '').trim(),
|
|
|
|
|
claimNo: String(draftPayload.claim_no || request.value.claimNo || request.value.documentNo || '').trim(),
|
|
|
|
|
status: String(draftPayload.status || request.value.status || '').trim(),
|
|
|
|
|
approvalStage: String(draftPayload.approval_stage || request.value.node || '').trim()
|
|
|
|
|
})
|
|
|
|
|
cancelApplicationDetailEditor()
|
|
|
|
|
toast('申请信息已更新。')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
toast(error?.message || '申请信息更新失败,请稍后重试。')
|
|
|
|
|
} finally {
|
|
|
|
|
applicationDetailEditor.saving = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
riskSubmit.disposeRiskSubmit()
|
|
|
|
|
expenseEditor.disposeExpenseEditor()
|
|
|
|
|
attachmentPreview.closeAttachmentPreview()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
emit,
|
|
|
|
|
actionBusy,
|
|
|
|
|
...attachmentPreview,
|
|
|
|
|
...approvalFlow,
|
|
|
|
|
...expenseEditor,
|
|
|
|
|
...paymentFlow,
|
|
|
|
|
...riskSubmit,
|
2026-06-26 22:42:29 +08:00
|
|
|
APPLICATION_TRANSPORT_MODE_OPTIONS,
|
|
|
|
|
applicationDetailEditor,
|
2026-06-22 11:58:53 +08:00
|
|
|
applicationDetailFactItems,
|
|
|
|
|
relatedApplicationFactItems,
|
2026-06-26 22:42:29 +08:00
|
|
|
canEditApplicationDetailItem,
|
2026-06-22 11:58:53 +08:00
|
|
|
canDeleteRequest,
|
|
|
|
|
canManageCurrentClaim,
|
2026-06-26 22:42:29 +08:00
|
|
|
canModifyApplication,
|
2026-06-22 11:58:53 +08:00
|
|
|
canOpenAiEntry,
|
|
|
|
|
canApproveRequest,
|
|
|
|
|
canReturnRequest,
|
|
|
|
|
currentProgressRingMotion,
|
|
|
|
|
expenseItems,
|
|
|
|
|
expenseTypeOptions: EXPENSE_TYPE_OPTIONS,
|
|
|
|
|
hasLeaderApprovalEvents,
|
|
|
|
|
hasSingleLeaderApprovalEvent,
|
|
|
|
|
heroFactItems,
|
|
|
|
|
isApplicationDocument,
|
2026-06-26 22:42:29 +08:00
|
|
|
isApplicationDetailEditing,
|
2026-06-22 11:58:53 +08:00
|
|
|
isDraftRequest,
|
|
|
|
|
isEditableRequest,
|
|
|
|
|
isTravelRequest,
|
|
|
|
|
leaderApprovalEvents,
|
|
|
|
|
leaderApprovalReadonlyMeta,
|
|
|
|
|
profile,
|
|
|
|
|
progressSteps,
|
|
|
|
|
request,
|
2026-06-26 22:42:29 +08:00
|
|
|
cancelApplicationDetailEditor,
|
|
|
|
|
openApplicationDetailEditor,
|
2026-06-22 11:58:53 +08:00
|
|
|
resolveExpenseReasonHelper,
|
|
|
|
|
resolveExpenseReasonPlaceholder,
|
2026-06-26 22:42:29 +08:00
|
|
|
resolveApplicationDetailEditorControl,
|
|
|
|
|
saveApplicationDetailEdit,
|
2026-06-22 11:58:53 +08:00
|
|
|
showApplicationLeaderOpinion,
|
|
|
|
|
showBudgetAnalysis,
|
|
|
|
|
showStageRiskAdvice,
|
|
|
|
|
submitConfirmAmountDisplay
|
|
|
|
|
}
|
|
|
|
|
}
|