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:
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
<!--
|
||||
CalendarV2 专用的交易列表组件
|
||||
|
||||
特殊功能:
|
||||
- 自定义 header(Items 数量、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 {
|
||||
|
||||
Reference in New Issue
Block a user