fix
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -286,11 +286,20 @@
|
||||
>
|
||||
<div class="popup-container">
|
||||
<div class="popup-header-fixed">
|
||||
<h3>{{ selectedCategoryTitle }}</h3>
|
||||
<p v-if="categoryBillsTotal">共 {{ categoryBillsTotal }} 笔交易</p>
|
||||
<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>
|
||||
Reference in New Issue
Block a user