import { computed, reactive, ref } from 'vue' import { fetchExpenseClaims } from '../services/reimbursements.js' import { filterActionableRiskFlags } from '../utils/riskFlags.js' const EXPENSE_TYPE_LABELS = { travel: '差旅费', travel_application: '差旅费用申请', expense_application: '费用申请', purchase_application: '采购费用申请', meeting_application: '会务费用申请', train_ticket: '火车票', flight_ticket: '机票', ship_ticket: '轮船票', ferry_ticket: '轮船票', hotel_ticket: '住宿票', ride_ticket: '乘车', travel_allowance: '出差补贴', entertainment: '业务招待费', marketing: '市场推广费', office: '办公用品费', meeting: '会务费', training: '培训费', software: '软件服务费', hotel: '住宿费', transport: '交通费', meal: '业务招待费', other: '其他费用' } const LOCATION_REQUIRED_EXPENSE_TYPES = new Set([ 'travel', 'train_ticket', 'flight_ticket', 'hotel_ticket', 'ride_ticket', 'meeting', 'entertainment' ]) const SYSTEM_GENERATED_EXPENSE_TYPES = new Set(['travel_allowance']) const LONG_DISTANCE_TRAVEL_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket']) const ROUTE_DESCRIPTION_EXPENSE_TYPES = new Set(['train_ticket', 'flight_ticket', 'ship_ticket', 'ferry_ticket', 'ride_ticket']) const HOTEL_DESCRIPTION_EXPENSE_TYPES = new Set(['hotel_ticket']) const DOCUMENT_TYPE_APPLICATION = 'application' const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement' const REIMBURSEMENT_PROGRESS_LABELS = [ '创建单据', '待提交', 'AI预审', '直属领导审批', '财务审批', '归档入账' ] const APPLICATION_PROGRESS_LABELS = [ '创建申请', '直属领导审批', '审批完成' ] function parseNumber(value) { const nextValue = Number(value) return Number.isFinite(nextValue) ? nextValue : 0 } function toDate(value) { if (!value) { return null } const nextDate = new Date(value) return Number.isNaN(nextDate.getTime()) ? null : nextDate } function formatDate(value) { const nextDate = toDate(value) if (!nextDate) { return '' } const year = nextDate.getFullYear() const month = String(nextDate.getMonth() + 1).padStart(2, '0') const day = String(nextDate.getDate()).padStart(2, '0') return `${year}-${month}-${day}` } function formatDateTime(value) { const nextDate = toDate(value) if (!nextDate) { return '' } const hours = String(nextDate.getHours()).padStart(2, '0') const minutes = String(nextDate.getMinutes()).padStart(2, '0') return `${formatDate(nextDate)} ${hours}:${minutes}` } function formatDurationFrom(value, now = Date.now()) { const startAt = toDate(value) if (!startAt) { return '' } const diffMs = Math.max(0, Number(now) - startAt.getTime()) const totalMinutes = Math.floor(diffMs / (60 * 1000)) if (totalMinutes < 1) { return '刚刚' } const days = Math.floor(totalMinutes / (24 * 60)) const hours = Math.floor((totalMinutes % (24 * 60)) / 60) const minutes = totalMinutes % 60 if (days > 0) { return hours > 0 ? `${days}天${hours}小时` : `${days}天` } if (hours > 0) { return minutes > 0 ? `${hours}小时${minutes}分钟` : `${hours}小时` } return `${minutes}分钟` } function formatAmount(value) { return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 0, maximumFractionDigits: Number.isInteger(value) ? 0 : 2 }).format(parseNumber(value)) } function resolveTypeLabel(typeCode) { return EXPENSE_TYPE_LABELS[String(typeCode || '').trim()] || EXPENSE_TYPE_LABELS.other } function resolveDocumentTypeMeta(claim, typeCode) { const explicitType = String( claim?.document_type_code || claim?.documentTypeCode || claim?.document_type || claim?.documentType || '' ).trim() const claimNo = String(claim?.claim_no || claim?.claimNo || '').trim().toUpperCase() const normalizedType = String(typeCode || '').trim() const isApplication = explicitType === DOCUMENT_TYPE_APPLICATION || explicitType === 'expense_application' || claimNo.startsWith('AP-') || claimNo.startsWith('APP-') || normalizedType === 'application' || normalizedType.endsWith('_application') return isApplication ? { documentTypeCode: DOCUMENT_TYPE_APPLICATION, documentTypeLabel: '申请单' } : { documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT, documentTypeLabel: '报销单' } } function normalizeExpenseType(typeCode) { return String(typeCode || '').trim() || 'other' } function isLocationRequiredExpenseType(typeCode) { return LOCATION_REQUIRED_EXPENSE_TYPES.has(normalizeExpenseType(typeCode)) } function resolveLocationDisplay(location, typeCode) { const normalized = String(location || '').trim() if (normalized) { return normalized } return isLocationRequiredExpenseType(typeCode) ? '待补充' : '非必填' } function resolveExpenseDescriptionDetail(itemType, itemLocation) { const normalizedType = normalizeExpenseType(itemType) if (ROUTE_DESCRIPTION_EXPENSE_TYPES.has(normalizedType)) { return '起始地-目的地' } if (HOTEL_DESCRIPTION_EXPENSE_TYPES.has(normalizedType)) { return '目的地酒店' } return resolveLocationDisplay(itemLocation, normalizedType) } function resolveExpenseItemViewId(item, index, claim) { return String(item?.id || `${claim?.id || 'claim'}-item-${index}`) } function buildTravelTimeLabelMap(items, claim) { const travelItems = items .map((item, index) => { const itemType = normalizeExpenseType(item?.item_type || claim?.expense_type) return { id: resolveExpenseItemViewId(item, index, claim), index, itemType, itemDate: formatDate(item?.item_date), isSystemGenerated: Boolean(item?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(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() 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 } function resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, claim, travelTimeLabelMap }) { if (isSystemGenerated) { return '系统自动计算' } if (travelTimeLabelMap?.has(id)) { return travelTimeLabelMap.get(id) } if (itemType === 'ride_ticket') { return '乘车时间' } if (itemType === 'hotel_ticket') { return '住宿时间' } return claim?.expense_type === 'travel' ? '出行时间' : '业务发生时间' } function resolveAttachmentDisplayName(value) { const normalized = String(value || '').trim() if (!normalized) { return '' } return normalized.split('/').filter(Boolean).pop() || normalized } function resolveApprovalMeta(status) { const normalized = String(status || '').trim().toLowerCase() if (normalized === 'draft') { return { key: 'draft', label: '草稿', tone: 'draft' } } if (normalized === 'returned') { return { key: 'supplement', label: '待提交', tone: 'warning' } } if (normalized === 'supplement') { return { key: 'supplement', label: '待补充', tone: 'warning' } } if (['approved', 'completed', 'paid'].includes(normalized)) { return { key: 'completed', label: '已完成', tone: 'success' } } if (['rejected', 'cancelled'].includes(normalized)) { return { key: 'rejected', label: '已退回', tone: 'danger' } } return { key: 'in_progress', label: '审批中', tone: 'info' } } function resolveWorkflowNode(claim, approvalMeta, isApplicationDocument = false) { if (String(claim?.status || '').trim().toLowerCase() === 'returned') { return '待提交' } const rawNode = String(claim?.approval_stage || '').trim() if (rawNode) { if (rawNode === '审批流转') { return 'AI预审' } if (rawNode === '待补充') { return approvalMeta.key === 'draft' ? '待提交' : 'AI预审' } return rawNode } if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') { return '待提交' } if (approvalMeta.key === 'completed') { return isApplicationDocument ? '审批完成' : '归档入账' } return isApplicationDocument ? '直属领导审批' : 'AI预审' } function stringifyRiskFlag(value) { if (typeof value === 'string') { return value.trim() } if (!value || typeof value !== 'object') { return '' } for (const key of ['message', 'label', 'reason', 'name']) { const nextValue = String(value[key] || '').trim() if (nextValue) { return nextValue } } return '' } function buildRiskSummary(riskFlags) { if (!Array.isArray(riskFlags) || !riskFlags.length) { return '无' } const items = filterActionableRiskFlags(riskFlags).map((item) => stringifyRiskFlag(item)).filter(Boolean) return items.length ? items.join(';') : '无' } function buildOccurredDisplay(claim) { const itemDates = Array.isArray(claim?.items) ? claim.items.map((item) => formatDate(item?.item_date)).filter(Boolean) : [] if (!itemDates.length) { return formatDate(claim?.occurred_at) || '待补充' } const sortedDates = [...new Set(itemDates)].sort() if (sortedDates.length === 1) { return sortedDates[0] } return `${sortedDates[0]} ~ ${sortedDates[sortedDates.length - 1]}` } function resolveProgressCurrentIndex(approvalMeta, workflowNode) { const normalizedNode = String(workflowNode || '').trim() if (approvalMeta.key === 'completed') { return 5 } if (normalizedNode.includes('归档') || normalizedNode.includes('入账')) { return 5 } if (normalizedNode.includes('财务')) { return 4 } if ( normalizedNode.includes('直属领导') || normalizedNode.includes('领导审批') || normalizedNode.includes('部门负责人') || normalizedNode.includes('负责人审批') ) { return 3 } if (normalizedNode.includes('AI预审') || normalizedNode.includes('AI验审') || normalizedNode.includes('审批流转')) { return 2 } if (normalizedNode.includes('待提交')) { return 1 } if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') { return 1 } return 2 } function resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) { const normalizedNode = String(workflowNode || '').trim() if (approvalMeta.key === 'completed') { return 2 } if (normalizedNode.includes('审批完成') || normalizedNode.includes('申请完成')) { return 2 } if ( normalizedNode.includes('直属领导') || normalizedNode.includes('领导审批') || normalizedNode.includes('部门负责人') || normalizedNode.includes('负责人审批') ) { return 1 } if (approvalMeta.key === 'draft' || approvalMeta.key === 'supplement' || approvalMeta.key === 'rejected') { return 0 } return 1 } function normalizeText(value) { return String(value || '').trim() } function isEmailLike(value) { return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(normalizeText(value)) } function resolveDisplayName(...values) { for (const value of values) { const normalized = normalizeText(value) if (normalized && !isEmailLike(normalized)) { return normalized } } return '' } function getRiskFlags(claim) { return Array.isArray(claim?.risk_flags_json) ? claim.risk_flags_json : [] } function getLatestEvent(events) { const sortedEvents = events .filter((item) => item && typeof item === 'object') .map((item) => ({ ...item, eventDate: toDate(item.created_at || item.createdAt) })) .filter((item) => item.eventDate) .sort((a, b) => a.eventDate.getTime() - b.eventDate.getTime()) return sortedEvents.length ? sortedEvents[sortedEvents.length - 1] : null } function findApprovalEventForStep(claim, label) { const stepLabel = normalizeText(label) const events = getRiskFlags(claim).filter((flag) => { if (!flag || typeof flag !== 'object') { return false } const source = normalizeText(flag.source) if (!['manual_approval', 'finance_approval'].includes(source)) { return false } const previousStage = normalizeText(flag.previous_approval_stage || flag.previousApprovalStage) const nextStage = normalizeText(flag.next_approval_stage || flag.nextApprovalStage) if (stepLabel === '直属领导审批') { return ( previousStage.includes('直属领导') || previousStage.includes('领导审批') || nextStage.includes('财务') ) } if (stepLabel === '财务审批') { return ( previousStage.includes('财务') || nextStage.includes('归档') || nextStage.includes('入账') || nextStage.includes('完成') ) } return false }) return getLatestEvent(events) } function findLatestReturnEvent(claim) { return getLatestEvent( getRiskFlags(claim).filter((flag) => ( flag && typeof flag === 'object' && normalizeText(flag.source) === 'manual_return' )) ) } function buildProgressStepMeta(time, detail = '', title = '') { return { time, detail, title: title || [time, detail].filter(Boolean).join(' ') } } function buildCompletedStepMeta(claim, label) { const stepLabel = normalizeText(label) const employeeName = normalizeText(claim?.employee_name) || '申请人' if (stepLabel === '创建单据' || stepLabel === '创建申请') { const createdAt = formatDateTime(claim?.created_at) return buildProgressStepMeta(stepLabel === '创建申请' ? `${employeeName}发起申请` : `${employeeName}创建`, createdAt) } if (stepLabel === '待提交') { const submittedAt = formatDateTime(claim?.submitted_at) return buildProgressStepMeta(`${employeeName}提交`, submittedAt) } if (stepLabel === 'AI预审') { const reviewedAt = formatDateTime(claim?.submitted_at || claim?.updated_at) return buildProgressStepMeta('AI预审通过', reviewedAt) } if (stepLabel === '直属领导审批' || stepLabel === '财务审批') { const approvalEvent = findApprovalEventForStep(claim, stepLabel) if (approvalEvent) { const operator = resolveDisplayName( approvalEvent.operator, approvalEvent.operator_name, approvalEvent.operatorName, stepLabel === '直属领导审批' ? claim?.manager_name : '' ) || (stepLabel === '财务审批' ? '财务' : '直属领导') const approvedAt = formatDateTime(approvalEvent.created_at || approvalEvent.createdAt) return buildProgressStepMeta(`${operator}通过`, approvedAt, `${operator}审批通过 ${approvedAt}`.trim()) } if (stepLabel === '财务审批') { const updatedAt = formatDateTime(claim?.updated_at) return buildProgressStepMeta('财务通过', updatedAt, `财务审批通过 ${updatedAt}`.trim()) } } if (stepLabel === '归档入账') { const archivedAt = formatDateTime(claim?.updated_at) return buildProgressStepMeta('归档入账', archivedAt) } if (stepLabel === '审批完成') { const completedAt = formatDateTime(claim?.updated_at) return buildProgressStepMeta('审批完成', completedAt) } return buildProgressStepMeta('已完成') } function resolveCurrentStepStartedAt(claim, label) { const stepLabel = normalizeText(label) if (stepLabel === '创建单据' || stepLabel === '创建申请') { return claim?.created_at } if (stepLabel === '待提交') { const returnEvent = findLatestReturnEvent(claim) return returnEvent?.created_at || returnEvent?.createdAt || claim?.updated_at || claim?.created_at } if (stepLabel === 'AI预审') { return claim?.updated_at || claim?.submitted_at || claim?.created_at } if (stepLabel === '直属领导审批') { return claim?.submitted_at || claim?.updated_at || claim?.created_at } if (stepLabel === '财务审批') { const leaderApprovalEvent = findApprovalEventForStep(claim, '直属领导审批') return leaderApprovalEvent?.created_at || leaderApprovalEvent?.createdAt || claim?.updated_at || claim?.submitted_at } if (stepLabel === '归档入账' || stepLabel === '审批完成') { return claim?.updated_at || claim?.submitted_at } return '' } function buildProgressSteps(approvalMeta, workflowNode, claim = {}, options = {}) { const documentTypeCode = String(options.documentTypeCode || '').trim() const progressLabels = documentTypeCode === DOCUMENT_TYPE_APPLICATION ? APPLICATION_PROGRESS_LABELS : REIMBURSEMENT_PROGRESS_LABELS const currentIndex = documentTypeCode === DOCUMENT_TYPE_APPLICATION ? resolveApplicationProgressCurrentIndex(approvalMeta, workflowNode) : resolveProgressCurrentIndex(approvalMeta, workflowNode) const currentTime = approvalMeta.key === 'completed' ? '已完成' : approvalMeta.key === 'supplement' ? '待补充' : approvalMeta.key === 'rejected' ? '已退回' : '进行中' return progressLabels.map((label, index) => { if (approvalMeta.key === 'completed') { const stepMeta = buildCompletedStepMeta(claim, label) return { index: index + 1, label, time: stepMeta.time, detail: stepMeta.detail, title: stepMeta.title, done: true, active: true, current: false } } if (index < currentIndex) { const stepMeta = buildCompletedStepMeta(claim, label) return { index: index + 1, label, time: stepMeta.time, detail: stepMeta.detail, title: stepMeta.title, done: true, active: true, current: false } } if (index === currentIndex) { const stayDuration = formatDurationFrom(resolveCurrentStepStartedAt(claim, label)) return { index: index + 1, label, time: stayDuration ? `停留 ${stayDuration}` : currentTime, detail: '', title: stayDuration ? `当前${label}已停留 ${stayDuration}` : currentTime, done: false, active: true, current: true } } return { index: index + 1, label, time: '待处理', detail: '', title: '待处理', done: false, active: false, current: false } }) } function buildExpenseItems(claim, riskSummary) { if (!Array.isArray(claim?.items)) { return [] } const sortedItems = [...claim.items].sort((left, right) => { const leftType = normalizeExpenseType(left?.item_type) const rightType = normalizeExpenseType(right?.item_type) return Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(leftType)) - Number(SYSTEM_GENERATED_EXPENSE_TYPES.has(rightType)) }) const travelTimeLabelMap = buildTravelTimeLabelMap(sortedItems, claim) return sortedItems.map((item, index) => { const invoiceId = String(item?.invoice_id || '').trim() const attachmentName = resolveAttachmentDisplayName(invoiceId) const attachments = invoiceId ? [attachmentName || invoiceId] : [] const itemType = normalizeExpenseType(item?.item_type || claim?.expense_type) const isSystemGenerated = Boolean(item?.is_system_generated || SYSTEM_GENERATED_EXPENSE_TYPES.has(itemType)) const id = resolveExpenseItemViewId(item, index, claim) const itemTypeLabel = resolveTypeLabel(itemType) const itemLocation = String(item?.item_location || '').trim() const itemReason = String(item?.item_reason || '').trim() const itemAmount = parseNumber(item?.item_amount) const itemAmountDisplay = itemAmount > 0 ? formatAmount(itemAmount) : '待补充' return { id, time: formatDate(item?.item_date) || '待补充', itemDate: formatDate(item?.item_date) || '', filledAt: formatDateTime(item?.created_at) || '待同步', itemType, itemReason, itemLocation, itemAmount, invoiceId, isSystemGenerated, dayLabel: resolveExpenseTimeLabel({ id, itemType, isSystemGenerated, claim, travelTimeLabelMap }), name: itemTypeLabel, category: itemTypeLabel, desc: itemReason || '待补充', detail: resolveExpenseDescriptionDetail(itemType, itemLocation), amount: itemAmountDisplay, status: isSystemGenerated ? '系统计算' : attachments.length ? '已识别' : '待补充', tone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'bad', attachmentStatus: isSystemGenerated ? '无需附件' : attachments.length ? '已关联票据' : '未上传', attachmentHint: isSystemGenerated ? '根据出差天数与职级自动测算' : attachments.length ? attachments[0] : '仅支持上传 1 张 JPG、PNG、PDF 单据', attachmentTone: isSystemGenerated ? 'system' : attachments.length ? 'ok' : 'missing', attachments, riskLabel: riskSummary === '无' ? '无' : '待关注', riskText: riskSummary === '无' ? '' : riskSummary, riskTone: riskSummary === '无' ? 'low' : 'medium' } }) } export function mapExpenseClaimToRequest(claim) { const typeCode = String(claim?.expense_type || '').trim() || 'other' const typeLabel = resolveTypeLabel(typeCode) const documentTypeMeta = resolveDocumentTypeMeta(claim, typeCode) const isApplicationDocument = documentTypeMeta.documentTypeCode === DOCUMENT_TYPE_APPLICATION const approvalMeta = resolveApprovalMeta(claim?.status) const workflowNode = resolveWorkflowNode(claim, approvalMeta, isApplicationDocument) const invoiceCount = Math.max(0, parseNumber(claim?.invoice_count)) const riskSummary = buildRiskSummary(claim?.risk_flags_json) const expenseItems = buildExpenseItems(claim, riskSummary) const applyDateTime = claim?.submitted_at || claim?.created_at return { id: String(claim?.claim_no || claim?.id || '').trim(), claimNo: String(claim?.claim_no || claim?.id || '').trim(), claimId: String(claim?.id || '').trim(), status: String(claim?.status || '').trim(), person: String(claim?.employee_name || '').trim() || '待补充', dept: String(claim?.department_name || '').trim() || '待补充', departmentName: String(claim?.department_name || '').trim() || '待补充', employeeName: String(claim?.employee_name || '').trim() || '待补充', employeePosition: String(claim?.employee_position || '').trim(), employeeGrade: String(claim?.employee_grade || '').trim(), managerName: resolveDisplayName(claim?.manager_name), roleLabels: Array.isArray(claim?.role_labels) ? claim.role_labels.filter(Boolean) : [], entity: '', typeCode, typeLabel, ...documentTypeMeta, detailVariant: typeCode === 'travel' || typeCode === 'travel_application' ? 'travel' : 'general', title: String(claim?.reason || '').trim() || (isApplicationDocument ? typeLabel : `${typeLabel}报销`), sceneLabel: typeLabel, sceneTarget: String(claim?.location || '').trim() || '待补充', location: String(claim?.location || '').trim() || '待补充', relatedCustomer: '', occurredDisplay: buildOccurredDisplay(claim), occurredAt: claim?.occurred_at || '', applyTime: formatDateTime(applyDateTime) || '待补充', submittedAt: applyDateTime || '', createdAt: claim?.created_at || '', updatedAt: claim?.updated_at || '', amount: parseNumber(claim?.amount), riskFlags: Array.isArray(claim?.risk_flags_json) ? claim.risk_flags_json : [], invoiceCount, workflowNode, approvalKey: approvalMeta.key, approvalStatus: approvalMeta.label, approvalTone: approvalMeta.tone, secondaryStatusLabel: isApplicationDocument ? '申请材料' : (typeCode === 'travel' ? '行程状态' : '票据状态'), secondaryStatusValue: isApplicationDocument ? '已进入审批流程' : (invoiceCount > 0 ? `已关联 ${invoiceCount} 张票据` : '待上传票据'), secondaryStatusTone: isApplicationDocument ? 'success' : (invoiceCount > 0 ? 'success' : 'warning'), riskSummary, attachmentSummary: isApplicationDocument ? '申请单' : (invoiceCount > 0 ? `${invoiceCount} 张票据` : '无'), expenseTableSummary: isApplicationDocument ? '预计金额已随申请提交' : expenseItems.length ? (invoiceCount > 0 ? `共 ${expenseItems.length} 条费用明细,已关联 ${invoiceCount} 张票据` : `共 ${expenseItems.length} 条费用明细,待补充票据`) : '暂无费用明细', note: String(claim?.reason || '').trim(), progressSteps: buildProgressSteps(approvalMeta, workflowNode, claim, { documentTypeCode: documentTypeMeta.documentTypeCode }), expenseItems } } function getWeekStart(date) { const nextDate = new Date(date) const day = nextDate.getDay() || 7 nextDate.setHours(0, 0, 0, 0) nextDate.setDate(nextDate.getDate() - day + 1) return nextDate } function getRecentDaysStart(date, days) { const nextDate = new Date(date) nextDate.setHours(0, 0, 0, 0) nextDate.setDate(nextDate.getDate() - Math.max(0, Number(days || 1) - 1)) return nextDate } function resolveRangeMatch(activeRange, item) { if (activeRange === 'custom' || activeRange === '本月') { if (activeRange !== '本月') { return true } } const targetDate = toDate(item?.submittedAt || item?.createdAt || item?.occurredAt) if (!targetDate) { return true } const now = new Date() const targetDay = formatDate(targetDate) if (activeRange === '今日') { return targetDay === formatDate(now) } if (activeRange === '近10日') { const recentStart = getRecentDaysStart(now, 10) return targetDate >= recentStart && targetDate <= now } if (activeRange === '本周') { const weekStart = getWeekStart(now) const nextWeekStart = new Date(weekStart) nextWeekStart.setDate(nextWeekStart.getDate() + 7) return targetDate >= weekStart && targetDate < nextWeekStart } if (activeRange === '本月') { return ( targetDate.getFullYear() === now.getFullYear() && targetDate.getMonth() === now.getMonth() ) } return true } export function useRequests() { const requests = ref([]) const loading = ref(false) const error = ref('') const search = ref('') const filters = reactive({ entity: '全部主体', category: '全部类型', risk: '全部状态' }) const ranges = ['今日', '近10日', '本周', '本月'] const activeRange = ref('近10日') const filteredRequests = computed(() => { const key = search.value.trim().toLowerCase() return requests.value.filter((item) => { const searchText = [ item.id, item.person, item.typeLabel, item.title, item.sceneTarget, item.riskSummary ] .filter(Boolean) .join('') .toLowerCase() const matchesSearch = !key || searchText.includes(key) const matchesRange = resolveRangeMatch(activeRange.value, item) return matchesSearch && matchesRange }) }) async function reload() { loading.value = true error.value = '' try { const payload = await fetchExpenseClaims() requests.value = Array.isArray(payload) ? payload.map((item) => mapExpenseClaimToRequest(item)) : [] } catch (nextError) { requests.value = [] error.value = nextError instanceof Error ? nextError.message : '个人报销列表加载失败。' } finally { loading.value = false } } function approveRequest(request) { return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。` } function rejectRequest(request) { return `${request.id} 未执行本地状态变更,列表当前只展示后端真实数据。` } void reload() return { requests, loading, error, search, filters, ranges, activeRange, filteredRequests, approveRequest, rejectRequest, reload } }