feat(web): 报销单新增关联申请单门控与草稿检测流程
- 新增 travelReimbursementAssociationGateModel,查询可关联申请单/草稿报销单并生成跳过/选择/单独新建动作,区分差旅费与业务招待费类型 - travelReimbursementApplicationLinkModel 补充 buildLinkedApplicationReferenceIndex/buildRequiredApplicationActions 等关联构建逻辑 - useTravelReimbursementSuggestedActions 接入 select_required_application/skip 系列动作,'我要报销'入口改为先走关联门控 - useWorkbenchAiActionRouter 新增 SKIP_REQUIRED_APPLICATION_LINK/SKIP_REIMBURSEMENT_DRAFT_CHECK 动作分发 - useWorkbenchAiExpenseFlow 暴露 startAiReimbursementAssociationGate,stewardPlanModel 待处理流程适配 - 新增 workbench-ai-action-router、workbench-ai-reimbursement-association-gate 测试并更新 guided-flow、steward-plan 测试
This commit is contained in:
@@ -258,6 +258,51 @@ function resolveCandidateFlowExpenseType(flow = {}) {
|
||||
return rawType
|
||||
}
|
||||
|
||||
function normalizeStewardExpenseTypeCode(value = '') {
|
||||
const text = String(value || '').trim()
|
||||
if (text === '差旅' || text === '差旅费' || text === 'travel') {
|
||||
return 'travel'
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
function resolveTaskExpenseType(task = null) {
|
||||
const fields = task?.ontologyFields || task?.ontology_fields || {}
|
||||
const explicitType = normalizeStewardExpenseTypeCode(
|
||||
fields.expense_type ||
|
||||
fields.expenseType ||
|
||||
fields.application_type ||
|
||||
fields.applicationType ||
|
||||
''
|
||||
)
|
||||
if (explicitType) {
|
||||
return explicitType
|
||||
}
|
||||
const taskText = [
|
||||
task?.title,
|
||||
task?.summary,
|
||||
fields.reason,
|
||||
fields.location
|
||||
].map((item) => String(item || '').trim()).join(' ')
|
||||
return /差旅|出差/.test(taskText) ? 'travel' : ''
|
||||
}
|
||||
|
||||
function buildStewardApplicationPreviewRoutePayload(actionType, task = null) {
|
||||
if (actionType !== 'confirm_create_application') {
|
||||
return {}
|
||||
}
|
||||
const expenseType = resolveTaskExpenseType(task)
|
||||
if (expenseType !== 'travel') {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
steward_confirm_flow: true,
|
||||
flow_id: 'travel_application',
|
||||
expense_type: expenseType,
|
||||
expense_type_label: FLOW_EXPENSE_TYPE_LABELS[expenseType] || ''
|
||||
}
|
||||
}
|
||||
|
||||
export function buildStewardSuggestedActions(plan) {
|
||||
const normalized = normalizeStewardPlan(plan)
|
||||
if (isOffTopicPlan(normalized)) {
|
||||
@@ -320,6 +365,7 @@ export function buildStewardSuggestedActions(plan) {
|
||||
action_type: ASSISTANT_SCOPE_ACTION_SWITCH,
|
||||
payload: {
|
||||
session_type: targetSessionType,
|
||||
...buildStewardApplicationPreviewRoutePayload(actionType, task),
|
||||
carry_text: buildStewardCarryText(actionType, task, group, normalized),
|
||||
carry_files: actionType !== 'confirm_create_application',
|
||||
auto_submit: true,
|
||||
|
||||
@@ -46,6 +46,22 @@ function uniqueValues(values) {
|
||||
return Array.from(new Set((Array.isArray(values) ? values : []).map(normalizeText).filter(Boolean)))
|
||||
}
|
||||
|
||||
function expandIdentityValues(values) {
|
||||
const expanded = []
|
||||
;(Array.isArray(values) ? values : []).forEach((value) => {
|
||||
const normalized = normalizeText(value)
|
||||
if (!normalized) {
|
||||
return
|
||||
}
|
||||
expanded.push(normalized)
|
||||
const atIndex = normalized.indexOf('@')
|
||||
if (atIndex > 0) {
|
||||
expanded.push(normalized.slice(0, atIndex))
|
||||
}
|
||||
})
|
||||
return uniqueValues(expanded)
|
||||
}
|
||||
|
||||
function normalizeClaimNo(claim) {
|
||||
return normalizeText(claim?.claim_no || claim?.claimNo).toUpperCase()
|
||||
}
|
||||
@@ -205,7 +221,7 @@ function hasAnyApplicationReference(index) {
|
||||
return Boolean(index?.ids?.size || index?.claimNos?.size)
|
||||
}
|
||||
|
||||
function buildLinkedApplicationReferenceIndex(claims) {
|
||||
export function buildLinkedApplicationReferenceIndex(claims) {
|
||||
const index = createReferenceIndex()
|
||||
;(Array.isArray(claims) ? claims : []).forEach((claim) => {
|
||||
if (isExpenseApplicationClaim(claim)) {
|
||||
@@ -297,6 +313,58 @@ export function getRequiredApplicationExpenseLabel(expenseType) {
|
||||
return EXPENSE_TYPE_LABELS[normalizeLower(expenseType)] || '报销'
|
||||
}
|
||||
|
||||
export function resolveRequiredApplicationReimbursementType(application = {}) {
|
||||
const expenseType = normalizeLower(
|
||||
application.application_expense_type
|
||||
|| application.expense_type
|
||||
|| application.expenseType
|
||||
|| application.type_code
|
||||
|| application.typeCode
|
||||
)
|
||||
const source = {
|
||||
expense_type: expenseType,
|
||||
reason: application.application_reason || application.reason,
|
||||
title: application.application_reason || application.reason,
|
||||
description: application.application_reason || application.reason,
|
||||
location: application.application_location || application.location
|
||||
}
|
||||
|
||||
if (APPLICATION_TYPE_ALIASES.travel.has(expenseType)) {
|
||||
return {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: EXPENSE_TYPE_LABELS.travel
|
||||
}
|
||||
}
|
||||
|
||||
if (APPLICATION_TYPE_ALIASES.meal.has(expenseType)) {
|
||||
return {
|
||||
expenseType: 'meal',
|
||||
expenseTypeLabel: EXPENSE_TYPE_LABELS.meal
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesGenericApplicationByText(source, 'meal')) {
|
||||
return {
|
||||
expenseType: 'meal',
|
||||
expenseTypeLabel: EXPENSE_TYPE_LABELS.meal
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesGenericApplicationByText(source, 'travel')) {
|
||||
return {
|
||||
expenseType: 'travel',
|
||||
expenseTypeLabel: EXPENSE_TYPE_LABELS.travel
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
expenseType: REQUIRED_APPLICATION_EXPENSE_TYPES.has(expenseType) ? expenseType : 'travel',
|
||||
expenseTypeLabel: getRequiredApplicationExpenseLabel(
|
||||
REQUIRED_APPLICATION_EXPENSE_TYPES.has(expenseType) ? expenseType : 'travel'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function isExpenseApplicationClaim(claim) {
|
||||
const documentType = normalizeDocumentType(claim)
|
||||
const expenseType = normalizeExpenseType(claim)
|
||||
@@ -323,7 +391,7 @@ export function matchesRequiredApplicationExpenseType(claim, expenseType) {
|
||||
}
|
||||
|
||||
export function isClaimOwnedByCurrentUser(claim, currentUser = {}) {
|
||||
const userIds = uniqueValues([
|
||||
const userIds = expandIdentityValues([
|
||||
currentUser.id,
|
||||
currentUser.employeeId,
|
||||
currentUser.employee_id,
|
||||
@@ -332,11 +400,13 @@ export function isClaimOwnedByCurrentUser(claim, currentUser = {}) {
|
||||
currentUser.username,
|
||||
currentUser.email
|
||||
])
|
||||
const claimIds = uniqueValues([
|
||||
const claimIds = expandIdentityValues([
|
||||
claim?.employee_id,
|
||||
claim?.employeeId,
|
||||
claim?.employee_no,
|
||||
claim?.employeeNo,
|
||||
claim?.employee_email,
|
||||
claim?.employeeEmail,
|
||||
claim?.username,
|
||||
claim?.user_id,
|
||||
claim?.userId
|
||||
|
||||
747
web/src/views/scripts/travelReimbursementAssociationGateModel.js
Normal file
747
web/src/views/scripts/travelReimbursementAssociationGateModel.js
Normal file
@@ -0,0 +1,747 @@
|
||||
import {
|
||||
buildLinkedApplicationReferenceIndex,
|
||||
buildRequiredApplicationActions,
|
||||
isClaimOwnedByCurrentUser,
|
||||
isExpenseApplicationClaim,
|
||||
isUsableRequiredApplicationClaim,
|
||||
normalizeRequiredApplicationCandidate,
|
||||
resolveRequiredApplicationReimbursementType
|
||||
} from './travelReimbursementApplicationLinkModel.js'
|
||||
|
||||
const REIMBURSEMENT_DRAFT_STATUSES = new Set(['draft', 'supplement', 'returned'])
|
||||
|
||||
const STATUS_LABELS = {
|
||||
draft: '草稿',
|
||||
supplement: '待补充',
|
||||
returned: '已退回',
|
||||
submitted: '审批中',
|
||||
approved: '已审批',
|
||||
completed: '已完成',
|
||||
archived: '已归档',
|
||||
pending_payment: '待付款',
|
||||
paid: '已付款'
|
||||
}
|
||||
|
||||
const EXPENSE_TYPE_LABELS = {
|
||||
travel: '差旅费',
|
||||
meal: '业务招待费'
|
||||
}
|
||||
const REIMBURSEMENT_ASSOCIATION_STEP_DELAY_MS = 320
|
||||
export const REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS = 12000
|
||||
const REIMBURSEMENT_ASSOCIATION_QUERY_PARAMS = Object.freeze({ page: 1, pageSize: 100 })
|
||||
const REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MESSAGE = '查询可关联申请单超时,请稍后重试。'
|
||||
|
||||
export const SKIP_REQUIRED_APPLICATION_LINK_ACTION = 'skip_required_application_link'
|
||||
export const SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION = 'skip_reimbursement_draft_check'
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '').trim()
|
||||
}
|
||||
|
||||
function normalizeLower(value) {
|
||||
return normalizeText(value).toLowerCase()
|
||||
}
|
||||
|
||||
function normalizeExpenseType(claim) {
|
||||
return normalizeLower(claim?.expense_type || claim?.expenseType || claim?.type_code || claim?.typeCode)
|
||||
}
|
||||
|
||||
function normalizeClaimStatus(claim) {
|
||||
return normalizeLower(claim?.status || claim?.state || claim?.approval_status || claim?.approvalStatus)
|
||||
}
|
||||
|
||||
function extractClaims(claimsPayload) {
|
||||
return Array.isArray(claimsPayload)
|
||||
? claimsPayload
|
||||
: Array.isArray(claimsPayload?.items)
|
||||
? claimsPayload.items
|
||||
: Array.isArray(claimsPayload?.claims)
|
||||
? claimsPayload.claims
|
||||
: []
|
||||
}
|
||||
|
||||
function escapeHtml(value = '') {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function toTimestamp(value) {
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? 0 : date.getTime()
|
||||
}
|
||||
|
||||
function formatAmount(value) {
|
||||
const numberValue = Number(String(value ?? '').replace(/[^\d.-]/g, ''))
|
||||
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
||||
return ''
|
||||
}
|
||||
return `¥${new Intl.NumberFormat('zh-CN', {
|
||||
minimumFractionDigits: Number.isInteger(numberValue) ? 0 : 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(numberValue)}`
|
||||
}
|
||||
|
||||
function resolveCurrentUser(currentUser) {
|
||||
return currentUser?.value && typeof currentUser.value === 'object'
|
||||
? currentUser.value
|
||||
: currentUser && typeof currentUser === 'object'
|
||||
? currentUser
|
||||
: {}
|
||||
}
|
||||
|
||||
function waitForReimbursementAssociationStep() {
|
||||
return new Promise((resolve) => {
|
||||
globalThis.setTimeout(resolve, REIMBURSEMENT_ASSOCIATION_STEP_DELAY_MS)
|
||||
})
|
||||
}
|
||||
|
||||
function createReimbursementAssociationQueryTimeoutError() {
|
||||
const error = new Error(REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MESSAGE)
|
||||
error.code = 'REQUEST_TIMEOUT'
|
||||
return error
|
||||
}
|
||||
|
||||
export function isReimbursementAssociationQueryTimeoutError(error) {
|
||||
return error?.code === 'REQUEST_TIMEOUT'
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationQueryFailedText(error) {
|
||||
if (isReimbursementAssociationQueryTimeoutError(error)) {
|
||||
return '查询可关联申请单超时。你可以稍后重试,也可以选择不关联申请单,单独新建报销单。'
|
||||
}
|
||||
return '查询可关联申请单时出现异常。你可以稍后重试,也可以选择不关联申请单,单独新建报销单。'
|
||||
}
|
||||
|
||||
export async function fetchReimbursementAssociationClaims({
|
||||
fetchExpenseClaims,
|
||||
timeoutMs = REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS
|
||||
} = {}) {
|
||||
if (typeof fetchExpenseClaims !== 'function') {
|
||||
return { items: [] }
|
||||
}
|
||||
|
||||
const queryPromise = Promise.resolve(fetchExpenseClaims(
|
||||
REIMBURSEMENT_ASSOCIATION_QUERY_PARAMS,
|
||||
{
|
||||
timeoutMs,
|
||||
timeoutMessage: REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MESSAGE
|
||||
}
|
||||
))
|
||||
|
||||
const normalizedTimeoutMs = Number(timeoutMs)
|
||||
if (!Number.isFinite(normalizedTimeoutMs) || normalizedTimeoutMs <= 0) {
|
||||
return queryPromise
|
||||
}
|
||||
|
||||
let timeoutId = 0
|
||||
const timeoutPromise = new Promise((resolve, reject) => {
|
||||
timeoutId = globalThis.setTimeout(() => {
|
||||
reject(createReimbursementAssociationQueryTimeoutError())
|
||||
}, normalizedTimeoutMs)
|
||||
})
|
||||
|
||||
try {
|
||||
return await Promise.race([queryPromise, timeoutPromise])
|
||||
} finally {
|
||||
if (timeoutId) {
|
||||
globalThis.clearTimeout(timeoutId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function filterReimbursementAssociationCandidates(claimsPayload, currentUser = {}) {
|
||||
const claims = extractClaims(claimsPayload)
|
||||
const linkedApplicationReferences = buildLinkedApplicationReferenceIndex(claims)
|
||||
|
||||
return claims
|
||||
.filter((claim) => (
|
||||
isExpenseApplicationClaim(claim)
|
||||
&& isUsableRequiredApplicationClaim(claim, linkedApplicationReferences)
|
||||
&& isClaimOwnedByCurrentUser(claim, currentUser)
|
||||
))
|
||||
.map(normalizeRequiredApplicationCandidate)
|
||||
.sort((left, right) => toTimestamp(right.application_date) - toTimestamp(left.application_date))
|
||||
}
|
||||
|
||||
export function normalizeReimbursementDraftCandidate(claim = {}) {
|
||||
const status = normalizeClaimStatus(claim)
|
||||
const amount = normalizeText(claim?.amount || claim?.total_amount || claim?.totalAmount)
|
||||
const createdAt = normalizeText(
|
||||
claim?.updated_at
|
||||
|| claim?.updatedAt
|
||||
|| claim?.created_at
|
||||
|| claim?.createdAt
|
||||
|| claim?.submitted_at
|
||||
|| claim?.submittedAt
|
||||
)
|
||||
|
||||
return {
|
||||
id: normalizeText(claim?.id || claim?.claim_id || claim?.claimId),
|
||||
claim_no: normalizeText(claim?.claim_no || claim?.claimNo),
|
||||
expense_type: normalizeExpenseType(claim),
|
||||
reason: normalizeText(claim?.reason || claim?.business_reason || claim?.description || claim?.title),
|
||||
location: normalizeText(claim?.location || claim?.business_location || claim?.businessLocation),
|
||||
amount,
|
||||
amount_label: formatAmount(amount),
|
||||
status,
|
||||
status_label: STATUS_LABELS[status] || normalizeText(claim?.status_label || claim?.statusLabel || claim?.approval_stage || claim?.approvalStage || status),
|
||||
created_at: createdAt,
|
||||
application_date: createdAt
|
||||
}
|
||||
}
|
||||
|
||||
export function filterReimbursementDraftCandidates(claimsPayload, currentUser = {}) {
|
||||
return extractClaims(claimsPayload)
|
||||
.filter((claim) => (
|
||||
!isExpenseApplicationClaim(claim)
|
||||
&& REIMBURSEMENT_DRAFT_STATUSES.has(normalizeClaimStatus(claim))
|
||||
&& isClaimOwnedByCurrentUser(claim, currentUser)
|
||||
))
|
||||
.map(normalizeReimbursementDraftCandidate)
|
||||
.sort((left, right) => toTimestamp(right.application_date) - toTimestamp(left.application_date))
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationSelectionText(applications) {
|
||||
const candidates = Array.isArray(applications) ? applications : []
|
||||
return [
|
||||
'### 可关联申请单',
|
||||
'',
|
||||
'我先检查你名下是否有可继续的报销草稿,没有查到可继续的报销草稿。',
|
||||
'',
|
||||
'我先查询可关联申请单,并筛选了你名下已审批且未关联报销的记录。',
|
||||
'',
|
||||
`查到 ${candidates.length} 个已审批且尚未关联报销的申请单。你可以选择关联其中一个,也可以选择不关联、单独新建报销单。`,
|
||||
'',
|
||||
buildReimbursementAssociationCardsHtml(candidates),
|
||||
'',
|
||||
'请通过下方按钮确认是否关联申请单。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationMissingText() {
|
||||
return [
|
||||
'我先检查你名下是否有可继续的报销草稿,没有查到可继续的报销草稿。',
|
||||
'',
|
||||
'我先查询可关联申请单,并筛选了你名下已审批且未关联报销的记录。',
|
||||
'',
|
||||
'暂时没有查到已审批且尚未关联报销的申请单。你仍然可以选择单独新建报销单,后续按报销类型继续补充信息。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildReimbursementDraftSelectionText(drafts) {
|
||||
const candidates = Array.isArray(drafts) ? drafts : []
|
||||
return [
|
||||
'### 可继续报销草稿',
|
||||
'',
|
||||
'我先检查你名下是否有可继续的报销草稿。',
|
||||
'',
|
||||
`查到 ${candidates.length} 个可继续的报销草稿。你可以先继续草稿;如果这次是新的报销,可以跳过草稿后再关联申请单新建报销单。`,
|
||||
'',
|
||||
buildReimbursementDraftCardsHtml(candidates),
|
||||
'',
|
||||
'请通过下方按钮确认继续草稿,或跳过草稿进入申请单关联。'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function buildSkipRequiredApplicationLinkAction(originalMessage = '') {
|
||||
return {
|
||||
label: '不关联,单独新建报销单',
|
||||
description: '跳过申请单关联,继续选择报销类型并新建报销单。',
|
||||
icon: 'mdi mdi-file-plus-outline',
|
||||
action_type: SKIP_REQUIRED_APPLICATION_LINK_ACTION,
|
||||
payload: {
|
||||
original_message: normalizeText(originalMessage) || '我要报销'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSkipReimbursementDraftCheckAction(originalMessage = '') {
|
||||
return {
|
||||
label: '不用草稿,关联申请单新建报销单',
|
||||
description: '跳过已有报销草稿,继续查询可关联申请单。',
|
||||
icon: 'mdi mdi-file-search-outline',
|
||||
action_type: SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION,
|
||||
payload: {
|
||||
original_message: normalizeText(originalMessage) || '我要报销'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildReimbursementDraftActions(drafts, originalMessage = '') {
|
||||
const sourceText = normalizeText(originalMessage) || '我要报销'
|
||||
return [
|
||||
...(Array.isArray(drafts) ? drafts : []).map((draft) => {
|
||||
const claimNo = normalizeText(draft.claim_no) || '未编号草稿'
|
||||
return {
|
||||
label: `继续草稿 ${claimNo}`,
|
||||
description: [
|
||||
draft.status_label,
|
||||
draft.created_at && `更新时间:${draft.created_at}`,
|
||||
draft.location && `地点:${draft.location}`,
|
||||
draft.amount_label && `金额:${draft.amount_label}`,
|
||||
draft.reason && `事由:${draft.reason}`
|
||||
].filter(Boolean).join(' · '),
|
||||
icon: 'mdi mdi-file-document-edit-outline',
|
||||
action_type: 'open_application_detail',
|
||||
payload: {
|
||||
claim_id: draft.id,
|
||||
claim_no: draft.claim_no,
|
||||
original_message: sourceText
|
||||
}
|
||||
}
|
||||
}),
|
||||
buildSkipReimbursementDraftCheckAction(sourceText)
|
||||
]
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationActions(applications, originalMessage = '') {
|
||||
const sourceText = normalizeText(originalMessage) || '我要报销'
|
||||
return [
|
||||
...buildRequiredApplicationActions(applications, 'select_required_application').map((action) => ({
|
||||
...action,
|
||||
payload: {
|
||||
...(action.payload || {}),
|
||||
original_message: sourceText
|
||||
}
|
||||
})),
|
||||
buildSkipRequiredApplicationLinkAction(sourceText)
|
||||
]
|
||||
}
|
||||
|
||||
function buildAssociationCardFieldHtml(label = '', value = '', options = {}) {
|
||||
const text = normalizeText(value)
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
const fieldClass = options.fieldClass ? ` ${options.fieldClass}` : ''
|
||||
const valueClass = options.valueClass ? ` ${options.valueClass}` : ''
|
||||
return [
|
||||
`<div class="ai-document-card__field${fieldClass}">`,
|
||||
`<span class="ai-document-card__label">${escapeHtml(label)}</span>`,
|
||||
`<strong class="ai-document-card__value${valueClass}">${escapeHtml(text)}</strong>`,
|
||||
'</div>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
function buildReimbursementDraftCardHtml(draft = {}) {
|
||||
const statusLabel = normalizeText(draft.status_label) || '草稿'
|
||||
const title = normalizeText(EXPENSE_TYPE_LABELS[normalizeLower(draft.expense_type)] || draft.expense_type) || '报销草稿'
|
||||
const summaryHtml = [
|
||||
buildAssociationCardFieldHtml('金额', draft.amount_label || draft.amount || '待确认', {
|
||||
valueClass: 'ai-document-card__amount'
|
||||
}),
|
||||
buildAssociationCardFieldHtml('更新时间', draft.created_at || '待确认')
|
||||
].join('')
|
||||
const detailsHtml = [
|
||||
buildAssociationCardFieldHtml('地点', draft.location || '待补充'),
|
||||
buildAssociationCardFieldHtml('单据编号', draft.claim_no || '未编号草稿', {
|
||||
valueClass: 'ai-document-card__number'
|
||||
}),
|
||||
buildAssociationCardFieldHtml('事由', draft.reason || '待补充'),
|
||||
buildAssociationCardFieldHtml('单据类型', `报销单 · ${title}`),
|
||||
buildAssociationCardFieldHtml('操作', '使用下方按钮继续', {
|
||||
fieldClass: 'ai-document-card__field--action'
|
||||
})
|
||||
].join('')
|
||||
|
||||
return [
|
||||
'<article class="ai-document-card ai-document-card--expense is-pending" aria-label="报销草稿详情">',
|
||||
'<header class="ai-document-card__head">',
|
||||
`<strong class="ai-document-card__reason">${escapeHtml(title)}</strong>`,
|
||||
`<span class="ai-document-card__status">${escapeHtml(statusLabel)}</span>`,
|
||||
'</header>',
|
||||
'<div class="ai-document-card__body">',
|
||||
summaryHtml ? `<div class="ai-document-card__summary">${summaryHtml}</div>` : '',
|
||||
'<div class="ai-document-card__details">',
|
||||
detailsHtml,
|
||||
'</div>',
|
||||
'</div>',
|
||||
'</article>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
export function buildReimbursementDraftCardsHtml(drafts = []) {
|
||||
const candidates = (Array.isArray(drafts) ? drafts : []).slice(0, 5)
|
||||
if (!candidates.length) {
|
||||
return ''
|
||||
}
|
||||
return [
|
||||
'<!-- ai-trusted-html:start -->',
|
||||
'<section class="ai-document-card-list" aria-label="可继续报销草稿">',
|
||||
...candidates.map((draft) => buildReimbursementDraftCardHtml(draft)),
|
||||
'</section>',
|
||||
'<!-- ai-trusted-html:end -->'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function buildReimbursementAssociationCardHtml(application = {}) {
|
||||
const statusLabel = normalizeText(application.status_label) || '已审批'
|
||||
const statusTone = ['已审批', '已完成', '已归档'].some((item) => statusLabel.includes(item))
|
||||
? 'is-success'
|
||||
: 'is-pending'
|
||||
const title = normalizeText(resolveRequiredApplicationReimbursementType(application).expenseTypeLabel) || '费用申请'
|
||||
const summaryHtml = [
|
||||
buildAssociationCardFieldHtml('时间', application.business_time || '待补充'),
|
||||
buildAssociationCardFieldHtml('预计金额', application.amount_label || application.amount || '待确认', {
|
||||
valueClass: 'ai-document-card__amount'
|
||||
})
|
||||
].join('')
|
||||
const detailsHtml = [
|
||||
buildAssociationCardFieldHtml('地点', application.location || '待补充'),
|
||||
buildAssociationCardFieldHtml('单据编号', application.claim_no || '未编号申请单', {
|
||||
valueClass: 'ai-document-card__number'
|
||||
}),
|
||||
buildAssociationCardFieldHtml('事由', application.reason || '待补充'),
|
||||
buildAssociationCardFieldHtml('单据类型', `申请单 · ${title}`),
|
||||
buildAssociationCardFieldHtml('操作', '使用下方按钮关联', {
|
||||
fieldClass: 'ai-document-card__field--action'
|
||||
})
|
||||
].join('')
|
||||
|
||||
return [
|
||||
`<article class="ai-document-card ai-document-card--application ${statusTone}" aria-label="申请单详情">`,
|
||||
'<header class="ai-document-card__head">',
|
||||
`<strong class="ai-document-card__reason">${escapeHtml(title)}</strong>`,
|
||||
`<span class="ai-document-card__status">${escapeHtml(statusLabel)}</span>`,
|
||||
'</header>',
|
||||
'<div class="ai-document-card__body">',
|
||||
summaryHtml ? `<div class="ai-document-card__summary">${summaryHtml}</div>` : '',
|
||||
'<div class="ai-document-card__details">',
|
||||
detailsHtml,
|
||||
'</div>',
|
||||
'</div>',
|
||||
'</article>'
|
||||
].join('')
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationCardsHtml(applications = []) {
|
||||
const candidates = (Array.isArray(applications) ? applications : []).slice(0, 5)
|
||||
if (!candidates.length) {
|
||||
return ''
|
||||
}
|
||||
return [
|
||||
'<!-- ai-trusted-html:start -->',
|
||||
'<section class="ai-document-card-list" aria-label="可关联申请单">',
|
||||
...candidates.map((application) => buildReimbursementAssociationCardHtml(application)),
|
||||
'</section>',
|
||||
'<!-- ai-trusted-html:end -->'
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function resolveAssociationStatusGroup(application = {}) {
|
||||
const status = normalizeLower(application.status)
|
||||
if (['approved', 'completed', 'archived'].includes(status)) {
|
||||
return { key: 'completed', label: '已审批' }
|
||||
}
|
||||
if (['submitted', 'review', 'pending'].includes(status)) {
|
||||
return { key: 'in_progress', label: '审批中' }
|
||||
}
|
||||
if (['returned', 'rejected'].includes(status)) {
|
||||
return { key: 'draft', label: '待完善' }
|
||||
}
|
||||
return { key: 'other', label: '其他状态' }
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationQueryPayload(applications = []) {
|
||||
const candidates = Array.isArray(applications) ? applications : []
|
||||
const records = candidates.map((application) => {
|
||||
const statusGroup = resolveAssociationStatusGroup(application)
|
||||
return {
|
||||
claimId: normalizeText(application.id || application.claim_no),
|
||||
claimNo: normalizeText(application.claim_no) || '未编号申请单',
|
||||
employeeName: '',
|
||||
expenseType: normalizeText(application.expense_type),
|
||||
expenseTypeLabel: resolveRequiredApplicationReimbursementType(application).expenseTypeLabel,
|
||||
amount: Number(String(application.amount || '').replace(/,/g, '')) || 0,
|
||||
amountDisplay: normalizeText(application.amount_label || application.amount) || '待确认',
|
||||
status: normalizeText(application.status),
|
||||
statusLabel: normalizeText(application.status_label) || statusGroup.label,
|
||||
statusGroup: statusGroup.key,
|
||||
statusGroupLabel: statusGroup.label,
|
||||
approvalStage: '',
|
||||
documentDate: normalizeText(application.application_date),
|
||||
occurredAt: '',
|
||||
reason: normalizeText(application.reason) || '费用申请',
|
||||
location: normalizeText(application.location),
|
||||
riskItems: [],
|
||||
summary: normalizeText(application.reason) || '费用申请',
|
||||
dateDisplay: normalizeText(application.business_time || application.application_date) || '待补充日期'
|
||||
}
|
||||
})
|
||||
const statusGroups = records.reduce((groups, record) => {
|
||||
const key = record.statusGroup || 'other'
|
||||
const existing = groups.get(key) || {
|
||||
key,
|
||||
label: record.statusGroupLabel || '其他状态',
|
||||
count: 0
|
||||
}
|
||||
existing.count += 1
|
||||
groups.set(key, existing)
|
||||
return groups
|
||||
}, new Map())
|
||||
return {
|
||||
resultType: 'expense_claim_list',
|
||||
scopeLabel: '可关联申请单',
|
||||
selectionMode: 'reimbursement_application_association',
|
||||
selectionLocked: false,
|
||||
selectedClaimId: '',
|
||||
title: '可关联申请单',
|
||||
emptyText: '当前没有可关联的已审批申请单。',
|
||||
recentWindowApplied: false,
|
||||
windowDays: null,
|
||||
windowStartDate: '',
|
||||
windowEndDate: '',
|
||||
recordCount: records.length,
|
||||
previewCount: records.length,
|
||||
previewLimit: 5,
|
||||
olderRecordCount: 0,
|
||||
hasMoreInWindow: false,
|
||||
totalAmount: records.reduce((sum, record) => sum + Number(record.amount || 0), 0),
|
||||
statusGroups: Array.from(statusGroups.values()),
|
||||
records,
|
||||
currentPage: 1
|
||||
}
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationThinkingEvents(stage = 'intent', options = {}) {
|
||||
const candidateCount = Number(options.candidateCount || 0)
|
||||
const failed = stage === 'failed'
|
||||
const stageOrder = {
|
||||
intent: 0,
|
||||
query: 1,
|
||||
filter: 2,
|
||||
completed: 4,
|
||||
failed: 4
|
||||
}
|
||||
const currentOrder = stageOrder[stage] ?? 0
|
||||
const resolveStatus = (index) => {
|
||||
if (failed && index >= currentOrder - 1) {
|
||||
return 'failed'
|
||||
}
|
||||
if (currentOrder > index) {
|
||||
return 'completed'
|
||||
}
|
||||
if (currentOrder === index) {
|
||||
return 'running'
|
||||
}
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
eventId: 'reimbursement-association-intent',
|
||||
title: '判断用户意图',
|
||||
content: currentOrder > 0
|
||||
? '已识别为报销创建请求,需要先检查是否已有报销草稿。'
|
||||
: '正在判断用户是否要创建报销单,并确认是否需要先进入草稿与申请单关联检查。',
|
||||
status: resolveStatus(0)
|
||||
},
|
||||
{
|
||||
eventId: 'reimbursement-draft-check',
|
||||
title: '检查报销草稿',
|
||||
content: currentOrder > 1
|
||||
? '已完成报销草稿检查,继续判断是否需要进入申请单关联。'
|
||||
: '正在查询你名下是否存在可继续的报销草稿。',
|
||||
status: resolveStatus(1)
|
||||
},
|
||||
{
|
||||
eventId: 'reimbursement-association-query',
|
||||
title: '查询可关联申请单',
|
||||
content: currentOrder > 2
|
||||
? `已完成申请单查询与筛选,命中 ${candidateCount} 张可推荐单据。`
|
||||
: '如未发现可继续草稿,就查询你名下已审批且尚未关联报销的申请单。',
|
||||
status: resolveStatus(2)
|
||||
},
|
||||
{
|
||||
eventId: 'reimbursement-association-card',
|
||||
title: '生成单据卡片',
|
||||
content: currentOrder > 3
|
||||
? '已按草稿优先、申请单后置的顺序生成单据卡片和快捷按钮。'
|
||||
: '等待查询结果返回后,以卡片形式展示可继续或可关联的单据。',
|
||||
status: resolveStatus(3)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildReimbursementAssociationSubmitOptions(application = {}, originalMessage = '') {
|
||||
const sourceText = normalizeText(originalMessage) || '我要报销'
|
||||
const resolvedType = resolveRequiredApplicationReimbursementType(application)
|
||||
const applicationId = normalizeText(application.application_claim_id || application.id)
|
||||
const applicationNo = normalizeText(application.application_claim_no || application.claim_no)
|
||||
const applicationReason = normalizeText(application.application_reason || application.reason)
|
||||
const applicationLocation = normalizeText(application.application_location || application.location)
|
||||
const applicationBusinessTime = normalizeText(application.application_business_time || application.business_time)
|
||||
const applicationTransportMode = normalizeText(application.application_transport_mode || application.transport_mode)
|
||||
|
||||
const rawText = [
|
||||
sourceText,
|
||||
`用户选择报销场景:${resolvedType.expenseTypeLabel}`,
|
||||
applicationNo && `关联申请单:${applicationNo}`
|
||||
].filter(Boolean).join('\n')
|
||||
|
||||
return {
|
||||
rawText,
|
||||
userText: `关联申请单 ${applicationNo || ''}`.trim() || '关联申请单',
|
||||
pendingText: `已关联申请单,正在生成${resolvedType.expenseTypeLabel}草稿...`,
|
||||
systemGenerated: true,
|
||||
skipUserMessage: true,
|
||||
skipDraftAssociationPrompt: true,
|
||||
associationConfirmed: true,
|
||||
extraContext: {
|
||||
draft_claim_id: '',
|
||||
review_action: 'save_draft',
|
||||
user_input_text: sourceText,
|
||||
expense_scene_selection: {
|
||||
expense_type: resolvedType.expenseType,
|
||||
expense_type_label: resolvedType.expenseTypeLabel,
|
||||
original_message: sourceText,
|
||||
application_claim_id: applicationId,
|
||||
application_claim_no: applicationNo
|
||||
},
|
||||
review_form_values: {
|
||||
expense_type: resolvedType.expenseTypeLabel,
|
||||
reason: applicationReason,
|
||||
location: applicationLocation,
|
||||
time_range: applicationBusinessTime,
|
||||
transport_mode: applicationTransportMode,
|
||||
amount: '',
|
||||
application_claim_id: applicationId,
|
||||
application_claim_no: applicationNo,
|
||||
application_reason: applicationReason,
|
||||
application_location: applicationLocation,
|
||||
application_amount: application.application_amount || application.amount || '',
|
||||
application_amount_label: application.application_amount_label || application.amount_label || '',
|
||||
application_business_time: applicationBusinessTime,
|
||||
application_days: application.application_days || application.days || '',
|
||||
application_transport_mode: applicationTransportMode,
|
||||
application_lodging_daily_cap: application.application_lodging_daily_cap || application.lodging_daily_cap || '',
|
||||
application_subsidy_daily_cap: application.application_subsidy_daily_cap || application.subsidy_daily_cap || '',
|
||||
application_transport_policy: application.application_transport_policy || application.transport_policy || '',
|
||||
application_policy_estimate: application.application_policy_estimate || application.policy_estimate || '',
|
||||
application_rule_name: application.application_rule_name || application.rule_name || '',
|
||||
application_rule_version: application.application_rule_version || application.rule_version || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildReimbursementAssociationPromptPayload({
|
||||
originalMessage = '我要报销',
|
||||
fetchExpenseClaims,
|
||||
currentUser,
|
||||
skipDraftCheck = false,
|
||||
queryTimeoutMs = REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS
|
||||
} = {}) {
|
||||
const sourceText = normalizeText(originalMessage) || '我要报销'
|
||||
try {
|
||||
const claimsPayload = await fetchReimbursementAssociationClaims({
|
||||
fetchExpenseClaims,
|
||||
timeoutMs: queryTimeoutMs
|
||||
})
|
||||
const user = resolveCurrentUser(currentUser)
|
||||
const drafts = skipDraftCheck ? [] : filterReimbursementDraftCandidates(claimsPayload, user)
|
||||
if (drafts.length) {
|
||||
return {
|
||||
text: buildReimbursementDraftSelectionText(drafts),
|
||||
meta: ['等待选择报销草稿'],
|
||||
suggestedActions: buildReimbursementDraftActions(drafts, sourceText),
|
||||
queryPayload: null,
|
||||
candidateCount: drafts.length
|
||||
}
|
||||
}
|
||||
const candidates = filterReimbursementAssociationCandidates(claimsPayload, user)
|
||||
return {
|
||||
text: candidates.length
|
||||
? buildReimbursementAssociationSelectionText(candidates)
|
||||
: buildReimbursementAssociationMissingText(),
|
||||
meta: candidates.length ? ['等待关联申请单'] : ['无可关联申请单'],
|
||||
suggestedActions: buildReimbursementAssociationActions(candidates, sourceText),
|
||||
queryPayload: buildReimbursementAssociationQueryPayload(candidates),
|
||||
candidateCount: candidates.length
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Fetch reimbursement association applications failed:', error)
|
||||
return {
|
||||
text: buildReimbursementAssociationQueryFailedText(error),
|
||||
meta: ['申请单查询失败'],
|
||||
suggestedActions: buildReimbursementAssociationActions([], sourceText),
|
||||
queryPayload: buildReimbursementAssociationQueryPayload([]),
|
||||
candidateCount: 0,
|
||||
failed: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function pushReimbursementAssociationPromptMessage({
|
||||
rawText,
|
||||
createMessage,
|
||||
messages,
|
||||
nextTick,
|
||||
scrollToBottom,
|
||||
persistSessionState,
|
||||
fetchExpenseClaims,
|
||||
currentUser,
|
||||
skipDraftCheck = false,
|
||||
queryTimeoutMs = REIMBURSEMENT_ASSOCIATION_QUERY_TIMEOUT_MS
|
||||
} = {}) {
|
||||
const pendingMessage = createMessage('assistant', '', [], {
|
||||
pending: true,
|
||||
meta: ['思考中'],
|
||||
stewardPlan: {
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: buildReimbursementAssociationThinkingEvents('intent')
|
||||
},
|
||||
suggestedActions: []
|
||||
})
|
||||
messages.value.push(pendingMessage)
|
||||
nextTick(scrollToBottom)
|
||||
persistSessionState()
|
||||
await waitForReimbursementAssociationStep()
|
||||
|
||||
pendingMessage.stewardPlan = {
|
||||
...(pendingMessage.stewardPlan || {}),
|
||||
streamStatus: 'streaming',
|
||||
thinkingEvents: buildReimbursementAssociationThinkingEvents('query')
|
||||
}
|
||||
nextTick(scrollToBottom)
|
||||
|
||||
const associationPrompt = await buildReimbursementAssociationPromptPayload({
|
||||
originalMessage: rawText,
|
||||
fetchExpenseClaims,
|
||||
currentUser,
|
||||
skipDraftCheck,
|
||||
queryTimeoutMs
|
||||
})
|
||||
pendingMessage.stewardPlan = {
|
||||
...(pendingMessage.stewardPlan || {}),
|
||||
streamStatus: associationPrompt.failed ? 'failed' : 'streaming',
|
||||
thinkingEvents: buildReimbursementAssociationThinkingEvents(
|
||||
associationPrompt.failed ? 'failed' : 'filter',
|
||||
{ candidateCount: associationPrompt.candidateCount }
|
||||
)
|
||||
}
|
||||
nextTick(scrollToBottom)
|
||||
if (!associationPrompt.failed) {
|
||||
await waitForReimbursementAssociationStep()
|
||||
}
|
||||
|
||||
pendingMessage.text = associationPrompt.text
|
||||
pendingMessage.pending = false
|
||||
pendingMessage.meta = associationPrompt.meta
|
||||
pendingMessage.suggestedActions = associationPrompt.suggestedActions
|
||||
pendingMessage.queryPayload = associationPrompt.queryPayload
|
||||
pendingMessage.stewardPlan = {
|
||||
...(pendingMessage.stewardPlan || {}),
|
||||
streamStatus: associationPrompt.failed ? 'failed' : 'completed',
|
||||
thinkingEvents: buildReimbursementAssociationThinkingEvents(
|
||||
associationPrompt.failed ? 'failed' : 'completed',
|
||||
{ candidateCount: associationPrompt.candidateCount }
|
||||
)
|
||||
}
|
||||
nextTick(scrollToBottom)
|
||||
persistSessionState()
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import {
|
||||
buildOperationFeedbackState,
|
||||
resolveAssistantResultText
|
||||
} from './travelReimbursementSubmitResponseModel.js'
|
||||
import {
|
||||
pushReimbursementAssociationPromptMessage
|
||||
} from './travelReimbursementAssociationGateModel.js'
|
||||
|
||||
export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
const {
|
||||
@@ -446,10 +449,16 @@ export function useTravelReimbursementSubmitComposer(ctx) {
|
||||
}
|
||||
|
||||
if (waitForExpenseSceneSelection) {
|
||||
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(rawText), [], {
|
||||
meta: ['等待选择场景'],
|
||||
suggestedActions: buildExpenseSceneSelectionActions(rawText)
|
||||
}))
|
||||
await pushReimbursementAssociationPromptMessage({
|
||||
rawText,
|
||||
createMessage,
|
||||
messages,
|
||||
nextTick,
|
||||
scrollToBottom,
|
||||
persistSessionState,
|
||||
fetchExpenseClaims,
|
||||
currentUser
|
||||
})
|
||||
composerDraft.value = ''
|
||||
composerBusinessTimeTags.value = []
|
||||
composerBusinessTimeDraftTouched.value = false
|
||||
|
||||
@@ -17,8 +17,15 @@ import {
|
||||
import {
|
||||
SESSION_TYPE_APPLICATION,
|
||||
SESSION_TYPE_BUDGET,
|
||||
SESSION_TYPE_EXPENSE,
|
||||
canUseBudgetAssistantSession
|
||||
} from './travelReimbursementConversationModel.js'
|
||||
import {
|
||||
SKIP_REQUIRED_APPLICATION_LINK_ACTION,
|
||||
SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION,
|
||||
buildReimbursementAssociationSubmitOptions,
|
||||
pushReimbursementAssociationPromptMessage
|
||||
} from './travelReimbursementAssociationGateModel.js'
|
||||
import { STEWARD_ASSISTANT_NAME } from './useTravelReimbursementStewardRuntime.js'
|
||||
import {
|
||||
buildStewardFieldCompletionContinuation,
|
||||
@@ -40,6 +47,7 @@ export function useTravelReimbursementSuggestedActions({
|
||||
createMessage,
|
||||
currentUser,
|
||||
emit,
|
||||
fetchExpenseClaims = async () => ({ items: [] }),
|
||||
handleGuidedShortcut,
|
||||
handleGuidedSuggestedAction,
|
||||
handleSceneSelectionApplicationGate,
|
||||
@@ -224,14 +232,14 @@ export function useTravelReimbursementSuggestedActions({
|
||||
return true
|
||||
}
|
||||
|
||||
function pushExpenseSceneSelectionPrompt(originalMessage) {
|
||||
function pushExpenseSceneSelectionPrompt(originalMessage, userEcho = '我要报销') {
|
||||
const sourceText = String(originalMessage || '').trim()
|
||||
if (!sourceText) {
|
||||
return
|
||||
}
|
||||
|
||||
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
|
||||
messages.value.push(createMessage('user', '我要报销'))
|
||||
messages.value.push(createMessage('user', String(userEcho || '我要报销').trim() || '我要报销'))
|
||||
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), [], {
|
||||
meta: ['等待选择场景'],
|
||||
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
|
||||
@@ -240,6 +248,23 @@ export function useTravelReimbursementSuggestedActions({
|
||||
persistSessionState()
|
||||
}
|
||||
|
||||
async function pushExpenseAssociationGatePrompt(originalMessage, options = {}) {
|
||||
const sourceText = String(originalMessage || '我要报销').trim() || '我要报销'
|
||||
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
|
||||
messages.value.push(createMessage('user', String(options.userText || '我要报销').trim() || '我要报销'))
|
||||
await pushReimbursementAssociationPromptMessage({
|
||||
rawText: sourceText,
|
||||
createMessage,
|
||||
messages,
|
||||
nextTick,
|
||||
scrollToBottom,
|
||||
persistSessionState,
|
||||
fetchExpenseClaims,
|
||||
currentUser,
|
||||
skipDraftCheck: Boolean(options.skipDraftCheck)
|
||||
})
|
||||
}
|
||||
|
||||
function applySuggestedActionPrefill(action) {
|
||||
const prefillText = resolveSuggestedActionPrefill(action)
|
||||
if (!prefillText) {
|
||||
@@ -263,6 +288,35 @@ export function useTravelReimbursementSuggestedActions({
|
||||
if (await handleGuidedSuggestedAction(message, action)) return
|
||||
if (await handleSceneSelectionApplicationGate(message, action)) return
|
||||
|
||||
if (actionType === SKIP_REIMBURSEMENT_DRAFT_CHECK_ACTION) {
|
||||
const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销'
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
await pushExpenseAssociationGatePrompt(originalMessage, {
|
||||
skipDraftCheck: true,
|
||||
userText: action?.label || '不用草稿,关联申请单新建报销单'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === SKIP_REQUIRED_APPLICATION_LINK_ACTION) {
|
||||
const originalMessage = String(action?.payload?.original_message || message?.text || '我要报销').trim() || '我要报销'
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
pushExpenseSceneSelectionPrompt(originalMessage, action?.label || '不关联,单独新建报销单')
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === 'select_required_application') {
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const applicationNo = String(actionPayload.application_claim_no || actionPayload.claim_no || '').trim()
|
||||
const originalMessage = String(actionPayload.original_message || message?.text || '我要报销').trim() || '我要报销'
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
messages.value.push(createMessage('user', `关联申请单 ${applicationNo || ''}`.trim() || '关联申请单'))
|
||||
nextTick(scrollToBottom)
|
||||
persistSessionState()
|
||||
await submitComposer(buildReimbursementAssociationSubmitOptions(actionPayload, originalMessage))
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === APPLICATION_PREVIEW_FIELD_ACTION_SET) {
|
||||
await applyApplicationPreviewFieldAction(message, action)
|
||||
return
|
||||
@@ -340,7 +394,7 @@ export function useTravelReimbursementSuggestedActions({
|
||||
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
if (targetSessionType === SESSION_TYPE_EXPENSE && carryText === '我要报销') {
|
||||
pushExpenseSceneSelectionPrompt(carryText)
|
||||
await pushExpenseAssociationGatePrompt(carryText)
|
||||
return
|
||||
}
|
||||
if (String(actionPayload.steward_plan_id || '').trim()) {
|
||||
@@ -400,7 +454,7 @@ export function useTravelReimbursementSuggestedActions({
|
||||
const originalMessage = String(action?.payload?.original_message || message?.text || '').trim()
|
||||
if (!originalMessage) return
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
pushExpenseSceneSelectionPrompt(originalMessage)
|
||||
await pushExpenseAssociationGatePrompt(originalMessage)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user