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

@@ -1,5 +1,11 @@
import { isApplicationDocumentNo } from '../../utils/documentClassification.js'
import { filterActionableRiskFlags, normalizeRiskFlagTone } from '../../utils/riskFlags.js'
import {
DOCUMENT_TYPE_APPLICATION,
DOCUMENT_TYPE_EXPENSE_APPLICATION,
DOCUMENT_TYPE_REIMBURSEMENT,
resolveDocumentTypeLabel
} from '../../constants/documentProtocol.js'
const EXPENSE_TYPE_LABELS = {
travel: '差旅费',
@@ -49,8 +55,6 @@ const DOCUMENT_BACKED_EXPENSE_TYPES = new Set([
'hotel_ticket',
'ride_ticket'
])
const DOCUMENT_TYPE_APPLICATION = 'application'
const DOCUMENT_TYPE_REIMBURSEMENT = 'reimbursement'
const RELATED_APPLICATION_STEP_LABEL = '关联单据'
const APPLICATION_LINK_STATUS_STEP_LABEL = '关联单据状态'
const APPLICATION_ARCHIVE_STAGE_LABEL = '申请归档'
@@ -179,14 +183,14 @@ function resolveDocumentTypeMeta(claim, typeCode) {
const normalizedType = String(typeCode || '').trim()
const isApplication =
explicitType === DOCUMENT_TYPE_APPLICATION
|| explicitType === 'expense_application'
|| explicitType === DOCUMENT_TYPE_EXPENSE_APPLICATION
|| isApplicationDocumentNo(claimNo)
|| normalizedType === 'application'
|| normalizedType.endsWith('_application')
return isApplication
? { documentTypeCode: DOCUMENT_TYPE_APPLICATION, documentTypeLabel: '申请单' }
: { documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT, documentTypeLabel: '报销单' }
? { documentTypeCode: DOCUMENT_TYPE_APPLICATION, documentTypeLabel: resolveDocumentTypeLabel(DOCUMENT_TYPE_APPLICATION) }
: { documentTypeCode: DOCUMENT_TYPE_REIMBURSEMENT, documentTypeLabel: resolveDocumentTypeLabel(DOCUMENT_TYPE_REIMBURSEMENT) }
}
function normalizeExpenseType(typeCode) {

View File

@@ -139,7 +139,14 @@ export function useTopBarNotificationStates() {
}
function markNotificationStateRead(item) {
return syncNotificationPatches([buildPatch(item, { read: true })])
return markNotificationStatesRead([item])
}
function markNotificationStatesRead(items) {
const patches = (Array.isArray(items) ? items : [])
.map((item) => buildPatch(item, { read: true, hidden: false }))
.filter(Boolean)
return syncNotificationPatches(patches)
}
function hideNotificationStates(items) {
@@ -158,6 +165,7 @@ export function useTopBarNotificationStates() {
isNotificationHidden,
isNotificationRead,
loadNotificationStates,
markNotificationStateRead
markNotificationStateRead,
markNotificationStatesRead
}
}

View File

@@ -24,8 +24,7 @@ import {
} from './workbenchAiComposerModel.js'
import {
createWorkbenchAiMessageRuntime,
formatMessageTime,
normalizeInlineAttachmentOcrDetails
formatMessageTime
} from './workbenchAiMessageModel.js'
import { useWorkbenchAiActionRouter } from './useWorkbenchAiActionRouter.js'
import { useWorkbenchAiAttachmentAssociationFlow } from './useWorkbenchAiAttachmentAssociationFlow.js'
@@ -34,8 +33,10 @@ import { useWorkbenchAiComposerFiles } from './useWorkbenchAiComposerFiles.js'
import { useWorkbenchAiDocumentQueryFlow } from './useWorkbenchAiDocumentQueryFlow.js'
import { useWorkbenchAiExpenseFlow } from './useWorkbenchAiExpenseFlow.js'
import { useWorkbenchAiMessageActions } from './useWorkbenchAiMessageActions.js'
import { useWorkbenchAiMessageExpansion } from './useWorkbenchAiMessageExpansion.js'
import { useWorkbenchAiSessionCommands } from './useWorkbenchAiSessionCommands.js'
import { useWorkbenchAiStewardFlow } from './useWorkbenchAiStewardFlow.js'
import { isReimbursementCreationIntent } from './workbenchAiApplicationGateModel.js'
const AI_SEARCH_CONVERSATION_ID = 'ai-search'
const INLINE_ANSWER_STREAM_CHUNK_SIZE = 6
@@ -174,6 +175,23 @@ export function usePersonalWorkbenchAiMode(props, emit) {
...card,
ocrState: attachmentFlow.resolveAiModeReceiptRecognitionState(selectedFiles.value[index])
})))
const {
hasInlineAttachmentOcrDetails,
hasInlineThinking,
isInlineAttachmentOcrExpanded,
isInlineThinkingExpanded,
resolveInlineAttachmentOcrDocuments,
resolveInlineAttachmentOcrFileCount,
resolveInlineThinkingEvents,
toggleInlineAttachmentOcrDetails,
toggleInlineThinking
} = useWorkbenchAiMessageExpansion({
attachmentOcrExpandedMessageIds,
inlineConversationAutoScrollPinned,
scrollInlineConversationToBottom,
thinkingCollapsedMessageIds,
thinkingExpandedMessageIds
})
const applicationFlow = useWorkbenchAiApplicationPreviewFlow({
activateInlineConversation,
@@ -324,6 +342,10 @@ export function usePersonalWorkbenchAiMode(props, emit) {
})
}
function setAssistantInputRef(element) {
assistantInputRef.value = element
}
function isInlineConversationNearBottom() {
const el = conversationScrollRef.value
if (!el) {
@@ -499,78 +521,6 @@ export function usePersonalWorkbenchAiMode(props, emit) {
return renderAiConversationHtml(content)
}
function resolveInlineThinkingEvents(message) {
return Array.isArray(message?.stewardPlan?.thinkingEvents) ? message.stewardPlan.thinkingEvents : []
}
function hasInlineThinking(message) {
return resolveInlineThinkingEvents(message).length > 0
}
function isInlineThinkingExpanded(message) {
if (!message?.id) {
return Boolean(message?.pending)
}
if (thinkingCollapsedMessageIds.value.has(message.id)) {
return false
}
return Boolean(message.pending || thinkingExpandedMessageIds.value.has(message.id))
}
function toggleInlineThinking(message) {
if (!message?.id) {
return
}
const nextExpandedIds = new Set(thinkingExpandedMessageIds.value)
const nextCollapsedIds = new Set(thinkingCollapsedMessageIds.value)
if (isInlineThinkingExpanded(message)) {
nextExpandedIds.delete(message.id)
nextCollapsedIds.add(message.id)
} else {
nextCollapsedIds.delete(message.id)
nextExpandedIds.add(message.id)
}
thinkingExpandedMessageIds.value = nextExpandedIds
thinkingCollapsedMessageIds.value = nextCollapsedIds
}
function hasInlineAttachmentOcrDetails(message = {}) {
const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)
return Boolean(details?.documents?.length || details?.fileNames?.length)
}
function resolveInlineAttachmentOcrDocuments(message = {}) {
return normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)?.documents || []
}
function resolveInlineAttachmentOcrFileCount(message = {}) {
const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)
return Math.max(details?.documents?.length || 0, details?.fileNames?.length || 0)
}
function isInlineAttachmentOcrExpanded(message = {}) {
return Boolean(message?.id && attachmentOcrExpandedMessageIds.value.has(message.id))
}
function toggleInlineAttachmentOcrDetails(message = {}, forceExpanded = null) {
if (!message?.id || !hasInlineAttachmentOcrDetails(message)) {
return
}
const nextExpandedIds = new Set(attachmentOcrExpandedMessageIds.value)
const shouldExpand = forceExpanded === null
? !nextExpandedIds.has(message.id)
: Boolean(forceExpanded)
if (shouldExpand) {
nextExpandedIds.add(message.id)
} else {
nextExpandedIds.delete(message.id)
}
attachmentOcrExpandedMessageIds.value = nextExpandedIds
nextTick(() => {
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
})
}
function buildInlinePromptText(rawPrompt, files = []) {
const prompt = buildWorkbenchPromptText(rawPrompt)
if (prompt) {
@@ -579,20 +529,6 @@ export function usePersonalWorkbenchAiMode(props, emit) {
return files.length ? '请帮我处理已上传的附件。' : ''
}
function isReimbursementCreationIntent(prompt = '') {
const compact = String(prompt || '').replace(/\s+/g, '')
if (!compact || !/报销|报账/.test(compact)) {
return false
}
if (/标准|制度|规则|政策|口径|怎么|如何|能不能|可以吗|查一下|查询|状态|进度|多少钱|多少/.test(compact)) {
return false
}
return (
/^(我要|我想|我需要|帮我|请帮我|需要|发起|新建|创建|提交|办理|走).{0,16}(报销|报账)/.test(compact) ||
/^(报销|报账)(一下|一笔|单|流程)?$/.test(compact)
)
}
function handleAiAnswerMarkdownClick(event) {
const target = event?.target
const link = target?.closest?.('a[href^="#ai-open-document-detail:"], a[href^="#ai-open-application-detail:"]')
@@ -800,6 +736,7 @@ export function usePersonalWorkbenchAiMode(props, emit) {
scrollInlineConversationToTop,
selectedFileCards,
sending,
setAssistantInputRef,
setWorkbenchDateMode,
submitAiModePrompt,
toggleInlineAttachmentOcrDetails,

View File

@@ -24,10 +24,15 @@ import {
buildInlineApplicationSubmitPrecheckPayload,
buildInlineApplicationSubmitThinkingEvents,
completeInlineThinkingEvents,
extractInlineApplicationDraftPayload,
resolveInlineApplicationPreviewActionFromText
extractInlineApplicationDraftPayload
} from './workbenchAiApplicationPreviewModel.js'
import { AI_ATTACHMENT_ASSOCIATION_CONFIRM_ACTION } from './workbenchAiMessageModel.js'
import {
isOrphanInlineApplicationPreviewMessage,
resolveInlineApplicationPreviewTextAction,
resolveLatestApplicationPreviewMessage,
resolveLatestOrphanApplicationPreviewMessage
} from './workbenchAiApplicationGateModel.js'
function isApplicationPreviewEstimatePendingPreview(applicationPreview = {}) {
const fields = normalizeApplicationPreview(applicationPreview).fields || {}
@@ -197,23 +202,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
].join('\n\n')
}
function resolveLatestApplicationPreviewMessage() {
return [...conversationMessages.value]
.reverse()
.find((message) => message.role === 'assistant' && message.applicationPreview)
function resolveLatestInlineApplicationPreviewMessage() {
return resolveLatestApplicationPreviewMessage(conversationMessages.value)
}
function isOrphanInlineApplicationPreviewMessage(message = {}) {
if (message?.applicationPreview || message?.role !== 'assistant') {
return false
}
return /下方表格|申请核对表|点击对应行即可直接编辑/.test(String(message.content || message.text || ''))
}
function resolveLatestOrphanApplicationPreviewMessage() {
return [...conversationMessages.value]
.reverse()
.find((message) => isOrphanInlineApplicationPreviewMessage(message))
function resolveLatestOrphanInlineApplicationPreviewMessage() {
return resolveLatestOrphanApplicationPreviewMessage(conversationMessages.value)
}
function requestInlineApplicationSubmitConfirmation(targetMessage, options = {}) {
@@ -310,7 +304,7 @@ export function useWorkbenchAiApplicationPreviewFlow({
}
async function executeInlineApplicationPreviewAction(actionType, sourceMessage = null, options = {}) {
const targetMessage = sourceMessage?.applicationPreview ? sourceMessage : resolveLatestApplicationPreviewMessage()
const targetMessage = sourceMessage?.applicationPreview ? sourceMessage : resolveLatestInlineApplicationPreviewMessage()
if (!targetMessage?.applicationPreview) {
toast('当前没有可提交的申请表。')
return false
@@ -446,12 +440,12 @@ export function useWorkbenchAiApplicationPreviewFlow({
toast('请等待费用测算完成后再继续操作。')
return true
}
const actionType = resolveInlineApplicationPreviewActionFromText(prompt)
const actionType = resolveInlineApplicationPreviewTextAction(prompt)
if (!actionType) {
return false
}
if (!resolveLatestApplicationPreviewMessage()) {
const orphanPreviewMessage = resolveLatestOrphanApplicationPreviewMessage()
if (!resolveLatestInlineApplicationPreviewMessage()) {
const orphanPreviewMessage = resolveLatestOrphanInlineApplicationPreviewMessage()
if (!orphanPreviewMessage) {
return false
}

View File

@@ -0,0 +1,95 @@
import { nextTick } from 'vue'
import { normalizeInlineAttachmentOcrDetails } from './workbenchAiMessageModel.js'
export function useWorkbenchAiMessageExpansion({
attachmentOcrExpandedMessageIds,
inlineConversationAutoScrollPinned,
scrollInlineConversationToBottom,
thinkingCollapsedMessageIds,
thinkingExpandedMessageIds
}) {
function resolveInlineThinkingEvents(message) {
return Array.isArray(message?.stewardPlan?.thinkingEvents) ? message.stewardPlan.thinkingEvents : []
}
function hasInlineThinking(message) {
return resolveInlineThinkingEvents(message).length > 0
}
function isInlineThinkingExpanded(message) {
if (!message?.id) {
return Boolean(message?.pending)
}
if (thinkingCollapsedMessageIds.value.has(message.id)) {
return false
}
return Boolean(message.pending || thinkingExpandedMessageIds.value.has(message.id))
}
function toggleInlineThinking(message) {
if (!message?.id) {
return
}
const nextExpandedIds = new Set(thinkingExpandedMessageIds.value)
const nextCollapsedIds = new Set(thinkingCollapsedMessageIds.value)
if (isInlineThinkingExpanded(message)) {
nextExpandedIds.delete(message.id)
nextCollapsedIds.add(message.id)
} else {
nextCollapsedIds.delete(message.id)
nextExpandedIds.add(message.id)
}
thinkingExpandedMessageIds.value = nextExpandedIds
thinkingCollapsedMessageIds.value = nextCollapsedIds
}
function hasInlineAttachmentOcrDetails(message = {}) {
const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)
return Boolean(details?.documents?.length || details?.fileNames?.length)
}
function resolveInlineAttachmentOcrDocuments(message = {}) {
return normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)?.documents || []
}
function resolveInlineAttachmentOcrFileCount(message = {}) {
const details = normalizeInlineAttachmentOcrDetails(message?.attachmentOcrDetails || null)
return Math.max(details?.documents?.length || 0, details?.fileNames?.length || 0)
}
function isInlineAttachmentOcrExpanded(message = {}) {
return Boolean(message?.id && attachmentOcrExpandedMessageIds.value.has(message.id))
}
function toggleInlineAttachmentOcrDetails(message = {}, forceExpanded = null) {
if (!message?.id || !hasInlineAttachmentOcrDetails(message)) {
return
}
const nextExpandedIds = new Set(attachmentOcrExpandedMessageIds.value)
const shouldExpand = forceExpanded === null
? !nextExpandedIds.has(message.id)
: Boolean(forceExpanded)
if (shouldExpand) {
nextExpandedIds.add(message.id)
} else {
nextExpandedIds.delete(message.id)
}
attachmentOcrExpandedMessageIds.value = nextExpandedIds
nextTick(() => {
scrollInlineConversationToBottom({ force: inlineConversationAutoScrollPinned.value })
})
}
return {
hasInlineAttachmentOcrDetails,
hasInlineThinking,
isInlineAttachmentOcrExpanded,
isInlineThinkingExpanded,
resolveInlineAttachmentOcrDocuments,
resolveInlineAttachmentOcrFileCount,
resolveInlineThinkingEvents,
toggleInlineAttachmentOcrDetails,
toggleInlineThinking
}
}

View File

@@ -0,0 +1,51 @@
import {
AI_APPLICATION_ACTION_SAVE_DRAFT,
AI_APPLICATION_ACTION_SUBMIT
} from '../../services/aiApplicationPreviewActions.js'
export function isReimbursementCreationIntent(prompt = '') {
const compact = String(prompt || '').replace(/\s+/g, '')
if (!compact || !/报销|报账/.test(compact)) {
return false
}
if (/标准|制度|规则|政策|口径|怎么|如何|能不能|可以吗|查一下|查询|状态|进度|多少钱|多少/.test(compact)) {
return false
}
return (
/^(我要|我想|我需要|帮我|请帮我|需要|发起|新建|创建|提交|办理|走).{0,16}(报销|报账)/.test(compact) ||
/^(报销|报账)(一下|一笔|单|流程)?$/.test(compact)
)
}
export function resolveInlineApplicationPreviewTextAction(text = '') {
const normalized = String(text || '').replace(/\s+/g, '').trim()
if (!normalized) {
return ''
}
if (/^(保存草稿|保存|存草稿|先保存)$/.test(normalized)) {
return AI_APPLICATION_ACTION_SAVE_DRAFT
}
if (/^(提交|提交申请|确认提交|提交审批|直接提交)$/.test(normalized)) {
return AI_APPLICATION_ACTION_SUBMIT
}
return ''
}
export function resolveLatestApplicationPreviewMessage(messages = []) {
return [...(Array.isArray(messages) ? messages : [])]
.reverse()
.find((message) => message?.role === 'assistant' && message.applicationPreview) || null
}
export function isOrphanInlineApplicationPreviewMessage(message = {}) {
if (message?.applicationPreview || message?.role !== 'assistant') {
return false
}
return /下方表格|申请核对表|点击对应行即可直接编辑/.test(String(message.content || message.text || ''))
}
export function resolveLatestOrphanApplicationPreviewMessage(messages = []) {
return [...(Array.isArray(messages) ? messages : [])]
.reverse()
.find((message) => isOrphanInlineApplicationPreviewMessage(message)) || null
}

View File

@@ -13,19 +13,8 @@ import {
AI_APPLICATION_ACTION_SAVE_DRAFT,
AI_APPLICATION_ACTION_SUBMIT
} from '../../services/aiApplicationPreviewActions.js'
const INLINE_APPLICATION_STATUS_LABELS = {
draft: '草稿',
submitted: '审批中',
pending: '待处理',
approved: '已审批',
completed: '已完成',
archived: '已归档',
returned: '已退回',
rejected: '已驳回',
pending_payment: '待付款',
paid: '已付款'
}
import { INLINE_APPLICATION_STATUS_LABELS } from '../../constants/documentProtocol.js'
import { resolveInlineApplicationPreviewTextAction } from './workbenchAiApplicationGateModel.js'
function normalizeInlineApplicationResultTableCell(value, fallback = '-') {
const text = String(value || '')
@@ -201,17 +190,7 @@ export function buildInlineApplicationDetailAction(draftPayload = {}) {
}
export function resolveInlineApplicationPreviewActionFromText(text = '') {
const normalized = String(text || '').replace(/\s+/g, '').trim()
if (!normalized) {
return ''
}
if (/^(保存草稿|保存|存草稿|先保存)$/.test(normalized)) {
return AI_APPLICATION_ACTION_SAVE_DRAFT
}
if (/^(提交|提交申请|确认提交|提交审批|直接提交)$/.test(normalized)) {
return AI_APPLICATION_ACTION_SUBMIT
}
return ''
return resolveInlineApplicationPreviewTextAction(text)
}
export function normalizeInlineApplicationTypeLabel(expenseTypeLabel, fallback = '费用申请') {