Files
tyapi-frontend/src/pages/admin/recharge-records/index.vue
2026-03-21 14:33:00 +08:00

812 lines
23 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 #stats v-if="singleUserMode">
<div class="flex items-center gap-2 text-sm text-gray-600">
<User class="w-4 h-4" />
<span>当前用户{{ currentUser?.company_name || currentUser?.phone }}</span>
<span class="text-gray-400">(仅显示当前用户)</span>
</div>
</template>
<!-- 单用户模式操作按钮 -->
<template #actions v-if="singleUserMode">
<div class="flex items-center gap-3">
<div class="flex items-center gap-2 text-sm text-gray-600">
<User class="w-4 h-4" />
<span>{{ currentUser?.enterprise_info?.company_name || currentUser?.phone }}</span>
</div>
<el-button size="small" @click="exitSingleUserMode">
<Close class="w-4 h-4 mr-1" />
取消
</el-button>
<el-button size="small" type="primary" @click="goBackToUsers">
<Back class="w-4 h-4 mr-1" />
返回用户管理
</el-button>
</div>
</template>
<!-- 筛选区域 -->
<template #filters>
<FilterSection>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<FilterItem label="企业名称" v-if="!singleUserMode">
<el-input
v-model="filters.company_name"
placeholder="输入企业名称"
clearable
@input="handleFilterChange"
class="w-full"
/>
</FilterItem>
<FilterItem label="充值类型">
<el-select
v-model="filters.recharge_type"
placeholder="选择充值类型"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="支付宝充值" value="alipay" />
<el-option label="微信充值" value="wechat" />
<el-option label="对公转账" value="transfer" />
<el-option label="赠送" value="gift" />
</el-select>
</FilterItem>
<FilterItem label="状态">
<el-select
v-model="filters.status"
placeholder="选择状态"
clearable
@change="handleFilterChange"
class="w-full"
>
<el-option label="全部" value="" />
<el-option label="成功" value="success" />
<el-option label="失败" value="failed" />
<el-option label="处理中" value="pending" />
</el-select>
</FilterItem>
<FilterItem label="充值时间" class="col-span-1 md:col-span-2">
<el-date-picker
v-model="dateRange"
type="datetimerange"
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
@change="handleTimeRangeChange"
class="w-full"
:size="isMobile ? 'small' : 'default'"
/>
</FilterItem>
<FilterItem label="金额范围">
<div class="flex gap-2">
<el-input
v-model="filters.min_amount"
placeholder="最小金额"
clearable
@input="handleFilterChange"
class="flex-1"
/>
<span class="text-gray-400 self-center">-</span>
<el-input
v-model="filters.max_amount"
placeholder="最大金额"
clearable
@input="handleFilterChange"
class="flex-1"
/>
</div>
</FilterItem>
</div>
<template #stats> 共找到 {{ total }} 条充值记录 </template>
<template #buttons>
<el-button @click="resetFilters">重置筛选</el-button>
<el-button type="primary" @click="loadRechargeRecords">应用筛选</el-button>
<el-button type="success" @click="showExportDialog">
<Download class="w-4 h-4 mr-1" />
导出数据
</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 class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<el-table
:data="rechargeRecords"
style="width: 100%"
:header-cell-style="{
background: '#f8fafc',
color: '#475569',
fontWeight: '600',
fontSize: '14px',
}"
:cell-style="{
fontSize: '14px',
color: '#1e293b',
}"
>
<el-table-column prop="id" label="记录ID" min-width="120">
<template #default="{ row }">
<span class="font-mono text-sm text-gray-600">{{ row.id }}</span>
</template>
</el-table-column>
<el-table-column
prop="company_name"
label="企业名称"
min-width="150"
v-if="!singleUserMode"
>
<template #default="{ row }">
<div>
<div class="font-medium text-blue-600">{{ row.company_name || '未知企业' }}</div>
<div class="text-xs text-gray-500">{{ row.user?.phone }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="amount" label="充值金额" width="120">
<template #default="{ row }">
<span class="font-semibold text-green-600">¥{{ formatPrice(row.amount) }}</span>
</template>
</el-table-column>
<el-table-column prop="recharge_type" label="充值类型" width="120">
<template #default="{ row }">
<el-tag :type="getRechargeTypeTag(row.recharge_type)" size="small" effect="light">
{{ getRechargeTypeText(row.recharge_type) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small" effect="light">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="订单号" min-width="180">
<template #default="{ row }">
<div class="text-sm">
<div v-if="row.alipay_order_id" class="text-xs">
<span class="text-gray-500">支付宝</span>
<span class="font-mono">{{ row.alipay_order_id }}</span>
</div>
<div v-if="row.wechat_order_id" class="text-xs">
<span class="text-gray-500">微信</span>
<span class="font-mono">{{ row.wechat_order_id }}</span>
</div>
<div v-if="row.transfer_order_id" class="text-xs">
<span class="text-gray-500">转账</span>
<span class="font-mono">{{ row.transfer_order_id }}</span>
</div>
<div v-if="!row.alipay_order_id && !row.wechat_order_id && !row.transfer_order_id" class="text-gray-400">
-
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="充值时间" width="160">
<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-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<div class="flex items-center space-x-2">
<el-button size="small" type="primary" @click="handleViewDetail(row)">
查看详情
</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</template>
<!-- 分页 -->
<template #pagination>
<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"
/>
</template>
<template #extra>
<!-- 充值记录详情弹窗 -->
<el-dialog
v-model="detailDialogVisible"
title="充值记录详情"
width="800px"
class="recharge-detail-dialog"
>
<div v-if="selectedRechargeRecord" class="space-y-6">
<!-- 基本信息 -->
<div class="info-section">
<h4 class="text-lg font-semibold text-gray-900 mb-4">基本信息</h4>
<div class="grid grid-cols-2 gap-4">
<div class="info-item">
<span class="info-label">记录ID</span>
<span class="info-value font-mono">{{ selectedRechargeRecord?.id || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">充值金额</span>
<span class="info-value text-green-600 font-semibold"
>¥{{ formatPrice(selectedRechargeRecord?.amount) }}</span
>
</div>
<div class="info-item">
<span class="info-label">充值类型</span>
<span class="info-value">
<el-tag
:type="getRechargeTypeTag(selectedRechargeRecord?.recharge_type)"
size="small"
>
{{ getRechargeTypeText(selectedRechargeRecord?.recharge_type) }}
</el-tag>
</span>
</div>
<div class="info-item">
<span class="info-label">状态</span>
<span class="info-value">
<el-tag :type="getStatusType(selectedRechargeRecord?.status)" size="small">
{{ getStatusText(selectedRechargeRecord?.status) }}
</el-tag>
</span>
</div>
<div class="info-item" v-if="!singleUserMode">
<span class="info-label">企业名称</span>
<span class="info-value">{{
selectedRechargeRecord?.company_name || '未知企业'
}}</span>
</div>
</div>
</div>
<!-- 订单信息 -->
<div class="info-section">
<h4 class="text-lg font-semibold text-gray-900 mb-4">订单信息</h4>
<div class="grid grid-cols-2 gap-4">
<div class="info-item" v-if="selectedRechargeRecord?.alipay_order_id">
<span class="info-label">支付宝订单号</span>
<span class="info-value font-mono">{{
selectedRechargeRecord?.alipay_order_id || '-'
}}</span>
</div>
<div class="info-item" v-if="selectedRechargeRecord?.wechat_order_id">
<span class="info-label">微信订单号</span>
<span class="info-value font-mono">{{
selectedRechargeRecord?.wechat_order_id || '-'
}}</span>
</div>
<div class="info-item" v-if="selectedRechargeRecord?.transfer_order_id">
<span class="info-label">转账订单号</span>
<span class="info-value font-mono">{{
selectedRechargeRecord?.transfer_order_id || '-'
}}</span>
</div>
<div class="info-item" v-if="selectedRechargeRecord?.platform">
<span class="info-label">支付平台</span>
<span class="info-value">{{
selectedRechargeRecord?.platform || '-'
}}</span>
</div>
</div>
</div>
<!-- 时间信息 -->
<div class="info-section">
<h4 class="text-lg font-semibold text-gray-900 mb-4">时间信息</h4>
<div class="grid grid-cols-2 gap-4">
<div class="info-item">
<span class="info-label">充值时间</span>
<span class="info-value">{{
formatDateTime(selectedRechargeRecord?.created_at)
}}</span>
</div>
<div class="info-item">
<span class="info-label">更新时间</span>
<span class="info-value">{{
formatDateTime(selectedRechargeRecord?.updated_at)
}}</span>
</div>
</div>
</div>
</div>
<div v-else class="flex justify-center items-center py-8">
<el-loading size="large" />
</div>
</el-dialog>
</template>
</ListPageLayout>
<!-- 导出弹窗 -->
<ExportDialog
v-model="exportDialogVisible"
title="导出充值记录"
:loading="exportLoading"
:show-company-select="true"
:show-product-select="false"
:show-recharge-type-select="true"
:show-status-select="true"
:show-date-range="true"
@confirm="handleExport"
/>
</template>
<script setup>
import { rechargeRecordApi, userApi } from '@/api'
import ExportDialog from '@/components/common/ExportDialog.vue'
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 { Back, Close, Download, User } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 移动端检测
const { isMobile } = useMobileTable()
// 响应式数据
const loading = ref(false)
const rechargeRecords = ref([])
const total = ref(0)
const currentPage = ref(1)
const pageSize = ref(10)
const detailDialogVisible = ref(false)
const selectedRechargeRecord = ref(null)
const dateRange = ref([])
// 单用户模式
const singleUserMode = ref(false)
const currentUser = ref(null)
// 导出相关
const exportDialogVisible = ref(false)
const exportLoading = ref(false)
// 筛选条件
const filters = reactive({
company_name: '',
recharge_type: '',
status: '',
min_amount: '',
max_amount: '',
start_time: '',
end_time: '',
})
// 初始化
onMounted(async () => {
await checkSingleUserMode()
await loadRechargeRecords()
})
// 检查单用户模式
const checkSingleUserMode = async () => {
const userId = route.query.user_id
if (userId) {
singleUserMode.value = true
await loadUserInfo(userId)
}
}
// 加载用户信息
const loadUserInfo = async (userId) => {
try {
const response = await userApi.getUserDetail(userId)
currentUser.value = response.data
} catch (error) {
console.error('加载用户信息失败:', error)
ElMessage.error('加载用户信息失败')
}
}
// 加载充值记录
const loadRechargeRecords = async () => {
loading.value = true
try {
const params = {
page: currentPage.value,
page_size: pageSize.value,
}
// 只传递非空的筛选条件
if (filters.company_name) {
params.company_name = filters.company_name
}
if (filters.recharge_type) {
params.recharge_type = filters.recharge_type
}
if (filters.status) {
params.status = filters.status
}
if (filters.min_amount) {
params.min_amount = filters.min_amount
}
if (filters.max_amount) {
params.max_amount = filters.max_amount
}
if (filters.start_time) {
params.start_time = filters.start_time
}
if (filters.end_time) {
params.end_time = filters.end_time
}
// 单用户模式添加用户ID筛选
if (singleUserMode.value && currentUser.value?.id) {
params.user_id = currentUser.value.id
}
const response = await rechargeRecordApi.getAdminRechargeRecords(params)
rechargeRecords.value = response.data?.items || []
total.value = response.data?.total || 0
} catch (error) {
console.error('加载充值记录失败:', error)
ElMessage.error('加载充值记录失败')
} finally {
loading.value = false
}
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return Number(price).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 formatDateTime = (date) => {
if (!date) return '-'
return new Date(date).toLocaleString('zh-CN')
}
// 获取充值类型标签
const getRechargeTypeTag = (type) => {
switch (type) {
case 'alipay':
return 'primary'
case 'wechat':
return 'success'
case 'transfer':
return 'warning'
case 'gift':
return 'success'
default:
return 'info'
}
}
// 获取充值类型文本
const getRechargeTypeText = (type) => {
switch (type) {
case 'alipay':
return '支付宝充值'
case 'wechat':
return '微信充值'
case 'transfer':
return '对公转账'
case 'gift':
return '赠送充值'
default:
return '未知类型'
}
}
// 获取状态类型
const getStatusType = (status) => {
switch (status) {
case 'success':
return 'success'
case 'failed':
return 'danger'
case 'pending':
return 'warning'
default:
return 'info'
}
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 'success':
return '成功'
case 'failed':
return '失败'
case 'pending':
return '处理中'
default:
return '未知'
}
}
// 处理筛选变化
const handleFilterChange = () => {
currentPage.value = 1
loadRechargeRecords()
}
// 处理时间范围变化
const handleTimeRangeChange = (range) => {
if (range && range.length === 2) {
filters.start_time = range[0]
filters.end_time = range[1]
} else {
filters.start_time = ''
filters.end_time = ''
}
currentPage.value = 1
loadRechargeRecords()
}
// 重置筛选
const resetFilters = () => {
Object.keys(filters).forEach((key) => {
filters[key] = ''
})
dateRange.value = []
currentPage.value = 1
loadRechargeRecords()
}
// 处理分页大小变化
const handleSizeChange = (size) => {
pageSize.value = size
currentPage.value = 1
loadRechargeRecords()
}
// 处理当前页变化
const handleCurrentChange = (page) => {
currentPage.value = page
loadRechargeRecords()
}
// 退出单用户模式
const exitSingleUserMode = () => {
singleUserMode.value = false
currentUser.value = null
router.replace({ name: 'AdminRechargeRecords' })
loadRechargeRecords()
}
// 返回用户管理
const goBackToUsers = () => {
const query = { user_id: currentUser.value?.id }
// 如果当前用户有企业名称,添加到查询参数
if (currentUser.value?.enterprise_info?.company_name) {
query.company_name = currentUser.value.enterprise_info.company_name
}
// 如果当前用户有手机号,添加到查询参数
if (currentUser.value?.phone) {
query.phone = currentUser.value.phone
}
console.log('query', query)
router.push({
name: 'AdminUsers',
query
})
}
// 查看详情
const handleViewDetail = (rechargeRecord) => {
selectedRechargeRecord.value = rechargeRecord
detailDialogVisible.value = true
console.log('detailDialogVisible', detailDialogVisible.value)
}
// 监听路由变化
watch(
() => route.query.user_id,
async (newUserId) => {
if (newUserId) {
singleUserMode.value = true
await loadUserInfo(newUserId)
} else {
singleUserMode.value = false
currentUser.value = null
}
await loadRechargeRecords()
},
)
// 导出相关方法
const showExportDialog = () => {
exportDialogVisible.value = true
}
const handleExport = async (options) => {
try {
exportLoading.value = true
// 构建导出参数
const params = {
format: options.format
}
// 添加企业筛选
if (options.companyIds.length > 0) {
params.user_ids = options.companyIds.join(',')
}
// 添加充值类型筛选
if (options.rechargeType) {
params.recharge_type = options.rechargeType
}
// 添加状态筛选
if (options.status) {
params.status = options.status
}
// 添加时间范围筛选
if (options.dateRange && options.dateRange.length === 2) {
params.start_time = options.dateRange[0]
params.end_time = options.dateRange[1]
}
// 调用导出API
const response = await rechargeRecordApi.exportAdminRechargeRecords(params)
// 创建下载链接
const blob = new Blob([response.data], {
type: options.format === 'excel'
? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
: 'text/csv;charset=utf-8'
})
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `充值记录.${options.format === 'excel' ? 'xlsx' : 'csv'}`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
ElMessage.success('导出成功')
exportDialogVisible.value = false
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败,请稍后重试')
} finally {
exportLoading.value = false
}
}
</script>
<style scoped>
.info-section {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
}
.info-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.info-label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
min-width: 100px;
}
.info-value {
font-size: 0.875rem;
color: #111827;
flex: 1;
}
.recharge-detail-dialog :deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
}
.recharge-detail-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
padding: 20px 24px;
}
.recharge-detail-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: 600;
color: #1e293b;
}
.recharge-detail-dialog :deep(.el-dialog__body) {
padding: 24px;
max-height: 70vh;
overflow-y: auto;
}
/* 表格样式优化 */
: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;
}
/* 响应式设计 */
@media (max-width: 768px) {
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.info-label {
min-width: auto;
}
}
</style>