556 lines
14 KiB
Vue
556 lines
14 KiB
Vue
<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>
|