Files
X-Financial/web/src/utils/requestViewModel.js
caoxiaozhu d0e946cf47 feat: 完善文档中心与报销申请交互及侧边栏重构
后端优化编排器报销查询和本体检测精度,增强报销单草稿保
存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导
航,完善文档中心状态筛选和详情提示,报销创建和审批详情
页优化会话管理和费用明细交互,新增助手应用服务和预设动
作工具函数,补充单元测试覆盖。
2026-05-25 13:35:39 +08:00

338 lines
11 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: '行程状态'
},
travel_application: {
label: '差旅费用申请',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '申请材料'
},
expense_application: {
label: '费用申请',
detailVariant: 'general',
tone: 'other',
secondaryStatusLabel: '申请材料'
},
purchase_application: {
label: '采购费用申请',
detailVariant: 'general',
tone: 'office',
secondaryStatusLabel: '申请材料'
},
meeting_application: {
label: '会务费用申请',
detailVariant: 'general',
tone: 'meeting',
secondaryStatusLabel: '申请材料'
},
train_ticket: {
label: '火车票',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '行程状态'
},
flight_ticket: {
label: '机票',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '行程状态'
},
hotel_ticket: {
label: '住宿票',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '票据状态'
},
ride_ticket: {
label: '乘车',
detailVariant: 'travel',
tone: 'travel',
secondaryStatusLabel: '票据状态'
},
travel_allowance: {
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] : []
}
function isEmailLike(value) {
return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(value || '').trim())
}
function resolveDisplayName(...values) {
for (const value of values) {
const normalized = String(value || '').trim()
if (normalized && !isEmailLike(normalized)) {
return normalized
}
}
return ''
}
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: resolveDisplayName(
request.profileManager,
request.managerName,
request.manager_name,
request.manager
) || '待补充',
roleLabels,
profileAvatar:
String(request.person || request.applicant || request.employeeName || '申').trim().slice(0, 1) || '申'
}
}