refactor: 统一账单列表组件,封装 BillListComponent

- 创建 BillListComponent 组件(基于 v2 风格,紧凑布局)
  - 支持筛选(类型、分类、日期范围)和排序(金额、时间)
  - 支持分页加载、左滑删除、点击详情、多选模式
  - 支持 API 自动加载和 Custom 自定义数据两种模式
- 迁移 6 个页面/组件到新组件:
  - TransactionsRecord.vue
  - EmailRecord.vue
  - ClassificationNLP.vue
  - UnconfirmedClassification.vue
  - BudgetCard.vue
  - ReasonGroupList.vue
- 删除旧版 TransactionList 组件
- 保留 CalendarV2 的特殊版本(有专用功能)
- 添加完整的使用文档和 JSDoc 注释
This commit is contained in:
SunCheng
2026-02-15 10:08:14 +08:00
parent 6f725dbb13
commit e51a3edd50
11 changed files with 1171 additions and 441 deletions

View File

@@ -4,9 +4,11 @@
**Parent:** EmailBill/AGENTS.md
## OVERVIEW
Vue 3 views using Composition API with Vant UI components for mobile-first budget tracking.
## STRUCTURE
```
Web/src/views/
├── BudgetView.vue # Main budget management
@@ -27,25 +29,36 @@ Web/src/views/
```
## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| Budget management | BudgetView.vue | Main budget interface |
| Transactions | TransactionsRecord.vue | CRUD operations |
| Statistics | StatisticsView.vue | Charts, analytics |
| Classification | Classification* | Transaction categorization |
| Authentication | LoginView.vue | User login flow |
| Settings | SettingView.vue | App configuration |
| Email features | EmailRecord.vue | Email integration |
| Task | Location | Notes |
| ----------------- | ---------------------- | -------------------------- |
| Budget management | BudgetView.vue | Main budget interface |
| Transactions | TransactionsRecord.vue | CRUD operations |
| Statistics | StatisticsView.vue | Charts, analytics |
| Classification | Classification\* | Transaction categorization |
| Authentication | LoginView.vue | User login flow |
| Settings | SettingView.vue | App configuration |
| Email features | EmailRecord.vue | Email integration |
## CONVENTIONS
- Vue 3 Composition API with `<script setup lang="ts">`
- Vue 3 Composition API with `<script setup>` (JavaScript)
- Vant UI components: `<van-*>`
- Mobile-first responsive design
- SCSS with BEM naming convention
- Pinia for state management
- Vue Router for navigation
## REUSABLE COMPONENTS
**BillListComponent** (`@/components/Bill/BillListComponent.vue`)
- **用途**: 统一的账单列表组件,替代旧版 TransactionList
- **特性**: 支持筛选、排序、分页、左滑删除、多选
- **数据模式**: API 模式(自动加载)或 Custom 模式(父组件传入数据)
- **文档**: 参见 `.doc/BillListComponent-usage.md`
## ANTI-PATTERNS (THIS LAYER)
- Never use Options API (always Composition API)
- Don't access APIs directly (use api/ modules)
- Avoid inline styles (use SCSS modules)
@@ -53,8 +66,9 @@ Web/src/views/
- Don't mutate props directly
## UNIQUE STYLES
- Chinese interface labels for business concepts
- Mobile-optimized layouts with Vant components
- Integration with backend API via api/ modules
- Real-time data updates via Pinia stores
- Gesture interactions for mobile users
- Gesture interactions for mobile users

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="page-container-flex classification-nlp">
<van-nav-bar
title="自然语言分类"
@@ -108,13 +108,15 @@
<!-- 交易记录列表 -->
<div class="records-list">
<TransactionList
<BillListComponent
data-source="custom"
:transactions="displayRecords"
:loading="false"
:finished="true"
:show-checkbox="true"
:selected-ids="selectedIds"
:show-delete="false"
:enable-filter="false"
@update:selected-ids="updateSelectedIds"
@click="handleRecordClick"
/>
@@ -129,7 +131,7 @@ import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import { nlpAnalysis, batchUpdateClassify } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import BillListComponent from '@/components/Bill/BillListComponent.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
import PopupContainer from '@/components/PopupContainer.vue'

View File

