From 4737f160b7de342b9cc030b63711e436114452ac Mon Sep 17 00:00:00 2001 From: wangxiaogang <1433729587@qq.com> Date: Fri, 29 May 2026 17:33:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20v1.4=20Bug=20=E4=BF=AE=E5=A4=8D=E4=B8=8E?= =?UTF-8?q?=E4=BD=93=E9=AA=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复所有日期相关逻辑使用 UTC 导致时区偏移问题 - 首页今日数据改用 stats API 聚合查询,消除 100 条上限 - 账单分页加载添加请求序号,防止并发覆盖 - 统计页 maxSingle 改为按金额排序 - 统计页 tab 切换时刷新月对比数据 - 首页 onShow 跳过首次加载,避免双重请求 - 今日卡片同时展示支出/收入,骨架屏对齐 - 账单页统一左滑删除,去掉长按 - 后端 transaction 支持 sortBy 参数 - 后端 stats overview 支持 startDate/endDate 查询 --- client/src/pages/add/index.vue | 6 ++- client/src/pages/bills/index.vue | 6 ++- client/src/pages/index/index.vue | 74 +++++++++++++++++++----------- client/src/pages/profile/index.vue | 9 +++- client/src/pages/stats/index.vue | 6 ++- server/src/routes/stats.ts | 32 +++++++++---- server/src/routes/transaction.ts | 6 ++- 7 files changed, 94 insertions(+), 45 deletions(-) diff --git a/client/src/pages/add/index.vue b/client/src/pages/add/index.vue index 6f6557c..510f992 100644 --- a/client/src/pages/add/index.vue +++ b/client/src/pages/add/index.vue @@ -67,12 +67,14 @@ const type = ref<'expense' | 'income'>('expense') const amountStr = ref('0') const selectedCat = ref(null) const note = ref('') -const selectedDate = ref(new Date().toISOString().slice(0, 10)) +const now = new Date() +const localToday = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` +const selectedDate = ref(localToday) const saving = ref(false) const editId = ref(null) const showSuccess = ref(false) const scrollToCat = ref('') -const todayStr = new Date().toISOString().slice(0, 10) +const todayStr = localToday // Remember last selected category per type const lastCatByType: Record = { expense: null, income: null } diff --git a/client/src/pages/bills/index.vue b/client/src/pages/bills/index.vue index 661ecfe..94c531d 100644 --- a/client/src/pages/bills/index.vue +++ b/client/src/pages/bills/index.vue @@ -26,7 +26,6 @@ :item="item" :hideDate="true" @tap="onEdit(item)" - @longpress="onDelete(item)" @delete="onDelete(item)" /> @@ -85,6 +84,7 @@ const pageSize = 20 const txList = ref([]) const total = ref(0) const loadError = ref(false) +let loadSeq = 0 // 请求序号,防止并发覆盖 const noMore = computed(() => txList.value.length >= total.value && total.value > 0) @@ -116,11 +116,14 @@ async function loadTx(reset = false) { page.value = 1 txList.value = [] } + const seq = ++loadSeq const params: any = { page: page.value, pageSize } if (filterType.value !== 'all') params.type = filterType.value try { loadError.value = false await txStore.fetchTransactions(params) + // 丢弃过期请求的结果 + if (seq !== loadSeq) return if (reset) { txList.value = txStore.transactions } else { @@ -128,6 +131,7 @@ async function loadTx(reset = false) { } total.value = txStore.total } catch (e) { + if (seq !== loadSeq) return console.error('Load bills error:', e) loadError.value = true } diff --git a/client/src/pages/index/index.vue b/client/src/pages/index/index.vue index 5396447..aa4730e 100644 --- a/client/src/pages/index/index.vue +++ b/client/src/pages/index/index.vue @@ -37,17 +37,28 @@ - + 今日支出 - {{ formatAmount(todayExpense) }} + {{ formatAmount(todayExpense) }} - - {{ todayCount }}笔 + + + 今日收入 + {{ formatAmount(todayIncome) }} + {{ todayCount }}笔 - - + + + + + + + + + + @@ -118,6 +129,7 @@ const recentTx = ref([]) const loading = ref(true) const loadError = ref(false) const todayExpense = ref(0) +const todayIncome = ref(0) const todayCount = ref(0) const greeting = computed(() => { @@ -133,16 +145,17 @@ async function loadData() { try { loading.value = true loadError.value = false - const today = new Date().toISOString().slice(0, 10) + const now = new Date() + const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}` await Promise.all([ statsStore.fetchOverview(), budgetStore.fetchBudget(), txStore.fetchTransactions({ page: 1 }), - fetchTodayExpense(today) + fetchTodayData(today) ]) overview.value = statsStore.overview budget.value = budgetStore.budget - recentTx.value = txStore.transactions.slice(0, 10) + recentTx.value = txStore.transactions.slice(0, 5) } catch (e) { console.error('Load error:', e) loadError.value = true @@ -151,26 +164,28 @@ async function loadData() { } } -async function fetchTodayExpense(today: string) { +async function fetchTodayData(today: string) { try { const { request } = await import('@/utils/request') - const data = await request({ - url: '/transactions', - data: { page: 1, pageSize: 100, type: 'expense', startDate: today, endDate: today } - }) - todayExpense.value = data.list.reduce((sum: number, t: any) => sum + t.amount, 0) - todayCount.value = data.list.length + const data = await request({ url: '/stats/overview', data: { startDate: today, endDate: today } }) + todayExpense.value = data.expense || 0 + todayIncome.value = data.income || 0 + todayCount.value = data.count || 0 } catch { todayExpense.value = 0 + todayIncome.value = 0 todayCount.value = 0 } } -onMounted(() => loadData()) +const initialLoaded = ref(false) +onMounted(() => { + loadData().finally(() => { initialLoaded.value = true }) +}) // Refresh data when returning from other pages (e.g., after adding a transaction) onShow(() => { - if (!loading.value) loadData() + if (initialLoaded.value && !loading.value) loadData() }) onPullDownRefresh(() => { @@ -311,11 +326,12 @@ function goBills() { border: 2rpx solid #F0E0D6; box-shadow: 4rpx 4rpx 12rpx rgba(45, 27, 27, 0.08); display: flex; - justify-content: space-between; align-items: center; + gap: 24rpx; } -.today-left { +.today-item { + flex: 1; display: flex; flex-direction: column; gap: 8rpx; @@ -328,18 +344,24 @@ function goBills() { .today-amount { font-family: 'Fredoka', sans-serif; - font-size: 36rpx; + font-size: 32rpx; font-weight: 600; - color: #FF6B6B; + + &.expense { color: #FF6B6B; } + &.income { color: #7BC67E; } } -.today-right { - text-align: right; +.today-divider { + width: 2rpx; + height: 56rpx; + background: #F0E0D6; + flex-shrink: 0; } .today-count { - font-size: 24rpx; - color: #8B7E7E; + font-size: 22rpx; + color: #BFB3B3; + flex-shrink: 0; } .quick-actions { diff --git a/client/src/pages/profile/index.vue b/client/src/pages/profile/index.vue index 9045b8c..0821ed5 100644 --- a/client/src/pages/profile/index.vue +++ b/client/src/pages/profile/index.vue @@ -109,6 +109,11 @@ function goCategoryManage() { uni.navigateTo({ url: '/pages/category-manage/index' }) } +function getLocalDateStr(): string { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` +} + async function exportData() { try { const data = await request({ url: '/transactions', data: { page: 1, pageSize: 9999 } }) @@ -141,7 +146,7 @@ async function exportData() { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `小菜记账_${new Date().toISOString().slice(0, 10)}.csv` + a.download = `小菜记账_${getLocalDateStr()}.csv` a.click() URL.revokeObjectURL(url) uni.showToast({ title: '导出成功', icon: 'success' }) @@ -149,7 +154,7 @@ async function exportData() { // #ifdef MP-WEIXIN const fs = uni.getFileSystemManager() - const filePath = `${wx.env.USER_DATA_PATH}/小菜记账_${new Date().toISOString().slice(0, 10)}.csv` + const filePath = `${wx.env.USER_DATA_PATH}/小菜记账_${getLocalDateStr()}.csv` fs.writeFileSync(filePath, csv, 'utf-8') uni.openDocument({ filePath, diff --git a/client/src/pages/stats/index.vue b/client/src/pages/stats/index.vue index a20e3aa..4c7542b 100644 --- a/client/src/pages/stats/index.vue +++ b/client/src/pages/stats/index.vue @@ -240,7 +240,9 @@ async function loadTabData() { try { await Promise.all([ statsStore.fetchCategoryStats(currentMonth.value, tabType.value), - statsStore.fetchTrend(currentMonth.value, tabType.value) + statsStore.fetchTrend(currentMonth.value, tabType.value), + fetchMaxSingle(), + fetchPrevMonthData() ]) categoryStats.value = statsStore.categoryStats trendData.value = statsStore.trendData @@ -258,7 +260,7 @@ async function fetchMaxSingle() { const { request } = await import('@/utils/request') const data = await request({ url: '/transactions', - data: { page: 1, pageSize: 1, type: tabType.value, startDate, endDate } + data: { page: 1, pageSize: 1, type: tabType.value, startDate, endDate, sortBy: 'amount' } }) maxSingle.value = data.list?.[0]?.amount || 0 } catch { diff --git a/server/src/routes/stats.ts b/server/src/routes/stats.ts index 71a4793..afadaae 100644 --- a/server/src/routes/stats.ts +++ b/server/src/routes/stats.ts @@ -7,11 +7,28 @@ const router = Router() router.get('/overview', async (req: AuthRequest, res: Response) => { try { - const { month } = req.query - const m = (month as string) || getCurrentMonth() - const range = getMonthRange(m) - if (!range) return res.status(400).json({ code: 40001, message: '月份格式无效' }) - const { startDate, endDate } = range + const { month, startDate: qStart, endDate: qEnd } = req.query + + let startDate: string + let endDate: string + let elapsedDays = 1 + + if (qStart && qEnd) { + // 按日期范围查询(用于今日等场景) + startDate = qStart as string + endDate = qEnd as string + elapsedDays = 1 + } else { + const m = (month as string) || getCurrentMonth() + const range = getMonthRange(m) + if (!range) return res.status(400).json({ code: 40001, message: '月份格式无效' }) + startDate = range.startDate + endDate = range.endDate + const now = new Date() + const [year, mon] = m.split('-').map(Number) + const isCurrentMonth = year === now.getFullYear() && mon === now.getMonth() + 1 + elapsedDays = isCurrentMonth ? now.getDate() : new Date(year, mon, 0).getDate() + } const [rows] = await pool.query( `SELECT @@ -22,11 +39,6 @@ router.get('/overview', async (req: AuthRequest, res: Response) => { [req.userId, startDate, endDate] ) const row = (rows as any[])[0] - - const now = new Date() - const [year, mon] = m.split('-').map(Number) - const isCurrentMonth = year === now.getFullYear() && mon === now.getMonth() + 1 - const elapsedDays = isCurrentMonth ? now.getDate() : new Date(year, mon, 0).getDate() const daily = elapsedDays > 0 ? Math.round(row.expense / elapsedDays) : 0 res.json({ diff --git a/server/src/routes/transaction.ts b/server/src/routes/transaction.ts index 669f390..dc3109f 100644 --- a/server/src/routes/transaction.ts +++ b/server/src/routes/transaction.ts @@ -35,7 +35,7 @@ router.get('/:id', async (req: AuthRequest, res: Response) => { router.get('/', async (req: AuthRequest, res: Response) => { try { - const { page, pageSize, type, startDate, endDate } = req.query + const { page, pageSize, type, startDate, endDate, sortBy } = req.query const pSize = safeInt(pageSize, 20, 1, 100) const pNum = safeInt(page, 1, 1, 99999) const offset = (pNum - 1) * pSize @@ -53,12 +53,14 @@ router.get('/', async (req: AuthRequest, res: Response) => { where += ' AND t.date <= ?'; params.push(endDate) } + const orderBy = sortBy === 'amount' ? 't.amount DESC' : 't.date DESC, t.created_at DESC' + const [rows] = await pool.query( `SELECT t.*, c.name as category_name, c.icon as category_icon, c.color as category_color FROM transactions t LEFT JOIN categories c ON t.category_id = c.id ${where} - ORDER BY t.date DESC, t.created_at DESC + ORDER BY ${orderBy} LIMIT ? OFFSET ?`, [...params, pSize, offset] )