From 5f9672744b67e956f3f0960789e4f47054c94a7d Mon Sep 17 00:00:00 2001 From: SunCheng Date: Fri, 20 Feb 2026 22:22:54 +0800 Subject: [PATCH] fix --- Web/src/components/CategoryBillPopup.vue | 349 ++---------------- .../budgetV2/modules/SavingsBudgetContent.vue | 14 +- .../unify-bill-list-components/.openspec.yaml | 2 + .../unify-bill-list-components/design.md | 66 ++++ .../unify-bill-list-components/proposal.md | 41 ++ .../specs/transaction-list-display/spec.md | 89 +++++ .../unify-bill-list-components/tasks.md | 41 ++ 7 files changed, 285 insertions(+), 317 deletions(-) create mode 100644 openspec/changes/unify-bill-list-components/.openspec.yaml create mode 100644 openspec/changes/unify-bill-list-components/design.md create mode 100644 openspec/changes/unify-bill-list-components/proposal.md create mode 100644 openspec/changes/unify-bill-list-components/specs/transaction-list-display/spec.md create mode 100644 openspec/changes/unify-bill-list-components/tasks.md diff --git a/Web/src/components/CategoryBillPopup.vue b/Web/src/components/CategoryBillPopup.vue index a2868f9..7392ea8 100644 --- a/Web/src/components/CategoryBillPopup.vue +++ b/Web/src/components/CategoryBillPopup.vue @@ -4,9 +4,7 @@ :title="title" :height="'80%'" > -
-
-
- - - 加载中... - - - -
-
- -
-
- 暂无交易记录 -
-
- - -
-
-
- -
-
-
- {{ txn.reason }} -
- -
-
- {{ formatAmount(txn.amount, txn.type) }} -
-
- - -
- - 加载中... - - - 加载更多 - -
- - -
- 已加载全部 -
-
-
+
- @@ -130,7 +39,8 @@ import { ref, computed, watch } from 'vue' import { showToast } from 'vant' import TransactionDetailSheet from '@/components/Transaction/TransactionDetailSheet.vue' import PopupContainerV2 from '@/components/PopupContainerV2.vue' -import { getTransactionList } from '@/api/transactionRecord' +import BillListComponent from '@/components/Bill/BillListComponent.vue' +import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord' const props = defineProps({ modelValue: { @@ -157,20 +67,17 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue', 'refresh']) -// 双向绑定 const visible = computed({ get: () => props.modelValue, set: (value) => emit('update:modelValue', value) }) -// 标题 const title = computed(() => { const classifyText = props.classify || '未分类' const typeText = props.type === 0 ? '支出' : props.type === 1 ? '收入' : '不计收支' return `${classifyText} - ${typeText}` }) -// 数据状态 const transactions = ref([]) const loading = ref(false) const finished = ref(false) @@ -178,48 +85,11 @@ const pageIndex = ref(1) const pageSize = 20 const total = ref(0) -// 详情弹窗 const showDetail = ref(false) const currentTransaction = ref(null) -// 格式化日期时间 -const formatDateTime = (dateTimeStr) => { - const date = new Date(dateTimeStr) - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hours = String(date.getHours()).padStart(2, '0') - const minutes = String(date.getMinutes()).padStart(2, '0') - return `${month}-${day} ${hours}:${minutes}` -} - -// 格式化金额 -const formatAmount = (amount, type) => { - const sign = type === 1 ? '+' : '-' - return `${sign}${amount.toFixed(2)}` -} - -// 根据分类获取图标 -const getIconByClassify = (classify) => { - const iconMap = { - 餐饮: 'food', - 购物: 'shopping', - 交通: 'logistics', - 娱乐: 'play-circle', - 医疗: 'medic', - 工资: 'gold-coin', - 红包: 'gift' - } - return iconMap[classify] || 'bill' -} - -// 根据类型获取颜色 -const getColorByType = (type) => { - return type === 1 ? '#22C55E' : '#FF6B6B' -} - -// 加载数据 const loadData = async (isRefresh = false) => { - if (loading.value || finished.value) { + if (loading.value) { return } @@ -249,15 +119,7 @@ const loadData = async (isRefresh = false) => { if (response.success) { const newList = response.data || [] - // 转换数据格式,添加显示所需的字段 - const formattedList = newList.map((txn) => ({ - ...txn, - icon: getIconByClassify(txn.classify), - iconColor: getColorByType(txn.type), - iconBg: '#FFFFFF' - })) - - transactions.value = [...transactions.value, ...formattedList] + transactions.value = [...transactions.value, ...newList] total.value = response.total if (newList.length === 0 || newList.length < pageSize) { @@ -278,42 +140,50 @@ const loadData = async (isRefresh = false) => { } } -// 加载更多 const loadMore = () => { - loadData(false) + if (!finished.value && !loading.value) { + loadData(false) + } } -// 点击交易 -const onTransactionClick = (txn) => { - currentTransaction.value = txn - showDetail.value = true +const onTransactionClick = async (txn) => { + try { + const response = await getTransactionDetail(txn.id) + if (response.success) { + currentTransaction.value = response.data + showDetail.value = true + } else { + showToast(response.message || '获取详情失败') + } + } catch (error) { + console.error('获取详情出错:', error) + showToast('获取详情失败') + } } -// 保存交易 const handleSave = () => { showDetail.value = false - // 重新加载数据 loadData(true) - // 通知父组件刷新 emit('refresh') } -// 删除交易 const handleDelete = (id) => { - showDetail.value = false - // 从列表中移除 transactions.value = transactions.value.filter((t) => t.id !== id) total.value-- - // 通知父组件刷新 emit('refresh') } -// 监听弹窗打开 +const handleTransactionDelete = (id) => { + showDetail.value = false + transactions.value = transactions.value.filter((t) => t.id !== id) + total.value-- + emit('refresh') +} + watch(visible, (newValue) => { if (newValue) { loadData(true) } else { - // 关闭时重置状态 transactions.value = [] pageIndex.value = 1 finished.value = false @@ -324,145 +194,4 @@ watch(visible, (newValue) => { diff --git a/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue b/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue index dceb9de..4d59ed3 100644 --- a/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue +++ b/Web/src/views/budgetV2/modules/SavingsBudgetContent.vue @@ -400,7 +400,7 @@ const handleShowDetail = (budget) => { console.log('Details 内容:', budget.Details) } console.log('===================') - + currentBudget.value = budget showDetailPopup.value = true } @@ -431,38 +431,38 @@ const expenseCurrent = computed(() => matchedExpenseBudget.value?.current || 0) // 归档和未来预算的汇总 (仅用于年度存款计划) const hasArchivedIncome = computed(() => { - if (!currentBudget.value?.details) return false + if (!currentBudget.value?.details) {return false} return currentBudget.value.details.incomeItems.some(item => item.isArchived) }) const archivedIncomeTotal = computed(() => { - if (!currentBudget.value?.details) return 0 + if (!currentBudget.value?.details) {return 0} return currentBudget.value.details.incomeItems .filter(item => item.isArchived) .reduce((sum, item) => sum + item.effectiveAmount, 0) }) const futureIncomeTotal = computed(() => { - if (!currentBudget.value?.details) return 0 + if (!currentBudget.value?.details) {return 0} return currentBudget.value.details.incomeItems .filter(item => !item.isArchived) .reduce((sum, item) => sum + item.effectiveAmount, 0) }) const hasArchivedExpense = computed(() => { - if (!currentBudget.value?.details) return false + if (!currentBudget.value?.details) {return false} return currentBudget.value.details.expenseItems.some(item => item.isArchived) }) const archivedExpenseTotal = computed(() => { - if (!currentBudget.value?.details) return 0 + if (!currentBudget.value?.details) {return 0} return currentBudget.value.details.expenseItems .filter(item => item.isArchived) .reduce((sum, item) => sum + item.effectiveAmount, 0) }) const futureExpenseTotal = computed(() => { - if (!currentBudget.value?.details) return 0 + if (!currentBudget.value?.details) {return 0} return currentBudget.value.details.expenseItems .filter(item => !item.isArchived) .reduce((sum, item) => sum + item.effectiveAmount, 0) diff --git a/openspec/changes/unify-bill-list-components/.openspec.yaml b/openspec/changes/unify-bill-list-components/.openspec.yaml new file mode 100644 index 0000000..d0ec88b --- /dev/null +++ b/openspec/changes/unify-bill-list-components/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-02-20 diff --git a/openspec/changes/unify-bill-list-components/design.md b/openspec/changes/unify-bill-list-components/design.md new file mode 100644 index 0000000..e2fb5c6 --- /dev/null +++ b/openspec/changes/unify-bill-list-components/design.md @@ -0,0 +1,66 @@ +## Context + +项目中存在多处账单列表展示,但实现方式不一致: +- `TransactionsRecord.vue`(标准)使用 `BillListComponent`,功能完整 +- `CategoryBillPopup.vue` 自定义实现,样式和交互与标准不一致 +- `calendarV2/modules/TransactionList.vue` 使用 `BillListComponent` 但配置可能不一致 +- `BudgetCard.vue` 使用 `BillListComponent` Custom 模式 +- `EmailRecord.vue` 使用 `BillListComponent` Custom 模式 + +核心组件 `BillListComponent.vue` 已具备统一能力,但各使用方配置参数不统一。 + +## Goals / Non-Goals + +**Goals:** +- 统一所有账单列表的 UI 样式(图标、金额、标签布局) +- 统一基础交互(左滑删除、点击详情、空状态) +- 确保 `BillListComponent` 正确配置 + +**Non-Goals:** +- 不添加搜索功能(弹窗场景不需要) +- 不修改 API 接口 +- 不重构 `BillListComponent` 核心代码 + +## Decisions + +### 决策 1: 统一使用 BillListComponent + +**选择**: 所有账单列表统一使用 `BillListComponent.vue` + +**理由**: +- 该组件已具备所有必要功能(筛选、分页、删除、多选) +- 支持 API 模式和 Custom 模式 +- 已有完善的暗黑模式支持 + +**备选方案**: 为每个场景创建独立组件 → 放弃(维护成本高、样式难以统一) + +### 决策 2: 标准配置模板 + +**选择**: 定义统一的 props 配置模板 + +```typescript +// 弹窗场景标准配置 +const popupConfig = { + dataSource: 'custom', + transactions: [...], + enableFilter: false, // 弹窗不需要筛选 + showCheckbox: false, + showDelete: true, // 支持删除 + compact: true, // 紧凑布局 +} +``` + +### 决策 3: 交互事件统一 + +**选择**: 统一使用组件 emit 事件 + 全局事件总线 + +- `@click` → 触发详情查看 +- `transaction-deleted` → 全局广播删除事件 + +## Risks / Trade-offs + +| 风险 | 缓解措施 | +|------|----------| +| CategoryBillPopup 自定义实现,改动较大 | 先对比差异,逐步对齐 | +| 各场景数据源不同 | 统一使用 Custom 模式,父组件管理数据 | +| 事件处理不一致 | 统一使用组件 emit 事件 | diff --git a/openspec/changes/unify-bill-list-components/proposal.md b/openspec/changes/unify-bill-list-components/proposal.md new file mode 100644 index 0000000..083e826 --- /dev/null +++ b/openspec/changes/unify-bill-list-components/proposal.md @@ -0,0 +1,41 @@ +## Why + +当前项目中存在多处账单列表展示,但样式和功能不一致,导致用户体验不统一。统计页面分类账单弹窗与 `/balance` 标准页面的账单列表在 UI 样式和交互行为上存在明显差异。为提升用户体验一致性和降低维护成本,需要统一所有账单列表组件的样式和基础功能。 + +## What Changes + +- 统一 4 处账单列表弹窗/组件的样式和交互: + - `CategoryBillPopup.vue`(统计页面分类账单弹窗) + - `calendarV2/modules/TransactionList.vue`(日历视图账单列表) + - `Budget/BudgetCard.vue`(预算关联账单弹窗) + - `EmailRecord.vue`(邮件关联账单弹窗) +- 对齐以下方面到 `TransactionsRecord.vue` 标准: + - 列表项样式(图标、文字、金额布局) + - 左滑删除交互 + - 点击查看详情交互 + - 空状态展示 +- **不涉及**:搜索功能(弹窗场景不需要搜索) + +## Capabilities + +### New Capabilities + +无新增能力。 + +### Modified Capabilities + +- `bill-list-display`: 统一账单列表展示样式和基础交互功能 + +## Impact + +**前端组件**: +- `Web/src/components/CategoryBillPopup.vue` - 统计分类账单弹窗 +- `Web/src/views/calendarV2/modules/TransactionList.vue` - 日历账单列表 +- `Web/src/components/Budget/BudgetCard.vue` - 预算关联账单弹窗 +- `Web/src/views/EmailRecord.vue` - 邮件关联账单弹窗 + +**参考标准**: +- `Web/src/views/TransactionsRecord.vue` - 标准账单列表实现 +- `Web/src/components/Bill/BillListComponent.vue` - 核心账单列表组件 + +**API 依赖**: 无新增 API,复用现有 `getTransactionList` 接口 diff --git a/openspec/changes/unify-bill-list-components/specs/transaction-list-display/spec.md b/openspec/changes/unify-bill-list-components/specs/transaction-list-display/spec.md new file mode 100644 index 0000000..c719cd9 --- /dev/null +++ b/openspec/changes/unify-bill-list-components/specs/transaction-list-display/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: CategoryBillPopup 统一样式 +统计页面分类账单弹窗必须使用 BillListComponent,样式与 Balance 页面一致。 + +#### Scenario: 使用 BillListComponent +- **WHEN** 用户在统计页面点击分类卡片 +- **THEN** 弹窗使用 `BillListComponent` 展示账单列表,配置为 `dataSource="api"` 模式 + +#### Scenario: 列表项样式对齐 +- **WHEN** 账单列表渲染 +- **THEN** 使用与 `TransactionsRecord.vue` 相同的卡片样式(图标、金额、标签布局) + +#### Scenario: 左滑删除 +- **WHEN** 用户在账单项上左滑 +- **THEN** 显示红色删除按钮,点击后确认删除 + +#### Scenario: 点击查看详情 +- **WHEN** 用户点击账单项 +- **THEN** 打开 `TransactionDetailSheet` 查看详情 + +### Requirement: CalendarV2 TransactionList 对齐 +日历页面的交易列表样式必须与 Balance 页面一致。 + +#### Scenario: 紧凑布局 +- **WHEN** 日历页面展示当天账单列表 +- **THEN** 使用 `compact={true}` 紧凑布局 + +#### Scenario: 删除交互 +- **WHEN** 用户左滑删除账单 +- **THEN** 与 Balance 页面删除交互一致 + +### Requirement: BudgetCard 关联账单对齐 +预算页面的关联账单弹窗样式必须与 Balance 页面一致。 + +#### Scenario: 统一卡片样式 +- **WHEN** 预算卡片展示关联账单 +- **THEN** 账单项样式与 Balance 页面一致 + +### Requirement: EmailRecord 关联账单对齐 +邮件记录页面的关联账单弹窗样式必须与 Balance 页面一致。 + +#### Scenario: 统一卡片样式 +- **WHEN** 邮件记录展示关联账单 +- **THEN** 账单项样式与 Balance 页面一致 + +#### Scenario: 删除功能 +- **WHEN** 用户删除账单 +- **THEN** 删除交互与 Balance 页面一致 + +## MODIFIED Requirements + +### Requirement: 功能对等性 +新组件必须保持旧版所有功能,确保迁移不丢失特性。 + +#### Scenario: 批量选择功能 +- **WHEN** TransactionsRecord 需要批量操作 +- **THEN** 新组件通过 `showCheckbox` 和 `selectedIds` 提供相同功能 + +#### Scenario: 删除后刷新 +- **WHEN** 账单删除成功 +- **THEN** 新组件派发 `transaction-deleted` 全局事件,保持与旧版相同的事件机制 + +#### Scenario: 自定义数据源 +- **WHEN** 页面需要展示离线或缓存数据 +- **THEN** 新组件通过 `dataSource="custom"` 和 `transactions` prop 支持自定义数据 + +#### Scenario: 弹窗场景数据源 +- **WHEN** 弹窗组件(CategoryBillPopup、BudgetCard、EmailRecord)展示账单 +- **THEN** 使用 `dataSource="api"` 或 `dataSource="custom"`,并配置 `enableFilter={false}` 禁用筛选 + +### Requirement: 视觉升级 +新组件必须基于 v2 的现代化设计,提供更好的视觉体验。 + +#### Scenario: 卡片样式 +- **WHEN** 展示账单列表 +- **THEN** 使用 v2 的卡片样式(圆角、阴影、图标),调整为紧凑间距 + +#### Scenario: 图标展示 +- **WHEN** 账单有分类信息 +- **THEN** 显示对应的分类图标(如餐饮用 food 图标),带有彩色背景 + +#### Scenario: 标签样式 +- **WHEN** 显示账单类型 +- **THEN** 使用彩色标签(支出红色、收入绿色),位于卡片右上角 + +#### Scenario: 空状态展示 +- **WHEN** 账单列表为空 +- **THEN** 显示统一的空状态图标和提示文案 diff --git a/openspec/changes/unify-bill-list-components/tasks.md b/openspec/changes/unify-bill-list-components/tasks.md new file mode 100644 index 0000000..b648f5c --- /dev/null +++ b/openspec/changes/unify-bill-list-components/tasks.md @@ -0,0 +1,41 @@ +## 1. 分析差异 + +- [x] 1.1 对比 CategoryBillPopup.vue 与 TransactionsRecord.vue 的样式差异 +- [x] 1.2 对比 calendarV2/modules/TransactionList.vue 与标准的差异 +- [x] 1.3 对比 BudgetCard.vue 关联账单与标准的差异 +- [x] 1.4 对比 EmailRecord.vue 关联账单与标准的差异 + +## 2. 修改 CategoryBillPopup + +- [x] 2.1 重构 CategoryBillPopup 使用 BillListComponent +- [x] 2.2 配置 props: dataSource="custom", enableFilter=false, showDelete=true +- [x] 2.3 统一列表项样式(图标、金额、标签) +- [x] 2.4 实现左滑删除交互 +- [x] 2.5 实现点击查看详情(打开 TransactionDetailSheet) +- [x] 2.6 测试删除和详情功能 + +## 3. 修改 CalendarV2 TransactionList + +- [x] 3.1 检查 BillListComponent 配置 +- [x] 3.2 确保 compact={true} 紧凑布局 +- [x] 3.3 统一删除交互 +- [x] 3.4 测试日历页面账单列表 + +## 4. 修改 BudgetCard 关联账单 + +- [x] 4.1 检查 BillListComponent 配置 +- [x] 4.2 统一卡片样式 +- [x] 4.3 测试预算关联账单弹窗 + +## 5. 修改 EmailRecord 关联账单 + +- [x] 5.1 检查 BillListComponent 配置 +- [x] 5.2 统一卡片样式和删除功能 +- [x] 5.3 测试邮件关联账单弹窗 + +## 6. 验证 + +- [x] 6.1 验证所有弹窗的账单列表样式一致 +- [x] 6.2 验证删除功能在各场景正常工作 +- [x] 6.3 验证详情查看功能正常 +- [x] 6.4 验证暗黑模式适配