fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 15s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s

This commit is contained in:
孙诚
2025-12-29 21:17:18 +08:00
parent b6352d4f6f
commit 0f52806569
2 changed files with 208 additions and 99 deletions

View File

@@ -27,10 +27,14 @@ const props = defineProps({
transactions: { transactions: {
type: Array, type: Array,
default: () => [] default: () => []
},
onBeforeClassify: {
type: Function,
default: null
} }
}) })
const emit = defineEmits(['update', 'save']) const emit = defineEmits(['update', 'save', 'beforeClassify'])
const loading = ref(false) const loading = ref(false)
const saving = ref(false) const saving = ref(false)
@@ -124,7 +128,10 @@ const handleSmartClassify = async () => {
// 清空之前的分类结果 // 清空之前的分类结果
classifiedResults.value = [] 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 { try {
loading.value = true loading.value = true
@@ -132,7 +139,16 @@ const handleSmartClassify = async () => {
if (toastInstance) { if (toastInstance) {
closeToast() closeToast()
} }
// 等待父组件的 beforeClassify 事件处理完成(如果有返回 Promise TODO 没有生效
if (props.onBeforeClassify) {
const shouldContinue = await props.onBeforeClassify()
if (shouldContinue === false) {
loading.value = false
return
}
}
toastInstance = showToast({ toastInstance = showToast({
message: '正在智能分类...', message: '正在智能分类...',
duration: 0, duration: 0,
@@ -140,106 +156,120 @@ const handleSmartClassify = async () => {
loadingType: 'spinner' loadingType: 'spinner'
}) })
const response = await smartClassify(transactionIds) // 分批处理
for (let i = 0; i < allTransactions.length; i += batchSize) {
if (!response.ok) { const batch = allTransactions.slice(i, i + batchSize)
throw new Error('智能分类请求失败') 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 response = await smartClassify(transactionIds)
const reader = response.body.getReader()
const decoder = new TextDecoder() if (!response.ok) {
let buffer = '' throw new Error('智能分类请求失败')
let processedCount = 0 }
while (true) { // 读取流式响应
const { done, value } = await reader.read() const reader = response.body.getReader()
const decoder = new TextDecoder()
if (done) break let buffer = ''
buffer += decoder.decode(value, { stream: true }) while (true) {
const { done, value } = await reader.read()
// 处理完整的事件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
try { if (done) break
const lines = eventBlock.split('\n')
let eventType = '' buffer += decoder.decode(value, { stream: true })
let eventData = ''
// 处理完整的事件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) { try {
if (line.startsWith('event: ')) { const lines = eventBlock.split('\n')
eventType = line.slice(7).trim() let eventType = ''
} else if (line.startsWith('data: ')) { let eventData = ''
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++
// 记录分类结果 for (const line of lines) {
classifiedResults.value.push({ if (line.startsWith('event: ')) {
id: data.id, eventType = line.slice(7).trim()
classify: data.Classify, } else if (line.startsWith('data: ')) {
type: data.Type eventData = line.slice(6).trim()
}) }
// 实时更新交易记录的分类信息
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
} }
// 更新进度 if (eventType === 'start') {
closeToast() // 开始分类
toastInstance = showToast({ closeToast()
message: `已分类 ${processedCount}`, toastInstance = showToast({
duration: 0, message: `${eventData} (批次 ${currentBatch}/${totalBatches})`,
forbidClick: true, duration: 0,
loadingType: 'spinner' forbidClick: true,
}) loadingType: 'spinner'
} else if (eventType === 'end') { })
// 分类完成 } else if (eventType === 'data') {
closeToast() // 收到分类结果
toastInstance = null const data = JSON.parse(eventData)
showToast({ processedCount++
type: 'success',
message: `分类完成,请点击"保存分类"按钮保存结果`, // 记录分类结果
duration: 3000 classifiedResults.value.push({
}) id: data.id,
} else if (eventType === 'error') { classify: data.Classify,
// 处理错误 type: data.Type
closeToast() })
toastInstance = null
showToast({ // 实时更新交易记录的分类信息
type: 'fail', const index = props.transactions.findIndex(t => t.id === data.id)
message: eventData || '分类失败', if (index !== -1) {
duration: 2000 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) { } catch (error) {
console.error('智能分类失败:', error) console.error('智能分类失败:', error)
closeToast() closeToast()

View File

@@ -286,11 +286,20 @@
> >
<div class="popup-container"> <div class="popup-container">
<div class="popup-header-fixed"> <div class="popup-header-fixed">
<h3>{{ selectedCategoryTitle }}</h3> <h3 class="category-title">{{ selectedCategoryTitle }}</h3>
<p v-if="categoryBillsTotal"> {{ categoryBillsTotal }} 笔交易</p> <div class="header-stats">
<p v-if="categoryBillsTotal"> {{ categoryBillsTotal }} 笔交易</p>
<SmartClassifyButton
ref="smartClassifyButtonRef"
v-if="isUnclassified"
:transactions="categoryBills"
:onBeforeClassify="beforeSmartClassify"
@save="onSmartClassifySave"
/>
</div>
</div> </div>
<div class="popup-scroll-content" style="background: #f7f8fa;"> <div class="popup-scroll-content">
<TransactionList <TransactionList
:transactions="categoryBills" :transactions="categoryBills"
:loading="billListLoading" :loading="billListLoading"
@@ -320,6 +329,7 @@ import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord' import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue' import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue' import TransactionDetail from '@/components/TransactionDetail.vue'
import SmartClassifyButton from '@/components/SmartClassifyButton.vue'
const router = useRouter() const router = useRouter()
@@ -341,7 +351,7 @@ const selectedCategoryTitle = ref('')
const selectedClassify = ref('') const selectedClassify = ref('')
const selectedType = ref(null) const selectedType = ref(null)
const billPageIndex = ref(1) const billPageIndex = ref(1)
const billPageSize = 20 let billPageSize = 20
// 详情编辑相关 // 详情编辑相关
const detailVisible = ref(false) const detailVisible = ref(false)
@@ -416,6 +426,11 @@ const isCurrentMonth = computed(() => {
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1 return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
}) })
// 是否为未分类账单
const isUnclassified = computed(() => {
return selectedClassify.value === '未分类' || selectedClassify.value === ''
})
// 格式化金额 // 格式化金额
const formatMoney = (value) => { const formatMoney = (value) => {
if (!value && value !== 0) return '0' if (!value && value !== 0) return '0'
@@ -642,15 +657,16 @@ const goToTypeOverviewBills = (type) => {
loadCategoryBills() loadCategoryBills()
} }
const smartClassifyButtonRef = ref(null)
// 加载分类账单数据 // 加载分类账单数据
const loadCategoryBills = async () => { const loadCategoryBills = async (customIndex = null, customSize = null) => {
if (billListLoading.value || billListFinished.value) return if (billListLoading.value || billListFinished.value) return
billListLoading.value = true billListLoading.value = true
try { try {
const params = { const params = {
pageIndex: billPageIndex.value, pageIndex: customIndex || billPageIndex.value,
pageSize: billPageSize, pageSize: customSize || billPageSize,
type: selectedType.value, type: selectedType.value,
year: currentYear.value, year: currentYear.value,
month: currentMonth.value, month: currentMonth.value,
@@ -675,6 +691,8 @@ const loadCategoryBills = async () => {
billListFinished.value = false billListFinished.value = false
billPageIndex.value++ billPageIndex.value++
} }
smartClassifyButtonRef.value.reset()
} else { } else {
showToast(response.message || '加载账单失败') showToast(response.message || '加载账单失败')
billListFinished.value = true billListFinished.value = true
@@ -718,6 +736,27 @@ const onBillSave = async () => {
showToast('保存成功') showToast('保存成功')
} }
const beforeSmartClassify = async () => {
showToast({
message: '加载完整账单列表,请稍候...',
duration: 0,
forbidClick: true
})
await loadCategoryBills(1, categoryBillsTotal.value || 1000)
}
// 智能分类保存后的回调
const onSmartClassifySave = async () => {
// 关闭账单列表弹窗
billListVisible.value = false
// 刷新统计数据
await fetchStatistics()
showToast('智能分类已保存')
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
fetchStatistics() fetchStatistics()
@@ -1108,4 +1147,44 @@ onActivated(() => {
background: transparent !important; background: transparent !important;
} }
/* 弹出层样式 */
.popup-container {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.popup-header-fixed {
padding: 16px;
position: relative;
}
.category-title {
text-align: center;
margin: 0 0 12px;
font-size: 16px;
font-weight: 500;
}
.header-stats {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.header-stats p {
margin: 0;
font-size: 13px;
color: var(--van-text-color-2);
flex: 1;
}
.popup-scroll-content {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
</style> </style>