feat: 完善文档中心与报销申请交互及侧边栏重构
后端优化编排器报销查询和本体检测精度,增强报销单草稿保 存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导 航,完善文档中心状态筛选和详情提示,报销创建和审批详情 页优化会话管理和费用明细交互,新增助手应用服务和预设动 作工具函数,补充单元测试覆盖。
This commit is contained in:
@@ -29,6 +29,11 @@ import {
|
||||
buildExpenseSceneSelectionActions
|
||||
} from '../../utils/expenseAssistantActions.js'
|
||||
import { buildSuggestedActionKey } from '../../utils/suggestedActionKey.js'
|
||||
import { ASSISTANT_SCOPE_ACTION_SWITCH } from '../../utils/assistantSessionScope.js'
|
||||
import {
|
||||
mergeComposerPrefill,
|
||||
resolveSuggestedActionPrefill
|
||||
} from '../../utils/assistantSuggestedActionPrefill.js'
|
||||
import {
|
||||
calculateTravelReimbursement,
|
||||
fetchExpenseClaims,
|
||||
@@ -143,11 +148,14 @@ import {
|
||||
resolveDocumentPreview
|
||||
} from './travelReimbursementAttachmentModel.js'
|
||||
import {
|
||||
ASSISTANT_SESSION_MODE_OPTIONS,
|
||||
ASSISTANT_DISPLAY_NAME,
|
||||
FLOW_STEP_FALLBACKS,
|
||||
HOT_KNOWLEDGE_QUESTIONS,
|
||||
INTENT_LABELS,
|
||||
SCENARIO_LABELS,
|
||||
SESSION_TYPE_APPLICATION,
|
||||
SESSION_TYPE_APPROVAL,
|
||||
SESSION_TYPE_EXPENSE,
|
||||
SESSION_TYPE_KNOWLEDGE,
|
||||
aiAvatar,
|
||||
@@ -156,6 +164,7 @@ import {
|
||||
buildMessageMeta,
|
||||
buildWelcomeInsight,
|
||||
createMessage,
|
||||
resolveAssistantSessionMode,
|
||||
resolveKnowledgeRankLabel,
|
||||
resolveKnowledgeRankTone,
|
||||
sanitizeRequest,
|
||||
@@ -195,6 +204,7 @@ const REVIEW_PANEL_SCOPE_OVERVIEW = 'overview'
|
||||
const REVIEW_PANEL_SCOPE_DOCUMENTS = 'documents'
|
||||
const REVIEW_PANEL_SCOPE_RISK = 'risk'
|
||||
const REVIEW_NEXT_STEP_HREF = '#review-next-step'
|
||||
const APPLICATION_SUBMIT_HREF = '#application-submit'
|
||||
const REVIEW_RISK_PANEL_HREF_PREFIX = '#review-risk'
|
||||
const REVIEW_QUICK_EDIT_HREF = '#review-quick-edit'
|
||||
const FLOW_STEP_STATUS_PENDING = 'pending'
|
||||
@@ -544,7 +554,6 @@ export default {
|
||||
resolveCurrentUserId,
|
||||
persistSessionState,
|
||||
applySessionState,
|
||||
clearKnowledgeSessionOnEntry,
|
||||
switchSessionType
|
||||
} = useTravelReimbursementSessionState({
|
||||
props,
|
||||
@@ -557,6 +566,10 @@ export default {
|
||||
getSessionRuntimeRefs: () => sessionRuntimeRefs
|
||||
})
|
||||
const deleteSessionDialogOpen = ref(false)
|
||||
const applicationSubmitConfirmDialog = ref({
|
||||
open: false,
|
||||
message: null
|
||||
})
|
||||
const nextStepConfirmDialog = ref({
|
||||
open: false,
|
||||
message: null,
|
||||
@@ -566,6 +579,9 @@ export default {
|
||||
const deleteSessionBusy = ref(false)
|
||||
const reviewDrawerMode = ref(REVIEW_DRAWER_MODE_REVIEW)
|
||||
const isKnowledgeSession = computed(() => activeSessionType.value === SESSION_TYPE_KNOWLEDGE)
|
||||
const activeAssistantMode = computed(() => resolveAssistantSessionMode(activeSessionType.value))
|
||||
const assistantHeaderTitle = computed(() => activeAssistantMode.value?.label || '财务助手')
|
||||
const assistantHeaderDescription = computed(() => activeAssistantMode.value?.description || '个人财务中心')
|
||||
const {
|
||||
flowRunId,
|
||||
flowSteps,
|
||||
@@ -640,6 +656,12 @@ export default {
|
||||
if (props.entrySource === 'detail' && linkedRequest.value?.id) {
|
||||
return `例如:解释一下 ${linkedRequest.value.id} 的报销风险,或帮我生成处理意见草稿。`
|
||||
}
|
||||
if (activeSessionType.value === SESSION_TYPE_APPLICATION) {
|
||||
return '例如:我想先申请一笔差旅费用,去上海支持项目部署。'
|
||||
}
|
||||
if (activeSessionType.value === SESSION_TYPE_APPROVAL) {
|
||||
return '例如:查一下待我审核的单据,或帮我生成这张单据的审核意见。'
|
||||
}
|
||||
return '例如:查一下近10日报销金额、解释酒店超标风险,或根据附件整理报销核对信息。'
|
||||
})
|
||||
const currentIntentLabel = computed(() => {
|
||||
@@ -652,12 +674,11 @@ export default {
|
||||
agent: '知识回答'
|
||||
}
|
||||
: {
|
||||
welcome: '财务助手',
|
||||
welcome: activeAssistantMode.value?.label || '财务助手',
|
||||
agent: '处理中'
|
||||
}
|
||||
return labels[currentInsight.value.intent] ?? 'AI 处理中'
|
||||
})
|
||||
let knowledgeSessionResetPromise = Promise.resolve()
|
||||
const canDeleteCurrentSession = computed(
|
||||
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
|
||||
)
|
||||
@@ -1008,14 +1029,15 @@ export default {
|
||||
}
|
||||
const isReviewOverviewDrawer = computed(() => reviewDrawerMode.value === REVIEW_DRAWER_MODE_REVIEW)
|
||||
|
||||
const shortcuts = computed(() => [
|
||||
{
|
||||
label: isKnowledgeSession.value ? '切换为个人工作台' : '切换为财务知识问答',
|
||||
icon: isKnowledgeSession.value ? 'mdi mdi-briefcase-outline' : 'mdi mdi-book-open-page-variant-outline',
|
||||
const shortcuts = computed(() =>
|
||||
ASSISTANT_SESSION_MODE_OPTIONS.map((mode) => ({
|
||||
label: mode.label,
|
||||
icon: mode.icon,
|
||||
action: 'switch_view',
|
||||
targetSessionType: isKnowledgeSession.value ? SESSION_TYPE_EXPENSE : SESSION_TYPE_KNOWLEDGE
|
||||
}
|
||||
])
|
||||
targetSessionType: mode.key,
|
||||
active: mode.key === activeSessionType.value
|
||||
}))
|
||||
)
|
||||
watch(
|
||||
() => [activeReviewPayload.value, activeReviewPanelScope.value],
|
||||
([payload]) => {
|
||||
@@ -1147,7 +1169,6 @@ export default {
|
||||
scrollToBottom()
|
||||
})
|
||||
})
|
||||
void clearKnowledgeSessionOnEntry()
|
||||
currentInsight.value =
|
||||
currentInsight.value
|
||||
|| buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value, currentUser.value)
|
||||
@@ -1269,6 +1290,9 @@ export default {
|
||||
|
||||
async function runShortcut(shortcut) {
|
||||
if (shortcut?.action === 'switch_view' && shortcut?.targetSessionType) {
|
||||
if (shortcut.active) {
|
||||
return
|
||||
}
|
||||
await switchSessionType(shortcut.targetSessionType)
|
||||
return
|
||||
}
|
||||
@@ -1325,12 +1349,52 @@ export default {
|
||||
persistSessionState()
|
||||
}
|
||||
|
||||
function applySuggestedActionPrefill(action) {
|
||||
const prefillText = resolveSuggestedActionPrefill(action)
|
||||
if (!prefillText) {
|
||||
return false
|
||||
}
|
||||
|
||||
composerDraft.value = mergeComposerPrefill(composerDraft.value, prefillText)
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
composerTextareaRef.value?.focus()
|
||||
})
|
||||
persistSessionState()
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
if (applySuggestedActionPrefill(action)) return
|
||||
if (await handleGuidedSuggestedAction(message, action)) return
|
||||
|
||||
if (actionType === ASSISTANT_SCOPE_ACTION_SWITCH) {
|
||||
const actionPayload = action?.payload && typeof action.payload === 'object' ? action.payload : {}
|
||||
const targetSessionType = String(actionPayload.session_type || '').trim()
|
||||
if (!targetSessionType) return
|
||||
const carryText = String(actionPayload.carry_text || '').trim()
|
||||
const carryFiles = actionPayload.carry_files ? Array.from(attachedFiles.value || []) : []
|
||||
if (!lockSuggestedActionMessage(message, action)) return
|
||||
await switchSessionType(targetSessionType)
|
||||
if (carryText) {
|
||||
composerDraft.value = carryText
|
||||
}
|
||||
if (carryFiles.length) {
|
||||
const fileMergeResult = mergeFilesWithLimit([], carryFiles, MAX_ATTACHMENTS)
|
||||
attachedFiles.value = fileMergeResult.files
|
||||
composerFilesExpanded.value = fileMergeResult.files.length > VISIBLE_ATTACHMENT_CHIPS
|
||||
}
|
||||
nextTick(() => {
|
||||
adjustComposerTextareaHeight()
|
||||
scrollToBottom()
|
||||
})
|
||||
persistSessionState()
|
||||
return
|
||||
}
|
||||
|
||||
if (actionType === 'confirm_expense_intent') {
|
||||
const originalMessage = String(action?.payload?.original_message || message?.text || '').trim()
|
||||
if (!originalMessage) return
|
||||
@@ -1571,6 +1635,60 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
function openApplicationSubmitConfirm(message) {
|
||||
if (!message) {
|
||||
return
|
||||
}
|
||||
applicationSubmitConfirmDialog.value = {
|
||||
open: true,
|
||||
message
|
||||
}
|
||||
}
|
||||
|
||||
function closeApplicationSubmitConfirm() {
|
||||
if (reviewActionBusy.value) {
|
||||
return
|
||||
}
|
||||
applicationSubmitConfirmDialog.value = {
|
||||
open: false,
|
||||
message: null
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmApplicationSubmit() {
|
||||
const message = applicationSubmitConfirmDialog.value.message
|
||||
if (!message || submitting.value || reviewActionBusy.value) {
|
||||
return
|
||||
}
|
||||
applicationSubmitConfirmDialog.value = {
|
||||
open: false,
|
||||
message: null
|
||||
}
|
||||
reviewActionBusy.value = true
|
||||
try {
|
||||
const payload = await submitComposer({
|
||||
rawText: '确认提交',
|
||||
userText: '确认提交',
|
||||
pendingText: '正在提交费用申请...',
|
||||
systemGenerated: true
|
||||
})
|
||||
const draftPayload = payload?.result?.draft_payload || {}
|
||||
const claimNo = String(draftPayload.claim_no || '').trim()
|
||||
const claimId = String(draftPayload.claim_id || '').trim()
|
||||
if (String(payload?.status || '').trim() === 'succeeded' && (claimNo || claimId)) {
|
||||
emit('draft-saved', {
|
||||
claimId,
|
||||
claimNo,
|
||||
status: 'submitted',
|
||||
approvalStage: String(draftPayload.approval_stage || '直属领导审批').trim(),
|
||||
documentType: 'application'
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
reviewActionBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function isWorkbenchBusy() {
|
||||
return submitting.value || reviewActionBusy.value || sessionSwitchBusy.value
|
||||
}
|
||||
@@ -1796,6 +1914,12 @@ export default {
|
||||
}
|
||||
|
||||
const href = String(anchor.getAttribute('href') || '').trim()
|
||||
if (href === APPLICATION_SUBMIT_HREF) {
|
||||
event.preventDefault()
|
||||
openApplicationSubmitConfirm(message)
|
||||
return
|
||||
}
|
||||
|
||||
if (href === REVIEW_NEXT_STEP_HREF) {
|
||||
event.preventDefault()
|
||||
openReviewNextStepConfirm(message)
|
||||
@@ -1890,16 +2014,16 @@ export default {
|
||||
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,
|
||||
hasInsightPanelContent, showInsightPanel, insightPanelToggleLabel, composerPlaceholder, currentIntentLabel, canDeleteCurrentSession, latestReviewMessage, activeReviewPayload, activeReviewPanelScope, activeReviewFilePreviews, reviewDrawerMode, isReviewOverviewDrawer, isReviewDocumentDrawer, isReviewRiskDrawer, isReviewFlowDrawer,
|
||||
hasInsightPanelContent, showInsightPanel, insightPanelToggleLabel, assistantHeaderTitle, assistantHeaderDescription, 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,
|
||||
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,
|
||||
workbenchVisible, reviewPanelConfidence, reviewRiskSummary, reviewRiskItems, reviewRiskEmpty, recognizedNarratives, reviewRecognitionNotes, reviewDocumentSummaries, reviewDocumentCount, reviewDocumentDirty, reviewHasUnsavedChanges,
|
||||
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, deleteSessionDialogOpen, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
|
||||
travelCalculatorOpen, travelCalculatorBusy, travelCalculatorError, travelCalculatorResult, travelCalculatorForm, travelCalculatorCanSubmit, deleteSessionDialogOpen, applicationSubmitConfirmDialog, nextStepConfirmDialog, reviewActionBusy, deleteSessionBusy, documentPreviewDialog, shortcuts,
|
||||
resolveReviewMissingSlotCards, resolveReviewRiskBriefs, buildReviewHeadline, buildReviewSubline, buildReviewStateLabel, buildReviewStateTone, buildReviewPlainFollowupCopy, buildReviewPlainFollowupForMessage, buildReviewNextStepRichCopyForMessage, buildMessageBubbleClass, resolveReviewFooterActions, resolveReviewSaveDraftAction, buildReviewPrimaryButtonLabel, buildReviewMainMessageText,
|
||||
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,
|
||||
requestCloseWorkbench, emitCloseAfterLeave, handleAssistantModalAfterEnter, openExpenseQueryRecord, handleExpenseQueryRecordClick, setExpenseQueryPage, shiftExpenseQueryPage, openDeleteSessionDialog, closeDeleteSessionDialog, confirmDeleteCurrentSession, openInlineReviewEditor, closeInlineReviewEditor, commitInlineReviewEditor, clearInlineReviewFieldError, selectInlineScene, selectReviewCategory, selectReviewOtherCategory,
|
||||
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
|
||||
queryDraftByClaimNo, appendReviewRiskBriefToConversation, appendExpenseQueryRiskToConversation, goReviewDocument, openActiveReviewDocumentPreview, closeDocumentPreview, saveInlineReviewChanges, submitComposer, handleAssistantMarkdownClick, handleReviewAction, handleSaveDraftDirectly, closeApplicationSubmitConfirm, confirmApplicationSubmit, closeReviewNextStepConfirm, confirmReviewNextStepSubmit, isDraftSavedReviewMessage, canUseInlineSaveDraft, handleInlineSaveDraft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user