Files
tyapi-frontend/src/pages/finance/purchase-records/index.vue

556 lines
14 KiB
Vue
Raw Normal View History

2025-12-22 18:32:34 +08:00
<template>
<ListPageLayout
title="购买记录"
subtitle="查看您的所有购买记录"
>
<template #filters>
<FilterSection>
<FilterItem label="支付类型">
<el-select
v-model="filters.payment_type"
placeholder="选择支付类型"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="支付宝" value="alipay" />
<el-option label="微信" value="wechat" />
<el-option label="免费" value="free" />
</el-select>
</FilterItem>
<FilterItem label="支付渠道">
<el-select
v-model="filters.pay_channel"
placeholder="选择支付渠道"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="支付宝" value="alipay" />
<el-option label="微信" value="wechat" />
</el-select>
</FilterItem>
<FilterItem label="订单状态">
<el-select
v-model="filters.status"
placeholder="选择订单状态"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="已创建" value="created" />
<el-option label="已支付" value="paid" />
<el-option label="支付失败" value="failed" />
<el-option label="已取消" value="cancelled" />
<el-option label="已退款" value="refunded" />
<el-option label="已关闭" value="closed" />
</el-select>
</FilterItem>
<FilterItem label="开始时间" class="col-span-1">
<el-date-picker
v-model="filters.start_time"
type="datetime"
placeholder="选择开始时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleFilterChange"
class="w-full"
:size="isMobile ? 'small' : 'default'"
/>
</FilterItem>
<FilterItem label="结束时间" class="col-span-1">
<el-date-picker
v-model="filters.end_time"
type="datetime"
placeholder="选择结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleFilterChange"
class="w-full"
:size="isMobile ? 'small' : 'default'"
/>
</FilterItem>
<template #stats>
共找到 {{ total }} 条购买记录
</template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadRecords">应用筛选</el-button>
</template>
</FilterSection>
</template>
<template #table>
<div v-if="loading" class="flex justify-center items-center py-12">
<el-loading size="large" />
</div>
<div v-else-if="records.length === 0" class="text-center py-12">
<el-empty description="暂无购买记录" />
</div>
<div v-else class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div class="table-container">
<el-table
:data="records"
style="width: 100%"
:header-cell-style="{
background: '#f8fafc',
color: '#475569',
fontWeight: '600',
fontSize: '14px'
}"
:cell-style="{
fontSize: '14px',
color: '#1e293b'
}"
>
<el-table-column prop="order_no" label="订单号">
<template #default="{ row }">
<div class="space-y-1">
<div class="text-sm">
<span class="text-gray-500">商户订单</span>
<span class="font-mono">{{ row.order_no }}</span>
</div>
<div v-if="row.trade_no" class="text-sm">
<span class="text-gray-500">交易号</span>
<span class="font-mono">{{ row.trade_no }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="product_name" label="产品名称" width="200">
<template #default="{ row }">
<div class="space-y-1">
<div class="text-sm font-medium">{{ row.product_name }}</div>
<div class="text-xs text-gray-500">{{ row.product_code }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="amount" label="订单金额" width="120">
<template #default="{ row }">
<span class="font-medium text-green-600">
¥{{ formatMoney(row.amount) }}
</span>
</template>
</el-table-column>
<el-table-column prop="payment_type" label="支付类型" width="100">
<template #default="{ row }">
<el-tag
:type="getPaymentTypeTagType(row.payment_type)"
size="small"
>
{{ getPaymentTypeText(row.payment_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="pay_channel" label="支付渠道" width="100">
<template #default="{ row }">
<el-tag
:type="getPayChannelTagType(row.pay_channel)"
size="small"
>
{{ getPayChannelText(row.pay_channel) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="订单状态" width="120">
<template #default="{ row }">
<el-tag
:type="getStatusTagType(row.status)"
size="small"
>
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="pay_time" label="支付时间" width="180">
<template #default="{ row }">
<div class="text-sm">
<div v-if="row.pay_time" class="text-gray-900">{{ formatDate(row.pay_time) }}</div>
<div v-if="row.pay_time" class="text-gray-500">{{ formatTime(row.pay_time) }}</div>
<div v-else class="text-gray-400">-</div>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" width="180">
<template #default="{ row }">
<div class="text-sm">
<div class="text-gray-900">{{ formatDate(row.created_at) }}</div>
<div class="text-gray-500">{{ formatTime(row.created_at) }}</div>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<template #pagination>
<div class="pagination-wrapper">
<el-pagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
</ListPageLayout>
</template>
<script setup>
import { financeApi } from '@/api'
import FilterItem from '@/components/common/FilterItem.vue'
import FilterSection from '@/components/common/FilterSection.vue'
import ListPageLayout from '@/components/common/ListPageLayout.vue'
import { useMobileTable } from '@/composables/useMobileTable'
import { ElMessage } from 'element-plus'
// 移动端检测
const { isMobile } = useMobileTable()
// 响应式数据
const loading = ref(false)
const records = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(20)
// 筛选条件
const filters = reactive({
payment_type: '',
pay_channel: '',
status: '',
start_time: '',
end_time: ''
})
// 搜索防抖定时器
let searchTimer = null
// 加载购买记录
const loadRecords = async () => {
loading.value = true
try {
// 构建参数,过滤掉空值
const params = {
page: currentPage.value,
page_size: pageSize.value
}
// 只添加非空的筛选条件
Object.keys(filters).forEach(key => {
const value = filters[key]
if (value !== '' && value !== null && value !== undefined) {
params[key] = value
}
})
const response = await financeApi.getUserPurchaseRecords(params)
records.value = response.data?.items || []
total.value = response.data?.total || 0
} catch (error) {
console.error('加载购买记录失败:', error)
ElMessage.error('加载购买记录失败')
} finally {
loading.value = false
}
}
// 格式化金额
const formatMoney = (amount) => {
if (!amount) return '0.00'
const num = parseFloat(amount)
if (isNaN(num)) return '0.00'
return num.toFixed(2)
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
// 格式化时间
const formatTime = (date) => {
if (!date) return '-'
return new Date(date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 获取支付类型标签样式
const getPaymentTypeTagType = (type) => {
const typeMap = {
alipay: 'primary',
wechat: 'success',
free: 'info'
}
return typeMap[type] || 'info'
}
// 获取支付类型文本
const getPaymentTypeText = (type) => {
const typeMap = {
alipay: '支付宝',
wechat: '微信',
free: '免费'
}
return typeMap[type] || type
}
// 获取支付渠道标签样式
const getPayChannelTagType = (channel) => {
const channelMap = {
alipay: 'primary',
wechat: 'success'
}
return channelMap[channel] || 'info'
}
// 获取支付渠道文本
const getPayChannelText = (channel) => {
const channelMap = {
alipay: '支付宝',
wechat: '微信'
}
return channelMap[channel] || channel
}
// 获取状态标签样式
const getStatusTagType = (status) => {
const statusMap = {
created: 'info',
paid: 'success',
failed: 'danger',
cancelled: 'warning',
refunded: 'info',
closed: 'info'
}
return statusMap[status] || 'info'
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
created: '已创建',
paid: '已支付',
failed: '支付失败',
cancelled: '已取消',
refunded: '已退款',
closed: '已关闭'
}
return statusMap[status] || status
}
// 处理筛选变化
const handleFilterChange = () => {
currentPage.value = 1
loadRecords()
}
// 重置筛选
const resetFilters = () => {
Object.keys(filters).forEach(key => {
filters[key] = ''
})
currentPage.value = 1
loadRecords()
}
// 处理分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
loadRecords()
}
// 处理当前页变化
const handleCurrentChange = (page) => {
currentPage.value = page
loadRecords()
}
// 页面加载时获取数据
onMounted(() => {
loadRecords()
})
</script>
<style scoped>
/* 表格样式优化 */
:deep(.el-table) {
border-radius: 8px;
overflow: hidden;
}
:deep(.el-table th) {
background: #f8fafc !important;
border-bottom: 1px solid #e2e8f0;
}
:deep(.el-table td) {
border-bottom: 1px solid #f1f5f9;
}
:deep(.el-table tr:hover > td) {
background: #f8fafc !important;
}
/* 表格容器 */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 分页器包装器 */
.pagination-wrapper {
padding: 16px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
/* 响应式设计 */
@media (max-width: 768px) {
.stat-item {
padding: 12px 16px;
min-width: 100px;
}
.stat-value {
font-size: 20px;
}
.stat-label {
font-size: 12px;
}
/* 表格在移动端优化 */
.table-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
:deep(.el-table) {
font-size: 12px;
min-width: 1200px;
}
:deep(.el-table th),
:deep(.el-table td) {
padding: 8px 4px;
}
:deep(.el-table .cell) {
padding: 0 4px;
word-break: break-word;
line-height: 1.4;
}
/* 分页组件在移动端优化 */
.pagination-wrapper {
padding: 12px 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.pagination-wrapper :deep(.el-pagination) {
flex-wrap: nowrap;
min-width: fit-content;
justify-content: flex-start;
}
.pagination-wrapper :deep(.el-pagination__sizes) {
display: none;
}
.pagination-wrapper :deep(.el-pagination__total) {
font-size: 12px;
margin-right: 8px;
white-space: nowrap;
}
.pagination-wrapper :deep(.el-pagination__jump) {
display: none;
}
.pagination-wrapper :deep(.el-pager li) {
min-width: 32px;
height: 32px;
line-height: 32px;
margin: 0 2px;
font-size: 12px;
}
.pagination-wrapper :deep(.btn-prev),
.pagination-wrapper :deep(.btn-next) {
min-width: 32px;
height: 32px;
line-height: 32px;
font-size: 12px;
}
}
/* 超小屏幕进一步优化 */
@media (max-width: 480px) {
.pagination-wrapper {
padding: 8px 0;
}
.pagination-wrapper :deep(.el-pagination__total) {
display: none;
}
.pagination-wrapper :deep(.el-pagination) {
font-size: 11px;
}
.pagination-wrapper :deep(.el-pager li) {
min-width: 28px;
height: 28px;
line-height: 28px;
font-size: 11px;
}
.pagination-wrapper :deep(.btn-prev),
.pagination-wrapper :deep(.btn-next) {
min-width: 28px;
height: 28px;
line-height: 28px;
font-size: 11px;
}
.stat-item {
padding: 10px 12px;
min-width: 80px;
}
.stat-value {
font-size: 18px;
}
.stat-label {
font-size: 11px;
}
}
</style>