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

@@ -43,4 +43,9 @@ public class MessageRecord : BaseEntity
/// 是否已读
/// </summary>
public bool IsRead { get; set; } = false;
/// <summary>
/// 跳转URL
/// </summary>
public string? Url { get; set; }
}

View File

@@ -50,6 +50,16 @@ public class TransactionRecord : BaseEntity
/// </summary>
public string Classify { get; set; } = string.Empty;
/// <summary>
/// 待确认的分类AI或规则建议但尚未正式确认
/// </summary>
public string? UnconfirmedClassify { get; set; }
/// <summary>
/// 待确认的类型
/// </summary>
public TransactionType? UnconfirmedType { get; set; }
/// <summary>
/// 导入编号
/// </summary>

View File

@@ -178,6 +178,18 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
/// <returns>候选交易列表</returns>
Task<List<TransactionRecord>> GetCandidatesForOffsetAsync(long currentId, decimal amount, TransactionType currentType);
/// <summary>
/// 获取待确认分类的账单列表
/// </summary>
/// <returns>待确认账单列表</returns>
Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync();
/// <summary>
/// 全部确认待确认的分类
/// </summary>
/// <returns>影响行数</returns>
Task<int> ConfirmAllUnconfirmedAsync();
/// <summary>
/// 更新分类名称
/// </summary>
@@ -677,6 +689,25 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.Where(a => a.Classify == oldName && a.Type == type)
.ExecuteAffrowsAsync();
}
public async Task<List<TransactionRecord>> GetUnconfirmedRecordsAsync()
{
return await FreeSql.Select<TransactionRecord>()
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
.OrderByDescending(t => t.OccurredAt)
.ToListAsync();
}
public async Task<int> ConfirmAllUnconfirmedAsync()
{
return await FreeSql.Update<TransactionRecord>()
.Set(t => t.Classify == t.UnconfirmedClassify)
.Set(t => t.Type == (t.UnconfirmedType ?? t.Type))
.Set(t => t.UnconfirmedClassify, null)
.Set(t => t.UnconfirmedType, null)
.Where(t => t.UnconfirmedClassify != null && t.UnconfirmedClassify != "")
.ExecuteAffrowsAsync();
}
}
/// <summary>

View File

