refactor: consolidate finance workflow modules

This commit is contained in:
caoxiaozhu
2026-06-23 11:21:18 +08:00
parent 1f40ce3df3
commit 73966b3a7b
52 changed files with 3468 additions and 2865 deletions

View File

@@ -28,183 +28,11 @@
<p>您可以直接向我提问,或选择下方推荐主题,快速完成费用相关事务</p>
</div>
<form class="workbench-ai-composer" @submit.prevent="submitAiModePrompt">
<div class="workbench-ai-composer-field">
<div v-if="workbenchDateTagLabel" class="workbench-ai-date-chip">
<i class="mdi mdi-calendar-check-outline"></i>
<span>{{ workbenchDateTagLabel }}</span>
<button type="button" aria-label="移除日期" :disabled="isAiModeInputLocked" @click="removeWorkbenchDateTag">
<i class="mdi mdi-close"></i>
</button>
</div>
<textarea
ref="assistantInputRef"
v-model="assistantDraft"
maxlength="1000"
rows="3"
:placeholder="isAiModeInputLocked ? '费用测算中,请稍等...' : '今天我能帮您做点什么?'"
:disabled="isAiModeInputLocked"
@keydown.enter.exact.prevent="submitAiModePrompt"
></textarea>
</div>
<div class="workbench-ai-composer-toolbar">
<div class="workbench-ai-tool-buttons">
<div class="workbench-date-anchor workbench-ai-date-anchor">
<button
type="button"
class="workbench-ai-icon-btn"
:class="{ active: workbenchDatePickerOpen || workbenchDateTagLabel }"
title="选择日期"
aria-label="选择日期"
:aria-expanded="workbenchDatePickerOpen"
:disabled="isAiModeInputLocked"
@click.stop="toggleWorkbenchDatePicker"
>
<i class="mdi mdi-calendar-range"></i>
</button>
<div
v-if="workbenchDatePickerOpen"
class="workbench-ai-date-popover"
role="dialog"
aria-label="选择业务日期"
@click.stop
>
<div class="workbench-ai-date-tabs" role="tablist" aria-label="日期模式">
<button
type="button"
:class="{ active: workbenchDateMode === 'single' }"
:disabled="isAiModeInputLocked"
@click="setWorkbenchDateMode('single')"
>
单日
</button>
<button
type="button"
:class="{ active: workbenchDateMode === 'range' }"
:disabled="isAiModeInputLocked"
@click="setWorkbenchDateMode('range')"
>
范围
</button>
</div>
<label v-if="workbenchDateMode === 'single'" class="workbench-ai-date-field">
<span>业务日期</span>
<input
v-model="workbenchSingleDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('single')"
/>
</label>
<div v-else class="workbench-ai-date-range">
<label class="workbench-ai-date-field">
<span>开始日期</span>
<input
v-model="workbenchRangeStartDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('range-start')"
/>
</label>
<label class="workbench-ai-date-field">
<span>结束日期</span>
<input
v-model="workbenchRangeEndDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('range-end')"
/>
</label>
</div>
<div class="workbench-ai-date-actions">
<button type="button" class="ghost" :disabled="isAiModeInputLocked" @click="clearWorkbenchDateSelection">清除</button>
<button
type="button"
class="primary"
:disabled="!workbenchCanApplyDateSelection || isAiModeInputLocked"
@click="applyWorkbenchDateSelection"
>
完成
</button>
</div>
</div>
</div>
<button
type="button"
class="workbench-ai-icon-btn"
title="上传附件"
aria-label="上传附件"
:disabled="isAiModeInputLocked"
@click="triggerAiModeFileUpload"
>
<i class="mdi mdi-paperclip"></i>
</button>
<button
type="button"
class="workbench-ai-icon-btn"
title="语音输入"
aria-label="语音输入"
:disabled="isAiModeInputLocked"
@click="handleVoiceInput"
>
<i class="mdi mdi-microphone-outline"></i>
</button>
</div>
<div class="workbench-ai-composer-right">
<div class="workbench-ai-model-selector" :title="modelSelectorTitle">
<span>{{ displayModelName }}</span>
<i class="mdi mdi-chevron-down"></i>
</div>
<button
type="submit"
class="workbench-ai-send-btn"
:disabled="!canSubmitAiModePrompt || sending || isAiModeInputLocked"
aria-label="发送给小财管家"
>
<i class="mdi mdi-arrow-up"></i>
</button>
</div>
</div>
</form>
<div v-if="selectedFileCards.length" class="workbench-ai-file-strip" aria-label="已选择附件">
<article v-for="file in selectedFileCards" :key="file.key" class="workbench-ai-file-card">
<span class="workbench-ai-file-card__icon" :class="`type-${file.tone}`" aria-hidden="true">
<i :class="file.icon"></i>
</span>
<span class="workbench-ai-file-card__body">
<strong :title="file.name">{{ file.name }}</strong>
<small>{{ file.typeLabel }}</small>
<small
v-if="file.ocrState?.label"
class="workbench-ai-file-card__ocr"
:class="`is-${file.ocrState.status || 'idle'}`"
:title="file.ocrState.title || file.ocrState.label"
>
<i v-if="file.ocrState.status === 'recognizing'" class="mdi mdi-loading"></i>
<i v-else-if="file.ocrState.status === 'recognized'" class="mdi mdi-text-recognition"></i>
<i v-else-if="file.ocrState.status === 'failed'" class="mdi mdi-alert-circle-outline"></i>
<span>{{ file.ocrState.label }}</span>
</small>
</span>
<button
type="button"
class="workbench-ai-file-card__remove"
:disabled="isAiModeInputLocked"
:aria-label="`移除附件 ${file.name}`"
@click="removeAiModeFile(file.key)"
>
<i class="mdi mdi-close"></i>
</button>
</article>
</div>
<WorkbenchAiComposer
:runtime="workbenchAiRuntime"
placeholder="今天我能帮您做点什么?"
/>
<WorkbenchAiFileStrip :runtime="workbenchAiRuntime" />
<div class="workbench-ai-quick-start-section">
<h3 class="workbench-ai-quick-start-title">快速开始</h3>
@@ -565,183 +393,12 @@
</div>
<div class="workbench-ai-conversation-bottom">
<div v-if="selectedFileCards.length" class="workbench-ai-file-strip inline" aria-label="已选择附件">
<article v-for="file in selectedFileCards" :key="file.key" class="workbench-ai-file-card">
<span class="workbench-ai-file-card__icon" :class="`type-${file.tone}`" aria-hidden="true">
<i :class="file.icon"></i>
</span>
<span class="workbench-ai-file-card__body">
<strong :title="file.name">{{ file.name }}</strong>
<small>{{ file.typeLabel }}</small>
<small
v-if="file.ocrState?.label"
class="workbench-ai-file-card__ocr"
:class="`is-${file.ocrState.status || 'idle'}`"
:title="file.ocrState.title || file.ocrState.label"
>
<i v-if="file.ocrState.status === 'recognizing'" class="mdi mdi-loading"></i>
<i v-else-if="file.ocrState.status === 'recognized'" class="mdi mdi-text-recognition"></i>
<i v-else-if="file.ocrState.status === 'failed'" class="mdi mdi-alert-circle-outline"></i>
<span>{{ file.ocrState.label }}</span>
</small>
</span>
<button
type="button"
class="workbench-ai-file-card__remove"
:disabled="isAiModeInputLocked"
:aria-label="`移除附件 ${file.name}`"
@click="removeAiModeFile(file.key)"
>
<i class="mdi mdi-close"></i>
</button>
</article>
</div>
<form class="workbench-ai-composer workbench-ai-composer--inline" @submit.prevent="submitAiModePrompt">
<div class="workbench-ai-composer-field">
<div v-if="workbenchDateTagLabel" class="workbench-ai-date-chip">
<i class="mdi mdi-calendar-check-outline"></i>
<span>{{ workbenchDateTagLabel }}</span>
<button type="button" aria-label="移除日期" :disabled="isAiModeInputLocked" @click="removeWorkbenchDateTag">
<i class="mdi mdi-close"></i>
</button>
</div>
<textarea
ref="assistantInputRef"
v-model="assistantDraft"
maxlength="1000"
rows="3"
:placeholder="isAiModeInputLocked ? '费用测算中,请稍等...' : '继续和小财管家对话...'"
:disabled="isAiModeInputLocked"
@keydown.enter.exact.prevent="submitAiModePrompt"
></textarea>
</div>
<div class="workbench-ai-composer-toolbar">
<div class="workbench-ai-tool-buttons">
<div class="workbench-date-anchor workbench-ai-date-anchor">
<button
type="button"
class="workbench-ai-icon-btn"
:class="{ active: workbenchDatePickerOpen || workbenchDateTagLabel }"
title="选择日期"
aria-label="选择日期"
:aria-expanded="workbenchDatePickerOpen"
:disabled="isAiModeInputLocked"
@click.stop="toggleWorkbenchDatePicker"
>
<i class="mdi mdi-calendar-range"></i>
</button>
<div
v-if="workbenchDatePickerOpen"
class="workbench-ai-date-popover"
role="dialog"
aria-label="选择业务日期"
@click.stop
>
<div class="workbench-ai-date-tabs" role="tablist" aria-label="日期模式">
<button
type="button"
:class="{ active: workbenchDateMode === 'single' }"
:disabled="isAiModeInputLocked"
@click="setWorkbenchDateMode('single')"
>
单日
</button>
<button
type="button"
:class="{ active: workbenchDateMode === 'range' }"
:disabled="isAiModeInputLocked"
@click="setWorkbenchDateMode('range')"
>
范围
</button>
</div>
<label v-if="workbenchDateMode === 'single'" class="workbench-ai-date-field">
<span>业务日期</span>
<input
v-model="workbenchSingleDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('single')"
/>
</label>
<div v-else class="workbench-ai-date-range">
<label class="workbench-ai-date-field">
<span>开始日期</span>
<input
v-model="workbenchRangeStartDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('range-start')"
/>
</label>
<label class="workbench-ai-date-field">
<span>结束日期</span>
<input
v-model="workbenchRangeEndDate"
type="date"
:disabled="isAiModeInputLocked"
@change="handleWorkbenchDateInputChange('range-end')"
/>
</label>
</div>
<div class="workbench-ai-date-actions">
<button type="button" class="ghost" :disabled="isAiModeInputLocked" @click="clearWorkbenchDateSelection">清除</button>
<button
type="button"
class="primary"
:disabled="!workbenchCanApplyDateSelection || isAiModeInputLocked"
@click="applyWorkbenchDateSelection"
>
完成
</button>
</div>
</div>
</div>
<button
type="button"
class="workbench-ai-icon-btn"
title="上传附件"
aria-label="上传附件"
:disabled="isAiModeInputLocked"
@click="triggerAiModeFileUpload"
>
<i class="mdi mdi-paperclip"></i>
</button>
<button
type="button"
class="workbench-ai-icon-btn"
title="语音输入"
aria-label="语音输入"
:disabled="isAiModeInputLocked"
@click="handleVoiceInput"
>
<i class="mdi mdi-microphone-outline"></i>
</button>
</div>
<div class="workbench-ai-composer-right">
<div class="workbench-ai-model-selector" :title="modelSelectorTitle">
<span>{{ displayModelName }}</span>
<i class="mdi mdi-chevron-down"></i>
</div>
<button
type="submit"
class="workbench-ai-send-btn"
:disabled="!canSubmitAiModePrompt || sending || isAiModeInputLocked"
aria-label="发送给小财管家"
>
<i class="mdi mdi-arrow-up"></i>
</button>
</div>
</div>
</form>
<WorkbenchAiFileStrip inline :runtime="workbenchAiRuntime" />
<WorkbenchAiComposer
inline
:runtime="workbenchAiRuntime"
placeholder="继续和小财管家对话..."
/>
<p class="workbench-ai-disclaimer">小财管家可能会出错,重要费用事项请核对单据、制度与审批结果。</p>
</div>

