refactor(web): update data and utils

- data/requests.js: update requests data
- utils/requestViewModel.js: update request view model utility
This commit is contained in:
caoxiaozhu
2026-05-13 03:31:07 +00:00
parent 2bf133d232
commit 473198c669
2 changed files with 260 additions and 76 deletions

View File

@@ -1,37 +1,72 @@
export const initialRequests = [
{ id: 'REQ-2026-0418', person: '刘倩', dept: '销售 · 华东区域', entity: 'Northstar China Ltd.', range: '今日', category: '机票', amount: '¥8,460', verdict: '可通过但需备注', status: 'warning', sla: '19h', slaStatus: 'warning', risk: '改签说明缺失,公务舱价格高于差标 12%' },
{ id: 'REQ-2026-0422', person: '韩阳', dept: '解决方案 · 北区', entity: 'Northstar China Ltd.', range: '本周', category: '酒店', amount: '¥3,280', verdict: '等待补件', status: 'warning', sla: '27h', slaStatus: 'warning', risk: '缺少出差行程单,酒店单晚超标 8%' },
{ id: 'REQ-2026-0431', person: '王鑫', dept: '运营管理 · 总部', entity: 'Northstar Singapore Pte.', range: '本周', category: '火车/用车', amount: '¥1,224', verdict: '规则全通过', status: 'success', sla: '4h', slaStatus: 'success', risk: '无明显风险' },
{ id: 'REQ-2026-0436', person: '陈嘉', dept: '市场 · 品牌活动', entity: 'Northstar US Inc.', range: '本月', category: '餐补及杂费', amount: '¥2,680', verdict: '建议人工复核', status: 'danger', sla: '51h', slaStatus: 'danger', risk: '发票号码重复,疑似重复报销' },
{ id: 'REQ-2026-0441', person: '赵敏', dept: '研发 · 后端组', entity: 'Northstar China Ltd.', range: '今日', category: '酒店', amount: '¥2,940', verdict: '规则全通过', status: 'success', sla: '6h', slaStatus: 'success', risk: '无明显风险' },
{ id: 'REQ-2026-0443', person: '周晨', dept: '销售 · 华南区域', entity: 'Northstar China Ltd.', range: '本周', category: '机票', amount: '¥6,520', verdict: '建议人工复核', status: 'danger', sla: '33h', slaStatus: 'danger', risk: '航班时间与出差申请不一致' },
{ id: 'REQ-2026-0448', person: '李娜', dept: '客户成功 · 华北', entity: 'Northstar Singapore Pte.', range: '本周', category: '火车/用车', amount: '¥1,860', verdict: '规则全通过', status: 'success', sla: '5h', slaStatus: 'success', risk: '无明显风险' },
{ id: 'REQ-2026-0452', person: '孙博', dept: '采购中心', entity: 'Northstar US Inc.', range: '本月', category: '酒店', amount: '¥4,780', verdict: '等待补件', status: 'warning', sla: '29h', slaStatus: 'warning', risk: '缺少住宿发票原件' },
{ id: 'REQ-2026-0455', person: '马骁', dept: '市场 · 品牌活动', entity: 'Northstar US Inc.', range: '本月', category: '机票', amount: '¥7,340', verdict: '规则全通过', status: 'success', sla: '8h', slaStatus: 'success', risk: '无明显风险' },
{ id: 'REQ-2026-0458', person: '高宁', dept: '运营管理 · 总部', entity: 'Northstar Singapore Pte.', range: '今日', category: '餐补及杂费', amount: '¥980', verdict: '可通过但需备注', status: 'warning', sla: '11h', slaStatus: 'warning', risk: '餐补天数与行程存在 1 天偏差' },
{ id: 'REQ-2026-0462', person: '何川', dept: '解决方案 · 北区', entity: 'Northstar China Ltd.', range: '本月', category: '机票', amount: '¥5,460', verdict: '规则全通过', status: 'success', sla: '7h', slaStatus: 'success', risk: '无明显风险' },
{ id: 'REQ-2026-0466', person: '宋雨', dept: '研发 · 后端组', entity: 'Northstar China Ltd.', range: '本周', category: '酒店', amount: '¥3,660', verdict: '已退回补件', status: 'danger', sla: '41h', slaStatus: 'danger', risk: '入住城市与项目地点不一致' }
]
export const initialRequests = []
export const documents = [
{ id: 'DOC-2026-0401', type: '出差申请', typeTag: 'travel', applicant: '刘倩', dept: '销售 · 华东区域', date: '2026-04-18', amount: '¥8,460', status: '审批中', statusClass: 'warning', conclusion: '待审核', destination: '上海→杭州', days: 3 },
{ id: 'DOC-2026-0402', type: '酒店预订', typeTag: 'hotel', applicant: '韩阳', dept: '解决方案 · 北区', date: '2026-04-22', amount: '¥1,280', status: '已通过', statusClass: 'success', conclusion: '规则全通过', destination: '北京·望京凯悦', days: 2 },
{ id: 'DOC-2026-0403', type: '机票预订', typeTag: 'flight', applicant: '王鑫', dept: '运营管理 · 总部', date: '2026-04-25', amount: '¥2,340', status: '已完成', statusClass: 'success', conclusion: '合规无风险', destination: '北京→深圳', days: 1 },
{ id: 'DOC-2026-0404', type: '出差申请', typeTag: 'travel', applicant: '陈嘉', dept: '市场 · 品牌活动', date: '2026-04-26', amount: '¥12,680', status: '待补件', statusClass: 'danger', conclusion: '需补充行程说明', destination: '上海→成都', days: 4 },
{ id: 'DOC-2026-0405', type: '火车票预订', typeTag: 'train', applicant: '赵敏', dept: '研发 · 后端组', date: '2026-04-27', amount: '¥553', status: '审批中', statusClass: 'warning', conclusion: '待审核', destination: '杭州→南京', days: 1 },
{ id: 'DOC-2026-0406', type: '酒店预订', typeTag: 'hotel', applicant: '刘倩', dept: '销售 · 华东区域', date: '2026-04-28', amount: '¥2,100', status: '已退回', statusClass: 'danger', conclusion: '住宿超标 23%', destination: '杭州·西湖国宾馆', days: 2 },
{ id: 'DOC-2026-0407', type: '机票预订', typeTag: 'flight', applicant: '韩阳', dept: '解决方案 · 北区', date: '2026-04-28', amount: '¥3,800', status: '已通过', statusClass: 'success', conclusion: '规则全通过', destination: '北京→广州', days: 1 },
{ id: 'DOC-2026-0408', type: '出差申请', typeTag: 'travel', applicant: '王鑫', dept: '运营管理 · 总部', date: '2026-04-29', amount: 4,200', status: '审批中', statusClass: 'warning', conclusion: '预算校验中', destination: '深圳→厦门', days: 2 }
{
id: 'DOC-2026-0501',
type: '个人报销单',
typeTag: 'expense',
applicant: '刘倩',
dept: '销售 · 华东区域',
date: '2026-05-12',
amount: 8,460',
status: '审批中',
statusClass: 'warning',
conclusion: '待审核',
destination: '上海 → 杭州',
days: 3
},
{
id: 'DOC-2026-0502',
type: '业务招待费',
typeTag: 'entertainment',
applicant: '韩阳',
dept: '解决方案 · 华南区',
date: '2026-05-09',
amount: '¥2,860',
status: '待补件',
statusClass: 'danger',
conclusion: '缺参与人员名单',
destination: '深圳福田',
days: 1
},
{
id: 'DOC-2026-0503',
type: '办公费报销',
typeTag: 'office',
applicant: '赵敏',
dept: '研发 · 平台组',
date: '2026-05-10',
amount: '¥1,280',
status: '草稿',
statusClass: 'warning',
conclusion: '待提交',
destination: '上海张江园区',
days: 1
},
{
id: 'DOC-2026-0504',
type: '会务费报销',
typeTag: 'meeting',
applicant: '陈嘉',
dept: '市场 · 品牌活动',
date: '2026-05-06',
amount: '¥6,520',
status: '已完成',
statusClass: 'success',
conclusion: '规则全通过',
destination: '苏州国际会议中心',
days: 2
}
]
export const docTypes = ['全部类型', '出差申请', '机票预订', '酒店预订', '火车票预订']
export const docStatuses = ['全部状态', '审批中', '已通过', '已完成', '待补件', '已退回']
export const docMonths = ['2026-04', '2026-03', '2026-02', '2026-01']
export const docTypes = ['全部类型', '个人报销单', '业务招待费', '办公费报销', '会务费报销']
export const docStatuses = ['全部状态', '草稿', '审批中', '已完成', '待补件']
export const docMonths = ['2026-05', '2026-04', '2026-03', '2026-02']
export const prompts = ['生成审批意见', '列出补件清单', '解释为什么拦截', '生成审计摘要']
export const prompts = ['生成审批意见', '列出补件清单', '解释风险原因', '生成沟通摘要']
export const initialMessages = [
{ id: 1, role: 'agent', text: '我已读取单据、发票、行程和公司差旅制度。当前建议:可通过,但需要补充改签说明。' },
{ id: 1, role: 'agent', text: '我已读取单据、发票和公司费用报销制度。当前建议:可以继续处理,但仍需补充部分说明。' },
{ id: 2, role: 'user', text: '请列出这张单据的主要风险。' },
{ id: 3, role: 'agent', text: '主要风险:缺少改签说明,且舱位价格高于差旅标准 12%。' }
{ id: 3, role: 'agent', text: '主要风险包括:字段缺失、票据不完整,或金额与制度标准存在偏差。' }
]

View File

@@ -1,5 +1,86 @@
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(/^REQ-(\d{4})-(\d{2})(\d{2})$/)
const match = String(id || '').match(/(\d{4})[-]?(\d{2})(\d{2})/)
if (!match) {
return ''
@@ -9,55 +90,85 @@ function parseRequestDateFromId(id) {
return `${year}-${month}-${day}`
}
function formatTripWindow(range) {
const normalized = String(range || '')
if (!normalized) {
return '待补充'
function parseAmount(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (normalized.includes('本月')) {
return '本月申请'
const normalized = String(value || '')
.replace(/[,\s]/g, '')
.replace(/[¥¥]/g, '')
.replace(/元/g, '')
.trim()
if (!normalized || !/^-?\d+(?:\.\d+)?$/.test(normalized)) {
return null
}
if (normalized.includes('本周')) {
return '本周申请'
}
if (normalized.includes('今天')) {
return '今日申请'
}
return normalized
const amount = Number(normalized)
return Number.isFinite(amount) ? amount : null
}
function mapApproval(status) {
if (status === 'success') {
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 {
node: '已完成归档',
approval: '已完成',
approvalTone: 'success',
travel: '已完成行程',
travelTone: 'success'
key: meta.key,
label: request.approvalStatus || request.approval || meta.label,
tone: request.approvalTone || meta.tone
}
}
if (status === 'danger') {
if (request?.approvalKey && APPROVAL_META[request.approvalKey]) {
return {
node: '异常待复核',
approval: '待处理',
approvalTone: 'danger',
travel: '存在异常',
travelTone: 'danger'
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 {
node: '财务审核中',
approval: '审批中',
approvalTone: 'info',
travel: '待安排行程',
travelTone: 'warning'
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
}
}
@@ -66,22 +177,60 @@ export function normalizeRequestForUi(request) {
return null
}
const applyTime = parseRequestDateFromId(request.id) || '2026-04-18'
const reason = `${request.category || '差旅'}申请`
const city = request.entity || '待补充'
const period = formatTripWindow(request.range)
const approvalState = mapApproval(request.status)
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 || '').trim() || '待提交'
const secondaryStatusValue =
String(request.secondaryStatusValue || request.travel || '').trim()
|| (detailVariant === 'travel' ? '待安排行程' : '待补充票据')
const secondaryStatusTone = String(request.secondaryStatusTone || request.travelTone || '').trim() || 'neutral'
return {
...request,
reason,
city,
period,
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,
node: approvalState.node,
approval: approvalState.approval,
approvalTone: approvalState.approvalTone,
travel: approvalState.travel,
travelTone: approvalState.travelTone
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() || '所属部门',
profileAvatar:
String(request.person || request.applicant || request.employeeName || '申').trim().slice(0, 1) || '申'
}
}