@@ -102,9 +102,7 @@ public class EmailHandleService(
records.Add(record);
}
// var analysisResult = await AnalyzeClassifyAsync(records.ToArray());
// TODO 不应该直接保存 应该保存在备用字段上,前端确认后再更新到正式字段
_ = AutoClassifyAsync(records.ToArray());
return allSuccess;
}
@@ -173,11 +171,34 @@ public class EmailHandleService(
records.Add(record);
}
_ = await AnalyzeClassifyAsync(records.ToArray());
_ = AutoClassifyAsync(records.ToArray());
return allSuccess;
}
private async Task AutoClassifyAsync(TransactionRecord[] records)
{
await AnalyzeClassifyAsync(records.ToArray());
foreach (var record in records)
{
record.UnconfirmedClassify = record.Classify;
record.UnconfirmedType = record.Type;
record.Classify = ""; // 重置为未分类,等待手动确认
}
await trxRepo.UpdateRangeAsync(records);
// 消息
await messageRecordService.AddAsync(
"交易记录待确认分类",
$"共有 {records.Length} 条交易记录待确认分类,请点击前往确认。",
MessageType.Url,
"/unconfirmed-classification"
);
}
private string GetEmailByName(string to)
{
return emailSettings.Value.SmtpList.FirstOrDefault(s => s.Email == to)?.Name ?? to;

View File

@@ -5,7 +5,7 @@ public interface IMessageRecordService
Task<(IEnumerable<MessageRecord> List, long Total)> GetPagedListAsync(int pageIndex, int pageSize);
Task<MessageRecord?> GetByIdAsync(long id);
Task<bool> AddAsync(MessageRecord message);
Task<bool> AddAsync(string title, string content, MessageType type = MessageType.Text);
Task<bool> AddAsync(string title, string content, MessageType type = MessageType.Text, string? url = null);
Task<bool> MarkAsReadAsync(long id);
Task<bool> MarkAllAsReadAsync();
Task<bool> DeleteAsync(long id);
@@ -29,19 +29,20 @@ public class MessageRecordService(IMessageRecordRepository messageRepo, INotific
return await messageRepo.AddAsync(message);
}
public async Task<bool> AddAsync(string title, string content, MessageType type = MessageType.Text)
public async Task<bool> AddAsync(string title, string content, MessageType type = MessageType.Text, string? url = null)
{
var message = new MessageRecord
{
Title = title,
Content = content,
MessageType = type,
Url = url,
IsRead = false
};
var result = await messageRepo.AddAsync(message);
if (result)
{
await notificationService.SendNotificationAsync(title);
await notificationService.SendNotificationAsync(title, url);
}
return result;
}

View File

@@ -19,6 +19,28 @@ export const getTransactionList = (params = {}) => {
})
}
/**
* 获取待确认分类的交易记录列表
* @returns {Promise<{success: boolean, data: Array}>}
*/
export const getUnconfirmedTransactionList = () => {
return request({
url: '/TransactionRecord/GetUnconfirmedList',
method: 'get'
})
}
/**
* 全部确认待确认的交易分类
* @returns {Promise<{success: boolean, data: number}>}
*/
export const confirmAllUnconfirmed = () => {
return request({
url: '/TransactionRecord/ConfirmAllUnconfirmed',
method: 'post'
})
}
/**
* 根据ID获取交易记录详情
* @param {number} id - 交易记录ID

View File

@@ -59,15 +59,14 @@
<van-button
v-if="budget.description"
:icon="showDescription ? 'info' : 'info-o'"
size="mini"
size="small"
:type="showDescription ? 'primary' : 'default'"
plain
round
@click.stop="showDescription = !showDescription"
/>
<van-button
icon="orders-o"
size="mini"
size="small"
plain
title="查询关联账单"
@click.stop="handleQueryBills"
@@ -75,13 +74,13 @@
<template v-if="budget.category !== 2">
<van-button
icon="edit"
size="mini"
size="small"
plain
@click.stop="$emit('click', budget)"
/>
<van-button
:icon="budget.isStopped ? 'play' : 'pause'"
size="mini"
size="small"
plain
@click.stop="$emit('toggle-stop', budget)"
/>

View File

@@ -333,6 +333,11 @@ const handleSmartClassify = async () => {
}
}
const removeClassifiedTransaction = (transactionId) => {
// 从已分类结果中移除指定ID的项
classifiedResults.value = classifiedResults.value.filter(item => item.id !== transactionId)
}
/**
* 重置组件状态
*/
@@ -344,7 +349,8 @@ const reset = () => {
}
defineExpose({
reset
reset,
removeClassifiedTransaction
});
</script>

View File