View File

@@ -1,7 +1,10 @@
<template src="./PersonalWorkbenchAiMode.template.html"></template>
<script setup>
import { proxyRefs } from 'vue'
import orbIcon from '../../assets/workbench-ai-mode-orb-icon.gif'
import WorkbenchAiComposer from './workbench-ai/WorkbenchAiComposer.vue'
import WorkbenchAiFileStrip from './workbench-ai/WorkbenchAiFileStrip.vue'
import { usePersonalWorkbenchAiMode } from '../../composables/workbenchAiMode/usePersonalWorkbenchAiMode.js'
const props = defineProps({
@@ -9,9 +12,12 @@ const props = defineProps({
})
const emit = defineEmits(['conversation-change', 'conversation-history-change', 'open-document', 'request-updated'])
const aiModeRuntime = usePersonalWorkbenchAiMode(props, emit)
const workbenchAiRuntime = proxyRefs(aiModeRuntime)
const {
activeConversationTitle, aiModeActionItems, applicationPreviewEditor, applicationSubmitConfirmOpen, assistantDraft, assistantInputRef, canShowInlineSuggestedActions, canSubmitAiModePrompt, cancelDeleteConversation, cancelInlineApplicationSubmitConfirm, clearWorkbenchDateSelection, commitInlineApplicationPreviewEditor, confirmDeleteConversation, confirmInlineApplicationSubmit, conversationMessages, conversationScrollRef, conversationStarted, deleteDialogOpen, displayModelName, displayUserName, fileInputRef, handleAiAnswerMarkdownClick, handleAiModeFilesChange, handleInlineApplicationPreviewEditorKeydown, handleInlineConversationScroll, handleInlineSuggestedAction, handleVoiceInput, hasInlineAttachmentOcrDetails, hasInlineThinking, isAiModeInputLocked, isApplicationPreviewEditing, isApplicationPreviewEstimatePending, isInlineAttachmentOcrExpanded, isInlineSuggestedActionDisabled, isInlineThinkingExpanded, markInlineMessageFeedback, modelSelectorTitle, openApplicationPreviewEditor, quoteInlineMessage, regenerateLastReply, removeAiModeFile, removeWorkbenchDateTag, renderInlineConversationHtml, requestDeleteCurrentConversation, resolveApplicationPreviewEditorOptions, resolveInlineApplicationPreviewEditorControl, resolveInlineApplicationPreviewEditorDateMax, resolveInlineApplicationPreviewEditorDateMin, resolveInlineApplicationPreviewMissingFields, resolveInlineApplicationPreviewRows, resolveInlineAttachmentOcrDocuments, resolveInlineAttachmentOcrFileCount, resolveInlineThinkingEvents, runAiModeAction, scrollInlineConversationToTop, selectedFileCards, sending, setWorkbenchDateMode, submitAiModePrompt, toggleInlineAttachmentOcrDetails, toggleInlineThinking, toggleWorkbenchDatePicker, triggerAiModeFileUpload, workbenchCanApplyDateSelection, workbenchDateMode, workbenchDatePickerOpen, workbenchDateTagLabel, workbenchRangeEndDate, workbenchRangeStartDate, workbenchSingleDate, applyWorkbenchDateSelection, buildInlineApplicationPreviewFooterText, copyInlineMessage, formatMessageTime, handleWorkbenchDateInputChange
} = usePersonalWorkbenchAiMode(props, emit)
} = aiModeRuntime
</script>
<style scoped src="../../assets/styles/components/personal-workbench-ai-mode.css"></style>

View File

@@ -12,6 +12,17 @@
<span class="workbench-ai-file-card__body">
<strong :title="file.name">{{ file.name }}</strong>
<small>{{ file.typeLabel }}</small>
<small
v-if="file.ocrState?.label"
class="workbench-ai-file-card__ocr"
:class="`is-${file.ocrState.status || 'idle'}`"
:title="file.ocrState.title || file.ocrState.label"
>
<i v-if="file.ocrState.status === 'recognizing'" class="mdi mdi-loading"></i>
<i v-else-if="file.ocrState.status === 'recognized'" class="mdi mdi-text-recognition"></i>
<i v-else-if="file.ocrState.status === 'failed'" class="mdi mdi-alert-circle-outline"></i>
<span>{{ file.ocrState.label }}</span>
</small>
</span>
<button
type="button"