From 0f5280656919256310e6711570d99e5c3370a915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E8=AF=9A?= Date: Mon, 29 Dec 2025 21:17:18 +0800 Subject: [PATCH] fix --- Web/src/components/SmartClassifyButton.vue | 214 ++++++++++++--------- Web/src/views/StatisticsView.vue | 93 ++++++++- 2 files changed, 208 insertions(+), 99 deletions(-) diff --git a/Web/src/components/SmartClassifyButton.vue b/Web/src/components/SmartClassifyButton.vue index 3158693..f1ea0a7 100644 --- a/Web/src/components/SmartClassifyButton.vue +++ b/Web/src/components/SmartClassifyButton.vue @@ -27,10 +27,14 @@ const props = defineProps({ transactions: { type: Array, default: () => [] + }, + onBeforeClassify: { + type: Function, + default: null } }) -const emit = defineEmits(['update', 'save']) +const emit = defineEmits(['update', 'save', 'beforeClassify']) const loading = ref(false) const saving = ref(false) @@ -124,7 +128,10 @@ const handleSmartClassify = async () => { // 清空之前的分类结果 classifiedResults.value = [] - const transactionIds = props.transactions.map(t => t.id) + const allTransactions = props.transactions + const totalCount = allTransactions.length + const batchSize = 30 + let processedCount = 0 try { loading.value = true @@ -132,7 +139,16 @@ const handleSmartClassify = async () => { if (toastInstance) { closeToast() } - + + // 等待父组件的 beforeClassify 事件处理完成(如果有返回 Promise) TODO 没有生效 + if (props.onBeforeClassify) { + const shouldContinue = await props.onBeforeClassify() + if (shouldContinue === false) { + loading.value = false + return + } + } + toastInstance = showToast({ message: '正在智能分类...', duration: 0, @@ -140,106 +156,120 @@ const handleSmartClassify = async () => { loadingType: 'spinner' }) - const response = await smartClassify(transactionIds) - - if (!response.ok) { - throw new Error('智能分类请求失败') - } + // 分批处理 + for (let i = 0; i < allTransactions.length; i += batchSize) { + const batch = allTransactions.slice(i, i + batchSize) + const transactionIds = batch.map(t => t.id) + const currentBatch = Math.floor(i / batchSize) + 1 + const totalBatches = Math.ceil(allTransactions.length / batchSize) + + // 更新批次进度 + closeToast() + toastInstance = showToast({ + message: `正在处理第 ${currentBatch}/${totalBatches} 批 (${i + 1}-${Math.min(i + batchSize, totalCount)} / ${totalCount})...`, + duration: 0, + forbidClick: true, + loadingType: 'spinner' + }) - // 读取流式响应 - const reader = response.body.getReader() - const decoder = new TextDecoder() - let buffer = '' - let processedCount = 0 + const response = await smartClassify(transactionIds) + + if (!response.ok) { + throw new Error('智能分类请求失败') + } - while (true) { - const { done, value } = await reader.read() - - if (done) break - - buffer += decoder.decode(value, { stream: true }) - - // 处理完整的事件(SSE格式:event: type\ndata: data\n\n) - const events = buffer.split('\n\n') - buffer = events.pop() || '' // 保留最后一个不完整的部分 - - for (const eventBlock of events) { - if (!eventBlock.trim()) continue + // 读取流式响应 + const reader = response.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + while (true) { + const { done, value } = await reader.read() - try { - const lines = eventBlock.split('\n') - let eventType = '' - let eventData = '' + if (done) break + + buffer += decoder.decode(value, { stream: true }) + + // 处理完整的事件(SSE格式:event: type\ndata: data\n\n) + const events = buffer.split('\n\n') + buffer = events.pop() || '' // 保留最后一个不完整的部分 + + for (const eventBlock of events) { + if (!eventBlock.trim()) continue - for (const line of lines) { - if (line.startsWith('event: ')) { - eventType = line.slice(7).trim() - } else if (line.startsWith('data: ')) { - eventData = line.slice(6).trim() - } - } - - if (eventType === 'start') { - // 开始分类 - closeToast() - toastInstance = showToast({ - message: eventData, - duration: 0, - forbidClick: true, - loadingType: 'spinner' - }) - } else if (eventType === 'data') { - // 收到分类结果 - const data = JSON.parse(eventData) - processedCount++ + try { + const lines = eventBlock.split('\n') + let eventType = '' + let eventData = '' - // 记录分类结果 - classifiedResults.value.push({ - id: data.id, - classify: data.Classify, - type: data.Type - }) - - // 实时更新交易记录的分类信息 - const index = props.transactions.findIndex(t => t.id === data.id) - if (index !== -1) { - const transaction = props.transactions[index] - transaction.upsetedClassify = data.Classify - transaction.upsetedType = data.Type + for (const line of lines) { + if (line.startsWith('event: ')) { + eventType = line.slice(7).trim() + } else if (line.startsWith('data: ')) { + eventData = line.slice(6).trim() + } } - // 更新进度 - closeToast() - toastInstance = showToast({ - message: `已分类 ${processedCount} 条`, - duration: 0, - forbidClick: true, - loadingType: 'spinner' - }) - } else if (eventType === 'end') { - // 分类完成 - closeToast() - toastInstance = null - showToast({ - type: 'success', - message: `分类完成,请点击"保存分类"按钮保存结果`, - duration: 3000 - }) - } else if (eventType === 'error') { - // 处理错误 - closeToast() - toastInstance = null - showToast({ - type: 'fail', - message: eventData || '分类失败', - duration: 2000 - }) + if (eventType === 'start') { + // 开始分类 + closeToast() + toastInstance = showToast({ + message: `${eventData} (批次 ${currentBatch}/${totalBatches})`, + duration: 0, + forbidClick: true, + loadingType: 'spinner' + }) + } else if (eventType === 'data') { + // 收到分类结果 + const data = JSON.parse(eventData) + processedCount++ + + // 记录分类结果 + classifiedResults.value.push({ + id: data.id, + classify: data.Classify, + type: data.Type + }) + + // 实时更新交易记录的分类信息 + const index = props.transactions.findIndex(t => t.id === data.id) + if (index !== -1) { + const transaction = props.transactions[index] + transaction.upsetedClassify = data.Classify + transaction.upsetedType = data.Type + } + + // 更新进度 + closeToast() + toastInstance = showToast({ + message: `已分类 ${processedCount}/${totalCount} 条 (批次 ${currentBatch}/${totalBatches})...`, + duration: 0, + forbidClick: true, + loadingType: 'spinner' + }) + } else if (eventType === 'end') { + // 当前批次完成 + console.log(`批次 ${currentBatch}/${totalBatches} 完成`) + } else if (eventType === 'error') { + // 处理错误 + throw new Error(eventData || '分类失败') + } + } catch (e) { + console.error('解析SSE事件失败:', e, eventBlock) + throw e } - } catch (e) { - console.error('解析SSE事件失败:', e, eventBlock) } } } + + // 所有批次完成 + closeToast() + toastInstance = null + showToast({ + type: 'success', + message: `分类完成,共处理 ${processedCount} 条记录,请点击"保存分类"按钮保存结果`, + duration: 3000 + }) } catch (error) { console.error('智能分类失败:', error) closeToast() diff --git a/Web/src/views/StatisticsView.vue b/Web/src/views/StatisticsView.vue index ea106f2..872a114 100644 --- a/Web/src/views/StatisticsView.vue +++ b/Web/src/views/StatisticsView.vue @@ -286,11 +286,20 @@ >