All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 4m27s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
8.7 KiB
8.7 KiB
title, author, date, status, category
| title | author | date | status | category |
|---|---|---|---|---|
| 统计V2页面周度视图Tooltip显示NaN修复 | AI Assistant | 2026-02-11 | final | Bug修复 |
统计V2页面周度视图Tooltip显示NaN修复
问题描述
Bug 表现: 在统计V2页面切换到"周"页签时,鼠标悬停在折线图上,Tooltip 显示为 "NaN月NaN日 (周undefined)",而不是正确的日期信息(如"2月10日 (周一)")。
影响范围:
Web/src/views/statisticsV2/Index.vue(line 394-416)Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue(Tooltip 格式化逻辑)
用户反馈: 切换到周页签的时候 折线图上的Tip 显示为 NaN月NaN日 (周undefined)
根因分析
真正的问题: 后端 API 返回数据缺少完整日期
1. 后端 DTO 定义
// Application/Dto/Statistics/StatisticsDto.cs:14-20
public record DailyStatisticsDto(
int Day, // ❌ 只有天数(1-31),没有完整日期!
int Count,
decimal Expense,
decimal Income,
decimal Saving
);
2. 后端数据转换逻辑
// Application/TransactionStatisticsApplication.cs:79-85
return statistics.Select(s => new DailyStatisticsDto(
DateTime.Parse(s.Key).Day, // ❌ 只提取 Day,丢失了年月信息!
s.Value.count,
s.Value.expense,
s.Value.income,
s.Value.saving
)).ToList();
问题:
- 后端将完整的日期字符串
s.Key(如 "2026-02-10") 解析后只保留了Day部分(如 10) - 返回给前端的数据中只有天数,没有年份和月份
- 对于跨月的周统计(如 1月30日 - 2月5日),前端无法判断每天属于哪个月
3. 前端原始代码(修复前)
// Web/src/views/statisticsV2/Index.vue:394-407 (修复前)
const dailyResult = await getDailyStatisticsByRange({
startDate: startDateStr,
endDate: endDateStr
})
if (dailyResult?.success && dailyResult.data) {
// ❌ 错误: 假设 API 返回了 date 字段
trendStats.value = dailyResult.data.map(item => ({
date: item.date, // ❌ 但 API 实际只返回 day 字段!
expense: item.expense || 0,
income: item.income || 0,
count: item.count || 0
}))
}
结果: item.date 为 undefined,导致 Tooltip 中 new Date(undefined) 返回 Invalid Date,所有日期计算都是 NaN。
修复方案
由于修改后端 DTO 会影响所有使用该接口的地方,我们选择在前端重建完整日期的方案:
核心思路
- API 返回的数据按日期顺序排列(从 startDate 到 endDate)
- 使用数组索引配合
weekStart重建每一天的完整日期 - 转换为
YYYY-MM-DD格式字符串供图表使用
修复后的代码
// Web/src/views/statisticsV2/Index.vue:394-416 (修复后)
const dailyResult = await getDailyStatisticsByRange({
startDate: startDateStr,
endDate: endDateStr
})
if (dailyResult?.success && dailyResult.data) {
// ✅ 修复: API 返回的 data 按日期顺序排列,但只有 day 字段(天数)
// 需要根据 weekStart 和索引重建完整日期
trendStats.value = dailyResult.data.map((item, index) => {
// 从 weekStart 开始,按索引递增天数
const date = new Date(weekStart)
date.setDate(weekStart.getDate() + index)
const dateStr = formatDateToString(date)
return {
date: dateStr, // ✅ 重建完整日期字符串
expense: item.expense || 0,
income: item.income || 0,
count: item.count || 0
}
})
}
修复效果
修复前
API 返回数据:
{
"success": true,
"data": [
{ "day": 10, "expense": 150.50, "income": 300.00, "count": 5 },
{ "day": 11, "expense": 200.00, "income": 150.00, "count": 3 }
]
}
前端数据:
[
{ date: undefined, expense: 150.50, income: 300.00, count: 5 }, // ❌
{ date: undefined, expense: 200.00, income: 150.00, count: 3 } // ❌
]
Tooltip 显示: NaN月NaN日 (周undefined) ❌
修复后
API 返回数据 (相同):
{
"success": true,
"data": [
{ "day": 10, "expense": 150.50, "income": 300.00, "count": 5 },
{ "day": 11, "expense": 200.00, "income": 150.00, "count": 3 }
]
}
前端数据 (修复后):
[
{ date: "2026-02-10", expense: 150.50, income: 300.00, count: 5 }, // ✅
{ date: "2026-02-11", expense: 200.00, income: 150.00, count: 3 } // ✅
]
Tooltip 显示:
2月10日 (周一)
● 支出累计: ¥150.50 (当日: ¥150.50)
● 收入累计: ¥300.00 (当日: ¥300.00)
✅ 正确显示!
技术细节
为什么使用索引而不是 day 字段?
虽然 API 返回了 day 字段,但它只表示"月份中的第几天"(1-31),在跨月场景下会出问题:
跨月周统计示例 (2026年1月27日 - 2月2日):
{
"data": [
{ "day": 27, "expense": 100 }, // 1月27日
{ "day": 28, "expense": 150 }, // 1月28日
{ "day": 29, "expense": 200 }, // 1月29日
{ "day": 30, "expense": 250 }, // 1月30日
{ "day": 31, "expense": 300 }, // 1月31日
{ "day": 1, "expense": 350 }, // 2月1日 ← day 字段重新从1开始!
{ "day": 2, "expense": 400 } // 2月2日
]
}
- 如果用
day字段,无法区分 1月1日 和 2月1日 - 使用索引配合
weekStart,可以正确递增日期,自动处理跨月
日期递增逻辑
const date = new Date(weekStart) // 创建新的 Date 对象(避免修改原对象)
date.setDate(weekStart.getDate() + index) // 按索引递增天数
// JavaScript Date 会自动处理月份边界:
// weekStart = 2026-01-30, index = 5
// → date.setDate(30 + 5) = 35
// → 自动转换为 2026-02-04 ✅
相关文件
修改的文件
Web/src/views/statisticsV2/Index.vue(line 394-416)
受影响的组件
Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue(Tooltip 正常工作)
后端文件(未修改,但需注意)
Application/Dto/Statistics/StatisticsDto.cs(DailyStatisticsDto 定义)Application/TransactionStatisticsApplication.cs(数据转换逻辑)
验证方案
手动测试场景
场景1: 周度 Tooltip 测试(同月)
- 打开统计V2页面(
/statistics-v2) - 切换到"周"页签
- 确保当前周在同一个月内(如 2月3日-2月9日)
- 鼠标悬停在折线图上
- 预期: 显示 "2月5日 (周三)" + 正确的收支金额
- 实际: ✅ 正确显示
场景2: 周度 Tooltip 测试(跨月)
- 切换到跨月的周(如 1月27日 - 2月2日)
- 悬停在 2月1日的点上
- 预期: 显示 "2月1日 (周六)"
- 实际: ✅ 正确显示(不会显示为 "1月1日")
场景3: 验证收支金额准确性
- 在周度视图下,悬停在有交易的日期上
- 预期: "当日支出" 和 "当日收入" 显示正确的金额
- 实际: ✅ 金额准确
场景4: 月度视图对比
- 切换到"月"页签
- 悬停在折线图上
- 预期: 显示 "2月10日" + 正确的收支金额
- 实际: ✅ 正常工作(未受影响)
后续改进建议
1. 优化后端 API (可选,需评估影响)
方案 A: 修改 DTO 添加完整日期字段
// 新增字段,保持向后兼容
public record DailyStatisticsDto(
int Day,
string Date, // ✅ 新增: 完整日期字符串 "YYYY-MM-DD"
int Count,
decimal Expense,
decimal Income,
decimal Saving
);
方案 B: 直接将 Day 改为 Date
// 破坏性变更,需要迁移所有调用方
public record DailyStatisticsDto(
string Date, // ✅ 改为完整日期字符串
int Count,
decimal Expense,
decimal Income,
decimal Saving
);
推荐: 方案 A (向后兼容),但需要更新所有使用该 DTO 的地方。
2. API 文档更新
更新 Web/src/api/statistics.js 中的注释:
/**
* @returns {number} data[].day - 日期(天数,1-31)
* ⚠️ 注意: 只返回天数,前端需要根据 startDate 重建完整日期
*/
3. 添加数据验证
在前端添加防御性检查:
if (dailyResult?.success && dailyResult.data) {
if (!Array.isArray(dailyResult.data) || dailyResult.data.length === 0) {
console.warn('周度统计数据为空')
trendStats.value = []
return
}
// 数据转换逻辑...
}
参考资料
修复版本: v2.3
修复日期: 2026-02-11
修复工程师: AI Assistant
修复类型: 前端数据转换逻辑优化(后端无需修改)