Files
X-Financial/web/src/utils/requestViewModel.js
caoxiaozhu 002bf4f756 feat: 完善报销单审批流程及退回原因追踪
新增直属领导审批通过接口和审批待办列表查询,报销单退回
支持原因码分类和审批环节标记,优化票据附件去重和路径
回退查找,前端新增退回原因对话框、审批收件箱和工作台
图标组件,补充工具函数和单元测试覆盖。
2026-05-20 21:00:47 +08:00

264 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const REQUEST_TYPE_META = {
travel: {
label: '差旅费',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '行程状态'
},
entertainment: {
label: '业务招待费',
detailVariant: 'general',
tone: 'entertainment',
secondaryStatusLabel: '票据状态'
},
hotel: {
label: '住宿费',
detailVariant: 'general',
tone: 'travel',
secondaryStatusLabel: '票据状态'
},
transport: {
label: '交通费',
detailVariant: 'general',
tone: 'travel',
secondaryStatusLabel: '票据状态'
},
meal: {
label: '餐费',
detailVariant: 'general',
tone: 'meeting',
secondaryStatusLabel: '票据状态'
},
office: {
label: '办公费',
detailVariant: 'general',
tone: 'office',
secondaryStatusLabel: '票据状态'
},
meeting: {
label: '会务费',
detailVariant: 'general',
tone: 'meeting',
secondaryStatusLabel: '票据状态'
},
training: {
label: '培训费',
detailVariant: 'general',
tone: 'training',
secondaryStatusLabel: '票据状态'
},
other: {
label: '其他费用',
detailVariant: 'general',
tone: 'other',
secondaryStatusLabel: '票据状态'
}
}
const APPROVAL_META = {
draft: { label: '草稿', tone: 'draft' },
in_progress: { label: '审批中', tone: 'info' },
supplement: { label: '待补充', tone: 'warning' },
completed: { label: '已完成', tone: 'success' },
rejected: { label: '已退回', tone: 'danger' }
}
const BACKEND_STATUS_META = {
draft: { key: 'draft', label: '草稿', tone: 'draft' },
submitted: { key: 'in_progress', label: '审批中', tone: 'info' },
pending: { key: 'in_progress', label: '审批中', tone: 'info' },
reviewing: { key: 'in_progress', label: '审批中', tone: 'info' },
in_review: { key: 'in_progress', label: '审批中', tone: 'info' },
in_progress: { key: 'in_progress', label: '审批中', tone: 'info' },
approved: { key: 'completed', label: '已完成', tone: 'success' },
paid: { key: 'completed', label: '已完成', tone: 'success' },
completed: { key: 'completed', label: '已完成', tone: 'success' },
supplement: { key: 'supplement', label: '待补充', tone: 'warning' },
returned: { key: 'supplement', label: '待提交', tone: 'warning' },
rejected: { key: 'rejected', label: '已退回', tone: 'danger' },
cancelled: { key: 'rejected', label: '已退回', tone: 'danger' }
}
function parseRequestDateFromId(id) {
const match = String(id || '').match(/(\d{4})[-]?(\d{2})(\d{2})/)
if (!match) {
return ''
}
const [, year, month, day] = match
return `${year}-${month}-${day}`
}
function parseAmount(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
const normalized = String(value || '')
.replace(/[,\s]/g, '')
.replace(/[¥¥]/g, '')
.replace(/元/g, '')
.trim()
if (!normalized || !/^-?\d+(?:\.\d+)?$/.test(normalized)) {
return null
}
const amount = Number(normalized)
return Number.isFinite(amount) ? amount : null
}
export function formatRequestCurrency(value) {
const amount = parseAmount(value)
if (amount === null) {
return String(value || '').trim() || '待补充'
}
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
minimumFractionDigits: 0,
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
}).format(amount)
}
function resolveApprovalState(request) {
const normalizedStatus = String(request?.status || '').trim().toLowerCase()
if (normalizedStatus && BACKEND_STATUS_META[normalizedStatus]) {
const meta = BACKEND_STATUS_META[normalizedStatus]
return {
key: meta.key,
label: request.approvalStatus || request.approval || meta.label,
tone: request.approvalTone || meta.tone
}
}
if (request?.approvalKey && APPROVAL_META[request.approvalKey]) {
return {
key: request.approvalKey,
label: request.approvalStatus || request.approval || APPROVAL_META[request.approvalKey].label,
tone: request.approvalTone || APPROVAL_META[request.approvalKey].tone
}
}
if (typeof request?.approval === 'string' && request.approval.trim()) {
return {
key: request.approvalKey || 'in_progress',
label: request.approval,
tone: request.approvalTone || 'info'
}
}
if (typeof request?.status === 'string') {
if (request.status === 'success') return { key: 'completed', label: '已完成', tone: 'success' }
if (request.status === 'danger') return { key: 'supplement', label: '待补充', tone: 'warning' }
}
return { key: 'in_progress', label: '审批中', tone: 'info' }
}
function resolveTypeMeta(request) {
const typeCode = String(request?.typeCode || request?.expense_type || '').trim() || 'other'
const typeMeta = REQUEST_TYPE_META[typeCode] || REQUEST_TYPE_META.other
return {
typeCode,
typeLabel: String(request?.typeLabel || request?.category || '').trim() || typeMeta.label,
detailVariant: String(request?.detailVariant || '').trim() || typeMeta.detailVariant,
typeTone: typeMeta.tone,
secondaryStatusLabel: request?.secondaryStatusLabel || typeMeta.secondaryStatusLabel
}
}
function normalizeRoleLabels(value) {
if (Array.isArray(value)) {
return value.map((item) => String(item || '').trim()).filter(Boolean)
}
const text = String(value || '').trim()
return text ? [text] : []
}
export function normalizeRequestForUi(request) {
if (!request) {
return null
}
const { typeCode, typeLabel, detailVariant, typeTone, secondaryStatusLabel } = resolveTypeMeta(request)
const approvalState = resolveApprovalState(request)
const amountValue = parseAmount(request.amount)
const amountDisplay = formatRequestCurrency(amountValue ?? request.amount)
const title = String(request.title || request.reason || '').trim() || `${typeLabel}报销`
const sceneTarget = String(request.sceneTarget || request.location || request.city || request.entity || '').trim() || '待补充'
const occurredDisplay = String(request.occurredDisplay || request.period || request.occurredAt || '').trim() || '待补充'
const applyTime = String(request.applyTime || parseRequestDateFromId(request.id) || '').trim() || '待补充'
const workflowNode = String(
request.workflowNode
|| request.node
|| request.approval_stage
|| request.approvalStage
|| ''
).trim() || '待提交'
const secondaryStatusValue =
String(request.secondaryStatusValue || request.travel || '').trim()
|| (detailVariant === 'travel' ? '待安排行程' : '待补充票据')
const secondaryStatusTone = String(request.secondaryStatusTone || request.travelTone || '').trim() || 'neutral'
const roleLabels = normalizeRoleLabels(request.roleLabels || request.role_labels)
const profileIdentity =
String(request.profileIdentity || request.employeeIdentity || request.identity || '').trim()
|| roleLabels.join(' / ')
|| '员工'
return {
...request,
claimId: String(request.claimId || request.claim_id || '').trim(),
documentNo: String(request.documentNo || request.claimNo || request.claim_no || request.id || '').trim(),
typeCode,
typeLabel,
detailVariant,
typeTone,
title,
reason: title,
sceneLabel: String(request.sceneLabel || '').trim() || typeLabel,
sceneTarget,
city: sceneTarget,
location: String(request.location || '').trim() || sceneTarget,
relatedCustomer: String(request.relatedCustomer || '').trim() || '待补充',
occurredDisplay,
period: occurredDisplay,
applyTime,
amountValue,
amountDisplay,
amount: amountDisplay,
workflowNode,
node: workflowNode,
approvalKey: approvalState.key,
approvalStatus: approvalState.label,
approval: approvalState.label,
approvalTone: approvalState.tone,
secondaryStatusLabel,
secondaryStatusValue,
secondaryStatusTone,
travel: secondaryStatusValue,
travelTone: secondaryStatusTone,
riskSummary: String(request.riskSummary || request.risk || '').trim() || '暂无异常',
attachmentSummary: String(request.attachmentSummary || '').trim() || '待补充',
rangeBucket: String(request.rangeBucket || request.range || '').trim() || '本周',
detailTitle: String(request.detailTitle || '').trim() || title,
note: String(request.note || '').trim(),
profileName: String(request.person || request.applicant || request.employeeName || '').trim() || '当前申请人',
profileDepartment: String(request.dept || request.department || request.departmentName || '').trim() || '所属部门',
profileIdentity,
profilePosition:
String(request.profilePosition || request.employeePosition || request.employee_position || request.position || '').trim()
|| '待补充',
profileGrade: String(request.profileGrade || request.employeeGrade || request.employee_grade || request.grade || '').trim() || '待补充',
profileManager: String(request.profileManager || request.managerName || request.manager_name || request.manager || '').trim() || '待补充',
roleLabels,
profileAvatar:
String(request.person || request.applicant || request.employeeName || '申').trim().slice(0, 1) || '申'
}
}