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

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