fix
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user