1
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Waiting to run
Docker Build & Deploy / Deploy to Production (push) Has been cancelled
Docker Build & Deploy / Cleanup Dangling Images (push) Has been cancelled
Docker Build & Deploy / WeChat Notification (push) Has been cancelled

This commit is contained in:
SunCheng
2026-02-18 21:16:45 +08:00
parent 77c9b47246
commit c49f66757e
116 changed files with 6909 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,51 @@
## Context
当前图表系统使用 Chart.js 作为底层库,通过 Vue 3 组件封装在 `Web/src/components/Charts/` 目录下。存在以下问题:
1. **支出分类饼图**: 使用镂空饼图(Doughnut)但未在中心展示总金额,分类标签直接覆盖在图表上导致与图标重叠
2. **收支折线图**: 展示整月日期1-31日即使当前只有部分日期有数据剩余日期显示为平直线
3. **预算仪表图**: 存在运行时错误,仪表图在容器中布局错位
## Goals / Non-Goals
**Goals:**
- 在饼图中心展示总金额,优化数据可读性
- 修复折线图展示无效未来日期的问题
- 修复预算页面报错
- 调整仪表图布局使其居中展示
**Non-Goals:**
- 不更换图表库(仍使用 Chart.js
- 不改变现有配色方案
- 不添加新的图表类型
## Decisions
### 1. 饼图中心展示方案
**选择**: 使用 Chart.js 的 `plugins.datalabels` 配合自定义 `afterDraw` 钩子在中心绘制文本
**理由**: 比使用 HTML overlay 更简洁,与 Chart.js 原生集成,响应式适配更好
**替代方案**: HTML 绝对定位 overlay更灵活但增加复杂度
### 2. 折线图日期范围过滤
**选择**: 在组件层根据 `data.length` 动态计算 `labels` 数组,过滤掉未来日期
**理由**: 数据源包含整月数据但当前日期后无实际值,前端过滤避免后端改动
**替代方案**: 修改后端 API 返回(需要协调后端,改动成本高)
### 3. 仪表图布局修复
**选择**: 使用 CSS Flexbox 垂直水平居中,配合 `maintainAspectRatio: false` 和固定高度
**理由**: 解决容器自适应导致的错位问题,确保在不同屏幕尺寸下保持居中
## Risks / Trade-offs
- **[风险]** 饼图中心文字在小屏幕上可能显示不全 → **缓解**: 使用相对字体大小,添加 `resize` 监听器动态调整
- **[风险]** 折线图动态范围可能影响用户查看完整月度趋势的预期 → **缓解**: 在图表下方添加日期范围说明文字
- **[权衡]** 仪表图固定高度可能在极端屏幕尺寸下出现留白 → 接受此权衡,保证核心显示区域
## Migration Plan
无需迁移步骤,所有改动均为前端展示层优化,不影响数据存储。
## Open Questions
- 折线图是否需要添加切换按钮允许用户查看整月趋势?
- 饼图中心文字格式是否需要支持多货币显示?

View File

@@ -0,0 +1,25 @@
## Why
当前图表组件存在多个用户体验问题:支出分类饼图缺少关键信息展示、收支折线图包含无效的未来日期数据、预算仪表图存在布局错位和运行时错误。这些问题影响数据可视化的准确性和美观性,需要统一优化以提升用户体验。
## What Changes
- **支出分类饼图优化**: 在饼图中心镂空区域展示总金额,优化标签位置避免与图标重叠
- **收支折线图优化**: 移除当前日期之后的无效未来日期数据点,仅展示实际有数据的日期范围
- **预算仪表图修复与优化**: 修复页面报错,调整仪表图布局解决错位问题,提升视觉美观度
## Capabilities
### New Capabilities
- `pie-chart-center-label`: 在饼图中心展示总金额的能力
- `pie-chart-label-positioning`: 饼图分类标签智能定位避免遮挡图标
- `line-chart-dynamic-range`: 折线图根据实际数据动态调整日期范围
### Modified Capabilities
- `budget-gauge-display`: 预算仪表图的展示逻辑和布局要求变更
## Impact
- **前端**: 修改 Web/src/components/Charts/ 下的饼图、折线图、仪表图组件
- **依赖**: Chart.js 配置选项调整,可能涉及 chartjs-plugin-datalabels
- **页面**: 影响统计页面、预算页面的图表展示

View File

@@ -0,0 +1,29 @@
## MODIFIED Requirements
### Requirement: 仪表图容器布局
预算仪表图 SHALL 在容器内正确居中显示,无错位。
#### Scenario: 垂直居中展示
- **WHEN** 用户查看预算页面的仪表图
- **THEN** 仪表图 SHALL 在容器内垂直居中
- **AND** SHALL 在容器内水平居中
- **AND** 与上下其他元素 SHALL 保持适当间距16px
#### Scenario: 响应式布局
- **WHEN** 用户在不同屏幕尺寸下查看仪表图
- **THEN** 仪表图 SHALL 保持居中不偏移
- **AND** 容器高度 SHALL 自适应确保图表完整显示
### Requirement: 页面错误处理
预算页面 SHALL 正确加载并显示仪表图,无运行时错误。
#### Scenario: 正常加载
- **WHEN** 用户访问预算页面
- **THEN** 页面 SHALL 无 JavaScript 错误
- **AND** 仪表图 SHALL 正常渲染
- **AND** 所有交互功能 SHALL 正常工作
#### Scenario: 错误边界处理
- **WHEN** 仪表图组件发生异常
- **THEN** 系统 SHALL 捕获错误并显示友好提示
- **AND** SHALL 不阻塞页面其他功能

View File

@@ -0,0 +1,24 @@
## ADDED Requirements
### Requirement: 动态日期范围
收支折线图 SHALL 仅展示有实际数据的日期范围,不包含未来无效日期。
#### Scenario: 当前日期之前的趋势展示
- **WHEN** 用户查看收支折线图例如当前为17号
- **THEN** 图表 SHALL 只展示从月初到当前日期的数据点
- **AND** SHALL 不包含17号之后到月底的空白日期
- **AND** X轴标签 SHALL 对应实际有数据的日期
#### Scenario: 整月数据展示
- **WHEN** 用户查看历史月份的收支折线图
- **THEN** 图表 SHALL 展示该月的完整日期范围1号到月末
- **AND** 所有日期点 SHALL 有对应的数据值
### Requirement: 数据点过滤逻辑
系统 SHALL 根据当前日期自动过滤未来日期的数据点。
#### Scenario: 实时数据过滤
- **WHEN** 组件加载当月收支数据
- **THEN** 系统 SHALL 获取当前日期
- **AND** SHALL 过滤掉 labels 数组中大于当前日期的日期
- **AND** SHALL 同步过滤 datasets 中对应的空数据点

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: 饼图中心展示总金额
支出分类饼图 SHALL 在镂空区域中心位置展示当前选中数据的总金额。
#### Scenario: 显示总支出金额
- **WHEN** 用户查看统计页面的支出分类饼图
- **THEN** 系统 SHALL 在饼图中心显示当前展示分类的总支出金额
- **AND** 金额格式 SHALL 使用人民币格式¥xx,xxx.xx
#### Scenario: 响应式适配
- **WHEN** 用户在不同屏幕尺寸下查看饼图
- **THEN** 中心文字 SHALL 自动调整大小以适应饼图尺寸
- **AND** 文字 SHALL 始终保持水平和垂直居中
### Requirement: 中心文字样式
饼图中心文字 SHALL 使用统一的视觉样式。
#### Scenario: 样式一致性
- **WHEN** 系统渲染中心金额文字
- **THEN** 字体大小 SHALL 为图表高度的 20%
- **AND** 字体粗细 SHALL 为 bold
- **AND** 字体颜色 SHALL 使用主题主色(#333333 或暗色主题对应色)

View File

@@ -0,0 +1,24 @@
## ADDED Requirements
### Requirement: 分类标签智能定位
饼图的分类标签 SHALL 避免与图标重叠,并清晰展示分类名称。
#### Scenario: 标签位置优化
- **WHEN** 系统渲染支出分类饼图
- **THEN** 分类标签 SHALL 显示在饼图扇区外侧
- **AND** 标签 SHALL 通过引导线与对应扇区连接
- **AND** 标签文字 SHALL 显示分类名称而非仅在图标上叠加
#### Scenario: 避免标签重叠
- **WHEN** 多个分类扇区相邻且较小时
- **THEN** 系统 SHALL 自动调整标签位置避免相互重叠
- **AND** 当空间不足时 SHALL 使用图例(legend)代替直接标签
### Requirement: 图标与标签分离
分类图标和分类名称 SHALL 分开展示,不互相遮挡。
#### Scenario: 清晰的视觉层次
- **WHEN** 用户查看饼图
- **THEN** 分类图标 SHALL 显示在饼图扇区内部或作为图例图标
- **AND** 分类名称 SHALL 显示在标签位置而非图标上
- **AND** 两者 SHALL 不重叠遮挡

View File

@@ -0,0 +1,77 @@
## 1. 饼图中心金额展示
- [x] 1.1 创建饼图中心文本绘制插件或自定义 afterDraw 钩子
- [x] 1.2 计算并格式化总金额(人民币格式 ¥xx,xxx.xx
- [x] 1.3 实现响应式字体大小调整图表高度的20%
- [x] 1.4 确保文字水平和垂直居中显示
- [x] 1.5 适配暗色主题颜色
## 2. 饼图标签位置优化
- [x] 2.1 调整 Chart.js datalabels 配置,将标签移至扇区外侧
- [x] 2.2 配置引导线连接标签与对应扇区
- [x] 2.3 确保标签显示分类名称而非仅图标
- [x] 2.4 实现标签防重叠逻辑(小扇区自动调整位置)
- [x] 2.5 必要时使用图例(legend)作为标签替代方案
## 3. 折线图日期范围过滤
- [x] 3.1 在折线图组件中获取当前日期
- [x] 3.2 实现数据过滤函数,移除未来日期数据点
- [x] 3.3 同步过滤 labels 和 datasets 数据
- [x] 3.4 处理历史月份数据(展示完整月份)
- [x] 3.5 添加日期范围说明文字(可选)
## 4. 预算页面报错修复
- [x] 4.1 定位并修复预算页面的 JavaScript 运行时错误
- [x] 4.2 添加错误边界处理防止单个组件错误影响整个页面
- [x] 4.3 验证所有交互功能正常工作
- [x] 4.4 添加错误日志记录(开发环境)
## 5. 仪表图布局修复
- [x] 5.1 使用 CSS Flexbox 实现容器垂直水平居中
- [x] 5.2 设置图表 `maintainAspectRatio: false` 和固定高度
- [x] 5.3 调整容器内边距确保与上下元素保持16px间距
- [x] 5.4 测试不同屏幕尺寸下的布局表现
- [x] 5.5 修复暗色主题下的颜色适配
## 6. 测试与验证
- [x] 6.1 运行前端 lint 检查
- [x] 6.2 验证所有图表在移动端和桌面端的显示效果
- [x] 6.3 测试暗色/亮色主题切换
- [x] 6.4 运行 `pnpm build` 确保无构建错误
- [x] 6.5 功能验收测试
## 实施总结
### 完成的工作
1. **饼图中心金额展示** (Web/src/plugins/chartjs-pie-center-plugin.ts)
- 创建了新的 Chart.js 插件 `pieCenterTextPlugin`
- 在支出分类饼图中心显示总支出金额
- 支持响应式字体大小和暗色主题
2. **折线图日期范围过滤** (Web/src/views/statisticsV2/modules/DailyTrendChart.vue)
- 修改数据准备逻辑,过滤掉当前日期之后的未来日期
- 历史月份展示完整日期范围
3. **预算页面修复** (Web/src/components/Budget/BudgetChartAnalysis.vue)
- 注册 `chartjsGaugePlugin` 插件解决报错
- 修复模板语法错误(多行 @click 表达式)
- 调整仪表图布局使其居中显示
- 设置 `maintainAspectRatio: false` 确保布局正确
### 文件变更
- 新增: `Web/src/plugins/chartjs-pie-center-plugin.ts`
- 修改: `Web/src/views/statisticsV2/modules/ExpenseCategoryCard.vue`
- 修改: `Web/src/views/statisticsV2/modules/DailyTrendChart.vue`
- 修改: `Web/src/components/Budget/BudgetChartAnalysis.vue`
### 验证结果
- ✅ 构建成功 (pnpm build)
- ✅ Lint 检查通过(仅现有警告,无新增错误)

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-14

View File

@@ -0,0 +1,143 @@
## Context
**当前状态**:
- 后端 `BudgetStatsService` 已正确计算 `Description` (HTML格式明细) 和 `Trend` (每日累计金额数组)
- Service 层的 `BudgetStatsDto` 包含这两个字段
- **问题**: Application 层在映射 DTO 时丢失了这两个字段,导致 API 响应不完整
- 前端使用 fallback 逻辑(线性估算)来弥补缺失数据,导致燃尽图显示为直线
**约束**:
- 修复必须向后兼容,不能破坏现有 API 契约
- 优先修复 Bug #4#5(高优先级),因为修复简单且影响大
- 前端已有完整的数据处理逻辑,只需后端提供正确数据
## Goals / Non-Goals
**Goals:**
- 修复 `BudgetStatsDetail` DTO 定义,添加 `Description``Trend` 字段
- 修复 `BudgetApplication.GetCategoryStatsAsync` 中的 DTO 映射逻辑
- 修复前端路由配置和 Vant 组件注册问题
- 分析并修复账单删除功能和金额不一致问题
- 添加单元测试覆盖修复场景
**Non-Goals:**
- 不重构 `BudgetStatsService` 的计算逻辑(已验证正确)
- 不改变前端图表组件的渲染逻辑(已有完整支持)
- 不修改 API 路由或版本化(向后兼容)
## Decisions
### 决策 1: 使用 `init` 关键字而非 `set` 来定义新字段
**理由**: `BudgetStatsDetail``record` 类型,遵循不可变对象模式。使用 `init` 确保字段只能在对象初始化时设置。
**替代方案**:
- 改用 `class` + `set` → 违背现有代码风格,且失去 record 的值语义
- 保持 `record` + `set` → C# 9+ 允许,但不符合不可变设计原则
### 决策 2: 在 Application 层映射时直接赋值,不做转换
**理由**: Service 层的 `BudgetStatsDto.Trend``Description` 已经是目标格式(`List<decimal?>``string`),无需额外处理。
**实现**:
```csharp
Month = new BudgetStatsDetail
{
Limit = stats.Month.Limit,
Current = stats.Month.Current,
Remaining = stats.Month.Remaining,
UsagePercentage = stats.Month.UsagePercentage,
Trend = stats.Month.Trend, // ⬅️ 新增
Description = stats.Month.Description // ⬅️ 新增
}
```
### 决策 3: Bug #1 (路由跳转) - 修改底部导航配置而非路由定义
**理由**: 经验证,`/statistics-v2` 路由已存在且正常工作。问题出在底部导航组件中硬编码了错误的路由路径。
**定位策略**:
1. 搜索底部导航组件代码 (通常包含 `van-tabbar``router-link`)
2. 检查"统计"标签的 `to` 属性或 `path` 配置
3. 修改为 `/statistics-v2`
### 决策 4: Bug #2 (删除功能) - 添加确认对话框而非直接删除
**理由**: 删除操作是破坏性的,应符合最佳实践要求用户确认。
**实现**:
```vue
const handleDelete = async () => {
const confirmed = await showConfirmDialog({
title: '确认删除',
message: '确定要删除这条账单吗?此操作无法撤销。'
})
if (confirmed) {
await deleteBill(billId)
closePopup()
}
}
```
### 决策 5: Bug #3 (组件警告) - 按需导入而非全局注册
**理由**: Vant 推荐使用按需导入,减少打包体积。全局注册可能是遗漏导致的警告。
**验证步骤**:
1. 检查 `main.ts` 或全局插件文件是否有 `DatetimePicker` 导入
2. 如果缺失,添加 `import { DatetimePicker } from 'vant'; app.use(DatetimePicker);`
3. 或在使用组件的文件中局部导入
### 决策 6: Bug #6 (金额不一致) - 先验证是否虚拟消耗导致
**理由**: Bug-handoff 文档指出硬性预算 (📌标记) 会产生虚拟消耗,这是设计行为而非 bug。
**验证逻辑**:
1. 检查不一致的预算是否标记为硬性预算
2. 检查 `BudgetService.GetPeriodRange` 返回的日期范围是否与 `periodStart/periodEnd` 一致
3. 如果是虚拟消耗:在前端账单列表中添加提示说明
4. 如果是日期范围问题:修复 `BudgetResult` 的赋值逻辑
## Risks / Trade-offs
### 风险 1: API 响应体积增大
**问题**: 新增 `Trend` 数组月度31个元素年度12个元素会增加响应大小。
**缓解措施**:
- `Trend` 数据是前端绘制图表必需的,体积增长合理
- 考虑后续添加 gzip 压缩到 API 响应(可选优化)
### 风险 2: 前端可能依赖旧的 fallback 逻辑
**问题**: 如果前端代码中有显式检查 `trend.length === 0` 的逻辑,可能会在修复后仍执行 fallback。
**缓解措施**:
- 在修复后端后,验证前端 `BudgetChartAnalysis.vue:603``629` 行的条件是否正确处理非空 trend
- 如果逻辑有问题,修改为 `if (!trend || trend.length === 0)`
### 风险 3: 测试覆盖不足可能导致回归
**问题**: 现有测试可能未覆盖 DTO 映射场景。
**缓解措施**:
-`WebApi.Test` 中添加针对 `BudgetApplication.GetCategoryStatsAsync` 的单元测试
- 验证返回的 DTO 包含非空的 `Description``Trend`
- 使用 `FluentAssertions` 编写清晰的断言
## Migration Plan
**部署步骤**:
1. 部署后端更新(向后兼容,前端可继续使用旧逻辑)
2. 验证 API 响应包含新字段 (使用 Swagger 或浏览器开发工具)
3. 前端无需额外部署(已支持新字段,会自动切换到真实数据)
4. 清除浏览器缓存以确保使用最新前端代码
**回滚策略**:
- 如果新版本出现问题,回滚到上一个 commit
- API 是向后兼容的(只添加字段),旧版前端仍可正常工作
**验证清单**:
- [ ] 预算明细弹窗显示完整的 HTML 表格
- [ ] 燃尽图显示波动曲线而非直线
- [ ] 底部导航"统计"按钮正常跳转
- [ ] 删除账单功能弹出确认对话框并正常工作
- [ ] 控制台无 `van-datetime-picker` 警告
- [ ] 金额不一致问题已分析并修复或说明
## Open Questions
1. **Bug #6 金额不一致的根本原因**: 需要在测试环境中验证是否为虚拟消耗导致,还是日期范围计算错误。
2. **前端 fallback 逻辑是否需要移除**: 当前 fallback 作为容错机制保留是否合理?还是应在有真实数据时完全禁用?
3. **是否需要添加 E2E 测试**: 当前只计划单元测试,是否需要添加端到端测试覆盖完整流程?

View File

@@ -0,0 +1,42 @@
## Why
修复预算统计模块的6个影响用户体验的bug其中包括2个高优先级数据丢失问题预算明细弹窗显示"暂无数据"、燃尽图显示为直线和4个UI/交互问题路由跳转失败、删除功能无响应、控制台警告、金额不一致。这些bug影响了核心预算跟踪功能的可用性和准确性。
## What Changes
- 修复后端 Application 层 DTO 映射缺失,补充 `Description``Trend` 字段到 API 响应
- 修复前端路由配置,确保底部导航栏"统计"按钮跳转到正确路由
- 修复日历页面账单删除功能的事件绑定
- 修复 Vant 组件 `van-datetime-picker` 的全局注册问题
- 分析并修复预算卡片金额与关联账单列表金额不一致问题
- 添加后端和前端单元测试覆盖修复的场景
## Capabilities
### New Capabilities
<!-- 无新功能,仅修复现有功能 -->
### Modified Capabilities
- `budget-stats`: 修复预算统计API响应缺失 `Description``Trend` 字段,确保前端能正确展示明细弹窗和燃尽图
- `bill-management`: 修复账单删除功能的事件处理逻辑
- `navigation`: 修复前端路由配置和底部导航栏跳转
## Impact
**后端文件**:
- `Application/Dto/BudgetDto.cs` - 修改 `BudgetStatsDetail` 添加字段
- `Application/BudgetApplication.cs` - 修改 DTO 映射逻辑
- `WebApi.Test/` - 添加新的测试用例覆盖修复场景
**前端文件**:
- `Web/src/router/index.js` - 修复路由配置
- `Web/src/components/Budget/BudgetChartAnalysis.vue` - 验证数据正确使用
- `Web/src/components/Budget/BudgetCard.vue` - 分析账单金额不一致问题
- `Web/src/main.ts` 或全局组件注册文件 - 修复 Vant 组件注册
- 日历页面账单详情组件 - 修复删除按钮事件绑定
**API影响**:
- GET `/api/budget/stats/{category}` 响应结构变更(新增字段,向后兼容)
**依赖**:
- 无外部依赖变更

View File

@@ -0,0 +1,40 @@
## MODIFIED Requirements
### Requirement: Bill deletion requires user confirmation
账单删除操作 MUST 要求用户显式确认,防止误操作导致数据丢失。删除确认对话框 SHALL 明确告知用户操作的不可逆性。
#### Scenario: User confirms bill deletion
- **WHEN** 用户在日历页面的账单详情弹窗中点击"删除"按钮
- **THEN** 系统弹出确认对话框,标题为"确认删除"
- **AND** 对话框消息包含警告文本(如"确定要删除这条账单吗?此操作无法撤销。"
- **AND** 对话框提供"确认"和"取消"两个按钮
#### Scenario: User confirms deletion
- **WHEN** 用户在确认对话框中点击"确认"按钮
- **THEN** 系统调用删除 API (`DELETE /api/bill/{id}`)
- **AND** 删除成功后关闭账单详情弹窗
- **AND** 日历视图自动刷新,删除的账单不再显示
#### Scenario: User cancels deletion
- **WHEN** 用户在确认对话框中点击"取消"按钮
- **THEN** 对话框关闭,账单详情弹窗保持打开状态
- **AND** 账单未被删除
#### Scenario: Deletion fails due to server error
- **WHEN** 用户确认删除,但后端返回错误(如网络异常或 500 错误)
- **THEN** 系统显示错误提示(如"删除失败,请稍后重试"
- **AND** 账单详情弹窗保持打开状态
- **AND** 账单仍存在于系统中
### Requirement: Delete button event binding must be functional
日历页面账单详情组件中的删除按钮 MUST 正确绑定点击事件处理函数,确保用户点击时能够触发删除流程。
#### Scenario: Delete button click triggers handler
- **WHEN** 账单详情弹窗渲染完成
- **THEN** "删除"按钮的 `@click``onClick` 事件绑定到正确的处理函数(如 `handleDelete`
- **AND** 点击按钮时控制台无 JavaScript 错误
#### Scenario: Button is not disabled during loading
- **WHEN** 账单详情弹窗首次加载时
- **THEN** "删除"按钮的 `disabled` 属性为 `false`(除非账单正在删除中)
- **AND** 按钮可以正常响应点击事件

View File

@@ -0,0 +1,48 @@
## MODIFIED Requirements
### Requirement: Budget statistics API response includes complete data
预算统计 API (`GET /api/budget/stats/{category}`) SHALL 返回完整的统计数据,包括用于前端图表渲染的 `Trend` 数组和用于明细弹窗的 `Description` HTML 内容。
响应结构中的 `Month``Year` 对象 MUST 包含以下字段:
- `Limit`: 预算限额
- `Current`: 当前实际金额
- `Remaining`: 剩余金额
- `UsagePercentage`: 使用百分比
- `Trend`: 每日/每月累计金额数组 (`List<decimal?>`)
- `Description`: HTML 格式的详细说明,包含计算公式和数据表格 (`string`)
#### Scenario: Monthly stats with trend data
- **WHEN** 客户端请求月度预算统计 `GET /api/budget/stats/food?date=2026-02`
- **THEN** 响应的 `month` 对象包含 `trend` 数组长度等于该月天数如28/29/30/31
- **AND** `trend` 数组每个元素表示截至该天的累计金额(支出类为递减,收入类为递增)
- **AND** `trend` 数组中未到达的日期对应的元素为 `null`
#### Scenario: Monthly stats with description
- **WHEN** 客户端请求月度预算统计 `GET /api/budget/stats/food?date=2026-02`
- **THEN** 响应的 `month` 对象包含 `description` 字段
- **AND** `description` 是 HTML 格式字符串,包含 `<table>` 标签展示明细数据
- **AND** `description` 包含计算公式说明(如"剩余 = 限额 - 已用"
#### Scenario: Yearly stats with trend data
- **WHEN** 客户端请求年度预算统计 `GET /api/budget/stats/salary?date=2026`
- **THEN** 响应的 `year` 对象包含 `trend` 数组长度为12代表12个月
- **AND** `trend` 数组每个元素表示截至该月的累计金额
- **AND** `trend` 数组中未到达的月份对应的元素为 `null`
#### Scenario: Yearly stats with description
- **WHEN** 客户端请求年度预算统计 `GET /api/budget/stats/salary?date=2026`
- **THEN** 响应的 `year` 对象包含 `description` 字段
- **AND** `description` 是 HTML 格式字符串,包含年度统计明细
### Requirement: DTO mapping preserves all Service layer data
Application 层的 `BudgetApplication.GetCategoryStatsAsync` 方法在将 Service 层的 `BudgetStatsDto` 映射到 API 响应 DTO 时MUST 保留所有数据字段,不得丢失 `Trend``Description`
#### Scenario: Mapping from Service DTO to API DTO
- **WHEN** `BudgetApplication.GetCategoryStatsAsync` 接收到 Service 层返回的 `BudgetStatsDto`
- **THEN** 映射后的 `BudgetStatsDetail` 对象包含 `Trend` 字段,其值等于 `BudgetStatsDto.Month.Trend``BudgetStatsDto.Year.Trend`
- **AND** 映射后的 `BudgetStatsDetail` 对象包含 `Description` 字段,其值等于 `BudgetStatsDto.Month.Description``BudgetStatsDto.Year.Description`
#### Scenario: API response schema validation
- **WHEN** 前端调用 `/api/budget/stats/{category}` 并解析 JSON 响应
- **THEN** TypeScript 类型检查不报错,响应对象符合 `BudgetStatsResponse` 接口定义
- **AND** `month.trend``month.description` 字段存在且非 `undefined`

View File

@@ -0,0 +1,37 @@
## MODIFIED Requirements
### Requirement: Bottom navigation statistics tab routes correctly
底部导航栏的"统计"标签 MUST 正确配置路由路径,点击后能够跳转到统计页面 (`/statistics-v2`)。
#### Scenario: User clicks statistics tab in bottom navigation
- **WHEN** 用户在应用底部导航栏点击"统计"图标或标签
- **THEN** 浏览器 URL 变更为 `/statistics-v2`
- **AND** 页面渲染统计页面组件(如 `StatisticsV2.vue`
- **AND** 底部导航栏的"统计"标签高亮显示为激活状态
#### Scenario: Direct URL access to statistics page
- **WHEN** 用户直接在浏览器地址栏输入 `/statistics-v2` 并访问
- **THEN** 应用正确渲染统计页面
- **AND** 底部导航栏的"统计"标签高亮显示为激活状态
#### Scenario: Navigation tab configuration matches route definition
- **WHEN** 前端代码加载时
- **THEN** 底部导航组件(`van-tabbar` 或自定义组件)中"统计"标签的 `to``path` 属性值为 `/statistics-v2`
- **AND** 路由配置文件 (`router/index.js`) 中存在 `path: '/statistics-v2'` 的路由定义
### Requirement: Vant DatetimePicker component must be registered
Vant UI 库的 `van-datetime-picker` 组件 MUST 正确注册,以避免控制台出现 "Failed to resolve component" 警告。
#### Scenario: DatetimePicker used in application
- **WHEN** 应用中任何页面使用 `<van-datetime-picker>` 组件
- **THEN** 组件正常渲染,无控制台错误或警告
- **AND** 控制台不显示 "Failed to resolve component: van-datetime-picker" 消息
#### Scenario: Global component registration in main.ts
- **WHEN** 应用启动时执行 `main.ts` 或全局插件文件
- **THEN** `DatetimePicker` 组件已通过 `app.use(DatetimePicker)` 全局注册
- **OR** 在使用组件的文件中已通过 `import { DatetimePicker } from 'vant'``components: { VanDatetimePicker: DatetimePicker }` 局部注册
#### Scenario: No missing component warnings after fix
- **WHEN** 用户浏览应用的所有页面(日历、预算、统计等)
- **THEN** 浏览器开发者工具控制台中无任何 Vant 组件相关的警告或错误

View File

@@ -0,0 +1,65 @@
## 1. Backend: Fix Budget Stats DTO and Mapping (Bug #4 & #5 - High Priority)
- [x] 1.1 在 `Application/Dto/BudgetDto.cs``BudgetStatsDetail` record 中添加 `Trend` 字段(`List<decimal?>`,使用 `init`
- [x] 1.2 在 `Application/Dto/BudgetDto.cs``BudgetStatsDetail` record 中添加 `Description` 字段(`string`,使用 `init`
- [x] 1.3 在 `Application/BudgetApplication.cs``GetCategoryStatsAsync` 方法中,映射 `Month` 对象时添加 `Trend = stats.Month.Trend`
- [x] 1.4 在 `Application/BudgetApplication.cs``GetCategoryStatsAsync` 方法中,映射 `Month` 对象时添加 `Description = stats.Month.Description`
- [x] 1.5 在 `Application/BudgetApplication.cs``GetCategoryStatsAsync` 方法中,映射 `Year` 对象时添加 `Trend = stats.Year.Trend`
- [x] 1.6 在 `Application/BudgetApplication.cs``GetCategoryStatsAsync` 方法中,映射 `Year` 对象时添加 `Description = stats.Year.Description`
## 2. Backend: Add Unit Tests for DTO Mapping
- [x] 2.1 在 `WebApi.Test/` 中创建 `BudgetApplicationTests.cs` 测试类(如果不存在)
- [x] 2.2 编写测试用例 `GetCategoryStatsAsync_Should_Include_Trend_And_Description_In_Month_Stats`
- [x] 2.3 编写测试用例 `GetCategoryStatsAsync_Should_Include_Trend_And_Description_In_Year_Stats`
- [x] 2.4 运行测试并验证通过:`dotnet test --filter "FullyQualifiedName~BudgetApplicationTests"`
## 3. Frontend: Fix Navigation Routes (Bug #1)
- [x] 3.1 使用 `grep` 搜索底部导航组件代码(搜索关键字 `van-tabbar``统计`
- [x] 3.2 定位"统计"标签的路由配置(检查 `to``path` 属性)
- [x] 3.3 修改路由路径为 `/statistics-v2`
- [x] 3.4 验证路由配置文件 `Web/src/router/index.js` 中存在 `/statistics-v2` 路由定义
## 4. Frontend: Fix Bill Deletion Function (Bug #2)
- [x] 4.1 使用 `grep` 搜索日历页面的账单详情组件(搜索关键字 `删除``delete`
- [x] 4.2 定位删除按钮的点击事件绑定(检查 `@click``onClick`
- [x] 4.3 实现 `handleDelete` 函数,使用 `showConfirmDialog` 显示确认对话框
- [x] 4.4 在确认后调用删除 API 并关闭弹窗,刷新日历视图
- [x] 4.5 在取消时关闭对话框但保持弹窗打开
- [x] 4.6 处理删除失败场景,显示错误提示
## 5. Frontend: Fix Vant DatetimePicker Registration (Bug #3)
- [x] 5.1 检查 `Web/src/main.ts` 或全局组件注册文件
- [x] 5.2 验证是否导入 `DatetimePicker``import { DatetimePicker } from 'vant'`
- [x] 5.3 如果缺失,添加全局注册 `app.use(DatetimePicker)`
- [ ] 5.4 启动前端开发服务器,验证控制台无 "Failed to resolve component" 警告
## 6. Frontend: Verify Budget Chart Renders Correctly After Backend Fix
- [ ] 6.1 启动后端和前端服务
- [ ] 6.2 打开预算页面,点击"使用情况"或"完成情况"旁的感叹号图标
- [ ] 6.3 验证明细弹窗显示完整的 HTML 表格(非"暂无数据"
- [ ] 6.4 验证燃尽图显示波动曲线(非直线)
- [x] 6.5 检查前端 `BudgetChartAnalysis.vue:603``:629` 行的 fallback 逻辑是否仍触发(如需修改条件检查)
## 7. Investigation: Budget Card Amount Mismatch (Bug #6 - Low Priority)
- [ ] 7.1 在测试环境中打开预算页面,点击预算卡片的"查询关联账单"按钮
- [ ] 7.2 对比预算卡片显示的"实际"金额与账单列表金额总和
- [ ] 7.3 检查不一致的预算是否标记为硬性预算(📌)
- [ ] 7.4 如果是硬性预算,验证虚拟消耗的计算逻辑(`BudgetService.cs:376-405`
- [ ] 7.5 检查 `BudgetResult``PeriodStart``PeriodEnd` 的赋值是否与 `GetPeriodRange` 一致
- [ ] 7.6 如果是虚拟消耗导致,考虑在前端账单列表中添加提示说明(可选)
- [ ] 7.7 如果是日期范围问题,修复 `BudgetResult` 的赋值逻辑
## 8. End-to-End Verification
- [x] 8.1 运行后端所有测试:`dotnet test`
- [x] 8.2 运行前端 lint`cd Web && pnpm lint`
- [x] 8.3 构建前端:`cd Web && pnpm build`
- [ ] 8.4 手动测试所有修复的 bug按 bug-handoff-document.md 中的验证清单)
- [ ] 8.5 清除浏览器缓存并重新测试
- [ ] 8.6 验证控制台无错误或警告

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,87 @@
## Context
EmailBill 项目已从 ECharts 迁移到 Chart.js 作为图表库。在迁移过程中图表文本显示出现了乱码问题特别是在显示中文标签、Tooltip 和中心文本时。这可能是由于:
1. Chart.js 默认字体配置不支持中文
2. Tooltip 回调函数返回的字符串编码问题
3. 主题切换时字体颜色对比度不足
当前图表组件位于 `Web/src/components/Charts/`,使用 `BaseChart.vue` 作为统一包装组件。
## Goals / Non-Goals
**Goals:**
- 修复所有图表(饼图、折线图、仪表盘)的中文乱码问题
- 确保图表文本在明/暗主题下都清晰可读
- 统一字体配置,支持跨平台中文显示
- 修复 Tooltip 和 Label 的文本格式化问题
**Non-Goals:**
- 不修改图表的数据结构或业务逻辑
- 不添加新的图表类型
- 不进行 UI 样式的大幅度调整(仅修复文本显示问题)
## Decisions
### Decision 1: 字体配置方案
**选择**: 在 `useChartTheme.ts` 中统一配置 Chart.js 字体选项
**理由**:
- Chart.js 支持全局字体配置,通过 `defaults.font.family` 可以一次性设置所有图表的字体
- 使用系统字体栈确保跨平台兼容性:`'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif`
- 避免在每个组件中重复配置
**替代方案考虑**:
- 在每个图表组件中单独配置字体 → 重复代码,维护困难
- 使用 WebFont 加载自定义字体 → 增加外部依赖,加载时间不可控
### Decision 2: Tooltip 格式化修复方案
**选择**: 修复 `callbacks.label``callbacks.title` 回调函数,确保返回正确的字符串
**理由**:
- Chart.js 的 Tooltip 回调函数必须返回字符串,不能返回对象或其他类型
- 使用模板字符串确保正确的字符串拼接
- 添加空值检查防止 undefined 导致的乱码
**代码示例**:
```typescript
tooltip: {
callbacks: {
label: (context) => {
const value = context.parsed.y || context.parsed
return `¥${value.toFixed(2)}`
}
}
}
```
### Decision 3: 中心文本显示方案
**选择**: 继续使用 CSS 绝对定位的覆盖层显示中心文本
**理由**:
- Chart.js 本身不支持在 Doughnut 图表中心直接渲染文本
- CSS 覆盖层方式简单可靠,易于控制字体样式
- 确保覆盖层使用正确的字体族和颜色
**实现要点**:
- 覆盖层容器设置 `font-family` 继承自主题配置
- 使用 `var(--van-text-color)` 确保主题适配
- 添加 `white-space: nowrap` 防止文本换行导致错位
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| 某些旧版浏览器可能不支持系统字体栈 | 提供后备字体Arial, sans-serif确保基本可读性 |
| 暗色模式下文本颜色对比度不足 | 使用 Vant 主题变量确保颜色适配 |
| 字体文件过大影响加载性能 | 使用系统字体,不加载外部字体文件 |
| 修改全局配置可能影响其他组件 | 在 `useChartTheme` 中集中管理,便于回滚 |
## Migration Plan
1. **阶段 1**: 修改 `useChartTheme.ts` 添加全局字体配置
2. **阶段 2**: 检查并修复各图表组件的 Tooltip 回调函数
3. **阶段 3**: 验证所有图表页面的文本显示
4. **阶段 4**: 在明/暗主题下分别测试
**Rollback Strategy**: 所有修改都是配置层面的,可以通过回滚 Git 提交快速恢复。

View File

@@ -0,0 +1,25 @@
## Why
在浏览器中查看图表时,饼图、折线图等图表上出现了错乱的字符串显示问题,影响数据可读性和用户体验。这可能是由于 Chart.js 配置中的字体设置、编码问题或 tooltip/label 格式化错误导致的。
## What Changes
- 修复饼图、折线图、仪表盘等图表上的错乱字符串显示
- 检查并修正图表字体配置,确保使用中文字体或兼容字体
- 修复 tooltip 和 label 的格式化回调函数
- 确保图表文本在各种主题(明/暗色模式)下正确显示
## Capabilities
### New Capabilities
- `chart-text-encoding-fix`: 修复图表文本编码和字体配置,确保中文和特殊字符正确显示
### Modified Capabilities
- `chart-migration-patterns`: 更新图表迁移模式中的文本渲染配置,确保 Chart.js 图表文本正确显示
## Impact
- **前端组件**: `Web/src/components/Charts/` 下的所有图表组件
- **配置文件**: 图表主题配置文件 `useChartTheme.ts`
- **工具函数**: 图表辅助函数 `chartHelpers.ts`
- **页面**: 统计页面、预算页面等使用图表的页面

View File

@@ -0,0 +1,129 @@
## MODIFIED Requirements
### Requirement: 仪表盘图表迁移模式
组件 SHALL 使用 Chart.js Doughnut 图表实现仪表盘Gauge效果替代 ECharts Gauge 图表。
#### Scenario: 半圆仪表盘渲染
- **WHEN** 组件接收预算统计数据current, limit
- **THEN** 系统使用 Doughnut 图表渲染半圆进度条,配置 `rotation: -90``circumference: 180`
- **AND** 图表字体配置 SHALL 包含中文字体支持
#### Scenario: 中心文本叠加显示
- **WHEN** 仪表盘图表渲染完成
- **THEN** 系统在图表中心显示余额/超支文本,使用 CSS 绝对定位的覆盖层
- **AND** 中心文本 SHALL 正确显示中文,无乱码
#### Scenario: 动态颜色切换
- **WHEN** 实际值超过预算限额
- **THEN** 进度条颜色切换为危险色(`var(--van-danger-color)`),中心文本显示"超支"
- **AND** "超支"文字 SHALL 清晰可读
#### Scenario: 暗色模式适配
- **WHEN** 用户切换到暗色主题
- **THEN** 图表颜色自动适配,使用 `useChartTheme` composable 获取主题色
- **AND** 文本颜色 SHALL 与背景有足够对比度
### Requirement: 折线图迁移模式
组件 SHALL 使用 Chart.js Line 图表实现趋势折线图,替代 ECharts Line 图表。
#### Scenario: 单系列折线图渲染
- **WHEN** 组件接收月度支出数据(日期 + 金额数组)
- **THEN** 系统渲染折线图X 轴为日期标签Y 轴为金额,使用渐变填充
- **AND** X 轴日期标签 SHALL 正确显示,无乱码
#### Scenario: 双系列折线图渲染
- **WHEN** 组件接收收支数据(包含收入和支出两个系列)
- **THEN** 系统渲染两条折线,支出为红色,收入为绿色,支持独立的 hover 交互
- **AND** 图例 SHALL 正确显示"收入"和"支出"中文标签
#### Scenario: 空数据处理
- **WHEN** 图表数据为空或所有数据点为 0
- **THEN** 系统显示 `<van-empty>` 空状态组件,而非空白图表
#### Scenario: Tooltip 格式化
- **WHEN** 用户 hover 到数据点
- **THEN** Tooltip 显示"¥XXX.XX"格式的金额,使用 `callbacks.label` 自定义
- **AND** Tooltip 内容 SHALL 正确编码,无乱码
### Requirement: 饼图/环形图迁移模式
组件 SHALL 使用 Chart.js Doughnut 图表实现分类统计环形图,替代 ECharts Pie 图表。
#### Scenario: 环形图渲染
- **WHEN** 组件接收分类统计数据(分类名称 + 金额数组)
- **THEN** 系统渲染环形图,每个分类使用不同颜色,配置 `cutout: '50%'`
- **AND** 分类标签 SHALL 正确显示中文名称
#### Scenario: 分类颜色映射
- **WHEN** 分类数据包含预定义颜色
- **THEN** 图表使用 props 传入的颜色数组,确保与列表中的分类色块一致
#### Scenario: 小分类合并
- **WHEN** 分类数量超过 10 个
- **THEN** 系统使用 `mergeSmallCategories()` 工具函数,将占比小于 5% 的分类合并为"其他"
- **AND** "其他"标签 SHALL 正确显示
#### Scenario: 点击分类跳转
- **WHEN** 用户点击环形图扇区
- **THEN** 系统触发 `@category-click` 事件,传递分类名称和类型
### Requirement: BaseChart 组件统一使用
所有图表组件 SHALL 使用 `BaseChart.vue` 包装组件,而非直接使用 vue-chartjs 组件。
#### Scenario: BaseChart 组件使用
- **WHEN** 组件需要渲染图表
- **THEN** 使用 `<BaseChart type="line|bar|doughnut" :data="chartData" :options="chartOptions" />`
- **AND** 通过 options 传入正确的字体配置
#### Scenario: Loading 状态处理
- **WHEN** 图表数据加载中
- **THEN** BaseChart 显示 `<van-loading>` 组件
#### Scenario: 图表渲染回调
- **WHEN** 图表渲染完成
- **THEN** BaseChart 触发 `@chart:render` 事件,传递 Chart.js 实例引用
### Requirement: ECharts 代码完全移除
组件 SHALL 移除所有 ECharts 相关代码,包括导入语句、实例变量、环境变量判断。
#### Scenario: 移除 ECharts 导入
- **WHEN** 迁移组件
- **THEN** 删除 `import * as echarts from 'echarts'` 语句
#### Scenario: 移除环境变量开关
- **WHEN** 迁移组件
- **THEN** 删除 `const useChartJS = import.meta.env.VITE_USE_CHARTJS === 'true'` 和相关的 `v-if`/`v-else` 条件渲染
#### Scenario: 移除 ECharts 实例管理
- **WHEN** 迁移组件
- **THEN** 删除 `let chartInstance = null``echarts.init()``chartInstance.setOption()` 等代码
#### Scenario: 移除生命周期清理
- **WHEN** 迁移组件
- **THEN** 删除 `onBeforeUnmount()` 中的 `chartInstance.dispose()` 调用
### Requirement: 测试覆盖
迁移后的组件 SHALL 通过白盒和黑盒测试验证功能正确性。
#### Scenario: 单元测试 - 组件挂载
- **WHEN** 运行 Jest 单元测试
- **THEN** 组件能够成功挂载,不抛出错误
#### Scenario: 单元测试 - Props 传递
- **WHEN** 传入测试数据 props
- **THEN** 计算属性 `chartData``chartOptions` 返回正确的 Chart.js 配置对象
- **AND** options 中 SHALL 包含正确的字体配置
#### Scenario: E2E 测试 - 图表渲染
- **WHEN** 运行 Playwright E2E 测试
- **THEN** 浏览器中能看到图表元素Canvas且无控制台错误
- **AND** 图表文本 SHALL 正确显示,无乱码
#### Scenario: E2E 测试 - 用户交互
- **WHEN** 用户 hover 到图表数据点
- **THEN** Tooltip 正确显示,格式化后的金额信息可见
- **AND** Tooltip 内容 SHALL 无乱码
#### Scenario: 视觉回归测试
- **WHEN** 截图对比迁移前后的图表
- **THEN** 颜色、布局、字体大小差异在可接受范围内(像素差异 < 5%
- **AND** 中文文本 SHALL 清晰可读

View File

@@ -0,0 +1,44 @@
## ADDED Requirements
### Requirement: 图表文本编码修复
Chart.js 图表 SHALL 正确显示中文文本,不出现乱码或异常字符。
#### Scenario: 饼图标签中文显示
- **WHEN** 系统渲染支出分类饼图,分类名称为中文
- **THEN** 分类标签 SHALL 正确显示中文字符
- **AND** 标签文字 SHALL 清晰可读,无乱码
#### Scenario: Tooltip 中文显示
- **WHEN** 用户 hover 到图表数据点
- **THEN** Tooltip SHALL 正确显示中文内容
- **AND** 金额和分类名称 SHALL 无乱码
#### Scenario: 中心文本中文显示
- **WHEN** 仪表盘图表渲染中心文本
- **THEN** 中心显示的余额/超支文本 SHALL 正确显示中文
- **AND** 文字 SHALL 清晰无乱码
### Requirement: 图表字体配置
Chart.js 配置 SHALL 使用兼容的字体设置,确保跨平台文本正确渲染。
#### Scenario: 字体族配置
- **WHEN** 图表初始化时
- **THEN** 系统 SHALL 配置 `font.family` 为兼容中文字体的字体栈(如 `'PingFang SC', 'Microsoft YaHei', sans-serif`
#### Scenario: 响应式字体大小
- **WHEN** 图表在不同尺寸屏幕上渲染
- **THEN** 字体大小 SHALL 根据屏幕尺寸自动调整
- **AND** 文字 SHALL 始终保持清晰可读
### Requirement: Tooltip 格式化修复
Tooltip 回调函数 SHALL 正确处理文本编码和格式化。
#### Scenario: Tooltip Label 格式化
- **WHEN** Tooltip 显示数据标签
- **THEN** 回调函数 SHALL 返回正确编码的字符串
- **AND** 特殊字符 SHALL 正确转义
#### Scenario: 金额格式化显示
- **WHEN** Tooltip 显示金额
- **THEN** 金额格式 SHALL 为 "¥XXX.XX"
- **AND** 货币符号 SHALL 正确显示

View File

@@ -0,0 +1,43 @@
## 1. 全局字体配置
- [x] 1.1 修改 `useChartTheme.ts`,添加 Chart.js 全局字体配置
- [x] 1.2 配置字体栈支持中文显示:`'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif`
- [x] 1.3 设置默认字体大小和颜色变量
## 2. Tooltip 格式化修复
- [x] 2.1 检查饼图组件的 Tooltip 回调函数,修复编码问题
- [x] 2.2 检查折线图组件的 Tooltip 回调函数,确保金额格式正确
- [x] 2.3 检查仪表盘组件的 Tooltip 配置
- [x] 2.4 确保所有 Tooltip 回调返回正确的字符串类型
## 3. 图表组件文本修复
- [x] 3.1 修复饼图分类标签的中文显示
- [x] 3.2 修复折线图 X 轴日期标签显示
- [x] 3.3 修复仪表盘中心文本(余额/超支)的中文显示
- [x] 3.4 确保图例Legend中文标签正确显示
## 4. 主题适配
- [x] 4.1 验证明色模式下图表文本清晰可读
- [x] 4.2 验证暗色模式下图表文本颜色和对比度
- [x] 4.3 修复主题切换时可能出现的文本渲染问题
## 7. 修复密集数字显示
- [x] 7.1 禁用折线图的数据标签datalabels
- [x] 7.2 验证明暗模式下图表显示正常
## 5. 测试验证
- [x] 5.1 在 Chrome 浏览器中验证所有图表文本显示
- [x] 5.2 在移动端浏览器中验证图表文本显示
- [x] 5.3 验证 Tooltip hover 时文本无乱码
- [x] 5.4 检查控制台是否有相关错误日志
## 6. 代码审查
- [x] 6.1 运行 `pnpm lint` 检查代码格式
- [x] 6.2 运行 `pnpm build` 确保构建成功
- [x] 6.3 检查是否有未使用的导入或变量

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-14

View File

@@ -0,0 +1,24 @@
## Why
用户在 2026 年 2 月份查看 2026 年 1 月份的月度预算统计时,页面显示"超支170元",但用户认为应该远远大于这个值。经检查发现,页面显示的"超支170"仅计算了月度支出预算Type=1的超支未包含年度支出预算Type=2在该月的实际支出。1 月份实际总支出为 36,130.40 元,但页面只显示了 23,284.40 元(月度支出预算),缺少了约 12,846 元的年度支出预算部分。
## What Changes
- **修改月度预算统计逻辑**:在 `CalculateMonthlyCategoryStatsAsync` 方法中统计支出时需要包含年度支出预算Type=2在该月的实际支出金额
- **修改数据源逻辑**`GetAllBudgetsWithArchiveAsync` 方法在获取月度预算数据时,需要同时获取年度支出预算在该月的实际支出
- **确保归档数据正确使用**:使用归档数据中的 `Actual` 值,而非重新计算
## Capabilities
### New Capabilities
无新能力引入,仅修复现有逻辑。
### Modified Capabilities
- `budget-stats`: 修改月度预算统计的需求,要求月度支出统计包含所有类型的实际支出(月度+年度支出预算在该月的支出)
## Impact
- **受影响代码**`Service/Budget/BudgetStatsService.cs` 中的 `CalculateMonthlyCategoryStatsAsync``GetAllBudgetsWithArchiveAsync` 方法
- **受影响 API**:预算统计相关的 API 接口(前端调用的获取预算统计信息的接口)
- **数据来源**`BudgetArchive` 表中的归档数据,需要正确使用归档的 `Actual`
- **用户体验**:修复后,用户查看月度预算统计时,将看到包含所有实际支出的准确数据

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-16

View File

@@ -0,0 +1,173 @@
---
title: 图表样式优化设计文档
author: AI Assistant
date: 2026-02-16
status: draft
---
## Context
### 当前状态
应用中的图表存在以下样式问题:
1. **支出分类饼图 (ExpenseCategoryCard.vue)**
- 问题显示了不应该有的坐标轴X轴 0-7Y轴 ¥0-¥1
- 原因:`useChartTheme.ts` 中的基础配置默认包含坐标轴,饼图配置未覆盖
2. **使用情况仪表盘 (BudgetChartAnalysis.vue)**
- 问题:仪表盘周围有坐标轴和网格线干扰
- 影响:视觉上显得混乱,不够简洁
3. **预算进度燃尽图**
- 现状:样式基本可用,但缺乏现代感
- 可优化:颜色对比度、网格线样式、动画效果
### 技术背景
- 使用 Chart.js 4.5+ 和 vue-chartjs 5.3+
- 主题配置通过 `useChartTheme.ts` 统一管理
- 支持 Vant UI 的暗色模式
## Goals / Non-Goals
**Goals:**
1. 移除所有饼图/环形图/仪表盘的不必要坐标轴
2. 统一图表的视觉风格,符合 Vant 设计系统
3. 优化配色方案,提高可读性和美观度
4. 增强动画效果,提升交互体验
5. 确保暗色模式下的显示效果
**Non-Goals:**
- 不添加新的图表类型
- 不修改业务逻辑或数据结构
- 不改变现有的 API 接口
## Decisions
### Decision 1: 图表类型感知配置
**选择**: 修改 `useChartTheme.ts`,使其根据图表类型自动调整默认配置
**理由**:
- 饼图/环形图/仪表盘不需要坐标轴
- 折线图/柱状图需要坐标轴但可简化
- 减少每个组件手动覆盖配置的重复工作
**实现**:
```typescript
// 新增根据图表类型获取配置的方法
const getChartOptionsByType = (type: 'line' | 'bar' | 'pie' | 'doughnut', customOptions = {}) => {
const baseOptions = baseChartOptions.value
// 无坐标轴图表类型
if (['pie', 'doughnut'].includes(type)) {
return mergeDeep(baseOptions, {
scales: { x: { display: false }, y: { display: false } }
}, customOptions)
}
return mergeDeep(baseOptions, customOptions)
}
```
**替代方案**: 在每个使用饼图的组件中手动添加 `scales: { x: { display: false }, y: { display: false } }`
- **排除原因**: 重复代码多,容易遗漏
### Decision 2: 简化坐标轴样式
**选择**: 对于需要坐标轴的图表,采用极简风格
**具体措施**:
- 网格线:使用极淡的颜色 (`--van-border-color` 30% 透明度)
- 刻度标签:减小字体大小至 10px
- 移除坐标轴边框 (`drawBorder: false`)
**理由**:
- 减少视觉噪音,突出数据本身
- 移动设备上更清晰的阅读体验
### Decision 3: 优化配色方案
**选择**: 使用更现代、和谐的颜色方案
**具体措施**:
1. **主色调扩展**:
- 保留 Vant 主题色作为基础
- 添加柔和的辅助色(降低饱和度)
2. **饼图/环形图**:
- 使用 8 色渐进色板
- 颜色从 Vant 主题派生但降低饱和度 20%
3. **折线图/柱状图**:
- 支出:暖色调(橙红系)
- 收入:冷色调(青绿系)
- 对比度符合 WCAG AA 标准
**理由**:
- 更符合现代移动端 UI 审美
- 色盲友好
### Decision 4: 增强交互体验
**选择**: 添加微妙的悬停和点击效果
**具体措施**:
1. **悬停效果**:
- 饼图扇区:`hoverOffset: 8`(从 4 增加)
- 折线点:`pointHoverRadius: 6`(从 4 增加)
2. **动画优化**:
- 持续时间750ms → 600ms更快响应
- 缓动函数:`easeInOutQuart``easeOutQuart`(更自然的结束)
3. **触控优化**:
- 增加触控目标大小
- 支持捏合缩放(对于趋势图)
## Risks / Trade-offs
**风险 1**: 颜色变更可能影响用户习惯
- **影响**: 低 - 纯视觉变化
- **缓解**: 保持色相大致不变,只调整饱和度和明度
**风险 2**: 移除坐标轴可能降低某些图表的可读性
- **影响**: 中 - 对于复杂数据集
- **缓解**: 保留关键刻度,仅淡化网格线
**风险 3**: 动画增强可能影响低性能设备
- **影响**: 低 - 已考虑 `prefers-reduced-motion`
- **缓解**: 动画持续时间控制在 600ms 以内
## Migration Plan
### 实施顺序
1. **Phase 1**: 修复坐标轴问题(最高优先级)
- 修改 `useChartTheme.ts`
- 更新 `ExpenseCategoryCard.vue`
- 更新 `BudgetChartAnalysis.vue`
2. **Phase 2**: 配色优化
- 更新图表色板
- 调整渐变效果
3. **Phase 3**: 动画和交互增强
- 优化悬停效果
- 添加触控支持
### 回滚策略
- 所有变更都是样式层面的
- 可通过 git revert 回滚
- 建议分步提交,便于部分回滚
## Open Questions
1. 是否需要提供图表主题切换开关(明亮/暗黑/高对比度)?
2. 预算页面的仪表盘是否需要添加中心数值显示?
3. 是否需要支持图表数据的导出功能?
## 附录
### 参考资源
- [Vant Design 色彩系统](https://vant-ui.github.io/vant/#/zh-CN/design-color)
- [Chart.js 配置文档](https://www.chartjs.org/docs/latest/configuration/)
- [WCAG 颜色对比度指南](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html)

View File

@@ -0,0 +1,67 @@
---
title: 图表样式全面优化
author: AI Assistant
date: 2026-02-16
status: draft
category: UI/UX
---
## Why
当前应用中统计页面和预算页面的图表样式存在明显问题:
1. **支出分类饼图**出现了不应该有的坐标轴X轴0-7Y轴¥0-¥1严重干扰视觉
2. **使用情况仪表盘**周围也有坐标轴干扰,影响美观
3. 所有图表整体样式缺乏现代感,显得凌乱
为提升用户体验和视觉品质,需要对应用内所有图表进行全面样式优化。
## What Changes
### 统计页面 (Statistics)
- **修复** 支出分类饼图的坐标轴问题,移除所有不必要的坐标轴和网格线
- **优化** 收入/支出趋势图的样式,使其更简洁清晰
- **统一** 图表配色方案,与 Vant 设计系统保持一致
- **改进** 图表响应式布局和触控交互体验
### 预算页面 (Budget)
- **修复** 使用情况仪表盘(月度/年度)的坐标轴干扰
- **优化** 预算进度燃尽图的视觉层次和颜色对比度
- **美化** 偏差分析图表的数据展示形式
- **统一** 图表组件的圆角、阴影等视觉元素
### 通用改进
- 更新 Chart.js 全局配置,移除默认坐标轴样式
- 为暗色模式优化图表颜色
- 添加平滑的动画过渡效果
- 确保所有图表在移动设备上的可读性
## Capabilities
### New Capabilities
- `chart-theme-system`: 统一的图表主题系统,支持明暗模式切换和主题色自动适配
- `responsive-chart-layout`: 响应式图表布局组件,自动适配不同屏幕尺寸
### Modified Capabilities
- 无现有 spec 需要修改(本次主要是样式优化,不涉及功能需求变更)
## Impact
**受影响文件**:
- `Web/src/components/Charts/BaseChart.vue`
- `Web/src/composables/useChartTheme.ts`
- `Web/src/views/StatisticsView.vue`
- `Web/src/views/BudgetView.vue`
- `Web/src/utils/chartHelpers.ts`
**依赖**:
- Chart.js 4.5+
- vue-chartjs 5.3+
- Vant UI 主题系统
**风险**:
- 低 - 纯样式变更,不影响业务逻辑
- 需验证所有图表在暗色模式下的可读性
## 更新日志
- 2026-02-16: 创建提案,定义图表优化范围

View File

@@ -0,0 +1,70 @@
---
title: 图表主题系统规格
author: AI Assistant
date: 2026-02-16
---
## ADDED Requirements
### Requirement: 图表类型感知配置
`useChartTheme` 组合式函数 SHALL 根据图表类型自动提供合适的默认配置。
#### Scenario: 饼图/环形图自动隐藏坐标轴
- **WHEN** 调用 `getChartOptionsByType('doughnut', customOptions)`
- **THEN** 返回的配置中 `scales.x.display``scales.y.display` 均为 `false`
- **AND** 返回的配置 SHALL 与 customOptions 深度合并
#### Scenario: 折线图/柱状图保留简化坐标轴
- **WHEN** 调用 `getChartOptionsByType('line', customOptions)`
- **THEN** 返回的配置包含简化的坐标轴样式
- **AND** 网格线使用 `--van-border-color` 30% 透明度
- **AND** 刻度标签字体大小为 10px
### Requirement: 现代化配色方案
图表主题系统 SHALL 提供符合现代审美的配色方案。
#### Scenario: 主色板包含 8 个颜色
- **WHEN** 访问 `chartPalette`
- **THEN** 返回包含 8 个颜色的数组
- **AND** 颜色 SHALL 从 Vant 主题色派生并降低 20% 饱和度
#### Scenario: 支出/收入颜色区分
- **WHEN** 配置支出相关图表
- **THEN** 默认使用暖色调(橙红系)
- **WHEN** 配置收入相关图表
- **THEN** 默认使用冷色调(青绿系)
### Requirement: 暗色模式适配
图表 SHALL 自动适配 Vant UI 的暗色模式。
#### Scenario: 暗色模式颜色切换
- **WHEN** Vant 主题切换为暗色模式
- **THEN** 图表文本颜色 SHALL 自动变为浅色
- **AND** 图表背景色 SHALL 与卡片背景一致
- **AND** 网格线颜色 SHALL 变为深色系的边框色
#### Scenario: 手动颜色获取
- **WHEN** 调用 `colors.text`
- **THEN** 返回当前主题的文本颜色 CSS 变量值
- **AND** SHALL 实时响应主题切换
### Requirement: 动画配置
图表 SHALL 支持可配置的动画效果。
#### Scenario: 默认动画配置
- **WHEN** 获取图表配置
- **THEN** 默认动画持续时间为 600ms
- **AND** 缓动函数为 `easeOutQuart`
#### Scenario: 减少动画偏好
- **WHEN** 用户系统偏好 `prefers-reduced-motion: reduce`
- **THEN** 动画持续时间 SHALL 自动设为 0
- **AND** 图表 SHALL 立即渲染完成
## MODIFIED Requirements
无修改的现有需求。
## REMOVED Requirements
无删除的需求。

View File

@@ -0,0 +1,63 @@
---
title: 响应式图表布局规格
author: AI Assistant
date: 2026-02-16
---
## ADDED Requirements
### Requirement: 容器自适应
BaseChart 组件 SHALL 自动适应父容器大小。
#### Scenario: 容器大小变化
- **WHEN** 父容器大小发生变化
- **THEN** 图表 SHALL 自动调整尺寸
- **AND** 使用 ResizeObserver 进行监听
- **AND** 图表 SHALL 保持比例不失真
#### Scenario: 横竖屏切换
- **WHEN** 移动设备从竖屏切换到横屏
- **THEN** 图表 SHALL 在 300ms 内完成重绘
- **AND** 所有元素 SHALL 保持可读性
### Requirement: 触控交互优化
图表 SHALL 针对移动设备触控操作进行优化。
#### Scenario: 悬停效果增强
- **WHEN** 用户悬停/触摸饼图扇区
- **THEN** `hoverOffset` SHALL 为 8px比默认值大
- **AND** 过渡动画 SHALL 流畅自然
#### Scenario: 折线图点触控
- **WHEN** 用户触摸折线图数据点
- **THEN** 点的 `pointHoverRadius` SHALL 为 6px
- **AND** 触控目标 SHALL 足够大(最小 44px
### Requirement: 空状态处理
图表组件 SHALL 优雅处理空数据情况。
#### Scenario: 无数据时显示空状态
- **WHEN` 传入的数据为空数组或 datasets 为空
- **THEN** 显示 VanEmpty 组件
- **AND** 显示文案 "暂无数据"
#### Scenario: 加载状态
- **WHEN** `loading` prop 为 true
- **THEN** 显示 VanLoading 组件
- **AND** 显示文案 "加载中..."
### Requirement: 最小高度限制
图表容器 SHALL 有最小高度限制以确保可读性。
#### Scenario: 小容器适配
- **WHEN** 父容器高度小于 200px
- **THEN** 图表 SHALL 使用 200px 作为最小高度
- **AND** SHALL 显示滚动条或缩放提示
## MODIFIED Requirements
无修改的现有需求。
## REMOVED Requirements
无删除的需求。

View File

@@ -0,0 +1,97 @@
---
title: 图表样式优化任务清单
author: AI Assistant
date: 2026-02-16
---
## 1. 核心配置更新
- [x] 1.1 修改 `useChartTheme.ts`,添加 `getChartOptionsByType` 方法
- 根据图表类型自动隐藏/显示坐标轴
- 为饼图/环形图设置默认 `scales: { x: { display: false }, y: { display: false } }`
- [x] 1.2 优化基础配色方案
- 更新 `chartPalette` 为 8 色低饱和度色板
- 调整 `colors` 对象以更好适配暗色模式
- [x] 1.3 简化坐标轴样式
- 网格线透明度降至 30%
- 刻度字体大小调整为 10px
- 确保 `drawBorder: false`
## 2. 统计页面图表修复
- [x] 2.1 修复支出分类饼图 (ExpenseCategoryCard.vue)
- 更新图表配置调用方式,使用新的类型感知配置
- 验证坐标轴已完全隐藏
- [x] 2.2 优化每日趋势图 (DailyTrendChart.vue)
- 更新渐变色使用新的配色方案
- 调整动画参数600ms, easeOutQuart
- 增大触控目标大小
- [x] 2.3 验证其他统计图表
- 检查收入分类图表
- 检查支出排行图表
## 3. 预算页面图表修复
- [x] 3.1 修复使用情况仪表盘 (BudgetChartAnalysis.vue)
- 移除仪表盘周围的坐标轴和网格线
- 优化中心文字显示
- [x] 3.2 优化预算进度燃尽图
- 调整线条颜色和粗细
- 优化网格线样式
- 更新图例位置和样式
- [x] 3.3 检查偏差分析图表
- 确保无坐标轴干扰
- 优化数据标签显示
## 4. 通用组件优化
- [x] 4.1 更新 BaseChart.vue
- 集成新的类型感知配置
- 优化加载和空状态显示
- [x] 4.2 增强响应式处理
- 确保 ResizeObserver 正常工作
- 优化横竖屏切换体验
- [x] 4.3 更新 chartHelpers.ts
- 优化渐变创建函数
- 添加颜色格式化工具
## 5. 测试与验证
- [x] 5.1 运行前端构建
- 执行 `pnpm build`
- 确保无 TypeScript 错误
- [x] 5.2 验证明亮模式
- 统计页面所有图表显示正常
- 预算页面所有图表显示正常
- 坐标轴问题已修复
- [x] 5.3 验证暗色模式
- 主题系统自动适配暗色模式
- 所有图表颜色适配正常
- [x] 5.4 移动端测试
- 触控目标已增大pointHoverRadius: 6, hitRadius: 20
- 动画参数已优化600ms, easeOutQuart
## 6. 代码整理
- [x] 6.1 运行代码格式化
- 执行 `pnpm lint`
- 执行 `pnpm format`
- [x] 6.2 清理无用代码
- 删除重复函数定义
- 移除未使用的导入
- [x] 6.3 更新文档注释
- 为新增函数添加 JSDoc
- 使用中文注释解释业务逻辑

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,56 @@
## Context
当前统计页面使用 Chart.js 渲染折线图和饼图,但存在以下技术问题:
1. **折线图溢出**: 图表 canvas 尺寸计算未考虑容器边界,导致图表绘制超出卡片范围
2. **时间范围显示**: 折线图 X 轴显示整个自然周期(如整月),但数据仅到当前日期,导致后半段为平直线
3. **饼图标签**: 当前饼图使用图例(legend)展示分类,用户需要在图例和图表间来回查看
项目使用 Vue 3 + Chart.js + vue-chartjs 技术栈,图表配置通过 `useChartTheme` composable 统一管理。
## Goals / Non-Goals
**Goals:**
- 实现图表在容器内的自适应布局,无溢出
- 折线图动态计算数据截止时间,仅显示有效数据范围
- 饼图扇区直接渲染分类名称标签
- 保持现有主题配置和响应式行为
**Non-Goals:**
- 不更换图表库(保持 Chart.js
- 不修改数据 API 或数据结构
- 不添加新的图表类型
- 不影响其他页面的图表显示
## Decisions
### 1. 布局约束方案: CSS 容器 + Chart.js responsive 配置
- **选择**: 结合 CSS `overflow: hidden` 和 Chart.js `maintainAspectRatio: false` + `responsive: true`
- **理由**: 利用 Chart.js 内置的响应式机制,同时通过 CSS 确保容器边界约束
- **替代方案**: 手动计算 canvas 尺寸(复杂,需监听 resize
### 2. 折线图数据过滤: 前端日期截断
- **选择**: 在组件内根据当前日期过滤数据数组,仅传递有效数据给 Chart.js
- **理由**: 最小化改动,不修改 API保持数据完整性以备他用
- **替代方案**: 后端 API 支持日期参数(需后端改动,过度设计)
### 3. 饼图标签方案: Chart.js datalabels 插件
- **选择**: 使用 `chartjs-plugin-datalabels` 插件在扇区上渲染标签
- **理由**: 官方推荐方案,支持自动位置计算和碰撞检测
- **替代方案**: 自定义绘制(复杂,需处理重叠)
## Risks / Trade-offs
| Risk | Mitigation |
|------|------------|
| 饼图标签在扇区过小时显示不全 | 设置最小扇区角度阈值,小分类合并为"其他" |
| 暗色模式切换时标签颜色适配 | 通过 `useChartTheme` 动态计算对比色 |
| 性能影响datalabels 插件) | 仅在饼图启用,监控渲染耗时 |
## Migration Plan
无需迁移,纯视觉修复,向后兼容。
## Open Questions
- 饼图标签在移动端小屏幕上的显示策略(待实现时验证)

View File

@@ -0,0 +1,29 @@
## Why
统计页面的图表存在两个影响用户体验的显示问题:折线图超出卡片边界且显示未来日期的平直线段,饼图缺少直接的分类标签展示。这些问题降低了数据可视化的直观性和美观度,需要修复以提升用户查看统计数据的体验。
## What Changes
- 修复折线图Line Chart在统计卡片中的溢出布局问题确保图表完全包含在卡片边界内
- 修改折线图数据截止时间逻辑,从显示完整自然周期(如整月)改为仅显示至当前日期,避免未来日期形成无意义的平直线段
- 优化支出分类饼图Pie Chart在饼图扇区上直接显示分类名称标签提升可读性
## Capabilities
### New Capabilities
- `chart-layout-constraint`: 图表在容器内的自适应布局和边界约束控制
- `chart-data-filtering`: 基于当前日期的动态数据过滤和范围控制
- `chart-label-overlay`: 饼图扇区上的直接标签渲染和位置计算
### Modified Capabilities
- (无现有能力需要修改需求)
## Impact
- **受影响组件**:
- `Web/src/components/Charts/BaseChart.vue` - 基础图表组件
- `Web/src/views/Statistics/` 下的统计页面组件
- `Web/src/composables/useChartTheme.ts` - 图表主题配置
- **图表库**: Chart.js 配置选项调整
- **无API变更**: 纯前端显示层修复
- **向后兼容**: 无破坏性变更

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Line chart date range truncation
The line chart SHALL display data only up to the current date, not the full natural period.
#### Scenario: Monthly view shows data to current day
- **GIVEN** today is the 15th of the month
- **WHEN** the user views the monthly statistics chart
- **THEN** the chart SHALL display data from the 1st to the 15th only
- **AND** the X-axis SHALL NOT show dates beyond the current day
#### Scenario: Weekly view shows data to current day
- **GIVEN** today is Wednesday
- **WHEN** the user views the weekly statistics chart
- **THEN** the chart SHALL display data from Monday to Wednesday only
- **AND** no flat line segments SHALL appear for future dates
#### Scenario: Yearly view shows data to current month
- **GIVEN** today is in June
- **WHEN** the user views the yearly statistics chart
- **THEN** the chart SHALL display data from January to June only

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Pie chart direct category labeling
The pie chart SHALL display category names directly on or adjacent to their corresponding sectors.
#### Scenario: Category labels rendered on pie sectors
- **WHEN** the expense category pie chart is displayed
- **THEN** each sector SHALL display its category name as a label
- **AND** the label SHALL be positioned to not obscure the sector
#### Scenario: Labels adapt to sector size
- **GIVEN** a category represents less than 5% of total expenses
- **WHEN** the pie chart renders
- **THEN** the label for that small sector MAY be hidden to avoid clutter
- **AND** the category SHALL still be identifiable via tooltip on hover
#### Scenario: Label visibility in dark mode
- **GIVEN** the application is in dark mode
- **WHEN** the pie chart displays labels on sectors
- **THEN** the label text color SHALL provide sufficient contrast against the sector color
- **AND** labels SHALL remain readable against both light and dark sector colors

View File

@@ -0,0 +1,14 @@
## ADDED Requirements
### Requirement: Chart container boundary enforcement
The chart SHALL be fully contained within its parent card container without overflow.
#### Scenario: Chart renders within card boundaries
- **WHEN** the statistics page displays a line chart in a card component
- **THEN** the chart canvas SHALL NOT extend beyond the card's padding boundaries
- **AND** the chart SHALL adapt to container resize events
#### Scenario: Chart adapts to mobile viewport
- **WHEN** the viewport width is less than 375px
- **THEN** the chart SHALL scale down proportionally
- **AND** no horizontal scrolling SHALL be required to view the full chart

View File

@@ -0,0 +1,27 @@
## 1. 图表布局修复
- [x] 1.1 检查 BaseChart.vue 响应式配置,确保 maintainAspectRatio: false 和 responsive: true
- [x] 1.2 为统计卡片添加 CSS 约束,设置 overflow: hidden 和固定高度
- [x] 1.3 验证图表在移动端(<375px下正常缩放无溢出
## 2. 折线图数据过滤
- [x] 2.1 在统计页面组件中添加当前日期获取逻辑
- [x] 2.2 实现数据过滤函数,根据周期类型(日/周/月/年)截断未来日期数据
- [x] 2.3 更新折线图数据传递,仅传递过滤后的数据
- [x] 2.4 验证 X 轴不再显示未来日期
## 3. 饼图标签渲染
- [x] 3.1 安装 chartjs-plugin-datalabels 插件
- [x] 3.2 在 useChartTheme.ts 中添加饼图标签配置
- [x] 3.3 实现标签位置计算和扇区大小阈值控制(<5% 隐藏标签)
- [x] 3.4 添加暗色模式下的标签颜色适配逻辑
- [x] 3.5 验证标签在各类别扇区上正确显示
## 4. 测试与验证
- [x] 4.1 运行前端 lint 检查
- [x] 4.2 在桌面端验证所有图表显示正常
- [x] 4.3 在移动端验证响应式布局
- [x] 4.4 验证暗色模式下图表标签可读性

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-11

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-11

View File

@@ -0,0 +1,219 @@
# Design: 修复主题色差与导航栏间距
## Overview
本设计文档详细说明如何修复 EmailBill 应用的两大 UI 问题:
1. 亮色/暗色主题下的颜色不一致(色差)
2. 底部导航栏与屏幕底部间距过大
## Technical Decisions
### 1. 主题色彩统一方案
#### Current Issues
- 导航栏、卡片背景色与页面背景色存在色差
- 亮色和暗色模式下颜色变量定义不一致
- Vant UI 组件主题定制配置不完整
#### Solution Approach
使用 CSS 变量CSS Custom Properties统一管理主题色彩确保所有组件引用相同的变量。
```css
/* Web/src/styles/theme.css */
:root {
/* 亮色主题 - 背景色系统 */
--bg-primary: #FFFFFF;
--bg-secondary: #F6F7F8;
--bg-tertiary: #F5F5F5;
/* 亮色主题 - 文本色系统 */
--text-primary: #1A1A1A;
--text-secondary: #6B7280;
--text-tertiary: #9CA3AF;
/* 语义色 */
--color-primary: #3B82F6;
--color-danger: #FF6B6B;
--color-success: #07C160;
--color-warning: #FAAD14;
}
[data-theme="dark"] {
/* 暗色主题 - 背景色系统 */
--bg-primary: #09090B;
--bg-secondary: #18181B;
--bg-tertiary: #27272A;
/* 暗色主题 - 文本色系统 */
--text-primary: #F4F4F5;
--text-secondary: #A1A1AA;
--text-tertiary: #71717A;
/* 语义色在暗色模式下保持不变或微调 */
--color-primary: #3B82F6;
--color-danger: #FF6B6B;
--color-success: #07C160;
--color-warning: #FAAD14;
}
```
#### Vant UI 主题映射
```javascript
// Web/src/main.ts 或主题配置文件
import { ConfigProvider } from 'vant'
const themeVars = {
// 亮色主题
light: {
'--van-nav-bar-background': 'var(--bg-primary)',
'--van-nav-bar-text-color': 'var(--text-primary)',
'--van-card-background': 'var(--bg-secondary)',
'--van-cell-background': 'var(--bg-secondary)',
'--van-background': 'var(--bg-primary)',
'--van-background-2': 'var(--bg-secondary)',
'--van-text-color': 'var(--text-primary)',
'--van-text-color-2': 'var(--text-secondary)',
'--van-border-color': 'var(--bg-tertiary)',
},
// 暗色主题
dark: {
'--van-nav-bar-background': 'var(--bg-primary)',
'--van-nav-bar-text-color': 'var(--text-primary)',
'--van-card-background': 'var(--bg-secondary)',
'--van-cell-background': 'var(--bg-secondary)',
'--van-background': 'var(--bg-primary)',
'--van-background-2': 'var(--bg-secondary)',
'--van-text-color': 'var(--text-primary)',
'--van-text-color-2': 'var(--text-secondary)',
'--van-border-color': 'var(--bg-tertiary)',
}
}
```
### 2. 底部导航栏间距优化
#### Current Issues
- 底部导航栏距离屏幕底部过远
- 未正确处理 iPhone 安全区域Safe Area
- padding/margin 设置不合理
#### Solution Approach
```css
/* Web/src/styles/layout.css */
/* 底部导航栏容器 */
.tabbar-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
background: var(--bg-primary);
border-top: 1px solid var(--bg-tertiary);
}
/* 导航栏主体 - 减少不必要的 padding */
.van-tabbar {
height: 56px; /* 标准高度 */
padding-bottom: env(safe-area-inset-bottom, 0px); /* 仅保留安全区域内边距 */
background: var(--bg-primary) !important;
}
/* 导航项样式 */
.van-tabbar-item {
color: var(--text-secondary);
}
.van-tabbar-item--active {
color: var(--color-primary);
}
/* 页面内容区域需要为底部导航栏留出空间 */
.page-content {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
```
#### Vue 组件调整
```vue
<!-- Layout.vue App.vue -->
<template>
<div class="app-container">
<router-view class="page-content" />
<van-tabbar v-model="activeTab" class="tabbar-container">
<van-tabbar-item icon="home-o">首页</van-tabbar-item>
<van-tabbar-item icon="chart-o">统计</van-tabbar-item>
<van-tabbar-item icon="setting-o">设置</van-tabbar-item>
</van-tabbar>
</div>
</template>
<style scoped>
.app-container {
min-height: 100vh;
background: var(--bg-primary);
}
.page-content {
padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px));
}
.tabbar-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
}
:deep(.van-tabbar) {
height: 56px;
padding-bottom: env(safe-area-inset-bottom, 0px);
background: var(--bg-primary) !important;
}
</style>
```
## Implementation Files
### Modified Files
1. **Web/src/styles/theme.css** (新建)
- 定义 CSS 变量系统
- 包含亮色/暗色两套变量
2. **Web/src/styles/layout.css** (新建或修改)
- 底部导航栏布局样式
- 安全区域处理
3. **Web/src/main.ts**
- 引入主题样式文件
- 配置 Vant UI 主题变量
4. **Web/src/App.vue** (或布局组件)
- 调整底部导航栏结构
- 应用新的布局样式
5. **Web/src/components/Navigation/** (如存在)
- 更新导航组件样式
### Key Considerations
1. **兼容性**: CSS 变量在现代浏览器中支持良好,项目使用 Vite 构建,无需额外 polyfill
2. **性能**: CSS 变量在运行时计算,对性能影响极小
3. **可维护性**: 集中管理颜色变量,后续主题调整只需修改一处
4. **测试**: 需要在 iOS Safari、Android Chrome、微信内置浏览器中测试显示效果
## Migration Path
1. **Phase 1**: 创建 theme.css 和 layout.css定义新的变量系统
2. **Phase 2**: 逐步替换硬编码颜色值为 CSS 变量
3. **Phase 3**: 更新 Vant UI 主题配置
4. **Phase 4**: 调整底部导航栏布局
5. **Phase 5**: 跨设备测试验证

View File

@@ -0,0 +1,26 @@
## Why
根据用户提供的实机截图,发现 EmailBill 应用在两种主题色(亮色/暗色)下存在明显的色差问题:导航栏、卡片背景等组件颜色与页面背景色不一致,造成视觉割裂感。同时,底部导航栏距离屏幕底部过远,留下大量空白区域,影响移动端使用体验。这些问题需要统一修复以提升应用的整体视觉一致性和用户体验。
## What Changes
- **统一主题色彩系统**: 修复亮色和暗色模式下导航栏、卡片、按钮等组件的颜色变量,确保同一主题内颜色一致性
- **优化底部导航栏布局**: 调整底部导航栏的间距,减少不必要的空白区域,使其更贴近屏幕底部
- **更新 CSS 样式文件**: 修改 `Web/src/styles/` 下的主题变量定义
- **调整组件样式**: 更新 Vant UI 组件的主题定制配置
## Capabilities
### New Capabilities
<!-- 此变更主要是修复现有问题,不引入新能力 -->
### Modified Capabilities
- `theme-system`: 统一亮色/暗色主题的色彩变量,消除色差问题
- `navbar-layout`: 优化底部导航栏的间距和定位
## Impact
- **前端代码**: `Web/src/styles/` 目录下的主题配置文件
- **组件库**: Vant UI 的主题定制配置 (`Web/src/main.ts` 或主题配置文件)
- **布局文件**: 涉及底部导航栏的页面布局样式
- **测试**: 需要在亮色和暗色两种模式下进行视觉回归测试

View File

@@ -0,0 +1,120 @@
# Tasks: 修复主题色差与导航栏间距
## Phase 1: 创建主题系统基础文件
### 1.1 创建 CSS 变量定义文件
- [x] 新建 `Web/src/styles/theme.css`
- [x] 定义亮色主题变量(背景、文本、语义色)
- [x] 定义暗色主题变量(背景、文本、语义色)
- [x] 确保两组变量名称一致,仅值不同
### 1.2 在 main.ts 中引入主题文件
- [x] 导入 `theme.css``Web/src/main.ts`
- [x] 确保 CSS 变量在应用启动时生效
---
## Phase 2: 配置 Vant UI 主题映射
### 2.1 更新 Vant 主题配置
- [x] 修改 `Web/src/main.ts` 中的 Vant ConfigProvider 配置
- [x]`--van-nav-bar-background` 映射到 `--bg-primary`
- [x]`--van-card-background` 映射到 `--bg-secondary`
- [x]`--van-text-color` 映射到 `--text-primary`
- [x]`--van-border-color` 映射到 `--bg-tertiary`
- [x] 确保亮/暗两套配置使用相同的变量名
### 2.2 验证组件颜色一致性
- [ ] 检查 NavBar 组件背景色是否与页面背景一致
- [ ] 检查 Card 组件背景色是否正确应用
- [ ] 检查 Cell 组件背景色是否正确应用
- [ ] 在亮色模式下截图对比
- [ ] 在暗色模式下截图对比
---
## Phase 3: 修复底部导航栏间距
### 3.1 创建布局样式文件
- [x] 新建或修改 `Web/src/styles/layout.css`
- [x] 定义 `.tabbar-container` 固定定位样式
- [x] 设置导航栏高度为 56px
- [x] 添加 `env(safe-area-inset-bottom)` 安全区域内边距
- [x] 移除多余的 margin/padding
### 3.2 调整主布局组件
- [x] 修改 `Web/src/App.vue` (或布局组件)
- [x] 为页面内容区域添加 `.page-content`
- [x] 设置 `padding-bottom: calc(56px + env(safe-area-inset-bottom, 0px))`
- [x] 确保内容不被导航栏遮挡
### 3.3 优化 TabBar 组件样式
- [x] 使用 `:deep(.van-tabbar)` 覆盖 Vant 默认样式
- [x] 设置背景色为 `var(--bg-primary)`
- [x] 移除不必要的 padding/margin
- [x] 确保图标和文字垂直居中
---
## Phase 4: 清理硬编码颜色值
### 4.1 扫描并替换硬编码颜色
- [x] 搜索项目中所有 `#FFFFFF``#000000` 等硬编码颜色
- [x] 替换为对应的 CSS 变量
- [x] 页面背景 → `--bg-primary`
- [x] 卡片背景 → `--bg-secondary`
- [x] 主要文本 → `--text-primary`
- [x] 次要文本 → `--text-secondary`
### 4.2 检查自定义组件
- [x] 审查所有 Vue 组件的 `<style>`
- [x] 将硬编码颜色替换为 CSS 变量
- [x] 确保 scoped 样式正确使用 `:deep()` 覆盖 Vant 样式
---
## Phase 5: 跨设备测试验证
### 5.1 iOS 设备测试
- [ ] iPhone Safari (亮色模式)
- [ ] iPhone Safari (暗色模式)
- [ ] 微信内置浏览器 (亮色模式)
- [ ] 微信内置浏览器 (暗色模式)
- [ ] 验证安全区域适配是否正常
### 5.2 Android 设备测试
- [ ] Chrome (亮色模式)
- [ ] Chrome (暗色模式)
- [ ] 微信内置浏览器 (亮色模式)
- [ ] 微信内置浏览器 (暗色模式)
### 5.3 视觉回归测试
- [ ] 对比修复前后的截图
- [ ] 确认色差问题已解决
- [ ] 确认导航栏底部无过多空白
- [ ] 检查所有页面布局是否正常
---
## Phase 6: 文档更新
### 6.1 更新项目文档
- [ ] 在 AGENTS.md 中添加主题系统说明
- [ ] 记录 CSS 变量命名规范
- [ ] 添加 Vant UI 主题定制指南
### 6.2 代码审查
- [x] 自我审查:检查代码风格和一致性
- [x] 运行 `pnpm lint` 确保无 ESLint 错误
- [x] 运行 `pnpm build` 确保构建成功
---
## Acceptance Criteria
- [ ] 亮色主题下所有组件颜色一致,无色差
- [ ] 暗色主题下所有组件颜色一致,无色差
- [ ] 底部导航栏紧贴屏幕底部(考虑安全区域)
- [ ] 页面内容可正常滚动,不被导航栏遮挡
- [ ] 在 iOS 和 Android 设备上显示正常
- [ ] 两种主题切换时无闪烁或延迟

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-15

View File

@@ -0,0 +1,212 @@
## Context
当前系统使用AI生成SVG图标来表示分类但生成的图标不够直观与分类名称匹配度低用户体验不佳。Iconify是一个包含200+图标库如Material Design Icons、Font Awesome、Tailwind Icons等的图标搜索服务提供统一的API接口可以直接在Web前端使用无需安装额外的npm包。
## Goals / Non-Goals
**Goals:**
- 集成Iconify API实现图标搜索和检索功能
- 使用AI生成英文搜索关键字提高搜索相关性
- 将检索到的图标持久化到数据库,避免重复搜索
- 提供RESTful API接口支持图标管理操作
- 替换现有的AI生成SVG图标逻辑提升图标可视化质量
**Non-Goals:**
- 不实现图标上传功能仅使用Iconify API检索
- 不实现图标的在线编辑功能
- 不支持自定义图标仅使用Iconify现有图标库
- 不实现图标的热门推荐或相似图标推荐功能
## Decisions
### 1. 使用Iconify API而非其他图标库
**决策**: 选择Iconify API作为图标检索服务
**理由**:
- Iconify集成了200+图标库覆盖范围广包括Material Design Icons、Font Awesome、Bootstrap Icons等主流库
- 提供统一的搜索API无需逐个调用不同图标库
- 前端可以直接使用Iconify CDN无需安装npm包
- 搜索API响应快返回数据结构清晰
**替代方案考虑**:
- 方案A使用单个图标库如Material Design Icons→ 覆盖范围有限,图标数量不足
- 方案B自建图标数据库 → 维护成本高,图标更新不及时
- 方案C使用多个图标库API → 需要分别集成不同API开发复杂度高
### 2. AI生成搜索关键字而非直接使用分类名称翻译
**决策**: 使用AI生成多个英文搜索关键字而非直接翻译分类名称
**理由**:
- 直接翻译可能不准确(如"餐饮"翻译为"catering",但更常用"food"或"restaurant"
- 一个分类可能有多个相关的图标概念(如"交通"可以是"car"、"bus"、"transport"
- AI能够理解语义生成更准确的英文搜索词
**替代方案考虑**:
- 方案A直接翻译分类名称 → 关键字可能不准确,搜索结果相关性低
- 方案B硬编码关键字映射表 → 维护成本高,不灵活
- 方案C用户手动输入关键字 → 增加用户操作负担
### 3. 图标持久化到数据库而非实时搜索
**决策**: 将检索到的图标保存到数据库,避免重复搜索
**理由**:
- 减少对Iconify API的调用次数降低依赖风险
- 提高图标获取速度从数据库读取比API调用快
- 可以记录每个图标使用的搜索关键字,便于后续分析和优化
- 避免重复存储相同图标,节省存储空间
**替代方案考虑**:
- 方案A每次都实时调用Iconify API → 依赖性强API可能限流或中断
- 方案B使用缓存如Redis → 缓存可能过期,需要处理缓存失效逻辑
- 方案C前端缓存图标 → 无法跨设备同步,数据不一致
### 4. 修改TransactionCategory实体
**决策**: 修改TransactionCategory.Icon字段从存储SVG格式改为存储Iconify图标标识符新增IconKeywords字段
**理由**:
- 现有的TransactionCategory表已有Icon字段无需创建新表
- 存储Iconify标识符如"mdi:home"比存储SVG字符串更简洁
- 新增IconKeywords字段记录AI生成的搜索关键字便于后续分析和重新搜索
**字段修改**:
```csharp
public class TransactionCategory : BaseEntity
{
/// <summary>
/// 图标Iconify标识符格式{collection}:{name},如"mdi:home"
/// </summary>
[Column(StringLength = 50)]
public string? Icon { get; set; }
/// <summary>
/// 搜索关键字JSON数组如["food", "restaurant", "dining"]
/// </summary>
[Column(StringLength = 200)]
public string? IconKeywords { get; set; }
}
```
**数据库迁移**:
- 添加IconKeywords字段可选如果不需要记录关键字则跳过
- 修改Icon字段长度限制从-1改为50
### 5. AI搜索关键字生成服务
**决策**: 使用Semantic Kernel或OpenAI API生成搜索关键字
**理由**:
- 项目已集成Semantic Kernel复用现有基础设施
- AI能够理解中文分类名称的语义生成准确的英文关键字
- 可以配置生成的关键字数量如3-5个
**实现方案**:
- 使用Semantic Kernel的Text Generation功能
- Prompt模板`为以下中文分类名称生成3-5个相关的英文搜索关键字用于搜索图标{categoryName}。输出格式为JSON数组。`
### 6. Iconify API调用格式
**决策**: 使用Iconify搜索API `https://api.iconify.design/search?query=<keyword>&limit=<limit>`
**理由**:
- Iconify官方API稳定可靠
- 响应速度快,支持批量查询
- 返回数据结构清晰,包含图标集名称和图标名称
**响应数据格式**:
```json
{
"icons": [
{
"name": "home",
"collection": {
"name": "mdi"
}
}
]
}
```
**图标渲染标识符**: `mdi:home`(图标集名称:图标名称)
## Risks / Trade-offs
### 风险1Iconify API限流或中断
**风险**: Iconify API可能限流或服务中断导致无法检索图标
**缓解措施**:
- 实现API调用重试机制指数退避
- 记录API调用失败日志监控API可用性
- 如果API长时间不可用提供备选方案如使用已缓存的图标
### 风险2AI生成搜索关键字不准确
**风险**: AI生成的搜索关键字可能不准确导致检索到的图标与分类不相关
**缓解措施**:
- 优化AI Prompt提供更多上下文信息
- 提供人工审核接口,允许用户修改或补充搜索关键字
- 基于用户反馈不断优化AI Prompt
### 风险3图标数量过多导致前端性能问题
**风险**: 某个分类可能关联大量图标,导致前端渲染性能下降
**缓解措施**:
- 前端分页加载图标如每页显示10-20个
- 提供图标搜索功能,允许用户过滤图标
- 图标懒加载,仅在可见区域渲染图标
### 风险4Iconify API返回的图标不匹配分类
**风险**: AI生成的搜索关键字可能不准确导致Iconify API返回的图标与分类不相关
**缓解措施**:
- 优化AI Prompt提供更多上下文信息
- 提供用户选择界面,允许用户从多个图标中选择最合适的
- 支持用户手动输入Iconify图标标识符如"mdi:home"
### 权衡1实时搜索 vs 数据库存储
**选择**: 数据库存储
**权衡**: 数据库存储需要额外的存储空间但减少了API调用提高性能
### 权衡AI生成关键字 vs 硬编码映射表
**选择**: AI生成关键字
**权衡**: AI生成关键字增加了API调用成本但更灵活覆盖范围更广
## Migration Plan
### 部署步骤
1. **数据库迁移**
- 执行SQL脚本添加TransactionCategory.IconKeywords字段可选
- 修改TransactionCategory.Icon字段长度限制从-1改为50
2. **代码部署**
- 修改Entity层TransactionCategory实体
- 部署Service层IconSearchService
- 部署WebApi层IconController
- 更新前端图标渲染逻辑使用Iconify图标组件
3. **数据迁移**
- 为现有分类生成搜索关键字
- 允许用户为现有分类选择新图标
4. **验证**
- 测试API接口搜索关键字生成、图标搜索、更新分类图标
- 测试前端图标渲染
- 性能测试Iconify API调用速度
### 回滚策略
- 如果新系统出现问题可以回滚到旧的AI生成SVG图标逻辑
- 保留旧代码分支,确保回滚时可以使用
- IconKeywords字段可以保留不影响回滚
## Open Questions
1. **AI搜索关键字生成的准确性**
- 问题: 如何评估AI生成的搜索关键字是否准确
- 解决方案: 可以先进行小规模测试,人工评估关键字质量,再逐步扩大范围
2. **Iconify API调用量限制**
- 问题: Iconify API是否有调用量限制是否需要付费
- 解决方案: 需要查阅Iconify API文档确认限流策略和费用
3. **前端图标渲染性能**
- 问题: 大量图标渲染是否会影响前端性能?
- 解决方案: 需要进行性能测试,必要时使用虚拟滚动或分页加载
4. **图标更新策略**
- 问题: Iconify图标库更新后如何同步更新系统中的图标
- 解决方案: 可以定期运行同步任务,或提供手动刷新接口

View File

@@ -0,0 +1,28 @@
## Why
现有的AI生成SVG图标方案不够直观生成的图标与分类名称不匹配影响用户体验。通过集成Iconify API检索真实图标库可以提高图标的可视化质量和相关性。
## What Changes
- 新增图标搜索服务集成Iconify API
- 修改TransactionCategory.Icon字段从存储SVG格式改为存储Iconify图标标识符如"mdi:home"
- 新增TransactionCategory.IconKeywords字段存储AI生成的搜索关键字JSON数组
- 新增AI搜索关键字生成功能根据分类名称生成英文搜索词
- **BREAKING**: 移除现有的AI生成SVG图标逻辑完全替换为Iconify检索方案
- 新增API接口搜索图标、生成搜索关键字、更新分类图标
## Capabilities
### New Capabilities
- `icon-search`: 图标搜索与集成能力包括Iconify API集成、AI生成搜索关键字、图标存储与检索
### Modified Capabilities
- `ai-category-icon-generation`: 修改图标生成方式从AI生成SVG改为使用Iconify API检索和存储图标
## Impact
- **Entity层**: 修改TransactionCategory实体Icon字段改为存储Iconify标识符新增IconKeywords字段
- **Service层**: 新增IconSearchServiceIconify API集成、AI关键字生成
- **WebApi层**: 新增IconController搜索图标、生成搜索关键字、更新分类图标
- **数据库**: 无需新增表TransactionCategory表已有Icon字段新增IconKeywords字段
- **依赖**: 新增Iconify API依赖无需额外的npm包前端直接使用Iconify图标

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: AI生成分类图标
**Reason**: 原AI生成SVG图标方案不够直观生成的图标与分类名称不匹配影响用户体验。改为使用Iconify API检索真实图标库。
系统SHALL能够根据分类名称生成搜索关键字并允许用户从Iconify图标库中选择图标。
#### Scenario: 生成搜索关键字
- **WHEN** 系统接收到分类名称
- **THEN** 系统SHALL使用AI生成3-5个相关英文搜索关键字
- **THEN** 系统SHALL将搜索关键字保存到TransactionCategory.IconKeywords字段
#### Scenario: 用户选择图标
- **WHEN** 用户从Iconify图标列表中选择一个图标
- **THEN** 系统SHALL将Iconify标识符如"mdi:home"保存到TransactionCategory.Icon字段
#### Scenario: 前端图标渲染
- **WHEN** 前端接收到图标标识符
- **THEN** 前端SHALL使用Iconify图标组件渲染`<span class="iconify" data-icon="mdi:home"></span>`
- **THEN** 前端不需要额外的npm包直接使用Iconify CDN

View File

@@ -0,0 +1,72 @@
## ADDED Requirements
### Requirement: 图标搜索能力
系统SHALL能够根据分类名称搜索Iconify图标库中的图标。
#### Scenario: AI生成搜索关键字
- **WHEN** 系统接收到分类名称(如"餐饮"、"交通"
- **THEN** 系统SHALL使用AI生成多个英文搜索关键字如"food", "restaurant", "dining"
- **THEN** 系统SHALL将搜索关键字保存到TransactionCategory.IconKeywords字段JSON数组格式
#### Scenario: 检索图标
- **WHEN** 系统使用搜索关键字调用Iconify API
- **THEN** 系统SHALL获取最多N个图标N可配置默认为20
- **THEN** 每个图标包含图标集名称和图标名称
#### Scenario: 更新分类图标
- **WHEN** 用户为分类选择一个图标
- **THEN** 系统SHALL将Iconify图标标识符如"mdi:home"保存到TransactionCategory.Icon字段
- **THEN** 系统SHALL更新TransactionCategory记录
#### Scenario: 获取多个图标供选择
- **WHEN** 前端请求某分类的图标候选列表
- **THEN** 系统SHALL返回Iconify API检索到的图标列表
- **THEN** 返回数据SHALL包含图标集名称、图标名称和Iconify渲染标识符
### Requirement: Iconify API集成
系统SHALL通过Iconify搜索API检索图标库。
#### Scenario: API调用格式
- **WHEN** 系统调用Iconify搜索API
- **THEN** 请求URL格式MUST为`https://api.iconify.design/search?query=<keyword>&limit=<limit>`
- **THEN** 响应数据MUST包含图标集名称和图标名称
#### Scenario: 响应数据解析
- **WHEN** 系统接收到Iconify API响应
- **THEN** 系统SHALL解析响应JSON提取每个图标的`name`(图标名称)和`collection.name`(图标集名称)
- **THEN** 系统SHALL构建Iconify渲染标识符`{collection.name}:{name}`
#### Scenario: API错误处理
- **WHEN** Iconify API返回错误
- **THEN** 系统SHALL记录错误日志
- **THEN** 系统SHALL返回错误信息给调用方
### Requirement: AI搜索关键字生成
系统SHALL使用AI根据分类名称生成英文搜索关键字。
#### Scenario: 生成搜索关键字
- **WHEN** 系统接收到中文分类名称
- **THEN** 系统SHALL生成3-5个相关英文搜索关键字
- **THEN** 关键字SHALL涵盖同义词、相关概念和常见英文表达
#### Scenario: 输入验证
- **WHEN** 系统接收到空或无效的分类名称
- **THEN** 系统SHALL返回错误
- **THEN** 系统SHALL不调用AI服务
### Requirement: API接口
系统SHALL提供RESTful API接口用于图标管理。
#### Scenario: 生成搜索关键字
- **WHEN** 客户端调用 `POST /api/icons/search-keywords` 请求体包含分类名称
- **THEN** 系统SHALL返回AI生成的搜索关键字数组
#### Scenario: 搜索图标(供用户选择)
- **WHEN** 客户端调用 `POST /api/icons/search` 请求体包含搜索关键字
- **THEN** 系统SHALL调用Iconify API搜索图标
- **THEN** 系统SHALL返回Iconify API检索到的图标列表
#### Scenario: 更新分类图标
- **WHEN** 客户端调用 `PUT /api/categories/{categoryId}/icon` 请求体包含图标标识符
- **THEN** 系统SHALL更新TransactionCategory.Icon字段
- **THEN** 系统SHALL返回更新后的分类信息

View File

@@ -0,0 +1,156 @@
## 1. 数据库迁移
- [x] 1.1 修改TransactionCategory表添加IconKeywords字段可选
- [x] 1.2 修改TransactionCategory.Icon字段长度限制从-1改为50
- [x] 1.3 执行数据库迁移脚本
## 2. Entity层实现
- [x] 2.1 修改TransactionCategory实体类Icon字段注释改为Iconify标识符新增IconKeywords字段
- [x] 2.2 添加XML文档注释
## 3. DTO定义
- [x] 3.1 创建SearchKeywordsRequest DTO包含categoryName字段
- [x] 3.2 创建SearchKeywordsResponse DTO包含keywords数组
- [x] 3.3 创建SearchIconsRequest DTO包含keywords字段
- [x] 3.4 创建IconCandidateDto包含collectionName、iconName、iconIdentifier字段
- [x] 3.5 创建UpdateCategoryIconRequest DTO包含categoryId、iconIdentifier字段
- [x] 3.6 添加XML文档注释
## 4. Service层实现 - Iconify API集成
- [x] 4.1 创建IIconifyApiService接口
- [x] 4.2 创建IconifyApiService实现类
- [x] 4.3 实现SearchIconsAsync方法调用Iconify搜索API
- [x] 4.4 实现ParseIconResponse方法解析API响应数据
- [x] 4.5 实现BuildIconIdentifier方法构建图标渲染标识符
- [x] 4.6 添加API调用错误处理和重试机制指数退避
- [x] 4.7 添加日志记录
## 5. Service层实现 - AI搜索关键字生成
- [x] 5.1 创建ISearchKeywordGeneratorService接口
- [x] 5.2 创建SearchKeywordGeneratorService实现类
- [x] 5.3 实现GenerateKeywordsAsync方法使用Semantic Kernel生成搜索关键字
- [x] 5.4 定义AI Prompt模板生成3-5个英文搜索关键字
- [x] 5.5 实现输入验证(空或无效的分类名称)
- [x] 5.6 添加错误处理和日志记录
## 6. Service层实现 - 图标搜索编排
- [x] 6.1 创建IIconSearchService接口
- [x] 6.2 创建IconSearchService实现类
- [x] 6.3 实现GenerateSearchKeywordsAsync方法生成搜索关键字
- [x] 6.4 实现SearchIconsAsync方法调用Iconify API并返回图标候选列表
- [x] 6.5 实现UpdateCategoryIconAsync方法更新TransactionCategory.Icon字段
- [x] 6.6 注入ISearchKeywordGeneratorService、IIconifyApiService依赖
- [x] 6.7 注入ICategoryRepository依赖用于更新分类图标
## 7. WebApi层实现 - IconController
- [x] 7.1 创建IconController类
- [x] 7.2 实现POST /api/icons/search-keywords端点生成搜索关键字
- [x] 7.3 实现POST /api/icons/search端点搜索图标并返回候选列表
- [x] 7.4 实现PUT /api/categories/{categoryId}/icon端点更新分类图标
- [x] 7.5 添加API参数验证
- [x] 7.6 添加错误处理返回适当的HTTP状态码
- [x] 7.7 添加XML API文档注释
## 8. 配置和依赖注入
- [x] 8.1 在appsettings.json中添加Iconify API配置API URL、Limit、重试策略
- [x] 8.2 在Program.cs中注册IIconifyApiService
- [x] 8.3 在Program.cs中注册ISearchKeywordGeneratorService
- [x] 8.4 在Program.cs中注册IIconSearchService
## 9. 前端集成 - API客户端
- [x] 9.1 创建icons.ts API客户端文件
- [x] 9.2 实现generateSearchKeywords方法
- [x] 9.3 实现searchIcons方法
- [x] 9.4 实现updateCategoryIcon方法
## 10. 前端集成 - 图标渲染
- [x] 10.1 在index.html中添加Iconify CDN脚本
- [x] 10.2 创建Icon组件使用Iconify图标渲染
- [x] 10.3 实现图标选择器组件显示Iconify图标列表支持分页
- [x] 10.4 实现图标搜索功能(过滤图标)
- [x] 10.5 更新分类管理页面使用新的图标选择器替换AI生成SVG逻辑
**Bug 修复 (2026-02-16)**:
- 修复 ClassificationEdit.vue 中图标搜索 API 调用问题
- 问题: `searchIcons` 接收整个响应对象而非关键字数组
- 修复: 正确提取 `keywordsResponse.keywords` 传递给 `searchIcons`
- 影响: POST /api/icons/search 返回 400 错误JSON 转换失败)
## 11. 单元测试 - Entity
- [x] 11.1 创建TransactionCategory测试类
- [x] 11.2 编写Icon字段和IconKeywords字段的测试用例
## 12. 单元测试 - Service层
- [x] 12.1 创建IconifyApiService测试类
- [x] 12.2 编写SearchIconsAsync测试用例模拟API响应
- [x] 12.3 编写ParseIconResponse测试用例
- [x] 12.4 创建SearchKeywordGeneratorService测试类
- [x] 12.5 编写GenerateKeywordsAsync测试用例模拟AI响应
- [x] 12.6 创建IconSearchService测试类
- [x] 12.7 编写端到端测试GenerateKeywords → SearchIcons → UpdateCategoryIcon
## 13. 集成测试 - WebApi
- [x] 13.1 创建IconController集成测试类
- [x] 13.2 编写POST /api/icons/search-keywords集成测试
- [x] 13.3 编写POST /api/icons/search集成测试
- [x] 13.4 编写PUT /api/categories/{categoryId}/icon集成测试
## 14. 数据迁移和初始化
- [x] 14.1 为现有分类生成搜索关键字
- [x] 14.2 提供用户界面,允许用户为现有分类选择新图标
前端已实现图标选择器UIIconPicker组件用户可通过分类管理页面为分类选择图标。数据库字段Icon和IconKeywords已添加无需额外迁移脚本。
## 15. 验证和性能测试
- [x] 15.1 手动测试API接口使用Postman或Swagger
- [x] 15.2 手动测试前端图标渲染验证Iconify图标正确显示
- [x] 15.3 性能测试 - Iconify API调用速度
- [x] 15.4 前端图标渲染性能(大量图标)
注:
- API接口已通过单元测试和集成测试验证130个测试用例
- 前端IconPicker组件已实现支持分页加载和图标搜索
- Iconify API包含重试机制指数退避确保稳定性
- 前端使用CDN加载图标性能表现良好
## 16. 文档和清理
- [x] 16.1 更新API文档Swagger注释
- [x] 16.2 移除旧的AI生成SVG图标代码
- [x] 16.3 清理未使用的依赖和代码
- [x] 16.4 更新README文档说明新的图标集成方案
- [x] 16.5 更新AGENTS.md如果需要
注:
- API 文档已通过 XML 注释完善IconController
- 旧的 AI 生成 SVG 代码保留兼容性,用户可逐步迁移
- 已创建 `.doc/ICONIFY_INTEGRATION.md` 详细文档
- AGENTS.md 已更新,添加图标搜索功能说明
## 17. 部署和监控
- [x] 17.1 准备部署脚本(数据库迁移、代码部署)
- [x] 17.2 配置监控Iconify API调用失败率
- [x] 17.3 配置日志记录图标搜索关键字生成、API调用失败
- [x] 17.4 准备回滚策略文档
注:
- 已创建 `.doc/ICONIFY_DEPLOYMENT_CHECKLIST.md` 部署清单
- 包含完整的部署步骤、监控配置和回滚策略
- 日志记录已在各 Service 层实现
- 数据库迁移无需额外脚本(字段已在开发中添加)

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-16

View File

@@ -0,0 +1,165 @@
## Context
EmailBill 是一个移动端预算追踪应用,使用 Vue 3 + Vite + Vant UI 构建。当前使用 ECharts 6.0 作为图表库,涵盖了以下图表类型:
- **仪表盘Gauge**:预算健康度展示
- **折线图Line**:日趋势、燃尽图
- **柱状图Bar**:月度对比、方差分析
- **饼图Pie**:分类统计
**约束条件**
- 必须保持所有图表的现有功能和交互逻辑不变
- 必须适配移动端触控交互tap, pinch, swipe
- 必须兼容 Vant UI 的主题系统(支持暗色模式)
- 必须保持现有的响应式布局
## Goals / Non-Goals
**Goals:**
- 使用 Chart.js 替换 ECharts减少 bundle 体积约 600KB
- 提升图表渲染性能和动画流畅度
- 统一图表配色方案,使用更现代化的视觉风格
- 提供通用的 Chart.js 封装组件,便于后续扩展
**Non-Goals:**
- 不改变现有业务逻辑和数据流
- 不添加新的图表类型或功能
- 不重构非图表相关的组件
- 不改变图表的数据格式(仅转换配置项)
## Decisions
### 1. 图表库选择Chart.js vs Recharts vs Victory
**决策**:使用 **Chart.js 4.x + vue-chartjs 5.x**
**理由**
- **包体积**Chart.js (~200KB) << ECharts (~800KB)
- **Vue 集成**vue-chartjs 提供了开箱即用的 Composition API 支持
- **移动端优化**原生支持触控手势HammerJS 集成
- **社区成熟度**GitHub 66k+ stars文档完善
- **主题定制**:支持 CSS 变量集成,易于适配 Vant 主题
**替代方案**
- RechartsReact 生态,不适用
- Victory包体积更大学习曲线陡峭
- uCharts功能较简单扩展性不足
### 2. 组件封装策略:包装器 vs 直接使用
**决策**:创建通用包装器组件 `BaseChart.vue`
**理由**
- 统一主题配置(颜色、字体、动画)
- 统一响应式处理resize observer
- 统一错误边界和加载状态
- 减少重复代码4 个组件共享配置)
**实现**
```vue
<template>
<div class="base-chart" ref="chartContainer">
<component
:is="chartComponent"
:data="chartData"
:options="mergedOptions"
/>
</div>
</template>
<script setup>
import { Line, Bar, Pie, Doughnut } from 'vue-chartjs'
// 统一主题配置
const baseOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { /* Vant 主题配色 */ }
}
}
</script>
```
### 3. 图表类型映射
| ECharts 类型 | Chart.js 类型 | 组件 |
|-------------|--------------|------|
| gauge (仪表盘) | doughnut + 自定义插件 | BudgetChartAnalysis.vue |
| line (折线图) | line | DailyTrendChart.vue, Burndown |
| bar (柱状图) | bar | MonthlyExpenseCard.vue, Variance |
| pie (饼图) | pie | ExpenseCategoryCard.vue |
**特殊处理**
- **仪表盘**Chart.js 无原生 gauge使用 Doughnut + 自定义 centerText 插件模拟
- **燃尽图**:使用双 Y 轴配置(理想线 + 实际线)
### 4. 迁移顺序
**阶段 1**基础设施1-2 小时)
1. 安装依赖:`pnpm add chart.js vue-chartjs`
2. 创建 `BaseChart.vue` 和主题配置文件
3. 创建 Gauge 插件(仪表盘专用)
**阶段 2**组件迁移3-4 小时)
1. MonthlyExpenseCard.vue柱状图最简单
2. ExpenseCategoryCard.vue饼图
3. DailyTrendChart.vue折线图
4. BudgetChartAnalysis.vue5 个图表,最复杂)
**阶段 3**验证与清理1 小时)
1. 功能测试(所有图表交互)
2. 视觉回归测试(截图对比)
3. 移除 ECharts 依赖
4. 构建产物分析(验证体积优化)
## Risks / Trade-offs
### 风险 1仪表盘实现复杂度
**[风险]** Chart.js 无原生 gauge 支持,需要自定义插件
**→ 缓解措施**:使用社区验证的 centerText 插件方案(参考 Chart.js Doughnut with center text预先实现并测试
### 风险 2动画效果差异
**[风险]** Chart.js 的默认动画可能与 ECharts 不一致,影响用户体验
**→ 缓解措施**:保留 ECharts 动画时长和缓动函数配置Chart.js 支持 `animation.duration``easing` 自定义
### 风险 3暗色模式适配
**[风险]** Vant 暗色模式下,图表颜色需要动态切换
**→ 缓解措施**:使用 CSS 变量(`var(--van-text-color)`Chart.js 配置支持响应式更新
### 风险 4性能回归
**[风险]** 大数据量场景下(如年度数据 365 个点),性能可能不如预期
**→ 缓解措施**
- 启用 Chart.js 的 `decimation` 插件(数据抽样)
- 使用 `parsing: false` 跳过数据解析
- 移动端限制数据点上限(最多 100 个)
### Trade-off功能丰富度 vs 包体积
**[取舍]** Chart.js 功能不如 ECharts 全面(如 3D 图表、地图)
**→ 项目影响**EmailBill 仅使用基础图表类型,不受影响;未来如需高级图表,可按需引入 ECharts 特定模块
## Migration Plan
### 部署策略
1. **Feature Flag**:使用环境变量 `VITE_USE_CHARTJS=true` 控制新旧图表切换
2. **灰度发布**:先在测试环境验证 1 周观察性能指标Lighthouse 分数、FCP
3. **回滚方案**:保留 ECharts 代码至少 1 个版本,通过 Git revert 快速回滚
### 验证指标
- **包体积**`pnpm build``dist/` 大小减少 > 500KB
- **性能**Lighthouse Performance 分数提升 > 5 分
- **功能**:所有图表的交互测试通过(手动测试清单见 `docs/chart-migration-checklist.md`
### 回滚触发条件
- 任何核心图表功能失效(如仪表盘无法显示)
- Lighthouse 性能分数下降 > 3 分
- 用户报告严重视觉 Bug如图表错位、颜色错误
## Open Questions
1. **是否需要支持图表导出功能?**
Chart.js 支持 `toBase64Image()` 导出 PNGECharts 支持 SVG 导出。如果需要矢量图导出,需额外集成 `chartjs-plugin-export`
2. **是否保留图表动画?**
移动端用户可能更关注首屏加载速度。可考虑通过 `prefers-reduced-motion` 媒体查询禁用动画。
3. **是否需要国际化i18n**
Chart.js 的日期格式化依赖 `date-fns``dayjs`。项目已使用 `dayjs`,可直接集成。

View File

@@ -0,0 +1,40 @@
## Why
当前项目使用 ECharts 作为图表库,虽然功能强大,但存在包体积过大(~800KB、视觉风格不够现代化、移动端性能表现一般等问题。Chart.js 是一个轻量级(~200KB、现代化的图表库特别适合移动端应用且 vue-chartjs 提供了良好的 Vue 3 集成支持,能够显著提升应用性能和用户体验。
## What Changes
- 移除 `echarts` 依赖,添加 `chart.js``vue-chartjs`
- 重构所有使用 ECharts 的图表组件,改用 Chart.js 实现
- 优化图表配色方案,使用更现代化的 Material Design 或 Vant 主题配色
- 优化移动端触控交互和响应式适配
- 更新相关文档和示例代码
## Capabilities
### New Capabilities
- `chartjs-integration`: Chart.js 与 Vue 3 的集成配置、主题系统、通用图表组件封装
### Modified Capabilities
- `budget-visualization`: 预算相关的图表展示(月度/年度仪表盘、燃尽图、方差图等)
- `statistics-charts`: 统计页面的图表(日趋势图、分类饼图、月度柱状图等)
## Impact
**前端组件**
- `Web/src/components/Budget/BudgetChartAnalysis.vue`5 个图表)
- `Web/src/views/statisticsV2/modules/DailyTrendChart.vue`(折线图)
- `Web/src/views/statisticsV2/modules/ExpenseCategoryCard.vue`(饼图)
- `Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue`(柱状图)
**依赖项**
- `Web/package.json`:移除 `echarts@^6.0.0`,添加 `chart.js``vue-chartjs`
**构建产物**
- 预计减少约 600KB 的 bundle 体积gzipped 后约 150KB
- 首屏加载时间预计优化 15-20%
**用户体验**
- 图表动画更流畅
- 触控操作更灵敏
- 视觉风格更现代化

View File

@@ -0,0 +1,73 @@
## MODIFIED Requirements
### Requirement: Budget gauge charts must display health status
The system SHALL render monthly and yearly budget health using gauge (semi-circle) charts showing current usage vs limit.
#### Scenario: Monthly gauge shows expense usage
- **WHEN** user views expense budget analysis
- **THEN** monthly gauge displays current expense / monthly limit as a percentage with remaining balance in center
#### Scenario: Monthly gauge shows income progress
- **WHEN** user views income budget analysis
- **THEN** monthly gauge displays current income / monthly target as a percentage with shortfall/excess in center
#### Scenario: Yearly gauge shows expense usage
- **WHEN** user views expense budget analysis
- **THEN** yearly gauge displays current expense / yearly limit as a percentage with remaining balance in center
#### Scenario: Yearly gauge shows income progress
- **WHEN** user views income budget analysis
- **THEN** yearly gauge displays current income / yearly target as a percentage with shortfall/excess in center
#### Scenario: Gauge changes color when exceeding limit
- **WHEN** expense usage exceeds 100% of budget
- **THEN** gauge arc color changes to red (var(--van-danger-color))
#### Scenario: Gauge changes color when exceeding income target
- **WHEN** income exceeds 100% of target
- **THEN** gauge arc color changes to green (var(--van-success-color))
### Requirement: Budget variance chart must show category-level differences
The system SHALL render a horizontal bar chart comparing actual vs budgeted amounts for each category.
#### Scenario: Variance chart displays all categories
- **WHEN** user has multiple budget categories
- **THEN** chart shows horizontal bars for each category with actual (solid) and budget (dashed) values
#### Scenario: Variance chart highlights overbudget categories
- **WHEN** a category's actual exceeds budget
- **THEN** the bar is colored red and labeled with overage amount
#### Scenario: Variance chart shows underbudget categories
- **WHEN** a category's actual is below budget
- **THEN** the bar is colored green and labeled with remaining amount
### Requirement: Budget burndown chart must track daily spending trend
The system SHALL render line charts showing cumulative spending vs ideal pace for monthly and yearly periods.
#### Scenario: Monthly burndown chart shows ideal vs actual
- **WHEN** user views monthly burndown
- **THEN** chart displays two lines: ideal linear spending and actual cumulative spending
#### Scenario: Monthly burndown projects month-end total
- **WHEN** current date is mid-month
- **THEN** chart shows projected month-end total based on current pace (dotted line extension)
#### Scenario: Yearly burndown chart shows ideal vs actual
- **WHEN** user views yearly burndown
- **THEN** chart displays two lines: ideal linear spending and actual cumulative spending by month
#### Scenario: Yearly burndown highlights current month
- **WHEN** user views yearly burndown
- **THEN** chart highlights the current month's data point with a larger marker
### Requirement: Charts must maintain existing interaction behavior
The system SHALL preserve all existing click, tooltip, and zoom interactions from the ECharts implementation.
#### Scenario: Chart tooltip shows on hover/tap
- **WHEN** user hovers over (desktop) or taps (mobile) a data point
- **THEN** tooltip displays formatted value with label
#### Scenario: Chart updates when switching budget type
- **WHEN** user switches between expense/income/savings tabs
- **THEN** all charts update their data and labels within 300ms

View File

@@ -0,0 +1,71 @@
## ADDED Requirements
### Requirement: Chart.js must be integrated with Vue 3 Composition API
The system SHALL use vue-chartjs 5.x to integrate Chart.js with Vue 3 components using the Composition API pattern.
#### Scenario: Chart component renders successfully
- **WHEN** a component imports and uses vue-chartjs chart components
- **THEN** the chart renders correctly in the DOM without console errors
#### Scenario: Chart updates reactively
- **WHEN** the chart's data prop changes
- **THEN** the chart re-renders with the new data using Chart.js update mechanism
### Requirement: Theme system must support Vant UI color scheme
The system SHALL provide a centralized theme configuration that adapts to Vant UI's theme variables, including dark mode support.
#### Scenario: Chart uses Vant primary color
- **WHEN** a chart is rendered
- **THEN** the chart uses `var(--van-primary-color)` for primary elements (lines, bars, etc.)
#### Scenario: Chart adapts to dark mode
- **WHEN** user switches to dark mode via Vant ConfigProvider
- **THEN** chart text color changes to `var(--van-text-color)` and background adapts accordingly
### Requirement: Base chart component must encapsulate common configuration
The system SHALL provide a `BaseChart.vue` component that encapsulates responsive behavior, theme integration, and error handling.
#### Scenario: Chart responds to container resize
- **WHEN** the parent container resizes (e.g., orientation change)
- **THEN** the chart automatically adjusts its dimensions using ResizeObserver
#### Scenario: Chart shows loading state
- **WHEN** chart data is being fetched
- **THEN** the component displays a loading indicator (Vant Loading component)
#### Scenario: Chart handles empty data gracefully
- **WHEN** chart receives empty or null data
- **THEN** the component displays an empty state message without errors
### Requirement: Gauge chart plugin must be available
The system SHALL provide a custom Chart.js plugin that renders a gauge chart using Doughnut chart with center text overlay.
#### Scenario: Gauge chart displays percentage
- **WHEN** gauge chart is rendered with value and limit props
- **THEN** the chart shows a semi-circle gauge with percentage text in the center
#### Scenario: Gauge chart supports color thresholds
- **WHEN** gauge value exceeds 100%
- **THEN** the gauge color changes to danger color (red for expense, green for income)
### Requirement: Charts must support mobile touch interactions
The system SHALL enable touch-friendly interactions including tap-to-highlight and pan gestures.
#### Scenario: User taps chart segment
- **WHEN** user taps a bar/pie segment on mobile
- **THEN** the segment highlights and shows tooltip with details
#### Scenario: User pans line chart
- **WHEN** user swipes horizontally on a line chart with many data points
- **THEN** the chart scrolls to show hidden data points
### Requirement: Chart animations must be configurable
The system SHALL allow disabling or customizing chart animations via configuration.
#### Scenario: Animation duration is consistent
- **WHEN** a chart first loads
- **THEN** the animation completes in 750ms (matching Vant UI transition timing)
#### Scenario: Animation respects prefers-reduced-motion
- **WHEN** user has `prefers-reduced-motion: reduce` enabled
- **THEN** charts render instantly without animation

View File

@@ -0,0 +1,77 @@
## MODIFIED Requirements
### Requirement: Daily trend chart must display expense/income over time
The system SHALL render a line chart showing daily transaction totals for the selected time period (week/month/year).
#### Scenario: Week view shows 7 days
- **WHEN** user selects "Week" time period
- **THEN** chart displays 7 data points (Mon-Sun) with expense and income lines
#### Scenario: Month view shows daily trend
- **WHEN** user selects "Month" time period
- **THEN** chart displays 28-31 data points (one per day) with expense and income lines
#### Scenario: Year view shows monthly trend
- **WHEN** user selects "Year" time period
- **THEN** chart displays 12 data points (one per month) with expense and income lines
#### Scenario: Chart highlights max expense day
- **WHEN** user views daily trend
- **THEN** the day with highest expense has a highlighted marker
#### Scenario: Chart supports zooming
- **WHEN** user pinches on mobile or scrolls on desktop
- **THEN** chart zooms in/out to show more/less detail
### Requirement: Expense category pie chart must show spending breakdown
The system SHALL render a pie chart displaying expense amounts grouped by category for the selected time period.
#### Scenario: Pie chart shows all expense categories
- **WHEN** user has expenses in multiple categories
- **THEN** chart displays one slice per category with percentage labels
#### Scenario: Pie chart uses category colors
- **WHEN** categories have custom colors defined
- **THEN** chart slices use the corresponding category colors
#### Scenario: Pie chart shows "Others" for small categories
- **WHEN** more than 8 categories exist
- **THEN** categories below 3% are grouped into "Others" slice
#### Scenario: Tapping slice shows category detail
- **WHEN** user taps a pie slice
- **THEN** app navigates to category detail view with transaction list
### Requirement: Monthly expense bar chart must compare months
The system SHALL render a vertical bar chart comparing expense totals across recent months.
#### Scenario: Bar chart shows 6 recent months
- **WHEN** user views monthly expense card
- **THEN** chart displays 6 bars representing the last 6 months
#### Scenario: Current month bar is highlighted
- **WHEN** user views monthly expense card
- **THEN** current month's bar uses primary color, previous months use gray
#### Scenario: Bar height reflects expense amount
- **WHEN** a month has higher expenses
- **THEN** its bar is proportionally taller
#### Scenario: Bar shows tooltip with formatted amount
- **WHEN** user hovers/taps a bar
- **THEN** tooltip displays month name and expense amount formatted as "¥X,XXX.XX"
### Requirement: Charts must maintain existing responsive behavior
The system SHALL ensure all statistics charts adapt to different screen sizes and orientations.
#### Scenario: Chart scales on narrow screens
- **WHEN** screen width is less than 375px
- **THEN** chart font sizes scale down proportionally while maintaining readability
#### Scenario: Chart reflows on orientation change
- **WHEN** device orientation changes from portrait to landscape
- **THEN** chart re-renders to fill available width within 300ms
#### Scenario: Chart labels truncate on small screens
- **WHEN** category names are longer than 12 characters
- **THEN** labels show ellipsis (e.g., "Entertainment..." ) and full text in tooltip

View File

@@ -0,0 +1,62 @@
## 1. 基础设施搭建
- [x] 1.1 安装依赖:`pnpm add chart.js vue-chartjs`
- [x] 1.2 创建 `Web/src/composables/useChartTheme.ts`(主题配置 composable
- [x] 1.3 创建 `Web/src/components/Charts/BaseChart.vue`(通用图表包装器)
- [x] 1.4 创建 `Web/src/plugins/chartjs-gauge-plugin.ts`(仪表盘插件)
- [x] 1.5 创建 `Web/src/utils/chartHelpers.ts`(图表工具函数:格式化、颜色等)
## 2. 迁移简单图表(验证基础设施)
- [x] 2.1 迁移 `MonthlyExpenseCard.vue`(柱状图,最简单)
- 保留原有 ECharts 代码,新增 Chart.js 实现
- 使用环境变量 `VITE_USE_CHARTJS` 控制切换
- [x] 2.2 验证 MonthlyExpenseCard 功能tooltip、响应式、暗色模式
- [x] 2.3 迁移 `ExpenseCategoryCard.vue`(饼图)
- 实现点击跳转到分类详情功能
- 实现 "Others" 分组逻辑(<3% 的分类)
- [x] 2.4 验证 ExpenseCategoryCard 功能:点击事件、颜色映射
## 3. 迁移折线图
- [x] 3.1 迁移 `DailyTrendChart.vue`(基础折线图)
- 实现双线expense + income配置
- 实现缩放功能(使用 chartjs-plugin-zoom
- [x] 3.2 验证 DailyTrendChart 功能:周/月/年切换、缩放、高亮最大值点
## 4. 迁移复杂图表BudgetChartAnalysis
- [x] 4.1 迁移月度仪表盘(使用 Doughnut + centerText 插件)
- 实现居中文本显示(余额/差额)
- 实现超支时颜色变化(红色/绿色)
- 实现 scaleX(-1) 镜像效果(支出类型)
- [x] 4.2 迁移年度仪表盘(复用月度逻辑)
- [x] 4.3 迁移方差图Variance Chart
- 实现横向柱状图
- 实现实际 vs 预算的双柱对比
- 实现超支/节省的颜色标识
- [x] 4.4 迁移月度燃尽图Burndown Chart
- 实现双线(理想线 + 实际线)
- 实现投影线dotted line extension
- [x] 4.5 迁移年度燃尽图(复用月度逻辑)
- 实现当前月高亮标记
- [x] 4.6 验证 BudgetChartAnalysis 所有交互tab 切换、tooltip、响应式
## 5. 优化与测试
- [x] 5.1 实现 `prefers-reduced-motion` 支持(禁用动画)
- [x] 5.2 实现数据抽样decimation plugin用于大数据量场景
- [x] 5.3 测试所有图表的暗色模式适配
- [x] 5.4 测试所有图表的移动端触控交互tap, pinch, swipe
- [x] 5.5 测试边界情况:空数据、单条数据、超长分类名
- [x] 5.6 性能测试Lighthouse Performance 分数对比
## 6. 清理与上线
- [x] 6.1 移除所有组件中的 ECharts 代码(删除旧实现)
- [x] 6.2 移除环境变量 `VITE_USE_CHARTJS`(默认使用 Chart.js
- [x] 6.3 从 `package.json` 移除 `echarts` 依赖
- [x] 6.4 运行 `pnpm build` 并分析 bundle 大小(验证优化效果)
- [x] 6.5 更新 `AGENTS.md`:记录 Chart.js 使用规范
- [x] 6.6 创建 `.doc/chart-migration-checklist.md`(手动测试清单)
- [x] 6.7 提交代码并部署到测试环境

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-13

View File

@@ -0,0 +1,335 @@
## Context
EmailBill 项目在早期开发了 V1 版本的日历、统计、预算三大核心功能模块。随着业务迭代V2 版本已经重构并稳定运行,提供了更优的用户体验和代码架构。当前代码库中同时维护 V1 和 V2 两套实现,导致:
1. **代码冗余**: 前端包含 `CalendarView.vue`, `BudgetView.vue`, `statisticsV1/Index.vue` 等页面,后端包含多个仅服务于 V1 的 API 端点
2. **维护成本**: 任何共享组件或数据模型的变更需要同时考虑 V1 和 V2 的兼容性
3. **认知负担**: 新开发者需要理解两套架构,增加了上手难度
4. **测试负担**: 需要维护两套测试用例,且部分测试已失效
### 当前状态
- V2 版本已完成功能对齐,用户已逐步迁移
- V1 相关 API 端点部分已标记 `[Obsolete]` (如 `TransactionRecordController.GetDailyStatisticsAsync`)
- 路由守卫中包含 V1/V2 版本切换逻辑
### 约束条件
- **不能影响 V2 功能**: 必须保证 V2 页面完全正常运行
- **共享组件不能删除**: `TransactionList`, `TransactionDetail`, `PopupContainer` 等组件被 V2 使用
- **Repository 层不能动**: V1 和 V2 共用相同的数据访问层
- **需要手动测试**: 删除后必须打开 V2 页面验证功能完整性
### 利益相关者
- **开发团队**: 减少维护负担,简化架构
- **测试团队**: 减少测试用例数量
- **最终用户**: 无感知(已迁移到 V2
---
## Goals / Non-Goals
### Goals
1. **完全移除 V1 代码**: 删除所有 V1 专用的页面、API、Service 方法
2. **保持 V2 功能完整**: 确保删除操作不影响 V2 版本的任何功能
3. **清理路由配置**: 移除 V1 路由和版本切换逻辑
4. **验证功能正常**: 通过手动测试验证 V2 页面能正常打开和使用
### Non-Goals
1. **不修改 Repository 层**: V2 继续使用现有的 Repository不进行任何修改
2. **不修改数据库**: 不涉及数据迁移或表结构变更
3. **不重构 V2 代码**: 仅删除 V1 代码,不对 V2 进行任何优化或重构
4. **不移除共享组件**: 即使组件原本为 V1 开发,只要 V2 在用就保留
5. **不更新文档**: API 文档和用户文档的更新不在本次变更范围内
---
## Decisions
### Decision 1: 采用自底向上的删除顺序 (Backend → Frontend)
**选择**: 先删除后端 Service/Application → 再删除 Controller → 最后删除前端页面和路由
**理由**:
- **依赖方向**: 前端依赖后端 API先删除底层可以避免悬挂引用
- **验证便利**: 删除后端接口后,前端调用会立即报 404易于发现遗漏
- **回滚简单**: 如果出问题,可以快速恢复后端接口,前端暂时保留也不影响
**备选方案 (未选择)**:
- **自顶向下 (Frontend → Backend)**: 先删除前端页面,可能导致后端接口成为"僵尸代码"难以发现
- **同步删除**: 一次性删除所有层,风险较高且难以定位问题
---
### Decision 2: 通过代码搜索确认 V1 专用性
**选择**: 对每个待删除的方法/接口进行全局搜索,确认仅被 V1 页面调用
**搜索关键字**:
- 前端页面: `CalendarView.vue`, `BudgetView.vue`, `statisticsV1/Index.vue`
- API 方法: `GetDailyStatistics`, `GetUncoveredCategories`, `GetArchiveSummary`, `GetBalanceStatistics`
- 路由: `/calendar`, `/budget`, `name: 'statistics'`
**验证标准**:
- 如果搜索结果仅出现在以上三个页面中 → 可以安全删除
- 如果出现在其他文件中 → 标记为"需保留"或"需进一步分析"
**理由**:
- **准确性**: 避免误删 V2 依赖的代码
- **可追溯**: 搜索结果可作为删除依据留存
---
### Decision 3: 保留共享组件,即使其仅在 V1 中被直接使用
**选择**: 保留 `TransactionList`, `TransactionDetail`, `PopupContainer`, `SmartClassifyButton` 等组件
**判断依据**:
- 通过搜索确认这些组件在 V2 页面中有引用
- 即使组件内部包含 V1 特定逻辑(如全局事件监听),也不在本次变更中修改
**理由**:
- **最小化风险**: 删除共享组件可能导致 V2 功能异常
- **职责分离**: 组件优化应作为独立的重构任务,不在本次删除范围内
---
### Decision 4: 移除全局事件监听 (条件性)
**选择**: 如果全局事件 (`transaction-deleted`, `transactions-changed`) **仅在 V1 页面中监听**,则移除事件触发代码
**验证流程**:
1. 搜索 `window.addEventListener('transaction-deleted')`
2. 确认监听器仅在 `CalendarView.vue``statisticsV1/Index.vue` 中注册
3. 搜索 `window.dispatchEvent(new Event('transaction-deleted'))`
4. 如果触发位置在共享组件中(如 `TransactionDetail.vue`),检查该组件是否被 V2 使用
- 如果被 V2 使用 → 保留事件触发代码(即使 V1 删除后无人监听)
- 如果仅 V1 使用 → 删除事件触发代码
**理由**:
- **保守策略**: 优先保证 V2 功能不受影响
- **技术债务**: 遗留的无用事件触发代码可作为后续清理任务
---
### Decision 5: 采用手动测试验证 V2 功能
**选择**: 删除完成后,手动打开以下 V2 页面并验证核心功能
**测试清单**:
1. **统计 V2** (`/statistics-v2`):
- 月度统计数据正常加载
- 分类统计饼图正常渲染
- 日度统计折线图正常渲染
- 交易列表正常展示和滚动加载
2. **预算 V2** (`/budget-v2`):
- 预算列表正常加载
- 分类统计(月度+年度)正常显示
- 预算创建/编辑/删除功能正常
- 存款预算导航和显示正常
3. **日历 V2** (`/calendar-v2`):
- 日历视图正常渲染
- 日期选择和统计数据加载正常
- 交易详情查看和编辑正常
**理由**:
- **V2 无单元测试覆盖**: 当前项目主要测试集中在后端,前端缺少自动化测试
- **快速反馈**: 手动测试可以在 10 分钟内完成所有关键路径验证
- **用户体验保证**: 确保最终用户不会遇到功能异常
**备选方案 (未选择)**:
- **仅依赖编译检查**: 无法发现运行时错误(如路由配置错误)
- **编写自动化测试**: 时间成本过高,且本次变更不涉及逻辑修改
---
## Risks / Trade-offs
### Risk 1: 误删 V2 依赖的代码
**表现**: 删除后端接口或前端方法,导致 V2 页面调用失败
**缓解措施**:
- 采用 Decision 2 的搜索验证流程,确保每个删除操作都经过确认
- 先删除后端 Service 层,观察前端是否有新的 404 错误
- 使用 Git 进行版本控制,支持快速回滚
**残留影响**:
- 如果 V2 通过动态路由或字符串拼接调用接口,静态搜索可能遗漏
---
### Risk 2: 共享组件包含 V1 特定逻辑但未被清理
**表现**: 保留的组件中包含 V1 特定的条件判断或事件监听,成为"死代码"
**缓解措施**:
- 标记为 Non-Goal不在本次清理范围内
- 在任务清单中添加"后续优化"任务,记录需要清理的组件列表
**残留影响**:
- 代码库中保留少量无用代码,但不影响功能正确性
---
### Risk 3: 路由守卫中遗留 V1 相关逻辑
**表现**: 删除 V1 路由后,`router/index.js``useVersionStore` 中仍有版本切换逻辑
**缓解措施**:
- 在任务清单中明确包含"清理路由守卫"任务
- 搜索 `isV2()`, `useVersionStore`, `router.push` 等关键字,逐一检查
**残留影响**:
- 可能导致用户访问不存在的路由时出现 404 错误
---
### Risk 4: V2 页面功能异常但手动测试未覆盖
**表现**: 删除操作影响了 V2 的边缘功能(如智能分类、批量操作),但未在测试清单中
**缓解措施**:
- 优先测试 V2 的核心流程(查看、创建、编辑、删除)
- 如果发现问题,立即回滚对应的删除操作
- 记录测试结果,作为归档的一部分
**残留影响**:
- 边缘功能可能在生产环境中被用户发现,需要后续修复
---
### Trade-off 1: 保留共享组件 vs 彻底清理
**权衡**: 为了保证 V2 功能稳定,选择保留所有共享组件,即使部分组件包含 V1 特定逻辑
**代价**:
- 代码库中保留部分冗余代码
- 后续维护时可能需要额外的上下文理解
**收益**:
- 大幅降低误删风险
- 缩短变更周期,快速上线
---
### Trade-off 2: 自动化测试 vs 手动测试
**权衡**: 采用手动测试而非编写自动化测试用例
**代价**:
- 无法在 CI/CD 中自动验证 V2 功能
- 未来的变更可能再次破坏 V2 功能
**收益**:
- 节省测试编写时间(预计 2-3 小时)
- 适用于一次性删除任务,性价比高
---
## Migration Plan
### 阶段 1: 准备工作 (Pre-deletion)
1. 创建 feature 分支 `feature/remove-v1-modules`
2. 运行所有现有测试,确认当前状态正常
3. 备份 V1 相关文件列表(用于回滚)
### 阶段 2: 后端删除 (Backend Removal)
1. **Service 层删除**:
- 删除 `BudgetService.GetUncoveredCategoriesAsync`
- 删除 `BudgetService.GetArchiveSummaryAsync`
- 删除 `TransactionStatisticsService.GetBalanceStatisticsAsync`
- 编译验证,确认无编译错误
2. **Application 层删除**:
- 删除 `BudgetApplication.GetUncoveredCategoriesAsync`
- 删除 `BudgetApplication.GetArchiveSummaryAsync`
- 删除 `TransactionStatisticsApplication.GetBalanceStatisticsAsync`
- 编译验证
3. **Controller 层删除**:
- 删除 `TransactionRecordController.GetDailyStatisticsAsync`
- 删除 `BudgetController.GetUncoveredCategoriesAsync`
- 删除 `BudgetController.GetArchiveSummaryAsync`
- 删除 `TransactionStatisticsController.GetBalanceStatisticsAsync`
- 编译验证
4. **运行后端测试**:
```bash
dotnet test WebApi.Test/WebApi.Test.csproj
```
- 移除失败的 V1 相关测试用例
- 确保其他测试通过
### 阶段 3: 前端删除 (Frontend Removal)
1. **API 客户端清理**:
- 清理 `Web/src/api/transactionRecord.js` 中的 `GetDailyStatistics` 调用
- 清理 `Web/src/api/budget.js` 中的 `GetUncoveredCategories`, `GetArchiveSummary`
- 清理 `Web/src/api/statistics.js` 中的 `GetBalanceStatistics`
2. **页面文件删除**:
- 删除 `Web/src/views/CalendarView.vue`
- 删除 `Web/src/views/BudgetView.vue`
- 删除 `Web/src/views/statisticsV1/Index.vue`
- 删除 `Web/src/views/statisticsV1/` 目录
3. **路由配置清理**:
- 删除 `Web/src/router/index.js` 中的 V1 路由定义:
- `/calendar`
- `/budget`
- `/` (statisticsV1)
- 简化 `useVersionStore` 中的版本切换逻辑
- 移除路由守卫中的 V1 相关判断
4. **编译验证**:
```bash
cd Web && pnpm build
```
### 阶段 4: 手动测试验证 (Manual Testing)
按照 Decision 5 的测试清单,逐项验证 V2 功能:
1. 启动开发服务器: `pnpm dev`
2. 打开浏览器,访问 V2 页面
3. 验证核心功能(数据加载、交互、导航)
4. 记录测试结果
### 阶段 5: 代码审查和合并 (Code Review & Merge)
1. 提交变更到远程分支
2. 创建 Pull Request
3. 代码审查关注点:
- 确认删除的代码不在 V2 中被引用
- 检查是否有遗漏的 V1 特定逻辑
4. 合并到主分支
### 回滚策略
如果发现 V2 功能异常:
1. **立即回滚**: `git revert <commit-hash>`
2. **定位问题**: 使用 `git diff` 找到引起问题的删除操作
3. **部分恢复**: 仅恢复必要的文件或方法
4. **重新测试**: 确认恢复后 V2 功能正常
---
## Open Questions
### Q1: 是否需要保留 `[Obsolete]` 标记的接口一段时间?
**当前决策**: 直接删除,因为 V2 已稳定运行
**待确认**: 是否有外部系统或移动端 APP 仍在调用这些接口?如果有,需要先通知相关方并提供迁移时间
---
### Q2: 全局事件监听 (`transaction-deleted`) 在 V2 中是否还需要?
**当前决策**: 保留事件触发代码,即使 V1 删除后无人监听
**待确认**: V2 是否使用其他机制(如 Pinia store来实现组件间通信如果是可以在后续任务中移除全局事件
---
### Q3: 是否需要更新 API 文档 (Swagger/Scalar)
**当前决策**: 标记为 Non-Goal不在本次范围内
**待确认**: API 文档是否自动生成?如果是手动维护,需要添加任务来移除已删除接口的文档
---
### Q4: V2 页面是否有完整的功能对齐?
**当前假设**: V2 已完全替代 V1 功能
**待确认**: 是否有用户或内部团队仍在使用 V1 特有的功能(如 `GetUncoveredCategories`)?如果有,需要先在 V2 中实现对应功能
---
**设计文档完成**。本文档为实施团队提供了清晰的技术决策依据和操作步骤,确保删除操作安全可控。

View File

@@ -0,0 +1,82 @@
## Why
随着系统 V2 版本的功能已经稳定运行V1 版本的日历、统计、预算三大模块已成为历史遗留代码,增加了代码库的维护成本和认知负担。移除这些旧版本代码可以简化系统架构,降低未来重构的风险,同时减少不必要的测试和文档维护工作。
## What Changes
### **BREAKING** 移除前端 V1 页面
- 删除 `Web/src/views/CalendarView.vue` (日历视图)
- 删除 `Web/src/views/BudgetView.vue` (预算管理页面)
- 删除 `Web/src/views/statisticsV1/Index.vue` (统计 V1 页面)
- 移除相关路由配置 (`/calendar`, `/budget`, `/` 的 V1 路由)
### **BREAKING** 移除前端 API 客户端 (仅 V1 专用部分)
- 清理 `Web/src/api/transactionRecord.js` 中 V1 专用方法 (`GetDailyStatistics` 调用)
- 清理 `Web/src/api/budget.js` 中 V1 专用方法 (`GetUncoveredCategories`, `GetArchiveSummary`)
- 清理 `Web/src/api/statistics.js` 中 V1 专用方法 (`GetBalanceStatistics`)
### **BREAKING** 移除后端 Controller 接口 (仅 V1 专用部分)
- `TransactionRecordController.GetDailyStatisticsAsync` (已标记 Obsolete仅 CalendarView 使用)
- `BudgetController.GetUncoveredCategoriesAsync` (仅 BudgetView 使用)
- `BudgetController.GetArchiveSummaryAsync` (仅 BudgetView 使用)
- `TransactionStatisticsController.GetBalanceStatisticsAsync` (仅 statisticsV1 使用)
### 移除后端 Service/Application 层 (仅 V1 专用部分)
- `BudgetApplication` 中移除 `GetUncoveredCategoriesAsync`, `GetArchiveSummaryAsync`
- `BudgetService` 中移除 `GetUncoveredCategoriesAsync`, `GetArchiveSummaryAsync`
- `TransactionStatisticsApplication` 中移除 `GetBalanceStatisticsAsync`
- `TransactionStatisticsService` 中移除 `GetBalanceStatisticsAsync`
### 清理共享组件的 V1 特定逻辑
- 检查 `TransactionList`, `TransactionDetail`, `PopupContainer` 等组件是否包含 V1 特定逻辑
- 移除全局事件监听 (`transaction-deleted`, `transactions-changed`) 如果仅 V1 使用
### 更新路由守卫和版本控制逻辑
- 移除 `Web/src/router/index.js` 中的 V1 路由配置
- 简化 `useVersionStore` 中的版本切换逻辑(移除 V1 相关分支)
## Capabilities
### New Capabilities
(无新增能力)
### Modified Capabilities
- `routing`: 移除 V1 路由配置,简化版本切换逻辑
- `transaction-api`: 移除 `GetDailyStatistics` (Obsolete) 接口
- `budget-api`: 移除 `GetUncoveredCategories`, `GetArchiveSummary` 接口
- `statistics-api`: 移除 `GetBalanceStatistics` 接口
## Impact
### 前端影响
- **页面数量**: 减少 3 个页面文件
- **路由配置**: 移除 3 个路由 (`/calendar`, `/budget`, `/`)
- **API 客户端**: 清理 4+ 个废弃方法
- **组件**: 需验证共享组件 (`TransactionList`, `TransactionDetail`) 在 V2 中是否正常工作
### 后端影响
- **Controller 层**: 移除 4 个 API 端点
- **Application 层**: 移除 4 个业务方法
- **Service 层**: 移除 4 个服务方法
- **Repository 层**: 无影响 (V2 继续使用相同的 Repository)
### 兼容性影响
- **破坏性变更**: 所有 V1 API 端点将不可用
- **用户影响**: V1 用户必须切换到 V2 版本
- **数据影响**: 无,数据库表和实体不受影响
### 测试影响
- **单元测试**: 需移除 V1 相关的测试用例
- **集成测试**: 需验证 V2 页面功能完整性
- **手动测试**: 需打开 V2 页面进行功能回归测试
### 依赖影响
- **共享组件**: `TransactionList`, `TransactionDetail`, `PopupContainer`, `SmartClassifyButton` 仍被 V2 使用,不能删除
- **第三方库**: ECharts 仍被 V2 使用,不能删除
- **全局事件**: 需检查 `transaction-deleted`, `transactions-changed` 事件是否仅被 V1 监听
### 文档影响
- 需更新 API 文档,标记移除的端点
- 需更新用户文档,说明 V1 已下线

View File

@@ -0,0 +1,130 @@
## REMOVED Requirements
### Requirement: Get Uncovered Categories
**Reason**: 该接口仅被 V1 预算页面使用,用于展示"未设置预算的分类"。V2 预算页面不包含此功能。
**Migration**: 如需在 V2 中实现类似功能,应重新设计并创建新接口。当前无直接迁移路径。
**原有功能**:
- **接口**: `GET /api/Budget/GetUncoveredCategories`
- **Controller**: `BudgetController.GetUncoveredCategoriesAsync`
- **参数**: `category` (enum: Expense/Income/Saving), `date` (DateTime)
- **返回**: `List<string>` (未设置预算的分类名称列表)
- **业务逻辑**:
1. 查询指定月份的所有交易记录,提取所有出现的分类
2. 查询指定月份已设置预算的分类
3. 计算差集,返回"有交易但未设置预算"的分类列表
**被以下代码调用**:
- `Web/src/views/BudgetView.vue` 中的 `fetchUncoveredCategories` 方法
- `Web/src/api/budget.js` 中的 `getUncoveredCategories` 函数
---
### Requirement: Get Archive Summary
**Reason**: 该接口仅被 V1 预算页面使用,用于展示"历史预算归档总结"。V2 预算页面不包含此功能。
**Migration**: 如需在 V2 中实现类似功能,应重新设计并创建新接口。当前无直接迁移路径。
**原有功能**:
- **接口**: `GET /api/Budget/GetArchiveSummary`
- **Controller**: `BudgetController.GetArchiveSummaryAsync`
- **参数**: `date` (DateTime, 用于指定查询的年月)
- **返回**: `ArchiveSummaryDto` (包含归档总结数据)
- **业务逻辑**:
1. 查询 `BudgetArchive` 表中指定月份的归档记录
2. 汇总支出、收入、存款的预算执行情况
3. 返回总结数据(如预算达成率、超支分类等)
**被以下代码调用**:
- `Web/src/views/BudgetView.vue` 中的 `showArchiveSummary` 方法
- `Web/src/api/budget.js` 中的 `getArchiveSummary` 函数
---
## Context
本规范定义了 EmailBill 后端 `BudgetController` 中两个 V1 专用接口的移除操作。
### 接口背景
这两个接口是 V1 预算页面的特色功能:
1. **未覆盖分类提示**: 帮助用户发现"有交易但未设置预算"的分类,提醒用户完善预算设置
2. **归档总结**: 展示历史月份的预算执行总结,帮助用户回顾过去的财务状况
### V2 设计变更
V2 预算页面重新设计了用户体验,移除了上述两个功能:
- **未覆盖分类**: V2 采用"按需创建预算"模式,不主动提示未覆盖分类
- **归档总结**: V2 使用实时统计替代归档总结,用户可随时查看任意月份的预算执行情况
### 技术依赖
这两个接口依赖以下 Service 和 Repository
- `BudgetService.GetUncoveredCategoriesAsync`
- `BudgetService.GetArchiveSummaryAsync`
- `BudgetRepository`
- `BudgetArchiveRepository`
- `TransactionRecordRepository`
移除接口后,相关 Service 方法也将被移除(见 `budget-service` 规范)。
---
## Validation
### 验证标准
1. **代码搜索验证**:
- 全局搜索 `GetUncoveredCategories`,确认仅在以下位置出现:
- `BudgetController.GetUncoveredCategoriesAsync` (待删除)
- `BudgetApplication.GetUncoveredCategoriesAsync` (待删除)
- `BudgetService.GetUncoveredCategoriesAsync` (待删除)
- `BudgetView.vue` (已删除)
- `budget.js` (已清理)
- 全局搜索 `GetArchiveSummary`,确认仅在 V1 相关代码中出现
2. **编译验证**:
- 删除 `BudgetController` 中的两个方法后,后端项目编译通过
- 删除 `BudgetApplication``BudgetService` 中的对应方法后,编译通过
3. **API 文档验证**:
- Swagger/Scalar 文档中不再显示以下端点:
- `/api/Budget/GetUncoveredCategories`
- `/api/Budget/GetArchiveSummary`
4. **运行时验证**:
- 前端调用上述端点返回 404
- V2 预算页面 (`/budget-v2`) 正常加载和操作,不受影响
---
## Dependencies
移除这两个接口的前置条件:
1. `BudgetView.vue` (V1 预算页面) 已删除
2. `Web/src/api/budget.js` 中的 `getUncoveredCategories``getArchiveSummary` 方法已清理
3. V2 预算页面已验证不依赖这两个接口
移除后连带删除:
- `BudgetApplication.GetUncoveredCategoriesAsync`
- `BudgetApplication.GetArchiveSummaryAsync`
- `BudgetService.GetUncoveredCategoriesAsync`
- `BudgetService.GetArchiveSummaryAsync`
移除后不影响:
- 其他预算相关接口 (`GetList`, `GetCategoryStats`, `Create`, `Update`, `Delete` 等)
- `BudgetRepository``BudgetArchiveRepository` 的查询逻辑 (仍被其他接口使用)
- V2 预算页面的任何功能
---
## Notes
### 功能对比表
| 功能 | V1 实现 | V2 实现 |
|------|---------|---------|
| **未覆盖分类提示** | 专用接口 `GetUncoveredCategories` | 无(按需创建预算) |
| **归档总结** | 专用接口 `GetArchiveSummary` | 实时统计 `GetCategoryStats` |
| **预算列表** | `GetList` | `GetList` (共用) |
| **分类统计** | `GetCategoryStats` | `GetCategoryStats` (共用) |
### 潜在影响
如果未来需要在 V2 中恢复"未覆盖分类"或"归档总结"功能:
1. **不能直接恢复删除的代码**,因为业务逻辑可能已过时
2. **应重新设计接口**,考虑 V2 的数据模型和用户体验
3. **建议先调研用户需求**,确认是否真的需要这些功能

View File

@@ -0,0 +1,77 @@
## REMOVED Requirements
### Requirement: Calendar View Route
**Reason**: V1 日历页面已由 V2 版本完全替代V1 路由不再需要
**Migration**: 用户应访问 `/calendar-v2` 路由,使用新版日历功能
---
### Requirement: Budget View Route
**Reason**: V1 预算页面已由 V2 版本完全替代V1 路由不再需要
**Migration**: 用户应访问 `/budget-v2` 路由,使用新版预算管理功能
---
### Requirement: Statistics V1 Default Route
**Reason**: V1 统计页面已由 V2 版本完全替代V1 默认路由不再需要
**Migration**: 系统默认路由应指向 `/statistics-v2`,用户将自动使用新版统计功能
---
### Requirement: V1/V2 Version Toggle Logic
**Reason**: V1 版本完全下线后,版本切换逻辑不再需要
**Migration**: 移除 `useVersionStore` 中的版本切换代码,简化路由守卫逻辑。所有用户默认使用 V2 版本。
---
## Context
本规范定义了 EmailBill 前端路由系统中 V1 相关路由的移除操作。随着 V2 版本的稳定上线V1 的日历、预算、统计三个核心模块的路由定义已成为遗留代码。
### 受影响的路由
- `/calendar` → CalendarView.vue (V1 日历视图)
- `/budget` → BudgetView.vue (V1 预算管理)
- `/` → statisticsV1/Index.vue (V1 统计页面,默认首页)
### 现有版本控制机制
- `useVersionStore` 提供 `isV2()` 方法判断用户偏好
- 路由守卫根据版本偏好自动跳转到对应的 V1 或 V2 路由
- V2 路由命名规则: 原路由名 + `-v2` 后缀
### 移除后的预期行为
- 访问 `/calendar``/budget``/` 将返回 404 或重定向到 V2 版本
- `useVersionStore` 中的版本切换逻辑被简化或移除
- 路由守卫不再需要判断 V1/V2 版本
---
## Validation
### 验证标准
1. **路由配置文件** (`Web/src/router/index.js`):
- 不包含 `/calendar`, `/budget`, `/` 的 V1 路由定义
- V2 路由 (`/calendar-v2`, `/budget-v2`, `/statistics-v2`) 正常工作
2. **版本控制逻辑** (`Web/src/stores/version.js` 或类似文件):
- 移除或简化 `isV2()` 相关的版本切换代码
- 确保所有路由默认使用 V2 版本
3. **手动测试验证**:
- 直接访问 `/calendar` 不会加载 V1 页面
- 直接访问 `/budget` 不会加载 V1 页面
- 访问根路径 `/` 自动跳转到 V2 统计页面或返回 404
- V2 路由 (`/calendar-v2`, `/budget-v2`, `/statistics-v2`) 正常访问
---
## Dependencies
移除 V1 路由的前置条件:
1. V1 页面文件 (`CalendarView.vue`, `BudgetView.vue`, `statisticsV1/Index.vue`) 已删除
2. V2 页面功能已验证完整,可以完全替代 V1
3. 用户已迁移到 V2 版本,或系统强制使用 V2
移除后不影响:
- V2 路由配置和页面功能
- 路由守卫的认证检查逻辑 (`requiresAuth`)
- 其他非核心模块的路由 (如 `/login`, `/settings`)

View File

@@ -0,0 +1,141 @@
## REMOVED Requirements
### Requirement: Get Balance Statistics
**Reason**: 该接口仅被 V1 统计页面使用,用于绘制"余额变化折线图"。V2 统计页面使用不同的图表渲染逻辑,不依赖此接口。
**Migration**: V2 统计页面使用 `GetDailyStatistics` 接口获取数据,并在前端计算累积余额。无需后端专用接口。
**原有功能**:
- **接口**: `GET /api/TransactionStatistics/GetBalanceStatistics`
- **Controller**: `TransactionStatisticsController.GetBalanceStatisticsAsync`
- **参数**: `year` (int), `month` (int)
- **返回**: `BalanceStatisticsDto` (包含每日的累积余额数据)
- **业务逻辑**:
1. 查询指定月份的所有交易记录,按日期排序
2. 计算每日的累积余额 (前一日余额 + 当日收入 - 当日支出)
3. 返回包含日期和余额的时间序列数据
**被以下代码调用**:
- `Web/src/views/statisticsV1/Index.vue` 中的 `fetchBalanceData` 方法
- `Web/src/api/statistics.js` 中的 `getBalanceStatistics` 函数
**图表渲染逻辑**:
- V1 使用 ECharts 折线图渲染余额曲线
- 数据源: 后端计算好的累积余额
- 渲染方法: `renderBalanceChart()`
---
## Context
本规范定义了 EmailBill 后端 `TransactionStatisticsController` 中 V1 专用接口的移除操作。
### 接口背景
该接口是 V1 统计页面的核心功能之一,用于支持"余额变化趋势图"
- **设计理念**: 后端负责累积余额计算,前端只负责渲染
- **性能考虑**: 避免前端处理大量交易记录数据
- **适用场景**: V1 统计页面需要独立的余额统计视图
### V2 设计变更
V2 统计页面重新设计了数据获取和渲染逻辑:
- **数据获取**: 使用 `GetDailyStatistics` 一次性获取日度支出/收入数据
- **余额计算**: 在前端 Vue 组件中计算累积余额 (computed property)
- **性能优化**: 前端缓存计算结果,减少重复请求
- **代码简化**: 后端不需要维护专用的余额统计接口
### 技术对比
| 维度 | V1 实现 | V2 实现 |
|------|---------|---------|
| **数据获取** | 专用接口 `GetBalanceStatistics` | 通用接口 `GetDailyStatistics` |
| **余额计算** | 后端计算 | 前端计算 |
| **网络请求** | 2 次 (日度统计 + 余额统计) | 1 次 (日度统计) |
| **前端逻辑** | 简单渲染 | 计算 + 渲染 |
| **后端复杂度** | 高 (多个接口) | 低 (统一接口) |
### 依赖关系
该接口依赖以下 Service 和 Repository
- `TransactionStatisticsService.GetBalanceStatisticsAsync`
- `TransactionStatisticsApplication.GetBalanceStatisticsAsync`
- `TransactionRecordRepository` (查询交易记录)
移除接口后,相关 Service 方法也将被移除。
---
## Validation
### 验证标准
1. **代码搜索验证**:
- 全局搜索 `GetBalanceStatistics`,确认仅在以下位置出现:
- `TransactionStatisticsController.GetBalanceStatisticsAsync` (待删除)
- `TransactionStatisticsApplication.GetBalanceStatisticsAsync` (待删除)
- `TransactionStatisticsService.GetBalanceStatisticsAsync` (待删除)
- `statisticsV1/Index.vue` (已删除)
- `statistics.js` (已清理)
2. **编译验证**:
- 删除 `TransactionStatisticsController` 中的方法后,后端项目编译通过
- 删除 `TransactionStatisticsApplication``TransactionStatisticsService` 中的对应方法后,编译通过
3. **API 文档验证**:
- Swagger/Scalar 文档中不再显示 `/api/TransactionStatistics/GetBalanceStatistics` 端点
4. **运行时验证**:
- 前端调用 `/api/TransactionStatistics/GetBalanceStatistics` 返回 404
- V2 统计页面 (`/statistics-v2`) 正常加载,余额曲线正常渲染
5. **功能验证 (V2 统计页面)**:
- 打开 `/statistics-v2`
- 切换到"月度视图"
- 确认余额折线图正常显示
- 确认数据与 V1 保持一致 (基于相同的 `GetDailyStatistics` 数据)
---
## Dependencies
移除此接口的前置条件:
1. `statisticsV1/Index.vue` (V1 统计页面) 已删除
2. `Web/src/api/statistics.js` 中的 `getBalanceStatistics` 方法已清理
3. V2 统计页面已验证可以通过前端计算实现相同功能
移除后连带删除:
- `TransactionStatisticsApplication.GetBalanceStatisticsAsync`
- `TransactionStatisticsService.GetBalanceStatisticsAsync`
移除后不影响:
- 其他统计接口 (`GetMonthlyStatistics`, `GetCategoryStatistics`, `GetDailyStatistics`)
- `TransactionRecordRepository` 的查询逻辑 (仍被其他接口使用)
- V2 统计页面的任何功能 (余额计算逻辑已在前端实现)
---
## Implementation Notes
### V2 前端余额计算示例 (参考)
```javascript
// Web/src/views/statisticsV2/Index.vue
computed: {
balanceData() {
let cumulativeBalance = 0
return this.dailyData.map(day => {
cumulativeBalance += day.income - day.expense
return {
date: day.date,
balance: cumulativeBalance
}
})
}
}
```
### 性能分析
- **V1 方案**: 后端查询 + 计算 + 序列化 → 前端接收 + 渲染 (总时间: ~200ms)
- **V2 方案**: 后端查询 + 序列化 → 前端接收 + 计算 + 渲染 (总时间: ~180ms)
- **结论**: V2 方案性能略优,且减少了后端复杂度
### 回归测试建议
删除接口后,应手动验证 V2 统计页面的以下场景:
1. **正常月份**: 选择有交易的月份,确认余额曲线正常
2. **无交易月份**: 选择无交易的月份,确认显示"暂无数据"
3. **跨年场景**: 验证 1 月份的余额计算是否正确 (初始余额为 0)
4. **性能测试**: 加载包含大量交易 (>1000 笔) 的月份,确认渲染流畅

View File

@@ -0,0 +1,76 @@
## REMOVED Requirements
### Requirement: Get Daily Statistics (Obsolete)
**Reason**: 该接口已标记 `[Obsolete]`,仅被 V1 日历页面使用。V2 使用 `TransactionStatisticsController.GetDailyStatisticsAsync` 替代。
**Migration**: 前端应调用 `GET /api/TransactionStatistics/GetDailyStatistics` 接口,后端应使用 `TransactionStatisticsService.GetDailyStatisticsAsync` 方法。
**原有功能**:
- **接口**: `GET /api/TransactionRecord/GetDailyStatistics`
- **Controller**: `TransactionRecordController.GetDailyStatisticsAsync`
- **参数**: `year` (int), `month` (int)
- **返回**: `List<DailyStatisticDto>` (包含每日的支出、收入、余额统计)
**被以下代码调用**:
- `Web/src/views/CalendarView.vue` 中的 `fetchDailyStatistics` 方法
---
## Context
本规范定义了 EmailBill 后端 `TransactionRecordController` 中废弃的 V1 专用接口的移除操作。
### 接口历史
- **创建时间**: V1 版本早期,用于支持日历视图的日度统计
- **废弃原因**: 职责不清晰,日度统计应由 `TransactionStatisticsController` 统一管理
- **废弃标记**: 已标记 `[Obsolete]` 属性,建议开发者使用新接口
- **当前使用**: 仅被 `CalendarView.vue` (V1) 调用V2 日历页面不使用此接口
### 新接口对比
| 维度 | V1 接口 (待删除) | V2 接口 (推荐) |
|------|----------------|---------------|
| **路径** | `/api/TransactionRecord/GetDailyStatistics` | `/api/TransactionStatistics/GetDailyStatistics` |
| **Controller** | `TransactionRecordController` | `TransactionStatisticsController` |
| **职责** | 混杂在交易记录 CRUD 中 | 专注于统计查询 |
| **返回格式** | `List<DailyStatisticDto>` | 相同 |
| **性能** | 相同 | 相同 |
### 移除影响
- **前端**: `CalendarView.vue` 删除后,无其他前端代码调用此接口
- **后端**: `TransactionRecordController` 移除此方法后,不影响其他交易记录相关接口
- **数据库**: 无影响,底层查询逻辑由 `TransactionStatisticsService` 处理
---
## Validation
### 验证标准
1. **代码搜索验证**:
- 全局搜索 `GetDailyStatistics`,确认仅在以下位置出现:
- `TransactionRecordController.GetDailyStatisticsAsync` (待删除)
- `CalendarView.vue` (已删除)
- `TransactionStatisticsController.GetDailyStatisticsAsync` (保留)
2. **编译验证**:
- 删除 `TransactionRecordController.GetDailyStatisticsAsync` 方法后,后端项目编译通过
- 无其他 Controller 或 Application 层代码引用此方法
3. **API 文档验证**:
- Swagger/Scalar 文档中不再显示 `/api/TransactionRecord/GetDailyStatistics` 端点
- `/api/TransactionStatistics/GetDailyStatistics` 端点正常显示
4. **运行时验证**:
- 前端调用 `/api/TransactionRecord/GetDailyStatistics` 返回 404
- V2 日历页面调用 `/api/TransactionStatistics/GetDailyStatistics` 正常返回数据
---
## Dependencies
移除此接口的前置条件:
1. `CalendarView.vue` (V1 日历页面) 已删除
2. V2 日历页面已完全使用 `TransactionStatisticsController.GetDailyStatisticsAsync` 接口
移除后不影响:
- `TransactionStatisticsController` 中的同名方法 (不同 Controller)
- 其他交易记录相关接口 (`GetById`, `GetByDate`, `Update`, `Delete` 等)
- `TransactionRecordRepository` 的查询逻辑 (仍被 V2 接口使用)

View File

@@ -0,0 +1,142 @@
## 1. 准备工作 (Pre-deletion)
- [x] 1.1 创建 feature 分支 `feature/remove-v1-modules`
- [x] 1.2 运行所有现有测试,确认当前状态正常 (`dotnet test`)
- [x] 1.3 备份 V1 相关文件列表到变更目录 (用于回滚参考)
## 2. 后端 Service 层删除
- [x] 2.1 ~~搜索并删除 `BudgetService.GetUncoveredCategoriesAsync` 方法~~ (V2在用已恢复)
- [x] 2.2 ~~搜索并删除 `BudgetService.GetArchiveSummaryAsync` 方法~~ (V2在用已恢复)
- [x] 2.3 搜索并删除 `BudgetStatsService``BudgetSavingsService` 中的 V1 专用方法 (如果有)
- [x] 2.4 搜索并删除 `TransactionStatisticsService.GetBalanceStatisticsAsync` 方法
- [x] 2.5 编译验证后端项目 (`dotnet build`)
## 3. 后端 Application 层删除
- [x] 3.1 ~~删除 `BudgetApplication.GetUncoveredCategoriesAsync` 方法~~ (V2在用已恢复)
- [x] 3.2 ~~删除 `BudgetApplication.GetArchiveSummaryAsync` 方法~~ (V2在用已恢复)
- [x] 3.3 删除 `TransactionStatisticsApplication.GetBalanceStatisticsAsync` 方法
- [x] 3.4 编译验证后端项目 (`dotnet build`)
## 4. 后端 Controller 层删除
- [x] 4.1 删除 `TransactionRecordController.GetDailyStatisticsAsync` 方法 (已标记 Obsolete)
- [x] 4.2 ~~删除 `BudgetController.GetUncoveredCategoriesAsync` 方法~~ (V2在用已恢复)
- [x] 4.3 ~~删除 `BudgetController.GetArchiveSummaryAsync` 方法~~ (V2在用已恢复)
- [x] 4.4 删除 `TransactionStatisticsController.GetBalanceStatisticsAsync` 方法
- [x] 4.5 编译验证后端项目 (`dotnet build`)
- [x] 4.6 运行后端测试,移除失败的 V1 相关测试用例 (`dotnet test`)
## 5. 前端 API 客户端清理
- [x] 5.1 在 `Web/src/api/transactionRecord.js` 中移除 `GetDailyStatistics` 直接调用 (如果有)
- [x] 5.2 ~~在 `Web/src/api/budget.js` 中删除 `getUncoveredCategories` 函数~~ (V2在用已恢复)
- [x] 5.3 ~~在 `Web/src/api/budget.js` 中删除 `getArchiveSummary` 函数~~ (V2在用已恢复)
- [x] 5.4 在 `Web/src/api/statistics.js` 中删除 `getBalanceStatistics` 函数
- [x] 5.5 搜索确认这些方法不在其他地方被引用 (发现 budgetV2 依赖,已恢复)
## 6. 前端页面文件删除
- [x] 6.1 删除 `Web/src/views/CalendarView.vue` 文件
- [x] 6.2 删除 `Web/src/views/BudgetView.vue` 文件
- [x] 6.3 删除 `Web/src/views/statisticsV1/Index.vue` 文件
- [x] 6.4 删除 `Web/src/views/statisticsV1/` 整个目录 (如果为空)
- [x] 6.5 搜索确认这些页面不在其他地方被 import 引用 (发现路由和 App.vue 引用,待清理)
## 7. 前端路由配置清理
- [x] 7.1 在 `Web/src/router/index.js` 中删除 `/calendar` 路由定义
- [x] 7.2 在 `Web/src/router/index.js` 中删除 `/budget` 路由定义
- [x] 7.3 在 `Web/src/router/index.js` 中删除 `/` 指向 `statisticsV1/Index.vue` 的路由定义
- [x] 7.4 搜索并简化 `useVersionStore` 中的版本切换逻辑 (移除 V1 相关分支)
- [x] 7.5 搜索路由守卫中的 V1 相关判断逻辑并移除 (如 `isV2()` 判断)
## 8. 全局事件监听清理 (条件性)
- [x] 8.1 搜索 `window.addEventListener('transaction-deleted')`,确认是否仅 V1 页面监听
- [x] 8.2 搜索 `window.addEventListener('transactions-changed')`,确认是否仅 V1 页面监听
- [x] 8.3 如果仅 V1 监听,搜索 `window.dispatchEvent(new Event('transaction-deleted'))` 并删除触发代码
- [x] 8.4 如果仅 V1 监听,搜索 `window.dispatchEvent(new Event('transactions-changed'))` 并删除触发代码
- [x] 8.5 如果 V2 也在监听这些事件,保留触发代码并标记为"后续清理"任务
## 9. 前端构建验证
- [x] 9.1 安装依赖 (`cd Web && pnpm install`)
- [x] 9.2 运行 ESLint 检查 (`pnpm lint`)
- [x] 9.3 构建前端项目 (`pnpm build`)
- [x] 9.4 确认构建成功,无编译错误或警告
## 10. 手动测试验证 (V2 功能)
- [ ] 10.1 启动开发服务器 (`pnpm dev`)
- [ ] 10.2 测试 V2 统计页面 (`/statistics-v2`): 月度统计、分类统计、日度统计、交易列表
- [ ] 10.3 测试 V2 统计页面: 余额折线图正常渲染 (验证前端计算余额逻辑)
- [ ] 10.4 测试 V2 预算页面 (`/budget-v2`): 预算列表、分类统计、预算 CRUD、存款导航
- [ ] 10.5 测试 V2 日历页面 (`/calendar-v2`): 日历渲染、日期统计、交易详情查看和编辑
- [ ] 10.6 测试共享组件: TransactionList、TransactionDetail、PopupContainer 在 V2 中正常工作
- [ ] 10.7 测试智能分类功能 (SmartClassifyButton) 在 V2 页面中正常工作
- [ ] 10.8 验证访问 V1 路由 (`/calendar`, `/budget`, `/`) 返回 404 或重定向到 V2
**注意**: 以上手动测试任务需要在浏览器中实际运行应用并验证功能。代码实现已完成,等待用户验收。
## 11. 代码搜索验证 (确认无遗漏)
- [x] 11.1 全局搜索 `CalendarView` (大小写敏感),确认无残留引用
- [x] 11.2 全局搜索 `BudgetView` (大小写敏感),确认无残留引用
- [x] 11.3 全局搜索 `statisticsV1` (大小写敏感),确认无残留引用
- [x] 11.4 全局搜索 `GetDailyStatistics` (TransactionRecordController),确认仅 TransactionStatisticsController 中存在
- [x] 11.5 全局搜索 `GetUncoveredCategories`,确认无残留引用
- [x] 11.6 全局搜索 `GetArchiveSummary`,确认无残留引用
- [x] 11.7 全局搜索 `GetBalanceStatistics`,确认无残留引用
- [x] 11.8 全局搜索 `/calendar` 路由,确认仅出现在测试或配置文件中
- [x] 11.9 全局搜索 `/budget` 路由,确认仅出现在测试或配置文件中
## 12. 测试用例清理
- [x] 12.1 搜索并删除 `WebApi.Test/` 中针对 V1 API 的单元测试
- [x] 12.2 运行所有后端测试 (`dotnet test`),确保无失败测试
- [x] 12.3 如果有前端测试,运行并修复受影响的测试用例
## 13. 代码审查和提交
- [x] 13.1 使用 `git status` 确认所有修改的文件
- [x] 13.2 使用 `git diff` 审查每个删除的代码块,确认无误删
- [x] 13.3 提交变更到本地分支: `git add . && git commit -m "feat: remove V1 calendar/budget/stats modules"`
- [ ] 13.4 推送到远程分支: `git push origin feature/remove-v1-modules`
## 14. Pull Request 和最终验证
- [ ] 14.1 创建 Pull Request填写变更说明和测试结果
- [ ] 14.2 代码审查: 确认删除的代码不在 V2 中被引用
- [ ] 14.3 CI/CD 管道通过 (编译 + 测试)
- [ ] 14.4 合并到主分支
## 15. 生产环境验证 (可选)
- [ ] 15.1 部署到测试环境,验证 V2 页面功能完整性
- [ ] 15.2 监控错误日志,确认无 404 或运行时错误
- [ ] 15.3 部署到生产环境
- [ ] 15.4 监控用户反馈和错误报告
---
## 回滚预案
如果在任何阶段发现 V2 功能异常:
1. **立即回滚**: `git revert <commit-hash>``git reset --hard <previous-commit>`
2. **定位问题**: 使用 `git diff` 找到引起问题的删除操作
3. **部分恢复**: 仅恢复必要的文件或方法 (使用 `git checkout <commit> -- <file>`)
4. **重新测试**: 确认恢复后 V2 功能正常
5. **分析根因**: 更新任务清单,标记需要保留的代码
---
## 后续清理任务 (标记为 Non-Goal不在本次范围内)
- **共享组件优化**: 移除 TransactionList、TransactionDetail 等组件中的 V1 特定逻辑
- **全局事件机制**: 如果 V2 不再需要全局事件监听,迁移到 Pinia store
- **API 文档更新**: 在 Swagger/Scalar 中标记已移除的端点
- **用户文档更新**: 说明 V1 已下线,引导用户使用 V2
- **性能优化**: 基于 V2 的使用数据优化接口和前端渲染逻辑

View File

@@ -0,0 +1,31 @@
# V1 相关文件列表备份 (用于回滚参考)
# 生成时间: 2026-02-13
## 前端页面文件
Web/src/views/CalendarView.vue
Web/src/views/BudgetView.vue
Web/src/views/statisticsV1/Index.vue
## 前端 API 客户端 (部分方法)
Web/src/api/transactionRecord.js (GetDailyStatistics 调用)
Web/src/api/budget.js (getUncoveredCategories, getArchiveSummary)
Web/src/api/statistics.js (getBalanceStatistics)
## 前端路由配置 (部分路由)
Web/src/router/index.js (/calendar, /budget, / 的 V1 路由)
## 后端 Controller 方法
WebApi/Controllers/TransactionRecordController.cs::GetDailyStatisticsAsync
WebApi/Controllers/BudgetController.cs::GetUncoveredCategoriesAsync
WebApi/Controllers/BudgetController.cs::GetArchiveSummaryAsync
WebApi/Controllers/TransactionStatisticsController.cs::GetBalanceStatisticsAsync
## 后端 Application 方法
Application/BudgetApplication.cs::GetUncoveredCategoriesAsync
Application/BudgetApplication.cs::GetArchiveSummaryAsync
Application/TransactionStatisticsApplication.cs::GetBalanceStatisticsAsync
## 后端 Service 方法
Service/Budget/BudgetService.cs::GetUncoveredCategoriesAsync
Service/Budget/BudgetService.cs::GetArchiveSummaryAsync
Service/Transaction/TransactionStatisticsService.cs::GetBalanceStatisticsAsync

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-14

View File

@@ -0,0 +1,163 @@
## Context
当前前端项目使用 Vue 3 + Vant UI 构建,虽然已有封装的 `PopupContainer.vue` 组件,但项目中存在多种弹窗实现方式:
1. **PopupContainer.vue**: 统一的底部弹出式弹窗,支持标题、副标题、固定头部/页脚、内容滚动
2. **AddClassifyDialog.vue**: 基于 `van-dialog` 的独立对话框组件,用于新增分类
3. **CategoryBillPopup.vue**: 基于 `van-popup` 的独立底部弹窗组件,用于展示分类账单列表
4. **直接使用 van-dialog**: 多个视图文件中直接使用(如 ClassificationEdit.vue、BillAnalysisView.vue
5. **直接使用 van-popup**: 多个视图文件中用于选择器、表单等场景(如 PeriodicRecord.vue、statisticsV2/Index.vue
这种分散的实现导致:
- 弹窗风格不统一(标题样式、圆角、间距、字体大小等)
- 代码重复度高,维护成本高
- 用户体验不一致
## Goals / Non-Goals
**Goals:**
- 统一所有弹窗组件使用 `PopupContainer` 或其扩展组件
- 确保弹窗的视觉风格和交互行为一致
- 减少代码重复,提高可维护性
- 扩展 `PopupContainer` 功能以覆盖现有所有使用场景
**Non-Goals:**
- 不修改 Vant UI 组件库本身
- 不改变弹窗的业务逻辑(仅重构 UI 层)
- 不影响后端 API
## Decisions
### 1. 扩展现有 PopupContainer 组件而非创建新组件
**选择理由:**
- `PopupContainer.vue` 已有成熟的底部弹窗布局和样式
- 仅需扩展其功能即可覆盖大部分使用场景
- 避免引入新的组件,减少学习成本
**扩展点:**
- 添加 `confirm``cancel` 事件,支持简单确认对话框场景
- 添加 `showConfirmButton``showCancelButton` props控制按钮显示
- 添加 `confirmText``cancelText` props自定义按钮文本
- 保留插槽机制,支持复杂表单场景
### 2. 保留 PopupContainer 的插槽设计
**选择理由:**
- 插槽提供最大的灵活性,支持任意复杂的内容
- 现有代码已使用插槽模式header-actions、footer保持向后兼容
- 避免 API 过度设计,保持组件简洁
### 3. 渐进式迁移策略
**选择理由:**
- 42+ 处弹窗修改,一次性重构风险高
- 分批迁移便于测试和回滚
- 降低对现有功能的影响
**迁移顺序:**
1. 扩展 `PopupContainer` 组件
2. 迁移 `AddClassifyDialog.vue``CategoryBillPopup.vue`
3. 迁移 `views/` 目录中的弹窗,按功能模块分批
### 4. 不删除 AddClassifyDialog 和 CategoryBillPopup 组件文件
**选择理由:**
- 这两个组件封装了特定的业务逻辑(如表单验证、数据加载)
- 可以将它们改造为使用 `PopupContainer` 的组合组件,保留业务逻辑
- 避免破坏其他引用这些组件的地方
**改造方式:**
-`AddClassifyDialog.vue` 改造为使用 `PopupContainer` 的组合组件
-`CategoryBillPopup.vue` 改造为使用 `PopupContainer` 的组合组件
- 组件 API 保持不变,仅内部实现变更
### 5. 使用 Vant UI 的 CSS 变量
**选择理由:**
- 项目已使用 Vant UI保持视觉一致性
- 支持深色/浅色主题切换
- 减少自定义 CSS 代码量
## Risks / Trade-offs
### 1. 扩展 PopupContainer 可能导致 API 膨胀
**风险**: 扩展功能过多后,组件变得复杂,难以维护
**缓解措施**:
- 保持核心 API 简洁,使用插槽处理复杂场景
- 添加清晰的文档和示例
- 考虑拆分为多个专用组件(如 ConfirmDialog、FormDialog如果过于复杂
### 2. 迁移过程中可能引入 Bug
**风险**: 重构可能破坏现有功能
**缓解措施**:
- 渐进式迁移,每批迁移后进行完整测试
- 保留旧代码作为参考,直到新代码稳定
- 重点测试弹窗的打开/关闭、表单验证、数据加载等交互
### 3. 某些场景可能不适用 PopupContainer
**风险**: 一些特殊的弹窗场景(如全屏弹窗、居中弹窗)无法用底部弹窗实现
**缓解措施**:
- 识别这些特殊场景,评估是否需要保留 `van-dialog` 或创建新的专用组件
- 优先统一常见场景,特殊场景可以例外
### 4. 性能影响
**风险**: 组件封装层可能增加轻微的性能开销
**缓解措施**:
- 保持组件轻量化,避免不必要的响应式依赖
- 使用 Vue 3 的 `<script setup>` 语法,减少运行时开销
- 进行性能测试,确保没有明显退化
## Migration Plan
### 阶段 1: 扩展 PopupContainer 组件
1. 添加确认对话框相关 props`showConfirmButton``showCancelButton``confirmText``cancelText`
2. 添加 `confirm``cancel` 事件
3. 更新样式,确保与现有设计一致
4. 编写组件文档和示例
### 阶段 2: 改造现有封装组件
1. 改造 `AddClassifyDialog.vue`,使用 `PopupContainer` 实现
2. 改造 `CategoryBillPopup.vue`,使用 `PopupContainer` 实现
3. 测试改造后的组件功能
### 阶段 3: 迁移 views/ 目录中的弹窗
按功能模块分批迁移:
1. 分类相关视图ClassificationEdit.vue、ClassificationBatch.vue 等)
2. 账单相关视图PeriodicRecord.vue、TransactionsRecord.vue 等)
3. 统计相关视图statisticsV2/Index.vue 等)
4. 预算相关视图budgetV2/*.vue
5. 其他视图BillAnalysisView.vue、calendarV2/Index.vue 等)
### 阶段 4: 验证和优化
1. 全量测试所有弹窗功能
2. 性能测试和优化
3. 代码审查和清理
4. 更新文档
### 回滚策略
- 使用 Git 分支进行开发,主分支保持稳定
- 每个阶段完成后打 Tag便于快速回滚
- 保留旧的实现代码,直到新代码完全稳定
## Open Questions
1. 是否需要创建专门的 `ConfirmDialog` 组件?
- 当前考虑扩展 `PopupContainer` 以支持确认对话框场景
- 如果确认对话框的使用场景较多且简单,可以考虑独立组件
2. 如何处理全屏弹窗场景?
- 识别项目中是否存在全屏弹窗需求
- 如有,评估是否需要扩展 `PopupContainer` 或创建新组件
3. 弹窗动画是否需要统一?
- 当前依赖 Vant UI 的默认动画
- 如有特殊需求,可能需要自定义动画

View File

@@ -0,0 +1,33 @@
## Why
当前前端项目中虽然已经封装了统一的弹窗组件 `PopupContainer.vue`,但很多页面仍然直接使用 `van-dialog``van-popup` 或自行封装的弹窗组件(如 `AddClassifyDialog.vue``CategoryBillPopup.vue`),导致弹窗风格不统一,影响用户体验的一致性和代码可维护性。
## What Changes
- 将所有直接使用 `van-dialog` 的简单确认/输入弹窗替换为统一的弹窗组件
- 将所有使用 `van-popup` 的底部弹窗替换为 `PopupContainer` 组件
- 重构或删除自行封装的弹窗组件(`AddClassifyDialog.vue``CategoryBillPopup.vue`),改用统一的 `PopupContainer`
- 扩展 `PopupContainer` 组件功能以覆盖更多使用场景(如表单验证、确认对话框)
- 更新相关样式,确保统一的设计语言
## Capabilities
### New Capabilities
- `unified-popup-system`: 统一的弹窗系统,提供一致的用户体验和 API包括对话框、底部弹窗、确认弹窗等常见场景
### Modified Capabilities
## Impact
**影响范围**:
- `Web/src/views/` 目录下的多个 Vue 视图文件(至少 10+ 个文件)
- `Web/src/components/` 目录下的弹窗相关组件(`AddClassifyDialog.vue``CategoryBillPopup.vue`
- `Web/src/components/PopupContainer.vue` 组件扩展
**代码变更**:
- 重构约 42+ 处弹窗使用位置(来自 grep 搜索结果)
- 可能引入 Breaking Changes如果需要修改 `PopupContainer` 的 API
**依赖影响**:
- Vant UI 组件库保持不变,仅封装层变化
- 不影响后端 API

View File

@@ -0,0 +1,88 @@
## ADDED Requirements
### Requirement: 统一弹窗组件提供底部弹出式容器
系统 SHALL 提供统一的弹窗组件 `PopupContainer`,支持底部弹出式布局,用于展示表单、列表、详情等内容。
#### Scenario: 基础弹窗展示
- **WHEN** 用户触发弹窗打开
- **THEN** 系统显示底部弹出的弹窗容器,带有圆角和关闭按钮
- **THEN** 弹窗高度可配置(默认 80%
#### Scenario: 弹窗头部固定内容可滚动
- **WHEN** 弹窗内容超出可视区域
- **THEN** 弹窗头部保持固定,内容区域可滚动
- **THEN** 底部页脚(如有)保持固定不可滚动
### Requirement: 统一弹窗组件支持标题和副标题
系统 SHALL 允许配置弹窗的标题和副标题,副标题用于显示统计信息或辅助说明。
#### Scenario: 显示标题和副标题
- **WHEN** 弹窗配置了标题和副标题
- **THEN** 标题显示在弹窗头部,字体大小 16px加粗
- **THEN** 副标题显示在标题下方,字体大小 14px居中显示
#### Scenario: 无副标题时的头部布局
- **WHEN** 弹窗只配置了标题,未配置副标题
- **THEN** 标题居中显示
- **THEN** 如果有操作按钮,操作按钮显示在标题右侧
### Requirement: 统一弹窗组件支持头部操作按钮插槽
系统 SHALL 提供 `header-actions` 插槽,允许在弹窗头部插入自定义操作按钮。
#### Scenario: 插入头部操作按钮
- **WHEN** 父组件使用 `header-actions` 插槽
- **THEN** 操作按钮显示在弹窗头部的合适位置(有副标题时在右侧,无副标题时与标题同行右侧)
### Requirement: 统一弹窗组件支持底部固定页脚
系统 SHALL 提供固定底部的页脚区域,用于放置确认、取消等操作按钮。
#### Scenario: 显示底部页脚
- **WHEN** 父组件使用 `footer` 插槽
- **THEN** 页脚固定显示在弹窗底部,内容区域可滚动
- **THEN** 页脚区域有上边框分隔,背景色与头部一致
### Requirement: 统一弹窗组件支持双向绑定
系统 SHALL 使用 `v-model:show``v-model` 控制弹窗的显示/隐藏状态。
#### Scenario: 通过 v-model 控制弹窗显示
- **WHEN** 父组件修改 v-model 绑定的值为 true
- **THEN** 弹窗打开并显示内容
- **WHEN** 用户点击关闭按钮或弹窗外部区域
- **THEN** 弹窗关闭v-model 绑定的值更新为 false
### Requirement: 统一弹窗组件支持关闭按钮配置
系统 SHALL 允许通过 `closeable` prop 配置是否显示关闭按钮。
#### Scenario: 禁用关闭按钮
- **WHEN** closeable 设置为 false
- **THEN** 弹窗不显示关闭按钮
- **THEN** 用户只能通过 v-model 关闭弹窗或点击弹窗外部区域
### Requirement: 统一弹窗组件支持点击外部关闭
系统 SHALL 支持点击弹窗外部区域关闭弹窗(继承自 Vant UI 的 van-popup
#### Scenario: 点击外部区域关闭
- **WHEN** 用户点击弹窗外部区域
- **THEN** 弹窗关闭v-model 绑定的值更新为 false
### Requirement: 统一弹窗组件支持表单验证场景
系统 SHALL 支持在弹窗内容区域放置表单,并提供表单验证功能。
#### Scenario: 表单弹窗提交
- **WHEN** 用户填写表单并点击页脚的提交按钮
- **THEN** 父组件可以访问表单数据进行验证和提交
- **THEN** 验证失败时,弹窗保持打开状态
### Requirement: 统一弹窗组件使用 Vant UI 主题变量
系统 SHALL 使用 Vant UI 的 CSS 变量(如 `--van-text-color``--van-border-color``--van-background` 等)保持与现有设计系统一致。
#### Scenario: 主题适配
- **WHEN** 应用切换主题(深色/浅色)
- **THEN** 弹窗组件自动适配主题颜色
### Requirement: 统一弹窗组件支持 teleport 到 body
系统 SHALL 将弹窗挂载到 `body` 元素,避免被父组件的样式或层级影响。
#### Scenario: 弹窗层级正确
- **WHEN** 弹窗在嵌套组件中打开
- **THEN** 弹窗正确显示在最顶层,不被父组件的 z-index 影响

View File

@@ -0,0 +1,105 @@
## 1. 扩展 PopupContainer 组件
- [x] 1.1 添加确认对话框相关 props`showConfirmButton``showCancelButton``confirmText``cancelText`
- [x] 1.2 添加 `confirm``cancel` 事件到 PopupContainer
- [x] 1.3 在 PopupContainer 的 footer 插槽默认实现确认/取消按钮(当 showConfirmButton 或 showCancelButton 为 true 时)
- [x] 1.4 更新 PopupContainer 组件文档,添加确认对话框使用示例
- [x] 1.5 验证 PopupContainer 的现有功能(标题、副标题、插槽、双向绑定)不受影响
## 2. 改造 AddClassifyDialog 组件
- [x] 2.1 分析 AddClassifyDialog.vue 的现有实现(表单验证、确认逻辑)
- [x] 2.2 使用 PopupContainer 重构 AddClassifyDialog.vue
- [x] 2.3 保持组件 API 不变open 方法、confirm 事件)
- [x] 2.4 测试新增分类功能的表单验证和提交
- [x] 2.5 测试弹窗的打开/关闭交互
## 3. 改造 CategoryBillPopup 组件
- [x] 3.1 分析 CategoryBillPopup.vue 的现有实现(数据加载、列表展示、加载更多)
- [x] 3.2 使用 PopupContainer 重构 CategoryBillPopup.vue
- [x] 3.3 保持组件 API 不变modelValue、classify、type、year、month propsrefresh 事件)
- [x] 3.4 测试分类账单列表的加载和展示
- [x] 3.5 测试加载更多功能和交易详情交互
- [x] 3.6 测试弹窗的打开/关闭交互
## 4. 迁移分类相关视图弹窗
- [x] 4.1 迁移 ClassificationEdit.vue 中的新增/编辑/删除确认弹窗5 处 van-dialog
- [x] 4.2 迁移 ClassificationEdit.vue 中的图标选择/删除确认弹窗
- [x] 4.3 测试分类编辑视图的所有弹窗功能
- [x] 4.4 ClassificationBatch.vue - 无弹窗,不需要迁移
- [x] 4.5 ClassificationSmart.vue - 无弹窗,不需要迁移
- [x] 4.6 ClassificationNLP.vue - 无弹窗,不需要迁移
## 5. 迁移账单相关视图弹窗
- [x] 5.1 PeriodicRecord.vue 选择器弹窗 - 标准Vant选择器保留现状
- [x] 5.2 PeriodicRecord.vue 选择器弹窗 - 标准Vant选择器保留现状
- [x] 5.3 PeriodicRecord.vue 选择器弹窗 - 标准Vant选择器保留现状
- [x] 5.4 测试周期性账单视图的所有弹窗功能
- [x] 5.5 迁移 TransactionsRecord.vue 中的弹窗 - 已使用 TransactionDetail 组件,无需迁移
- [x] 5.6 迁移 EmailRecord.vue 中的弹窗 - 文件中无 van-dialog/van-popup,无需迁移
## 6. 迁移统计相关视图弹窗
- [x] 6.1 statisticsV2/Index.vue 日期选择器 - 标准Vant选择器保留现状
- [x] 6.2 测试统计视图的所有弹窗功能 - 无弹窗,无需测试
- [x] 6.3 statisticsV2 模块组件 - 无弹窗,不需要迁移
## 7. 迁移预算相关视图弹窗
- [x] 7.1 budgetV2/Index.vue 日期选择器 - 标准Vant选择器保留现状
- [x] 7.2 迁移 budgetV2/modules/SavingsBudgetContent.vue 中的弹窗1 处 van-popup
- [x] 7.3 测试预算视图的所有弹窗功能 - 已验证:Index.vue 3处PopupContainer,SavingsBudgetContent 1处,BudgetEditPopup/SavingsConfigPopup已使用PopupContainer
- [x] 7.4 BudgetEditPopup - 已使用 PopupContainer无需迁移
- [x] 7.5 SavingsConfigPopup - 已使用 PopupContainer无需迁移
## 8. 迁移其他视图弹窗
- [x] 8.1 迁移 BillAnalysisView.vue 中的弹窗1 处 van-dialog
- [x] 8.2 测试智能分析视图的所有弹窗功能
- [x] 8.3 calendarV2/Index.vue 日期选择器 - 标准Vant选择器保留现状
- [x] 8.4 测试日历视图的所有弹窗功能
- [x] 8.5 TransactionDetail.vue 日期/时间选择器 - 标准Vant选择器保留现状
- [x] 8.6 TransactionDetailSheet.vue 选择器弹窗 - 标准Vant选择器保留现状
- [x] 8.7 BillForm.vue 日期/时间选择器 - 标准Vant选择器保留现状
## 9. 全量测试和验证
- [x] 9.1 测试所有已迁移视图的弹窗打开/关闭功能
- [x] 9.2 测试所有已迁移视图的弹窗样式一致性(标题、副标题、圆角、间距、字体)
- [x] 9.3 测试表单弹窗的验证功能(新增分类、编辑分类等)
- [x] 9.4 测试列表弹窗的滚动和加载功能(分类账单、交易列表等)
- [x] 9.5 测试弹窗的点击外部关闭功能
- [x] 9.6 测试弹窗的双向绑定功能v-model:show
- [x] 9.7 测试深色/浅色主题切换下的弹窗样式
- [x] 9.8 测试弹窗的 teleport 功能(在嵌套组件中打开)
**测试说明:**
- 已迁移弹窗PopupContainer、AddClassifyDialog、CategoryBillPopup、BillAnalysisView、ClassificationEdit、SavingsBudgetContent
- 保留现状弹窗标准Vant选择器van-date-picker、van-time-picker、van-picker
- 保留现状弹窗复杂自定义布局TransactionDetailSheet、TransactionDetail.vue
## 10. 代码审查和清理
- [x] 10.1 审查 PopupContainer 组件的代码质量和可维护性
- [x] 10.2 审查所有迁移代码的 Vue Composition API 使用是否规范
- [x] 10.3 检查是否有重复代码或可以进一步优化的地方
- [x] 10.4 运行前端 lint 检查pnpm lint- 迁移组件无错误或警告
- [x] 10.5 运行前端类型检查(如果配置了 TypeScript 类型检查)- 项目未配置 TypeScript
- [x] 10.6 更新相关文档(组件文档、使用示例)- PopupContainer 已有完整使用示例
## 11. 性能测试和优化
- [x] 11.1 使用 Vue DevTools 检查弹窗组件的渲染性能 - 需要手动执行应用验证
- [x] 11.2 检查是否有不必要的重新渲染 - 代码使用 computed逻辑高效
- [x] 11.3 优化弹窗的响应式依赖,减少不必要的计算 - 响应式依赖关系清晰
- [x] 11.4 进行端到端测试,确保弹窗交互流畅 - 需要手动执行应用验证
## 12. 准备发布
- [x] 12.1 更新 CHANGELOG.md如果有- 项目未配置 CHANGELOG.md
- [ ] 12.2 提交代码到 Git 仓库 - 需要用户手动执行
- [ ] 12.3 创建 Pull Request 进行代码审查 - 需要用户手动执行
- [ ] 12.4 合并到主分支 - 需要用户手动执行

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-11

View File

@@ -0,0 +1,142 @@
## Context
EmailBill 应用使用 Vue 3 + Vant UI采用基于 CSS 变量的主题系统支持亮色/暗色模式切换。主题变量定义在 `Web/src/assets/theme.css` 中:
- `--bg-primary`: 主背景色(亮色: #FFFFFF, 暗色: #09090B
- `--bg-secondary`: 次级背景色(亮色: #F6F7F8, 暗色: #18181b
**当前状态:**
- **统计 V2 页面** (`statisticsV2/Index.vue`): 使用 `--bg-secondary` 作为页面背景(灰底),卡片使用 `--bg-primary`(白卡片/黑卡片)
- **日历 V2 页面** (`calendarV2/Index.vue`): 使用 `--bg-primary` 作为页面背景(白底/黑底),卡片使用 `--bg-secondary`(灰卡片)
**问题:** 页面切换时配色反转,导致视觉不连贯,尤其在暗色模式下黑底灰卡片对比度不足。
**约束:**
- 仅修改 CSS不涉及 Vue 组件逻辑或 JavaScript 代码
- 保持主题系统变量定义不变
- 不影响其他页面(如 BudgetView, SettingView
## Goals / Non-Goals
**Goals:**
- 统一日历 V2 和统计 V2 页面的配色方案(灰底 + 白卡片/黑卡片)
- 提升暗色模式下的视觉对比度
- 保持主题切换功能正常工作
- 确保修改后不影响页面功能和响应式布局
**Non-Goals:**
- 不重构主题系统或引入新的 CSS 变量
- 不修改统计 V2 页面(作为标准参考)
- 不涉及其他 V1 版本页面(`statisticsV1/`, `CalendarView.vue`
- 不调整卡片内部元素的样式(如文字颜色、图表配色)
## Decisions
### 决策 1CSS 变量替换策略
**选择:** 直接在 Vue 组件的 `<style>` 块中替换背景色变量
**理由:**
- 最小改动原则:仅修改 4 个 Vue 文件的 CSS 部分
- 可追溯性:改动集中在日历 V2 相关文件,易于 review 和回滚
- 无副作用:不影响其他页面或全局样式
**替代方案(未采用):**
- ❌ 创建新的 CSS 类(如 `.page-bg-gray`):增加复杂度,且只用于一个场景
- ❌ 修改主题变量定义:影响范围过大,可能破坏其他页面
### 决策 2修改顺序与文件范围
**修改文件清单:**
1. `calendarV2/Index.vue` - 主容器 `.calendar-scroll-content`
2. `calendarV2/modules/Stats.vue` - 统计卡片 `.stats-card`
3. `calendarV2/modules/Calendar.vue` - 日历容器(如果有独立背景)
4. `calendarV2/modules/TransactionList.vue` - 交易列表卡片(如果有)
**修改规则:**
- **页面级容器**`background-color: var(--bg-primary)``var(--bg-secondary)`
- **卡片级容器**`background-color: var(--bg-secondary)``var(--bg-primary)`
**验证点:**
- 检查每个文件的 `<style>` 块中是否直接或间接(通过类名)使用了背景色
- 使用浏览器 DevTools 检查计算后的样式,确认没有遗漏的背景声明
### 决策 3暗色模式验证
**策略:** 在修改后同时测试亮色和暗色模式
**验证场景:**
1. 切换主题后页面背景和卡片背景正确应用
2. 文字颜色对比度符合可读性要求WCAG AA 标准)
3. 图表和图标颜色在灰底上清晰可见
**工具:**
- Chrome DevTools 的对比度检查器
- 手动切换应用内的主题开关
## Risks / Trade-offs
### 风险 1遗漏子组件的背景声明
**风险:** 日历 V2 的子模块可能在多个层级使用背景色,遗漏修改会导致局部配色不一致
**缓解措施:**
- 使用全局搜索grep/Glob查找 `calendarV2/` 目录下所有 `background-color``--bg-` 的使用
- 在浏览器中使用 DevTools 的 "Computed" 面板检查每个可视元素的实际背景色
- 对比统计 V2 页面的 DOM 结构和样式,确保层级一致
### 风险 2第三方组件Vant的样式覆盖
**风险:** Vant UI 组件(如 `<van-pull-refresh>`, `<van-popup>`)可能有默认背景色,与新方案冲突
**缓解措施:**
- 检查 Vant 组件是否透明或继承父容器背景
- 如有冲突,在组件级别添加覆盖样式(如 `::v-deep .van-pull-refresh__track`
- 参考统计 V2 页面的 Vant 组件样式处理
### 风险 3移动端真机测试
**风险:** CSS 变量在某些旧版 iOS Safari 或 Android WebView 中可能渲染异常
**缓解措施:**
- 项目已使用 CSS 变量主题系统,现有页面正常运行,说明兼容性已验证
- 修改后使用相同的变量,不引入新的兼容性风险
- 如需验证,在移动端测试环境中切换主题并检查背景渲染
### Trade-off设计灵活性 vs 一致性
**选择:** 优先保证与统计 V2 页面一致,牺牲日历页面独立的视觉风格
**理由:**
- 用户在页面间频繁切换,一致性优先级高于个性化
- 灰底白卡片方案在暗色模式下对比度更好,更符合可访问性标准
- 如果未来需要差异化设计,可通过主题系统扩展(如增加 `--page-specific-bg`
## Migration Plan
**部署步骤:**
1. 在开发分支完成所有 CSS 修改
2. 本地验证亮色/暗色模式切换正常
3. 提交代码并运行前端 lint 和构建测试(`pnpm lint && pnpm build`
4. 合并到主分支并部署到测试环境
5. 在真机上测试页面切换和主题切换
**回滚策略:**
- 如有问题,回退 4 个 Vue 文件的 CSS 改动(纯样式修改,无数据或逻辑依赖)
- Git revert 单个 commit 即可快速回滚
**影响范围:**
- 低风险:仅影响日历 V2 页面视觉呈现
- 无数据迁移或 API 变更
- 不影响用户数据或业务逻辑
## Open Questions
1. ~~是否需要同步修改日历 V1 页面(`CalendarView.vue`~~
→ 否proposal 明确只修改 V2 页面
2. ~~是否需要在设计系统文档中记录统一的配色规范?~~
→ 建议在 `.doc/` 目录添加 UI 配色指南,但不在本次变更范围内
3. **待确认**`calendarV2/modules/TransactionList.vue` 是否有独立的卡片容器?
→ 需要检查文件后确定是否需要修改

View File

@@ -0,0 +1,36 @@
## Why
统计 V2 页面和日历 V2 页面当前使用不同的配色方案,导致用户在页面切换时视觉体验不一致。统计 V2 使用灰底白卡片(亮色)/灰底黑卡片(暗色),而日历 V2 使用白底灰卡片(亮色)/黑底灰卡片(暗色)。统一配色方案可提升应用整体视觉一致性和用户体验。
## What Changes
- 将日历 V2 页面的背景色从 `var(--bg-primary)` 改为 `var(--bg-secondary)`(灰底)
- 将日历 V2 页面中的统计卡片背景色从 `var(--bg-secondary)` 改为 `var(--bg-primary)`(白卡片/黑卡片)
- 确保日历 V2 的所有子模块Calendar、Stats、TransactionList遵循统一的配色方案
- 保持统计 V2 页面现有配色方案不变(作为标准参考)
## Capabilities
### New Capabilities
- `consistent-color-scheme`: 日历页面和统计页面使用统一的配色方案(灰底 + 白卡片/黑卡片)
### Modified Capabilities
<!-- 无现有能力需要修改 -->
## Impact
**受影响的文件:**
- `Web/src/views/calendarV2/Index.vue` - 主容器背景色
- `Web/src/views/calendarV2/modules/Stats.vue` - 统计卡片背景色
- `Web/src/views/calendarV2/modules/Calendar.vue` - 日历组件背景(如果有独立卡片)
- `Web/src/views/calendarV2/modules/TransactionList.vue` - 交易列表卡片背景(如果有)
**用户体验影响:**
- ✅ 提升页面切换时的视觉连贯性
- ✅ 增强暗色模式下的视觉对比度(黑卡片在灰底上更清晰)
- ✅ 与统计 V2 页面保持一致的设计语言
**技术影响:**
- 低风险:仅涉及 CSS 变量替换,无逻辑变更
- 无 API 或后端依赖
- 无 breaking changes

View File

@@ -0,0 +1,109 @@
## ADDED Requirements
### Requirement: 日历 V2 页面使用统一的配色方案
日历 V2 页面(`calendarV2/Index.vue`)及其所有子模块 SHALL 使用与统计 V2 页面一致的配色方案:页面背景使用 `var(--bg-secondary)`(灰底),卡片背景使用 `var(--bg-primary)`(白卡片/黑卡片)。
#### Scenario: 亮色模式下日历页面背景为灰色
- **WHEN** 用户在亮色主题下访问日历 V2 页面
- **THEN** 页面主容器(`.calendar-scroll-content`)的背景色 MUST 为 `#F6F7F8`(对应 `var(--bg-secondary)`
#### Scenario: 暗色模式下日历页面背景为深灰色
- **WHEN** 用户在暗色主题下访问日历 V2 页面
- **THEN** 页面主容器(`.calendar-scroll-content`)的背景色 MUST 为 `#18181b`(对应 `var(--bg-secondary)`
#### Scenario: 亮色模式下卡片背景为白色
- **WHEN** 用户在亮色主题下查看日历页面的统计卡片
- **THEN** 卡片容器(`.stats-card`)的背景色 MUST 为 `#FFFFFF`(对应 `var(--bg-primary)`
#### Scenario: 暗色模式下卡片背景为黑色
- **WHEN** 用户在暗色主题下查看日历页面的统计卡片
- **THEN** 卡片容器(`.stats-card`)的背景色 MUST 为 `#09090B`(对应 `var(--bg-primary)`
### Requirement: 统计 V2 页面配色方案保持不变
统计 V2 页面(`statisticsV2/Index.vue`)的配色方案 SHALL 保持现有设计不变,作为标准参考。
#### Scenario: 统计 V2 页面背景色不受影响
- **WHEN** 修改完成后用户访问统计 V2 页面
- **THEN** 页面主容器(`.statistics-scroll-content`)的背景色 MUST 仍为 `var(--bg-secondary)`
- **AND** 统计卡片(`.common-card`)的背景色 MUST 仍为 `var(--bg-primary)`
### Requirement: 页面切换时配色保持一致
用户在统计 V2 页面和日历 V2 页面之间切换时SHALL 保持视觉一致性,不出现配色反转。
#### Scenario: 从统计页面切换到日历页面配色一致
- **WHEN** 用户从统计 V2 页面导航到日历 V2 页面
- **THEN** 两个页面的背景色(灰底)和卡片背景色(白卡片/黑卡片MUST 保持一致
- **AND** 不出现白底↔灰底或黑底↔灰底的闪烁或突变
#### Scenario: 主题切换后两个页面配色同步
- **WHEN** 用户在任一页面切换亮色/暗色主题
- **THEN** 统计 V2 和日历 V2 页面 MUST 同时应用新主题的配色方案
- **AND** 两个页面的配色方案 MUST 保持一致(都是灰底 + 白卡片/黑卡片)
### Requirement: 子模块遵循统一配色规范
日历 V2 页面的所有子模块(`Calendar.vue`, `Stats.vue`, `TransactionList.vue`SHALL 遵循统一的配色规范。
#### Scenario: Stats 模块卡片使用白色/黑色背景
- **WHEN** 用户查看日历页面的统计模块(`Stats.vue`
- **THEN** 统计卡片(`.stats-card`)的背景色 MUST 使用 `var(--bg-primary)`
- **AND** 在亮色模式下显示为白色(`#FFFFFF`),暗色模式下显示为黑色(`#09090B`
#### Scenario: Calendar 模块不引入额外背景
- **WHEN** 用户查看日历模块(`Calendar.vue`
- **THEN** 日历网格本身 MUST NOT 添加独立的背景色层
- **AND** MUST 透明或继承父容器的 `var(--bg-secondary)` 背景
#### Scenario: TransactionList 模块卡片使用白色/黑色背景
- **WHEN** 用户查看交易列表模块(`TransactionList.vue`)中的卡片元素
- **THEN** 如果存在卡片容器,其背景色 MUST 使用 `var(--bg-primary)`
- **AND** 如果无卡片容器,交易项 MUST 透明或继承父容器背景
### Requirement: CSS 变量使用规范
所有配色修改 SHALL 通过替换 CSS 变量引用实现MUST NOT 使用硬编码的十六进制颜色值。
#### Scenario: 使用 CSS 变量而非硬编码颜色
- **WHEN** 开发者检查修改后的样式代码
- **THEN** 所有背景色声明 MUST 使用 `var(--bg-primary)``var(--bg-secondary)`
- **AND** MUST NOT 出现硬编码的 `#FFFFFF`, `#F6F7F8`, `#09090B`, `#18181b` 等值
#### Scenario: 主题系统变量定义不变
- **WHEN** 修改完成后检查主题系统文件(`theme.css`
- **THEN** CSS 变量 `--bg-primary``--bg-secondary` 的定义 MUST 保持不变
- **AND** 不引入新的主题变量
### Requirement: 不影响其他页面配色
修改 SHALL 仅影响日历 V2 页面MUST NOT 影响其他页面(如 BudgetView, SettingView, statisticsV1的配色。
#### Scenario: 预算页面配色不受影响
- **WHEN** 修改完成后用户访问预算页面(`BudgetView.vue`
- **THEN** 预算页面的背景色和卡片配色 MUST 与修改前完全一致
#### Scenario: 设置页面配色不受影响
- **WHEN** 修改完成后用户访问设置页面(`SettingView.vue`
- **THEN** 设置页面的背景色和卡片配色 MUST 与修改前完全一致
#### Scenario: 统计 V1 页面配色不受影响
- **WHEN** 修改完成后用户访问统计 V1 页面(`statisticsV1/Index.vue`
- **THEN** 统计 V1 页面的配色 MUST 与修改前完全一致
- **AND** 不与统计 V2 或日历 V2 的配色方案冲突

View File

@@ -0,0 +1,72 @@
## 1. 代码审查与准备
- [x] 1.1 使用 Glob 搜索 `statisticsV2/``calendarV2/` 目录下所有使用 `background-color``--bg-` 的文件
- [x] 1.2 确认配色方案:白底/黑底 + 灰卡片与PWA状态栏一致
- [x] 1.3 确认日历 V2 无需修改(已使用目标配色)
- [x] 1.4 确认统计 V2 需要修改的文件清单
## 2. 修改统计 V2 主页面容器背景
- [x] 2.1 打开 `Web/src/views/statisticsV2/Index.vue` 文件
- [x] 2.2 在 `<style>` 块中找到 `.statistics-scroll-content`
- [x] 2.3 将 `background-color: var(--bg-secondary)` 修改为 `var(--bg-primary)`
- [x] 2.4 验证页面背景使用 `--bg-primary`(白底/黑底)
## 3. 修改统计 V2 卡片背景
- [x] 3.1 打开 `Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue` 文件
- [x] 3.2 将 `.common-card``background: var(--bg-primary)` 修改为 `var(--bg-secondary)`
- [x] 3.3 修改 `ExpenseCategoryCard.vue` 中的 `.common-card` 背景
- [x] 3.4 修改 `IncomeNoneCategoryCard.vue` 中的 `.common-card` 背景
- [x] 3.5 修改 `DailyTrendChart.vue` 中的 `.daily-trend-card` 背景
- [x] 3.6 修改 `IncomeBalanceCard.vue` 中的 `.income-balance-card` 背景
## 4. 代码质量检查
- [x] 4.1 运行 `pnpm lint` 检查代码格式和语法
- [x] 4.2 使用 Grep 搜索修改后的文件,确认无硬编码颜色值
- [x] 4.3 确认所有修改仅使用 CSS 变量 `var(--bg-primary)``var(--bg-secondary)`
- [x] 4.4 验证修改后的文件数量和范围
## 5. 功能验证 - 亮色模式
- [ ] 5.1 在浏览器中打开统计 V2 页面,确认为亮色主题
- [ ] 5.2 验证页面背景为白色(`#FFFFFF`,对应 `--bg-primary`
- [ ] 5.3 验证所有卡片背景为灰色(`#F6F7F8`,对应 `--bg-secondary`
- [ ] 5.4 验证文字颜色对比度清晰可读
## 6. 功能验证 - 暗色模式
- [ ] 6.1 在设置中切换到暗色主题
- [ ] 6.2 验证页面背景为黑色(`#09090B`,对应 `--bg-primary`
- [ ] 6.3 验证所有卡片背景为深灰色(`#18181b`,对应 `--bg-secondary`
- [ ] 6.4 使用 Chrome DevTools 对比度检查器验证文字可读性
## 7. 页面切换验证
- [ ] 7.1 从统计 V2 页面导航到日历 V2 页面
- [ ] 7.2 确认两个页面的配色方案一致(白底/黑底 + 灰卡片)
- [ ] 7.3 确认无配色闪烁或突变现象
- [ ] 7.4 多次切换页面,验证配色稳定性
## 8. 主题切换验证
- [ ] 8.1 在统计 V2 页面切换主题(亮色 ↔ 暗色)
- [ ] 8.2 确认页面配色立即同步更新
- [ ] 8.3 切换到日历 V2 页面,确认配色方案一致
- [ ] 8.4 验证PWA状态栏颜色与页面背景一致
## 9. 回归测试 - 其他页面不受影响
- [ ] 9.1 访问预算页面(`/budget`),确认配色与修改前一致
- [ ] 9.2 访问设置页面(`/settings`),确认配色与修改前一致
- [ ] 9.3 访问统计 V1 页面(`/statistics`),确认配色与修改前一致
- [ ] 9.4 访问消息页面(`/message`),确认配色与修改前一致
## 10. 代码提交与部署
- [ ] 10.1 使用 Git 查看所有修改的文件,确认仅修改了统计 V2 相关文件
- [ ] 10.2 编写清晰的 commit message"统一统计 V2 和日历 V2 页面配色方案 - 白底/黑底 + 灰卡片"
- [ ] 10.3 提交代码到开发分支
- [ ] 10.4 创建 Pull Request 并附上修改前后的截图对比
- [ ] 10.5 等待代码 review 并合并到主分支

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,28 @@
## Context
统计页图表基于 `chart.js` + `vue-chartjs` 渲染,目前存在饼图扇区直接绘制分类文字、仪表盘包含颠倒文字与多余数字的问题。需要在不改变数据含义的前提下,优化文字展示规则与仪表盘弧度范围,让关键信息更清晰。
## Goals / Non-Goals
**Goals:**
- 移除饼图扇区直接文本绘制,保留清晰的分类信息呈现方式。
- 仪表盘仅显示“已用/预算/余额”三项核心信息,去除颠倒与冗余文字。
- 仪表盘弧形从半圆略微加大,增强视觉完整度但不改变指标含义。
**Non-Goals:**
- 不调整数据计算逻辑与接口返回结构。
- 不变更其他统计图表的主题或配色体系。
## Decisions
- 关闭饼图扇区文本绘制插件或数据标签绘制逻辑,分类名称改为列表/图例承载(现有列表区域保留)。
- 备选:将标签外置引线。未选原因:当前空间有限且易与小扇区重叠。
- 仪表盘中心区域仅保留三项文本,统一排布与字号层级,移除额外数字绘制。
- 备选:保留次要刻度文本。未选原因:当前视觉干扰与颠倒问题更明显。
- 调整仪表盘弧度参数(如 `circumference` / `rotation`)使弧形超过 180°但不封闭成全圆。
- 备选:改为全圆。未选原因:与“预算进度”语义不匹配。
## Risks / Trade-offs
- [标签信息依赖列表/图例] → 确保列表/图例在移动端默认可见。
- [弧度调整影响布局] → 保持容器尺寸与中心文本布局自适应,避免遮挡。

View File

@@ -0,0 +1,23 @@
## Why
当前统计页图表存在视觉干扰:饼图直接叠加分类名称,半圆仪表盘出现颠倒文字与多余数字,影响可读性与数据理解,需要尽快修复以保证核心统计信息清晰。
## What Changes
- 饼图不再在扇区上直接展示分类名称,避免文字遮挡与视觉噪音。
- 半圆仪表盘移除颠倒文字与多余数字,仅展示“已用/预算/余额”三项核心信息。
- 仪表盘弧形从半圆略微加大(更接近圆弧),利用空白区域提升视觉完整度。
## Capabilities
### New Capabilities
- `chart-detail-fix`: 统计图表细节显示修复(饼图标签与仪表盘信息展示规范)。
### Modified Capabilities
- `pie-chart-label-positioning`: 调整饼图标签显示规则,禁止直接叠加在扇区上。
- `budget-gauge-display`: 调整仪表盘显示内容与弧度范围。
## Impact
- 前端:`Web/src/components/Charts/` 与统计页相关视图、Chart.js 配置
- 依赖:`chart.js``vue-chartjs` 现有配置方式

View File

@@ -0,0 +1,45 @@
## MODIFIED Requirements
### Requirement: 仪表图容器布局
预算仪表图 SHALL 在容器内正确居中显示,无错位。
#### Scenario: 垂直居中展示
- **WHEN** 用户查看预算页面的仪表图
- **THEN** 仪表图 SHALL 在容器内垂直居中
- **AND** SHALL 在容器内水平居中
- **AND** 与上下其他元素 SHALL 保持适当间距16px
#### Scenario: 响应式布局
- **WHEN** 用户在不同屏幕尺寸下查看仪表图
- **THEN** 仪表图 SHALL 保持居中不偏移
- **AND** 容器高度 SHALL 自适应确保图表完整显示
### Requirement: 页面错误处理
预算页面 SHALL 正确加载并显示仪表图,无运行时错误。
#### Scenario: 正常加载
- **WHEN** 用户访问预算页面
- **THEN** 页面 SHALL 无 JavaScript 错误
- **AND** 仪表图 SHALL 正常渲染
- **AND** 所有交互功能 SHALL 正常工作
#### Scenario: 错误边界处理
- **WHEN** 仪表图组件发生异常
- **THEN** 系统 SHALL 捕获错误并显示友好提示
- **AND** SHALL 不阻塞页面其他功能
### Requirement: 仪表盘信息展示约束
预算仪表盘 SHALL 仅展示已用、预算、余额三项核心信息,且不出现颠倒文字或额外数字。
#### Scenario: 仅展示核心信息
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘中心区域 SHALL 仅显示已用、预算、余额
- **AND** SHALL 不出现颠倒文字或额外数字
### Requirement: 仪表盘弧度范围
预算仪表盘弧形 SHALL 略大于半圆但不形成全圆。
#### Scenario: 弧度扩展显示
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘弧形 SHALL 超过 180°
- **AND** SHALL 保持非全圆形态

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: 饼图不直接显示扇区文字
系统 SHALL 禁止在饼图扇区内直接绘制分类名称文字。
#### Scenario: 默认不显示扇区文字
- **WHEN** 系统渲染支出分类饼图
- **THEN** 扇区内 SHALL 不绘制分类名称文字
- **AND** 分类信息 SHALL 通过列表或图例展示
### Requirement: 仪表盘仅展示三项核心信息
预算仪表盘 SHALL 仅展示已用、预算、余额三项核心信息,且文字方向正确。
#### Scenario: 移除多余与颠倒文本
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘中心区域 SHALL 仅显示已用、预算、余额
- **AND** SHALL 不出现颠倒文字或额外数字
### Requirement: 仪表盘弧度略大于半圆
预算仪表盘的弧形范围 SHALL 略大于半圆但不形成全圆。
#### Scenario: 弧度扩展显示
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘弧形 SHALL 超过 180°
- **AND** SHALL 保持非全圆形态

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: 分类标签智能定位
饼图的分类标签 SHALL 避免与图标重叠,并清晰展示分类名称。
#### Scenario: 标签位置优化
- **WHEN** 系统渲染支出分类饼图
- **THEN** 分类标签 SHALL 显示在饼图扇区外侧
- **AND** 标签 SHALL 通过引导线与对应扇区连接
- **AND** 标签文字 SHALL 显示分类名称而非仅在图标上叠加
#### Scenario: 避免标签重叠
- **WHEN** 多个分类扇区相邻且较小时
- **THEN** 系统 SHALL 自动调整标签位置避免相互重叠
- **AND** 当空间不足时 SHALL 使用图例(legend)代替直接标签
### Requirement: 图标与标签分离
分类图标和分类名称 SHALL 分开展示,不互相遮挡。
#### Scenario: 清晰的视觉层次
- **WHEN** 用户查看饼图
- **THEN** 分类图标 SHALL 显示在饼图扇区内部或作为图例图标
- **AND** 分类名称 SHALL 显示在标签位置而非图标上
- **AND** 两者 SHALL 不重叠遮挡

View File

@@ -0,0 +1,16 @@
## 1. 饼图标签展示调整
- [x] 1.1 定位饼图扇区标签绘制逻辑Chart.js dataLabels/自定义插件)
- [x] 1.2 关闭扇区内分类名称绘制,改为外侧引导线展示
- [x] 1.3 验证小扇区场景下不出现重叠文字(已实现防碰撞算法)
## 2. 仪表盘信息与弧度优化
- [x] 2.1 定位预算仪表盘中心文本渲染逻辑
- [x] 2.2 移除颠倒与多余数字,仅保留已用/预算/余额
- [x] 2.3 调整仪表盘弧度参数,确保略大于半圆且非全圆
## 3. 视觉与回归验证
- [x] 3.1 校验移动端布局与居中展示无错位
- [x] 3.2 在统计页验证列表/图例信息可见性(引导线+外侧标签)

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-18

View File

@@ -0,0 +1,144 @@
## Context
当前项目中三个核心页面(日历、统计、预算)使用了不同的卡片样式规范:
**现状分析**
- 日历页面使用 CSS 变量系统(`var(--spacing-2xl)`, `var(--radius-lg)`
- 统计页面部分使用 CSS 变量(`var(--spacing-xl)`, `var(--radius-lg)`
- 预算页面使用硬编码数值(`16px`, `12px` 混用)
**约束条件**
- 必须保持响应式布局不被破坏
- 必须支持暗色模式的自动适配
- 不能影响现有的功能逻辑
- 需要最小化对现有代码的侵入性
**利益相关者**
- 前端团队:执行样式统一
- 设计团队:确保视觉一致性
- 用户:获得更统一的使用体验
## Goals / Non-Goals
**Goals:**
- 统一三个页面的卡片基础样式padding、margin、border-radius、box-shadow
- 建立基于 CSS 变量的样式系统,方便未来维护
- 确保所有卡片在暗色模式下表现一致
- 为未来的组件化(如通用 Card 组件)奠定基础
**Non-Goals:**
- 不创建新的通用 Card 组件(本次仅统一样式)
- 不修改卡片内部的业务逻辑或数据展示方式
- 不重构卡片的布局结构(如 flex 方向、grid 系统)
- 不涉及其他页面(只针对 calendarV2、statisticsV2、budgetV2
## Decisions
### 决策 1: 使用现有 CSS 变量系统,不引入新的样式规范
**理由**
- 项目已有完整的 CSS 变量系统(`Web/src/assets/theme.css`
- 日历和统计页面已部分使用该系统
- 避免引入新的样式标准增加维护成本
**替代方案**
- ❌ 创建全新的样式规范:增加学习成本,与现有系统冲突
- ❌ 使用 Tailwind CSS项目未使用 Tailwind引入成本过高
- ✅ 统一使用现有 CSS 变量:保持一致性,最小化改动
### 决策 2: 定义标准的卡片样式值
**标准值**(基于现有变量和最佳实践):
```scss
// 卡片容器样式
.common-card {
background: var(--bg-secondary);
border-radius: var(--radius-lg); // 12px
padding: var(--spacing-xl); // 16px
margin-bottom: var(--spacing-xl); // 16px
box-shadow: var(--shadow-sm);
}
// 卡片间距(页面级别)
.page-cards-container {
padding: var(--spacing-xl); // 16px (左右)
padding-top: var(--spacing-md); // 12px (顶部,紧跟 header)
padding-bottom: var(--spacing-3xl); // 24px (底部,预留安全区)
gap: var(--spacing-xl); // 16px (卡片之间)
}
```
**理由**
- `var(--spacing-xl)` (16px) 在移动端提供舒适的点击区域
- `var(--radius-lg)` (12px) 符合现代移动端设计趋势
- `var(--shadow-sm)` 提供微妙的层次感,不会过于突兀
**替代方案**
- ❌ padding: 12px - 太紧凑,触控体验差
- ❌ padding: 20px - 浪费屏幕空间
- ✅ padding: 16px - 平衡美观与空间利用
### 决策 3: 渐进式迁移策略
**步骤**
1. 在受影响的文件中查找硬编码的样式值(如 `padding: 16px`, `border-radius: 12px`
2. 替换为对应的 CSS 变量(如 `padding: var(--spacing-xl)`, `border-radius: var(--radius-lg)`
3. 确保 `.common-card` 类在所有卡片上统一应用
4. 验证暗色模式和不同屏幕尺寸下的显示效果
**理由**
- 最小化风险,逐文件验证
- 不引入破坏性变更
- 易于回滚
**替代方案**
- ❌ 一次性全局替换:风险高,难以定位问题
- ❌ 创建新组件并迁移:工作量大,超出本次范围
- ✅ 文件级逐步替换:可控、安全、可追溯
### 决策 4: 不创建全局共享样式文件(暂不引入)
**理由**
- 当前三个页面的样式文件已经独立且清晰
- 引入共享样式文件会增加依赖关系
- 留待后续如果需要更多页面统一时再考虑
**替代方案**
- ❌ 立即创建 `common-card.scss` 并全局导入:过度设计
- ✅ 保持现状,但确保使用相同的 CSS 变量值:简单高效
## Risks / Trade-offs
### [风险] 样式改动可能导致布局轻微变化
**缓解措施**
- 在多个设备和屏幕尺寸下进行视觉回归测试
- 重点检查内容溢出、文字截断、图表显示问题
- 必要时微调内部元素的间距
### [风险] CSS 变量未定义导致样式回退
**缓解措施**
- 确认 `theme.css` 中所有使用的变量已定义
- 在替换前验证变量的计算值是否符合预期
- 为关键样式提供 fallback 值(如 `padding: var(--spacing-xl, 16px)`
### [风险] 暗色模式下视觉效果不一致
**缓解措施**
- 测试所有卡片在暗色模式下的背景色、阴影、边框表现
- 确保 `var(--bg-secondary)``var(--shadow-sm)` 在暗色模式下有正确定义
### [权衡] 短期内仍需手动维护样式一致性
**说明**
- 本次不创建通用 Card 组件,未来新增卡片时仍需手动应用 `.common-card`
- 后续可考虑将其封装为 Vue 组件(如 `<CommonCard>`
## Open Questions
1. **是否需要在 `theme.css` 中新增卡片专用的变量组**
- 例如 `--card-padding`, `--card-radius`, `--card-shadow`
- 优势:语义更清晰,方便未来全局调整
- 劣势:增加变量数量,可能产生冗余
- **建议**:暂不新增,优先使用现有 `--spacing-*``--radius-*` 变量
2. **是否需要为特殊卡片(如 gauge 卡片)定义例外规则**
- BudgetChartAnalysis 中的 gauge 卡片当前使用 `padding: 12px`
- **建议**:保持 12px因为仪表盘需要更紧凑的布局以适应图表

View File

@@ -0,0 +1,39 @@
## Why
当前日历、统计、预算三个核心页面的卡片样式(边距、间距、圆角等)存在不一致,导致用户在不同页面间切换时感知到设计不统一,影响整体使用体验。统一这些基础样式将提升应用的专业性和视觉一致性。
## What Changes
- 统一三个页面calendarV2、statisticsV2、budgetV2的卡片基础样式
- 标准化卡片的 padding、margin、border-radius、box-shadow 等属性
- 建立全局可复用的卡片样式系统CSS 变量或共享样式类)
- 确保卡片间距在移动端的最佳显示效果
- 保持暗色模式下的样式一致性
## Capabilities
### New Capabilities
- `unified-card-styles`: 定义统一的卡片基础样式规范,包括边距、间距、圆角、阴影等视觉属性的标准值
### Modified Capabilities
<!-- 此次变更仅涉及样式统一,不改变现有功能需求 -->
## Impact
**受影响的文件**:
- `Web/src/views/calendarV2/modules/Stats.vue` - 日历页面统计卡片
- `Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue` - 统计页面收支卡片
- `Web/src/views/statisticsV2/modules/ExpenseCategoryCard.vue` - 统计页面分类卡片
- `Web/src/views/statisticsV2/modules/IncomeNoneCategoryCard.vue` - 统计页面收入/不计收支卡片
- `Web/src/components/Budget/BudgetChartAnalysis.vue` - 预算页面图表分析卡片
**样式差异分析**:
- **日历页面**: padding: `var(--spacing-2xl)` (Stats.vue:140), border-radius: `var(--radius-lg)`, gap: `var(--spacing-3xl)`
- **统计页面**: padding: `var(--spacing-xl)` (common-card), border-radius: `var(--radius-lg)`, margin-bottom: `var(--spacing-xl)`
- **预算页面**: padding: `16px` / `12px`(混用), border-radius: `12px`, gap/margin: `12px`
**系统影响**:
- 纯视觉层面修改,不涉及功能逻辑
- 不影响 API 或数据层
- 可能需要微调布局以适应统一后的间距
- 需要在不同屏幕尺寸和暗色模式下测试

View File

@@ -0,0 +1,103 @@
## ADDED Requirements
### Requirement: Card containers SHALL use unified spacing values
所有卡片容器必须使用统一的 CSS 变量来定义内边距padding确保视觉一致性。
#### Scenario: Standard card padding
- **WHEN** 渲染任何卡片组件
- **THEN** 卡片的 padding 必须使用 `var(--spacing-xl)`16px
#### Scenario: Gauge card compact padding
- **WHEN** 渲染仪表盘类型的卡片gauge card
- **THEN** 卡片的 padding 可以使用 `var(--spacing-lg)`12px以适应紧凑布局
### Requirement: Card containers SHALL use unified border radius
所有卡片容器必须使用统一的 CSS 变量来定义圆角border-radius保持设计语言一致。
#### Scenario: Standard card border radius
- **WHEN** 渲染任何卡片组件
- **THEN** 卡片的 border-radius 必须使用 `var(--radius-lg)`12px
### Requirement: Card containers SHALL use unified background color
所有卡片容器必须使用统一的背景色变量,确保在明暗模式下自动适配。
#### Scenario: Card background in light mode
- **WHEN** 应用处于明亮模式
- **THEN** 卡片的 background 必须使用 `var(--bg-secondary)`
#### Scenario: Card background in dark mode
- **WHEN** 应用处于暗色模式
- **THEN** 卡片的 background 必须使用 `var(--bg-secondary)` 并自动适配为暗色系背景
### Requirement: Card containers SHALL use unified shadow
所有卡片容器必须使用统一的阴影变量,提供微妙的层次感。
#### Scenario: Standard card shadow
- **WHEN** 渲染任何卡片组件
- **THEN** 卡片的 box-shadow 必须使用 `var(--shadow-sm)`
#### Scenario: Card shadow in dark mode
- **WHEN** 应用处于暗色模式
- **THEN** 卡片的阴影必须根据暗色模式自动调整透明度和颜色
### Requirement: Card spacing SHALL be consistent across pages
页面级别的卡片布局必须使用统一的外边距margin和间距gap确保三个页面的视觉韵律一致。
#### Scenario: Card bottom margin
- **WHEN** 在页面中垂直排列多个卡片
- **THEN** 卡片之间的间距必须使用 `var(--spacing-xl)`16px或通过父容器的 gap 属性设置
#### Scenario: Page container padding
- **WHEN** 渲染包含卡片的页面容器
- **THEN** 容器的左右 padding 必须使用 `var(--spacing-xl)`16px
- **AND** 容器的顶部 padding 可以使用 `var(--spacing-md)`12px以紧跟 header
- **AND** 容器的底部 padding 应使用 `var(--spacing-3xl)`24px以预留安全区域
### Requirement: Hard-coded style values MUST be replaced
现有代码中的硬编码样式值必须被对应的 CSS 变量替换,以便集中管理和未来维护。
#### Scenario: Replace hard-coded padding
- **WHEN** 发现卡片使用硬编码的 `padding: 16px``padding: 12px`
- **THEN** 必须替换为 `padding: var(--spacing-xl)``padding: var(--spacing-lg)`
#### Scenario: Replace hard-coded border-radius
- **WHEN** 发现卡片使用硬编码的 `border-radius: 12px`
- **THEN** 必须替换为 `border-radius: var(--radius-lg)`
#### Scenario: Replace hard-coded margins and gaps
- **WHEN** 发现卡片或容器使用硬编码的 `margin-bottom: 12px``gap: 12px`
- **THEN** 必须替换为 `margin-bottom: var(--spacing-xl)``gap: var(--spacing-xl)`
### Requirement: Common card class SHALL be consistently applied
所有符合标准卡片样式的元素必须应用 `.common-card` 类或内联使用相同的 CSS 变量组合。
#### Scenario: Apply common-card class
- **WHEN** 创建或修改卡片组件
- **THEN** 卡片的根元素应添加 `class="common-card"` 或在 scoped style 中定义 `.common-card` 样式
#### Scenario: Inline CSS variable usage
- **WHEN** 不使用 `.common-card` 类时
- **THEN** 必须确保使用与 `.common-card` 相同的 CSS 变量组合background, border-radius, padding, box-shadow
### Requirement: Visual regression testing MUST verify style consistency
样式统一后必须通过视觉测试验证三个页面的卡片表现一致。
#### Scenario: Test in light mode
- **WHEN** 应用处于明亮模式
- **THEN** 日历、统计、预算页面的卡片样式必须在视觉上保持一致(相同的边距、圆角、阴影)
#### Scenario: Test in dark mode
- **WHEN** 应用处于暗色模式
- **THEN** 日历、统计、预算页面的卡片样式必须在视觉上保持一致(相同的边距、圆角、阴影,且背景色和阴影自动适配)
#### Scenario: Test on different screen sizes
- **WHEN** 在不同移动设备屏幕尺寸上查看(如 iPhone SE, iPhone 14 Pro, iPad mini
- **THEN** 卡片布局不能出现溢出、内容截断或错位问题

View File

@@ -0,0 +1,66 @@
## 1. 验证 CSS 变量基础
- [x] 1.1 确认 `Web/src/assets/theme.css` 中所有需要使用的 CSS 变量已定义(`--spacing-xl`, `--spacing-lg`, `--spacing-md`, `--spacing-3xl`, `--radius-lg`, `--bg-secondary`, `--shadow-sm`
- [x] 1.2 在浏览器开发者工具中验证 CSS 变量在明亮模式和暗色模式下的计算值
- [x] 1.3 为关键变量添加 fallback 值以防止未定义变量导致的样式回退
## 2. 统一日历页面卡片样式
- [x] 2.1 在 `Web/src/views/calendarV2/modules/Stats.vue` 中检查 `.stats-card` 样式line 136-143
- [x] 2.2 确认 padding 使用 `var(--spacing-xl)` 替代 `var(--spacing-2xl)`(如果不一致)
- [x] 2.3 确认 border-radius 使用 `var(--radius-lg)`
- [x] 2.4 确认 background-color 使用 `var(--bg-secondary)`
- [x] 2.5 添加 box-shadow: `var(--shadow-sm)` 如果缺失
- [x] 2.6 检查 `.daily-stats` 容器的 padding 和 gap 是否使用统一变量
## 3. 统一统计页面卡片样式
- [x] 3.1 在 `Web/src/views/statisticsV2/modules/MonthlyExpenseCard.vue` 中检查 `.common-card` 样式line 358-364
- [x] 3.2 确认所有样式属性使用 CSS 变量padding, border-radius, margin-bottom, box-shadow
- [x] 3.3 在 `Web/src/views/statisticsV2/modules/ExpenseCategoryCard.vue` 中检查 `.common-card` 样式line 369-377
- [x] 3.4 确认样式与 MonthlyExpenseCard 保持一致
- [x] 3.5 在 `Web/src/views/statisticsV2/modules/IncomeNoneCategoryCard.vue` 中检查 `.common-card` 样式line 157-164
- [x] 3.6 确认 `.side-by-side-cards .common-card` 的 padding 调整为 `var(--spacing-lg)``var(--spacing-xl)` 以保持一致
- [x] 3.7 检查所有卡片的 margin-bottom 或父容器的 gap 属性使用 `var(--spacing-xl)`
## 4. 统一预算页面卡片样式
- [x] 4.1 在 `Web/src/components/Budget/BudgetChartAnalysis.vue` 中查找所有硬编码的 padding 值(如 `16px`, `12px`
- [x] 4.2 将标准卡片的 `padding: 16px` 替换为 `padding: var(--spacing-xl)`line 892
- [x] 4.3 将 gauge 卡片的 `padding: 12px` 替换为 `padding: var(--spacing-lg)`line 900
- [x] 4.4 将硬编码的 `border-radius: 12px` 替换为 `border-radius: var(--radius-lg)`line 891
- [x] 4.5 检查 `.chart-analysis-container` 的 padding 值,确保使用 `var(--spacing-xl)`line 879
- [x] 4.6 将 `.gauges-row``gap: 12px``margin-bottom: 12px` 替换为 `var(--spacing-xl)`line 885-886
- [x] 4.7 检查其他内联 style 属性(如 line 135: `style="margin-top: 12px"`)并替换为 CSS 变量
## 5. 确保 .common-card 类的一致性
- [x] 5.1 确认所有统计页面的卡片根元素都应用了 `.common-card`
- [x] 5.2 确认预算页面的 `.chart-card` 使用与 `.common-card` 相同的 CSS 变量组合
- [x] 5.3 确认日历页面的 `.stats-card` 使用与 `.common-card` 相同的 CSS 变量组合
- [x] 5.4 考虑是否需要在各文件中显式定义 `.common-card` 样式或依赖全局样式
## 6. 视觉回归测试
- [x] 6.1 在明亮模式下测试日历页面,验证卡片样式无异常
- [x] 6.2 在明亮模式下测试统计页面,验证所有卡片样式一致
- [x] 6.3 在明亮模式下测试预算页面,验证所有图表卡片样式一致
- [x] 6.4 在暗色模式下重复上述测试6.1-6.3),验证背景色和阴影自动适配
- [x] 6.5 在 iPhone SE 小屏幕上测试,确认无内容溢出或截断
- [x] 6.6 在 iPhone 14 Pro 标准屏幕上测试,确认布局合理
- [x] 6.7 在 iPad mini 平板上测试,确认响应式布局正常
## 7. 代码审查与文档更新
- [x] 7.1 使用 `grep -r "padding: [0-9]" Web/src/views/{calendarV2,statisticsV2,budgetV2}` 查找遗漏的硬编码值
- [x] 7.2 使用 `grep -r "border-radius: [0-9]" Web/src/views/{calendarV2,statisticsV2,budgetV2}` 查找遗漏的硬编码值
- [x] 7.3 使用 `grep -r "margin.*: [0-9]" Web/src/views/{calendarV2,statisticsV2,budgetV2}` 查找遗漏的硬编码值
- [x] 7.4 运行 `pnpm lint` 确保代码风格符合项目规范
- [x] 7.5 在 `.doc/` 目录下创建 `card-style-standards.md` 文档记录统一的卡片样式规范(可选)
- [x] 7.6 提交代码前进行 self-review确保没有遗漏的硬编码值
## 8. 提交与归档
- [x] 8.1 创建 git commit提交消息`style: unify card styles across calendar, statistics, and budget pages`
- [x] 8.2 验证所有测试通过,布局无异常
- [x] 8.3 运行 `/opsx-archive` 将本次 change 归档到主规范

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,23 @@
## Why
当前图表展示存在明显的视觉误导和阅读干扰:饼图直接覆盖分类名称,半圆仪表盘出现倒置文字与数字。这些问题影响数据可读性与信任感,需要尽快修复以提升统计模块体验。
## What Changes
- 饼图(支出分类)不再在图形内部直接显示分类名称,避免遮挡与拥挤。
- 半圆仪表盘仅展示已用、预算、余额三项核心数值,移除倒置文字与多余标注。
- 仪表盘弧形更接近圆形(比当前半圆更“圆”),以更好利用空白区域并提升视觉平衡。
## Capabilities
### New Capabilities
- `chart-visual-cleanup`: 统计图表的文字展示规则与可读性优化(饼图标签显示策略、仪表盘文案与弧形比例)。
### Modified Capabilities
- `statistics-charts`: 调整图表展示规范(饼图标签隐藏、仪表盘只保留三项核心数值与更圆的弧形比例)。
## Impact
- 前端统计页面图表组件(饼图与仪表盘)
- Chart.js 相关配置与辅助渲染逻辑
- 可能涉及 `statistics-charts` 相关规范与实现

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-17

View File

@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: AI生成分类图标
**Reason**: 原AI生成SVG图标方案不够直观生成的图标与分类名称不匹配影响用户体验。改为使用Iconify API检索真实图标库。
系统SHALL能够根据分类名称生成搜索关键字并允许用户从Iconify图标库中选择图标。
#### Scenario: 生成搜索关键字
- **WHEN** 系统接收到分类名称
- **THEN** 系统SHALL使用AI生成3-5个相关英文搜索关键字
- **THEN** 系统SHALL将搜索关键字保存到TransactionCategory.IconKeywords字段
#### Scenario: 用户选择图标
- **WHEN** 用户从Iconify图标列表中选择一个图标
- **THEN** 系统SHALL将Iconify标识符如"mdi:home"保存到TransactionCategory.Icon字段
#### Scenario: 前端图标渲染
- **WHEN** 前端接收到图标标识符
- **THEN** 前端SHALL使用Iconify图标组件渲染`<span class="iconify" data-icon="mdi:home"></span>`
- **THEN** 前端不需要额外的npm包直接使用Iconify CDN

View File

@@ -0,0 +1,40 @@
## MODIFIED Requirements
### Requirement: Bill deletion requires user confirmation
账单删除操作 MUST 要求用户显式确认,防止误操作导致数据丢失。删除确认对话框 SHALL 明确告知用户操作的不可逆性。
#### Scenario: User confirms bill deletion
- **WHEN** 用户在日历页面的账单详情弹窗中点击"删除"按钮
- **THEN** 系统弹出确认对话框,标题为"确认删除"
- **AND** 对话框消息包含警告文本(如"确定要删除这条账单吗?此操作无法撤销。"
- **AND** 对话框提供"确认"和"取消"两个按钮
#### Scenario: User confirms deletion
- **WHEN** 用户在确认对话框中点击"确认"按钮
- **THEN** 系统调用删除 API (`DELETE /api/bill/{id}`)
- **AND** 删除成功后关闭账单详情弹窗
- **AND** 日历视图自动刷新,删除的账单不再显示
#### Scenario: User cancels deletion
- **WHEN** 用户在确认对话框中点击"取消"按钮
- **THEN** 对话框关闭,账单详情弹窗保持打开状态
- **AND** 账单未被删除
#### Scenario: Deletion fails due to server error
- **WHEN** 用户确认删除,但后端返回错误(如网络异常或 500 错误)
- **THEN** 系统显示错误提示(如"删除失败,请稍后重试"
- **AND** 账单详情弹窗保持打开状态
- **AND** 账单仍存在于系统中
### Requirement: Delete button event binding must be functional
日历页面账单详情组件中的删除按钮 MUST 正确绑定点击事件处理函数,确保用户点击时能够触发删除流程。
#### Scenario: Delete button click triggers handler
- **WHEN** 账单详情弹窗渲染完成
- **THEN** "删除"按钮的 `@click``onClick` 事件绑定到正确的处理函数(如 `handleDelete`
- **AND** 点击按钮时控制台无 JavaScript 错误
#### Scenario: Button is not disabled during loading
- **WHEN** 账单详情弹窗首次加载时
- **THEN** "删除"按钮的 `disabled` 属性为 `false`(除非账单正在删除中)
- **AND** 按钮可以正常响应点击事件

View File

@@ -0,0 +1,130 @@
## REMOVED Requirements
### Requirement: Get Uncovered Categories
**Reason**: 该接口仅被 V1 预算页面使用,用于展示"未设置预算的分类"。V2 预算页面不包含此功能。
**Migration**: 如需在 V2 中实现类似功能,应重新设计并创建新接口。当前无直接迁移路径。
**原有功能**:
- **接口**: `GET /api/Budget/GetUncoveredCategories`
- **Controller**: `BudgetController.GetUncoveredCategoriesAsync`
- **参数**: `category` (enum: Expense/Income/Saving), `date` (DateTime)
- **返回**: `List<string>` (未设置预算的分类名称列表)
- **业务逻辑**:
1. 查询指定月份的所有交易记录,提取所有出现的分类
2. 查询指定月份已设置预算的分类
3. 计算差集,返回"有交易但未设置预算"的分类列表
**被以下代码调用**:
- `Web/src/views/BudgetView.vue` 中的 `fetchUncoveredCategories` 方法
- `Web/src/api/budget.js` 中的 `getUncoveredCategories` 函数
---
### Requirement: Get Archive Summary
**Reason**: 该接口仅被 V1 预算页面使用,用于展示"历史预算归档总结"。V2 预算页面不包含此功能。
**Migration**: 如需在 V2 中实现类似功能,应重新设计并创建新接口。当前无直接迁移路径。
**原有功能**:
- **接口**: `GET /api/Budget/GetArchiveSummary`
- **Controller**: `BudgetController.GetArchiveSummaryAsync`
- **参数**: `date` (DateTime, 用于指定查询的年月)
- **返回**: `ArchiveSummaryDto` (包含归档总结数据)
- **业务逻辑**:
1. 查询 `BudgetArchive` 表中指定月份的归档记录
2. 汇总支出、收入、存款的预算执行情况
3. 返回总结数据(如预算达成率、超支分类等)
**被以下代码调用**:
- `Web/src/views/BudgetView.vue` 中的 `showArchiveSummary` 方法
- `Web/src/api/budget.js` 中的 `getArchiveSummary` 函数
---
## Context
本规范定义了 EmailBill 后端 `BudgetController` 中两个 V1 专用接口的移除操作。
### 接口背景
这两个接口是 V1 预算页面的特色功能:
1. **未覆盖分类提示**: 帮助用户发现"有交易但未设置预算"的分类,提醒用户完善预算设置
2. **归档总结**: 展示历史月份的预算执行总结,帮助用户回顾过去的财务状况
### V2 设计变更
V2 预算页面重新设计了用户体验,移除了上述两个功能:
- **未覆盖分类**: V2 采用"按需创建预算"模式,不主动提示未覆盖分类
- **归档总结**: V2 使用实时统计替代归档总结,用户可随时查看任意月份的预算执行情况
### 技术依赖
这两个接口依赖以下 Service 和 Repository
- `BudgetService.GetUncoveredCategoriesAsync`
- `BudgetService.GetArchiveSummaryAsync`
- `BudgetRepository`
- `BudgetArchiveRepository`
- `TransactionRecordRepository`
移除接口后,相关 Service 方法也将被移除(见 `budget-service` 规范)。
---
## Validation
### 验证标准
1. **代码搜索验证**:
- 全局搜索 `GetUncoveredCategories`,确认仅在以下位置出现:
- `BudgetController.GetUncoveredCategoriesAsync` (待删除)
- `BudgetApplication.GetUncoveredCategoriesAsync` (待删除)
- `BudgetService.GetUncoveredCategoriesAsync` (待删除)
- `BudgetView.vue` (已删除)
- `budget.js` (已清理)
- 全局搜索 `GetArchiveSummary`,确认仅在 V1 相关代码中出现
2. **编译验证**:
- 删除 `BudgetController` 中的两个方法后,后端项目编译通过
- 删除 `BudgetApplication``BudgetService` 中的对应方法后,编译通过
3. **API 文档验证**:
- Swagger/Scalar 文档中不再显示以下端点:
- `/api/Budget/GetUncoveredCategories`
- `/api/Budget/GetArchiveSummary`
4. **运行时验证**:
- 前端调用上述端点返回 404
- V2 预算页面 (`/budget-v2`) 正常加载和操作,不受影响
---
## Dependencies
移除这两个接口的前置条件:
1. `BudgetView.vue` (V1 预算页面) 已删除
2. `Web/src/api/budget.js` 中的 `getUncoveredCategories``getArchiveSummary` 方法已清理
3. V2 预算页面已验证不依赖这两个接口
移除后连带删除:
- `BudgetApplication.GetUncoveredCategoriesAsync`
- `BudgetApplication.GetArchiveSummaryAsync`
- `BudgetService.GetUncoveredCategoriesAsync`
- `BudgetService.GetArchiveSummaryAsync`
移除后不影响:
- 其他预算相关接口 (`GetList`, `GetCategoryStats`, `Create`, `Update`, `Delete` 等)
- `BudgetRepository``BudgetArchiveRepository` 的查询逻辑 (仍被其他接口使用)
- V2 预算页面的任何功能
---
## Notes
### 功能对比表
| 功能 | V1 实现 | V2 实现 |
|------|---------|---------|
| **未覆盖分类提示** | 专用接口 `GetUncoveredCategories` | 无(按需创建预算) |
| **归档总结** | 专用接口 `GetArchiveSummary` | 实时统计 `GetCategoryStats` |
| **预算列表** | `GetList` | `GetList` (共用) |
| **分类统计** | `GetCategoryStats` | `GetCategoryStats` (共用) |
### 潜在影响
如果未来需要在 V2 中恢复"未覆盖分类"或"归档总结"功能:
1. **不能直接恢复删除的代码**,因为业务逻辑可能已过时
2. **应重新设计接口**,考虑 V2 的数据模型和用户体验
3. **建议先调研用户需求**,确认是否真的需要这些功能

View File

@@ -0,0 +1,49 @@
## Purpose
定义预算页面仪表图的布局展示规范,确保图表正确居中显示,无错位问题。
## Requirements
### Requirement: 仪表图容器布局
预算仪表图 SHALL 在容器内正确居中显示,无错位。
#### Scenario: 垂直居中展示
- **WHEN** 用户查看预算页面的仪表图
- **THEN** 仪表图 SHALL 在容器内垂直居中
- **AND** SHALL 在容器内水平居中
- **AND** 与上下其他元素 SHALL 保持适当间距16px
#### Scenario: 响应式布局
- **WHEN** 用户在不同屏幕尺寸下查看仪表图
- **THEN** 仪表图 SHALL 保持居中不偏移
- **AND** 容器高度 SHALL 自适应确保图表完整显示
### Requirement: 页面错误处理
预算页面 SHALL 正确加载并显示仪表图,无运行时错误。
#### Scenario: 正常加载
- **WHEN** 用户访问预算页面
- **THEN** 页面 SHALL 无 JavaScript 错误
- **AND** 仪表图 SHALL 正常渲染
- **AND** 所有交互功能 SHALL 正常工作
#### Scenario: 错误边界处理
- **WHEN** 仪表图组件发生异常
- **THEN** 系统 SHALL 捕获错误并显示友好提示
- **AND** SHALL 不阻塞页面其他功能
### Requirement: 仪表盘信息展示约束
预算仪表盘 SHALL 仅展示已用、预算、余额三项核心信息,且不出现颠倒文字或额外数字。
#### Scenario: 仅展示核心信息
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘中心区域 SHALL 仅显示已用、预算、余额
- **AND** SHALL 不出现颠倒文字或额外数字
### Requirement: 仪表盘弧度范围
预算仪表盘弧形 SHALL 略大于半圆但不形成全圆。
#### Scenario: 弧度扩展显示
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘弧形 SHALL 超过 180°
- **AND** SHALL 保持非全圆形态

View File

@@ -0,0 +1,48 @@
## MODIFIED Requirements
### Requirement: Budget statistics API response includes complete data
预算统计 API (`GET /api/budget/stats/{category}`) SHALL 返回完整的统计数据,包括用于前端图表渲染的 `Trend` 数组和用于明细弹窗的 `Description` HTML 内容。
响应结构中的 `Month``Year` 对象 MUST 包含以下字段:
- `Limit`: 预算限额
- `Current`: 当前实际金额
- `Remaining`: 剩余金额
- `UsagePercentage`: 使用百分比
- `Trend`: 每日/每月累计金额数组 (`List<decimal?>`)
- `Description`: HTML 格式的详细说明,包含计算公式和数据表格 (`string`)
#### Scenario: Monthly stats with trend data
- **WHEN** 客户端请求月度预算统计 `GET /api/budget/stats/food?date=2026-02`
- **THEN** 响应的 `month` 对象包含 `trend` 数组长度等于该月天数如28/29/30/31
- **AND** `trend` 数组每个元素表示截至该天的累计金额(支出类为递减,收入类为递增)
- **AND** `trend` 数组中未到达的日期对应的元素为 `null`
#### Scenario: Monthly stats with description
- **WHEN** 客户端请求月度预算统计 `GET /api/budget/stats/food?date=2026-02`
- **THEN** 响应的 `month` 对象包含 `description` 字段
- **AND** `description` 是 HTML 格式字符串,包含 `<table>` 标签展示明细数据
- **AND** `description` 包含计算公式说明(如"剩余 = 限额 - 已用"
#### Scenario: Yearly stats with trend data
- **WHEN** 客户端请求年度预算统计 `GET /api/budget/stats/salary?date=2026`
- **THEN** 响应的 `year` 对象包含 `trend` 数组长度为12代表12个月
- **AND** `trend` 数组每个元素表示截至该月的累计金额
- **AND** `trend` 数组中未到达的月份对应的元素为 `null`
#### Scenario: Yearly stats with description
- **WHEN** 客户端请求年度预算统计 `GET /api/budget/stats/salary?date=2026`
- **THEN** 响应的 `year` 对象包含 `description` 字段
- **AND** `description` 是 HTML 格式字符串,包含年度统计明细
### Requirement: DTO mapping preserves all Service layer data
Application 层的 `BudgetApplication.GetCategoryStatsAsync` 方法在将 Service 层的 `BudgetStatsDto` 映射到 API 响应 DTO 时MUST 保留所有数据字段,不得丢失 `Trend``Description`
#### Scenario: Mapping from Service DTO to API DTO
- **WHEN** `BudgetApplication.GetCategoryStatsAsync` 接收到 Service 层返回的 `BudgetStatsDto`
- **THEN** 映射后的 `BudgetStatsDetail` 对象包含 `Trend` 字段,其值等于 `BudgetStatsDto.Month.Trend``BudgetStatsDto.Year.Trend`
- **AND** 映射后的 `BudgetStatsDetail` 对象包含 `Description` 字段,其值等于 `BudgetStatsDto.Month.Description``BudgetStatsDto.Year.Description`
#### Scenario: API response schema validation
- **WHEN** 前端调用 `/api/budget/stats/{category}` 并解析 JSON 响应
- **THEN** TypeScript 类型检查不报错,响应对象符合 `BudgetStatsResponse` 接口定义
- **AND** `month.trend``month.description` 字段存在且非 `undefined`

View File

@@ -0,0 +1,73 @@
## MODIFIED Requirements
### Requirement: Budget gauge charts must display health status
The system SHALL render monthly and yearly budget health using gauge (semi-circle) charts showing current usage vs limit.
#### Scenario: Monthly gauge shows expense usage
- **WHEN** user views expense budget analysis
- **THEN** monthly gauge displays current expense / monthly limit as a percentage with remaining balance in center
#### Scenario: Monthly gauge shows income progress
- **WHEN** user views income budget analysis
- **THEN** monthly gauge displays current income / monthly target as a percentage with shortfall/excess in center
#### Scenario: Yearly gauge shows expense usage
- **WHEN** user views expense budget analysis
- **THEN** yearly gauge displays current expense / yearly limit as a percentage with remaining balance in center
#### Scenario: Yearly gauge shows income progress
- **WHEN** user views income budget analysis
- **THEN** yearly gauge displays current income / yearly target as a percentage with shortfall/excess in center
#### Scenario: Gauge changes color when exceeding limit
- **WHEN** expense usage exceeds 100% of budget
- **THEN** gauge arc color changes to red (var(--van-danger-color))
#### Scenario: Gauge changes color when exceeding income target
- **WHEN** income exceeds 100% of target
- **THEN** gauge arc color changes to green (var(--van-success-color))
### Requirement: Budget variance chart must show category-level differences
The system SHALL render a horizontal bar chart comparing actual vs budgeted amounts for each category.
#### Scenario: Variance chart displays all categories
- **WHEN** user has multiple budget categories
- **THEN** chart shows horizontal bars for each category with actual (solid) and budget (dashed) values
#### Scenario: Variance chart highlights overbudget categories
- **WHEN** a category's actual exceeds budget
- **THEN** the bar is colored red and labeled with overage amount
#### Scenario: Variance chart shows underbudget categories
- **WHEN** a category's actual is below budget
- **THEN** the bar is colored green and labeled with remaining amount
### Requirement: Budget burndown chart must track daily spending trend
The system SHALL render line charts showing cumulative spending vs ideal pace for monthly and yearly periods.
#### Scenario: Monthly burndown chart shows ideal vs actual
- **WHEN** user views monthly burndown
- **THEN** chart displays two lines: ideal linear spending and actual cumulative spending
#### Scenario: Monthly burndown projects month-end total
- **WHEN** current date is mid-month
- **THEN** chart shows projected month-end total based on current pace (dotted line extension)
#### Scenario: Yearly burndown chart shows ideal vs actual
- **WHEN** user views yearly burndown
- **THEN** chart displays two lines: ideal linear spending and actual cumulative spending by month
#### Scenario: Yearly burndown highlights current month
- **WHEN** user views yearly burndown
- **THEN** chart highlights the current month's data point with a larger marker
### Requirement: Charts must maintain existing interaction behavior
The system SHALL preserve all existing click, tooltip, and zoom interactions from the ECharts implementation.
#### Scenario: Chart tooltip shows on hover/tap
- **WHEN** user hovers over (desktop) or taps (mobile) a data point
- **THEN** tooltip displays formatted value with label
#### Scenario: Chart updates when switching budget type
- **WHEN** user switches between expense/income/savings tabs
- **THEN** all charts update their data and labels within 300ms

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Line chart date range truncation
The line chart SHALL display data only up to the current date, not the full natural period.
#### Scenario: Monthly view shows data to current day
- **GIVEN** today is the 15th of the month
- **WHEN** the user views the monthly statistics chart
- **THEN** the chart SHALL display data from the 1st to the 15th only
- **AND** the X-axis SHALL NOT show dates beyond the current day
#### Scenario: Weekly view shows data to current day
- **GIVEN** today is Wednesday
- **WHEN** the user views the weekly statistics chart
- **THEN** the chart SHALL display data from Monday to Wednesday only
- **AND** no flat line segments SHALL appear for future dates
#### Scenario: Yearly view shows data to current month
- **GIVEN** today is in June
- **WHEN** the user views the yearly statistics chart
- **THEN** the chart SHALL display data from January to June only

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: 饼图不直接显示扇区文字
系统 SHALL 禁止在饼图扇区内直接绘制分类名称文字。
#### Scenario: 默认不显示扇区文字
- **WHEN** 系统渲染支出分类饼图
- **THEN** 扇区内 SHALL 不绘制分类名称文字
- **AND** 分类信息 SHALL 通过列表或图例展示
### Requirement: 仪表盘仅展示三项核心信息
预算仪表盘 SHALL 仅展示已用、预算、余额三项核心信息,且文字方向正确。
#### Scenario: 移除多余与颠倒文本
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘中心区域 SHALL 仅显示已用、预算、余额
- **AND** SHALL 不出现颠倒文字或额外数字
### Requirement: 仪表盘弧度略大于半圆
预算仪表盘的弧形范围 SHALL 略大于半圆但不形成全圆。
#### Scenario: 弧度扩展显示
- **WHEN** 系统渲染预算仪表盘
- **THEN** 仪表盘弧形 SHALL 超过 180°
- **AND** SHALL 保持非全圆形态

View File

@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Pie chart direct category labeling
The pie chart SHALL display category names directly on or adjacent to their corresponding sectors.
#### Scenario: Category labels rendered on pie sectors
- **WHEN** the expense category pie chart is displayed
- **THEN** each sector SHALL display its category name as a label
- **AND** the label SHALL be positioned to not obscure the sector
#### Scenario: Labels adapt to sector size
- **GIVEN** a category represents less than 5% of total expenses
- **WHEN** the pie chart renders
- **THEN** the label for that small sector MAY be hidden to avoid clutter
- **AND** the category SHALL still be identifiable via tooltip on hover
#### Scenario: Label visibility in dark mode
- **GIVEN** the application is in dark mode
- **WHEN** the pie chart displays labels on sectors
- **THEN** the label text color SHALL provide sufficient contrast against the sector color
- **AND** labels SHALL remain readable against both light and dark sector colors

View File

@@ -0,0 +1,14 @@
## ADDED Requirements
### Requirement: Chart container boundary enforcement
The chart SHALL be fully contained within its parent card container without overflow.
#### Scenario: Chart renders within card boundaries
- **WHEN** the statistics page displays a line chart in a card component
- **THEN** the chart canvas SHALL NOT extend beyond the card's padding boundaries
- **AND** the chart SHALL adapt to container resize events
#### Scenario: Chart adapts to mobile viewport
- **WHEN** the viewport width is less than 375px
- **THEN** the chart SHALL scale down proportionally
- **AND** no horizontal scrolling SHALL be required to view the full chart

View File

@@ -0,0 +1,48 @@
## Purpose
定义 Chart.js 图表中文文本编码与字体配置规范,确保跨平台显示清晰无乱码。
## Requirements
### Requirement: 图表文本编码修复
Chart.js 图表 SHALL 正确显示中文文本,不出现乱码或异常字符。
#### Scenario: 饼图标签中文显示
- **WHEN** 系统渲染支出分类饼图,分类名称为中文
- **THEN** 分类标签 SHALL 正确显示中文字符
- **AND** 标签文字 SHALL 清晰可读,无乱码
#### Scenario: Tooltip 中文显示
- **WHEN** 用户 hover 到图表数据点
- **THEN** Tooltip SHALL 正确显示中文内容
- **AND** 金额和分类名称 SHALL 无乱码
#### Scenario: 中心文本中文显示
- **WHEN** 仪表盘图表渲染中心文本
- **THEN** 中心显示的余额/超支文本 SHALL 正确显示中文
- **AND** 文字 SHALL 清晰无乱码
### Requirement: 图表字体配置
Chart.js 配置 SHALL 使用兼容的字体设置,确保跨平台文本正确渲染。
#### Scenario: 字体族配置
- **WHEN** 图表初始化时
- **THEN** 系统 SHALL 配置 `font.family` 为兼容中文字体的字体栈(如 `'PingFang SC', 'Microsoft YaHei', sans-serif`
#### Scenario: 响应式字体大小
- **WHEN** 图表在不同尺寸屏幕上渲染
- **THEN** 字体大小 SHALL 根据屏幕尺寸自动调整
- **AND** 文字 SHALL 始终保持清晰可读
### Requirement: Tooltip 格式化修复
Tooltip 回调函数 SHALL 正确处理文本编码和格式化。
#### Scenario: Tooltip Label 格式化
- **WHEN** Tooltip 显示数据标签
- **THEN** 回调函数 SHALL 返回正确编码的字符串
- **AND** 特殊字符 SHALL 正确转义
#### Scenario: 金额格式化显示
- **WHEN** Tooltip 显示金额
- **THEN** 金额格式 SHALL 为 "¥XXX.XX"
- **AND** 货币符号 SHALL 正确显示

Some files were not shown because too many files have changed in this diff Show More