feat: 添加待确认分类功能,支持获取和确认未分类交易记录;优化相关组件和服务
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 10s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s

This commit is contained in:
2026-01-10 12:22:37 +08:00
parent 50843d43ff
commit 037bad2d9b
20 changed files with 548 additions and 79 deletions

View File

@@ -10,7 +10,7 @@
>
<template #right>
<van-icon
name="question-o"
name="setting-o"
size="20"
style="cursor: pointer; padding-right: 12px;"
@click="onClickPrompt"

View File

@@ -10,7 +10,7 @@
/>
<van-icon
v-else
name="info-o"
name="setting-o"
size="20"
@click="savingsConfigRef.open()"
/>

View File

@@ -195,21 +195,21 @@ const viewDetail = async (transaction) => {
// 详情保存后的回调
const onDetailSave = async (saveData) => {
// 重新加载当前日期的交易列表
if (saveData && dateTransactions.value) {
var updatedIndex = dateTransactions.value.findIndex(tx => tx.id === saveData.id);
if (updatedIndex !== -1) {
// 更新已有记录
dateTransactions.value[updatedIndex].amount = saveData.amount;
dateTransactions.value[updatedIndex].balance = saveData.balance;
dateTransactions.value[updatedIndex].type = saveData.type;
dateTransactions.value[updatedIndex].upsetedType = '';
dateTransactions.value[updatedIndex].classify = saveData.classify;
dateTransactions.value[updatedIndex].upsetedClassify = '';
dateTransactions.value[updatedIndex].reason = saveData.reason;
}
var item = dateTransactions.value.find(tx => tx.id === saveData.id);
if(!item) return
// 如果分类发生了变化 移除智能分类的内容,防止被智能分类覆盖
if(item.classify !== saveData.classify) {
// 通知智能分类按钮组件移除指定项
smartClassifyButtonRef.value?.removeClassifiedTransaction(saveData.id)
item.upsetedClassify = ''
}
// 更新当前日期交易列表中的数据
Object.assign(item, saveData);
// 重新加载当前月份的统计数据
const now = selectedDate.value || new Date();
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);

View File

@@ -53,18 +53,25 @@
<div v-else class="detail-content">
{{ currentMessage.content }}
</div>
<div v-if="currentMessage.url" class="detail-footer" style="padding: 16px;">
<van-button type="primary" block @click="handleUrlJump(currentMessage.url)">
查看详情
</van-button>
</div>
</PopupContainer>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { showToast, showDialog } from 'vant';
import { getMessageList, markAsRead, deleteMessage, markAllAsRead } from '@/api/message';
import { useMessageStore } from '@/stores/message';
import PopupContainer from '@/components/PopupContainer.vue';
const messageStore = useMessageStore();
const router = useRouter();
const list = ref([]);
const loading = ref(false);
const finished = ref(false);
@@ -138,11 +145,7 @@ const viewDetail = async (item) => {
}
if (item.messageType === 1) {
if (item.content.startsWith('http')) {
window.open(item.content, '_blank');
} else {
showToast('无效的URL');
}
handleUrlJump(item.url || item.content);
return;
}
@@ -150,6 +153,19 @@ const viewDetail = async (item) => {
detailVisible.value = true;
};
const handleUrlJump = (targetUrl) => {
if (!targetUrl) return;
if (targetUrl.startsWith('http')) {
window.open(targetUrl, '_blank');
} else if (targetUrl.startsWith('/')) {
router.push(targetUrl);
detailVisible.value = false;
} else {
showToast('无效的URL');
}
};
const handleDelete = (item) => {
showDialog({
title: '提示',

View File

@@ -18,6 +18,7 @@
<p>分类</p>
</div>
<van-cell-group inset>
<van-cell title="待确认分类" is-link @click="handleUnconfirmedClassification" />
<van-cell title="编辑分类" is-link @click="handleEditClassification" />
<van-cell title="批量分类" is-link @click="handleBatchClassification" />
<van-cell title="智能分类" is-link @click="handleSmartClassification" />
@@ -267,6 +268,10 @@ const handleLogView = () => {
router.push({ name: 'log' })
}
const handleUnconfirmedClassification = () => {
router.push({ name: 'unconfirmed-classification' })
}
const handleReloadFromNetwork = async () => {
try {
await showConfirmDialog({

View File

@@ -295,6 +295,7 @@
</template>
<TransactionList
ref="transactionListRef"
:transactions="categoryBills"
:loading="billListLoading"
:finished="billListFinished"
@@ -683,6 +684,7 @@ const goToTypeOverviewBills = (type) => {
}
const smartClassifyButtonRef = ref(null)
const transactionListRef = ref(null)
// 加载分类账单数据
const loadCategoryBills = async (customIndex = null, customSize = null) => {
if (billListLoading.value || billListFinished.value) return
@@ -756,16 +758,26 @@ const handleCategoryBillsDelete = (deletedId) => {
}
// 账单保存后的回调
const onBillSave = async () => {
const onBillSave = async (updatedTransaction) => {
// 刷新统计数据
await fetchStatistics()
// 刷新账单列表
categoryBills.value = []
billPageIndex.value = 1
billListFinished.value = false
await loadCategoryBills()
// 刷新列表中指定的账单项
const item = categoryBills.value.find(t => t.id === updatedTransaction.id)
if(!item) return
// 如果分类发生了变化
if(item.classify !== updatedTransaction.classify) {
// 从列表中移除该项
categoryBills.value = categoryBills.value.filter(t => t.id !== updatedTransaction.id)
categoryBillsTotal.value--
// 通知智能分类按钮组件移除指定项
smartClassifyButtonRef.value?.removeClassifiedTransaction(updatedTransaction.id)
return
}
Object.assign(item, updatedTransaction)
showToast('保存成功')
}
@@ -798,12 +810,15 @@ const onSmartClassifySave = async () => {
const handleNotifiedTransactionId = async (transactionId) => {
console.log('收到已处理交易ID通知:', transactionId)
// 滚动到指定的交易项
const index = categoryBills.value.findIndex(item => item.id === transactionId)
const index = categoryBills.value.findIndex(item => String(item.id) === String(transactionId))
if (index !== -1) {
// 等待 DOM 更新
await nextTick()
const listElement = document.querySelector('.transaction-list')
// 允许一丁点延迟让浏览器响应渲染
await new Promise(resolve => setTimeout(resolve, 0))
const listElement = transactionListRef.value?.$el
if (listElement) {
const items = listElement.querySelectorAll('.transaction-item')
const itemElement = items[index]

View File

@@ -1,5 +1,16 @@
<template>
<div class="page-container-flex">
<!-- 顶部固定搜索框 -->
<div class="top-search-bar">
<van-search
v-model="searchKeyword"
placeholder="搜索交易摘要、来源、卡号、分类"
shape="round"
@update:model-value="onSearchChange"
@clear="onSearchClear"
/>
</div>
<!-- 下拉刷新区域 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<!-- 加载提示 -->
@@ -31,21 +42,6 @@
:transaction="currentTransaction"
@save="onDetailSave"
/>
<!-- 底部浮动搜索框 -->
<div class="floating-search">
<van-search
v-model="searchKeyword"
placeholder="搜索交易摘要、来源、卡号、分类"
shape="round"
@update:model-value="onSearchChange"
@clear="onSearchClear"
/>
</div>
</div>
</template>
@@ -230,22 +226,16 @@ onBeforeUnmount(() => {
-webkit-overflow-scrolling: touch;
}
.floating-search {
position: fixed;
bottom: 90px;
left: 0;
right: 0;
z-index: 999;
padding: 8px 16px;
background: transparent;
pointer-events: none;
.top-search-bar {
background: var(--van-background-2);
padding: 4px 12px;
z-index: 100;
border-bottom: 1px solid var(--van-border-color);
}
.floating-search :deep(.van-search) {
pointer-events: auto;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
border-radius: 20px;
border: none;
.top-search-bar :deep(.van-search) {
padding: 4px 0;
background: transparent;
}

View File

@@ -0,0 +1,148 @@
<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="transactions.length > 0"
type="primary"
size="small"
:loading="confirming"
@click="handleConfirmAll"
>
全部确认
</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>
<TransactionList
v-else
:transactions="displayTransactions"
:loading="loading"
:finished="true"
@click="handleTransactionClick"
@delete="handleTransactionDeleted"
/>
</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 TransactionList from '@/components/TransactionList.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
const router = useRouter()
const loading = ref(false)
const confirming = ref(false)
const transactions = ref([])
const showDetail = ref(false)
const currentTransaction = ref(null)
const onClickLeft = () => {
router.back()
}
const handleConfirmAll = async () => {
try {
await showConfirmDialog({
title: '提示',
message: `确定要将这 ${transactions.value.length} 条记录的所有建议分类转为正式分类吗?`
})
confirming.value = true
const response = await confirmAllUnconfirmed()
if (response && response.success) {
showToast(`成功确认 ${response.data} 条记录`)
loadData()
} else {
showToast(response.message || '确认失败')
}
} catch (err) {
if (err !== 'cancel') {
console.error('批量确认出错:', err)
}
} finally {
confirming.value = false
}
}
// 转换数据格式以适配 TransactionList 组件
const displayTransactions = computed(() => {
return transactions.value.map(t => ({
...t,
upsetedClassify: t.unconfirmedClassify,
upsetedType: t.unconfirmedType
}))
})
const loadData = async () => {
loading.value = true
try {
const response = await getUnconfirmedTransactionList()
if (response && response.success) {
transactions.value = response.data || []
}
} catch (error) {
console.error('获取待确认列表失败:', error)
} finally {
loading.value = false
}
}
const handleTransactionClick = (transaction) => {
currentTransaction.value = transaction
showDetail.value = true
}
const handleTransactionDeleted = (id) => {
transactions.value = transactions.value.filter(t => t.id !== id)
}
const handleDetailSave = () => {
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.unconfirmed-classification {
height: 100vh;
display: flex;
flex-direction: column;
}
.scroll-content {
flex: 1;
overflow-y: auto;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
</style>