- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览 - 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示 - 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿 - PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善 - DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配 - 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
297 lines
11 KiB
JavaScript
297 lines
11 KiB
JavaScript
import {
|
||
buildApplicationTemplatePreview,
|
||
buildLocalApplicationPreview,
|
||
normalizeApplicationPreview
|
||
} from '../../utils/expenseApplicationPreview.js'
|
||
import {
|
||
buildAiApplicationPrecheckThinkingEvents,
|
||
isAiApplicationPrecheckBlocking
|
||
} from '../../utils/aiApplicationPrecheckModel.js'
|
||
import { extractExpenseClaimItems } from '../../services/reimbursements.js'
|
||
import {
|
||
AI_APPLICATION_ACTION_SAVE_DRAFT,
|
||
AI_APPLICATION_ACTION_SUBMIT
|
||
} from '../../services/aiApplicationPreviewActions.js'
|
||
import { INLINE_APPLICATION_STATUS_LABELS } from '../../constants/documentProtocol.js'
|
||
import { resolveInlineApplicationPreviewTextAction } from './workbenchAiApplicationGateModel.js'
|
||
|
||
function normalizeInlineApplicationResultTableCell(value, fallback = '-') {
|
||
const text = String(value || '')
|
||
.replace(/\s*\n+\s*/g, ' ')
|
||
.replace(/\|/g, '|')
|
||
.trim()
|
||
return text || fallback
|
||
}
|
||
|
||
export function normalizeInlineApplicationStatusLabel(value, fallback = '') {
|
||
const text = String(value || '').trim()
|
||
if (!text) {
|
||
return fallback
|
||
}
|
||
return INLINE_APPLICATION_STATUS_LABELS[text.toLowerCase()] || text
|
||
}
|
||
|
||
export function resolveInlineApplicationActionDocumentInfo(draftPayload = {}) {
|
||
const source = draftPayload && typeof draftPayload === 'object' ? draftPayload : {}
|
||
const body = String(source.body || source.markdown || '').trim()
|
||
const resolveBodyField = (labels = []) => {
|
||
for (const label of labels) {
|
||
const pattern = new RegExp(`${label}\\s*[::]\\s*([^\\n|]+)`, 'u')
|
||
const match = body.match(pattern)
|
||
if (match?.[1]) {
|
||
return String(match[1]).replace(/\*\*/g, '').trim()
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
const startDate = String(source.start_date || source.startDate || source.trip_start_date || source.tripStartDate || '').trim()
|
||
const endDate = String(source.end_date || source.endDate || source.trip_end_date || source.tripEndDate || '').trim()
|
||
const dateText = String(
|
||
source.business_time ||
|
||
source.businessTime ||
|
||
source.time ||
|
||
source.occurred_at ||
|
||
source.occurredAt ||
|
||
source.apply_time ||
|
||
source.applyTime ||
|
||
''
|
||
).trim()
|
||
const rangeText = startDate && endDate && startDate !== endDate
|
||
? `${startDate} 至 ${endDate}`
|
||
: startDate || endDate
|
||
return {
|
||
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim(),
|
||
claimId: String(source.claim_id || source.claimId || source.id || '').trim(),
|
||
statusLabel: normalizeInlineApplicationStatusLabel(source.status_label || source.statusLabel || source.status),
|
||
approvalStage: String(source.approval_stage || source.approvalStage || '').trim(),
|
||
dateLabel: rangeText || dateText || resolveBodyField(['时间', '日期', '申请时间']) || '待补充',
|
||
locationLabel: String(
|
||
source.location ||
|
||
source.application_location ||
|
||
source.applicationLocation ||
|
||
source.destination ||
|
||
source.destination_city ||
|
||
source.destinationCity ||
|
||
''
|
||
).trim() || resolveBodyField(['地点', '目的地']) || '待补充',
|
||
reasonLabel: String(
|
||
source.reason ||
|
||
source.business_reason ||
|
||
source.businessReason ||
|
||
source.description ||
|
||
source.title ||
|
||
''
|
||
).trim() || resolveBodyField(['事由', '事件', '申请事由']) || '待补充',
|
||
amountLabel: String(
|
||
source.amount ||
|
||
source.application_amount ||
|
||
source.applicationAmount ||
|
||
source.estimated_amount ||
|
||
source.estimatedAmount ||
|
||
''
|
||
).trim() || resolveBodyField(['金额', '预计金额', '申请金额']) || '-',
|
||
documentTypeLabel: String(
|
||
source.document_type_label ||
|
||
source.documentTypeLabel ||
|
||
source.application_type_label ||
|
||
source.applicationTypeLabel ||
|
||
source.expense_type_label ||
|
||
source.expenseTypeLabel ||
|
||
''
|
||
).trim()
|
||
}
|
||
}
|
||
|
||
export function buildInlineApplicationResultTable(draftPayload = {}, options = {}) {
|
||
const info = resolveInlineApplicationActionDocumentInfo(draftPayload)
|
||
const reference = info.claimNo || info.claimId
|
||
const statusLabel = normalizeInlineApplicationStatusLabel(info.statusLabel, options.statusLabel)
|
||
return [
|
||
'| 单据类型 | 单据编号 | 单据状态 | 当前节点 | 日期 | 地点 | 事由 | 金额 |',
|
||
'| --- | --- | --- | --- | --- | --- | --- | --- |',
|
||
`| ${normalizeInlineApplicationResultTableCell(info.documentTypeLabel || options.documentTypeLabel, '出差申请')} | ${normalizeInlineApplicationResultTableCell(reference)} | ${normalizeInlineApplicationResultTableCell(statusLabel)} | ${normalizeInlineApplicationResultTableCell(info.approvalStage || options.stageLabel)} | ${normalizeInlineApplicationResultTableCell(info.dateLabel)} | ${normalizeInlineApplicationResultTableCell(info.locationLabel)} | ${normalizeInlineApplicationResultTableCell(info.reasonLabel)} | ${normalizeInlineApplicationResultTableCell(info.amountLabel, '-')} |`
|
||
].join('\n')
|
||
}
|
||
|
||
export function extractInlineApplicationDraftPayload(payload = {}) {
|
||
const result = payload?.result && typeof payload.result === 'object' ? payload.result : {}
|
||
return result.draft_payload && typeof result.draft_payload === 'object'
|
||
? result.draft_payload
|
||
: payload?.draft_payload && typeof payload.draft_payload === 'object'
|
||
? payload.draft_payload
|
||
: null
|
||
}
|
||
|
||
export function buildInlineApplicationPreviewActionResultText(actionType, payload = {}) {
|
||
const draftPayload = extractInlineApplicationDraftPayload(payload) || {}
|
||
const claimNo = String(draftPayload.claim_no || draftPayload.claimNo || '').trim()
|
||
const approvalStage = String(draftPayload.approval_stage || draftPayload.approvalStage || '').trim()
|
||
if (actionType === AI_APPLICATION_ACTION_SUBMIT) {
|
||
return [
|
||
'### 申请单据已生成,并已进入审批流程',
|
||
approvalStage ? `系统已推送到 **${approvalStage}**,当前节点:${approvalStage}。` : '系统已推送到审批流程,当前节点:审批中。',
|
||
buildInlineApplicationResultTable(draftPayload, {
|
||
statusLabel: '审批中',
|
||
stageLabel: approvalStage || '直属领导审批',
|
||
documentTypeLabel: '出差申请'
|
||
})
|
||
].filter(Boolean).join('\n\n')
|
||
}
|
||
return [
|
||
'### 申请草稿已保存',
|
||
claimNo ? `系统已保存当前申请草稿,草稿单号:**${claimNo}**。` : '系统已保存当前申请草稿。',
|
||
buildInlineApplicationResultTable(draftPayload, {
|
||
statusLabel: '草稿',
|
||
stageLabel: '待提交',
|
||
documentTypeLabel: '出差申请'
|
||
})
|
||
].filter(Boolean).join('\n\n')
|
||
}
|
||
|
||
export function buildInlineApplicationDetailAction(draftPayload = {}) {
|
||
const claimNo = String(draftPayload?.claim_no || draftPayload?.claimNo || '').trim()
|
||
if (!claimNo) {
|
||
return []
|
||
}
|
||
return [{
|
||
label: '查看单据详情',
|
||
description: '打开刚生成的申请单详情。',
|
||
icon: 'mdi mdi-open-in-new',
|
||
action_type: 'open_application_detail',
|
||
payload: {
|
||
claim_no: claimNo,
|
||
claim_id: String(draftPayload.claim_id || draftPayload.claimId || '').trim(),
|
||
document_type: 'application'
|
||
}
|
||
}]
|
||
}
|
||
|
||
export function resolveInlineApplicationPreviewActionFromText(text = '') {
|
||
return resolveInlineApplicationPreviewTextAction(text)
|
||
}
|
||
|
||
export function normalizeInlineApplicationTypeLabel(expenseTypeLabel, fallback = '费用申请') {
|
||
const label = String(expenseTypeLabel || '').trim()
|
||
if (!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}申请`
|
||
}
|
||
|
||
export function buildInlineApplicationPreview(expenseTypeLabel, sourceText = '', currentUser = {}) {
|
||
const rawText = String(sourceText || '').trim()
|
||
const preview = rawText
|
||
? buildLocalApplicationPreview(rawText, currentUser)
|
||
: buildApplicationTemplatePreview(currentUser)
|
||
const normalized = normalizeApplicationPreview(preview)
|
||
return normalizeApplicationPreview({
|
||
...normalized,
|
||
fields: {
|
||
...(normalized.fields || {}),
|
||
applicationType: normalizeInlineApplicationTypeLabel(
|
||
expenseTypeLabel,
|
||
normalized.fields?.applicationType || '费用申请'
|
||
)
|
||
}
|
||
})
|
||
}
|
||
|
||
export function resolveInlineApplicationDraftIdentity(payload = {}) {
|
||
const source = payload && typeof payload === 'object' ? payload : {}
|
||
return {
|
||
claimId: String(source.claim_id || source.claimId || source.id || '').trim(),
|
||
claimNo: String(source.claim_no || source.claimNo || source.document_no || source.documentNo || '').trim()
|
||
}
|
||
}
|
||
|
||
export function isSameInlineApplicationDraftClaim(claim = {}, draftPayload = {}) {
|
||
const draftIdentity = resolveInlineApplicationDraftIdentity(draftPayload)
|
||
if (!draftIdentity.claimId && !draftIdentity.claimNo) {
|
||
return false
|
||
}
|
||
const claimIdentity = resolveInlineApplicationDraftIdentity(claim)
|
||
return Boolean(
|
||
(draftIdentity.claimId && claimIdentity.claimId && draftIdentity.claimId === claimIdentity.claimId) ||
|
||
(draftIdentity.claimNo && claimIdentity.claimNo && draftIdentity.claimNo === claimIdentity.claimNo)
|
||
)
|
||
}
|
||
|
||
export function buildInlineApplicationSubmitPrecheckPayload(claimsPayload, draftPayload = {}) {
|
||
const items = extractExpenseClaimItems(claimsPayload)
|
||
.filter((claim) => !isSameInlineApplicationDraftClaim(claim, draftPayload))
|
||
return { items }
|
||
}
|
||
|
||
export function completeInlineThinkingEvents(events = []) {
|
||
return events.map((event) => ({
|
||
...event,
|
||
status: event.status === 'failed' ? 'failed' : 'completed'
|
||
}))
|
||
}
|
||
|
||
export function buildInitialInlineApplicationSubmitThinkingEvents() {
|
||
return [
|
||
{
|
||
eventId: 'application-precheck-overlap',
|
||
title: '核查同时间段申请单',
|
||
content: '正在查询您名下可见申请单,检查是否存在相同或重叠日期。',
|
||
status: 'running'
|
||
},
|
||
{
|
||
eventId: 'application-precheck-budget',
|
||
title: '评估预算与审批影响',
|
||
content: '等待单据重叠核查完成后,继续评估预算占用和审批影响。',
|
||
status: 'pending'
|
||
},
|
||
{
|
||
eventId: 'application-submit',
|
||
title: '提交申请单据',
|
||
content: '等待提交前核查完成。',
|
||
status: 'pending'
|
||
}
|
||
]
|
||
}
|
||
|
||
export function buildInlineApplicationSubmitThinkingEvents(precheck = {}) {
|
||
const blocked = isAiApplicationPrecheckBlocking(precheck)
|
||
return buildAiApplicationPrecheckThinkingEvents(precheck).map((event) => {
|
||
if (event.eventId !== 'application-precheck-form') {
|
||
return event
|
||
}
|
||
return {
|
||
eventId: 'application-submit',
|
||
title: blocked ? '暂停提交申请' : '提交申请单据',
|
||
content: blocked
|
||
? '发现相同或重叠日期已有申请单,已暂停本次提交。'
|
||
: '提交前核查通过,正在生成申请单据并推送审批流程。',
|
||
status: blocked ? 'completed' : 'running'
|
||
}
|
||
})
|
||
}
|
||
|
||
export function buildFailedInlineApplicationSubmitThinkingEvents(error) {
|
||
return [
|
||
{
|
||
eventId: 'application-precheck-overlap',
|
||
title: '核查同时间段申请单',
|
||
content: `查询已有申请单失败:${String(error?.message || error || '未知错误')}`,
|
||
status: 'failed'
|
||
},
|
||
{
|
||
eventId: 'application-submit',
|
||
title: '暂停提交申请',
|
||
content: '因为未能完成提交前重复日期核查,系统没有提交本次申请。',
|
||
status: 'failed'
|
||
}
|
||
]
|
||
}
|