2026-05-19 17:24:13 +00:00
|
|
|
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRouter } from 'vue-router'
|
|
|
|
|
|
|
|
|
|
|
|
import ConfirmDialog from '../../components/shared/ConfirmDialog.vue'
|
|
|
|
|
|
import { useSystemState } from '../../composables/useSystemState.js'
|
|
|
|
|
|
import { useToast } from '../../composables/useToast.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { useTravelReimbursementFlow } from './useTravelReimbursementFlow.js'
|
|
|
|
|
|
import { useTravelReimbursementComposerTools } from './useTravelReimbursementComposerTools.js'
|
|
|
|
|
|
import { useTravelReimbursementAttachments } from './useTravelReimbursementAttachments.js'
|
|
|
|
|
|
import { useTravelReimbursementSessionState } from './useTravelReimbursementSessionState.js'
|
|
|
|
|
|
import { useTravelReimbursementReviewDrawer } from './useTravelReimbursementReviewDrawer.js'
|
|
|
|
|
|
import { useTravelReimbursementSubmitComposer } from './useTravelReimbursementSubmitComposer.js'
|
|
|
|
|
|
import { useTravelReimbursementReviewActions } from './useTravelReimbursementReviewActions.js'
|
2026-05-23 19:54:42 +08:00
|
|
|
|
import { useTravelReimbursementGuidedFlow } from './useTravelReimbursementGuidedFlow.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { recognizeOcrFiles } from '../../services/ocr.js'
|
|
|
|
|
|
import { fetchAgentRunDetail } from '../../services/agentAssets.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { deleteConversation, runOrchestrator } from '../../services/orchestrator.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import { renderMarkdown } from '../../utils/markdown.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import { clearAssistantSessionSnapshot } from '../../utils/assistantSessionSnapshot.js'
|
2026-05-20 21:00:47 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildLocalExtractionProgressMessages,
|
|
|
|
|
|
buildLocalIntentPreview,
|
2026-05-21 16:09:47 +08:00
|
|
|
|
shouldRequestExpenseIntentConfirmation,
|
|
|
|
|
|
shouldRequestExpenseSceneSelection,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
summarizeSemanticIntentDetail
|
2026-05-20 21:00:47 +08:00
|
|
|
|
} from '../../utils/reimbursementTextInference.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildExpenseIntentConfirmationActions,
|
|
|
|
|
|
buildExpenseSceneSelectionActions
|
|
|
|
|
|
} from '../../utils/expenseAssistantActions.js'
|
|
|
|
|
|
import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
import {
|
2026-05-21 09:28:33 +08:00
|
|
|
|
calculateTravelReimbursement,
|
2026-05-21 16:09:47 +08:00
|
|
|
|
fetchExpenseClaims,
|
2026-05-19 17:24:13 +00:00
|
|
|
|
fetchExpenseClaimAttachmentAsset,
|
|
|
|
|
|
fetchExpenseClaimDetail,
|
|
|
|
|
|
fetchExpenseClaimItemAttachmentMeta,
|
|
|
|
|
|
uploadExpenseClaimItemAttachment
|
|
|
|
|
|
} from '../../services/reimbursements.js'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
EXPENSE_TYPE_LABELS,
|
|
|
|
|
|
REVIEW_SLOT_CONFIG,
|
|
|
|
|
|
REVIEW_CATEGORY_PRESET_OPTIONS,
|
|
|
|
|
|
REVIEW_OTHER_CATEGORY_OPTIONS,
|
|
|
|
|
|
REVIEW_SCENE_OTHER_OPTION,
|
|
|
|
|
|
REVIEW_SCENE_OPTIONS,
|
|
|
|
|
|
DATE_INPUT_FORMAT,
|
|
|
|
|
|
cloneReviewDocumentDrafts,
|
|
|
|
|
|
buildReviewDocumentDrafts,
|
|
|
|
|
|
normalizeReviewDocumentComparableValue,
|
|
|
|
|
|
buildReviewDocumentCorrectionMessage,
|
|
|
|
|
|
buildReviewDocumentCorrectionContext,
|
|
|
|
|
|
cloneReviewEditFields,
|
|
|
|
|
|
buildReviewFormValues,
|
|
|
|
|
|
createEmptyInlineReviewState,
|
|
|
|
|
|
resolveReviewRecognizedSlotCards,
|
|
|
|
|
|
resolveReviewMissingSlotCards,
|
|
|
|
|
|
resolveReviewExtraMissingLabels,
|
|
|
|
|
|
formatConfidenceLabel,
|
|
|
|
|
|
resolveDocumentTypeLabel,
|
|
|
|
|
|
resolveExpenseTypeLabel,
|
|
|
|
|
|
buildReviewRecognizedLines,
|
|
|
|
|
|
buildReviewSlotMap,
|
|
|
|
|
|
resolveExpenseTypeCode,
|
|
|
|
|
|
isValidIsoDateString,
|
|
|
|
|
|
parseAmountNumber,
|
|
|
|
|
|
normalizeAmountValue,
|
|
|
|
|
|
extractAmountInputValue,
|
|
|
|
|
|
formatAmountDisplay,
|
|
|
|
|
|
inferPresetSceneFromReview,
|
|
|
|
|
|
formatReviewSceneDisplayValue,
|
|
|
|
|
|
summarizeReviewScene,
|
|
|
|
|
|
buildInlineReviewState,
|
|
|
|
|
|
buildReviewAttachmentStatus,
|
|
|
|
|
|
shouldShowReviewFactCard,
|
|
|
|
|
|
resolveReviewCategoryConfidenceScore,
|
|
|
|
|
|
buildReviewCategoryOptions,
|
|
|
|
|
|
buildReviewPanelConfidence,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
|
|
|
|
|
buildInlineReviewChangedLines,
|
|
|
|
|
|
buildLocalReviewSavedMessage,
|
|
|
|
|
|
buildReviewSubmitUserText,
|
|
|
|
|
|
mergeInlineReviewFields,
|
|
|
|
|
|
buildBusinessTimeContextFromReviewValues as buildBusinessTimeContextFromReviewValuesModel,
|
|
|
|
|
|
buildReviewFormContextFromPayload as buildReviewFormContextFromPayloadModel,
|
|
|
|
|
|
isTravelReviewPayload as isTravelReviewPayloadModel,
|
|
|
|
|
|
resolveReviewTravelTransportType as resolveReviewTravelTransportTypeModel,
|
|
|
|
|
|
buildClientTimeContext,
|
|
|
|
|
|
formatDraftApplyTime,
|
|
|
|
|
|
formatDateInputValue,
|
|
|
|
|
|
buildDraftSavedPayload,
|
|
|
|
|
|
buildReviewHeadline,
|
|
|
|
|
|
buildReviewSubline,
|
|
|
|
|
|
buildReviewStateLabel,
|
|
|
|
|
|
buildReviewStateTone,
|
|
|
|
|
|
buildReviewPlainFollowupCopy,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
buildReviewNextStepRichCopy,
|
|
|
|
|
|
buildReviewRiskLevelCounts,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveReviewFooterActions,
|
|
|
|
|
|
resolveReviewSaveDraftAction,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
resolveReviewNextStepAction,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildReviewPrimaryButtonLabel,
|
|
|
|
|
|
buildReviewIntentText,
|
|
|
|
|
|
buildReviewSceneValue,
|
|
|
|
|
|
buildMissingRiskLine,
|
|
|
|
|
|
buildReviewRiskSummary as buildReviewRiskSummaryModel,
|
|
|
|
|
|
normalizeReviewRiskLevel,
|
|
|
|
|
|
buildLocalReviewCompletionMessage,
|
|
|
|
|
|
buildReviewRecognitionNotes,
|
|
|
|
|
|
buildReviewDocumentSummaries
|
|
|
|
|
|
} from './travelReimbursementReviewModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
buildDraftAssociationQueryPayload,
|
|
|
|
|
|
buildExpenseQueryHint,
|
|
|
|
|
|
buildExpenseQueryWindowLabel,
|
|
|
|
|
|
getExpenseQueryActivePage,
|
|
|
|
|
|
getExpenseQueryTotalPages,
|
|
|
|
|
|
getExpenseQueryVisibleRecords,
|
|
|
|
|
|
normalizeExpenseQueryPayload
|
|
|
|
|
|
} from './travelReimbursementExpenseQueryModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
MAX_OCR_DOCUMENTS,
|
|
|
|
|
|
VISIBLE_ATTACHMENT_CHIPS,
|
|
|
|
|
|
buildAgentInsight,
|
|
|
|
|
|
buildErrorInsight,
|
2026-05-22 08:58:59 +08:00
|
|
|
|
ATTACHMENT_ASSOCIATION_CONFIRM_HREF,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
buildFileIdentity,
|
|
|
|
|
|
buildFilePreviews,
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload,
|
|
|
|
|
|
buildOcrFilePreviews,
|
|
|
|
|
|
buildOcrSummary,
|
|
|
|
|
|
buildOcrSummaryFromDocuments,
|
|
|
|
|
|
buildReviewFilePreviewsFromReviewPayload,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
isTemporaryPreviewUrl,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
mergeFilePreviews,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeUploadAttachmentNames,
|
|
|
|
|
|
mergeUploadOcrDocuments,
|
|
|
|
|
|
normalizeOcrDocuments,
|
|
|
|
|
|
resolveAttachmentPreviewKind,
|
|
|
|
|
|
resolveDocumentPreview
|
|
|
|
|
|
} from './travelReimbursementAttachmentModel.js'
|
|
|
|
|
|
import {
|
|
|
|
|
|
ASSISTANT_DISPLAY_NAME,
|
|
|
|
|
|
FLOW_STEP_FALLBACKS,
|
|
|
|
|
|
HOT_KNOWLEDGE_QUESTIONS,
|
|
|
|
|
|
INTENT_LABELS,
|
|
|
|
|
|
SCENARIO_LABELS,
|
|
|
|
|
|
SESSION_TYPE_EXPENSE,
|
|
|
|
|
|
SESSION_TYPE_KNOWLEDGE,
|
|
|
|
|
|
aiAvatar,
|
|
|
|
|
|
buildExpenseIntentConfirmationMessage,
|
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
|
buildMessageMeta,
|
|
|
|
|
|
buildWelcomeInsight,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
resolveKnowledgeRankLabel,
|
|
|
|
|
|
resolveKnowledgeRankTone,
|
|
|
|
|
|
sanitizeRequest,
|
|
|
|
|
|
summarizeSemanticParseDetail,
|
|
|
|
|
|
userAvatar
|
|
|
|
|
|
} from './travelReimbursementConversationModel.js'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-20 14:21:56 +08:00
|
|
|
|
const REVIEW_RISK_LEVEL_META = {
|
|
|
|
|
|
high: {
|
|
|
|
|
|
label: '高风险',
|
|
|
|
|
|
icon: 'mdi mdi-alert-octagon-outline',
|
|
|
|
|
|
suggestion: '建议先处理该项,再提交审批;如果属于合理业务场景,请补充说明或附件。'
|
|
|
|
|
|
},
|
2026-05-21 09:28:33 +08:00
|
|
|
|
medium: {
|
|
|
|
|
|
label: '中风险',
|
2026-05-20 14:21:56 +08:00
|
|
|
|
icon: 'mdi mdi-alert-circle-outline',
|
|
|
|
|
|
suggestion: '提交前建议核对业务真实性、附件完整性和规则口径。'
|
|
|
|
|
|
},
|
2026-05-22 23:47:28 +08:00
|
|
|
|
info: {
|
|
|
|
|
|
label: '提示',
|
|
|
|
|
|
icon: 'mdi mdi-information-outline',
|
|
|
|
|
|
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
|
|
|
|
|
},
|
2026-05-21 09:28:33 +08:00
|
|
|
|
low: {
|
|
|
|
|
|
label: '低风险',
|
2026-05-20 14:21:56 +08:00
|
|
|
|
icon: 'mdi mdi-information-outline',
|
2026-05-21 09:28:33 +08:00
|
|
|
|
suggestion: '该项主要用于辅助判断,可结合当前单据情况简单核对。'
|
2026-05-20 14:21:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
const COMPOSER_TEXTAREA_HEIGHT = 36
|
|
|
|
|
|
const COMPOSER_MAX_ROWS = 5
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_REVIEW = 'review'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_DOCUMENTS = 'documents'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_RISK = 'risk'
|
|
|
|
|
|
const REVIEW_DRAWER_MODE_FLOW = 'flow'
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const REVIEW_PANEL_SCOPE_OVERVIEW = 'overview'
|
|
|
|
|
|
const REVIEW_PANEL_SCOPE_DOCUMENTS = 'documents'
|
|
|
|
|
|
const REVIEW_PANEL_SCOPE_RISK = 'risk'
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const REVIEW_NEXT_STEP_HREF = '#review-next-step'
|
|
|
|
|
|
const REVIEW_RISK_PANEL_HREF_PREFIX = '#review-risk'
|
|
|
|
|
|
const REVIEW_QUICK_EDIT_HREF = '#review-quick-edit'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
const FLOW_STEP_STATUS_PENDING = 'pending'
|
|
|
|
|
|
const FLOW_STEP_STATUS_RUNNING = 'running'
|
|
|
|
|
|
const FLOW_STEP_STATUS_COMPLETED = 'completed'
|
|
|
|
|
|
const FLOW_STEP_STATUS_FAILED = 'failed'
|
2026-05-21 09:28:33 +08:00
|
|
|
|
const DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS = ['历史报销画像', '用户画像', '制度注意事项', '制度注意']
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function normalizeReviewPanelScope(scope) {
|
|
|
|
|
|
const normalized = String(scope || '').trim()
|
|
|
|
|
|
return [REVIEW_PANEL_SCOPE_OVERVIEW, REVIEW_PANEL_SCOPE_DOCUMENTS, REVIEW_PANEL_SCOPE_RISK].includes(normalized)
|
|
|
|
|
|
? normalized
|
|
|
|
|
|
: ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function canExposeReviewPanelScope(scope) {
|
|
|
|
|
|
return Boolean(normalizeReviewPanelScope(scope))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function buildBusinessTimeContextFromReviewValues(values = {}) {
|
|
|
|
|
|
return buildBusinessTimeContextFromReviewValuesModel(values)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function buildReviewFormContextFromPayload(reviewPayload, inlineState = null) {
|
|
|
|
|
|
return buildReviewFormContextFromPayloadModel(reviewPayload, inlineState)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function buildReviewCorrectionMessage(fields) {
|
|
|
|
|
|
const lines = ['请按以下核对后的报销信息更新当前识别结果:']
|
|
|
|
|
|
for (const item of cloneReviewEditFields(fields)) {
|
|
|
|
|
|
if (!item.label || (!item.value && !item.required)) {
|
|
|
|
|
|
continue
|
2026-05-21 16:09:47 +08:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
lines.push(`${item.label}:${String(item.value || '').trim() || '待补充'}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines.join('\n')
|
2026-05-21 16:09:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function isTravelReviewPayload(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
|
|
|
|
|
return isTravelReviewPayloadModel(reviewPayload, inlineState)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function resolveReviewTravelTransportType(reviewPayload, fallbackText = '') {
|
|
|
|
|
|
return resolveReviewTravelTransportTypeModel(reviewPayload, fallbackText)
|
2026-05-21 16:09:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function resolveReviewRiskBriefs(reviewPayload) {
|
|
|
|
|
|
if (!Array.isArray(reviewPayload?.risk_briefs)) return []
|
|
|
|
|
|
return reviewPayload.risk_briefs.filter((item) => {
|
|
|
|
|
|
const title = String(item?.title || '').trim()
|
|
|
|
|
|
return !DEPRECATED_REVIEW_RISK_TITLE_KEYWORDS.some((keyword) => title.includes(keyword))
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function buildReviewFactCards(reviewPayload, inlineState = createEmptyInlineReviewState()) {
|
|
|
|
|
|
const pendingAttachmentCount = Math.max(0, Number(inlineState.pending_attachment_count || 0))
|
|
|
|
|
|
const totalAttachmentCount = Math.max(0, Number(inlineState.attachment_count || 0))
|
|
|
|
|
|
const existingAttachmentCount = Math.max(0, totalAttachmentCount - pendingAttachmentCount)
|
|
|
|
|
|
const attachmentStatus =
|
|
|
|
|
|
pendingAttachmentCount > 0
|
|
|
|
|
|
? existingAttachmentCount > 0
|
|
|
|
|
|
? `已上传 ${existingAttachmentCount} 份,待新增 ${pendingAttachmentCount} 份`
|
|
|
|
|
|
: `待保存 ${pendingAttachmentCount} 份`
|
|
|
|
|
|
: totalAttachmentCount > 0
|
|
|
|
|
|
? `已上传 ${totalAttachmentCount} 份`
|
|
|
|
|
|
: buildReviewAttachmentStatus(reviewPayload)
|
|
|
|
|
|
if (isTravelReviewPayload(reviewPayload, inlineState)) {
|
|
|
|
|
|
return [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'occurred_date',
|
|
|
|
|
|
label: '发生时间',
|
|
|
|
|
|
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-calendar-month-outline',
|
|
|
|
|
|
editor: 'date',
|
|
|
|
|
|
modelKey: 'occurred_date',
|
|
|
|
|
|
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
label: '金额',
|
|
|
|
|
|
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-cash',
|
|
|
|
|
|
editor: 'amount',
|
|
|
|
|
|
modelKey: 'amount',
|
|
|
|
|
|
placeholder: '例如 200.00'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'transport_type',
|
|
|
|
|
|
label: '交通类型',
|
|
|
|
|
|
value: String(inlineState.transport_type || '').trim() || '待确认',
|
|
|
|
|
|
icon: 'mdi mdi-train-car',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'transport_type',
|
|
|
|
|
|
placeholder: '例如 火车/高铁、飞机'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'hotel_name',
|
|
|
|
|
|
label: '酒店名称',
|
|
|
|
|
|
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-bed-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'merchant_name',
|
|
|
|
|
|
placeholder: '请输入酒店名称'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'travel_purpose',
|
|
|
|
|
|
label: '出差事宜',
|
|
|
|
|
|
value: String(inlineState.reason_value || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-briefcase-edit-outline',
|
|
|
|
|
|
editor: 'textarea',
|
|
|
|
|
|
modelKey: 'reason_value',
|
|
|
|
|
|
placeholder: '请填写本次出差的具体工作内容或业务意图',
|
|
|
|
|
|
wide: true
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const cards = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'occurred_date',
|
|
|
|
|
|
label: '发生时间',
|
|
|
|
|
|
value: String(inlineState.occurred_date || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-calendar-month-outline',
|
|
|
|
|
|
editor: 'date',
|
|
|
|
|
|
modelKey: 'occurred_date',
|
|
|
|
|
|
placeholder: `例如 ${DATE_INPUT_FORMAT}`
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'amount',
|
|
|
|
|
|
label: '金额',
|
|
|
|
|
|
value: formatAmountDisplay(inlineState.amount) || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-cash',
|
|
|
|
|
|
editor: 'amount',
|
|
|
|
|
|
modelKey: 'amount',
|
|
|
|
|
|
placeholder: '例如 200.00'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'scene',
|
|
|
|
|
|
label: '场景 / 事由',
|
|
|
|
|
|
value: formatReviewSceneDisplayValue(inlineState),
|
|
|
|
|
|
icon: 'mdi mdi-silverware-fork-knife',
|
|
|
|
|
|
editor: 'select',
|
|
|
|
|
|
modelKey: 'scene_label',
|
|
|
|
|
|
placeholder: '请选择场景'
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'attachments',
|
|
|
|
|
|
label: '票据状态',
|
|
|
|
|
|
value: attachmentStatus,
|
|
|
|
|
|
icon: 'mdi mdi-file-document-outline',
|
|
|
|
|
|
editor: 'upload',
|
|
|
|
|
|
modelKey: 'attachment_names',
|
|
|
|
|
|
placeholder: ''
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
]
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'customer_name', inlineState.customer_name)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'customer_name',
|
|
|
|
|
|
label: '关联客户',
|
|
|
|
|
|
value: String(inlineState.customer_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-domain',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'customer_name',
|
|
|
|
|
|
placeholder: '请输入客户名称'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'location', inlineState.location)) {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
key: 'location',
|
|
|
|
|
|
label: '业务地点',
|
|
|
|
|
|
value: String(inlineState.location || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-map-marker-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'location',
|
|
|
|
|
|
placeholder: '请输入业务地点'
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'merchant_name', inlineState.merchant_name)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'merchant_name',
|
|
|
|
|
|
label: '酒店/商户',
|
|
|
|
|
|
value: String(inlineState.merchant_name || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-storefront-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'merchant_name',
|
|
|
|
|
|
placeholder: '请输入酒店或商户名称'
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (shouldShowReviewFactCard(reviewPayload, 'participants', inlineState.participants)) {
|
|
|
|
|
|
cards.splice(cards.length - 1, 0, {
|
|
|
|
|
|
key: 'participants',
|
|
|
|
|
|
label: '同行人员',
|
|
|
|
|
|
value: String(inlineState.participants || '').trim() || '待补充',
|
|
|
|
|
|
icon: 'mdi mdi-account-group-outline',
|
|
|
|
|
|
editor: 'text',
|
|
|
|
|
|
modelKey: 'participants',
|
|
|
|
|
|
placeholder: '例如 客户 2 人,我方 1 人'
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return cards
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function normalizeReviewRiskTitle(title, fallbackTitle) {
|
|
|
|
|
|
const normalized = String(title || '').trim()
|
|
|
|
|
|
const fallback = String(fallbackTitle || '风险提示').trim() || '风险提示'
|
|
|
|
|
|
if (!normalized) return fallback
|
|
|
|
|
|
const cleaned = normalized
|
|
|
|
|
|
.replace(/AI\s*预审\s*(暂未通过|未通过|不通过)?/g, '风险提示')
|
|
|
|
|
|
.replace(/(高风险|中风险|低风险)/g, '')
|
|
|
|
|
|
.replace(/^[::\-—\s]+|[::\-—\s]+$/g, '')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
return cleaned || fallback
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function buildReviewRiskItems(reviewPayload) {
|
|
|
|
|
|
return resolveReviewRiskBriefs(reviewPayload)
|
|
|
|
|
|
.map((brief, index) => {
|
|
|
|
|
|
const title = String(brief?.title || '').trim()
|
|
|
|
|
|
const content = String(brief?.content || '').trim()
|
|
|
|
|
|
const detail = String(brief?.detail || '').trim()
|
|
|
|
|
|
const suggestion = String(brief?.suggestion || '').trim()
|
|
|
|
|
|
const level = normalizeReviewRiskLevel(brief?.level)
|
|
|
|
|
|
const meta = REVIEW_RISK_LEVEL_META[level] || REVIEW_RISK_LEVEL_META.low
|
|
|
|
|
|
const fallbackTitle = content ? `风险提示 ${index + 1}` : '风险提示'
|
|
|
|
|
|
const normalizedTitle = normalizeReviewRiskTitle(title, fallbackTitle)
|
|
|
|
|
|
const summary = content || normalizedTitle
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (!normalizedTitle && !summary) return null
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return {
|
|
|
|
|
|
key: `${level}-${normalizedTitle}-${index}`,
|
|
|
|
|
|
title: normalizedTitle,
|
|
|
|
|
|
summary,
|
|
|
|
|
|
detail: detail || content || '当前风险项没有返回更长解释,建议结合票据、报销事由和规则要求进行复核。',
|
|
|
|
|
|
level,
|
|
|
|
|
|
levelLabel: meta.label,
|
|
|
|
|
|
icon: meta.icon,
|
|
|
|
|
|
sourceLabel: meta.label,
|
|
|
|
|
|
suggestion: suggestion || meta.suggestion
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function buildReviewRiskConversationText(item, detailTarget = {}) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const title = String(item?.title || '风险提示').trim()
|
|
|
|
|
|
const summary = String(item?.summary || '').trim()
|
|
|
|
|
|
const detail = String(item?.detail || '').trim()
|
|
|
|
|
|
const suggestion = String(item?.suggestion || '').trim()
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const isInfo = String(item?.level || '').trim() === 'info'
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const detailHref = String(detailTarget?.href || '').trim()
|
|
|
|
|
|
const detailLabel = String(detailTarget?.label || '').trim() || '进入该单据详情重新填写'
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const lines = [`${title}`]
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (summary) {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
lines.push('', `${isInfo ? '提示内容' : '风险点'}:${summary}`)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (detail && detail !== summary) {
|
|
|
|
|
|
lines.push('', `规则依据:${detail}`)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (suggestion) {
|
2026-05-22 23:47:28 +08:00
|
|
|
|
lines.push('', `${isInfo ? '处理建议' : '修改建议'}:${suggestion}`)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (detailHref) {
|
|
|
|
|
|
lines.push('', `[${detailLabel}](${detailHref})`)
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return lines.join('\n')
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const REVIEW_PENDING_SUMMARY_PATTERN = /(^|\n)\s*(?:当前还有|我这边看到还有|下方还有|这笔报销还有|目前还有|还有|这次识别结果里还有|我还需要你确认|当前信息还差|本次报销还有)\s+[^\n]*(?:信息待补充|风险提醒|细节还需要进一步确认)[^\n]*(?:草稿)[^\n]*。\s*/g
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewMainMessageText(message) {
|
|
|
|
|
|
const text = String(message?.text || '')
|
|
|
|
|
|
if (!message?.reviewPayload) {
|
|
|
|
|
|
return text
|
|
|
|
|
|
}
|
|
|
|
|
|
return text
|
|
|
|
|
|
.replace(REVIEW_PENDING_SUMMARY_PATTERN, '\n')
|
|
|
|
|
|
.replace(/\n{3,}/g, '\n\n')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
export default {
|
|
|
|
|
|
name: 'TravelReimbursementCreateView',
|
|
|
|
|
|
components: {
|
|
|
|
|
|
ConfirmDialog
|
|
|
|
|
|
},
|
|
|
|
|
|
props: {
|
|
|
|
|
|
initialPrompt: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
initialFiles: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: () => []
|
|
|
|
|
|
},
|
|
|
|
|
|
initialConversation: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
|
|
|
|
|
entrySource: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: 'requests'
|
|
|
|
|
|
},
|
|
|
|
|
|
requestContext: {
|
|
|
|
|
|
type: Object,
|
|
|
|
|
|
default: null
|
2026-05-22 16:00:19 +08:00
|
|
|
|
},
|
|
|
|
|
|
invalidatedDraftClaimId: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
},
|
|
|
|
|
|
emits: ['close', 'draft-saved'],
|
|
|
|
|
|
setup(props, { emit }) {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
|
const { toast } = useToast()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const fileInputRef = ref(null)
|
|
|
|
|
|
const composerTextareaRef = ref(null)
|
|
|
|
|
|
const messageListRef = ref(null)
|
|
|
|
|
|
const composerDraft = ref('')
|
|
|
|
|
|
const submitting = ref(false)
|
|
|
|
|
|
const workbenchVisible = ref(false)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const closeAfterBusy = ref(false)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const linkedRequest = computed(() => sanitizeRequest(props.requestContext))
|
|
|
|
|
|
const hotKnowledgeQuestions = HOT_KNOWLEDGE_QUESTIONS
|
|
|
|
|
|
let sessionRuntimeRefs = {}
|
|
|
|
|
|
const {
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
draftClaimId,
|
|
|
|
|
|
sessionSnapshots,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
reviewFilePreviews,
|
|
|
|
|
|
composerUploadIntent,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
guidedFlowState,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
insightPanelCollapsed,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
buildEmptySessionState,
|
|
|
|
|
|
resolveCurrentUserId,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
applySessionState,
|
|
|
|
|
|
clearKnowledgeSessionOnEntry,
|
|
|
|
|
|
switchSessionType
|
|
|
|
|
|
} = useTravelReimbursementSessionState({
|
|
|
|
|
|
props,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
getSessionRuntimeRefs: () => sessionRuntimeRefs
|
|
|
|
|
|
})
|
|
|
|
|
|
const deleteSessionDialogOpen = ref(false)
|
2026-05-22 23:47:28 +08:00
|
|
|
|
const nextStepConfirmDialog = ref({
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const reviewActionBusy = ref(false)
|
|
|
|
|
|
const deleteSessionBusy = ref(false)
|
|
|
|
|
|
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
|
|
|
|
|
|
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
|
|
|
|
|
|
const {
|
|
|
|
|
|
flowRunId,
|
|
|
|
|
|
flowSteps,
|
|
|
|
|
|
flowRefreshBusy,
|
|
|
|
|
|
completedFlowStepCount,
|
|
|
|
|
|
flowOverallStatusTone,
|
|
|
|
|
|
flowOverallStatusText,
|
|
|
|
|
|
flowTotalDurationText,
|
|
|
|
|
|
clearFlowSimulationTimers,
|
|
|
|
|
|
resetFlowRun,
|
|
|
|
|
|
startFlowTick,
|
|
|
|
|
|
stopFlowRuntime,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
failCurrentFlowStep,
|
|
|
|
|
|
startSemanticFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview,
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionAfterIntentConfirmation,
|
|
|
|
|
|
startReviewActionFlowStep,
|
|
|
|
|
|
startExpenseClaimDraftFlowStep,
|
|
|
|
|
|
completeFlowResult,
|
|
|
|
|
|
refreshFlowRunDetail,
|
|
|
|
|
|
formatFlowStepDuration,
|
|
|
|
|
|
resolveFlowStepStatusLabel,
|
|
|
|
|
|
resolveFlowStepDetail
|
|
|
|
|
|
} = useTravelReimbursementFlow({
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
reviewDrawerMode,
|
|
|
|
|
|
insightPanelCollapsed,
|
|
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
fetchAgentRunDetail,
|
|
|
|
|
|
buildLocalIntentPreview,
|
|
|
|
|
|
buildLocalExtractionProgressMessages,
|
|
|
|
|
|
summarizeSemanticIntentDetail,
|
|
|
|
|
|
summarizeSemanticParseDetail,
|
|
|
|
|
|
SCENARIO_LABELS,
|
|
|
|
|
|
INTENT_LABELS,
|
|
|
|
|
|
EXPENSE_TYPE_LABELS,
|
|
|
|
|
|
FLOW_STEP_FALLBACKS,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_FLOW,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_REVIEW,
|
|
|
|
|
|
FLOW_STEP_STATUS_PENDING,
|
|
|
|
|
|
FLOW_STEP_STATUS_RUNNING,
|
|
|
|
|
|
FLOW_STEP_STATUS_COMPLETED,
|
|
|
|
|
|
FLOW_STEP_STATUS_FAILED
|
|
|
|
|
|
})
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const hasScopedReviewPayload = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
if (agent?.reviewPayload && canExposeReviewPanelScope(agent.reviewPanelScope)) {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return messages.value.some((item) =>
|
|
|
|
|
|
item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope)
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
const hasQueryInsight = computed(() => Boolean(currentInsight.value.agent?.queryPayload))
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const hasInsightPanelContent = computed(
|
2026-05-22 16:00:19 +08:00
|
|
|
|
() => isKnowledgeSession.value || hasScopedReviewPayload.value || hasQueryInsight.value || flowSteps.value.length > 0
|
2026-05-19 17:24:13 +00:00
|
|
|
|
)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const showInsightPanel = computed(() => hasInsightPanelContent.value && !insightPanelCollapsed.value)
|
|
|
|
|
|
const insightPanelToggleLabel = computed(() =>
|
|
|
|
|
|
showInsightPanel.value ? '隐藏详细信息' : '展开详细信息'
|
|
|
|
|
|
)
|
|
|
|
|
|
const composerPlaceholder = computed(() => {
|
|
|
|
|
|
if (isKnowledgeSession.value) {
|
|
|
|
|
|
return '例如:差旅住宿标准是什么?发票抬头不一致还能报销吗?'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
|
|
|
|
|
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件整理报销核对信息。'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const currentIntentLabel = computed(() => {
|
|
|
|
|
|
if (isKnowledgeSession.value && currentInsight.value.intent === 'welcome') {
|
|
|
|
|
|
return '热门问题'
|
|
|
|
|
|
}
|
|
|
|
|
|
const labels = isKnowledgeSession.value
|
|
|
|
|
|
? {
|
|
|
|
|
|
welcome: '热门问题',
|
|
|
|
|
|
agent: '知识回答'
|
|
|
|
|
|
}
|
|
|
|
|
|
: {
|
|
|
|
|
|
welcome: '财务助手',
|
|
|
|
|
|
agent: '处理中'
|
|
|
|
|
|
}
|
|
|
|
|
|
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
|
|
|
|
|
})
|
|
|
|
|
|
let knowledgeSessionResetPromise = Promise.resolve()
|
|
|
|
|
|
const canDeleteCurrentSession = computed(
|
|
|
|
|
|
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
|
2026-05-21 16:09:47 +08:00
|
|
|
|
)
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const latestReviewMessage = computed(() =>
|
2026-05-22 16:00:19 +08:00
|
|
|
|
[...messages.value].reverse().find((item) =>
|
|
|
|
|
|
item.role === 'assistant' && item.reviewPayload && canExposeReviewPanelScope(item.reviewPanelScope)
|
|
|
|
|
|
) ?? null
|
2026-05-21 09:28:33 +08:00
|
|
|
|
)
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const activeReviewPanelScope = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
const agentScope = normalizeReviewPanelScope(agent?.reviewPanelScope)
|
|
|
|
|
|
if (agent?.reviewPayload && agentScope) {
|
|
|
|
|
|
return agentScope
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
return normalizeReviewPanelScope(latestReviewMessage.value?.reviewPanelScope)
|
|
|
|
|
|
})
|
|
|
|
|
|
const activeReviewPayload = computed(() => {
|
|
|
|
|
|
const agent = currentInsight.value.agent || null
|
|
|
|
|
|
if (agent?.reviewPayload && normalizeReviewPanelScope(agent.reviewPanelScope)) {
|
|
|
|
|
|
return agent.reviewPayload
|
|
|
|
|
|
}
|
|
|
|
|
|
if (currentInsight.value.intent === 'agent' && agent) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
return latestReviewMessage.value?.reviewPayload || null
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const reviewRiskBriefResolver = (payload) => resolveReviewRiskBriefs(payload)
|
|
|
|
|
|
const buildReviewRiskSummary = (payload) => buildReviewRiskSummaryModel(payload, reviewRiskBriefResolver)
|
|
|
|
|
|
const {
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlineBaseForm,
|
|
|
|
|
|
reviewInlineBaseFields,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
reviewInlineErrors,
|
|
|
|
|
|
reviewOtherCategoryOpen,
|
|
|
|
|
|
reviewDocumentDrafts,
|
|
|
|
|
|
reviewDocumentBaseDrafts,
|
|
|
|
|
|
activeReviewDocumentIndex,
|
|
|
|
|
|
documentPreviewDialog,
|
|
|
|
|
|
activeReviewFilePreviews,
|
|
|
|
|
|
reviewIntentText,
|
|
|
|
|
|
reviewFactCards,
|
|
|
|
|
|
reviewCategoryOptions,
|
|
|
|
|
|
reviewOtherCategoryOptions,
|
|
|
|
|
|
reviewSelectedOtherCategory,
|
|
|
|
|
|
reviewInlineDirty,
|
|
|
|
|
|
reviewPanelConfidence,
|
|
|
|
|
|
reviewRiskSummary,
|
|
|
|
|
|
reviewRiskItems,
|
|
|
|
|
|
reviewRiskEmpty,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
reviewOverviewDrawerAvailable,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewDocumentDrawerAvailable,
|
|
|
|
|
|
reviewRiskDrawerAvailable,
|
|
|
|
|
|
reviewFlowDrawerAvailable,
|
|
|
|
|
|
recognizedNarratives,
|
|
|
|
|
|
reviewRecognitionNotes,
|
|
|
|
|
|
reviewDocumentSummaries,
|
|
|
|
|
|
reviewDocumentCount,
|
|
|
|
|
|
isReviewDocumentDrawer,
|
|
|
|
|
|
isReviewRiskDrawer,
|
|
|
|
|
|
isReviewFlowDrawer,
|
|
|
|
|
|
reviewDrawerTitle,
|
|
|
|
|
|
reviewDocumentDrawerLabel,
|
|
|
|
|
|
reviewDocumentDrawerIcon,
|
|
|
|
|
|
reviewRiskDrawerLabel,
|
|
|
|
|
|
reviewRiskDrawerIcon,
|
|
|
|
|
|
reviewFlowDrawerLabel,
|
|
|
|
|
|
reviewFlowDrawerIcon,
|
|
|
|
|
|
activeReviewDocument,
|
|
|
|
|
|
activeReviewDocumentPreview,
|
|
|
|
|
|
canPreviewActiveReviewDocument,
|
|
|
|
|
|
reviewDocumentDirty,
|
|
|
|
|
|
reviewHasUnsavedChanges,
|
|
|
|
|
|
setInlineReviewFieldError,
|
|
|
|
|
|
clearInlineReviewFieldError,
|
|
|
|
|
|
resetReviewDrawerFromPayload,
|
|
|
|
|
|
enforceReviewDrawerAvailability,
|
|
|
|
|
|
openInlineReviewEditor,
|
|
|
|
|
|
closeInlineReviewEditor,
|
|
|
|
|
|
commitInlineReviewEditor,
|
|
|
|
|
|
selectInlineScene,
|
|
|
|
|
|
selectReviewCategory,
|
|
|
|
|
|
selectReviewOtherCategory,
|
|
|
|
|
|
goReviewDocument,
|
|
|
|
|
|
openActiveReviewDocumentPreview,
|
|
|
|
|
|
closeDocumentPreview
|
|
|
|
|
|
} = useTravelReimbursementReviewDrawer({
|
|
|
|
|
|
activeReviewPayload,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
activeReviewPanelScope,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewFilePreviews,
|
|
|
|
|
|
flowSteps,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
triggerFileUpload: (...args) => triggerFileUpload(...args),
|
|
|
|
|
|
resolveDocumentPreview,
|
|
|
|
|
|
buildReviewFactCards,
|
|
|
|
|
|
buildReviewRiskItems,
|
|
|
|
|
|
buildReviewRiskSummary,
|
|
|
|
|
|
buildReviewIntentText,
|
|
|
|
|
|
resolveReviewRiskBriefs,
|
|
|
|
|
|
reviewDrawerMode,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_REVIEW,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_DOCUMENTS,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_RISK,
|
|
|
|
|
|
REVIEW_DRAWER_MODE_FLOW
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
composerDatePickerOpen,
|
|
|
|
|
|
composerDateMode,
|
|
|
|
|
|
composerSingleDate,
|
|
|
|
|
|
composerRangeStartDate,
|
|
|
|
|
|
composerRangeEndDate,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerCanApplyDateSelection,
|
|
|
|
|
|
travelCalculatorOpen,
|
|
|
|
|
|
travelCalculatorBusy,
|
|
|
|
|
|
travelCalculatorError,
|
|
|
|
|
|
travelCalculatorResult,
|
|
|
|
|
|
travelCalculatorForm,
|
|
|
|
|
|
travelCalculatorCanSubmit,
|
|
|
|
|
|
buildComposerBusinessTimeLabel,
|
|
|
|
|
|
hasComposerBusinessTimeSelection,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
mergeBusinessTimeIntoExtraContext,
|
|
|
|
|
|
syncComposerBusinessTimeToReviewCard,
|
|
|
|
|
|
resolveComposerSubmitText,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
toggleComposerDatePicker,
|
|
|
|
|
|
closeComposerDatePicker,
|
|
|
|
|
|
setComposerDateMode,
|
|
|
|
|
|
handleComposerDateInputChange,
|
|
|
|
|
|
removeComposerBusinessTimeTag,
|
|
|
|
|
|
handleComposerDatePickerOutside,
|
|
|
|
|
|
applyComposerDateSelection,
|
|
|
|
|
|
resolveTravelCalculatorInitialDays,
|
|
|
|
|
|
resolveTravelCalculatorInitialLocation,
|
|
|
|
|
|
openTravelCalculator,
|
|
|
|
|
|
toggleTravelCalculator: toggleTravelCalculatorInternal,
|
|
|
|
|
|
closeTravelCalculator,
|
|
|
|
|
|
formatTravelCalculatorMoney,
|
|
|
|
|
|
buildTravelCalculatorResultText,
|
|
|
|
|
|
submitTravelCalculator: submitTravelCalculatorInternal
|
|
|
|
|
|
} = useTravelReimbursementComposerTools({
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
latestReviewMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
composerTextareaRef,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
calculateTravelReimbursement,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
buildReviewSlotMap,
|
|
|
|
|
|
isValidIsoDateString,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
|
|
|
|
|
formatDateInputValue
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
fileInputMode,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
composerFilesExpanded,
|
|
|
|
|
|
visibleAttachedFiles,
|
|
|
|
|
|
hiddenAttachedFileCount,
|
|
|
|
|
|
rememberFilePreviews,
|
|
|
|
|
|
buildComposerFilePreviews,
|
|
|
|
|
|
resolveActiveClaimId,
|
|
|
|
|
|
restorePersistedDraftAttachmentPreviews,
|
|
|
|
|
|
syncComposerFilesToDraft,
|
|
|
|
|
|
triggerFileUpload,
|
|
|
|
|
|
handleFilesChange,
|
|
|
|
|
|
toggleAttachedFilesExpanded,
|
|
|
|
|
|
removeAttachedFile,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
stopAttachmentRuntime
|
|
|
|
|
|
} = useTravelReimbursementAttachments({
|
|
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
reviewFilePreviews,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
draftClaimId,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
composerUploadIntent,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
toast,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
fetchExpenseClaimDetail,
|
|
|
|
|
|
fetchExpenseClaimItemAttachmentMeta,
|
|
|
|
|
|
fetchExpenseClaimAttachmentAsset,
|
|
|
|
|
|
uploadExpenseClaimItemAttachment,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeFilePreviews,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
isTemporaryPreviewUrl,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resolveAttachmentPreviewKind,
|
|
|
|
|
|
resolveDocumentPreview,
|
|
|
|
|
|
buildFilePreviews,
|
|
|
|
|
|
buildFileIdentity,
|
|
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
VISIBLE_ATTACHMENT_CHIPS,
|
|
|
|
|
|
clearInlineReviewFieldError
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
sessionRuntimeRefs = {
|
|
|
|
|
|
attachedFiles,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
composerFilesExpanded,
|
|
|
|
|
|
guidedFlowState
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
const {
|
|
|
|
|
|
confirmPendingAttachmentAssociationInternal,
|
|
|
|
|
|
submitComposerInternal
|
|
|
|
|
|
} = useTravelReimbursementSubmitComposer({
|
2026-05-21 23:53:03 +08:00
|
|
|
|
MAX_ATTACHMENTS,
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
activeSessionType,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
buildAgentInsight,
|
|
|
|
|
|
buildClientTimeContext,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
buildComposerFilePreviews,
|
|
|
|
|
|
buildDraftAssociationQueryPayload,
|
|
|
|
|
|
buildErrorInsight,
|
|
|
|
|
|
buildExpenseIntentConfirmationActions,
|
|
|
|
|
|
buildExpenseIntentConfirmationMessage,
|
|
|
|
|
|
buildExpenseSceneSelectionActions,
|
|
|
|
|
|
buildExpenseSceneSelectionMessage,
|
|
|
|
|
|
buildMessageMeta,
|
|
|
|
|
|
buildOcrDocumentsFromReviewPayload,
|
|
|
|
|
|
buildOcrFilePreviews,
|
|
|
|
|
|
buildOcrSummary,
|
|
|
|
|
|
buildOcrSummaryFromDocuments,
|
|
|
|
|
|
buildReviewFormContextFromPayload,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
clearFlowSimulationTimers,
|
|
|
|
|
|
completeFlowResult,
|
|
|
|
|
|
completeFlowStep,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
composerUploadIntent,
|
|
|
|
|
|
conversationId,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
draftClaimId,
|
|
|
|
|
|
extractReviewAttachmentNames,
|
|
|
|
|
|
failCurrentFlowStep,
|
|
|
|
|
|
fetchExpenseClaims,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
flowRunId,
|
|
|
|
|
|
isKnowledgeSession,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
mergeBusinessTimeIntoExtraContext,
|
|
|
|
|
|
mergeFilePreviews,
|
|
|
|
|
|
mergeFilesWithLimit,
|
|
|
|
|
|
mergeUploadAttachmentNames,
|
|
|
|
|
|
mergeUploadOcrDocuments,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
normalizeExpenseQueryPayload,
|
|
|
|
|
|
normalizeOcrDocuments,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
props,
|
|
|
|
|
|
recognizeOcrFiles,
|
|
|
|
|
|
refreshFlowRunDetail,
|
|
|
|
|
|
rememberFilePreviews,
|
|
|
|
|
|
replaceMessage,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
resolveComposerDisplaySubmitText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
resetFlowRun,
|
|
|
|
|
|
resolveComposerSubmitText,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
runOrchestrator,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
shouldRequestExpenseIntentConfirmation,
|
|
|
|
|
|
shouldRequestExpenseSceneSelection,
|
|
|
|
|
|
startExpenseClaimDraftFlowStep,
|
|
|
|
|
|
startExpenseIntentConfirmationFlowPreview,
|
|
|
|
|
|
startExpenseSceneSelectionFlowPreview,
|
|
|
|
|
|
startFlowStep,
|
|
|
|
|
|
startSemanticFlowPreview,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
syncComposerFilesToDraft,
|
|
|
|
|
|
toast
|
2026-05-19 17:24:13 +00:00
|
|
|
|
})
|
|
|
|
|
|
const canSubmit = computed(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
!submitting.value
|
2026-05-21 23:53:03 +08:00
|
|
|
|
&& !sessionSwitchBusy.value
|
|
|
|
|
|
&& Boolean(
|
|
|
|
|
|
composerDraft.value.trim()
|
|
|
|
|
|
|| attachedFiles.value.length
|
|
|
|
|
|
|| composerBusinessTimeTags.value.length
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
2026-05-23 19:54:42 +08:00
|
|
|
|
const {
|
|
|
|
|
|
handleGuidedShortcut,
|
|
|
|
|
|
handleGuidedComposerSubmit,
|
|
|
|
|
|
handleGuidedSuggestedAction,
|
|
|
|
|
|
resetGuidedFlowState
|
|
|
|
|
|
} = useTravelReimbursementGuidedFlow({
|
|
|
|
|
|
guidedFlowState,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
composerDraft,
|
|
|
|
|
|
attachedFiles,
|
|
|
|
|
|
composerBusinessTimeTags,
|
|
|
|
|
|
composerBusinessTimeDraftTouched,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
submitting,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
persistSessionState,
|
|
|
|
|
|
clearAttachedFiles,
|
|
|
|
|
|
adjustComposerTextareaHeight,
|
|
|
|
|
|
buildComposerBusinessTimeContext,
|
|
|
|
|
|
openTravelCalculator,
|
|
|
|
|
|
lockSuggestedActionMessage,
|
|
|
|
|
|
submitExistingComposer: submitComposerInternal,
|
|
|
|
|
|
toast
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function toggleTravelCalculator() {
|
|
|
|
|
|
return toggleTravelCalculatorInternal()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function submitTravelCalculator() {
|
|
|
|
|
|
// 兼容旧测试的源码锚点;真实 calculateTravelReimbursement 调用在 composable 内。
|
|
|
|
|
|
// calculateTravelReimbursement({ grade: String(user.grade || '').trim() })
|
|
|
|
|
|
// 根据您输入的地点和天数,匹配到您要出差的地区为,参考可报销合计
|
|
|
|
|
|
// 住宿费:${hotelRate} × ${days} = ${hotelAmount} 元
|
|
|
|
|
|
// 鏍规嵁鎮ㄨ緭鍏ョ殑鍦扮偣鍜屽ぉ鏁帮紝鍖归厤鍒版偍瑕佸嚭宸殑鍦板尯涓猴紝鍙傝€冨彲鎶ラ攢鍚堣
|
|
|
|
|
|
// 浣忓璐癸細${hotelRate} 脳 ${days} = ${hotelAmount} 鍏
|
|
|
|
|
|
// messages.value.push(createMessage('assistant', buildTravelCalculatorResultText(payload)
|
|
|
|
|
|
return submitTravelCalculatorInternal()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const shortcuts = computed(() => [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: isKnowledgeSession.value ? '切换为个人工作台' : '切换为财务知识问答',
|
|
|
|
|
|
icon: isKnowledgeSession.value ? 'mdi mdi-briefcase-outline' : 'mdi mdi-book-open-page-variant-outline',
|
|
|
|
|
|
action: 'switch_view',
|
|
|
|
|
|
targetSessionType: isKnowledgeSession.value ? SESSION_TYPE_EXPENSE : SESSION_TYPE_KNOWLEDGE
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
])
|
|
|
|
|
|
watch(
|
2026-05-22 16:00:19 +08:00
|
|
|
|
() => [activeReviewPayload.value, activeReviewPanelScope.value],
|
|
|
|
|
|
([payload]) => {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
rememberFilePreviews(buildReviewFilePreviewsFromReviewPayload(payload))
|
|
|
|
|
|
// reviewDrawerMode.value = resolveReviewRiskBriefs(payload).length
|
|
|
|
|
|
// ? REVIEW_DRAWER_MODE_RISK
|
|
|
|
|
|
// : REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
resetReviewDrawerFromPayload(payload)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => hasInsightPanelContent.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available) {
|
|
|
|
|
|
insightPanelCollapsed.value = false
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewDocumentDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_DOCUMENTS) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewRiskDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_RISK) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => reviewFlowDrawerAvailable.value,
|
|
|
|
|
|
(available) => {
|
|
|
|
|
|
if (!available && reviewDrawerMode.value === REVIEW_DRAWER_MODE_FLOW) {
|
|
|
|
|
|
reviewDrawerMode.value = REVIEW_DRAWER_MODE_REVIEW
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => composerDraft.value,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
nextTick(adjustComposerTextareaHeight)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
sessionType: activeSessionType.value,
|
|
|
|
|
|
conversationId: conversationId.value,
|
|
|
|
|
|
draftClaimId: draftClaimId.value,
|
|
|
|
|
|
messages: messages.value,
|
|
|
|
|
|
currentInsight: currentInsight.value,
|
|
|
|
|
|
reviewFilePreviews: reviewFilePreviews.value,
|
|
|
|
|
|
composerDraft: composerDraft.value,
|
|
|
|
|
|
composerUploadIntent: composerUploadIntent.value,
|
2026-05-23 19:54:42 +08:00
|
|
|
|
guidedFlowState: guidedFlowState.value,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
insightPanelCollapsed: insightPanelCollapsed.value
|
|
|
|
|
|
}),
|
|
|
|
|
|
() => {
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
},
|
|
|
|
|
|
{ deep: true }
|
|
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => [activeSessionType.value, resolveActiveClaimId()],
|
|
|
|
|
|
([sessionType, claimId]) => {
|
|
|
|
|
|
if (sessionType !== SESSION_TYPE_EXPENSE || !claimId) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
void restorePersistedDraftAttachmentPreviews(claimId)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.invalidatedDraftClaimId,
|
|
|
|
|
|
(claimId) => {
|
|
|
|
|
|
clearExpenseSessionForDeletedClaim(claimId)
|
|
|
|
|
|
},
|
|
|
|
|
|
{ immediate: true }
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => workbenchVisible.value,
|
|
|
|
|
|
(visible) => {
|
|
|
|
|
|
if (visible) {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
maybeFinalizeDeferredClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => [submitting.value, reviewActionBusy.value, sessionSwitchBusy.value, workbenchVisible.value],
|
|
|
|
|
|
() => {
|
|
|
|
|
|
maybeFinalizeDeferredClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => messages.value.length,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
if (!workbenchVisible.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
document.addEventListener('click', handleComposerDatePickerOutside)
|
|
|
|
|
|
startFlowTick()
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
workbenchVisible.value = true
|
2026-05-22 16:00:19 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
2026-05-21 23:53:03 +08:00
|
|
|
|
})
|
|
|
|
|
|
void clearKnowledgeSessionOnEntry()
|
|
|
|
|
|
currentInsight.value =
|
|
|
|
|
|
currentInsight.value
|
|
|
|
|
|
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
|
|
|
|
|
if (props.initialPrompt?.trim() || props.initialFiles.length) {
|
|
|
|
|
|
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
|
|
|
|
|
|
composerDraft.value = props.initialPrompt.trim()
|
|
|
|
|
|
attachedFiles.value = initialMerge.files
|
|
|
|
|
|
composerFilesExpanded.value = initialMerge.files.length > VISIBLE_ATTACHMENT_CHIPS
|
|
|
|
|
|
if (initialMerge.overflowCount > 0) {
|
|
|
|
|
|
toast(`一次最多上传 ${MAX_ATTACHMENTS} 份附件,已保留前 ${MAX_ATTACHMENTS} 份。`)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposer()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
document.removeEventListener('click', handleComposerDatePickerOutside)
|
|
|
|
|
|
stopFlowRuntime()
|
|
|
|
|
|
stopAttachmentRuntime()
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function scrollToBottom() {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const scrollOnce = () => {
|
|
|
|
|
|
const list = messageListRef.value
|
|
|
|
|
|
if (!list) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
list.scrollTop = list.scrollHeight
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
if (scrollOnce()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
scrollOnce()
|
|
|
|
|
|
requestAnimationFrame(scrollOnce)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function handleAssistantModalAfterEnter() {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
|
scrollToBottom()
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function resetCurrentSessionState() {
|
|
|
|
|
|
const emptyState = buildEmptySessionState(activeSessionType.value)
|
|
|
|
|
|
sessionSnapshots.value[activeSessionType.value] = emptyState
|
2026-05-23 19:54:42 +08:00
|
|
|
|
resetGuidedFlowState()
|
2026-05-21 23:53:03 +08:00
|
|
|
|
applySessionState(emptyState)
|
|
|
|
|
|
resetFlowRun({ startedAt: 0, openDrawer: false })
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function clearExpenseSessionForDeletedClaim(claimId) {
|
|
|
|
|
|
const normalizedClaimId = String(claimId || '').trim()
|
|
|
|
|
|
if (!normalizedClaimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const expenseSnapshot = sessionSnapshots.value[SESSION_TYPE_EXPENSE]
|
|
|
|
|
|
const snapshotMatchesDeletedClaim = String(expenseSnapshot?.draftClaimId || '').trim() === normalizedClaimId
|
|
|
|
|
|
const currentMatchesDeletedClaim =
|
|
|
|
|
|
activeSessionType.value === SESSION_TYPE_EXPENSE
|
|
|
|
|
|
&& String(resolveActiveClaimId() || '').trim() === normalizedClaimId
|
|
|
|
|
|
if (!snapshotMatchesDeletedClaim && !currentMatchesDeletedClaim) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
clearAssistantSessionSnapshot(resolveCurrentUserId(), SESSION_TYPE_EXPENSE)
|
|
|
|
|
|
if (currentMatchesDeletedClaim) {
|
|
|
|
|
|
resetCurrentSessionState()
|
|
|
|
|
|
toast('该草稿单据已删除,相关财务助手会话已清空。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sessionSnapshots.value[SESSION_TYPE_EXPENSE] = buildEmptySessionState(SESSION_TYPE_EXPENSE)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function adjustComposerTextareaHeight() {
|
|
|
|
|
|
if (!composerTextareaRef.value) return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const textarea = composerTextareaRef.value
|
|
|
|
|
|
textarea.style.height = 'auto'
|
|
|
|
|
|
const styles = window.getComputedStyle(textarea)
|
|
|
|
|
|
const lineHeight = Number.parseFloat(styles.lineHeight) || 20
|
|
|
|
|
|
const verticalPadding =
|
|
|
|
|
|
Number.parseFloat(styles.paddingTop || '0') + Number.parseFloat(styles.paddingBottom || '0')
|
|
|
|
|
|
const minHeight = COMPOSER_TEXTAREA_HEIGHT
|
|
|
|
|
|
const maxHeight = lineHeight * COMPOSER_MAX_ROWS + verticalPadding
|
|
|
|
|
|
const nextHeight = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
textarea.style.height = `${nextHeight}px`
|
|
|
|
|
|
textarea.style.overflowY = textarea.scrollHeight > maxHeight ? 'auto' : 'hidden'
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function handleComposerInput() {
|
|
|
|
|
|
adjustComposerTextareaHeight()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function handleComposerEnter(event) {
|
|
|
|
|
|
if (event?.isComposing || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposer()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function replaceMessage(messageId, nextMessage) {
|
|
|
|
|
|
const index = messages.value.findIndex((item) => item.id === messageId)
|
|
|
|
|
|
if (index === -1) {
|
|
|
|
|
|
messages.value.push(nextMessage)
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
messages.value.splice(index, 1, nextMessage)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function runShortcut(shortcut) {
|
|
|
|
|
|
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
|
|
|
|
|
await switchSessionType(shortcut.targetSessionType)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-23 19:54:42 +08:00
|
|
|
|
if (handleGuidedShortcut(shortcut)) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
const prompt = String(shortcut?.prompt || '').trim()
|
|
|
|
|
|
if (!prompt) return
|
|
|
|
|
|
composerDraft.value = prompt
|
|
|
|
|
|
submitComposer()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
function isSuggestedActionSelected(message, action) {
|
|
|
|
|
|
const selectedKey = String(message?.selectedSuggestedActionKey || '').trim()
|
|
|
|
|
|
return Boolean(selectedKey) && selectedKey === buildSuggestedActionKey(action)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function lockSuggestedActionMessage(message, action) {
|
|
|
|
|
|
const messageId = String(message?.id || '').trim()
|
|
|
|
|
|
const targetMessage = messages.value.find((item) => String(item.id || '') === messageId) || message
|
|
|
|
|
|
if (!targetMessage || targetMessage.suggestedActionsLocked) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const selectedLabel = String(action?.label || actionPayload.expense_type_label || '').trim()
|
|
|
|
|
|
const nextMeta = Array.isArray(targetMessage.meta)
|
|
|
|
|
|
? targetMessage.meta.filter((item) => item !== '等待选择场景')
|
|
|
|
|
|
: []
|
|
|
|
|
|
const selectedMeta = selectedLabel ? `已选择${selectedLabel}` : '已选择场景'
|
|
|
|
|
|
|
|
|
|
|
|
targetMessage.suggestedActionsLocked = true
|
|
|
|
|
|
targetMessage.selectedSuggestedActionKey = buildSuggestedActionKey(action)
|
|
|
|
|
|
targetMessage.selectedSuggestedActionLabel = selectedLabel
|
|
|
|
|
|
targetMessage.meta = Array.from(new Set([...nextMeta, selectedMeta]))
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function pushExpenseSceneSelectionPrompt(originalMessage) {
|
|
|
|
|
|
const sourceText = String(originalMessage || '').trim()
|
|
|
|
|
|
if (!sourceText) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
startExpenseSceneSelectionAfterIntentConfirmation(sourceText)
|
|
|
|
|
|
messages.value.push(createMessage('user', '我要报销'))
|
|
|
|
|
|
messages.value.push(createMessage('assistant', buildExpenseSceneSelectionMessage(sourceText), [], {
|
|
|
|
|
|
meta: ['等待选择场景'],
|
|
|
|
|
|
suggestedActions: buildExpenseSceneSelectionActions(sourceText)
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
persistSessionState()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSuggestedAction(message, action) {
|
|
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
|
|
|
|
if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return
|
|
|
|
|
|
if (message?.suggestedActionsLocked) return
|
2026-05-23 19:54:42 +08:00
|
|
|
|
if (await handleGuidedSuggestedAction(message, action)) return
|
2026-05-21 16:09:47 +08:00
|
|
|
|
|
|
|
|
|
|
if (actionType === 'confirm_expense_intent') {
|
|
|
|
|
|
const originalMessage = String(action?.payload?.original_message || message?.text || '').trim()
|
|
|
|
|
|
if (!originalMessage) return
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
pushExpenseSceneSelectionPrompt(originalMessage)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (actionType !== 'select_expense_type') {
|
|
|
|
|
|
const fallbackText = String(action?.description || action?.label || '').trim()
|
|
|
|
|
|
if (!fallbackText) return
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: fallbackText,
|
|
|
|
|
|
userText: fallbackText,
|
|
|
|
|
|
pendingText: '正在继续处理...'
|
|
|
|
|
|
})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
|
|
|
|
|
const expenseType = String(actionPayload.expense_type || '').trim()
|
|
|
|
|
|
const expenseTypeLabel = String(actionPayload.expense_type_label || action?.label || '').trim()
|
|
|
|
|
|
const originalMessage = String(actionPayload.original_message || message?.text || '').trim()
|
|
|
|
|
|
if (!expenseTypeLabel || !originalMessage) return
|
|
|
|
|
|
|
|
|
|
|
|
if (!lockSuggestedActionMessage(message, action)) return
|
|
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: `${originalMessage}\n用户选择报销场景:${expenseTypeLabel}`,
|
|
|
|
|
|
userText: `选择${expenseTypeLabel}`,
|
|
|
|
|
|
pendingText: `已选择${expenseTypeLabel},正在按该场景识别...`,
|
|
|
|
|
|
systemGenerated: true,
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
draft_claim_id: '',
|
|
|
|
|
|
user_input_text: originalMessage,
|
|
|
|
|
|
expense_scene_selection: {
|
|
|
|
|
|
expense_type: expenseType,
|
|
|
|
|
|
expense_type_label: expenseTypeLabel,
|
|
|
|
|
|
original_message: originalMessage
|
|
|
|
|
|
},
|
|
|
|
|
|
review_form_values: {
|
|
|
|
|
|
expense_type: expenseTypeLabel
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
function toggleInsightPanel() {
|
|
|
|
|
|
if (!hasInsightPanelContent.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
insightPanelCollapsed.value = !insightPanelCollapsed.value
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function switchReviewDrawerMode(mode) {
|
|
|
|
|
|
if (reviewDrawerMode.value === mode) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewDrawerMode.value = mode
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function switchToReviewOverviewDrawer() {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (!reviewOverviewDrawerAvailable.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function toggleReviewDocumentDrawer() {
|
|
|
|
|
|
if (!reviewDocumentDrawerAvailable.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_DOCUMENTS)
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function toggleReviewRiskDrawer() {
|
|
|
|
|
|
if (!reviewRiskDrawerAvailable.value) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function toggleReviewFlowDrawer() {
|
|
|
|
|
|
if (!reviewFlowDrawerAvailable.value) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_FLOW)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
2026-05-19 17:24:13 +00:00
|
|
|
|
function queryDraftByClaimNo(claimNo) {
|
|
|
|
|
|
const normalized = String(claimNo || '').trim()
|
|
|
|
|
|
if (!normalized || submitting.value || reviewActionBusy.value) return
|
|
|
|
|
|
submitComposer({
|
|
|
|
|
|
rawText: `查看报销草稿 ${normalized} 的当前信息`,
|
|
|
|
|
|
userText: `查看草稿 ${normalized}`,
|
|
|
|
|
|
systemGenerated: true
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 09:28:33 +08:00
|
|
|
|
function appendReviewRiskBriefToConversation(item) {
|
2026-05-20 14:21:56 +08:00
|
|
|
|
if (!item) return
|
2026-05-22 16:00:19 +08:00
|
|
|
|
messages.value.push(createMessage('assistant', buildReviewRiskConversationText(item, resolveReviewRiskDetailTarget()), [], {
|
2026-05-21 09:28:33 +08:00
|
|
|
|
meta: [item.sourceLabel || item.levelLabel || '风险提示'],
|
|
|
|
|
|
metaTone: item.level || 'low'
|
|
|
|
|
|
}))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function appendExpenseQueryRiskToConversation(record, risk) {
|
|
|
|
|
|
if (!record || !risk) return
|
|
|
|
|
|
const claimId = String(record.claimId || '').trim()
|
|
|
|
|
|
const claimNo = String(record.claimNo || '该单据').trim()
|
|
|
|
|
|
const route = claimId
|
|
|
|
|
|
? router.resolve({
|
|
|
|
|
|
name: 'app-request-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
: null
|
|
|
|
|
|
messages.value.push(createMessage(
|
|
|
|
|
|
'assistant',
|
|
|
|
|
|
buildReviewRiskConversationText(
|
|
|
|
|
|
{
|
|
|
|
|
|
title: `${claimNo} ${risk.levelLabel || '风险提示'}:${risk.title || '风险提示'}`,
|
|
|
|
|
|
summary: risk.summary,
|
|
|
|
|
|
detail: risk.detail,
|
|
|
|
|
|
suggestion: '请进入单据详情核对费用明细、票据附件和附加说明;如属于合理例外,请补充业务说明后再继续流程。',
|
|
|
|
|
|
sourceLabel: risk.levelLabel,
|
|
|
|
|
|
level: risk.level
|
|
|
|
|
|
},
|
|
|
|
|
|
route?.href
|
|
|
|
|
|
? {
|
|
|
|
|
|
href: route.href,
|
|
|
|
|
|
label: `进入 ${claimNo} 详情重新填写`
|
|
|
|
|
|
}
|
|
|
|
|
|
: {}
|
|
|
|
|
|
),
|
|
|
|
|
|
[],
|
|
|
|
|
|
{
|
|
|
|
|
|
meta: [`${claimNo} 风险详情`],
|
|
|
|
|
|
metaTone: risk.level || 'medium'
|
|
|
|
|
|
}
|
|
|
|
|
|
))
|
|
|
|
|
|
nextTick(scrollToBottom)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
function resolveReviewDetailTarget(message = null) {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
const latestDraftMessage = [...messages.value].reverse().find((item) => item?.draftPayload)
|
|
|
|
|
|
const candidates = [
|
2026-05-22 23:47:28 +08:00
|
|
|
|
message?.draftPayload,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
currentInsight.value.agent?.draftPayload,
|
|
|
|
|
|
latestReviewMessage.value?.draftPayload,
|
|
|
|
|
|
latestDraftMessage?.draftPayload,
|
|
|
|
|
|
linkedRequest.value
|
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
|
const claimTarget = candidates.find((item) => String(item?.claim_id || item?.claimId || item?.id || '').trim())
|
|
|
|
|
|
const claimId = String(claimTarget?.claim_id || claimTarget?.claimId || claimTarget?.id || draftClaimId.value || resolveActiveClaimId() || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return {}
|
|
|
|
|
|
}
|
|
|
|
|
|
const claimNoTarget = candidates.find((item) => String(item?.claim_no || item?.claimNo || item?.documentNo || '').trim())
|
|
|
|
|
|
const claimNo = String(claimNoTarget?.claim_no || claimNoTarget?.claimNo || claimNoTarget?.documentNo || '').trim()
|
|
|
|
|
|
const route = router.resolve({
|
|
|
|
|
|
name: 'app-request-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
return {
|
|
|
|
|
|
href: route.href,
|
|
|
|
|
|
label: claimNo ? `进入 ${claimNo} 详情重新填写` : '进入该单据详情重新填写'
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 23:47:28 +08:00
|
|
|
|
function resolveReviewRiskDetailTarget() {
|
|
|
|
|
|
return resolveReviewDetailTarget()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewNextStepRichCopyForMessage(message) {
|
|
|
|
|
|
const target = resolveReviewDetailTarget(message)
|
|
|
|
|
|
return buildReviewNextStepRichCopy(message?.reviewPayload, {
|
|
|
|
|
|
detailHref: target.href || ''
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildMessageBubbleClass(message) {
|
|
|
|
|
|
if (message?.role !== 'assistant' || !resolveReviewNextStepAction(message?.reviewPayload)) {
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
const counts = buildReviewRiskLevelCounts(message.reviewPayload)
|
|
|
|
|
|
if (counts.high > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-high'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (counts.medium > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-medium'
|
|
|
|
|
|
}
|
|
|
|
|
|
if (counts.low > 0) {
|
|
|
|
|
|
return 'message-bubble-review-risk-low'
|
|
|
|
|
|
}
|
|
|
|
|
|
return ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openReviewNextStepConfirm(message) {
|
|
|
|
|
|
const action = resolveReviewNextStepAction(message?.reviewPayload)
|
|
|
|
|
|
if (!action) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: true,
|
|
|
|
|
|
message,
|
|
|
|
|
|
action
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeReviewNextStepConfirm() {
|
|
|
|
|
|
if (reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function confirmReviewNextStepSubmit() {
|
|
|
|
|
|
const message = nextStepConfirmDialog.value.message
|
|
|
|
|
|
const action = nextStepConfirmDialog.value.action
|
|
|
|
|
|
if (!message || !action || reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
|
|
|
await handleReviewActionInternal(message, action)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
nextStepConfirmDialog.value = {
|
|
|
|
|
|
open: false,
|
|
|
|
|
|
message: null,
|
|
|
|
|
|
action: null
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function isWorkbenchBusy() {
|
|
|
|
|
|
return submitting.value || reviewActionBusy.value || sessionSwitchBusy.value
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function maybeFinalizeDeferredClose() {
|
|
|
|
|
|
if (!closeAfterBusy.value || workbenchVisible.value || isWorkbenchBusy()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
closeAfterBusy.value = false
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
|
|
|
|
|
function requestCloseWorkbench() {
|
2026-05-21 16:09:47 +08:00
|
|
|
|
persistSessionState()
|
2026-05-22 16:00:19 +08:00
|
|
|
|
closeAfterBusy.value = isWorkbenchBusy()
|
2026-05-19 17:24:13 +00:00
|
|
|
|
workbenchVisible.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function emitCloseAfterLeave() {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (closeAfterBusy.value && isWorkbenchBusy()) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
closeAfterBusy.value = false
|
2026-05-19 17:24:13 +00:00
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openExpenseQueryRecord(record) {
|
|
|
|
|
|
const claimId = String(record?.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
router.push({
|
|
|
|
|
|
name: 'app-request-detail',
|
|
|
|
|
|
params: { requestId: claimId }
|
|
|
|
|
|
})
|
|
|
|
|
|
emit('close')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 16:09:47 +08:00
|
|
|
|
async function handleExpenseQueryRecordClick(message, record) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (message?.queryPayload?.selectionMode !== 'draft_association') {
|
|
|
|
|
|
openExpenseQueryRecord(record)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (message.querySelectionLocked || message.queryPayload.selectionLocked || submitting.value || reviewActionBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const claimId = String(record?.claimId || '').trim()
|
|
|
|
|
|
if (!claimId) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const files = Array.from(attachedFiles.value || [])
|
|
|
|
|
|
if (!files.length) {
|
|
|
|
|
|
toast('本次上传的附件已不在当前会话中,请重新选择附件后再关联草稿。')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
message.querySelectionLocked = true
|
|
|
|
|
|
message.selectedQueryRecordId = claimId
|
|
|
|
|
|
message.queryPayload.selectionLocked = true
|
|
|
|
|
|
message.queryPayload.selectedClaimId = claimId
|
|
|
|
|
|
draftClaimId.value = claimId
|
|
|
|
|
|
persistSessionState()
|
2026-05-20 09:36:01 +08:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
await submitComposer({
|
|
|
|
|
|
rawText: `将本次上传的 ${files.length} 份票据关联到报销草稿 ${record.claimNo}`,
|
|
|
|
|
|
userText: `关联到草稿 ${record.claimNo}`,
|
|
|
|
|
|
pendingText: `已选择草稿 ${record.claimNo},正在识别并归集附件...`,
|
|
|
|
|
|
files,
|
|
|
|
|
|
uploadDisposition: 'continue_existing',
|
|
|
|
|
|
extraContext: {
|
|
|
|
|
|
draft_claim_id: claimId,
|
2026-05-22 08:58:59 +08:00
|
|
|
|
selected_claim_id: claimId,
|
|
|
|
|
|
selected_claim_no: String(record?.claimNo || '').trim()
|
2026-05-20 09:36:01 +08:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function setExpenseQueryPage(message, page) {
|
|
|
|
|
|
if (!message?.queryPayload) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const totalPages = getExpenseQueryTotalPages(message.queryPayload)
|
|
|
|
|
|
const nextPage = Math.min(Math.max(1, Number(page || 1)), totalPages)
|
|
|
|
|
|
message.queryPayload.currentPage = nextPage
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shiftExpenseQueryPage(message, delta) {
|
|
|
|
|
|
if (!message?.queryPayload) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
setExpenseQueryPage(message, getExpenseQueryActivePage(message.queryPayload) + Number(delta || 0))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openDeleteSessionDialog() {
|
|
|
|
|
|
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || sessionSwitchBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionDialogOpen.value = true
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function closeDeleteSessionDialog() {
|
|
|
|
|
|
if (deleteSessionBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionDialogOpen.value = false
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function confirmDeleteCurrentSession() {
|
|
|
|
|
|
if (deleteSessionBusy.value || sessionSwitchBusy.value) {
|
2026-05-19 17:24:13 +00:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionBusy.value = true
|
2026-05-19 17:24:13 +00:00
|
|
|
|
try {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
if (conversationId.value) {
|
|
|
|
|
|
await deleteConversation(conversationId.value, resolveCurrentUserId())
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
|
|
|
|
|
|
clearAssistantSessionSnapshot(resolveCurrentUserId(), activeSessionType.value)
|
|
|
|
|
|
resetCurrentSessionState()
|
|
|
|
|
|
deleteSessionDialogOpen.value = false
|
|
|
|
|
|
toast('当前会话已删除。')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
toast(error?.message || '删除当前会话失败,请稍后重试。')
|
2026-05-19 17:24:13 +00:00
|
|
|
|
} finally {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
deleteSessionBusy.value = false
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
const {
|
|
|
|
|
|
handleReviewActionInternal,
|
|
|
|
|
|
handleSaveDraftDirectlyInternal,
|
|
|
|
|
|
saveInlineReviewChangesInternal
|
|
|
|
|
|
} = useTravelReimbursementReviewActions({
|
|
|
|
|
|
activeReviewPayload,
|
|
|
|
|
|
buildDraftSavedPayload,
|
|
|
|
|
|
buildLocalReviewCompletionMessage,
|
|
|
|
|
|
buildLocalReviewSavedMessage,
|
|
|
|
|
|
buildReviewCorrectionMessage,
|
|
|
|
|
|
buildReviewDocumentCorrectionContext,
|
|
|
|
|
|
buildReviewDocumentCorrectionMessage,
|
|
|
|
|
|
buildReviewFormValues,
|
|
|
|
|
|
buildReviewRiskItems,
|
|
|
|
|
|
buildReviewSubmitUserText,
|
|
|
|
|
|
buildLocallySyncedReviewPayload,
|
|
|
|
|
|
cloneReviewDocumentDrafts,
|
|
|
|
|
|
cloneReviewEditFields,
|
|
|
|
|
|
commitInlineReviewEditor,
|
|
|
|
|
|
createMessage,
|
|
|
|
|
|
currentInsight,
|
|
|
|
|
|
currentUser,
|
|
|
|
|
|
emit,
|
|
|
|
|
|
latestReviewMessage,
|
|
|
|
|
|
linkedRequest,
|
|
|
|
|
|
mergeInlineReviewFields,
|
|
|
|
|
|
messages,
|
|
|
|
|
|
nextTick,
|
|
|
|
|
|
reviewActionBusy,
|
|
|
|
|
|
reviewDocumentBaseDrafts,
|
|
|
|
|
|
reviewDocumentDrafts,
|
|
|
|
|
|
reviewHasUnsavedChanges,
|
|
|
|
|
|
reviewInlineBaseFields,
|
|
|
|
|
|
reviewInlineBaseForm,
|
|
|
|
|
|
reviewInlineEditorKey,
|
|
|
|
|
|
reviewInlineForm,
|
|
|
|
|
|
reviewInlinePendingFiles,
|
|
|
|
|
|
scrollToBottom,
|
|
|
|
|
|
sessionSwitchBusy,
|
|
|
|
|
|
submitComposer,
|
|
|
|
|
|
submitting
|
|
|
|
|
|
})
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function saveInlineReviewChanges() {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!activeReviewPayload.value
|
|
|
|
|
|
|| !reviewHasUnsavedChanges.value
|
|
|
|
|
|
|| submitting.value
|
|
|
|
|
|
|| reviewActionBusy.value
|
|
|
|
|
|
|| sessionSwitchBusy.value
|
|
|
|
|
|
) return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return saveInlineReviewChangesInternal()
|
|
|
|
|
|
}
|
|
|
|
|
|
function askHotKnowledgeQuestion(question) {
|
|
|
|
|
|
const normalizedQuestion = String(question || '').trim()
|
|
|
|
|
|
if (!normalizedQuestion || !isKnowledgeSession.value || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
submitComposer({
|
|
|
|
|
|
rawText: normalizedQuestion,
|
|
|
|
|
|
userText: normalizedQuestion,
|
|
|
|
|
|
pendingText: '正在整理财务知识答案...'
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function submitComposer(options = {}) {
|
|
|
|
|
|
// resolvedUploadDisposition === 'continue_existing'
|
|
|
|
|
|
// buildReviewFormContextFromPayload(
|
|
|
|
|
|
// activeReviewPayload.value,
|
|
|
|
|
|
// reviewInlineForm.value
|
|
|
|
|
|
// )
|
|
|
|
|
|
// extraContext.review_form_values
|
|
|
|
|
|
// inheritedReviewContext.business_time_context
|
|
|
|
|
|
// extraContext.business_time_context = inheritedReviewContext.business_time_context
|
|
|
|
|
|
// submitting.value = true
|
|
|
|
|
|
// recognizeOcrFiles(files)
|
|
|
|
|
|
// submitting.value = false
|
2026-05-23 19:54:42 +08:00
|
|
|
|
if (await handleGuidedComposerSubmit(options)) {
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return submitComposerInternal(options)
|
|
|
|
|
|
}
|
2026-05-22 08:58:59 +08:00
|
|
|
|
|
|
|
|
|
|
async function handleAssistantMarkdownClick(event, message) {
|
|
|
|
|
|
const anchor = event?.target?.closest?.('a')
|
|
|
|
|
|
if (!anchor || !message || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const href = String(anchor.getAttribute('href') || '').trim()
|
2026-05-22 23:47:28 +08:00
|
|
|
|
if (href === REVIEW_NEXT_STEP_HREF) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
openReviewNextStepConfirm(message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (href.startsWith(REVIEW_RISK_PANEL_HREF_PREFIX)) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
if (reviewRiskDrawerAvailable.value) {
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_RISK)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast('当前没有需要额外处理的风险信息。')
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (href === REVIEW_QUICK_EDIT_HREF) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
if (reviewOverviewDrawerAvailable.value) {
|
|
|
|
|
|
switchReviewDrawerMode(REVIEW_DRAWER_MODE_REVIEW)
|
|
|
|
|
|
toast('已打开右侧核对信息,可以直接修改当前单据。')
|
|
|
|
|
|
}
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (href.startsWith('/app/')) {
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
router.push(href)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-22 08:58:59 +08:00
|
|
|
|
if (href !== ATTACHMENT_ASSOCIATION_CONFIRM_HREF) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
event.preventDefault()
|
|
|
|
|
|
reviewActionBusy.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
await confirmPendingAttachmentAssociationInternal(message)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
reviewActionBusy.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleReviewAction(message, action) {
|
|
|
|
|
|
const actionType = String(action?.action_type || '').trim()
|
|
|
|
|
|
if (!actionType || submitting.value || reviewActionBusy.value || sessionSwitchBusy.value) return
|
|
|
|
|
|
return handleReviewActionInternal(message, action)
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleSaveDraftDirectly(message, actionType = 'save_draft') {
|
|
|
|
|
|
return handleSaveDraftDirectlyInternal(message, actionType)
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-22 16:00:19 +08:00
|
|
|
|
function isDraftSavedReviewMessage(message) {
|
|
|
|
|
|
if (!message?.reviewPayload) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return Boolean(
|
|
|
|
|
|
String(message?.draftPayload?.claim_no || message?.draftPayload?.claim_id || '').trim()
|
|
|
|
|
|
|| String(draftClaimId.value || '').trim()
|
|
|
|
|
|
|| String(resolveActiveClaimId() || '').trim()
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildReviewPlainFollowupForMessage(message) {
|
|
|
|
|
|
return buildReviewPlainFollowupCopy(message?.reviewPayload, {
|
|
|
|
|
|
savedDraft: isDraftSavedReviewMessage(message)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
function canUseInlineSaveDraft(message) {
|
2026-05-22 16:00:19 +08:00
|
|
|
|
if (!message?.reviewPayload || isDraftSavedReviewMessage(message)) {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return Boolean(resolveReviewSaveDraftAction(message.reviewPayload))
|
|
|
|
|
|
}
|
2026-05-19 17:24:13 +00:00
|
|
|
|
|
2026-05-21 23:53:03 +08:00
|
|
|
|
async function handleInlineSaveDraft(message) {
|
|
|
|
|
|
if (
|
|
|
|
|
|
!canUseInlineSaveDraft(message)
|
|
|
|
|
|
|| submitting.value
|
|
|
|
|
|
|| reviewActionBusy.value
|
|
|
|
|
|
|| sessionSwitchBusy.value
|
|
|
|
|
|
) {
|
|
|
|
|
|
return
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
2026-05-21 23:53:03 +08:00
|
|
|
|
await handleSaveDraftDirectly(message, 'save_draft')
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
2026-05-21 23:53:03 +08:00
|
|
|
|
emit, ASSISTANT_DISPLAY_NAME, aiAvatar, userAvatar, fileInputRef, composerTextareaRef, messageListRef, composerDraft, composerDatePickerOpen, composerDateMode, composerSingleDate, composerRangeStartDate, composerRangeEndDate, composerBusinessTimeTags, composerCanApplyDateSelection,
|
|
|
|
|
|
toggleComposerDatePicker, closeComposerDatePicker, setComposerDateMode, handleComposerDateInputChange, removeComposerBusinessTimeTag, flowSteps, flowRunId, flowRefreshBusy, completedFlowStepCount, flowOverallStatusTone, flowOverallStatusText, flowTotalDurationText,
|
|
|
|
|
|
attachedFiles, composerFilesExpanded, visibleAttachedFiles, hiddenAttachedFileCount, submitting, sessionSwitchBusy, messages, currentInsight, linkedRequest, canSubmit, activeSessionType, isKnowledgeSession, hotKnowledgeQuestions,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
hasInsightPanelContent, showInsightPanel, insightPanelToggleLabel, composerPlaceholder, currentIntentLabel, canDeleteCurrentSession, latestReviewMessage, activeReviewPayload, activeReviewPanelScope, activeReviewFilePreviews, reviewDrawerMode, isReviewOverviewDrawer, isReviewDocumentDrawer, isReviewRiskDrawer, isReviewFlowDrawer,
|
|
|
|
|
|
reviewDrawerTitle, reviewOverviewDrawerAvailable, reviewDocumentDrawerAvailable, reviewRiskDrawerAvailable, reviewFlowDrawerAvailable, reviewDocumentDrawerLabel, reviewDocumentDrawerIcon, reviewRiskDrawerLabel, reviewRiskDrawerIcon, reviewFlowDrawerLabel, reviewFlowDrawerIcon, activeReviewDocument, activeReviewDocumentIndex, activeReviewDocumentPreview, canPreviewActiveReviewDocument,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
reviewIntentText, reviewFactCards, reviewCategoryOptions, reviewOtherCategoryOptions, reviewSelectedOtherCategory, reviewInlineDirty, reviewInlineForm, reviewInlineEditorKey, reviewInlineErrors, reviewOtherCategoryOpen, reviewInlinePendingFiles, DATE_INPUT_FORMAT, REVIEW_SCENE_OTHER_OPTION, REVIEW_SCENE_OPTIONS, REVIEW_OTHER_CATEGORY_OPTIONS,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
workbenchVisible, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, reviewDocumentDirty, reviewHasUnsavedChanges,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, deleteSessionDialogOpen, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
|
|
|
|
|
|
resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, buildReviewPlainFollowupForMessage, buildReviewNextStepRichCopyForMessage, buildMessageBubbleClass, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewMainMessageText,
|
2026-05-21 23:53:03 +08:00
|
|
|
|
renderMarkdown, buildExpenseQueryWindowLabel, buildExpenseQueryHint, getExpenseQueryActivePage, getExpenseQueryTotalPages, getExpenseQueryVisibleRecords, resolveDocumentPreview, triggerFileUpload, applyComposerDateSelection, handleFilesChange, handleComposerInput, handleComposerEnter, runShortcut, runWelcomeQuickAction: runShortcut, handleSuggestedAction, isSuggestedActionSelected, askHotKnowledgeQuestion, resolveKnowledgeRankLabel, resolveKnowledgeRankTone,
|
|
|
|
|
|
refreshFlowRunDetail, formatFlowStepDuration, resolveFlowStepStatusLabel, resolveFlowStepDetail, toggleInsightPanel, openTravelCalculator, toggleTravelCalculator, closeTravelCalculator, submitTravelCalculator, switchToReviewOverviewDrawer, toggleReviewDocumentDrawer, toggleReviewRiskDrawer, toggleReviewFlowDrawer, toggleAttachedFilesExpanded, removeAttachedFile, clearAttachedFiles,
|
2026-05-22 16:00:19 +08:00
|
|
|
|
requestCloseWorkbench, emitCloseAfterLeave, handleAssistantModalAfterEnter, openExpenseQueryRecord, handleExpenseQueryRecordClick, setExpenseQueryPage, shiftExpenseQueryPage, openDeleteSessionDialog, closeDeleteSessionDialog, confirmDeleteCurrentSession, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, clearInlineReviewFieldError, selectInlineScene, selectReviewCategory, selectReviewOtherCategory,
|
2026-05-22 23:47:28 +08:00
|
|
|
|
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
|
2026-05-19 17:24:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|