export const EXPENSE_TYPE_OPTIONS = [ { value: 'travel', label: '差旅费' }, { value: 'train_ticket', label: '火车票' }, { value: 'flight_ticket', label: '机票' }, { value: 'ship_ticket', label: '轮船票' }, { value: 'ferry_ticket', label: '轮船票' }, { value: 'hotel_ticket', label: '住宿票' }, { value: 'ride_ticket', label: '乘车' }, { value: 'office', label: '办公用品费' }, { value: 'meeting', label: '会务费' }, { value: 'training', label: '培训费' }, { value: 'hotel', label: '住宿费' }, { value: 'transport', label: '交通费' }, { value: 'meal', label: '业务招待费' }, { value: 'travel_allowance', label: '出差补贴' }, { value: 'other', label: '其他费用' } ] const LEGACY_EXPENSE_TYPE_LABELS = { entertainment: '业务招待费' } export const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([ 'travel', 'meeting', 'entertainment' ]) export const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance']) export const OPTIONAL_ATTACHMENT_EXPENSE_TYPES = new Set(['ride_ticket', 'travel_allowance']) export const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket']) export const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket']) export const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket']) export const ROUTE_DESCRIPTION_PATTERN = /^[A-Za-z0-9\u4e00-\u9fa5()()·]{2,40}\s*-\s*[A-Za-z0-9\u4e00-\u9fa5()()·]{2,40}$/ export const STANDARD_ADJUSTMENT_RISK_SOURCE = 'reimbursement_standard_adjustment' export function parseCurrency(value) { return Number.parseFloat(String(value).replace(/[^\d.]/g, '')) || 0 } function parseOptionalCurrency(value) { if (value === null || value === undefined || String(value).trim() === '') { return null } const normalized = String(value).replace(/[^\d.]/g, '') if (!normalized) { return null } const amount = Number.parseFloat(normalized) return Number.isFinite(amount) && amount >= 0 ? amount : null } export function formatCurrency(value) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(value) ? 0 : 2 }).format(value) } export function normalizeExpenseType(value) { return String(value || '').trim() || 'other' } export function isApplicationDocumentRequest(request) { const documentType = String( request?.documentTypeCode || request?.document_type_code || request?.documentType || request?.document_type || '' ).trim() const claimNo = String( request?.claimNo || request?.claim_no || request?.documentNo || request?.id || '' ).trim().toUpperCase() const typeCode = normalizeExpenseType(request?.typeCode || request?.expense_type) return ( documentType === 'application' || documentType === 'expense_application' || claimNo.startsWith('AP-') || claimNo.startsWith('APP-') || typeCode === 'application' || typeCode.endsWith('_application') ) } export function resolveExpenseTypeLabel(value) { const normalized = normalizeExpenseType(value) return EXPENSE_TYPE_OPTIONS.find((option) => option.value === normalized)?.label || LEGACY_EXPENSE_TYPE_LABELS[normalized] || '其他费用' } export function isSystemGeneratedExpenseItemSource(source) { const itemType = normalizeExpenseType(source?.itemType || source?.item_type) return Boolean(source?.isSystemGenerated || source?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) } export function isAttachmentRequiredExpenseItem(source) { const itemType = normalizeExpenseType(source?.itemType || source?.item_type) return !isSystemGeneratedExpenseItemSource({ ...source, itemType }) && !OPTIONAL_ATTACHMENT_EXPENSE_TYPES.has(itemType) } export function hasUploadedReceiptReference(source) { const invoiceId = String(source?.invoiceId ?? source?.invoice_id ?? '').trim() if (!isPlaceholderValue(invoiceId)) { return true } return Array.isArray(source?.attachments) && source.attachments.some((item) => !isPlaceholderValue(item)) } export function isIgnorableExpenseDraftPlaceholder(item) { if (!item || isSystemGeneratedExpenseItemSource(item) || hasUploadedReceiptReference(item)) { return false } const amount = Number(item?.itemAmount ?? item?.item_amount ?? 0) const missingAmount = !Number.isFinite(amount) || amount <= 0 const missingReason = isPlaceholderValue(item?.itemReason ?? item?.item_reason) const missingLocation = isPlaceholderValue(item?.itemLocation ?? item?.item_location) return missingAmount && missingReason && missingLocation } export function isLocationRequiredExpenseType(value) { return LOCATION_REQUIRED_EXPENSE_TYPES.has(normalizeExpenseType(value)) } export function resolveLocationSummaryLabel(value) { return isLocationRequiredExpenseType(value) ? '业务地点' : '采购/收货地点' } export function isRouteDescriptionExpenseType(value) { return ROUTE_DESCRIPTION_EXPENSE_TYPES.has(normalizeExpenseType(value)) } export function isHotelDescriptionExpenseType(value) { return HOTEL_DESCRIPTION_EXPENSE_TYPES.has(normalizeExpenseType(value)) } export function resolveExpenseDetailHint(expenseType) { if (isRouteDescriptionExpenseType(expenseType)) { return '起始地-目的地' } if (isHotelDescriptionExpenseType(expenseType)) { return '目的地酒店' } if (!isLocationRequiredExpenseType(expenseType)) { return '非必填' } return '待补充' } export function resolveLocationDisplay(value, expenseType) { return isPlaceholderValue(value) ? resolveExpenseDetailHint(expenseType) : value } export function isSyntheticLocationDisplay(value, expenseType) { const text = String(value || '').trim() return ['待补充', '非必填', resolveExpenseDetailHint(expenseType)].includes(text) } export function isValidRouteDescription(value) { const text = String(value || '').trim() return ROUTE_DESCRIPTION_PATTERN.test(text) && !/\d{4}[-/年.]\d{1,2}[-/月.]\d{1,2}/.test(text) } export function resolveExpenseReasonPlaceholder(itemType) { if (isRouteDescriptionExpenseType(itemType)) { return '起始地-目的地,例如:广州南-北京南' } if (isHotelDescriptionExpenseType(itemType)) { return '目的地酒店,例如:北京中心酒店' } return '输入费用说明' } export function resolveExpenseReasonHelper(itemType) { if (isRouteDescriptionExpenseType(itemType)) { return '起始地-目的地' } if (isHotelDescriptionExpenseType(itemType)) { return '目的地酒店' } return '业务报销说明' } export function resolveExpenseDescriptionDetail(itemType, itemLocation) { if (isRouteDescriptionExpenseType(itemType) || isHotelDescriptionExpenseType(itemType)) { return resolveExpenseReasonHelper(itemType) } return resolveLocationDisplay(itemLocation, itemType) } export function buildFallbackProgressSteps(requestModel = {}) { const node = String(requestModel?.node || requestModel?.workflowNode || requestModel?.approvalStage || '').trim() const approvalKey = String(requestModel?.approvalKey || '').trim() const pendingPayment = approvalKey === 'pending_payment' || /待付款/.test(node) const paid = /已付款/.test(node) const completed = approvalKey === 'completed' || paid || /审批完成|申请完成|已完成/.test(node) const archived = /申请归档|已归档/.test(node) const hasRelatedApplication = Boolean(requestModel?.relatedApplication?.claimNo) if (isApplicationDocumentRequest(requestModel)) { const inLeaderApproval = approvalKey === 'in_progress' || /直属领导|领导审批|审批中/.test(node) return [ { index: 1, label: '创建申请', time: completed || inLeaderApproval ? '已完成' : '进行中', done: completed || inLeaderApproval, active: true, current: !(completed || inLeaderApproval) }, { index: 2, label: '直属领导审批', time: completed ? '已完成' : inLeaderApproval ? '进行中' : '待处理', done: completed, active: completed || inLeaderApproval, current: !completed && inLeaderApproval }, { index: 3, label: '关联单据状态', time: hasRelatedApplication ? '已关联' : completed ? '未关联' : '待处理', done: archived, active: completed || archived, current: completed && !archived }, { index: 4, label: '已归档', time: archived ? '已完成' : '待处理', done: archived, active: archived, current: archived } ] } return [ { index: 1, label: '关联单据', time: hasRelatedApplication ? '已关联' : '待核对', done: hasRelatedApplication, active: true, current: !hasRelatedApplication }, { index: 2, label: '待提交', time: hasRelatedApplication ? '进行中' : '待处理', active: hasRelatedApplication, current: hasRelatedApplication }, { index: 3, label: '直属领导审批', time: '待处理' }, { index: 4, label: '财务审批', time: '待处理' }, { index: 5, label: '待付款', time: pendingPayment ? '进行中' : completed ? '已完成' : '待处理', done: completed, active: pendingPayment || completed, current: pendingPayment }, { index: 6, label: '已付款', time: paid || completed ? '已完成' : '待处理', done: paid || completed, active: paid || completed, current: false }, { index: 7, label: '已归档', time: paid || completed ? '已完成' : '待处理', done: paid || completed, active: paid || completed, current: false } ] } export function buildFallbackExpenseItems(request) { if (isApplicationDocumentRequest(request)) { return [] } return [ buildExpenseItemViewModel({ id: 'fallback-1', itemDate: '', itemType: request.typeCode || 'other', itemReason: request.reason, itemLocation: request.sceneTarget, itemAmount: parseCurrency(request.amountDisplay), invoiceId: '', time: '待补充', dayLabel: request.detailVariant === 'travel' ? '出行日' : '业务发生日', name: request.typeLabel, category: request.typeLabel, desc: request.reason, detail: resolveLocationDisplay(request.sceneTarget, request.typeCode), amount: request.amountDisplay, status: '待补充', tone: 'bad', attachmentStatus: '待上传', attachmentHint: '请在此单据中继续补充附件', attachmentTone: 'missing', attachments: [], riskLabel: '待补材料', riskText: request.riskSummary, riskTone: 'medium' }, 0, request) ] } export function isPlaceholderValue(value) { const text = String(value || '').trim() if (!text) { return true } return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, '')) } export function normalizeDetailNoteDraftValue(value) { const text = String(value || '').trim() return isPlaceholderValue(text) ? '' : text } export function isValidIsoDate(value) { const normalized = String(value || '').trim() if (!/^\d{4}-\d{2}-\d{2}$/.test(normalized)) { return false } const [yearText, monthText, dayText] = normalized.split('-') const year = Number(yearText) const month = Number(monthText) const day = Number(dayText) if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) { return false } const candidate = new Date(Date.UTC(year, month - 1, day)) return ( candidate.getUTCFullYear() === year && candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day ) } export function normalizeIsoDateValue(value) { const normalized = String(value || '').trim() if (isValidIsoDate(normalized)) { return normalized } const match = normalized.match(/^(\d{4}-\d{2}-\d{2})/) if (match && isValidIsoDate(match[1])) { return match[1] } const candidate = value instanceof Date ? value : new Date(normalized) if (Number.isNaN(candidate.getTime())) { return '' } const year = candidate.getFullYear() const month = String(candidate.getMonth() + 1).padStart(2, '0') const day = String(candidate.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } export function formatExpenseFilledTime(value) { const normalized = String(value || '').trim() if (!normalized) { return '' } const candidate = value instanceof Date ? value : new Date(normalized) if (Number.isNaN(candidate.getTime())) { return normalized } const year = candidate.getFullYear() const month = String(candidate.getMonth() + 1).padStart(2, '0') const day = String(candidate.getDate()).padStart(2, '0') const hours = String(candidate.getHours()).padStart(2, '0') const minutes = String(candidate.getMinutes()).padStart(2, '0') return `${year}-${month}-${day} ${hours}:${minutes}` } export function resolveExpenseUploadHint(value) { const normalized = String(value || '').trim() return normalized || '仅支持上传 1 张 JPG、PNG、PDF 单据' } export function extractAttachmentDisplayName(value) { const normalized = String(value || '').trim() if (!normalized) { return '' } return normalized.split('/').filter(Boolean).pop() || normalized } export function resolveExpenseItemViewId(source, index, requestModel) { return String(source?.id || `${requestModel?.claimId || requestModel?.id || 'claim'}-item-${index}`) } export function buildTravelTimeLabelMap(items, requestModel) { const travelItems = items .map((item, index) => { const itemType = normalizeExpenseType(item?.itemType || item?.item_type || requestModel?.typeCode || 'other') return { id: resolveExpenseItemViewId(item, index, requestModel), index, itemType, itemDate: normalizeIsoDateValue(item?.itemDate ?? item?.item_date), isSystemGenerated: isSystemGeneratedExpenseItemSource({ ...item, itemType }) } }) .filter((item) => !item.isSystemGenerated && LONG_DISTANCE_TRAVEL_EXPENSE_TYPES.has(item.itemType)) .sort((left, right) => { const dateCompare = String(left.itemDate || '').localeCompare(String(right.itemDate || '')) return dateCompare || left.index - right.index }) const labels = new Map() if (!travelItems.length) { return labels } travelItems.forEach((item, index) => { if (index === 0) { labels.set(item.id, '出发时间') } else if (index === travelItems.length - 1) { labels.set(item.id, '返回时间') } else { labels.set(item.id, '中转时间') } }) return labels } export function resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, requestModel, travelTimeLabelMap }) { if (isSystemGenerated) { return '系统自动计算' } if (travelTimeLabelMap?.has(id)) { return travelTimeLabelMap.get(id) } if (itemType === 'ride_ticket') { return '乘车时间' } if (itemType === 'hotel_ticket') { return '住宿时间' } return requestModel?.detailVariant === 'travel' ? '出行时间' : '业务发生时间' } export function buildStandardAdjustmentMap(requestModel = {}) { const flags = Array.isArray(requestModel?.riskFlags) ? requestModel.riskFlags : Array.isArray(requestModel?.risk_flags_json) ? requestModel.risk_flags_json : [] const adjustmentMap = new Map() flags.forEach((flag) => { if (!flag || typeof flag !== 'object') { return } if (String(flag.source || '').trim() !== STANDARD_ADJUSTMENT_RISK_SOURCE) { return } const itemId = String(flag.item_id || flag.itemId || '').trim() if (!itemId) { return } const originalAmount = parseOptionalCurrency(flag.original_amount ?? flag.originalAmount) const reimbursableAmount = parseOptionalCurrency(flag.reimbursable_amount ?? flag.reimbursableAmount) if (reimbursableAmount === null) { return } const employeeAbsorbedAmount = parseOptionalCurrency(flag.employee_absorbed_amount ?? flag.employeeAbsorbedAmount) || 0 adjustmentMap.set(itemId, { originalAmount, reimbursableAmount, employeeAbsorbedAmount, message: String(flag.message || flag.summary || '').trim() }) }) return adjustmentMap } function resolveSourceStandardAdjustment(source, id, requestModel) { const requestAdjustment = buildStandardAdjustmentMap(requestModel).get(id) if (requestAdjustment) { return requestAdjustment } const reimbursableAmount = parseOptionalCurrency(source?.reimbursableAmount ?? source?.reimbursable_amount) if (reimbursableAmount === null) { return null } const originalAmount = parseOptionalCurrency( source?.originalItemAmount ?? source?.original_item_amount ?? source?.originalAmount ?? source?.original_amount ) const employeeAbsorbedAmount = parseOptionalCurrency( source?.employeeAbsorbedAmount ?? source?.employee_absorbed_amount ) || 0 const hasExplicitAdjustmentMarker = Boolean( source?.standardAdjustmentAccepted || source?.standard_adjustment_accepted || source?.hasStandardAdjustment || source?.has_standard_adjustment || String(source?.standardAdjustmentMessage || source?.standard_adjustment_message || '').trim() || employeeAbsorbedAmount > 0 || (originalAmount !== null && reimbursableAmount < originalAmount) ) if (!hasExplicitAdjustmentMarker) { return null } return { originalAmount, reimbursableAmount, employeeAbsorbedAmount, message: String(source?.standardAdjustmentMessage || source?.standard_adjustment_message || '').trim() } } export function buildExpenseItemViewModel(source, index, requestModel, travelTimeLabelMap = new Map()) { const itemType = normalizeExpenseType(source?.itemType || source?.item_type || requestModel?.typeCode || 'other') const isSystemGenerated = isSystemGeneratedExpenseItemSource({ ...source, itemType }) const id = resolveExpenseItemViewId(source, index, requestModel) const itemReason = String(source?.itemReason ?? source?.item_reason ?? '').trim() const itemLocation = String(source?.itemLocation ?? source?.item_location ?? '').trim() const itemNote = String(source?.itemNote ?? source?.item_note ?? '').trim() const itemDate = normalizeIsoDateValue(source?.itemDate ?? source?.item_date) const itemAmount = parseCurrency(source?.itemAmount ?? source?.item_amount) const invoiceId = String(source?.invoiceId ?? source?.invoice_id ?? '').trim() const attachmentName = String(source?.attachmentName || source?.attachment_name || extractAttachmentDisplayName(invoiceId)).trim() const attachments = invoiceId ? [attachmentName || invoiceId] : [] const standardAdjustment = resolveSourceStandardAdjustment(source, id, requestModel) const originalItemAmount = standardAdjustment?.originalAmount ?? itemAmount const reimbursableAmount = standardAdjustment?.reimbursableAmount ?? itemAmount const employeeAbsorbedAmount = standardAdjustment?.employeeAbsorbedAmount || Math.max(originalItemAmount - reimbursableAmount, 0) const hasStandardAdjustment = reimbursableAmount >= 0 && reimbursableAmount < originalItemAmount const amountDisplay = itemAmount > 0 ? formatCurrency(itemAmount) : '待补充' const reimbursableAmountDisplay = reimbursableAmount > 0 ? formatCurrency(reimbursableAmount) : '待补充' const riskText = String(source?.riskText || '').trim() const filledAt = formatExpenseFilledTime( source?.filledAt || source?.filled_at || source?.createdAt || source?.created_at ) return { id, itemDate, itemType, itemReason, itemLocation, itemNote, itemAmount, originalItemAmount, originalAmountDisplay: originalItemAmount > 0 ? formatCurrency(originalItemAmount) : amountDisplay, reimbursableAmount, reimbursableAmountDisplay, employeeAbsorbedAmount, employeeAbsorbedAmountDisplay: employeeAbsorbedAmount > 0 ? formatCurrency(employeeAbsorbedAmount) : '', hasStandardAdjustment, standardAdjustmentAccepted: Boolean(standardAdjustment), standardAdjustmentMessage: standardAdjustment?.message || '', invoiceId, isSystemGenerated, time: itemDate || '待补充', filledAt: filledAt || '待同步', dayLabel: resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, requestModel, travelTimeLabelMap }), name: resolveExpenseTypeLabel(itemType), category: resolveExpenseTypeLabel(itemType), desc: itemReason || '待补充', detail: resolveExpenseDescriptionDetail(itemType, itemLocation), amount: amountDisplay, status: isSystemGenerated ? '系统计算' : attachments.length ? '已识别' : '待补充', tone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'bad', attachmentStatus: isSystemGenerated ? '无需附件' : attachments.length ? '已关联票据' : '未上传', attachmentHint: isSystemGenerated ? '根据出差天数与职级自动测算' : attachments.length ? attachmentName || attachments[0] : resolveExpenseUploadHint(), attachmentTone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'missing', attachments, riskLabel: String(source?.riskLabel || '').trim() || '无', riskText, riskTone: String(source?.riskTone || '').trim() || 'low' } } export function rebuildExpenseItems(items, requestModel) { const sortedItems = [...items] .filter((item) => !isIgnorableExpenseDraftPlaceholder(item)) .sort((left, right) => Number(isSystemGeneratedExpenseItemSource(left)) - Number(isSystemGeneratedExpenseItemSource(right))) const travelTimeLabelMap = buildTravelTimeLabelMap(sortedItems, requestModel) return sortedItems.map((item, index) => buildExpenseItemViewModel(item, index, requestModel, travelTimeLabelMap)) } export function buildExpenseDraftIssues(item) { const issues = [] if (item.isSystemGenerated || isSystemGeneratedExpenseItemSource(item)) { return issues } if (isIgnorableExpenseDraftPlaceholder(item)) { return issues } const locationRequired = isLocationRequiredExpenseType(item.itemType) const hasUploadedReceipt = hasUploadedReceiptReference(item) if (!hasUploadedReceipt && !isValidIsoDate(item.itemDate)) { issues.push('缺少日期') } if (isPlaceholderValue(item.itemType)) { issues.push('缺少费用项目') } if (!hasUploadedReceipt && isPlaceholderValue(item.itemReason)) { issues.push('缺少说明') } else if (!hasUploadedReceipt && isRouteDescriptionExpenseType(item.itemType) && !isValidRouteDescription(item.itemReason)) { issues.push('行程说明格式错误') } if (!hasUploadedReceipt && locationRequired && isPlaceholderValue(item.itemLocation)) { issues.push('缺少地点') } if (!hasUploadedReceipt && (!Number.isFinite(Number(item.itemAmount)) || Number(item.itemAmount) <= 0)) { issues.push('缺少金额') } if (isAttachmentRequiredExpenseItem(item) && !hasUploadedReceipt) { issues.push('缺少票据标识') } return issues } export function buildDraftBlockingIssues(request, expenseItems) { const issues = [] const isApplication = isApplicationDocumentRequest(request) const locationRequired = isLocationRequiredExpenseType(request.typeCode) const normalizedItems = Array.isArray(expenseItems) ? expenseItems : [] const effectiveItems = normalizedItems.filter((item) => !isIgnorableExpenseDraftPlaceholder(item)) const itemAmountTotal = effectiveItems.reduce((sum, item) => { const amount = Number(item?.itemAmount || 0) return Number.isFinite(amount) && amount > 0 ? sum + amount : sum }, 0) const hasValidItemDate = effectiveItems.some((item) => isValidIsoDate(item?.itemDate)) const hasValidItemType = effectiveItems.some((item) => !isPlaceholderValue(item?.itemType)) const hasValidItemReason = effectiveItems.some((item) => !isPlaceholderValue(item?.itemReason)) const hasValidItemLocation = effectiveItems.some((item) => !isPlaceholderValue(item?.itemLocation)) if (isPlaceholderValue(request.profileName)) { issues.push('申请人未完善') } if (isApplication) { if (isPlaceholderValue(request.typeLabel)) { issues.push('申请类型未完善') } if (isPlaceholderValue(request.reason)) { issues.push('申请事由未完善') } if (isPlaceholderValue(request.location)) { issues.push('业务地点未完善') } if (isPlaceholderValue(request.occurredDisplay)) { issues.push('申请时间未完善') } if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) { issues.push('预计总费用未完善') } return [...new Set(issues)] } if (isPlaceholderValue(request.typeLabel) && !hasValidItemType) { issues.push('报销类型未完善') } if (isPlaceholderValue(request.reason) && !hasValidItemReason) { issues.push('报销事由未完善') } if (locationRequired && isPlaceholderValue(request.location) && !hasValidItemLocation) { issues.push('业务地点未完善') } if (isPlaceholderValue(request.occurredDisplay) && !hasValidItemDate) { issues.push('发生时间未完善') } if ((!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) && itemAmountTotal <= 0) { issues.push('报销金额未完善') } if (!effectiveItems.length) { issues.push('费用明细不能为空') } normalizedItems.forEach((item, index) => { buildExpenseDraftIssues(item).forEach((issue) => { issues.push(`费用明细第 ${index + 1} 条${issue}`) }) }) return [...new Set(issues)] } export function mapIssueToAdvice(issue) { const text = String(issue || '').trim() if (!text) { return '' } if (text === '费用明细不能为空') { return '先新增至少 1 条费用明细,再补充金额、用途和附件。' } if (text === '申请人未完善') { return '补充申请人信息,确保审批单据归属明确。' } if (text === '所属部门未完善') { return '补充所属部门,便于财务和审批人识别成本归属。' } if (text === '报销类型未完善') { return '选择报销类型,明确本次费用归类。' } if (text === '申请类型未完善') { return '补充申请类型,明确本次申请的费用或业务场景。' } if (text === '报销事由未完善') { return '补充报销事由,说明本次费用用途。' } if (text === '申请事由未完善') { return '补充申请事由,说明本次申请的业务背景。' } if (text === '业务地点未完善') { return '补充业务地点,方便审核业务发生场景。' } if (text === '发生时间未完善') { return '补充费用发生时间,确保单据时间完整。' } if (text === '申请时间未完善') { return '补充申请时间或行程时间,确保申请周期完整。' } if (text === '报销金额未完善') { return '补充报销金额,并与费用明细金额保持一致。' } if (text === '预计总费用未完善') { return '补充预计总费用,供审批人判断预算占用和申请额度。' } const itemMatch = text.match(/^费用明细第\s*(\d+)\s*条(.+)$/) if (!itemMatch) { return text } const [, indexText, fieldText] = itemMatch const labelPrefix = `完善第 ${indexText} 条费用明细` if (fieldText === '缺少日期') { return `${labelPrefix}的发生日期。` } if (fieldText === '缺少费用项目') { return `${labelPrefix}的费用项目。` } if (fieldText === '缺少说明') { return `${labelPrefix}的用途说明。` } if (fieldText === '行程说明格式错误') { return `${labelPrefix}的行程说明,格式应为“起始地-目的地”。` } if (fieldText === '缺少地点') { return `${labelPrefix}的业务地点。` } if (fieldText === '缺少金额') { return `${labelPrefix}的金额。` } if (fieldText === '缺少票据标识') { return `为第 ${indexText} 条费用明细上传或关联票据附件。` } return `${labelPrefix}。` }