- Migrated 4 components from ECharts to Chart.js: * MonthlyExpenseCard.vue (折线图) * DailyTrendChart.vue (双系列折线图) * ExpenseCategoryCard.vue (环形图) * BudgetChartAnalysis.vue (仪表盘 + 多种图表) - Removed all ECharts imports and environment variable switches - Unified all charts to use BaseChart.vue component - Build verified: pnpm build success ✓ - No echarts imports remaining ✓ Refs: openspec/changes/migrate-remaining-echarts-to-chartjs
8.7 KiB
Context
当前 EmailBill 项目中存在两个 TransactionList.vue 实现:
- 旧版(
Web/src/components/TransactionList.vue):传统一行一卡片布局,功能完整但样式较旧 - v2 版(
Web/src/views/calendarV2/modules/TransactionList.vue):现代卡片式设计,视觉层次更好
两个组件分别服务不同页面,导致:
- 代码重复(格式化、API 调用、交互逻辑)
- 样式不一致(用户体验割裂)
- 维护成本高(修改需同步两处)
技术栈约束:
- Vue 3 Composition API +
<script setup>(JavaScript) - Vant UI 组件库(移动端)
- Pinia 状态管理
- 后端 API:
@/api/transactionRecord
重要说明:
- 项目使用 JavaScript 而非 TypeScript
- 使用 JSDoc 注释提供类型提示
- Props 和 Emits 使用
defineProps()和defineEmits()的对象语法
设计目标:
创建统一的 BillListComponent.vue,整合两者优点,高内聚设计,支持筛选、排序、分页、左滑删除、详情查看。
Goals / Non-Goals
Goals:
- 创建可复用的账单列表组件
BillListComponent.vue(位于Web/src/components/Bill/) - 基于 v2 风格,但调整为紧凑列表(非一行一卡片)
- 内置筛选(类型、分类、日期范围)、排序(金额、时间)、分页加载
- 支持左滑删除(van-swipe-cell)、点击详情(emit 事件)
- 保留旧版的特殊功能(checkbox 选择模式)
- 迁移所有使用旧组件的页面到新组件
- 删除旧版
Web/src/components/TransactionList.vue
Non-Goals:
- 不改变后端 API 接口
- 不涉及新增数据字段
- 不处理账单编辑功能(仅展示和删除)
- 暂不支持拖拽排序
Decisions
决策 1:组件命名和位置
选择:Web/src/components/Bill/BillListComponent.vue
理由:
- 放在
Bill/目录下与现有BillForm.vue、ManualBillAdd.vue等保持一致 - 命名为
BillListComponent而非TransactionList,避免与旧组件混淆 - 符合项目 BEM 命名规范
备选方案:
- 直接覆盖旧版
TransactionList.vue→ 拒绝:迁移期间需要并存,且容易引起冲突
决策 2:Props 设计(高内聚 vs 灵活配置)
选择:高内聚 - 组件内部管理筛选、排序、分页状态
Props 定义:
// 使用 defineProps 对象语法 + JSDoc
const props = defineProps({
// 数据源模式
dataSource: {
type: String, // 'api' | 'custom'
default: 'api'
},
// API 模式参数
apiParams: {
type: Object, // { dateRange?: [string, string], category?: string, type?: 0|1|2 }
default: () => ({})
},
// Custom 模式参数
transactions: {
type: Array, // Transaction[]
default: () => []
},
// 功能开关
showDelete: {
type: Boolean,
default: true
},
showCheckbox: {
type: Boolean,
default: false
},
enableFilter: {
type: Boolean,
default: true
},
enableSort: {
type: Boolean,
default: true
},
// 样式配置
compact: {
type: Boolean,
default: true
},
// 多选状态
selectedIds: {
type: Set,
default: () => new Set()
}
})
理由:
- 大部分场景只需传
apiParams,组件自动处理筛选、排序、分页 dataSource='custom'模式兼容特殊场景(如离线数据、缓存数据)- 功能开关满足不同页面需求(如 TransactionsRecord 需要 checkbox)
备选方案:
- 低内聚(父组件管理所有状态)→ 拒绝:每个使用方都要重复实现筛选、排序逻辑,违背复用目标
决策 3:筛选 UI 实现
选择:van-dropdown-menu + van-popup(日期选择器)
布局:
[类型 ▼] [分类 ▼] [日期 ▼] [排序 ▼]
理由:
- Vant 的
van-dropdown-menu适合移动端,节省空间 - 日期范围选择使用
van-calendar弹出层 - 与项目现有 UI 风格一致
备选方案:
- 使用
van-tabs切换筛选项 → 拒绝:占用空间大,不适合同时筛选多个维度
决策 4:数据加载策略
选择:虚拟滚动 + 分页加载(van-list)
实现:
- 初始加载 20 条
- 滚动到底部触发
@load事件,追加 20 条 - 筛选/排序变更时,重置列表并重新加载
理由:
- Vant 的
van-list内置分页逻辑,简单易用 - 账单数据量通常不大(日常使用 < 1000 条),无需复杂虚拟滚动库
备选方案:
- 一次性加载全部数据 → 拒绝:账单数量多时性能差
决策 5:删除功能的事务处理
选择:组件内部调用 deleteTransaction API,删除成功后 emit 事件并派发全局事件
流程:
const handleDelete = async (transaction) => {
await showConfirmDialog({ message: '确定删除?' })
await deleteTransaction(transaction.id) // API 调用
emit('delete', transaction.id) // 通知父组件
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id })) // 全局事件
// 刷新当前列表
}
理由:
- 保持与旧版一致的删除逻辑(已验证可用)
- 全局事件通知其他组件刷新(如统计图表)
- 父组件可通过
@delete监听执行额外逻辑
备选方案:
- 父组件负责删除 → 拒绝:增加使用成本,每个页面都要实现删除逻辑
决策 6:样式调整(紧凑列表)
选择:修改 v2 的卡片布局,减少卡片间距和内边距
调整细节:
.bill-card {
margin-top: 6px; // 原 10px
padding: 10px 12px; // 原 var(--spacing-xl) (约 16px)
gap: 10px; // 原 14px
}
理由:
- v2 原始设计间距较大,适合日历单日视图
- 列表视图需要更紧凑以显示更多条目
- 保留 v2 的视觉元素(图标、标签、颜色)
Risks / Trade-offs
风险 1:迁移期间功能遗漏
风险:旧版组件可能有未文档化的特殊用法,迁移时遗漏
缓解措施:
- 迁移前全面梳理旧版所有 props 和 emits
- 保留
showCheckbox和selectedIds功能(TransactionsRecord 批量操作依赖) - 迁移分阶段进行,逐个页面验证
风险 2:性能退化
风险:新增筛选、排序逻辑可能影响渲染性能
缓解措施:
- 使用
computed缓存筛选结果 - 大数据量时依赖后端 API 筛选(而非前端过滤)
- 测试场景:1000+ 条数据的滚动流畅度
风险 3:样式兼容性
风险:不同页面的主题色、暗黑模式可能导致显示异常
缓解措施:
- 使用 CSS 变量(
var(--van-danger-color)等),自动适配主题 - 测试暗黑模式下的视觉效果
- 提供
themeOverrideprop 允许父组件覆盖样式
Trade-off:组件复杂度 vs 易用性
权衡:高内聚设计会增加组件内部复杂度(300+ 行代码)
选择:接受复杂度换取易用性
理由:
- 简化所有使用方的代码(10+ 处引用)
- 统一维护点,避免分散的重复逻辑
- 内部复杂度可通过单元测试覆盖
Migration Plan
阶段 1:组件开发(第 1-2 天)
- 创建
Web/src/components/Bill/BillListComponent.vue - 实现核心功能:数据展示、分页、左滑删除
- 实现筛选、排序 UI 和逻辑
- 单元测试覆盖(Vue Test Utils)
阶段 2:试点迁移(第 3 天)
- 选择一个简单页面试点(如
BillAnalysisView.vue) - 替换旧组件为新组件
- 验证功能完整性和视觉效果
- 修复发现的问题
阶段 3:全面迁移(第 4-5 天)
- 迁移
TransactionsRecord.vue(重点验证 checkbox 功能) - 迁移其他引用旧组件的页面
- 回归测试所有相关页面
阶段 4:清理(第 6 天)
- 删除旧版
Web/src/components/TransactionList.vue - 删除
Web/src/views/calendarV2/modules/TransactionList.vue(如不再需要) - 更新文档和 AGENTS.md
Rollback 策略
- 保留旧版组件直到所有页面迁移完成
- 使用 Git 分支隔离迁移工作
- 如发现严重问题,可快速恢复旧版(修改 import 路径)
Open Questions
-
calendarV2 的 TransactionList 是否有特殊逻辑?
需要确认 calendarV2 是否依赖其特有功能(如 Smart 按钮、日期联动)。如果有,可能需要保留该文件,仅迁移其他页面。 -
是否需要支持自定义列渲染?
当前设计固定显示字段(reason, amount, classify 等)。未来是否需要 slot 支持自定义?暂时不实现,等实际需求再扩展。 -
筛选条件的持久化?
用户设置的筛选条件是否需要保存到 localStorage?当前设计不持久化,每次刷新恢复默认。