feat(web): AI 工作台文件预览/附件关联任务与草稿分支
- 新增 WorkbenchAiFilePreviewDialog 附件预览对话框及 useWorkbenchAiFilePreview,附件支持点击预览 - 新增 attachmentAssociationJobs/linkedReimbursementDraftJobs 前端服务与对应 composable,接入后台任务轮询与状态展示 - 新增 travelReimbursementDraftBranchModel 草稿分支模型,报销关联门控支持跳过/选择草稿 - PersonalWorkbenchAiMode 及各 composable(expense/document/steward/application-preview/attachment-association)重构适配,WorkbenchAiComposer/FileStrip 样式与交互完善 - DocumentsCenter/ReceiptFolder/TravelReimbursementCreate 等视图及 scripts 重构,风险/差旅规划/审批等工具适配 - 新增/更新前端测试:application-result-card、reimbursement-list-preview-fetch、guided-flow、composer-components 等
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useSystemState } from '../useSystemState.js'
|
||||
import { useToast } from '../useToast.js'
|
||||
import { useWorkbenchComposerDate } from '../useWorkbenchComposerDate.js'
|
||||
@@ -19,7 +18,6 @@ import {
|
||||
} from '../../utils/aiDocumentDetailReference.js'
|
||||
import {
|
||||
AI_MODE_ACTION_ITEMS,
|
||||
buildSelectedFileCards,
|
||||
shouldRunAiAttachmentAutoAssociation
|
||||
} from './workbenchAiComposerModel.js'
|
||||
import {
|
||||
@@ -32,6 +30,7 @@ import { useWorkbenchAiApplicationPreviewFlow } from './useWorkbenchAiApplicatio
|
||||
import { useWorkbenchAiComposerFiles } from './useWorkbenchAiComposerFiles.js'
|
||||
import { useWorkbenchAiDocumentQueryFlow } from './useWorkbenchAiDocumentQueryFlow.js'
|
||||
import { useWorkbenchAiExpenseFlow } from './useWorkbenchAiExpenseFlow.js'
|
||||
import { useWorkbenchAiFilePreview } from './useWorkbenchAiFilePreview.js'
|
||||
import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js'
|
||||
import { useWorkbenchAiMessageExpansion } from './useWorkbenchAiMessageExpansion.js'
|
||||
import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js'
|
||||
@@ -75,7 +74,6 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
normalizeRuntimeMessage,
|
||||
serializeRuntimeMessage
|
||||
} = messageRuntime
|
||||
|
||||
const {
|
||||
applicationPreviewEditor,
|
||||
resolveApplicationPreviewEditorControl,
|
||||
@@ -139,6 +137,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
fileInputRef,
|
||||
focusAiModeInput,
|
||||
isInputLocked: () => isAiModeInputLocked.value,
|
||||
resolveInputLockedMessage: () => resolveAiModeInputLockMessage(),
|
||||
selectedFiles,
|
||||
toast
|
||||
})
|
||||
@@ -154,6 +153,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
|
||||
const attachmentFlow = useWorkbenchAiAttachmentAssociationFlow({
|
||||
aiAttachmentAssociationRuntime,
|
||||
conversationId,
|
||||
conversationMessages,
|
||||
createAiAttachmentAssociationId,
|
||||
createInlineMessage,
|
||||
@@ -167,14 +167,12 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
toast
|
||||
})
|
||||
|
||||
watch(selectedFiles, (files) => {
|
||||
attachmentFlow.primeAiModeReceiptContext(files)
|
||||
const filePreview = useWorkbenchAiFilePreview({
|
||||
attachmentFlow,
|
||||
conversationStarted,
|
||||
scrollInlineConversationToBottom,
|
||||
selectedFiles
|
||||
})
|
||||
|
||||
const selectedFileCards = computed(() => buildSelectedFileCards(selectedFiles.value).map((card, index) => ({
|
||||
...card,
|
||||
ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index])
|
||||
})))
|
||||
const {
|
||||
hasInlineAttachmentOcrDetails,
|
||||
hasInlineThinking,
|
||||
@@ -319,9 +317,13 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
const applicationPreviewEstimatePending = computed(() => (
|
||||
conversationMessages.value.some((message) => applicationFlow.isApplicationPreviewEstimatePending(message))
|
||||
))
|
||||
const isAiModeInputLocked = computed(() => applicationPreviewEstimatePending.value)
|
||||
const isAiModeReceiptRecognitionPending = computed(() => attachmentFlow.hasPendingAiModeReceiptRecognition(selectedFiles.value))
|
||||
const hasAiModeReceiptRecognitionFailure = computed(() => attachmentFlow.hasFailedAiModeReceiptRecognition(selectedFiles.value))
|
||||
const isAiModeInputLocked = computed(() => applicationPreviewEstimatePending.value || isAiModeReceiptRecognitionPending.value)
|
||||
const aiModeInputLockMessage = computed(() => resolveAiModeInputLockMessage())
|
||||
const canSubmitAiModePrompt = computed(() => (
|
||||
!isAiModeInputLocked.value && (
|
||||
!isAiModeInputLocked.value &&
|
||||
!hasAiModeReceiptRecognitionFailure.value && (
|
||||
Boolean(assistantDraft.value.trim()) ||
|
||||
selectedFiles.value.length > 0 ||
|
||||
Boolean(workbenchDateTagLabel.value)
|
||||
@@ -517,18 +519,47 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
emit('conversation-change', { id: conversationId.value, title: activeConversationTitle.value })
|
||||
}
|
||||
|
||||
function renderInlineConversationHtml(content) {
|
||||
return renderAiConversationHtml(content)
|
||||
}
|
||||
function renderInlineConversationHtml(content) { return renderAiConversationHtml(content) }
|
||||
|
||||
function buildInlinePromptText(rawPrompt, files = []) {
|
||||
const prompt = buildWorkbenchPromptText(rawPrompt)
|
||||
if (prompt) {
|
||||
return prompt
|
||||
}
|
||||
if (prompt) return prompt
|
||||
return files.length ? '请帮我处理已上传的附件。' : ''
|
||||
}
|
||||
|
||||
function resolveAiModeInputLockMessage() {
|
||||
if (isAiModeReceiptRecognitionPending.value) {
|
||||
return '附件识别中,请稍等...'
|
||||
}
|
||||
if (applicationPreviewEstimatePending.value) {
|
||||
return '费用测算中,请稍等...'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function resolveAiModeSubmitBlockedMessage() {
|
||||
if (applicationPreviewEstimatePending.value) {
|
||||
return '请等待费用测算完成后再继续操作。'
|
||||
}
|
||||
if (isAiModeReceiptRecognitionPending.value) {
|
||||
return '附件 OCR 识别中,请稍等,识别完成后再继续对话。'
|
||||
}
|
||||
if (hasAiModeReceiptRecognitionFailure.value) {
|
||||
return '请先移除识别失败的附件或重新上传。'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function ensureAiModeCanStartConversation() {
|
||||
const blockedMessage = resolveAiModeSubmitBlockedMessage()
|
||||
if (!blockedMessage) {
|
||||
return true
|
||||
}
|
||||
toast(blockedMessage)
|
||||
focusAiModeInput()
|
||||
return false
|
||||
}
|
||||
|
||||
function handleAiAnswerMarkdownClick(event) {
|
||||
const target = event?.target
|
||||
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
|
||||
@@ -546,8 +577,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
|
||||
function startInlineConversation(prompt, entry = {}, files = []) {
|
||||
if (isAiModeInputLocked.value) {
|
||||
toast('请等待费用测算完成后再继续操作。')
|
||||
if (!ensureAiModeCanStartConversation()) {
|
||||
return
|
||||
}
|
||||
const cleanPrompt = buildInlinePromptText(prompt, files)
|
||||
@@ -595,6 +625,9 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
|
||||
function submitAiModePrompt() {
|
||||
if (!ensureAiModeCanStartConversation()) {
|
||||
return
|
||||
}
|
||||
if (!canSubmitAiModePrompt.value) {
|
||||
toast('请输入需求后再发送。')
|
||||
focusAiModeInput()
|
||||
@@ -604,6 +637,9 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
|
||||
function runAiModeAction(item) {
|
||||
if (!ensureAiModeCanStartConversation()) {
|
||||
return
|
||||
}
|
||||
if (String(item?.label || '').trim() === '发起报销') {
|
||||
void expenseFlow.startAiReimbursementAssociationGate(item.prompt, item.label)
|
||||
return
|
||||
@@ -624,9 +660,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
void stewardFlow.requestInlineAssistantReply(lastUserMessage.content, { source: 'workbench', sessionType: 'steward' }, [])
|
||||
}
|
||||
|
||||
function pushInlineUserMessage(text) {
|
||||
conversationMessages.value.push(createInlineMessage('user', String(text || '').trim()))
|
||||
}
|
||||
function pushInlineUserMessage(text) { conversationMessages.value.push(createInlineMessage('user', String(text || '').trim())) }
|
||||
|
||||
function pushInlineApplicationActionUserMessage(text) {
|
||||
pushInlineUserMessage(text)
|
||||
@@ -637,13 +671,11 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
|
||||
function resolveLatestInlineUserPrompt() {
|
||||
const latestUserMessage = [...conversationMessages.value].reverse().find((message) => message.role === 'user')
|
||||
return String(latestUserMessage?.content || '').trim()
|
||||
return String([...conversationMessages.value].reverse().find((message) => message.role === 'user')?.content || '').trim()
|
||||
}
|
||||
|
||||
function handleVoiceInput() {
|
||||
if (isAiModeInputLocked.value) {
|
||||
toast('请等待费用测算完成后再继续操作。')
|
||||
if (!ensureAiModeCanStartConversation()) {
|
||||
return
|
||||
}
|
||||
toast('语音输入正在准备中,您可以先输入文字需求。')
|
||||
@@ -664,6 +696,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
}
|
||||
if (command.type === 'open-recent') {
|
||||
sessionCommands.openInlineRecentConversation(command.payload || {})
|
||||
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
|
||||
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -671,6 +705,8 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
onMounted(() => {
|
||||
loadSystemSettings()
|
||||
refreshConversationHistory()
|
||||
attachmentFlow.resumePendingAiAttachmentAssociationJobs()
|
||||
expenseFlow.resumePendingLinkedReimbursementDraftJobs()
|
||||
document.addEventListener('click', handleWorkbenchDatePickerOutside)
|
||||
})
|
||||
|
||||
@@ -681,6 +717,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
return {
|
||||
activeConversationTitle,
|
||||
aiModeActionItems,
|
||||
aiModeInputLockMessage,
|
||||
applicationPreviewEditor,
|
||||
applicationSubmitConfirmOpen,
|
||||
assistantInputRef,
|
||||
@@ -734,7 +771,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
|
||||
resolveInlineThinkingEvents,
|
||||
runAiModeAction,
|
||||
scrollInlineConversationToTop,
|
||||
selectedFileCards,
|
||||
...filePreview,
|
||||
sending,
|
||||
setAssistantInputRef,
|
||||
setWorkbenchDateMode,
|
||||
|
||||
Reference in New Issue
Block a user