544 lines
13 KiB
Vue
544 lines
13 KiB
Vue
<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> |