2025-12-30 16:49:57 +08:00

830 lines
20 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>
<div class="recharge-page">
<!-- 面包屑导航 -->
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item>
<router-link to="/">首页</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>我的余额</a-breadcrumb-item>
</a-breadcrumb>
<!-- 页面标题 -->
<div class="page-header">
<h1 class="page-title">我的余额</h1>
</div>
<div class="main-content">
<!-- 余额和充值部分 -->
<div class="balance-section">
<!-- 余额卡片带充值方式 -->
<a-card class="balance-card" :bordered="false">
<div class="balance-header">
<div class="balance-info">
<div class="balance-amount">
<div class="amount-label">账户余额</div>
<div class="amount-value">¥{{ formatCurrency(balance) }}</div>
</div>
</div>
</div>
<div class="recharge-content">
<!-- 充值金额选择 -->
<div class="recharge-section">
<h3 class="section-title">充值金额¥</h3>
<div class="amount-options">
<div
v-for="option in amountOptions"
:key="option.value"
:class="['amount-option', { 'selected': selectedAmount === option.value }]"
@click="selectAmount(option.value)"
>
<div class="amount-number">¥{{ option.value }}</div>
</div>
</div>
</div>
<!-- 自定义金额输入 -->
<div class="recharge-section">
<h3 class="section-title">自定义充值金额</h3>
<div class="custom-amount">
<a-input
v-model:value="customAmount"
placeholder="请输入金额"
size="large"
:disabled="loading"
@change="handleCustomAmountChange"
>
<template #prefix>
<span class="input-prefix">¥</span>
</template>
</a-input>
<div v-if="customAmountError" class="error-text">
{{ customAmountError }}
</div>
</div>
</div>
<!-- 充值方式 -->
<div class="payment-methods-container">
<div class="payment-methods-title">充值方式</div>
<div class="payment-methods-row">
<div
v-for="method in paymentMethods"
:key="method.value"
:class="['payment-method-item', { 'selected': selectedPaymentMethod === method.value }]"
@click="selectPaymentMethod(method.value)"
>
<div class="method-icon">
<component :is="method.icon" />
</div>
<div class="method-info">
<div class="method-name">{{ method.name }}</div>
<div v-if="method.description" class="method-desc">{{ method.description }}</div>
</div>
<div v-if="selectedPaymentMethod === method.value" class="method-selected">
<CheckCircleFilled />
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-section">
<a-button
type="primary"
size="large"
:loading="loading"
:disabled="!canRecharge"
@click="handleRecharge"
class="recharge-btn"
>
立即充值
</a-button>
</div>
</div>
</a-card>
</div>
<!-- 充值记录 -->
<a-card class="records-card" title="充值记录">
<!-- 筛选条件 -->
<div class="filter-section">
<a-range-picker
v-model:value="dateRange"
format="YYYY-MM-DD"
placeholder="开始时间 - 结束时间"
style="width: 300px; margin-right: 16px;"
/>
<a-button type="primary" @click="handleSearch" :loading="searchLoading">
<template #icon><SearchOutlined /></template>
查询
</a-button>
<a-button style="margin-left: 8px" @click="handleReset">
重置
</a-button>
</div>
<!-- 记录表格 -->
<div class="records-table">
<a-table
:columns="columns"
:data-source="rechargeRecords"
:pagination="pagination"
:loading="tableLoading"
@change="handleTableChange"
size="middle"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'amount'">
<span class="amount-cell">¥{{ record.amount }}</span>
</template>
<template v-else-if="column.key === 'rechargeTime'">
{{ formatDateTime(record.rechargeTime) }}
</template>
<template v-else-if="column.key === 'paymentMethod'">
<a-tag :color="getPaymentMethodColor(record.paymentMethod)">
{{ getPaymentMethodLabel(record.paymentMethod) }}
</a-tag>
</template>
<template v-else-if="column.key === 'accountBalance'">
¥{{ formatCurrency(record.accountBalance) }}
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusLabel(record.status) }}
</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-button type="link" size="small" @click="viewDetail(record)">详情</a-button>
</template>
</template>
<template #emptyText>
<div class="empty-state">
<a-empty description="暂无充值记录" />
</div>
</template>
</a-table>
</div>
</a-card>
</div>
<!-- 充值确认模态框 -->
<a-modal
v-model:visible="rechargeModalVisible"
title="确认充值"
:confirm-loading="modalLoading"
@ok="handleRechargeConfirm"
@cancel="handleRechargeCancel"
centered
>
<div class="recharge-confirm">
<div class="confirm-item">
<span class="confirm-label">充值金额</span>
<span class="confirm-value">¥{{ selectedRechargeAmount }}</span>
</div>
<div class="confirm-item">
<span class="confirm-label">充值方式</span>
<span class="confirm-value">{{ getPaymentMethodLabel(selectedPaymentMethod) }}</span>
</div>
<div class="confirm-item">
<span class="confirm-label">当前余额</span>
<span class="confirm-value">¥{{ formatCurrency(balance) }}</span>
</div>
<div class="confirm-item">
<span class="confirm-label">充值后余额</span>
<span class="confirm-value highlight">¥{{ formatCurrency(balance + selectedRechargeAmount) }}</span>
</div>
<div class="confirm-tips">
<InfoCircleOutlined />
充值成功后金额将立即到账
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import {
SearchOutlined,
CheckCircleFilled,
InfoCircleOutlined,
AlipayOutlined,
WechatOutlined,
BankOutlined
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
// 用户余额
const balance = ref(5.00)
// 充值金额选项
const amountOptions = ref([
{ value: 100, label: '100元' },
{ value: 500, label: '500元' },
{ value: 1000, label: '1000元' },
{ value: 5000, label: '5000元' }
])
const selectedAmount = ref(0)
const customAmount = ref('')
const customAmountError = ref('')
// 充值方式
const paymentMethods = ref([
{ value: 'alipay', name: '支付宝支付', icon: AlipayOutlined, color: '#1677ff' },
{ value: 'wechat', name: '微信支付', icon: WechatOutlined, color: '#07c160' },
{ value: 'bank', name: '对公付款', icon: BankOutlined, color: '#722ed1' }
])
const selectedPaymentMethod = ref('alipay')
// 加载状态
const loading = ref(false)
const tableLoading = ref(false)
const searchLoading = ref(false)
const modalLoading = ref(false)
// 模态框
const rechargeModalVisible = ref(false)
// 筛选条件
const dateRange = ref([])
// 表格数据
const rechargeRecords = ref([])
// 表格列
const columns = ref([
{ title: '充值金额', dataIndex: 'amount', key: 'amount', align: 'right', width: 120 },
{ title: '充值时间', dataIndex: 'rechargeTime', key: 'rechargeTime', width: 180, sorter: true },
{ title: '支付方式', dataIndex: 'paymentMethod', key: 'paymentMethod', width: 120 },
{ title: '账户余额', dataIndex: 'accountBalance', key: 'accountBalance', align: 'right', width: 140 },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '操作', key: 'action', width: 80, align: 'center' }
])
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
pageSizeOptions: ['10', '20', '50', '100']
})
// 选中金额(优先自定义)
const selectedRechargeAmount = computed(() => {
if (selectedAmount.value > 0) return selectedAmount.value
const amount = parseFloat(customAmount.value)
return isNaN(amount) || amount <= 0 ? 0 : amount
})
const canRecharge = computed(() => {
return selectedRechargeAmount.value > 0 && selectedPaymentMethod.value && !loading.value
})
// 格式化货币(带千分位)
const formatCurrency = (value) => {
return parseFloat(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
// 格式化日期
const formatDateTime = (dateTime) => {
return dayjs(dateTime).format('YYYY-MM-DD HH:mm:ss')
}
// 选择预设金额
const selectAmount = (amount) => {
selectedAmount.value = amount
customAmount.value = ''
customAmountError.value = ''
}
// 自定义金额校验
const handleCustomAmountChange = () => {
selectedAmount.value = 0
const val = customAmount.value.trim()
if (!val) {
customAmountError.value = ''
return
}
const amount = parseFloat(val)
if (isNaN(amount) || amount <= 0) {
customAmountError.value = '请输入有效金额'
} else if (amount < 1) {
customAmountError.value = '充值金额不能小于1元'
} else if (amount > 50000) {
customAmountError.value = '单次最大充值金额为50000元'
} else {
customAmountError.value = ''
}
}
// 选择支付方式
const selectPaymentMethod = (method) => {
selectedPaymentMethod.value = method
}
// 支付方式标签 & 颜色
const getPaymentMethodLabel = (method) => {
const payment = paymentMethods.value.find(m => m.value === method)
return payment ? payment.name : method
}
const getPaymentMethodColor = (method) => {
const payment = paymentMethods.value.find(m => m.value === method)
return payment ? payment.color : 'default'
}
// 状态标签 & 颜色
const getStatusLabel = (status) => {
const map = { success: '成功', processing: '处理中', failed: '失败', pending: '待支付' }
return map[status] || status
}
const getStatusColor = (status) => {
const map = { success: 'green', processing: 'blue', failed: 'red', pending: 'orange' }
return map[status] || 'default'
}
// 充值主流程
const handleRecharge = () => {
if (selectedRechargeAmount.value <= 0) return message.error('请选择或输入充值金额')
if (!selectedPaymentMethod.value) return message.error('请选择支付方式')
rechargeModalVisible.value = true
}
// 确认充值
const handleRechargeConfirm = () => {
modalLoading.value = true
setTimeout(() => {
balance.value += selectedRechargeAmount.value
const now = new Date()
const newRecord = {
key: rechargeRecords.value.length + 1,
amount: selectedRechargeAmount.value,
rechargeTime: now,
paymentMethod: selectedPaymentMethod.value,
accountBalance: balance.value,
status: 'success'
}
rechargeRecords.value.unshift(newRecord)
pagination.total += 1
// 重置表单
selectedAmount.value = 0
customAmount.value = ''
customAmountError.value = ''
modalLoading.value = false
rechargeModalVisible.value = false
message.success(`充值成功!¥${selectedRechargeAmount.value} 已到账`)
}, 1500)
}
const handleRechargeCancel = () => {
rechargeModalVisible.value = false
}
// 充值记录查询
const handleSearch = () => {
searchLoading.value = true
pagination.current = 1
loadRechargeRecords()
}
const handleReset = () => {
dateRange.value = []
pagination.current = 1
loadRechargeRecords()
}
const handleTableChange = (pag) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadRechargeRecords()
}
const viewDetail = (record) => {
message.info(`充值¥${record.amount},时间:${formatDateTime(record.rechargeTime)}`)
}
// 加载记录(含模拟数据)
const loadRechargeRecords = () => {
tableLoading.value = true
setTimeout(() => {
const mockData = generateMockData(35)
let filtered = [...mockData]
if (dateRange.value?.length === 2) {
const start = dayjs(dateRange.value[0]).startOf('day')
const end = dayjs(dateRange.value[1]).endOf('day')
filtered = filtered.filter(r => {
const d = dayjs(r.rechargeTime)
return d.isAfter(start) && d.isBefore(end)
})
}
const startIdx = (pagination.current - 1) * pagination.pageSize
rechargeRecords.value = filtered.slice(startIdx, startIdx + pagination.pageSize)
pagination.total = filtered.length
tableLoading.value = false
searchLoading.value = false
}, 500)
}
// 生成模拟数据(无算力点)
const generateMockData = (count) => {
const methods = ['alipay', 'wechat', 'bank']
const statuses = ['success', 'processing', 'failed', 'pending']
let currentBalance = 5.00
const data = []
for (let i = 1; i <= count; i++) {
const amount = [100, 500, 1000, 5000][Math.floor(Math.random() * 4)]
const method = methods[Math.floor(Math.random() * methods.length)]
const status = statuses[Math.floor(Math.random() * statuses.length)]
if (status === 'success') currentBalance += amount
data.push({
key: i,
amount: amount,
rechargeTime: dayjs().subtract(Math.floor(Math.random() * 30), 'day').toDate(),
paymentMethod: method,
accountBalance: currentBalance,
status: status
})
}
return data.sort((a, b) => new Date(b.rechargeTime) - new Date(a.rechargeTime))
}
onMounted(() => {
rechargeRecords.value = generateMockData(15)
pagination.total = rechargeRecords.value.length
})
</script>
<style scoped>
.recharge-page {
padding: 24px;
background-color: #fafafa;
min-height: 100vh;
}
.breadcrumb {
margin-bottom: 24px;
}
.page-header {
margin-bottom: 32px;
}
.page-title {
font-size: 28px;
font-weight: 600;
color: #1f1f1f;
margin: 0;
}
.main-content {
max-width: 1200px;
margin: 0 auto;
}
/* 余额区域 */
.balance-section {
margin-bottom: 24px;
}
.balance-card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 24px;
}
.balance-card :deep(.ant-card-body) {
padding: 0;
}
.balance-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
}
@media (max-width: 992px) {
.balance-header {
flex-direction: column;
gap: 24px;
}
}
.balance-info {
display: block;
}
.balance-amount {
margin: 0;
}
.amount-label {
font-size: 16px;
color: #8c8c8c;
margin-bottom: 12px;
}
.amount-value {
font-size: 36px;
font-weight: 600;
color: #1f1f1f;
}
/* 充值方式容器 */
.payment-methods-container {
max-width: 600px;
}
.payment-methods-title {
font-size: 16px;
font-weight: 600;
color: #1f1f1f;
margin-bottom: 12px;
}
.payment-methods-row {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.payment-methods-row {
flex-direction: column;
}
}
.payment-method-item {
display: flex;
align-items: center;
border: 2px solid #f0f0f0;
border-radius: 8px;
padding: 12px 16px;
cursor: pointer;
background: #fff;
transition: all 0.2s;
min-width: 150px;
flex: 1;
}
@media (max-width: 768px) {
.payment-method-item {
min-width: auto;
}
}
.payment-method-item:hover {
border-color: #1890ff;
}
.payment-method-item.selected {
border-color: #1890ff;
background: #f0f9ff;
}
.method-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12px;
font-size: 18px;
}
.payment-method-item:nth-child(1) .method-icon { color: #1677ff; }
.payment-method-item:nth-child(2) .method-icon { color: #07c160; }
.payment-method-item:nth-child(3) .method-icon { color: #722ed1; }
.method-info {
flex: 1;
}
.method-name {
font-size: 14px;
font-weight: 500;
color: #1f1f1f;
white-space: nowrap;
}
.method-desc {
font-size: 11px;
color: #999;
margin-top: 2px;
}
.method-selected {
color: #52c41a;
font-size: 16px;
}
/* 充值内容 */
.recharge-content {
padding-top: 16px;
}
.recharge-section {
margin-bottom: 24px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #1f1f1f;
margin-bottom: 12px;
}
/* 金额选项 */
.amount-options {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
max-width: 500px;
}
@media (max-width: 768px) {
.amount-options {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 480px) {
.amount-options {
grid-template-columns: 1fr;
}
}
.amount-option {
border: 2px solid #f0f0f0;
border-radius: 8px;
padding: 14px 8px;
cursor: pointer;
transition: all 0.2s;
background: #fff;
text-align: center;
font-weight: 500;
font-size: 15px;
color: #333;
}
.amount-option:hover {
border-color: #1890ff;
}
.amount-option.selected {
border-color: #1890ff;
background: #e6f7ff;
color: #1890ff;
}
/* 自定义金额 */
.custom-amount {
max-width: 300px;
}
.custom-amount :deep(.ant-input) {
height: 40px;
border-radius: 8px;
font-size: 15px;
}
.input-prefix {
color: #8c8c8c;
font-size: 16px;
}
.error-text {
color: #ff4d4f;
font-size: 12px;
margin-top: 6px;
}
/* 按钮 */
.action-section {
margin-top: 32px;
text-align: center;
}
.recharge-btn {
width: 200px;
height: 44px;
font-size: 16px;
font-weight: 600;
border-radius: 8px;
}
/* 充值记录 */
.records-card {
border-radius: 16px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 24px;
}
.records-card :deep(.ant-card-head) {
border-bottom: none;
padding: 0 24px;
}
.records-card :deep(.ant-card-head-title) {
font-size: 18px;
font-weight: 600;
padding: 16px 0;
}
.records-card :deep(.ant-card-body) {
padding: 0 24px 24px;
}
.filter-section {
margin: 20px 0;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
@media (max-width: 768px) {
.filter-section {
flex-direction: column;
align-items: stretch;
}
}
.records-table {
overflow-x: auto;
}
.amount-cell {
color: #1890ff;
font-weight: 600;
}
.empty-state {
padding: 40px 0;
}
/* 模态框 */
.recharge-confirm {
padding: 16px 0;
}
.confirm-item {
display: flex;
justify-content: space-between;
padding-bottom: 14px;
margin-bottom: 14px;
border-bottom: 1px solid #f5f5f5;
}
.confirm-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.confirm-label {
color: #8c8c8c;
font-size: 14px;
}
.confirm-value {
font-weight: 600;
color: #1f1f1f;
font-size: 14px;
}
.confirm-value.highlight {
color: #52c41a;
font-size: 18px;
}
.confirm-tips {
margin-top: 20px;
padding: 10px;
background: #f6ffed;
border-radius: 6px;
color: #52c41a;
font-size: 13px;
}
.confirm-tips .anticon {
margin-right: 8px;
}
</style>