Files
X-Financial/web/src/composables/workbenchAiMode/workbenchAiApplicationPreviewModel.js
caoxiaozhu ee730aa31c feat(web): AI 工作台文件预览/附件关联任务与草稿分支
- 新增 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 等
2026-06-24 10:42:50 +08:00

297 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.
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'
}
]
}