@@ -11,11 +11,21 @@
<van-form style="margin-top: 12px;">
<van-cell-group inset>
<van-cell title="交易时间" :value="formatDate(transaction.occurredAt)" />
<van-cell title="记录时间" :value="formatDate(transaction.createTime)" />
</van-cell-group>
<van-cell-group inset title="交易明细">
<van-field
v-model="occurredAtLabel"
name="occurredAt"
label="交易时间"
readonly
is-link
placeholder="请选择交易时间"
:rules="[{ required: true, message: '请选择交易时间' }]"
@click="showDatePicker = true"
/>
<van-field
v-model="editForm.reason"
name="reason"
@@ -56,12 +66,27 @@
<van-field name="classify" label="交易分类">
<template #input>
<span v-if="!editForm.classify" style="color: #c8c9cc;">请选择交易分类</span>
<span v-else>{{ editForm.classify }}</span>
<div style="flex: 1;">
<div
v-if="transaction && transaction.unconfirmedClassify && transaction.unconfirmedClassify !== editForm.classify"
class="suggestion-tip"
@click="applySuggestion"
>
<van-icon name="bulb-o" class="suggestion-icon" />
<span class="suggestion-text">
建议: {{ transaction.unconfirmedClassify }}
<span v-if="transaction.unconfirmedType !== null && transaction.unconfirmedType !== undefined && transaction.unconfirmedType !== editForm.type">
({{ getTypeName(transaction.unconfirmedType) }})
</span>
</span>
<div class="suggestion-apply">应用</div>
</div>
<span v-else-if="!editForm.classify" style="color: #c8c9cc;">请选择交易分类</span>
<span v-else>{{ editForm.classify }}</span>
</div>
</template>
</van-field>
<!-- 分类选择组件 -->
<ClassifySelector
v-model="editForm.classify"
:type="editForm.type"
@@ -102,11 +127,32 @@
<van-empty v-if="offsetCandidates.length === 0" description="暂无匹配的抵账交易" />
</van-list>
</PopupContainer>
<!-- 日期选择弹窗 -->
<van-popup v-model:show="showDatePicker" position="bottom" round teleport="body">
<van-date-picker
v-model="currentDate"
title="选择日期"
@confirm="onConfirmDate"
@cancel="showDatePicker = false"
/>
</van-popup>
<!-- 时间选择弹窗 -->
<van-popup v-model:show="showTimePicker" position="bottom" round teleport="body">
<van-time-picker
v-model="currentTime"
title="选择时间"
@confirm="onConfirmTime"
@cancel="showTimePicker = false"
/>
</van-popup>
</template>
<script setup>
import { ref, reactive, watch, defineProps, defineEmits } from 'vue'
import { ref, reactive, watch, defineProps, defineEmits, computed } from 'vue'
import { showToast, showConfirmDialog } from 'vant'
import dayjs from 'dayjs'
import PopupContainer from '@/components/PopupContainer.vue'
import ClassifySelector from '@/components/ClassifySelector.vue'
import { updateTransaction, getCandidatesForOffset, offsetTransactions } from '@/api/transactionRecord'
@@ -127,6 +173,12 @@ const emit = defineEmits(['update:show', 'save'])
const visible = ref(false)
const submitting = ref(false)
// 日期选择相关
const showDatePicker = ref(false)
const showTimePicker = ref(false)
const currentDate = ref([])
const currentTime = ref([])
// 编辑表单
const editForm = reactive({
id: 0,
@@ -134,7 +186,13 @@ const editForm = reactive({
amount: '',
balance: '',
type: 0,
classify: ''
classify: '',
occurredAt: ''
})
// 显示用的日期格式化
const occurredAtLabel = computed(() => {
return formatDate(editForm.occurredAt)
})
// 监听props变化
@@ -151,6 +209,14 @@ watch(() => props.transaction, (newVal) => {
editForm.balance = String(newVal.balance)
editForm.type = newVal.type
editForm.classify = newVal.classify || ''
// 初始化日期时间
if (newVal.occurredAt) {
editForm.occurredAt = newVal.occurredAt
const dt = dayjs(newVal.occurredAt)
currentDate.value = dt.format('YYYY-MM-DD').split('-')
currentTime.value = dt.format('HH:mm').split(':')
}
}
})
@@ -158,12 +224,48 @@ watch(visible, (newVal) => {
emit('update:show', newVal)
})
// 处理日期确认
const onConfirmDate = ({ selectedValues }) => {
const dateStr = selectedValues.join('-')
const timeStr = currentTime.value.join(':')
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
showDatePicker.value = false
// 接着选时间
showTimePicker.value = true
}
const onConfirmTime = ({ selectedValues }) => {
currentTime.value = selectedValues
const dateStr = currentDate.value.join('-')
const timeStr = selectedValues.join(':')
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
showTimePicker.value = false
}
// 监听交易类型变化,重新加载分类
watch(() => editForm.type, (newVal) => {
// 清空已选的分类
editForm.classify = ''
})
const applySuggestion = () => {
if (props.transaction.unconfirmedClassify) {
editForm.classify = props.transaction.unconfirmedClassify
if (props.transaction.unconfirmedType !== null && props.transaction.unconfirmedType !== undefined) {
editForm.type = props.transaction.unconfirmedType
}
}
}
const getTypeName = (type) => {
const typeMap = {
0: '支出',
1: '收入',
2: '不计'
}
return typeMap[type] || '未知'
}
// 提交编辑
const onSubmit = async () => {
try {
@@ -175,7 +277,8 @@ const onSubmit = async () => {
amount: parseFloat(editForm.amount),
balance: parseFloat(editForm.balance),
type: editForm.type,
classify: editForm.classify
classify: editForm.classify,
occurredAt: editForm.occurredAt
}
const response = await updateTransaction(data)
@@ -262,4 +365,53 @@ const handleCandidateSelect = (candidate) => {
</script>
<style scoped>
.suggestion-tip {
font-size: 12px;
display: flex;
align-items: center;
padding: 6px 10px;
background: #ecf9ff;
color: #1989fa;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.2s;
border: 1px solid rgba(25, 137, 250, 0.1);
width: fit-content;
}
.suggestion-tip:active {
opacity: 0.7;
}
.suggestion-icon {
margin-right: 4px;
font-size: 14px;
}
.suggestion-text {
font-weight: 500;
}
.suggestion-apply {
margin-left: 8px;
padding: 0 6px;
background: #1989fa;
color: #fff;
border-radius: 4px;
font-size: 10px;
height: 18px;
line-height: 18px;
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
.suggestion-tip {
background: rgba(25, 137, 250, 0.15);
border-color: rgba(25, 137, 250, 0.2);
color: #58a6ff;
}
.suggestion-apply {
background: #58a6ff;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="transaction-list-container">
<div class="transaction-list-container transaction-list">
<van-list
:loading="loading"
:finished="finished"
@@ -10,6 +10,7 @@
<van-swipe-cell
v-for="transaction in transactions"
:key="transaction.id"
class="transaction-item"
>
<div class="transaction-row">
<van-checkbox

View File

@@ -99,6 +99,13 @@ const router = createRouter({
name: 'scheduled-tasks',
component: () => import('../views/ScheduledTasksView.vue'),
meta: { requiresAuth: true },
},
{
// 待确认的分类项
path: '/unconfirmed-classification',
name: 'unconfirmed-classification',
component: () => import('../views/UnconfirmedClassification.vue'),
meta: { requiresAuth: true },
}
],
})

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,15 +758,25 @@ 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>

View File

@@ -71,6 +71,42 @@ public class TransactionRecordController(
}
}
/// <summary>
/// 获取待确认分类的交易记录列表
/// </summary>
[HttpGet]
public async Task<BaseResponse<List<TransactionRecord>>> GetUnconfirmedListAsync()
{
try
{
var list = await transactionRepository.GetUnconfirmedRecordsAsync();
return list.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "获取待确认分类交易列表失败");
return $"获取待确认分类交易列表失败: {ex.Message}".Fail<List<TransactionRecord>>();
}
}
/// <summary>
/// 全部确认待确认的交易分类
/// </summary>
[HttpPost]
public async Task<BaseResponse<int>> ConfirmAllUnconfirmedAsync()
{
try
{
var count = await transactionRepository.ConfirmAllUnconfirmedAsync();
return count.Ok();
}
catch (Exception ex)
{
logger.LogError(ex, "全部确认待确认分类失败");
return $"全部确认待确认分类失败: {ex.Message}".Fail<int>();
}
}
/// <summary>
/// 根据ID获取交易记录详情
/// </summary>
@@ -177,6 +213,10 @@ public class TransactionRecordController(
transaction.Type = dto.Type;
transaction.Classify = dto.Classify ?? string.Empty;
// 清除待确认状态
transaction.UnconfirmedClassify = null;
transaction.UnconfirmedType = null;
var success = await transactionRepository.UpdateAsync(transaction);
if (success)
{