feat: 增强风险规则生成引擎与预算中心页面
后端拆分风险规则生成为解释器、语义分析、本体对齐等子模块, 优化模板执行和流程图生成,完善员工种子数据和导入逻辑,增强 报销单权限策略和草稿持久化,前端新增预算中心视图和趋势图 组件,重构审计页面和风险规则测试对话框交互,完善文档中心 和报销创建页面细节,补充单元测试覆盖。
This commit is contained in:
528
web/src/utils/expenseApplicationPreview.js
Normal file
528
web/src/utils/expenseApplicationPreview.js
Normal file
@@ -0,0 +1,528 @@
|
||||
import { buildApplicationFieldsFromOntology } from './expenseApplicationOntology.js'
|
||||
|
||||
const APPLICATION_SESSION_TYPE = 'application'
|
||||
const APPLICATION_CREATE_PATTERN = /申请|事前|前置|预算|出差|差旅|采购|会务|会议|培训|办公用品|交通|住宿/
|
||||
const APPLICATION_QUERY_PATTERN = /查询|状态|进度|列表|有哪些|材料清单|需要哪些|制度|标准|规则|怎么规定/
|
||||
|
||||
export const APPLICATION_PREVIEW_FIELD_DEFINITIONS = [
|
||||
{ key: 'applicationType', label: '申请类型' },
|
||||
{ key: 'grade', label: '职级', highlight: true },
|
||||
{ key: 'time', label: '发生时间' },
|
||||
{ key: 'location', label: '地点' },
|
||||
{ key: 'reason', label: '事由' },
|
||||
{ key: 'days', label: '天数' },
|
||||
{ key: 'transportMode', label: '出行方式' },
|
||||
{ key: 'lodgingDailyCap', label: '住宿上限/天', highlight: true, editable: false, required: false },
|
||||
{ key: 'subsidyDailyCap', label: '补贴标准/天', highlight: true, editable: false, required: false },
|
||||
{ key: 'transportPolicy', label: '交通费用口径', highlight: true, editable: false, required: false },
|
||||
{ key: 'policyEstimate', label: '规则测算参考', highlight: true, editable: false, required: false },
|
||||
{ key: 'amount', label: '用户预估费用', highlight: true }
|
||||
]
|
||||
|
||||
export const APPLICATION_TRANSPORT_MODE_OPTIONS = ['火车', '飞机', '轮船']
|
||||
|
||||
const APPLICATION_POLICY_PENDING_TEXT = '填写地点和天数后自动测算'
|
||||
const APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT = '车票、机票暂无实时价格接口,按真实票据实报实销'
|
||||
|
||||
function compactText(value) {
|
||||
return String(value || '').replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function resolveFirstMatch(text, patterns = []) {
|
||||
for (const pattern of patterns) {
|
||||
const match = text.match(pattern)
|
||||
const value = String(match?.groups?.value || match?.[1] || '').trim()
|
||||
if (value) return value.replace(/[,。;;]$/, '')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizeDateText(value) {
|
||||
return String(value || '').replace(/[/.]/g, '-').replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
function parseIsoDate(value) {
|
||||
const match = normalizeDateText(value).match(/^(20\d{2})-(\d{1,2})-(\d{1,2})$/)
|
||||
if (!match) return null
|
||||
const [, year, month, day] = match
|
||||
const date = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)))
|
||||
return Number.isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
function formatIsoDate(date) {
|
||||
return date.toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
function buildEndDateFromDays(startText, daysText = '') {
|
||||
const days = Number(String(daysText || '').replace(/[^\d]/g, ''))
|
||||
const start = parseIsoDate(startText)
|
||||
if (!days || !start) return ''
|
||||
const end = new Date(start.getTime())
|
||||
end.setUTCDate(end.getUTCDate() + days)
|
||||
return formatIsoDate(end)
|
||||
}
|
||||
|
||||
function resolveDaysFromDateRange(rangeText) {
|
||||
const match = String(rangeText || '').match(/(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})\s*至\s*(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/)
|
||||
if (!match) return ''
|
||||
const start = parseIsoDate(match[1])
|
||||
const end = parseIsoDate(match[2])
|
||||
if (!start || !end) return ''
|
||||
const diffDays = Math.round((end.getTime() - start.getTime()) / 86400000)
|
||||
return diffDays > 0 ? `${diffDays}天` : '1天'
|
||||
}
|
||||
|
||||
function resolveApplicationType(text) {
|
||||
const compact = compactText(text)
|
||||
if (/差旅|出差|高铁|动车|火车|飞机|机票|航班|酒店|住宿/.test(compact)) return '差旅费用申请'
|
||||
if (/交通|出租车|的士|网约车|打车|通勤/.test(compact)) return '交通费用申请'
|
||||
if (/住宿|酒店/.test(compact)) return '住宿费用申请'
|
||||
if (/会务|会议|发布会|展会/.test(compact)) return '会务费用申请'
|
||||
if (/采购|办公用品|文具|设备|耗材/.test(compact)) return '采购费用申请'
|
||||
if (/培训|课程|学习/.test(compact)) return '培训费用申请'
|
||||
return '费用申请'
|
||||
}
|
||||
|
||||
function resolveApplicationAmount(text) {
|
||||
const compact = compactText(text)
|
||||
const labeled = resolveFirstMatch(text, [
|
||||
/(?:用户预估费用|预估费用|申请金额|预计金额|预计费用|预计总费用|预算|金额|费用)\s*[::]?\s*(?<value>\d+(?:\.\d+)?)\s*(?:元|块|人民币)?/u,
|
||||
/(?<value>\d+(?:\.\d+)?)\s*(?:元|块|人民币)/u
|
||||
])
|
||||
if (labeled) return `${labeled}元`
|
||||
if (/不知道预算|预算不清楚|预算待定|待测算/.test(compact)) return '待测算'
|
||||
return ''
|
||||
}
|
||||
|
||||
function resolveCurrentUserGrade(currentUser = {}) {
|
||||
return String(
|
||||
currentUser.grade
|
||||
|| currentUser.employeeGrade
|
||||
|| currentUser.employee_grade
|
||||
|| currentUser.profileGrade
|
||||
|| ''
|
||||
).trim()
|
||||
}
|
||||
|
||||
function parseApplicationDaysValue(value) {
|
||||
const match = String(value || '').match(/\d+/)
|
||||
const days = match ? Number(match[0]) : 0
|
||||
return Number.isFinite(days) && days > 0 ? Math.max(1, Math.floor(days)) : 0
|
||||
}
|
||||
|
||||
function parseMoneyNumber(value) {
|
||||
const normalized = String(value ?? '').replace(/[^\d.-]/g, '')
|
||||
const amount = Number(normalized)
|
||||
return Number.isFinite(amount) ? amount : null
|
||||
}
|
||||
|
||||
function formatPolicyMoney(value) {
|
||||
const amount = parseMoneyNumber(value)
|
||||
if (amount === null) return String(value || '').trim()
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
|
||||
}).format(amount)
|
||||
}
|
||||
|
||||
function formatDailyPolicyMoney(value) {
|
||||
const display = formatPolicyMoney(value)
|
||||
return display ? `${display}元/天` : APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
|
||||
function buildTransportPolicyText(transportMode) {
|
||||
const mode = String(transportMode || '').trim()
|
||||
if (!mode) return APPLICATION_TRANSPORT_REIMBURSEMENT_TEXT
|
||||
return `${mode}票据暂无实时价格接口,按真实票据实报实销`
|
||||
}
|
||||
|
||||
function ensureApplicationPolicyFields(fields = {}) {
|
||||
const nextFields = { ...fields }
|
||||
if (!String(nextFields.lodgingDailyCap || '').trim()) {
|
||||
nextFields.lodgingDailyCap = APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
if (!String(nextFields.subsidyDailyCap || '').trim()) {
|
||||
nextFields.subsidyDailyCap = APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
if (!String(nextFields.transportPolicy || '').trim() || /实报实销/.test(String(nextFields.transportPolicy || ''))) {
|
||||
nextFields.transportPolicy = buildTransportPolicyText(nextFields.transportMode)
|
||||
}
|
||||
if (!String(nextFields.policyEstimate || '').trim()) {
|
||||
nextFields.policyEstimate = APPLICATION_POLICY_PENDING_TEXT
|
||||
}
|
||||
return nextFields
|
||||
}
|
||||
|
||||
function resolveApplicationDays(text) {
|
||||
const value = resolveFirstMatch(text, [
|
||||
/(?:出差|申请)?(?<value>\d+)\s*天/u,
|
||||
/(?<value>\d+)\s*(?:个)?工作日/u
|
||||
])
|
||||
return value ? `${value}天` : ''
|
||||
}
|
||||
|
||||
function resolveApplicationTime(text, daysText = '') {
|
||||
const range = text.match(
|
||||
/(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})\s*(?:至|到|~|—|–|--)\s*(20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u
|
||||
)
|
||||
if (range) {
|
||||
return `${normalizeDateText(range[1])} 至 ${normalizeDateText(range[2])}`
|
||||
}
|
||||
|
||||
const single = resolveFirstMatch(text, [
|
||||
/(?:发生时间|业务发生时间|申请时间|时间)\s*[::]\s*(?<value>20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u,
|
||||
/(?<value>20\d{2}[-/.]\d{1,2}[-/.]\d{1,2})/u
|
||||
])
|
||||
if (!single) return ''
|
||||
const normalized = normalizeDateText(single)
|
||||
const endDate = buildEndDateFromDays(normalized, daysText)
|
||||
return endDate ? `${normalized} 至 ${endDate}` : normalized
|
||||
}
|
||||
|
||||
function resolveApplicationLocation(text) {
|
||||
return resolveFirstMatch(text, [
|
||||
/(?:地点|业务地点|发生地点|目的地)\s*[::]\s*(?<value>[^。;;\n]+)/u,
|
||||
/(?:去|到|前往)(?<value>[\u4e00-\u9fa5,,、]{2,24}?)(?:出差|支撑|支持|部署|开会|培训|拜访|验收|项目|客户|。|\s|$)/u
|
||||
])
|
||||
}
|
||||
|
||||
function resolveApplicationTransportMode(text) {
|
||||
const compact = compactText(text)
|
||||
if (/高铁|动车|火车|铁路/.test(compact)) return '火车'
|
||||
if (/飞机|机票|航班/.test(compact)) return '飞机'
|
||||
if (/轮船|船票|客轮|渡轮/.test(compact)) return '轮船'
|
||||
return ''
|
||||
}
|
||||
|
||||
function stripKnownContextFromReason(value, context = {}) {
|
||||
const location = String(context.location || '').trim()
|
||||
let cleaned = String(value || '')
|
||||
.replace(/(?:发生时间|业务发生时间|申请时间|时间)\s*[::]\s*(?=[,,、。;;\s]|$)/gu, '')
|
||||
.replace(/(?:地点|业务地点|发生地点|目的地)\s*[::]\s*(?=[,,、。;;\s]|$)/gu, '')
|
||||
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}\s*(?:至|到|~|—|–|--)\s*20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
|
||||
.replace(/20\d{2}[-/.]\d{1,2}[-/.]\d{1,2}/gu, '')
|
||||
.replace(/(?:出差|申请)?\d+\s*天/gu, '')
|
||||
.replace(/(?:用户预估费用|预估费用|预计总费用|预计费用|预计金额|申请金额|预算|金额|费用)\s*[::]?\s*\d+(?:\.\d+)?\s*(?:元|块|人民币)?/gu, '')
|
||||
.replace(/(?:高铁|动车|火车|铁路|飞机|机票|航班|轮船|船票|客轮|渡轮|出租车|的士|网约车|打车|自驾)/gu, '')
|
||||
.replace(/[,,、。;;]+/g, ',')
|
||||
.replace(/^\s*(去|到|前往)/u, '')
|
||||
.replace(/^[,\s]+|[,\s]+$/g, '')
|
||||
.trim()
|
||||
|
||||
if (location) {
|
||||
const escapedLocation = location.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
cleaned = cleaned
|
||||
.replace(new RegExp(`^${escapedLocation}(?:出差)?(?:,|,|、)?`, 'u'), '')
|
||||
.replace(new RegExp(`^(?:去|到|前往)${escapedLocation}(?:出差)?(?:,|,|、)?`, 'u'), '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
function pickBusinessReasonSegment(text) {
|
||||
const segments = String(text || '')
|
||||
.split(/[,,、。;;\n]+/u)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
return segments.find((item) => /服务|支撑|支持|部署|实施|验收|拜访|对接|沟通|培训|会议|采购|安装|维护|上线|调试|项目/.test(item)) || ''
|
||||
}
|
||||
|
||||
function resolveApplicationReason(text, context = {}) {
|
||||
const labeled = resolveFirstMatch(text, [
|
||||
/(?:事由|申请事由|出差事由|原因|用途)\s*[::]\s*(?<value>[^,。;;\n]+)/u
|
||||
])
|
||||
if (labeled) return stripKnownContextFromReason(labeled, context)
|
||||
const cleaned = String(text || '')
|
||||
.replace(/^\s*(我想|我要|帮我)?\s*(先)?\s*(发起|提交|申请)?\s*(一笔)?\s*(费用申请|申请)\s*/u, '')
|
||||
const withoutContext = stripKnownContextFromReason(cleaned, context)
|
||||
const businessSegment = pickBusinessReasonSegment(withoutContext)
|
||||
if (businessSegment) return stripKnownContextFromReason(businessSegment, context)
|
||||
return withoutContext
|
||||
}
|
||||
|
||||
function isApplicationPreviewValueProvided(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
return Boolean(normalized) && !['待测算', '待补充', '未知'].includes(normalized)
|
||||
}
|
||||
|
||||
function resolveProvidedValue(value, fallback = '') {
|
||||
return isApplicationPreviewValueProvided(value) ? String(value).trim() : fallback
|
||||
}
|
||||
|
||||
function normalizeApplicationTypeLabel(value, fallback = '') {
|
||||
const label = String(value || '').trim()
|
||||
if (!label || label === '其他费用') return fallback || '费用申请'
|
||||
if (label.endsWith('费用申请') || label.endsWith('申请')) return label
|
||||
if (label.endsWith('费用')) return `${label}申请`
|
||||
if (label.endsWith('费')) return `${label.slice(0, -1)}费用申请`
|
||||
return `${label}申请`
|
||||
}
|
||||
|
||||
function normalizeTransportModeOption(value, fallback = '') {
|
||||
const text = String(value || '').trim()
|
||||
if (/飞机|机票|航班/.test(text)) return '飞机'
|
||||
if (/轮船|船票|客轮|渡轮|邮轮/.test(text)) return '轮船'
|
||||
if (/火车|高铁|动车|铁路|列车/.test(text)) return '火车'
|
||||
return APPLICATION_TRANSPORT_MODE_OPTIONS.includes(text) ? text : fallback
|
||||
}
|
||||
|
||||
function normalizeAmountFromOntology(fields = {}, fallback = '') {
|
||||
const numericAmount = Number(fields.amount || 0)
|
||||
if (Number.isFinite(numericAmount) && numericAmount > 0) {
|
||||
return `${numericAmount}元`
|
||||
}
|
||||
|
||||
const display = String(fields.amountDisplay || '').trim()
|
||||
if (display && display !== '待补充') {
|
||||
const normalized = display.replace(/^¥\s*/, '').replace(/,/g, '').trim()
|
||||
return normalized.endsWith('元') ? normalized : `${normalized}元`
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
function buildMissingFields(fields) {
|
||||
return APPLICATION_PREVIEW_FIELD_DEFINITIONS
|
||||
.filter((item) => item.key !== 'applicationType' && item.required !== false)
|
||||
.filter((item) => !isApplicationPreviewValueProvided(fields?.[item.key]))
|
||||
.map((item) => item.label)
|
||||
}
|
||||
|
||||
export function buildApplicationPolicyEstimateRequest(preview = {}, currentUser = {}) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
const days = parseApplicationDaysValue(fields.days)
|
||||
const location = String(fields.location || '').trim()
|
||||
const grade = String(fields.grade || resolveCurrentUserGrade(currentUser)).trim()
|
||||
const applicationType = String(fields.applicationType || '').trim()
|
||||
const transportMode = String(fields.transportMode || '').trim()
|
||||
const shouldEstimate = /差旅|住宿|交通/.test(applicationType) || Boolean(transportMode)
|
||||
|
||||
if (!shouldEstimate || !days || !location) {
|
||||
return {
|
||||
canCalculate: false,
|
||||
reason: '缺少地点或天数',
|
||||
payload: null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canCalculate: true,
|
||||
reason: '',
|
||||
payload: {
|
||||
days,
|
||||
location,
|
||||
grade
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyApplicationPolicyEstimateResult(preview = {}, result = {}, currentUser = {}) {
|
||||
const fields = { ...(preview?.fields || {}) }
|
||||
const days = Number(result?.days) || parseApplicationDaysValue(fields.days) || 1
|
||||
const hotelRate = formatPolicyMoney(result?.hotel_rate)
|
||||
const hotelAmount = formatPolicyMoney(result?.hotel_amount)
|
||||
const allowanceRate = formatPolicyMoney(result?.total_allowance_rate)
|
||||
const allowanceAmount = formatPolicyMoney(result?.allowance_amount)
|
||||
const totalAmount = formatPolicyMoney(result?.total_amount)
|
||||
const matchedCity = String(result?.matched_city || fields.location || '').trim()
|
||||
const grade = String(result?.grade || fields.grade || resolveCurrentUserGrade(currentUser)).trim()
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
...preview,
|
||||
fields: {
|
||||
...fields,
|
||||
grade,
|
||||
lodgingDailyCap: formatDailyPolicyMoney(result?.hotel_rate),
|
||||
subsidyDailyCap: formatDailyPolicyMoney(result?.total_allowance_rate),
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode),
|
||||
policyEstimate: `住宿 ${hotelAmount}元 + 补贴 ${allowanceAmount}元 = ${totalAmount}元(${days}天,不含交通票据)`,
|
||||
matchedCity,
|
||||
ruleName: String(result?.rule_name || '').trim(),
|
||||
ruleVersion: String(result?.rule_version || '').trim(),
|
||||
hotelAmount: hotelAmount ? `${hotelAmount}元` : '',
|
||||
allowanceAmount: allowanceAmount ? `${allowanceAmount}元` : '',
|
||||
policyTotalAmount: totalAmount ? `${totalAmount}元` : ''
|
||||
},
|
||||
policyEstimate: {
|
||||
...result,
|
||||
grade,
|
||||
matchedCity
|
||||
},
|
||||
policyEstimateStatus: 'completed'
|
||||
})
|
||||
}
|
||||
|
||||
export function applyApplicationPolicyEstimateError(preview = {}, error = null, currentUser = {}) {
|
||||
const fields = { ...(preview?.fields || {}) }
|
||||
const message = String(error?.message || error || '').trim()
|
||||
return normalizeApplicationPreview({
|
||||
...preview,
|
||||
fields: {
|
||||
...fields,
|
||||
grade: fields.grade || resolveCurrentUserGrade(currentUser),
|
||||
transportPolicy: buildTransportPolicyText(fields.transportMode),
|
||||
policyEstimate: message ? `规则中心暂未完成测算:${message}` : APPLICATION_POLICY_PENDING_TEXT
|
||||
},
|
||||
policyEstimateStatus: message ? 'failed' : 'pending'
|
||||
})
|
||||
}
|
||||
|
||||
export function shouldUseLocalApplicationPreview(rawText, options = {}) {
|
||||
if (String(options.sessionType || '').trim() !== APPLICATION_SESSION_TYPE) return false
|
||||
if (options.systemGenerated || options.reviewAction || Number(options.attachmentCount || 0) > 0) return false
|
||||
|
||||
const compact = compactText(rawText)
|
||||
if (!compact || APPLICATION_QUERY_PATTERN.test(compact)) return false
|
||||
return APPLICATION_CREATE_PATTERN.test(compact)
|
||||
}
|
||||
|
||||
export function normalizeApplicationPreview(preview = {}) {
|
||||
const fields = ensureApplicationPolicyFields(preview?.fields || {})
|
||||
const missingFields = buildMissingFields(fields)
|
||||
return {
|
||||
...preview,
|
||||
fields,
|
||||
missingFields,
|
||||
readyToSubmit: missingFields.length === 0
|
||||
}
|
||||
}
|
||||
|
||||
export function buildModelRefinedApplicationPreview(localPreview = {}, ontology = {}, rawText = '', currentUser = {}) {
|
||||
const currentFields = localPreview?.fields || {}
|
||||
const ontologyFields = buildApplicationFieldsFromOntology(ontology || {}, rawText, currentUser)
|
||||
const parseStrategy = String(ontology?.parse_strategy || '').trim()
|
||||
const refinedFields = {
|
||||
...currentFields,
|
||||
applicationType: normalizeApplicationTypeLabel(
|
||||
ontologyFields.expenseTypeLabel,
|
||||
currentFields.applicationType
|
||||
),
|
||||
time: resolveProvidedValue(ontologyFields.timeRange, currentFields.time),
|
||||
location: resolveProvidedValue(ontologyFields.location, currentFields.location),
|
||||
reason: resolveProvidedValue(ontologyFields.reason, currentFields.reason),
|
||||
days: resolveProvidedValue(ontologyFields.days, currentFields.days),
|
||||
transportMode: normalizeTransportModeOption(
|
||||
ontologyFields.transportMode,
|
||||
currentFields.transportMode
|
||||
),
|
||||
amount: normalizeAmountFromOntology(ontologyFields, currentFields.amount),
|
||||
grade: resolveProvidedValue(currentFields.grade, resolveCurrentUserGrade(currentUser)),
|
||||
applicant: resolveProvidedValue(ontologyFields.applicant, currentFields.applicant),
|
||||
department: resolveProvidedValue(ontologyFields.department, currentFields.department)
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
...localPreview,
|
||||
sourceText: String(rawText || localPreview.sourceText || '').trim(),
|
||||
fields: refinedFields,
|
||||
modelRefined: true,
|
||||
parseStrategy,
|
||||
modelReviewStatus: parseStrategy === 'llm_primary' ? 'completed' : 'fallback'
|
||||
})
|
||||
}
|
||||
|
||||
export function buildApplicationPreviewRows(preview = {}) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const fields = normalized.fields || {}
|
||||
return APPLICATION_PREVIEW_FIELD_DEFINITIONS.map((item) => {
|
||||
const rawValue = fields[item.key]
|
||||
const value = String(rawValue || '').trim() || '待补充'
|
||||
return {
|
||||
...item,
|
||||
value,
|
||||
editable: item.editable !== false,
|
||||
highlight: Boolean(item.highlight),
|
||||
missing: item.required !== false && !isApplicationPreviewValueProvided(rawValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function buildApplicationPreviewSubmitText(preview = {}) {
|
||||
const rows = buildApplicationPreviewRows(preview)
|
||||
return [
|
||||
'费用申请确认提交',
|
||||
...rows.map((row) => `${row.label}:${row.value}`),
|
||||
'',
|
||||
'确认提交'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildLocalApplicationPreview(rawText, currentUser = {}) {
|
||||
const sourceText = String(rawText || '').trim()
|
||||
const explicitDays = resolveApplicationDays(sourceText)
|
||||
const time = resolveApplicationTime(sourceText, explicitDays)
|
||||
const days = explicitDays || resolveDaysFromDateRange(time)
|
||||
const location = resolveApplicationLocation(sourceText)
|
||||
const fields = {
|
||||
applicationType: resolveApplicationType(sourceText),
|
||||
time,
|
||||
location,
|
||||
reason: resolveApplicationReason(sourceText, { location }),
|
||||
days,
|
||||
transportMode: resolveApplicationTransportMode(sourceText),
|
||||
amount: resolveApplicationAmount(sourceText),
|
||||
grade: resolveCurrentUserGrade(currentUser),
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充'
|
||||
}
|
||||
|
||||
return normalizeApplicationPreview({
|
||||
sourceText,
|
||||
fields,
|
||||
modelReviewStatus: 'local'
|
||||
})
|
||||
}
|
||||
|
||||
export function buildApplicationTemplatePreview(currentUser = {}) {
|
||||
return normalizeApplicationPreview({
|
||||
sourceText: '快速发起申请',
|
||||
fields: {
|
||||
applicationType: '费用申请',
|
||||
time: '',
|
||||
location: '',
|
||||
reason: '',
|
||||
days: '',
|
||||
transportMode: '',
|
||||
amount: '',
|
||||
grade: resolveCurrentUserGrade(currentUser),
|
||||
applicant: currentUser.name || currentUser.username || '当前用户',
|
||||
department: currentUser.department || currentUser.departmentName || '待补充'
|
||||
},
|
||||
modelReviewStatus: 'template'
|
||||
})
|
||||
}
|
||||
|
||||
export function buildLocalApplicationPreviewMessage(preview) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const modelReviewStatus = String(normalized.modelReviewStatus || '').trim()
|
||||
return [
|
||||
modelReviewStatus === 'completed'
|
||||
? '我已完成模型复核,并整理成下方表格。请核查识别结果;点击对应行即可直接编辑。'
|
||||
: modelReviewStatus === 'fallback'
|
||||
? '模型复核没有返回稳定结果,我已先按规则兜底整理成下方表格。请重点核查识别结果;点击对应行即可直接编辑。'
|
||||
: modelReviewStatus === 'failed'
|
||||
? '模型复核暂时失败,我先保留一份临时核对表,方便您核查和补充信息。点击对应行即可直接编辑。'
|
||||
: modelReviewStatus === 'template'
|
||||
? '我已为你准备好费用申请模板。本步骤不调用大模型,也不会保存草稿;请点击对应行直接填写。'
|
||||
: '我先整理出下方表格,请核查识别结果。点击对应行即可直接编辑。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildApplicationPreviewFooterMessage(preview) {
|
||||
const normalized = normalizeApplicationPreview(preview)
|
||||
const missingFields = Array.isArray(normalized.missingFields) ? normalized.missingFields : []
|
||||
const modelReviewStatus = String(normalized.modelReviewStatus || '').trim()
|
||||
if (missingFields.length) {
|
||||
return `当前还需要补充:${missingFields.join('、')}。补齐后我再帮您提交申请。`
|
||||
}
|
||||
if (modelReviewStatus === 'fallback') {
|
||||
return '当前结果仅完成规则兜底复核,暂不直接提交。请核查表格内容,或稍后重新发起模型复核。'
|
||||
}
|
||||
if (modelReviewStatus === 'failed') {
|
||||
return '当前结果仅作为临时预览,暂不直接提交。请稍后重试,或补充更明确的信息后再提交。'
|
||||
}
|
||||
|
||||
return '请核对表格信息无误,确认无误后点击 [确认](#application-submit) 提交至审批流程。'
|
||||
}
|
||||
Reference in New Issue
Block a user