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
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:
@@ -43,4 +43,9 @@ public class MessageRecord : BaseEntity
|
||||
/// 是否已读
|
||||
/// </summary>
|
||||
public bool IsRead { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 跳转URL
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
>
|
||||
<template #right>
|
||||
<van-icon
|
||||
name="question-o"
|
||||
name="setting-o"
|
||||
size="20"
|
||||
style="cursor: pointer; padding-right: 12px;"
|
||||
@click="onClickPrompt"
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
<van-icon
|
||||
v-else
|
||||
name="info-o"
|
||||
name="setting-o"
|
||||
size="20"
|
||||
@click="savingsConfigRef.open()"
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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: '提示',
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
148
Web/src/views/UnconfirmedClassification.vue
Normal file
148
Web/src/views/UnconfirmedClassification.vue
Normal 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>
|
||||
@@ -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>
|
||||
@@ -176,6 +212,10 @@ public class TransactionRecordController(
|
||||
transaction.Balance = dto.Balance;
|
||||
transaction.Type = dto.Type;
|
||||
transaction.Classify = dto.Classify ?? string.Empty;
|
||||
|
||||
// 清除待确认状态
|
||||
transaction.UnconfirmedClassify = null;
|
||||
transaction.UnconfirmedType = null;
|
||||
|
||||
var success = await transactionRepository.UpdateAsync(transaction);
|
||||
if (success)
|
||||
|
||||
Reference in New Issue
Block a user