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:
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>
|
||||
Reference in New Issue
Block a user