@@ -1,4 +1,4 @@
<!-- eslint-disable vue/no-v-html -->
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="page-container-flex">
<!-- 下拉刷新区域 -->
@@ -148,11 +148,13 @@
title="关联账单列表"
height="75%"
>
<TransactionList
<BillListComponent
data-source="custom"
:transactions="transactionList"
:loading="false"
:finished="true"
:show-delete="true"
:enable-filter="false"
@click="handleTransactionClick"
@delete="handleTransactionDelete"
/>
@@ -180,7 +182,7 @@ import {
getEmailTransactions
} from '@/api/emailRecord'
import { getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import BillListComponent from '@/components/Bill/BillListComponent.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
import PopupContainer from '@/components/PopupContainer.vue'

View File

@@ -26,19 +26,16 @@
</van-loading>
<!-- 交易记录列表 -->
<TransactionList
<BillListComponent
data-source="custom"
:transactions="transactionList"
:loading="loading"
:finished="finished"
:show-delete="true"
:enable-filter="false"
@load="onLoad"
@click="viewDetail"
@delete="
(id) => {
// 从当前的交易列表中移除该交易
transactionList.value = transactionList.value.filter((t) => t.id !== id)
}
"
@delete="handleDelete"
/>
<!-- 底部安全距离 -->
@@ -58,7 +55,7 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { showToast } from 'vant'
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
import TransactionList from '@/components/TransactionList.vue'
import BillListComponent from '@/components/Bill/BillListComponent.vue'
import TransactionDetail from '@/components/TransactionDetail.vue'
const transactionList = ref([])
@@ -183,7 +180,13 @@ const onDetailSave = async () => {
loadData(true)
}
// 删除功能由 TransactionList 组件内部处理,组件通过 :show-delete 启用
// 处理删除事件
const handleDelete = (id) => {
// 从当前的交易列表中移除该交易
transactionList.value = transactionList.value.filter((t) => t.id !== id)
}
// 删除功能由 BillListComponent 组件内部处理,组件通过 :show-delete 启用
onMounted(async () => {
// 不需要手动调用 loadDatavan-list 会自动触发 onLoad

View File

@@ -74,11 +74,13 @@
</div>
</template>
<TransactionList
:transactions="classifyNode.children.map(c => c.transaction)"
<BillListComponent
data-source="custom"
:transactions="classifyNode.children.map((c) => c.transaction)"
:show-delete="false"
:show-checkbox="true"
:selected-ids="selectedIds"
:enable-filter="false"
@click="handleTransactionClick"
@update:selected-ids="handleUpdateSelectedIds"
/>
@@ -103,7 +105,7 @@ import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant'
import { getUnconfirmedTransactionList, confirmAllUnconfirmed } from '@/api/transactionRecord'
import TransactionDetail from '@/components/TransactionDetail.vue'
import TransactionList from '@/components/TransactionList.vue'
import BillListComponent from '@/components/Bill/BillListComponent.vue'
const router = useRouter()
const loading = ref(false)
@@ -154,9 +156,13 @@ const handleConfirmSelected = async () => {
}
const formatAmount = (amount) => {
if (amount === null || amount === undefined) {return ''}
if (amount === null || amount === undefined) {
return ''
}
const num = parseFloat(amount)
if (isNaN(num)) {return ''}
if (isNaN(num)) {
return ''
}
return num.toFixed(2)
}
@@ -321,7 +327,7 @@ onMounted(() => {
.classify-collapse :deep(.van-cell-group--inset) {
margin-left: -24px;
width: calc(100vw - 48px)
width: calc(100vw - 48px);
}
:deep(.van-nav-bar) {

View File

@@ -1,3 +1,14 @@
<!--
CalendarV2 专用的交易列表组件
特殊功能
- 自定义 headerItems 数量Smart 按钮
- 与日历视图紧密集成
- 特定的 UI 风格和交互
注意此组件不是通用的 BillListComponent专为 CalendarV2 视图设计
如需通用账单列表功能请使用 @/components/Bill/BillListComponent.vue
-->
<template>
<!-- 交易列表 -->
<div class="transactions">
@@ -128,13 +139,13 @@ const formatAmount = (amount, type) => {
// 根据分类获取图标
const getIconByClassify = (classify) => {
const iconMap = {
'餐饮': 'food',
'购物': 'shopping',
'交通': 'transport',
'娱乐': 'play',
'医疗': 'medical',
'工资': 'money',
'红包': 'red-packet'
餐饮: 'food',
购物: 'shopping',
交通: 'transport',
娱乐: 'play',
医疗: 'medical',
工资: 'money',
红包: 'red-packet'
}
return iconMap[classify] || 'star'
}
@@ -153,7 +164,7 @@ const fetchDayTransactions = async (date) => {
if (response.success && response.data) {
// 转换为界面需要的格式
transactions.value = response.data.map(txn => ({
transactions.value = response.data.map((txn) => ({
id: txn.id,
name: txn.reason || '未知交易',
time: formatTime(txn.occurredAt),
@@ -173,11 +184,15 @@ const fetchDayTransactions = async (date) => {
}
// 监听 selectedDate 变化,重新加载数据
watch(() => props.selectedDate, async (newDate) => {
if (newDate) {
await fetchDayTransactions(newDate)
}
}, { immediate: true })
watch(
() => props.selectedDate,
async (newDate) => {
if (newDate) {
await fetchDayTransactions(newDate)
}
},
{ immediate: true }
)
// 交易数量
const transactionCount = computed(() => transactions.value.length)
@@ -338,7 +353,7 @@ const onSmartClick = () => {
.txn-classify-tag.tag-expense {
background-color: rgba(59, 130, 246, 0.15);
color: #3B82F6;
color: #3b82f6;
}
.txn-amount {