Files
EmailBill/Web/src/views/UnconfirmedClassification.vue
SunCheng b78774bc39
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 1m55s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
优化待分类页面
2026-01-27 16:46:16 +08:00

334 lines
7.9 KiB
Vue

<template>
<div class="page-container-flex unconfirmed-classification">
<van-nav-bar
title="待确认分类"
left-text="返回"
left-arrow
@click-left="onClickLeft"
>
<template #right>
<van-button
v-if="selectedIds.size > 0"
type="primary"
size="small"
:loading="confirming"
@click="handleConfirmSelected"
>
确认所选 ({{ selectedIds.size }})
</van-button>
</template>
</van-nav-bar>
<div class="scroll-content">
<div
v-if="loading && transactions.length === 0"
class="loading-container"
>
<van-loading vertical>
加载中...
</van-loading>
</div>
<div
v-else-if="treeData.length === 0 && !loading"
class="empty-container"
>
<van-empty description="暂无待确认分类" />
</div>
<van-collapse
v-else
v-model="activeNames"
:border="false"
>
<van-collapse-item
v-for="typeNode in treeData"
:key="typeNode.id"
:name="typeNode.id"
class="type-node"
>
<template #title>
<div class="node-title">
<span class="node-count">{{ typeNode.count }}</span>
<span class="node-name">{{ typeNode.text }}</span>
<span class="node-amount">{{ formatAmount(typeNode.amount) }}</span>
</div>
</template>
<van-collapse
v-model="activeClassifyNames"
:border="false"
class="classify-collapse"
>
<van-collapse-item
v-for="classifyNode in typeNode.children"
:key="classifyNode.id"
:name="classifyNode.id"
class="classify-node"
>
<template #title>
<div class="node-title">
<span class="node-count">{{ classifyNode.count }}</span>
<span class="node-name">{{ classifyNode.text }}</span>
<span class="node-amount">{{ formatAmount(classifyNode.amount) }}</span>
</div>
</template>
<TransactionList
:transactions="classifyNode.children.map(c => c.transaction)"
:show-delete="false"
:show-checkbox="true"
:selected-ids="selectedIds"
@click="handleTransactionClick"
@update:selected-ids="handleUpdateSelectedIds"
/>
</van-collapse-item>
</van-collapse>
</van-collapse-item>
</van-collapse>
</div>
<!-- 交易详情弹窗 -->
<TransactionDetail
v-model:show="showDetail"
:transaction="currentTransaction"
@save="handleDetailSave"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import { getUnconfirmedTransactionList, confirmAllUnconfirmed } from '@/api/transactionRecord'
import TransactionDetail from '@/components/TransactionDetail.vue'
import TransactionList from '@/components/TransactionList.vue'
const router = useRouter()
const loading = ref(false)
const confirming = ref(false)
const transactions = ref([])
const showDetail = ref(false)
const currentTransaction = ref(null)
const selectedIds = ref(new Set())
const activeNames = ref([])
const activeClassifyNames = ref([])
const TYPE_NAMES = {
0: '支出',
1: '收入',
2: '不记收支'
}
const onClickLeft = () => {
if (window.history.length > 1) {
router.back()
} else {
router.replace('/')
}
}
const handleConfirmSelected = async () => {
try {
await showConfirmDialog({
title: '提示',
message: `确定要将这 ${selectedIds.value.size} 条记录的所有建议分类转为正式分类吗?`
})
confirming.value = true
const response = await confirmAllUnconfirmed(Array.from(selectedIds.value))
if (response && response.success) {
showToast(`成功确认 ${response.data} 条记录`)
loadData()
} else {
showToast(response.message || '确认失败')
}
} catch (err) {
if (err !== 'cancel') {
console.error('批量确认出错:', err)
}
} finally {
confirming.value = false
}
}
const formatAmount = (amount) => {
if (amount === null || amount === undefined) {return ''}
const num = parseFloat(amount)
if (isNaN(num)) {return ''}
return num.toFixed(2)
}
const buildTreeData = (data) => {
const typeMap = {}
data.forEach((item) => {
const type = item.unconfirmedType ?? item.type
const classify = item.unconfirmedClassify ?? item.classify
const typeName = TYPE_NAMES[type] || '未分类'
const classifyName = classify || '未分类'
if (!typeMap[typeName]) {
typeMap[typeName] = {
id: `type-${type}`,
text: typeName,
type: 'type',
typeId: type,
children: [],
count: 0,
amount: 0
}
}
let classifyNode = typeMap[typeName].children.find((c) => c.text === classifyName)
if (!classifyNode) {
classifyNode = {
id: `classify-${type}-${classifyName}`,
text: classifyName,
type: 'classify',
typeId: type,
classify: classifyName,
children: [],
count: 0,
amount: 0
}
typeMap[typeName].children.push(classifyNode)
}
classifyNode.children.push({
id: item.id,
text: item.reason || item.occurredAt,
type: 'transaction',
transaction: item
})
classifyNode.count += 1
classifyNode.amount += parseFloat(item.amount) || 0
typeMap[typeName].count += 1
typeMap[typeName].amount += parseFloat(item.amount) || 0
})
return Object.values(typeMap).map((typeNode) => {
typeNode.children.forEach((classifyNode) => {
classifyNode.children.sort((a, b) => {
const dateA = new Date(a.transaction.occurredAt)
const dateB = new Date(b.transaction.occurredAt)
return dateB - dateA
})
})
return typeNode
})
}
const treeData = computed(() => {
return buildTreeData(transactions.value)
})
const loadData = async () => {
loading.value = true
try {
const response = await getUnconfirmedTransactionList()
if (response && response.success) {
transactions.value = response.data || []
selectedIds.value = new Set(response.data.map((t) => t.id))
activeNames.value = treeData.value.map((node) => node.id)
}
} catch (error) {
console.error('获取待确认列表失败:', error)
} finally {
loading.value = false
}
}
const handleTransactionClick = (transaction) => {
currentTransaction.value = transaction
showDetail.value = true
}
const handleUpdateSelectedIds = (newSelectedIds) => {
selectedIds.value = newSelectedIds
}
const handleDetailSave = () => {
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.unconfirmed-classification {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.loading-container,
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
.node-title {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.node-name {
flex: 1;
font-weight: 500;
}
.node-count {
font-size: 12px;
color: #fff;
background: var(--van-primary-color);
padding: 2px 6px;
border-radius: 4px;
}
.node-amount {
font-size: 14px;
font-weight: 600;
color: var(--van-orange);
}
.type-node :deep(.van-collapse-item__title) {
font-size: 16px;
font-weight: 600;
}
.classify-node :deep(.van-collapse-item__title) {
font-size: 14px;
}
.classify-collapse {
padding-left: 8px;
}
.classify-collapse :deep(.van-cell-group--inset) {
margin-left: -8px;
}
:deep(.van-nav-bar) {
background: transparent !important;
}
:deep(.van-cell) {
padding: 8px 12px;
}
</style>