Files
EmailBill/openspec/changes/refactor-bill-list-component/design.md
SunCheng a88556c784 fix
2026-02-15 10:10:28 +08:00

8.7 KiB
Raw Blame History

Context

当前 EmailBill 项目中存在两个 TransactionList.vue 实现:

  1. 旧版Web/src/components/TransactionList.vue):传统一行一卡片布局,功能完整但样式较旧
  2. 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.vueManualBillAdd.vue 等保持一致
  • 命名为 BillListComponent 而非 TransactionList,避免与旧组件混淆
  • 符合项目 BEM 命名规范

备选方案

  • 直接覆盖旧版 TransactionList.vue拒绝:迁移期间需要并存,且容易引起冲突

决策 2Props 设计(高内聚 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
  • 保留 showCheckboxselectedIds 功能TransactionsRecord 批量操作依赖)
  • 迁移分阶段进行,逐个页面验证

风险 2性能退化

风险:新增筛选、排序逻辑可能影响渲染性能
缓解措施

  • 使用 computed 缓存筛选结果
  • 大数据量时依赖后端 API 筛选(而非前端过滤)
  • 测试场景1000+ 条数据的滚动流畅度

风险 3样式兼容性

风险:不同页面的主题色、暗黑模式可能导致显示异常
缓解措施

  • 使用 CSS 变量(var(--van-danger-color) 等),自动适配主题
  • 测试暗黑模式下的视觉效果
  • 提供 themeOverride prop 允许父组件覆盖样式

Trade-off组件复杂度 vs 易用性

权衡高内聚设计会增加组件内部复杂度300+ 行代码)
选择:接受复杂度换取易用性
理由

  • 简化所有使用方的代码10+ 处引用)
  • 统一维护点,避免分散的重复逻辑
  • 内部复杂度可通过单元测试覆盖

Migration Plan

阶段 1组件开发第 1-2 天)

  1. 创建 Web/src/components/Bill/BillListComponent.vue
  2. 实现核心功能:数据展示、分页、左滑删除
  3. 实现筛选、排序 UI 和逻辑
  4. 单元测试覆盖Vue Test Utils

阶段 2试点迁移第 3 天)

  1. 选择一个简单页面试点(如 BillAnalysisView.vue
  2. 替换旧组件为新组件
  3. 验证功能完整性和视觉效果
  4. 修复发现的问题

阶段 3全面迁移第 4-5 天)

  1. 迁移 TransactionsRecord.vue(重点验证 checkbox 功能)
  2. 迁移其他引用旧组件的页面
  3. 回归测试所有相关页面

阶段 4清理第 6 天)

  1. 删除旧版 Web/src/components/TransactionList.vue
  2. 删除 Web/src/views/calendarV2/modules/TransactionList.vue(如不再需要)
  3. 更新文档和 AGENTS.md

Rollback 策略

  • 保留旧版组件直到所有页面迁移完成
  • 使用 Git 分支隔离迁移工作
  • 如发现严重问题,可快速恢复旧版(修改 import 路径)

Open Questions

  1. calendarV2 的 TransactionList 是否有特殊逻辑?
    需要确认 calendarV2 是否依赖其特有功能(如 Smart 按钮、日期联动)。如果有,可能需要保留该文件,仅迁移其他页面。

  2. 是否需要支持自定义列渲染?
    当前设计固定显示字段reason, amount, classify 等)。未来是否需要 slot 支持自定义?暂时不实现,等实际需求再扩展。

  3. 筛选条件的持久化?
    用户设置的筛选条件是否需要保存到 localStorage当前设计不持久化每次刷新恢复默认。