Files
EmailBill/openspec/changes/archive/2026-02-15-refactor-bill-list-component/design.md

261 lines
8.7 KiB
Markdown
Raw Normal View History

2026-02-15 10:10:28 +08:00
## 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.vue``ManualBillAdd.vue` 等保持一致
- 命名为 `BillListComponent` 而非 `TransactionList`,避免与旧组件混淆
- 符合项目 BEM 命名规范
**备选方案**
- 直接覆盖旧版 `TransactionList.vue`**拒绝**:迁移期间需要并存,且容易引起冲突
### 决策 2Props 设计(高内聚 vs 灵活配置)
**选择****高内聚** - 组件内部管理筛选、排序、分页状态
**Props 定义**
```javascript
// 使用 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 事件并派发全局事件
**流程**
```javascript
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 的卡片布局,减少卡片间距和内边距
**调整细节**
```scss
.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)` 等),自动适配主题
- 测试暗黑模式下的视觉效果
- 提供 `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当前设计不持久化每次刷新恢复默认。