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:
257
.doc/BillListComponent-usage.md
Normal file
257
.doc/BillListComponent-usage.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# BillListComponent 使用文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
`BillListComponent` 是一个高内聚的账单列表组件,基于 CalendarV2 风格设计,支持筛选、排序、分页、左滑删除、多选等功能。已替代项目中的旧版 `TransactionList` 组件。
|
||||||
|
|
||||||
|
**文件位置**: `Web/src/components/Bill/BillListComponent.vue`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### dataSource
|
||||||
|
- **类型**: `String`
|
||||||
|
- **默认值**: `'api'`
|
||||||
|
- **可选值**: `'api'` | `'custom'`
|
||||||
|
- **说明**: 数据源模式
|
||||||
|
- `'api'`: 组件内部调用 API 获取数据(支持分页、筛选)
|
||||||
|
- `'custom'`: 父组件传入数据(通过 `transactions` prop)
|
||||||
|
|
||||||
|
### apiParams
|
||||||
|
- **类型**: `Object`
|
||||||
|
- **默认值**: `{}`
|
||||||
|
- **说明**: API 模式下的筛选参数(仅 `dataSource='api'` 时有效)
|
||||||
|
- **属性**:
|
||||||
|
- `dateRange`: `[string, string]` - 日期范围,如 `['2026-01-01', '2026-01-31']`
|
||||||
|
- `category`: `String` - 分类筛选
|
||||||
|
- `type`: `0 | 1 | 2` - 类型筛选(0=支出, 1=收入, 2=不计入)
|
||||||
|
|
||||||
|
### transactions
|
||||||
|
- **类型**: `Array`
|
||||||
|
- **默认值**: `[]`
|
||||||
|
- **说明**: 自定义数据源(仅 `dataSource='custom'` 时有效)
|
||||||
|
|
||||||
|
### showDelete
|
||||||
|
- **类型**: `Boolean`
|
||||||
|
- **默认值**: `true`
|
||||||
|
- **说明**: 是否显示左滑删除功能
|
||||||
|
|
||||||
|
### showCheckbox
|
||||||
|
- **类型**: `Boolean`
|
||||||
|
- **默认值**: `false`
|
||||||
|
- **说明**: 是否显示多选复选框
|
||||||
|
|
||||||
|
### enableFilter
|
||||||
|
- **类型**: `Boolean`
|
||||||
|
- **默认值**: `true`
|
||||||
|
- **说明**: 是否启用筛选栏(类型、分类、日期、排序)
|
||||||
|
|
||||||
|
### enableSort
|
||||||
|
- **类型**: `Boolean`
|
||||||
|
- **默认值**: `true`
|
||||||
|
- **说明**: 是否启用排序功能(与 `enableFilter` 配合使用)
|
||||||
|
|
||||||
|
### compact
|
||||||
|
- **类型**: `Boolean`
|
||||||
|
- **默认值**: `true`
|
||||||
|
- **说明**: 是否使用紧凑模式(卡片间距 6px)
|
||||||
|
|
||||||
|
### selectedIds
|
||||||
|
- **类型**: `Set`
|
||||||
|
- **默认值**: `new Set()`
|
||||||
|
- **说明**: 已选中的账单 ID 集合(多选模式下)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Events
|
||||||
|
|
||||||
|
### @load
|
||||||
|
- **参数**: 无
|
||||||
|
- **说明**: 触发分页加载(API 模式下自动处理,Custom 模式可用于通知父组件)
|
||||||
|
|
||||||
|
### @click
|
||||||
|
- **参数**: `transaction` (Object) - 被点击的账单对象
|
||||||
|
- **说明**: 点击账单卡片时触发
|
||||||
|
|
||||||
|
### @delete
|
||||||
|
- **参数**: `id` (Number | String) - 被删除的账单 ID
|
||||||
|
- **说明**: 删除账单成功后触发
|
||||||
|
|
||||||
|
### @update:selectedIds
|
||||||
|
- **参数**: `ids` (Set) - 新的选中 ID 集合
|
||||||
|
- **说明**: 多选状态变更时触发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 示例 1: API 模式(组件自动加载数据)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<BillListComponent
|
||||||
|
data-source="api"
|
||||||
|
:api-params="{ type: 0, dateRange: ['2026-01-01', '2026-01-31'] }"
|
||||||
|
:show-delete="true"
|
||||||
|
:enable-filter="true"
|
||||||
|
@click="handleBillClick"
|
||||||
|
@delete="handleBillDelete"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import BillListComponent from '@/components/Bill/BillListComponent.vue'
|
||||||
|
|
||||||
|
const handleBillClick = (transaction) => {
|
||||||
|
console.log('点击账单:', transaction)
|
||||||
|
// 打开详情弹窗等
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBillDelete = (id) => {
|
||||||
|
console.log('删除账单:', id)
|
||||||
|
// 刷新统计数据等
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 2: Custom 模式(父组件管理数据)
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
|
:transactions="billList"
|
||||||
|
:loading="loading"
|
||||||
|
:finished="finished"
|
||||||
|
:show-delete="true"
|
||||||
|
:enable-filter="false"
|
||||||
|
@load="loadMore"
|
||||||
|
@click="viewDetail"
|
||||||
|
@delete="handleDelete"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import BillListComponent from '@/components/Bill/BillListComponent.vue'
|
||||||
|
import { getTransactionList } from '@/api/transactionRecord'
|
||||||
|
|
||||||
|
const billList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
|
||||||
|
const loadMore = async () => {
|
||||||
|
loading.value = true
|
||||||
|
const response = await getTransactionList({ pageIndex: 1, pageSize: 20 })
|
||||||
|
if (response.success) {
|
||||||
|
billList.value = response.data
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewDetail = (transaction) => {
|
||||||
|
// 查看详情
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
billList.value = billList.value.filter(t => t.id !== id)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例 3: 多选模式
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
|
:transactions="billList"
|
||||||
|
:show-checkbox="true"
|
||||||
|
:selected-ids="selectedIds"
|
||||||
|
:show-delete="false"
|
||||||
|
:enable-filter="false"
|
||||||
|
@update:selected-ids="selectedIds = $event"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div v-if="selectedIds.size > 0">
|
||||||
|
已选中 {{ selectedIds.size }} 条
|
||||||
|
<van-button @click="batchDelete">批量删除</van-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import BillListComponent from '@/components/Bill/BillListComponent.vue'
|
||||||
|
|
||||||
|
const billList = ref([...])
|
||||||
|
const selectedIds = ref(new Set())
|
||||||
|
|
||||||
|
const batchDelete = () => {
|
||||||
|
// 批量删除逻辑
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据源模式选择**:
|
||||||
|
- 如果需要自动分页、筛选,使用 `dataSource="api"`
|
||||||
|
- 如果需要自定义数据管理(如搜索、离线数据),使用 `dataSource="custom"`
|
||||||
|
|
||||||
|
2. **筛选功能**:
|
||||||
|
- `enableFilter` 控制筛选栏的显示/隐藏
|
||||||
|
- 筛选栏包括:类型、分类、日期范围、排序四个下拉菜单
|
||||||
|
- Custom 模式下,筛选仅在前端过滤,不会调用 API
|
||||||
|
|
||||||
|
3. **删除功能**:
|
||||||
|
- 组件内部自动调用 `deleteTransaction` API
|
||||||
|
- 删除成功后会派发全局事件 `transaction-deleted`
|
||||||
|
- 父组件通过 `@delete` 事件可执行额外逻辑
|
||||||
|
|
||||||
|
4. **多选功能**:
|
||||||
|
- 启用 `showCheckbox` 后,账单项左侧显示复选框
|
||||||
|
- 使用 `v-model:selectedIds` 或 `@update:selectedIds` 同步选中状态
|
||||||
|
|
||||||
|
5. **样式适配**:
|
||||||
|
- 组件自动适配暗黑模式(使用 CSS 变量)
|
||||||
|
- `compact` 模式适合列表视图,舒适模式适合详情查看
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 与旧版 TransactionList 的差异
|
||||||
|
|
||||||
|
| 特性 | 旧版 TransactionList | 新版 BillListComponent |
|
||||||
|
|------|---------------------|----------------------|
|
||||||
|
| 数据管理 | 仅支持 Custom 模式 | 支持 API + Custom 模式 |
|
||||||
|
| 筛选功能 | 无内置筛选 | 内置筛选栏(类型、分类、日期、排序) |
|
||||||
|
| 样式 | 一行一卡片,间距大 | 紧凑列表,间距 6px |
|
||||||
|
| 图标 | 无分类图标 | 显示分类图标和彩色背景 |
|
||||||
|
| Props 命名 | `show-delete` | `show-delete`(保持兼容) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- 组件实现: `Web/src/components/Bill/BillListComponent.vue`
|
||||||
|
- API 接口: `Web/src/api/transactionRecord.js`
|
||||||
|
- 设计文档: `openspec/changes/refactor-bill-list-component/design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: 如何禁用筛选栏?**
|
||||||
|
A: 设置 `:enable-filter="false"`
|
||||||
|
|
||||||
|
**Q: Custom 模式下分页如何实现?**
|
||||||
|
A: 父组件监听 `@load` 事件,追加数据到 `transactions` 数组,并控制 `loading` 和 `finished` 状态
|
||||||
|
|
||||||
|
**Q: 如何自定义卡片样式?**
|
||||||
|
A: 使用 `compact` prop 切换紧凑/舒适模式,或通过全局 CSS 变量覆盖样式
|
||||||
|
|
||||||
|
**Q: CalendarV2 为什么还有单独的 TransactionList?**
|
||||||
|
A: CalendarV2 的 TransactionList 有特定于日历视图的功能(Smart 按钮、特殊 UI),暂时保留
|
||||||
814
Web/src/components/Bill/BillListComponent.vue
Normal file
814
Web/src/components/Bill/BillListComponent.vue
Normal file
@@ -0,0 +1,814 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bill-list-component">
|
||||||
|
<!-- 4.1 筛选栏 UI -->
|
||||||
|
<div
|
||||||
|
v-if="enableFilter"
|
||||||
|
class="filter-bar"
|
||||||
|
>
|
||||||
|
<van-dropdown-menu active-color="#1989fa">
|
||||||
|
<!-- 4.2 类型筛选 -->
|
||||||
|
<van-dropdown-item
|
||||||
|
v-model="selectedType"
|
||||||
|
:options="typeOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 4.3 分类筛选 -->
|
||||||
|
<van-dropdown-item
|
||||||
|
v-model="selectedCategory"
|
||||||
|
:options="categoryOptions"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 4.4 日期范围筛选 -->
|
||||||
|
<van-dropdown-item
|
||||||
|
ref="dateDropdown"
|
||||||
|
title="日期"
|
||||||
|
>
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-cell
|
||||||
|
:title="dateRangeText"
|
||||||
|
is-link
|
||||||
|
@click="showCalendar = true"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
<div style="padding: 16px">
|
||||||
|
<van-button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
@click="closeDateDropdown"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-dropdown-item>
|
||||||
|
|
||||||
|
<!-- 4.5 排序功能 -->
|
||||||
|
<van-dropdown-item
|
||||||
|
v-model="sortBy"
|
||||||
|
:options="sortOptions"
|
||||||
|
/>
|
||||||
|
</van-dropdown-menu>
|
||||||
|
|
||||||
|
<!-- 4.6 重置按钮 -->
|
||||||
|
<van-button
|
||||||
|
size="small"
|
||||||
|
type="default"
|
||||||
|
style="margin-top: 8px"
|
||||||
|
@click="resetFilters"
|
||||||
|
>
|
||||||
|
重置筛选
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 4.4 日期选择弹出层 -->
|
||||||
|
<van-calendar
|
||||||
|
v-model:show="showCalendar"
|
||||||
|
type="range"
|
||||||
|
:min-date="new Date(2020, 0, 1)"
|
||||||
|
:max-date="new Date(2030, 11, 31)"
|
||||||
|
@confirm="onDateConfirm"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 账单列表 -->
|
||||||
|
<van-list
|
||||||
|
:loading="loading"
|
||||||
|
:finished="finished"
|
||||||
|
finished-text="没有更多了"
|
||||||
|
@load="onLoad"
|
||||||
|
>
|
||||||
|
<van-cell-group
|
||||||
|
v-if="displayTransactions && displayTransactions.length"
|
||||||
|
inset
|
||||||
|
style="margin-top: 10px"
|
||||||
|
>
|
||||||
|
<van-swipe-cell
|
||||||
|
v-for="transaction in displayTransactions"
|
||||||
|
:key="transaction.id"
|
||||||
|
class="bill-item"
|
||||||
|
>
|
||||||
|
<div class="bill-row">
|
||||||
|
<!-- 多选框 -->
|
||||||
|
<van-checkbox
|
||||||
|
v-if="showCheckbox"
|
||||||
|
:model-value="isSelected(transaction.id)"
|
||||||
|
class="checkbox-col"
|
||||||
|
@update:model-value="toggleSelection(transaction)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 账单卡片 -->
|
||||||
|
<div
|
||||||
|
class="bill-card"
|
||||||
|
@click="handleClick(transaction)"
|
||||||
|
>
|
||||||
|
<!-- 5.1 左侧图标 -->
|
||||||
|
<div
|
||||||
|
class="card-icon"
|
||||||
|
:style="{ backgroundColor: getIconBg(transaction.type) }"
|
||||||
|
>
|
||||||
|
<van-icon
|
||||||
|
:name="getIconByClassify(transaction.classify)"
|
||||||
|
:color="getIconColor(transaction.type)"
|
||||||
|
size="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.1 中间内容 -->
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="card-title">
|
||||||
|
<span class="reason">{{ transaction.reason || '(无摘要)' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-info">
|
||||||
|
<!-- 5.6 时间格式化 -->
|
||||||
|
<span class="time">{{ formatTime(transaction.occurredAt) }}</span>
|
||||||
|
<!-- 5.5 分类标签 -->
|
||||||
|
<span
|
||||||
|
v-if="transaction.classify"
|
||||||
|
class="classify-tag"
|
||||||
|
:class="getClassifyTagClass(transaction.type)"
|
||||||
|
>
|
||||||
|
{{ transaction.classify }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 5.1 右侧金额 -->
|
||||||
|
<div class="card-right">
|
||||||
|
<div
|
||||||
|
class="amount"
|
||||||
|
:class="getAmountClass(transaction.type)"
|
||||||
|
>
|
||||||
|
{{ formatAmount(transaction.amount, transaction.type) }}
|
||||||
|
</div>
|
||||||
|
<!-- 5.5 类型标签 -->
|
||||||
|
<van-tag
|
||||||
|
:type="getTypeTagType(transaction.type)"
|
||||||
|
size="small"
|
||||||
|
class="type-tag"
|
||||||
|
>
|
||||||
|
{{ getTypeName(transaction.type) }}
|
||||||
|
</van-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 删除按钮 -->
|
||||||
|
<template
|
||||||
|
v-if="showDelete"
|
||||||
|
#right
|
||||||
|
>
|
||||||
|
<van-button
|
||||||
|
square
|
||||||
|
type="danger"
|
||||||
|
text="删除"
|
||||||
|
class="delete-button"
|
||||||
|
@click="handleDeleteClick(transaction)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</van-swipe-cell>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<van-empty
|
||||||
|
v-if="!loading && !(displayTransactions && displayTransactions.length)"
|
||||||
|
description="暂无交易记录"
|
||||||
|
/>
|
||||||
|
</van-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
/**
|
||||||
|
* BillListComponent - 统一的账单列表组件
|
||||||
|
*
|
||||||
|
* @component
|
||||||
|
* @description
|
||||||
|
* 高内聚的账单列表组件,基于 v2 风格设计,支持筛选、排序、分页、左滑删除、多选等功能。
|
||||||
|
* 可用于替代项目中的旧版 TransactionList 组件。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```vue
|
||||||
|
* <BillListComponent
|
||||||
|
* dataSource="api"
|
||||||
|
* :apiParams="{ type: 0, dateRange: ['2026-01-01', '2026-01-31'] }"
|
||||||
|
* :showDelete="true"
|
||||||
|
* :enableFilter="true"
|
||||||
|
* @click="handleBillClick"
|
||||||
|
* @delete="handleBillDelete"
|
||||||
|
* />
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @props
|
||||||
|
* - dataSource?: 'api' | 'custom' - 数据源模式,默认 'api'
|
||||||
|
* - apiParams?: { dateRange?, category?, type? } - API 模式的筛选参数
|
||||||
|
* - transactions?: Array - 自定义数据源(dataSource='custom' 时使用)
|
||||||
|
* - showDelete?: Boolean - 是否显示左滑删除,默认 true
|
||||||
|
* - showCheckbox?: Boolean - 是否显示多选框,默认 false
|
||||||
|
* - enableFilter?: Boolean - 是否启用筛选栏,默认 true
|
||||||
|
* - enableSort?: Boolean - 是否启用排序,默认 true
|
||||||
|
* - compact?: Boolean - 是否使用紧凑模式,默认 true
|
||||||
|
* - selectedIds?: Set - 已选中的账单 ID 集合
|
||||||
|
*
|
||||||
|
* @emits
|
||||||
|
* - load - 触发分页加载
|
||||||
|
* - click - 点击账单卡片,参数: transaction
|
||||||
|
* - delete - 删除账单成功,参数: id
|
||||||
|
* - update:selectedIds - 多选状态变更,参数: Set<id>
|
||||||
|
*
|
||||||
|
* @author AI Assistant
|
||||||
|
* @since 2026-02-15
|
||||||
|
*/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
import { showConfirmDialog, showToast } from 'vant'
|
||||||
|
import { getTransactionList, deleteTransaction } from '@/api/transactionRecord'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Transaction
|
||||||
|
* @property {number|string} id - 账单 ID
|
||||||
|
* @property {string} reason - 摘要
|
||||||
|
* @property {number} amount - 金额
|
||||||
|
* @property {0|1|2} type - 类型:0=支出, 1=收入, 2=不计入
|
||||||
|
* @property {string} [classify] - 分类
|
||||||
|
* @property {string} occurredAt - 交易时间
|
||||||
|
* @property {number} [balance] - 余额
|
||||||
|
* @property {string} [importFrom] - 导入来源
|
||||||
|
* @property {number} [upsetedType] - 修改后类型
|
||||||
|
* @property {string} [upsetedClassify] - 修改后分类
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Props 定义
|
||||||
|
const props = defineProps({
|
||||||
|
dataSource: {
|
||||||
|
type: String,
|
||||||
|
default: 'api',
|
||||||
|
validator: (value) => ['api', 'custom'].includes(value)
|
||||||
|
},
|
||||||
|
apiParams: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
transactions: {
|
||||||
|
type: Array,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Emits 定义
|
||||||
|
const emit = defineEmits(['load', 'click', 'delete', 'update:selectedIds'])
|
||||||
|
|
||||||
|
// 响应式数据状态
|
||||||
|
const rawTransactions = ref([]) // API 或 custom 数据的原始数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const finished = ref(false)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
// 筛选状态管理
|
||||||
|
const selectedType = ref(null) // null=全部, 0=支出, 1=收入, 2=不计入
|
||||||
|
const selectedCategory = ref(null) // null=全部
|
||||||
|
const dateRange = ref(null)
|
||||||
|
const sortBy = ref('time-desc')
|
||||||
|
|
||||||
|
// 4.1-4.5 筛选选项数据
|
||||||
|
const typeOptions = [
|
||||||
|
{ text: '全部类型', value: null },
|
||||||
|
{ text: '支出', value: 0 },
|
||||||
|
{ text: '收入', value: 1 },
|
||||||
|
{ text: '不计入收支', value: 2 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const categoryOptions = ref([
|
||||||
|
{ text: '全部分类', value: null },
|
||||||
|
{ text: '餐饮', value: '餐饮' },
|
||||||
|
{ text: '购物', value: '购物' },
|
||||||
|
{ text: '交通', value: '交通' },
|
||||||
|
{ text: '娱乐', value: '娱乐' },
|
||||||
|
{ text: '医疗', value: '医疗' },
|
||||||
|
{ text: '工资', value: '工资' },
|
||||||
|
{ text: '红包', value: '红包' },
|
||||||
|
{ text: '其他', value: '其他' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const sortOptions = [
|
||||||
|
{ text: '时间降序', value: 'time-desc' },
|
||||||
|
{ text: '时间升序', value: 'time-asc' },
|
||||||
|
{ text: '金额降序', value: 'amount-desc' },
|
||||||
|
{ text: '金额升序', value: 'amount-asc' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// 4.4 日期选择相关
|
||||||
|
const showCalendar = ref(false)
|
||||||
|
const dateDropdown = ref(null)
|
||||||
|
|
||||||
|
const dateRangeText = computed(() => {
|
||||||
|
if (!dateRange.value) {return '选择日期范围'}
|
||||||
|
return `${dateRange.value[0]} 至 ${dateRange.value[1]}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const onDateConfirm = (values) => {
|
||||||
|
const [start, end] = values
|
||||||
|
dateRange.value = [formatDateKey(start), formatDateKey(end)]
|
||||||
|
showCalendar.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDateDropdown = () => {
|
||||||
|
dateDropdown.value?.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateKey = (date) => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.6 重置筛选
|
||||||
|
const resetFilters = () => {
|
||||||
|
selectedType.value = null
|
||||||
|
selectedCategory.value = null
|
||||||
|
dateRange.value = null
|
||||||
|
sortBy.value = 'time-desc'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多选状态管理(本地状态,与 prop 同步)
|
||||||
|
const localSelectedIds = ref(new Set())
|
||||||
|
|
||||||
|
// 监听 props.selectedIds 变化,同步到本地状态
|
||||||
|
watch(
|
||||||
|
() => props.selectedIds,
|
||||||
|
(newIds) => {
|
||||||
|
localSelectedIds.value = new Set(newIds)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 数据源模式切换逻辑
|
||||||
|
const displayTransactions = computed(() => {
|
||||||
|
let data = []
|
||||||
|
|
||||||
|
// 2.1 根据 dataSource 选择数据源
|
||||||
|
if (props.dataSource === 'custom') {
|
||||||
|
data = props.transactions || []
|
||||||
|
} else {
|
||||||
|
data = rawTransactions.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5 应用筛选逻辑
|
||||||
|
let filtered = data
|
||||||
|
|
||||||
|
// 类型筛选
|
||||||
|
if (selectedType.value !== null) {
|
||||||
|
filtered = filtered.filter((t) => t.type === selectedType.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类筛选
|
||||||
|
if (selectedCategory.value) {
|
||||||
|
filtered = filtered.filter((t) => t.classify === selectedCategory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日期范围筛选
|
||||||
|
if (dateRange.value) {
|
||||||
|
const [start, end] = dateRange.value
|
||||||
|
filtered = filtered.filter((t) => {
|
||||||
|
const date = new Date(t.occurredAt).toISOString().split('T')[0]
|
||||||
|
return date >= start && date <= end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2.5 应用排序逻辑
|
||||||
|
const sorted = [...filtered]
|
||||||
|
switch (sortBy.value) {
|
||||||
|
case 'amount-desc':
|
||||||
|
sorted.sort((a, b) => b.amount - a.amount)
|
||||||
|
break
|
||||||
|
case 'amount-asc':
|
||||||
|
sorted.sort((a, b) => a.amount - b.amount)
|
||||||
|
break
|
||||||
|
case 'time-desc':
|
||||||
|
sorted.sort((a, b) => new Date(b.occurredAt).getTime() - new Date(a.occurredAt).getTime())
|
||||||
|
break
|
||||||
|
case 'time-asc':
|
||||||
|
sorted.sort((a, b) => new Date(a.occurredAt).getTime() - new Date(b.occurredAt).getTime())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========== 格式化和样式方法 ==========
|
||||||
|
|
||||||
|
// 5.3 根据分类获取图标
|
||||||
|
const getIconByClassify = (classify) => {
|
||||||
|
const iconMap = {
|
||||||
|
餐饮: 'food-o',
|
||||||
|
购物: 'shopping-cart-o',
|
||||||
|
交通: 'logistics',
|
||||||
|
娱乐: 'music-o',
|
||||||
|
医疗: 'hospital-o',
|
||||||
|
工资: 'balance-o',
|
||||||
|
红包: 'envelop-o',
|
||||||
|
其他: 'star-o'
|
||||||
|
}
|
||||||
|
return iconMap[classify || ''] || 'star-o'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.3 根据类型获取图标背景色
|
||||||
|
const getIconBg = (type) => {
|
||||||
|
if (type === 0) {return '#FEE2E2'} // 支出 - 浅红色
|
||||||
|
if (type === 1) {return '#D1FAE5'} // 收入 - 浅绿色
|
||||||
|
return '#E5E7EB' // 不计入 - 灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.3 根据类型获取图标颜色
|
||||||
|
const getIconColor = (type) => {
|
||||||
|
if (type === 0) {return '#EF4444'} // 支出 - 红色
|
||||||
|
if (type === 1) {return '#10B981'} // 收入 - 绿色
|
||||||
|
return '#6B7280' // 不计入 - 灰色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.4 格式化金额
|
||||||
|
const formatAmount = (amount, type) => {
|
||||||
|
const formatted = `¥${Number(amount).toFixed(2)}`
|
||||||
|
if (type === 0) {return `- ${formatted}`}
|
||||||
|
if (type === 1) {return `+ ${formatted}`}
|
||||||
|
return formatted
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.4 获取金额样式类
|
||||||
|
const getAmountClass = (type) => {
|
||||||
|
if (type === 0) {return 'amount-expense'}
|
||||||
|
if (type === 1) {return 'amount-income'}
|
||||||
|
return 'amount-neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.5 获取类型名称
|
||||||
|
const getTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
0: '支出',
|
||||||
|
1: '收入',
|
||||||
|
2: '不计入'
|
||||||
|
}
|
||||||
|
return typeMap[type] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.5 获取类型标签类型
|
||||||
|
const getTypeTagType = (type) => {
|
||||||
|
if (type === 0) {return 'danger'}
|
||||||
|
if (type === 1) {return 'success'}
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.5 获取分类标签样式类
|
||||||
|
const getClassifyTagClass = (type) => {
|
||||||
|
if (type === 0) {return 'tag-expense'}
|
||||||
|
if (type === 1) {return 'tag-income'}
|
||||||
|
return 'tag-neutral'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.6 格式化时间
|
||||||
|
const formatTime = (dateString) => {
|
||||||
|
if (!dateString) {return ''}
|
||||||
|
const date = new Date(dateString)
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== API 数据加载 ==========
|
||||||
|
|
||||||
|
// 3.2 初始加载逻辑
|
||||||
|
const fetchTransactions = async () => {
|
||||||
|
if (props.dataSource !== 'api') {return}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const params = {
|
||||||
|
latestId:
|
||||||
|
page.value === 1 ? undefined : rawTransactions.value[rawTransactions.value.length - 1]?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用 apiParams 筛选
|
||||||
|
if (props.apiParams?.type !== undefined) {
|
||||||
|
selectedType.value = props.apiParams.type
|
||||||
|
}
|
||||||
|
if (props.apiParams?.category) {
|
||||||
|
selectedCategory.value = props.apiParams.category
|
||||||
|
}
|
||||||
|
if (props.apiParams?.dateRange) {
|
||||||
|
dateRange.value = props.apiParams.dateRange
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getTransactionList(params)
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
const newData = response.data || []
|
||||||
|
|
||||||
|
if (page.value === 1) {
|
||||||
|
rawTransactions.value = newData
|
||||||
|
} else {
|
||||||
|
rawTransactions.value = [...rawTransactions.value, ...newData]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否加载完成
|
||||||
|
finished.value = newData.length < pageSize.value
|
||||||
|
} else {
|
||||||
|
showToast(response?.message || '加载失败')
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载交易记录失败:', error)
|
||||||
|
showToast('加载失败,请稍后重试')
|
||||||
|
finished.value = true
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.3 分页加载逻辑
|
||||||
|
const onLoad = () => {
|
||||||
|
if (props.dataSource === 'api') {
|
||||||
|
page.value++
|
||||||
|
fetchTransactions()
|
||||||
|
}
|
||||||
|
emit('load')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.4 筛选条件变更时的数据重载逻辑
|
||||||
|
const resetAndReload = () => {
|
||||||
|
if (props.dataSource !== 'api') {return}
|
||||||
|
|
||||||
|
page.value = 1
|
||||||
|
rawTransactions.value = []
|
||||||
|
finished.value = false
|
||||||
|
fetchTransactions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听筛选条件变化
|
||||||
|
watch([selectedType, selectedCategory, dateRange, sortBy], () => {
|
||||||
|
resetAndReload()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 监听 apiParams 变化
|
||||||
|
watch(
|
||||||
|
() => props.apiParams,
|
||||||
|
() => {
|
||||||
|
resetAndReload()
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 组件挂载时初始加载
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.dataSource === 'api') {
|
||||||
|
fetchTransactions()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 临时实现:点击处理
|
||||||
|
const handleClick = (transaction) => {
|
||||||
|
emit('click', transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6.3-6.8 删除处理(完整实现)
|
||||||
|
const handleDeleteClick = async (transaction) => {
|
||||||
|
try {
|
||||||
|
await showConfirmDialog({
|
||||||
|
title: '提示',
|
||||||
|
message: '确定要删除这条交易记录吗?'
|
||||||
|
})
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const response = await deleteTransaction(transaction.id)
|
||||||
|
|
||||||
|
if (response && response.success) {
|
||||||
|
showToast('删除成功')
|
||||||
|
|
||||||
|
// 6.5 删除成功后更新本地列表
|
||||||
|
rawTransactions.value = rawTransactions.value.filter((t) => t.id !== transaction.id)
|
||||||
|
|
||||||
|
// 6.7 派发全局事件
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id }))
|
||||||
|
} catch (e) {
|
||||||
|
console.log('非浏览器环境,跳过派发 transaction-deleted 事件', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6.8 触发父组件事件
|
||||||
|
emit('delete', transaction.id)
|
||||||
|
} else {
|
||||||
|
showToast(response?.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
console.error('删除出错:', err)
|
||||||
|
showToast('删除失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 临时实现:多选相关
|
||||||
|
const isSelected = (id) => {
|
||||||
|
return localSelectedIds.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleSelection = (transaction) => {
|
||||||
|
const newSelectedIds = new Set(localSelectedIds.value)
|
||||||
|
if (newSelectedIds.has(transaction.id)) {
|
||||||
|
newSelectedIds.delete(transaction.id)
|
||||||
|
} else {
|
||||||
|
newSelectedIds.add(transaction.id)
|
||||||
|
}
|
||||||
|
localSelectedIds.value = newSelectedIds
|
||||||
|
emit('update:selectedIds', newSelectedIds)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bill-list-component {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: var(--van-background-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-col {
|
||||||
|
padding: 12px 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.1-5.2 账单卡片布局(紧凑模式)
|
||||||
|
.bill-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 6px; // 5.2 紧凑间距
|
||||||
|
}
|
||||||
|
|
||||||
|
.bill-card:active {
|
||||||
|
background-color: var(--van-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.3 左侧图标
|
||||||
|
.card-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.1 中间内容
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reason {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--van-text-color);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--van-text-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.5 分类标签
|
||||||
|
.classify-tag {
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-expense {
|
||||||
|
background-color: rgba(239, 68, 68, 0.1);
|
||||||
|
color: #EF4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-income {
|
||||||
|
background-color: rgba(16, 185, 129, 0.1);
|
||||||
|
color: #10B981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-neutral {
|
||||||
|
background-color: rgba(107, 114, 128, 0.1);
|
||||||
|
color: #6B7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.1 右侧金额区域
|
||||||
|
.card-right {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.4 金额样式
|
||||||
|
.amount {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-expense {
|
||||||
|
color: var(--van-danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-income {
|
||||||
|
color: var(--van-success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-neutral {
|
||||||
|
color: var(--van-text-color-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5.5 类型标签
|
||||||
|
.type-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-button {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9.4 根据 compact prop 调整样式
|
||||||
|
.bill-list-component.comfortable {
|
||||||
|
.bill-card {
|
||||||
|
padding: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -214,12 +214,14 @@
|
|||||||
title="关联账单列表"
|
title="关联账单列表"
|
||||||
height="75%"
|
height="75%"
|
||||||
>
|
>
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="billList"
|
:transactions="billList"
|
||||||
:loading="billLoading"
|
:loading="billLoading"
|
||||||
:finished="true"
|
:finished="true"
|
||||||
:show-delete="false"
|
:show-delete="false"
|
||||||
:show-checkbox="false"
|
:show-checkbox="false"
|
||||||
|
:enable-filter="false"
|
||||||
@click="handleBillClick"
|
@click="handleBillClick"
|
||||||
@delete="handleBillDelete"
|
@delete="handleBillDelete"
|
||||||
/>
|
/>
|
||||||
@@ -409,12 +411,14 @@
|
|||||||
title="关联账单列表"
|
title="关联账单列表"
|
||||||
height="75%"
|
height="75%"
|
||||||
>
|
>
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="billList"
|
:transactions="billList"
|
||||||
:loading="billLoading"
|
:loading="billLoading"
|
||||||
:finished="true"
|
:finished="true"
|
||||||
:show-delete="false"
|
:show-delete="false"
|
||||||
:show-checkbox="false"
|
:show-checkbox="false"
|
||||||
|
:enable-filter="false"
|
||||||
@click="handleBillClick"
|
@click="handleBillClick"
|
||||||
@delete="handleBillDelete"
|
@delete="handleBillDelete"
|
||||||
/>
|
/>
|
||||||
@@ -426,7 +430,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { BudgetPeriodType } from '@/constants/enums'
|
import { BudgetPeriodType } from '@/constants/enums'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
import TransactionList from '@/components/TransactionList.vue'
|
import BillListComponent from '@/components/Bill/BillListComponent.vue'
|
||||||
import { getTransactionList } from '@/api/transactionRecord'
|
import { getTransactionList } from '@/api/transactionRecord'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="reason-group-list-v2">
|
<div class="reason-group-list-v2">
|
||||||
<van-empty
|
<van-empty
|
||||||
v-if="groups.length === 0 && !loading"
|
v-if="groups.length === 0 && !loading"
|
||||||
@@ -78,10 +78,12 @@
|
|||||||
</van-button>
|
</van-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="groupTransactions"
|
:transactions="groupTransactions"
|
||||||
:loading="transactionLoading"
|
:loading="transactionLoading"
|
||||||
:finished="transactionFinished"
|
:finished="transactionFinished"
|
||||||
|
:enable-filter="false"
|
||||||
@load="loadGroupTransactions"
|
@load="loadGroupTransactions"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
@delete="handleGroupTransactionDelete"
|
@delete="handleGroupTransactionDelete"
|
||||||
@@ -185,7 +187,7 @@ import { ref, computed, watch, onBeforeUnmount } from 'vue'
|
|||||||
import { showToast, showSuccessToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
import { showToast, showSuccessToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant'
|
||||||
import { getReasonGroups, batchUpdateByReason, getTransactionList } from '@/api/transactionRecord'
|
import { getReasonGroups, batchUpdateByReason, getTransactionList } from '@/api/transactionRecord'
|
||||||
import ClassifySelector from './ClassifySelector.vue'
|
import ClassifySelector from './ClassifySelector.vue'
|
||||||
import TransactionList from './TransactionList.vue'
|
import BillListComponent from './Bill/BillListComponent.vue'
|
||||||
import TransactionDetail from './TransactionDetail.vue'
|
import TransactionDetail from './TransactionDetail.vue'
|
||||||
import PopupContainer from './PopupContainer.vue'
|
import PopupContainer from './PopupContainer.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -1,389 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="transaction-list-container transaction-list">
|
|
||||||
<van-list
|
|
||||||
:loading="loading"
|
|
||||||
:finished="finished"
|
|
||||||
finished-text="没有更多了"
|
|
||||||
@load="onLoad"
|
|
||||||
>
|
|
||||||
<van-cell-group
|
|
||||||
v-if="transactions && transactions.length"
|
|
||||||
inset
|
|
||||||
style="margin-top: 10px"
|
|
||||||
>
|
|
||||||
<van-swipe-cell
|
|
||||||
v-for="transaction in transactions"
|
|
||||||
:key="transaction.id"
|
|
||||||
class="transaction-item"
|
|
||||||
>
|
|
||||||
<div class="transaction-row">
|
|
||||||
<van-checkbox
|
|
||||||
v-if="showCheckbox"
|
|
||||||
:model-value="isSelected(transaction.id)"
|
|
||||||
class="checkbox-col"
|
|
||||||
@update:model-value="toggleSelection(transaction)"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="transaction-card"
|
|
||||||
@click="handleClick(transaction)"
|
|
||||||
>
|
|
||||||
<div class="card-left">
|
|
||||||
<div class="transaction-title">
|
|
||||||
<span class="reason">{{ transaction.reason || '(无摘要)' }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="transaction-info">
|
|
||||||
<div>交易时间: {{ formatDate(transaction.occurredAt) }}</div>
|
|
||||||
<div>
|
|
||||||
<span v-if="transaction.classify"> 分类: {{ transaction.classify }} </span>
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
transaction.upsetedClassify &&
|
|
||||||
transaction.upsetedClassify !== transaction.classify
|
|
||||||
"
|
|
||||||
style="color: var(--van-warning-color)"
|
|
||||||
>
|
|
||||||
→ {{ transaction.upsetedClassify }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div v-if="transaction.importFrom">
|
|
||||||
来源: {{ transaction.importFrom }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-middle">
|
|
||||||
<van-tag
|
|
||||||
:type="getTypeTagType(transaction.type)"
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
{{ getTypeName(transaction.type) }}
|
|
||||||
</van-tag>
|
|
||||||
<template
|
|
||||||
v-if="
|
|
||||||
Number.isFinite(transaction.upsetedType) &&
|
|
||||||
transaction.upsetedType !== transaction.type
|
|
||||||
"
|
|
||||||
>
|
|
||||||
→
|
|
||||||
<van-tag
|
|
||||||
:type="getTypeTagType(transaction.upsetedType)"
|
|
||||||
size="medium"
|
|
||||||
>
|
|
||||||
{{ getTypeName(transaction.upsetedType) }}
|
|
||||||
</van-tag>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-right">
|
|
||||||
<div class="transaction-amount">
|
|
||||||
<div :class="['amount', getAmountClass(transaction.type)]">
|
|
||||||
{{ formatAmount(transaction.amount, transaction.type) }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="transaction.balance && transaction.balance > 0"
|
|
||||||
class="balance"
|
|
||||||
>
|
|
||||||
余额: {{ formatMoney(transaction.balance) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<van-icon
|
|
||||||
name="arrow"
|
|
||||||
size="16"
|
|
||||||
color="var(--van-gray-5)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template
|
|
||||||
v-if="showDelete"
|
|
||||||
#right
|
|
||||||
>
|
|
||||||
<van-button
|
|
||||||
square
|
|
||||||
type="danger"
|
|
||||||
text="删除"
|
|
||||||
class="delete-button"
|
|
||||||
@click="handleDeleteClick(transaction)"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</van-swipe-cell>
|
|
||||||
</van-cell-group>
|
|
||||||
|
|
||||||
<van-empty
|
|
||||||
v-if="!loading && !(transactions && transactions.length)"
|
|
||||||
description="暂无交易记录"
|
|
||||||
/>
|
|
||||||
</van-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { showConfirmDialog, showToast } from 'vant'
|
|
||||||
import { deleteTransaction } from '@/api/transactionRecord'
|
|
||||||
|
|
||||||
import { defineEmits } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
transactions: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
finished: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
showDelete: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
showCheckbox: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
},
|
|
||||||
selectedIds: {
|
|
||||||
type: Set,
|
|
||||||
default: () => new Set()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits(['load', 'click', 'delete', 'update:selectedIds'])
|
|
||||||
|
|
||||||
const deletingIds = ref(new Set())
|
|
||||||
|
|
||||||
const onLoad = () => {
|
|
||||||
emit('load')
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClick = (transaction) => {
|
|
||||||
emit('click', transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDeleteClick = async (transaction) => {
|
|
||||||
try {
|
|
||||||
await showConfirmDialog({
|
|
||||||
title: '提示',
|
|
||||||
message: '确定要删除这条交易记录吗?'
|
|
||||||
})
|
|
||||||
|
|
||||||
deletingIds.value.add(transaction.id)
|
|
||||||
const response = await deleteTransaction(transaction.id)
|
|
||||||
deletingIds.value.delete(transaction.id)
|
|
||||||
|
|
||||||
if (response && response.success) {
|
|
||||||
showToast('删除成功')
|
|
||||||
emit('delete', transaction.id)
|
|
||||||
try {
|
|
||||||
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id }))
|
|
||||||
} catch (e) {
|
|
||||||
// ignore in non-browser environment
|
|
||||||
console.log('非浏览器环境,跳过派发 transaction-deleted 事件', e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showToast(response.message || '删除失败')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// 用户取消确认会抛出 'cancel' 或类似错误
|
|
||||||
if (err !== 'cancel') {
|
|
||||||
console.error('删除出错:', err)
|
|
||||||
showToast('删除失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSelected = (id) => {
|
|
||||||
return props.selectedIds.has(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSelection = (transaction) => {
|
|
||||||
const newSelectedIds = new Set(props.selectedIds)
|
|
||||||
if (newSelectedIds.has(transaction.id)) {
|
|
||||||
newSelectedIds.delete(transaction.id)
|
|
||||||
} else {
|
|
||||||
newSelectedIds.add(transaction.id)
|
|
||||||
}
|
|
||||||
emit('update:selectedIds', newSelectedIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取交易类型名称
|
|
||||||
const getTypeName = (type) => {
|
|
||||||
const typeMap = {
|
|
||||||
0: '支出',
|
|
||||||
1: '收入',
|
|
||||||
2: '不计入收支'
|
|
||||||
}
|
|
||||||
return typeMap[type] || '未知'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取交易类型标签类型
|
|
||||||
const getTypeTagType = (type) => {
|
|
||||||
const typeMap = {
|
|
||||||
0: 'danger',
|
|
||||||
1: 'success',
|
|
||||||
2: 'default'
|
|
||||||
}
|
|
||||||
return typeMap[type] || 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取金额样式类
|
|
||||||
const getAmountClass = (type) => {
|
|
||||||
if (type === 0) {
|
|
||||||
return 'expense'
|
|
||||||
}
|
|
||||||
if (type === 1) {
|
|
||||||
return 'income'
|
|
||||||
}
|
|
||||||
return 'neutral'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化金额(带符号)
|
|
||||||
const formatAmount = (amount, type) => {
|
|
||||||
const formatted = formatMoney(amount)
|
|
||||||
if (type === 0) {
|
|
||||||
return `- ${formatted}`
|
|
||||||
}
|
|
||||||
if (type === 1) {
|
|
||||||
return `+ ${formatted}`
|
|
||||||
}
|
|
||||||
return formatted
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化金额
|
|
||||||
const formatMoney = (amount) => {
|
|
||||||
return `¥${Number(amount).toFixed(2)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
const date = new Date(dateString)
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.transaction-list-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-col {
|
|
||||||
padding: 12px 8px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-card {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-left {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
padding-right: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-middle {
|
|
||||||
position: absolute;
|
|
||||||
top: 12px;
|
|
||||||
right: 20px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-title {
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reason {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-info {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--van-text-color-2);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.original-info {
|
|
||||||
color: var(--van-orange);
|
|
||||||
font-style: italic;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.transaction-amount {
|
|
||||||
text-align: right;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
min-width: 90px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.expense {
|
|
||||||
color: var(--van-danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount.income {
|
|
||||||
color: var(--van-success-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--van-text-color-2);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delete-button {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -4,9 +4,11 @@
|
|||||||
**Parent:** EmailBill/AGENTS.md
|
**Parent:** EmailBill/AGENTS.md
|
||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
|
|
||||||
Vue 3 views using Composition API with Vant UI components for mobile-first budget tracking.
|
Vue 3 views using Composition API with Vant UI components for mobile-first budget tracking.
|
||||||
|
|
||||||
## STRUCTURE
|
## STRUCTURE
|
||||||
|
|
||||||
```
|
```
|
||||||
Web/src/views/
|
Web/src/views/
|
||||||
├── BudgetView.vue # Main budget management
|
├── BudgetView.vue # Main budget management
|
||||||
@@ -27,25 +29,36 @@ Web/src/views/
|
|||||||
```
|
```
|
||||||
|
|
||||||
## WHERE TO LOOK
|
## WHERE TO LOOK
|
||||||
| Task | Location | Notes |
|
|
||||||
|------|----------|-------|
|
| Task | Location | Notes |
|
||||||
| Budget management | BudgetView.vue | Main budget interface |
|
| ----------------- | ---------------------- | -------------------------- |
|
||||||
| Transactions | TransactionsRecord.vue | CRUD operations |
|
| Budget management | BudgetView.vue | Main budget interface |
|
||||||
| Statistics | StatisticsView.vue | Charts, analytics |
|
| Transactions | TransactionsRecord.vue | CRUD operations |
|
||||||
| Classification | Classification* | Transaction categorization |
|
| Statistics | StatisticsView.vue | Charts, analytics |
|
||||||
| Authentication | LoginView.vue | User login flow |
|
| Classification | Classification\* | Transaction categorization |
|
||||||
| Settings | SettingView.vue | App configuration |
|
| Authentication | LoginView.vue | User login flow |
|
||||||
| Email features | EmailRecord.vue | Email integration |
|
| Settings | SettingView.vue | App configuration |
|
||||||
|
| Email features | EmailRecord.vue | Email integration |
|
||||||
|
|
||||||
## CONVENTIONS
|
## CONVENTIONS
|
||||||
- Vue 3 Composition API with `<script setup lang="ts">`
|
|
||||||
|
- Vue 3 Composition API with `<script setup>` (JavaScript)
|
||||||
- Vant UI components: `<van-*>`
|
- Vant UI components: `<van-*>`
|
||||||
- Mobile-first responsive design
|
- Mobile-first responsive design
|
||||||
- SCSS with BEM naming convention
|
- SCSS with BEM naming convention
|
||||||
- Pinia for state management
|
- Pinia for state management
|
||||||
- Vue Router for navigation
|
- Vue Router for navigation
|
||||||
|
|
||||||
|
## REUSABLE COMPONENTS
|
||||||
|
|
||||||
|
**BillListComponent** (`@/components/Bill/BillListComponent.vue`)
|
||||||
|
- **用途**: 统一的账单列表组件,替代旧版 TransactionList
|
||||||
|
- **特性**: 支持筛选、排序、分页、左滑删除、多选
|
||||||
|
- **数据模式**: API 模式(自动加载)或 Custom 模式(父组件传入数据)
|
||||||
|
- **文档**: 参见 `.doc/BillListComponent-usage.md`
|
||||||
|
|
||||||
## ANTI-PATTERNS (THIS LAYER)
|
## ANTI-PATTERNS (THIS LAYER)
|
||||||
|
|
||||||
- Never use Options API (always Composition API)
|
- Never use Options API (always Composition API)
|
||||||
- Don't access APIs directly (use api/ modules)
|
- Don't access APIs directly (use api/ modules)
|
||||||
- Avoid inline styles (use SCSS modules)
|
- Avoid inline styles (use SCSS modules)
|
||||||
@@ -53,6 +66,7 @@ Web/src/views/
|
|||||||
- Don't mutate props directly
|
- Don't mutate props directly
|
||||||
|
|
||||||
## UNIQUE STYLES
|
## UNIQUE STYLES
|
||||||
|
|
||||||
- Chinese interface labels for business concepts
|
- Chinese interface labels for business concepts
|
||||||
- Mobile-optimized layouts with Vant components
|
- Mobile-optimized layouts with Vant components
|
||||||
- Integration with backend API via api/ modules
|
- Integration with backend API via api/ modules
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container-flex classification-nlp">
|
<div class="page-container-flex classification-nlp">
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="自然语言分类"
|
title="自然语言分类"
|
||||||
@@ -108,13 +108,15 @@
|
|||||||
|
|
||||||
<!-- 交易记录列表 -->
|
<!-- 交易记录列表 -->
|
||||||
<div class="records-list">
|
<div class="records-list">
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="displayRecords"
|
:transactions="displayRecords"
|
||||||
:loading="false"
|
:loading="false"
|
||||||
:finished="true"
|
:finished="true"
|
||||||
:show-checkbox="true"
|
:show-checkbox="true"
|
||||||
:selected-ids="selectedIds"
|
:selected-ids="selectedIds"
|
||||||
:show-delete="false"
|
:show-delete="false"
|
||||||
|
:enable-filter="false"
|
||||||
@update:selected-ids="updateSelectedIds"
|
@update:selected-ids="updateSelectedIds"
|
||||||
@click="handleRecordClick"
|
@click="handleRecordClick"
|
||||||
/>
|
/>
|
||||||
@@ -129,7 +131,7 @@ import { ref, computed } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import { nlpAnalysis, batchUpdateClassify } from '@/api/transactionRecord'
|
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 TransactionDetail from '@/components/TransactionDetail.vue'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<div class="page-container-flex">
|
<div class="page-container-flex">
|
||||||
<!-- 下拉刷新区域 -->
|
<!-- 下拉刷新区域 -->
|
||||||
@@ -148,11 +148,13 @@
|
|||||||
title="关联账单列表"
|
title="关联账单列表"
|
||||||
height="75%"
|
height="75%"
|
||||||
>
|
>
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="transactionList"
|
:transactions="transactionList"
|
||||||
:loading="false"
|
:loading="false"
|
||||||
:finished="true"
|
:finished="true"
|
||||||
:show-delete="true"
|
:show-delete="true"
|
||||||
|
:enable-filter="false"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
@delete="handleTransactionDelete"
|
@delete="handleTransactionDelete"
|
||||||
/>
|
/>
|
||||||
@@ -180,7 +182,7 @@ import {
|
|||||||
getEmailTransactions
|
getEmailTransactions
|
||||||
} from '@/api/emailRecord'
|
} from '@/api/emailRecord'
|
||||||
import { getTransactionDetail } from '@/api/transactionRecord'
|
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 TransactionDetail from '@/components/TransactionDetail.vue'
|
||||||
import PopupContainer from '@/components/PopupContainer.vue'
|
import PopupContainer from '@/components/PopupContainer.vue'
|
||||||
|
|
||||||
|
|||||||
@@ -26,19 +26,16 @@
|
|||||||
</van-loading>
|
</van-loading>
|
||||||
|
|
||||||
<!-- 交易记录列表 -->
|
<!-- 交易记录列表 -->
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
|
data-source="custom"
|
||||||
:transactions="transactionList"
|
:transactions="transactionList"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:finished="finished"
|
:finished="finished"
|
||||||
:show-delete="true"
|
:show-delete="true"
|
||||||
|
:enable-filter="false"
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
@click="viewDetail"
|
@click="viewDetail"
|
||||||
@delete="
|
@delete="handleDelete"
|
||||||
(id) => {
|
|
||||||
// 从当前的交易列表中移除该交易
|
|
||||||
transactionList.value = transactionList.value.filter((t) => t.id !== id)
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 底部安全距离 -->
|
<!-- 底部安全距离 -->
|
||||||
@@ -58,7 +55,7 @@
|
|||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { getTransactionList, getTransactionDetail } from '@/api/transactionRecord'
|
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'
|
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||||
|
|
||||||
const transactionList = ref([])
|
const transactionList = ref([])
|
||||||
@@ -183,7 +180,13 @@ const onDetailSave = async () => {
|
|||||||
loadData(true)
|
loadData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除功能由 TransactionList 组件内部处理,组件通过 :show-delete 启用
|
// 处理删除事件
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
// 从当前的交易列表中移除该交易
|
||||||
|
transactionList.value = transactionList.value.filter((t) => t.id !== id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除功能由 BillListComponent 组件内部处理,组件通过 :show-delete 启用
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
// 不需要手动调用 loadData,van-list 会自动触发 onLoad
|
||||||
|
|||||||
@@ -74,11 +74,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<TransactionList
|
<BillListComponent
|
||||||
:transactions="classifyNode.children.map(c => c.transaction)"
|
data-source="custom"
|
||||||
|
:transactions="classifyNode.children.map((c) => c.transaction)"
|
||||||
:show-delete="false"
|
:show-delete="false"
|
||||||
:show-checkbox="true"
|
:show-checkbox="true"
|
||||||
:selected-ids="selectedIds"
|
:selected-ids="selectedIds"
|
||||||
|
:enable-filter="false"
|
||||||
@click="handleTransactionClick"
|
@click="handleTransactionClick"
|
||||||
@update:selected-ids="handleUpdateSelectedIds"
|
@update:selected-ids="handleUpdateSelectedIds"
|
||||||
/>
|
/>
|
||||||
@@ -103,7 +105,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { showToast, showConfirmDialog } from 'vant'
|
import { showToast, showConfirmDialog } from 'vant'
|
||||||
import { getUnconfirmedTransactionList, confirmAllUnconfirmed } from '@/api/transactionRecord'
|
import { getUnconfirmedTransactionList, confirmAllUnconfirmed } from '@/api/transactionRecord'
|
||||||
import TransactionDetail from '@/components/TransactionDetail.vue'
|
import TransactionDetail from '@/components/TransactionDetail.vue'
|
||||||
import TransactionList from '@/components/TransactionList.vue'
|
import BillListComponent from '@/components/Bill/BillListComponent.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -154,9 +156,13 @@ const handleConfirmSelected = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const formatAmount = (amount) => {
|
const formatAmount = (amount) => {
|
||||||
if (amount === null || amount === undefined) {return ''}
|
if (amount === null || amount === undefined) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
const num = parseFloat(amount)
|
const num = parseFloat(amount)
|
||||||
if (isNaN(num)) {return ''}
|
if (isNaN(num)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
return num.toFixed(2)
|
return num.toFixed(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +327,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
.classify-collapse :deep(.van-cell-group--inset) {
|
.classify-collapse :deep(.van-cell-group--inset) {
|
||||||
margin-left: -24px;
|
margin-left: -24px;
|
||||||
width: calc(100vw - 48px)
|
width: calc(100vw - 48px);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-nav-bar) {
|
:deep(.van-nav-bar) {
|
||||||
|
|||||||
@@ -1,3 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
CalendarV2 专用的交易列表组件
|
||||||
|
|
||||||
|
特殊功能:
|
||||||
|
- 自定义 header(Items 数量、Smart 按钮)
|
||||||
|
- 与日历视图紧密集成
|
||||||
|
- 特定的 UI 风格和交互
|
||||||
|
|
||||||
|
注意:此组件不是通用的 BillListComponent,专为 CalendarV2 视图设计。
|
||||||
|
如需通用账单列表功能,请使用 @/components/Bill/BillListComponent.vue
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<!-- 交易列表 -->
|
<!-- 交易列表 -->
|
||||||
<div class="transactions">
|
<div class="transactions">
|
||||||
@@ -128,13 +139,13 @@ const formatAmount = (amount, type) => {
|
|||||||
// 根据分类获取图标
|
// 根据分类获取图标
|
||||||
const getIconByClassify = (classify) => {
|
const getIconByClassify = (classify) => {
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
'餐饮': 'food',
|
餐饮: 'food',
|
||||||
'购物': 'shopping',
|
购物: 'shopping',
|
||||||
'交通': 'transport',
|
交通: 'transport',
|
||||||
'娱乐': 'play',
|
娱乐: 'play',
|
||||||
'医疗': 'medical',
|
医疗: 'medical',
|
||||||
'工资': 'money',
|
工资: 'money',
|
||||||
'红包': 'red-packet'
|
红包: 'red-packet'
|
||||||
}
|
}
|
||||||
return iconMap[classify] || 'star'
|
return iconMap[classify] || 'star'
|
||||||
}
|
}
|
||||||
@@ -153,7 +164,7 @@ const fetchDayTransactions = async (date) => {
|
|||||||
|
|
||||||
if (response.success && response.data) {
|
if (response.success && response.data) {
|
||||||
// 转换为界面需要的格式
|
// 转换为界面需要的格式
|
||||||
transactions.value = response.data.map(txn => ({
|
transactions.value = response.data.map((txn) => ({
|
||||||
id: txn.id,
|
id: txn.id,
|
||||||
name: txn.reason || '未知交易',
|
name: txn.reason || '未知交易',
|
||||||
time: formatTime(txn.occurredAt),
|
time: formatTime(txn.occurredAt),
|
||||||
@@ -173,11 +184,15 @@ const fetchDayTransactions = async (date) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 监听 selectedDate 变化,重新加载数据
|
// 监听 selectedDate 变化,重新加载数据
|
||||||
watch(() => props.selectedDate, async (newDate) => {
|
watch(
|
||||||
if (newDate) {
|
() => props.selectedDate,
|
||||||
await fetchDayTransactions(newDate)
|
async (newDate) => {
|
||||||
}
|
if (newDate) {
|
||||||
}, { immediate: true })
|
await fetchDayTransactions(newDate)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
// 交易数量
|
// 交易数量
|
||||||
const transactionCount = computed(() => transactions.value.length)
|
const transactionCount = computed(() => transactions.value.length)
|
||||||
@@ -338,7 +353,7 @@ const onSmartClick = () => {
|
|||||||
|
|
||||||
.txn-classify-tag.tag-expense {
|
.txn-classify-tag.tag-expense {
|
||||||
background-color: rgba(59, 130, 246, 0.15);
|
background-color: rgba(59, 130, 246, 0.15);
|
||||||
color: #3B82F6;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.txn-amount {
|
.txn-amount {
|
||||||
|
|||||||
Reference in New Issue
Block a user