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
@@ -133,6 +140,15 @@ const handleSmartClassify = async () => {
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,6 +156,22 @@ const handleSmartClassify = async () => {
loadingType: 'spinner' loadingType: 'spinner'
}) })
// 分批处理
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 response = await smartClassify(transactionIds) const response = await smartClassify(transactionIds)
if (!response.ok) { if (!response.ok) {
@@ -150,7 +182,6 @@ const handleSmartClassify = async () => {
const reader = response.body.getReader() const reader = response.body.getReader()
const decoder = new TextDecoder() const decoder = new TextDecoder()
let buffer = '' let buffer = ''
let processedCount = 0
while (true) { while (true) {
const { done, value } = await reader.read() const { done, value } = await reader.read()
@@ -183,7 +214,7 @@ const handleSmartClassify = async () => {
// 开始分类 // 开始分类
closeToast() closeToast()
toastInstance = showToast({ toastInstance = showToast({
message: eventData, message: `${eventData} (批次 ${currentBatch}/${totalBatches})`,
duration: 0, duration: 0,
forbidClick: true, forbidClick: true,
loadingType: 'spinner' loadingType: 'spinner'
@@ -211,35 +242,34 @@ const handleSmartClassify = async () => {
// 更新进度 // 更新进度
closeToast() closeToast()
toastInstance = showToast({ toastInstance = showToast({
message: `已分类 ${processedCount}`, message: `已分类 ${processedCount}/${totalCount} 条 (批次 ${currentBatch}/${totalBatches})...`,
duration: 0, duration: 0,
forbidClick: true, forbidClick: true,
loadingType: 'spinner' loadingType: 'spinner'
}) })
} else if (eventType === 'end') { } 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
}
}
}
}
// 所有批次完成
closeToast() closeToast()
toastInstance = null toastInstance = null
showToast({ showToast({
type: 'success', type: 'success',
message: `分类完成,请点击"保存分类"按钮保存结果`, message: `分类完成,共处理 ${processedCount} 条记录,请点击"保存分类"按钮保存结果`,
duration: 3000 duration: 3000
}) })
} else if (eventType === 'error') {
// 处理错误
closeToast()
toastInstance = null
showToast({
type: 'fail',
message: eventData || '分类失败',
duration: 2000
})
}
} catch (e) {
console.error('解析SSE事件失败:', e, eventBlock)
}
}
}
} 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>
<div class="header-stats">
<p v-if="categoryBillsTotal"> {{ categoryBillsTotal }} 笔交易</p> <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>