调整
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 9s
Docker Build & Deploy / Deploy to Production (push) Has been skipped

This commit is contained in:
2025-12-27 22:05:50 +08:00
parent 3b5675d50d
commit 1a805e51f7
6 changed files with 183 additions and 101 deletions

View File

@@ -7,31 +7,36 @@ public interface ITransactionRecordRepository : IBaseRepository<TransactionRecor
Task<TransactionRecord?> ExistsByImportNoAsync(string importNo, string importFrom); Task<TransactionRecord?> ExistsByImportNoAsync(string importNo, string importFrom);
/// <summary> /// <summary>
/// 分页获取交易记录列表(游标分页) /// 分页获取交易记录列表
/// </summary> /// </summary>
/// <param name="lastOccurredAt">上一页最后一条记录的发生时间</param> /// <param name="pageIndex">页码从1开始</param>
/// <param name="lastId">上一页最后一条记录的ID</param>
/// <param name="pageSize">每页数量</param> /// <param name="pageSize">每页数量</param>
/// <param name="searchKeyword">搜索关键词(搜索交易摘要和分类)</param> /// <param name="searchKeyword">搜索关键词(搜索交易摘要和分类)</param>
/// <param name="classify">筛选分类</param> /// <param name="classify">筛选分类</param>
/// <param name="type">筛选交易类型</param> /// <param name="type">筛选交易类型</param>
/// <param name="year">筛选年份</param> /// <param name="year">筛选年份</param>
/// <param name="month">筛选月份</param> /// <param name="month">筛选月份</param>
/// <returns>交易记录列表、最后发生时间和最后ID</returns> /// <param name="sortByAmount">是否按金额降序排列默认为false按时间降序</param>
Task<(List<TransactionRecord> list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync( /// <returns>交易记录列表</returns>
DateTime? lastOccurredAt, Task<List<TransactionRecord>> GetPagedListAsync(
long? lastId, int pageIndex = 1,
int pageSize = 20, int pageSize = 20,
string? searchKeyword = null, string? searchKeyword = null,
string? classify = null, string? classify = null,
TransactionType? type = null, TransactionType? type = null,
int? year = null, int? year = null,
int? month = null); int? month = null,
bool sortByAmount = false);
/// <summary> /// <summary>
/// 获取总数 /// 获取总数
/// </summary> /// </summary>
Task<long> GetTotalCountAsync(); Task<long> GetTotalCountAsync(
string? searchKeyword = null,
string? classify = null,
TransactionType? type = null,
int? year = null,
int? month = null);
/// <summary> /// <summary>
/// 获取所有不同的交易分类 /// 获取所有不同的交易分类
@@ -162,10 +167,68 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
.FirstAsync(); .FirstAsync();
} }
public async Task<(List<TransactionRecord> list, DateTime? lastOccurredAt, long lastId)> GetPagedListAsync( public async Task<List<TransactionRecord>> GetPagedListAsync(
DateTime? lastOccurredAt, int pageIndex = 1,
long? lastId,
int pageSize = 20, int pageSize = 20,
string? searchKeyword = null,
string? classify = null,
TransactionType? type = null,
int? year = null,
int? month = null,
bool sortByAmount = false)
{
var query = FreeSql.Select<TransactionRecord>();
// 如果提供了搜索关键词,则添加搜索条件
query = query.WhereIf(!string.IsNullOrWhiteSpace(searchKeyword),
t => t.Reason.Contains(searchKeyword!) ||
t.Classify.Contains(searchKeyword!) ||
t.Card.Contains(searchKeyword!) ||
t.ImportFrom.Contains(searchKeyword!));
// 按分类筛选
if (!string.IsNullOrWhiteSpace(classify))
{
if (classify == "未分类")
{
classify = string.Empty;
}
query = query.Where(t => t.Classify == classify);
}
// 按交易类型筛选
query = query.WhereIf(type.HasValue, t => t.Type == type!.Value);
// 按年月筛选
if (year.HasValue && month.HasValue)
{
var startDate = new DateTime(year.Value, month.Value, 1);
var endDate = startDate.AddMonths(1);
query = query.Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate);
}
// 根据sortByAmount参数决定排序方式
if (sortByAmount)
{
// 按金额降序排列
return await query
.OrderByDescending(t => t.Amount)
.OrderByDescending(t => t.Id)
.Page(pageIndex, pageSize)
.ToListAsync();
}
else
{
// 按时间降序排列
return await query
.OrderByDescending(t => t.OccurredAt)
.OrderByDescending(t => t.Id)
.Page(pageIndex, pageSize)
.ToListAsync();
}
}
public async Task<long> GetTotalCountAsync(
string? searchKeyword = null, string? searchKeyword = null,
string? classify = null, string? classify = null,
TransactionType? type = null, TransactionType? type = null,
@@ -202,26 +265,7 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository<Tran
query = query.Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate); query = query.Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate);
} }
// 如果提供了游标,则获取小于游标位置的记录 return await query.CountAsync();
if (lastOccurredAt.HasValue && lastId.HasValue)
{
query = query.Where(t => t.OccurredAt < lastOccurredAt.Value ||
(t.OccurredAt == lastOccurredAt.Value && t.Id < lastId.Value));
}
var list = await query
.OrderByDescending(t => t.OccurredAt)
.OrderByDescending(t => t.Id)
.Page(1, pageSize)
.ToListAsync();
var lastRecord = list.Count > 0 ? list.Last() : null;
return (list, lastRecord?.OccurredAt, lastRecord?.Id ?? 0);
}
public async Task<long> GetTotalCountAsync()
{
return await FreeSql.Select<TransactionRecord>().CountAsync();
} }
public async Task<List<string>> GetDistinctClassifyAsync() public async Task<List<string>> GetDistinctClassifyAsync()

