qiuyuan f2b2ab6b9e 1
2025-12-30 15:46:26 +08:00

544 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="expense-detail-page">
<!-- 面包屑导航 -->
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>
<router-link to="/layout/admin/myMoney">费用总览</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>消费明细</a-breadcrumb-item>
</a-breadcrumb>
<!-- 筛选区域 -->
<div class="filter-section">
<a-card size="small" class="filter-card">
<a-form layout="inline" :model="filterForm">
<a-form-item label="交易时间">
<a-range-picker
v-model:value="filterForm.dateRange"
format="YYYY-MM-DD"
/>
</a-form-item>
<a-form-item label="收支类型">
<a-select
v-model:value="filterForm.incomeExpenseType"
placeholder="请选择"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="income">收入</a-select-option>
<a-select-option value="expense">支出</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="交易类型">
<a-select
v-model:value="filterForm.transactionType"
placeholder="请选择"
style="width: 120px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option
v-for="type in transactionTypeOptions"
:key="type.value"
:value="type.value"
>
{{ type.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<a-button type="primary" @click="handleSearch">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
<template #icon><RedoOutlined /></template>
重置
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
<!-- 表格区域 -->
<div class="table-section">
<a-card>
<!-- 表格容器实现滚动效果 -->
<div class="table-container">
<a-table
:columns="columns"
:data-source="tableData"
:pagination="pagination"
:scroll="{ x: 1500, y: 500 }"
:loading="loading"
@change="handleTableChange"
size="middle"
bordered
>
<!-- 流水号列 -->
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'serialNumber'">
<a-tag color="blue">{{ record.serialNumber }}</a-tag>
</template>
<!-- 收支类型列 -->
<template v-else-if="column.key === 'incomeExpenseType'">
<a-tag :color="record.incomeExpenseType === 'income' ? 'green' : 'red'">
{{ record.incomeExpenseType === 'income' ? '收入' : '支出' }}
</a-tag>
</template>
<!-- 交易金额列 -->
<template v-else-if="column.key === 'amount'">
<span :class="record.incomeExpenseType === 'income' ? 'income-amount' : 'expense-amount'">
{{ record.incomeExpenseType === 'income' ? '+' : '-' }}{{ formatCurrency(record.amount) }}
</span>
</template>
<!-- 账户余额列 -->
<template v-else-if="column.key === 'balance'">
<span class="balance-amount">{{ formatCurrency(record.balance) }}</span>
</template>
<!-- 交易时间列 -->
<template v-else-if="column.key === 'transactionTime'">
{{ formatDateTime(record.transactionTime) }}
</template>
<!-- 交易渠道列 -->
<template v-else-if="column.key === 'channel'">
<a-tag :color="getChannelColor(record.channel)">
{{ getChannelLabel(record.channel) }}
</a-tag>
</template>
</template>
</a-table>
</div>
<!-- 统计信息 -->
<div class="summary-info">
<div class="summary-item">
<span class="summary-label">总收入:</span>
<span class="summary-value income">{{ formatCurrency(summary.totalIncome) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">总支出:</span>
<span class="summary-value expense">{{ formatCurrency(summary.totalExpense) }}</span>
</div>
<div class="summary-item">
<span class="summary-label">净收入:</span>
<span class="summary-value net-income">{{ formatCurrency(summary.netIncome) }}</span>
</div>
</div>
</a-card>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { SearchOutlined, RedoOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
// 表格列定义
const columns = [
{
title: '流水号',
dataIndex: 'serialNumber',
key: 'serialNumber',
width: 150,
fixed: 'left'
},
{
title: '交易时间',
dataIndex: 'transactionTime',
key: 'transactionTime',
width: 180,
sorter: true
},
{
title: '收支类型',
dataIndex: 'incomeExpenseType',
key: 'incomeExpenseType',
width: 120,
filters: [
{ text: '收入', value: 'income' },
{ text: '支出', value: 'expense' }
]
},
{
title: '交易类型',
dataIndex: 'transactionType',
key: 'transactionType',
width: 150
},
{
title: '交易渠道',
dataIndex: 'channel',
key: 'channel',
width: 120
},
{
title: '交易金额',
dataIndex: 'amount',
key: 'amount',
width: 150,
align: 'right',
sorter: true
},
{
title: '账户余额',
dataIndex: 'balance',
key: 'balance',
width: 150,
align: 'right',
sorter: true
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 200,
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 100,
fixed: 'right',
align: 'center',
slots: { customRender: 'action' }
}
]
// 交易类型选项
const transactionTypeOptions = [
{ value: 'shopping', label: '购物消费' },
{ value: 'transfer', label: '转账' },
{ value: 'withdraw', label: '提现' },
{ value: 'recharge', label: '充值' },
{ value: 'refund', label: '退款' },
{ value: 'salary', label: '工资收入' },
{ value: 'investment', label: '投资收益' }
]
// 交易渠道映射
const channelMap = {
'alipay': '支付宝',
'wechat': '微信支付',
'bank': '银行卡',
'cash': '现金',
'credit': '信用卡'
}
// 渠道颜色映射
const channelColorMap = {
'alipay': 'blue',
'wechat': 'green',
'bank': 'purple',
'cash': 'orange',
'credit': 'red'
}
// 筛选表单
const filterForm = reactive({
dateRange: [],
incomeExpenseType: '',
transactionType: ''
})
// 分页配置
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100']
})
// 表格数据
const tableData = ref([])
// 加载状态
const loading = ref(false)
// 统计信息
const summary = reactive({
totalIncome: 0,
totalExpense: 0,
netIncome: 0
})
// 生成模拟数据
const generateMockData = (count) => {
const mockData = []
const transactionTypes = ['shopping', 'transfer', 'withdraw', 'recharge', 'refund', 'salary', 'investment']
const channels = ['alipay', 'wechat', 'bank', 'cash', 'credit']
const incomeExpenseTypes = ['income', 'expense']
const remarks = [
'购物消费', '转账给朋友', 'ATM取现', '账户充值', '商品退款',
'工资收入', '理财产品收益', '餐饮消费', '交通出行', '生活缴费'
]
let balance = 10000
for (let i = 1; i <= count; i++) {
const incomeExpenseType = incomeExpenseTypes[Math.floor(Math.random() * incomeExpenseTypes.length)]
const amount = Math.floor(Math.random() * 5000) + 10
if (incomeExpenseType === 'income') {
balance += amount
} else {
balance -= amount
}
const transactionType = transactionTypes[Math.floor(Math.random() * transactionTypes.length)]
const transactionTypeLabel = transactionTypeOptions.find(item => item.value === transactionType)?.label || transactionType
mockData.push({
key: i,
serialNumber: `SN${String(i).padStart(8, '0')}`,
transactionTime: dayjs().subtract(Math.floor(Math.random() * 30), 'day').format('YYYY-MM-DD HH:mm:ss'),
incomeExpenseType,
transactionType: transactionTypeLabel,
channel: channels[Math.floor(Math.random() * channels.length)],
amount,
balance,
remark: remarks[Math.floor(Math.random() * remarks.length)]
})
}
return mockData
}
// 计算统计信息
const calculateSummary = (data) => {
let totalIncome = 0
let totalExpense = 0
data.forEach(item => {
if (item.incomeExpenseType === 'income') {
totalIncome += item.amount
} else {
totalExpense += item.amount
}
})
summary.totalIncome = totalIncome
summary.totalExpense = totalExpense
summary.netIncome = totalIncome - totalExpense
}
// 格式化货币
const formatCurrency = (value) => {
return `¥${value.toFixed(2)}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 格式化日期时间
const formatDateTime = (dateTime) => {
return dayjs(dateTime).format('YYYY-MM-DD HH:mm')
}
// 获取渠道标签
const getChannelLabel = (channel) => {
return channelMap[channel] || channel
}
// 获取渠道颜色
const getChannelColor = (channel) => {
return channelColorMap[channel] || 'default'
}
// 处理搜索
const handleSearch = () => {
pagination.current = 1
loadTableData()
}
// 处理重置
const handleReset = () => {
filterForm.dateRange = []
filterForm.incomeExpenseType = ''
filterForm.transactionType = ''
pagination.current = 1
loadTableData()
}
// 处理表格变化(分页、排序、筛选)
const handleTableChange = (pag, filters, sorter) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadTableData()
}
// 加载表格数据
const loadTableData = () => {
loading.value = true
// 模拟API请求延迟
setTimeout(() => {
const allData = generateMockData(200)
// 模拟筛选逻辑
let filteredData = [...allData]
if (filterForm.incomeExpenseType) {
filteredData = filteredData.filter(item => item.incomeExpenseType === filterForm.incomeExpenseType)
}
if (filterForm.transactionType) {
filteredData = filteredData.filter(item => {
const transactionTypeValue = transactionTypeOptions.find(option => option.label === item.transactionType)?.value
return transactionTypeValue === filterForm.transactionType
})
}
// 模拟分页
const startIndex = (pagination.current - 1) * pagination.pageSize
const endIndex = startIndex + pagination.pageSize
const pageData = filteredData.slice(startIndex, endIndex)
tableData.value = pageData
pagination.total = filteredData.length
// 计算统计信息(基于当前筛选条件下的所有数据)
calculateSummary(filteredData)
loading.value = false
}, 300)
}
// 初始化加载数据
onMounted(() => {
loadTableData()
})
</script>
<style scoped>
.expense-detail-page {
padding: 20px;
background-color: #f0f2f5;
min-height: 100vh;
}
.breadcrumb {
margin-bottom: 16px;
}
.page-header {
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 8px;
}
.page-description {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
margin: 0;
}
.filter-section {
margin-bottom: 20px;
}
.filter-card {
border-radius: 8px;
}
.table-section {
margin-bottom: 20px;
}
.table-container {
border-radius: 8px;
overflow: hidden;
}
.summary-info {
margin-top: 24px;
padding: 16px;
background-color: #fafafa;
border-radius: 8px;
display: flex;
justify-content: flex-end;
gap: 24px;
}
.summary-item {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.summary-label {
font-size: 14px;
color: rgba(0, 0, 0, 0.45);
margin-bottom: 4px;
}
.summary-value {
font-size: 18px;
font-weight: 600;
}
.summary-value.income {
color: #52c41a;
}
.summary-value.expense {
color: #f5222d;
}
.summary-value.net-income {
color: #1890ff;
}
.income-amount {
color: #52c41a;
font-weight: 600;
}
.expense-amount {
color: #f5222d;
font-weight: 600;
}
.balance-amount {
color: #1890ff;
font-weight: 600;
}
/* 响应式设计 */
@media (max-width: 768px) {
.expense-detail-page {
padding: 12px;
}
.summary-info {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.summary-item {
align-items: flex-start;
}
.filter-card :deep(.ant-form) {
flex-direction: column;
align-items: flex-start;
}
.filter-card :deep(.ant-form-item) {
margin-bottom: 12px;
width: 100%;
}
}
</style>