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: {
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
@@ -133,6 +140,15 @@ const handleSmartClassify = async () => {
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,6 +156,22 @@ const handleSmartClassify = async () => {
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)
if (!response.ok) {
@@ -150,7 +182,6 @@ const handleSmartClassify = async () => {
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let processedCount = 0
while (true) {
const { done, value } = await reader.read()
@@ -183,7 +214,7 @@ const handleSmartClassify = async () => {
// 开始分类
closeToast()
toastInstance = showToast({
message: eventData,
message: `${eventData} (批次 ${currentBatch}/${totalBatches})`,
duration: 0,
forbidClick: true,
loadingType: 'spinner'
@@ -211,35 +242,34 @@ const handleSmartClassify = async () => {
// 更新进度
closeToast()
toastInstance = showToast({
message: `已分类 ${processedCount}`,
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
}
}
}
}
// 所有批次完成
closeToast()
toastInstance = null
showToast({
type: 'success',
message: `分类完成,请点击"保存分类"按钮保存结果`,
message: `分类完成,共处理 ${processedCount} 条记录,请点击"保存分类"按钮保存结果`,
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) {
console.error('智能分类失败:', error)
closeToast()

View File

@@ -286,11 +286,20 @@
>
<div class="popup-container">
<div class="popup-header-fixed">
<h3>{{ selectedCategoryTitle }}</h3>
<h3 class="category-title">{{ selectedCategoryTitle }}</h3>
<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 class="popup-scroll-content" style="background: #f7f8fa;">
<div class="popup-scroll-content">
<TransactionList
:transactions="categoryBills"
:loading="billListLoading"
@@ -320,6 +329,7 @@ import { getMonthlyStatistics, getCategoryStatistics, getTrendStatistics } from
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
import SmartClassifyButton from '@/components/SmartClassifyButton.vue'
const router = useRouter()
@@ -341,7 +351,7 @@ const selectedCategoryTitle = ref('')
const selectedClassify = ref('')
const selectedType = ref(null)
const billPageIndex = ref(1)
const billPageSize = 20
let billPageSize = 20
// 详情编辑相关
const detailVisible = ref(false)
@@ -416,6 +426,11 @@ const isCurrentMonth = computed(() => {
return currentYear.value === now.getFullYear() && currentMonth.value === now.getMonth() + 1
})
// 是否为未分类账单
const isUnclassified = computed(() => {
return selectedClassify.value === '未分类' || selectedClassify.value === ''
})
// 格式化金额
const formatMoney = (value) => {
if (!value && value !== 0) return '0'
@@ -642,15 +657,16 @@ const goToTypeOverviewBills = (type) => {
loadCategoryBills()
}
const smartClassifyButtonRef = ref(null)
// 加载分类账单数据
const loadCategoryBills = async () => {
const loadCategoryBills = async (customIndex = null, customSize = null) => {
if (billListLoading.value || billListFinished.value) return
billListLoading.value = true
try {
const params = {
pageIndex: billPageIndex.value,
pageSize: billPageSize,
pageIndex: customIndex || billPageIndex.value,
pageSize: customSize || billPageSize,
type: selectedType.value,
year: currentYear.value,
month: currentMonth.value,
@@ -675,6 +691,8 @@ const loadCategoryBills = async () => {
billListFinished.value = false
billPageIndex.value++
}
smartClassifyButtonRef.value.reset()
} else {
showToast(response.message || '加载账单失败')
billListFinished.value = true
@@ -718,6 +736,27 @@ const onBillSave = async () => {
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(() => {
fetchStatistics()
@@ -1108,4 +1147,44 @@ onActivated(() => {
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>