View File

@@ -12,6 +12,7 @@
<h3>交易详情</h3> <h3>交易详情</h3>
</div> </div>
<div class="scroll-area">
<van-form @submit="onSubmit"> <van-form @submit="onSubmit">
<van-cell-group inset> <van-cell-group inset>
<van-cell title="卡号" :value="transaction.card" /> <van-cell title="卡号" :value="transaction.card" />
@@ -102,6 +103,7 @@
</van-button> </van-button>
</div> </div>
</van-form> </van-form>
</div>
</div> </div>
</van-popup> </van-popup>
@@ -334,13 +336,27 @@ const formatDate = (dateString) => {
<style scoped> <style scoped>
.transaction-detail { .transaction-detail {
padding-bottom: 20px; display: flex;
flex-direction: column;
height: 100%;
}
.detail-header {
flex-shrink: 0;
border-bottom: 1px solid #ebedf0;
} }
.detail-header h3 { .detail-header h3 {
margin: 10px 0; margin: 10px 0;
} }
.scroll-area {
flex: 1;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
padding-bottom: 20px;
}
.classify-buttons { .classify-buttons {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@@ -175,8 +175,8 @@ const emailList = ref([])
const loading = ref(false) const loading = ref(false)
const refreshing = ref(false) const refreshing = ref(false)
const finished = ref(false) const finished = ref(false)
const lastId = ref(null) // 游标分页记录最后一条记录的ID const pageIndex = ref(1) // 页码
const lastTime = ref(null) // 游标分页:记录最后一条记录的时间 const pageSize = 20 // 每页数量
const total = ref(0) const total = ref(0)
const detailVisible = ref(false) const detailVisible = ref(false)
const currentEmail = ref(null) const currentEmail = ref(null)
@@ -192,18 +192,16 @@ const loadData = async (isRefresh = false) => {
if (loading.value) return // 防止重复加载 if (loading.value) return // 防止重复加载
if (isRefresh) { if (isRefresh) {
lastId.value = null pageIndex.value = 1
lastTime.value = null
emailList.value = [] emailList.value = []
finished.value = false finished.value = false
} }
loading.value = true loading.value = true
try { try {
const params = {} const params = {
if (lastTime.value && lastId.value) { pageIndex: pageIndex.value,
params.lastReceivedDate = lastTime.value pageSize: pageSize
params.lastId = lastId.value
} }
const response = await getEmailList(params) const response = await getEmailList(params)
@@ -211,8 +209,6 @@ const loadData = async (isRefresh = false) => {
if (response.success) { if (response.success) {
const newList = response.data || [] const newList = response.data || []
total.value = response.total || 0 total.value = response.total || 0
const newLastId = response.lastId || 0
const newLastTime = response.lastTime
if (isRefresh) { if (isRefresh) {
emailList.value = newList emailList.value = newList
@@ -220,17 +216,12 @@ const loadData = async (isRefresh = false) => {
emailList.value = [...(emailList.value || []), ...newList] emailList.value = [...(emailList.value || []), ...newList]
} }
// 更新游标 // 判断是否还有更多数据返回数据少于pageSize条或为空说明没有更多了
if (newLastId > 0 && newLastTime) { if (newList.length === 0 || newList.length < pageSize) {
lastId.value = newLastId
lastTime.value = newLastTime
}
// 判断是否还有更多数据返回数据少于20条或为空说明没有更多了
if (newList.length === 0 || newList.length < 20) {
finished.value = true finished.value = true
} else { } else {
finished.value = false finished.value = false
pageIndex.value++
} }
} else { } else {
showToast(response.message || '加载数据失败') showToast(response.message || '加载数据失败')

View File

@@ -40,19 +40,19 @@
<!-- 月度概览卡片 --> <!-- 月度概览卡片 -->
<div class="overview-card"> <div class="overview-card">
<div class="overview-item"> <div class="overview-item clickable" @click="goToTypeOverviewBills(0)">
<div class="label">总支出</div> <div class="label">总支出</div>
<div class="value expense">¥{{ formatMoney(monthlyData.totalExpense) }}</div> <div class="value expense">¥{{ formatMoney(monthlyData.totalExpense) }}</div>
<div class="sub-text">{{ monthlyData.expenseCount }}</div> <div class="sub-text">{{ monthlyData.expenseCount }}</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="overview-item"> <div class="overview-item clickable" @click="goToTypeOverviewBills(1)">
<div class="label">总收入</div> <div class="label">总收入</div>
<div class="value income">¥{{ formatMoney(monthlyData.totalIncome) }}</div> <div class="value income">¥{{ formatMoney(monthlyData.totalIncome) }}</div>
<div class="sub-text">{{ monthlyData.incomeCount }}</div> <div class="sub-text">{{ monthlyData.incomeCount }}</div>
</div> </div>
<div class="divider"></div> <div class="divider"></div>
<div class="overview-item"> <div class="overview-item clickable" @click="goToTypeOverviewBills(null)">
<div class="label">结余</div> <div class="label">结余</div>
<div class="value" :class="monthlyData.balance >= 0 ? 'income' : 'expense'"> <div class="value" :class="monthlyData.balance >= 0 ? 'income' : 'expense'">
{{ monthlyData.balance >= 0 ? '' : '-' }}¥{{ formatMoney(Math.abs(monthlyData.balance)) }} {{ monthlyData.balance >= 0 ? '' : '-' }}¥{{ formatMoney(Math.abs(monthlyData.balance)) }}
@@ -257,7 +257,7 @@
<div class="category-bills"> <div class="category-bills">
<div class="popup-header"> <div class="popup-header">
<h3>{{ selectedCategoryTitle }}</h3> <h3>{{ selectedCategoryTitle }}</h3>
<p v-if="categoryBills.length"> {{ categoryBills.length }} 笔交易</p> <p v-if="categoryBillsTotal"> {{ categoryBillsTotal }} 笔交易</p>
</div> </div>
<div class="bills-scroll-container"> <div class="bills-scroll-container">
@@ -306,11 +306,12 @@ const billListVisible = ref(false)
const billListLoading = ref(false) const billListLoading = ref(false)
const billListFinished = ref(false) const billListFinished = ref(false)
const categoryBills = ref([]) const categoryBills = ref([])
const categoryBillsTotal = ref(0)
const selectedCategoryTitle = ref('') const selectedCategoryTitle = ref('')
const selectedClassify = ref('') const selectedClassify = ref('')
const selectedType = ref(null) const selectedType = ref(null)
const lastBillId = ref(null) const billPageIndex = ref(1)
const lastBillTime = ref(null) const billPageSize = 20
// 详情编辑相关 // 详情编辑相关
const detailVisible = ref(false) const detailVisible = ref(false)
@@ -569,11 +570,29 @@ const goToCategoryBills = (classify, type) => {
// 重置分页状态 // 重置分页状态
categoryBills.value = [] categoryBills.value = []
lastBillId.value = null categoryBillsTotal.value = 0
lastBillTime.value = null billPageIndex.value = 1
billListFinished.value = false billListFinished.value = false
billListVisible.value = true billListVisible.value = true
// 打开弹窗后加载数据
loadCategoryBills()
}
// 打开总支出/总收入的所有账单列表
const goToTypeOverviewBills = (type) => {
selectedClassify.value = null
selectedType.value = type
selectedCategoryTitle.value = `${type === 0 ? '总支出' : '总收入'} - 明细`
// 重置分页状态
categoryBills.value = []
billPageIndex.value = 1
billListFinished.value = false
billListVisible.value = true
// 打开弹窗后加载数据
loadCategoryBills()
} }
// 加载分类账单数据 // 加载分类账单数据
@@ -583,15 +602,17 @@ const loadCategoryBills = async () => {
billListLoading.value = true billListLoading.value = true
try { try {
const params = { const params = {
classify: selectedClassify.value, pageIndex: billPageIndex.value,
pageSize: billPageSize,
type: selectedType.value, type: selectedType.value,
year: currentYear.value, year: currentYear.value,
month: currentMonth.value month: currentMonth.value,
sortByAmount: true
} }
if (lastBillTime.value && lastBillId.value) { // 仅当选择了分类时才添加classify参数
params.lastOccurredAt = lastBillTime.value if (selectedClassify.value !== null) {
params.lastId = lastBillId.value params.classify = selectedClassify.value
} }
const response = await getTransactionList(params) const response = await getTransactionList(params)
@@ -599,15 +620,13 @@ const loadCategoryBills = async () => {
if (response.success) { if (response.success) {
const newList = response.data || [] const newList = response.data || []
categoryBills.value = [...categoryBills.value, ...newList] categoryBills.value = [...categoryBills.value, ...newList]
categoryBillsTotal.value = response.total
if (newList.length > 0) { if (newList.length === 0 || newList.length < billPageSize) {
const lastRecord = newList[newList.length - 1]
lastBillId.value = response.lastId || lastRecord.id
lastBillTime.value = response.lastTime || lastRecord.occurredAt
}
if (newList.length === 0 || newList.length < 20) {
billListFinished.value = true billListFinished.value = true
} else {
billListFinished.value = false
billPageIndex.value++
} }
} else { } else {
showToast(response.message || '加载账单失败') showToast(response.message || '加载账单失败')
@@ -645,8 +664,7 @@ const onBillSave = async () => {
// 刷新账单列表 // 刷新账单列表
categoryBills.value = [] categoryBills.value = []
lastBillId.value = null billPageIndex.value = 1
lastBillTime.value = null
billListFinished.value = false billListFinished.value = false
await loadCategoryBills() await loadCategoryBills()
@@ -700,6 +718,23 @@ onActivated(() => {
text-align: center; text-align: center;
} }
.overview-item.clickable {
cursor: pointer;
transition: background-color 0.2s;
padding: 8px;
border-radius: 8px;
}
.overview-item.clickable:active {
background-color: #f0f0f0;
}
@media (prefers-color-scheme: dark) {
.overview-item.clickable:active {
background-color: #2c2c2c;
}
}
.overview-item .label { .overview-item .label {
font-size: 13px; font-size: 13px;
color: var(--van-text-color-2); color: var(--van-text-color-2);

View File

@@ -191,8 +191,8 @@ const transactionList = ref([])
const loading = ref(false) const loading = ref(false)
const refreshing = ref(false) const refreshing = ref(false)
const finished = ref(false) const finished = ref(false)
const lastId = ref(null) const pageIndex = ref(1)
const lastTime = ref(null) const pageSize = 20
const total = ref(0) const total = ref(0)
const detailVisible = ref(false) const detailVisible = ref(false)
const currentTransaction = ref(null) const currentTransaction = ref(null)
@@ -251,18 +251,16 @@ const loadClassifyList = async (type = null) => {
// 加载数据 // 加载数据
const loadData = async (isRefresh = false) => { const loadData = async (isRefresh = false) => {
if (isRefresh) { if (isRefresh) {
lastId.value = null pageIndex.value = 1
lastTime.value = null
transactionList.value = [] transactionList.value = []
finished.value = false finished.value = false
} }
loading.value = true loading.value = true
try { try {
const params = {} const params = {
if (lastTime.value && lastId.value) { pageIndex: pageIndex.value,
params.lastOccurredAt = lastTime.value pageSize: pageSize
params.lastId = lastId.value
} }
// 添加搜索关键词 // 添加搜索关键词
@@ -275,8 +273,6 @@ const loadData = async (isRefresh = false) => {
if (response.success) { if (response.success) {
const newList = response.data || [] const newList = response.data || []
total.value = response.total || 0 total.value = response.total || 0
const newLastId = response.lastId || 0
const newLastTime = response.lastTime
if (isRefresh) { if (isRefresh) {
transactionList.value = newList transactionList.value = newList
@@ -284,15 +280,11 @@ const loadData = async (isRefresh = false) => {
transactionList.value = [...(transactionList.value || []), ...newList] transactionList.value = [...(transactionList.value || []), ...newList]
} }
if (newLastId > 0 && newLastTime) { if (newList.length === 0 || newList.length < pageSize) {
lastId.value = newLastId
lastTime.value = newLastTime
}
if (newList.length === 0 || newList.length < 20) {
finished.value = true finished.value = true
} else { } else {
finished.value = false finished.value = false
pageIndex.value++
} }
} else { } else {
showToast(response.message || '加载数据失败') showToast(response.message || '加载数据失败')

View File

@@ -16,41 +16,45 @@ public class TransactionRecordController(
/// </summary> /// </summary>
[HttpGet] [HttpGet]
public async Task<PagedResponse<TransactionRecord>> GetListAsync( public async Task<PagedResponse<TransactionRecord>> GetListAsync(
[FromQuery] DateTime? lastOccurredAt = null, [FromQuery] int pageIndex = 1,
[FromQuery] long? lastId = null, [FromQuery] int pageSize = 20,
[FromQuery] string? searchKeyword = null, [FromQuery] string? searchKeyword = null,
[FromQuery] string? classify = null, [FromQuery] string? classify = null,
[FromQuery] int? type = null, [FromQuery] int? type = null,
[FromQuery] int? year = null, [FromQuery] int? year = null,
[FromQuery] int? month = null [FromQuery] int? month = null,
[FromQuery] bool sortByAmount = false
) )
{ {
try try
{ {
TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null; TransactionType? transactionType = type.HasValue ? (TransactionType)type.Value : null;
var (list, lastTime, lastIdResult) = await transactionRepository.GetPagedListAsync( var list = await transactionRepository.GetPagedListAsync(
lastOccurredAt, pageIndex,
lastId, pageSize,
20, searchKeyword,
classify,
transactionType,
year,
month,
sortByAmount);
var total = await transactionRepository.GetTotalCountAsync(
searchKeyword, searchKeyword,
classify, classify,
transactionType, transactionType,
year, year,
month); month);
var total = await transactionRepository.GetTotalCountAsync();
return new PagedResponse<TransactionRecord> return new PagedResponse<TransactionRecord>
{ {
Success = true, Success = true,
Data = list.ToArray(), Data = list.ToArray(),
Total = (int)total, Total = (int)total
LastId = lastIdResult,
LastTime = lastTime
}; };
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(ex, "获取交易记录列表失败,时间: {LastTime}, ID: {LastId}", lastOccurredAt, lastId); logger.LogError(ex, "获取交易记录列表失败,页码: {PageIndex}, 页大小: {PageSize}", pageIndex, pageSize);
return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}"); return PagedResponse<TransactionRecord>.Fail($"获取交易记录列表失败: {ex.Message}");
} }
} }