Files
tyapi-frontend/src/pages/finance/Wallet.vue
2026-06-08 15:28:53 +08:00

1757 lines
48 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="list-page-container">
<div class="list-page-card">
<!-- 页面头部 -->
<div class="list-page-header">
<div class="flex justify-between items-start">
<div>
<h1 class="list-page-title">钱包充值</h1>
<p class="list-page-subtitle">选择充值方式为您的钱包添加余额</p>
</div>
</div>
</div>
<div class="list-page-wrapper">
<!-- 钱包余额信息 -->
<div class="wallet-info-section">
<div class="wallet-balance-card" :class="getBalanceCardClass()">
<div class="balance-icon">
<i class="el-icon-wallet"></i>
</div>
<div class="balance-content">
<div class="balance-label">当前余额</div>
<div class="balance-amount">¥{{ formatPrice(walletInfo.balance || 0) }}</div>
<div v-if="walletInfo.balance_status" class="balance-status">
{{ getBalanceStatusText() }}
</div>
</div>
</div>
<!-- 充值优惠提示仅在开启赠送时显示 -->
<div class="recharge-benefit-alert" v-if="rechargeConfig.recharge_bonus_enabled">
<el-alert
title="充值优惠"
description="充值满1000元即可享受商务洽谈优惠获得专属服务支持"
type="success"
:closable="false"
show-icon
>
<template #default>
<div class="benefit-content">
<span>充值满1000元即可享受商务洽谈优惠获得专属服务支持</span>
<el-button
type="primary"
size="small"
@click="showBusinessConsultation = true"
class="consultation-btn"
>
商务洽谈
</el-button>
</div>
</template>
</el-alert>
</div>
<!-- API商店充值提示只要后端配置了就直接展示在页面中 -->
<div v-if="rechargeConfig.api_store_recharge_tip" class="recharge-benefit-alert api-store-recharge-tip">
<div class="benefit-content">
<span class="api-store-recharge-tip-text">
{{ rechargeConfig.api_store_recharge_tip }}
</span>
<el-button
type="primary"
size="small"
@click="showBusinessConsultation = true"
class="consultation-btn"
>
商务洽谈
</el-button>
</div>
</div>
<!-- 余额状态提示 -->
<div v-if="walletInfo.is_arrears" class="balance-alert arrears-alert">
<el-alert
title="账户已欠费"
description="您的账户已欠费,服务已停止。请立即充值以恢复服务。"
type="error"
:closable="false"
show-icon
/>
</div>
<div v-else-if="walletInfo.is_low_balance" class="balance-alert low-balance-alert">
<el-alert
title="余额不足"
description="您的余额较低,建议及时充值以确保服务正常使用。"
type="warning"
:closable="false"
show-icon
/>
</div>
</div>
<!-- 充值方式选择 -->
<div class="recharge-methods-section">
<h3 class="section-title">选择充值方式</h3>
<div class="recharge-methods-grid">
<!-- 支付宝充值 -->
<div
class="recharge-method-card"
:class="{ active: selectedMethod === 'alipay' }"
@click="selectMethod('alipay')"
>
<div class="method-icon alipay-icon">
<CurrencyYenIcon class="h-7 w-7" />
</div>
<div class="method-content">
<div class="method-title">支付宝充值</div>
<div class="method-description">在线支付即时到账</div>
</div>
<div
class="method-check"
:class="{
checked: selectedMethod === 'alipay',
unchecked: selectedMethod !== 'alipay',
}"
>
<el-icon v-if="selectedMethod === 'alipay'"><Check /></el-icon>
</div>
</div>
<!-- 微信充值 -->
<div
class="recharge-method-card"
:class="{ active: selectedMethod === 'wechat' }"
@click="selectMethod('wechat')"
>
<div class="method-icon wechat-icon">
<svg class="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 2.048-1.715 4.81-2.04 6.765-.632 0 0 .127-.08.289-.183a8.262 8.262 0 0 1-.79-3.228c0-4.054-3.89-7.342-8.691-7.342zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm6.581 4.853c-1.894-.006-3.598.723-4.712 1.87-.792.813-1.235 1.865-1.235 3.004 0 .918.3 1.762.81 2.426.393.511.893.93 1.468 1.22.464.23.97.345 1.483.345.276 0 .543-.027.811-.05a.86.86 0 0 1 .717.098l1.903 1.114c.11.065.167.054.167.054.16 0 .29-.132.29-.295 0-.072-.03-.142-.048-.213l-.39-1.48a.59.59 0 0 1 .213-.665c1.832-1.347 3.002-3.338 3.002-5.55 0-2.24-1.35-4.237-3.405-5.314a8.21 8.21 0 0 0-1.484-.456zm-2.834 3.524c.518 0 .938.427.938.953a.946.946 0 0 1-.938.953.946.946 0 0 1-.938-.953c0-.526.42-.953.938-.953zm4.604 0c.518 0 .938.427.938.953a.946.946 0 0 1-.938.953.946.946 0 0 1-.938-.953c0-.526.42-.953.938-.953z"/>
</svg>
</div>
<div class="method-content">
<div class="method-title">微信充值</div>
<div class="method-description">在线支付即时到账</div>
</div>
<div
class="method-check"
:class="{
checked: selectedMethod === 'wechat',
unchecked: selectedMethod !== 'wechat',
}"
>
<el-icon v-if="selectedMethod === 'wechat'"><Check /></el-icon>
</div>
</div>
<!-- 对公转账 -->
<div
class="recharge-method-card"
:class="{ active: selectedMethod === 'transfer' }"
@click="selectMethod('transfer')"
>
<div class="method-icon transfer-icon">
<CreditCardIcon class="h-7 w-7" />
</div>
<div class="method-content">
<div class="method-title">对公转账</div>
<div class="method-description">银行转账人工确认</div>
</div>
<div
class="method-check"
:class="{
checked: selectedMethod === 'transfer',
unchecked: selectedMethod !== 'transfer',
}"
>
<el-icon v-if="selectedMethod === 'transfer'"><Check /></el-icon>
</div>
</div>
</div>
</div>
<!-- 微信充值表单 -->
<div v-if="selectedMethod === 'wechat'" class="recharge-form-section">
<h3 class="section-title">微信充值</h3>
<!-- 预设充值金额选择 -->
<div class="preset-amounts-section">
<h4 class="preset-title">选择充值金额</h4>
<div class="preset-amounts-grid">
<div
v-for="item in presetAmountOptions"
:key="item.recharge_amount"
class="preset-amount-card"
:class="{ active: selectedPresetAmount === item.recharge_amount }"
@click="selectPresetAmount(item.recharge_amount)"
>
<div class="preset-amount-main">
<div class="preset-amount-value">¥{{ formatPrice(item.recharge_amount) }}</div>
<div v-if="rechargeConfig.recharge_bonus_enabled && item.bonus_amount > 0" class="preset-bonus-info">
<span class="bonus-label">赠送</span>
<span class="bonus-amount">¥{{ formatPrice(item.bonus_amount) }}</span>
</div>
</div>
<div class="preset-amount-total">
实到账¥{{ formatPrice(item.recharge_amount + (rechargeConfig.recharge_bonus_enabled ? item.bonus_amount : 0)) }}
</div>
</div>
<!-- 自定义金额选项 -->
<div
class="preset-amount-card custom-amount-card"
:class="{ active: selectedPresetAmount === 'custom' }"
@click="selectCustomAmount"
>
<div class="preset-amount-main">
<div class="preset-amount-value">自定义金额</div>
<div v-if="rechargeConfig.recharge_bonus_enabled" class="preset-bonus-info">
<span class="bonus-label">赠送</span>
<span class="bonus-amount">{{ getCustomBonusText() }}</span>
</div>
</div>
<div class="preset-amount-total">
实到账¥{{ getCustomTotalAmount() }}
</div>
</div>
</div>
</div>
<el-form
ref="wechatFormRef"
:model="wechatForm"
:rules="wechatRules"
label-width="120px"
class="recharge-form"
>
<el-form-item label="充值金额" prop="amount">
<el-input
v-model="wechatForm.amount"
placeholder="请输入充值金额"
@input="handleWechatAmountInput"
class="amount-input"
>
<template #prepend>¥</template>
</el-input>
<div class="form-tip">最低充值金额¥{{ rechargeConfig.min_amount }}最多支持两位小数</div>
<!-- 赠送开启时显示赠送信息 -->
<div v-if="rechargeConfig.recharge_bonus_enabled && wechatForm.amount && getCurrentBonusAmount() > 0" class="bonus-info">
<el-alert
:title="`充值 ¥${wechatForm.amount} 可享受赠送 ¥${formatPrice(getCurrentBonusAmount())}`"
type="success"
:closable="false"
show-icon
>
<template #default>
<div class="bonus-detail">
<span>实际到账¥{{ formatPrice(parseFloat(wechatForm.amount || 0) + getCurrentBonusAmount()) }}</span>
</div>
</template>
</el-alert>
</div>
</el-form-item>
<!-- PC/H5 场景不再必填 openid -->
<el-form-item>
<el-button
type="primary"
size="large"
@click="handleWechatRecharge"
:loading="wechatLoading"
class="submit-btn"
>
立即充值
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 支付宝充值表单 -->
<div v-if="selectedMethod === 'alipay'" class="recharge-form-section">
<h3 class="section-title">支付宝充值</h3>
<!-- 预设充值金额选择 -->
<div class="preset-amounts-section">
<h4 class="preset-title">选择充值金额</h4>
<div class="preset-amounts-grid">
<div
v-for="item in presetAmountOptions"
:key="item.recharge_amount"
class="preset-amount-card"
:class="{ active: selectedPresetAmount === item.recharge_amount }"
@click="selectPresetAmount(item.recharge_amount)"
>
<div class="preset-amount-main">
<div class="preset-amount-value">¥{{ formatPrice(item.recharge_amount) }}</div>
<div v-if="rechargeConfig.recharge_bonus_enabled && item.bonus_amount > 0" class="preset-bonus-info">
<span class="bonus-label">赠送</span>
<span class="bonus-amount">¥{{ formatPrice(item.bonus_amount) }}</span>
</div>
</div>
<div class="preset-amount-total">
实到账¥{{ formatPrice(item.recharge_amount + (rechargeConfig.recharge_bonus_enabled ? item.bonus_amount : 0)) }}
</div>
</div>
<!-- 自定义金额选项 -->
<div
class="preset-amount-card custom-amount-card"
:class="{ active: selectedPresetAmount === 'custom' }"
@click="selectCustomAmount"
>
<div class="preset-amount-main">
<div class="preset-amount-value">自定义金额</div>
<div v-if="rechargeConfig.recharge_bonus_enabled" class="preset-bonus-info">
<span class="bonus-label">赠送</span>
<span class="bonus-amount">{{ getCustomBonusText() }}</span>
</div>
</div>
<div class="preset-amount-total">
实到账¥{{ getCustomTotalAmount() }}
</div>
</div>
</div>
</div>
<el-form
ref="alipayFormRef"
:model="alipayForm"
:rules="alipayRules"
label-width="120px"
class="recharge-form"
>
<el-form-item label="充值金额" prop="amount">
<el-input
v-model="alipayForm.amount"
placeholder="请输入充值金额"
@input="handleAmountInput"
class="amount-input"
>
<template #prepend>¥</template>
</el-input>
<div class="form-tip">最低充值金额¥{{ rechargeConfig.min_amount }}最多支持两位小数</div>
<!-- 赠送开启时显示赠送信息 -->
<div v-if="rechargeConfig.recharge_bonus_enabled && alipayForm.amount && getCurrentBonusAmount() > 0" class="bonus-info">
<el-alert
:title="`充值 ¥${alipayForm.amount} 可享受赠送 ¥${formatPrice(getCurrentBonusAmount())}`"
type="success"
:closable="false"
show-icon
>
<template #default>
<div class="bonus-detail">
<span>实际到账¥{{ formatPrice(parseFloat(alipayForm.amount || 0) + getCurrentBonusAmount()) }}</span>
</div>
</template>
</el-alert>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
@click="handleAlipayRecharge"
:loading="alipayLoading"
class="submit-btn"
>
立即充值
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 对公转账信息 -->
<div v-if="selectedMethod === 'transfer'" class="transfer-info-section">
<h3 class="section-title">对公转账信息</h3>
<div class="transfer-info-card">
<div class="transfer-info-item">
<label class="info-label">开户银行</label>
<span class="info-value">{{ transferInfo.bankName }}</span>
</div>
<div class="transfer-info-item">
<label class="info-label">银行账号</label>
<span class="info-value account-number">{{ transferInfo.bankAccount }}</span>
<el-button
size="small"
type="primary"
@click="copyToClipboard(transferInfo.bankAccount)"
class="copy-btn"
>
复制
</el-button>
</div>
<div class="transfer-info-item">
<label class="info-label">开户名</label>
<span class="info-value">{{ transferInfo.accountName }}</span>
</div>
<div class="transfer-info-item">
<label class="info-label">转账金额</label>
<span class="info-value amount">请按实际充值金额转账</span>
</div>
</div>
<div class="transfer-notice">
<el-alert title="转账说明" type="info" :closable="false" show-icon>
<template #default>
<ul class="notice-list">
<li>
转账时请在备注中填写您的企业名称<strong>{{
userInfo.enterprise_info.company_name
}}</strong>
</li>
<li>转账成功后我们会在24小时内为您确认充值</li>
<li>如有疑问请联系客服</li>
</ul>
</template>
</el-alert>
</div>
</div>
</div>
</div>
<!-- 商务洽谈弹窗 -->
<BusinessConsultationDialog v-model:visible="showBusinessConsultation" />
<!-- 微信支付二维码弹窗 -->
<el-dialog
v-model="showQrCodeDialog"
title="微信扫码支付"
width="400px"
:close-on-click-modal="false"
:close-on-press-escape="false"
class="qr-code-dialog"
@close="handleQrCodeDialogClose"
>
<div class="qr-code-container">
<div class="qr-code-wrapper">
<canvas ref="qrCodeCanvas" class="qr-code-canvas"></canvas>
</div>
<p class="qr-code-tip">请使用微信扫描上方二维码完成支付</p>
<p class="qr-code-amount">支付金额¥{{ formatPrice(wechatForm.amount) }}</p>
<p v-if="isCheckingPayment" class="qr-code-checking">正在确认支付状态请稍候...</p>
<p class="qr-code-tip">关闭窗口后将继续确认支付结果请勿重复支付</p>
<el-button type="primary" @click="closeQrCodeDialog" class="close-qr-btn">关闭</el-button>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { financeApi } from '@/api'
import BusinessConsultationDialog from '@/components/common/BusinessConsultationDialog.vue'
import { useCertification } from '@/composables/useCertification'
import { useUserStore } from '@/stores/user'
import { Check } from '@element-plus/icons-vue'
import { CreditCardIcon, CurrencyYenIcon } from '@heroicons/vue/24/outline'
import { ElMessage, ElMessageBox } from 'element-plus'
import QRCode from 'qrcode'
const userStore = useUserStore()
const userInfo = userStore.userInfo
const router = useRouter()
// 认证相关
const {
isCertified,
certificationLoading,
requiresCertification,
callProtectedAPI,
canCallAPI
} = useCertification()
// 响应式数据
const selectedMethod = ref('alipay')
const alipayLoading = ref(false)
const wechatLoading = ref(false)
const showBusinessConsultation = ref(false)
const showQrCodeDialog = ref(false)
const qrCodeCanvas = ref(null)
const currentWechatOrderNo = ref(null)
const isCheckingPayment = ref(false)
const wechatPaymentResolved = ref(false)
const skipQrCloseRedirect = ref(false)
const wechatDialogPollCount = ref(0)
const WECHAT_POLL_INTERVAL_MS = 5000
const WECHAT_DIALOG_MAX_POLL_COUNT = 12 // 弹窗内最多轮询12次约1分钟之后跳转处理页
let wechatOrderPollTimer = null
// 钱包信息
const walletInfo = ref({
balance: 0,
balance_status: '',
is_arrears: false,
is_low_balance: false,
})
// 充值配置(含赠送开关与 API 商店充值提示)
const rechargeConfig = ref({
min_amount: '1.00',
max_amount: '100000.00',
recharge_bonus_enabled: false,
api_store_recharge_tip: '',
alipay_recharge_bonus: []
})
// 关闭赠送时的预设金额(仅展示金额,无赠送)
const PRESET_AMOUNTS_NO_BONUS = [1000, 5000, 10000]
// 预设金额选项:开启赠送用配置规则,关闭赠送用固定金额列表
const presetAmountOptions = computed(() => {
if (rechargeConfig.value.recharge_bonus_enabled && rechargeConfig.value.alipay_recharge_bonus?.length) {
return rechargeConfig.value.alipay_recharge_bonus.map((b) => ({
recharge_amount: b.recharge_amount,
bonus_amount: b.bonus_amount ?? 0
}))
}
return PRESET_AMOUNTS_NO_BONUS.map((amount) => ({ recharge_amount: amount, bonus_amount: 0 }))
})
// 对公转账信息
const transferInfo = ref({
bankName: '中国银行股份有限公司海口美苑路支行',
bankAccount: '2662 9305 2910',
accountName: '海南海宇大数据有限公司',
})
// 支付宝充值表单
const alipayFormRef = ref()
const alipayForm = reactive({
amount: '',
})
// 微信充值表单
const wechatFormRef = ref()
const wechatForm = reactive({
amount: '',
})
// 预设金额选择
const selectedPresetAmount = ref(null)
// 格式化金额输入
const formatAmountInput = (value) => {
// 移除非数字和小数点
let formatted = value.replace(/[^\d.]/g, '')
// 确保只有一个小数点
const parts = formatted.split('.')
if (parts.length > 2) {
formatted = parts[0] + '.' + parts.slice(1).join('')
}
// 限制小数点后最多两位
if (parts.length === 2 && parts[1].length > 2) {
formatted = parts[0] + '.' + parts[1].substring(0, 2)
}
return formatted
}
// 处理金额输入变化
const handleAmountInput = (value) => {
// Element Plus 的 @input 事件直接传递值,而不是事件对象
const formatted = formatAmountInput(value || '')
alipayForm.amount = formatted
// 如果输入了自定义金额,更新选择状态
if (formatted && selectedPresetAmount.value !== 'custom') {
selectedPresetAmount.value = 'custom'
}
}
// 处理微信金额输入变化
const handleWechatAmountInput = (value) => {
const formatted = formatAmountInput(value || '')
wechatForm.amount = formatted
// 如果输入了自定义金额,更新选择状态
if (formatted && selectedPresetAmount.value !== 'custom') {
selectedPresetAmount.value = 'custom'
}
}
const alipayRules = {
amount: [
{ required: true, message: '请输入充值金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
// 检查是否为有效数字格式
const amountRegex = /^\d+(\.\d{1,2})?$/
if (!amountRegex.test(value)) {
callback(new Error('请输入正确的金额格式,最多支持两位小数'))
return
}
// 检查金额范围
const amount = parseFloat(value)
const minAmount = parseFloat(rechargeConfig.value.min_amount)
const maxAmount = parseFloat(rechargeConfig.value.max_amount)
if (amount < minAmount) {
callback(new Error(`充值金额不能少于${minAmount}`))
return
}
if (amount > maxAmount) {
callback(new Error(`单次充值金额不能超过${maxAmount}`))
return
}
callback()
},
trigger: 'blur',
},
],
}
const wechatRules = {
amount: [
{ required: true, message: '请输入充值金额', trigger: 'blur' },
{
validator: (rule, value, callback) => {
if (!value) {
callback()
return
}
// 检查是否为有效数字格式
const amountRegex = /^\d+(\.\d{1,2})?$/
if (!amountRegex.test(value)) {
callback(new Error('请输入正确的金额格式,最多支持两位小数'))
return
}
// 检查金额范围
const amount = parseFloat(value)
const minAmount = parseFloat(rechargeConfig.value.min_amount)
const maxAmount = parseFloat(rechargeConfig.value.max_amount)
if (amount < minAmount) {
callback(new Error(`充值金额不能少于${minAmount}`))
return
}
if (amount > maxAmount) {
callback(new Error(`单次充值金额不能超过${maxAmount}`))
return
}
callback()
},
trigger: 'blur',
},
],
}
// 初始化
onMounted(() => {
loadWalletInfo()
loadRechargeConfig()
})
// 加载钱包信息
const loadWalletInfo = async () => {
try {
const response = await callProtectedAPI(financeApi.getWallet)
if (response) {
walletInfo.value = response.data || { balance: 0 }
} else {
// 如果API调用被阻止显示默认数据
walletInfo.value = { balance: 0, balance_status: '', is_arrears: false, is_low_balance: false }
}
} catch (error) {
console.error('加载钱包信息失败:', error)
if (canCallAPI.value) {
ElMessage.error('加载钱包信息失败')
}
}
}
// 加载充值配置
const loadRechargeConfig = async () => {
try {
// 直接调用API不需要认证保护
const response = await financeApi.getRechargeConfig()
if (response && response.data) {
rechargeConfig.value = {
...response.data,
min_amount: response.data?.min_amount ?? '50.00',
max_amount: response.data?.max_amount ?? '100000.00',
recharge_bonus_enabled: response.data?.recharge_bonus_enabled ?? false,
api_store_recharge_tip: response.data?.api_store_recharge_tip ?? '',
alipay_recharge_bonus: response.data?.alipay_recharge_bonus ?? []
}
// 设置默认选中的预设金额:有赠送规则选第一条,否则选第一个预设金额(如 1000
const options = presetAmountOptions.value
if (options && options.length > 0) {
const first = options[0]
selectedPresetAmount.value = first.recharge_amount
const amountStr = first.recharge_amount.toString()
alipayForm.amount = amountStr
wechatForm.amount = amountStr
}
}
} catch (error) {
console.error('加载充值配置失败:', error)
// 使用默认配置,不显示错误信息
}
}
// 选择充值方式
const selectMethod = (method) => {
selectedMethod.value = method
}
// 格式化价格
const formatPrice = (price) => {
if (!price) return '0.00'
return Number(price).toFixed(2)
}
// 获取余额状态文本
const getBalanceStatusText = () => {
switch (walletInfo.value.balance_status) {
case 'arrears':
return '账户欠费'
case 'low':
return '余额较低'
case 'normal':
return '余额正常'
default:
return ''
}
}
// 获取余额卡片样式类
const getBalanceCardClass = () => {
if (walletInfo.value.is_arrears) {
return 'balance-card-arrears'
} else if (walletInfo.value.is_low_balance) {
return 'balance-card-low'
}
return ''
}
// 复制到剪贴板
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text)
ElMessage.success('已复制到剪贴板')
} catch (error) {
console.error('复制失败:', error)
ElMessage.error('复制失败')
}
}
// 显示商务洽谈弹窗
// 选择预设金额
const selectPresetAmount = (amount) => {
selectedPresetAmount.value = amount
const amountStr = amount.toString()
alipayForm.amount = amountStr
wechatForm.amount = amountStr
}
// 选择自定义金额
const selectCustomAmount = () => {
selectedPresetAmount.value = 'custom'
alipayForm.amount = '' // 清空金额输入框
wechatForm.amount = '' // 清空微信金额输入框
}
// 根据充值金额获取赠送金额(关闭赠送时恒为 0
const getBonusAmount = (rechargeAmount) => {
if (!rechargeConfig.value.recharge_bonus_enabled || !rechargeAmount || !rechargeConfig.value.alipay_recharge_bonus?.length) {
return 0
}
const amount = parseFloat(rechargeAmount)
const bonusRules = rechargeConfig.value.alipay_recharge_bonus
for (let i = bonusRules.length - 1; i >= 0; i--) {
const rule = bonusRules[i]
if (amount >= rule.recharge_amount) {
return rule.bonus_amount
}
}
return 0
}
// 获取当前预设金额的赠送金额(关闭赠送时恒为 0
const getCurrentBonusAmount = () => {
if (!rechargeConfig.value.recharge_bonus_enabled) return 0
if (selectedPresetAmount.value === 'custom') {
const currentAmount = selectedMethod.value === 'wechat' ? wechatForm.amount : alipayForm.amount
return getBonusAmount(currentAmount)
}
const bonus = rechargeConfig.value.alipay_recharge_bonus?.find(
(item) => item.recharge_amount === selectedPresetAmount.value
)
return bonus ? parseFloat(bonus.bonus_amount) : 0
}
// 获取自定义金额的赠送文本
const getCustomBonusText = () => {
if (!rechargeConfig.value.recharge_bonus_enabled) return '暂无'
if (selectedPresetAmount.value === 'custom') {
return '根据实际充值金额计算'
}
return '0.00'
}
// 获取自定义金额的总到账金额(关闭赠送时仅为本金)
const getCustomTotalAmount = () => {
if (selectedPresetAmount.value === 'custom') {
const currentAmount = selectedMethod.value === 'wechat' ? wechatForm.amount : alipayForm.amount
const amount = parseFloat(currentAmount || 0)
const bonus = getBonusAmount(amount)
return formatPrice(amount + bonus)
}
const item = presetAmountOptions.value.find((i) => i.recharge_amount === selectedPresetAmount.value)
if (!item) return '0.00'
const bonus = rechargeConfig.value.recharge_bonus_enabled ? item.bonus_amount : 0
return formatPrice(parseFloat(item.recharge_amount) + parseFloat(bonus))
}
// 支付宝充值
const handleAlipayRecharge = async () => {
if (!alipayFormRef.value) return
try {
await alipayFormRef.value.validate()
// 显示确认框
await ElMessageBox.confirm(
`确认充值 ¥${alipayForm.amount} 到您的钱包吗?`,
'确认充值',
{
confirmButtonText: '确认充值',
cancelButtonText: '取消',
type: 'warning',
customClass: 'custom-message-box',
dangerouslyUseHTMLString: false
}
)
alipayLoading.value = true
// 调用后端创建支付宝充值订单
const response = await callProtectedAPI(financeApi.createAlipayRecharge, {
amount: alipayForm.amount, // 直接传递字符串类型
subject: `钱包充值 ¥${alipayForm.amount}`,
platform: 'pc' // 根据实际需求设置pc/h5/app
})
if (!response) {
ElMessage.error('请先完成企业认证后再进行充值操作')
return
}
if (response.data && response.data.pay_url) {
ElMessage.success('正在跳转到支付宝支付...')
// 跳转到支付宝支付页面
window.location.href = response.data.pay_url
}
} catch (error) {
// 如果是用户取消,不显示错误信息
if (error === 'cancel' || error === 'close') {
return
}
console.error('支付宝充值失败:', error)
} finally {
alipayLoading.value = false
}
}
// 微信充值
const handleWechatRecharge = async () => {
if (!wechatFormRef.value) return
try {
await wechatFormRef.value.validate()
// 显示确认框
await ElMessageBox.confirm(
`确认充值 ¥${wechatForm.amount} 到您的钱包吗?`,
'确认充值',
{
confirmButtonText: '确认充值',
cancelButtonText: '取消',
type: 'warning',
customClass: 'custom-message-box',
dangerouslyUseHTMLString: false
}
)
wechatLoading.value = true
// 调用后端创建微信充值订单
const response = await callProtectedAPI(financeApi.createWechatRecharge, {
amount: wechatForm.amount, // 直接传递字符串类型
subject: `钱包充值 ¥${wechatForm.amount}`,
platform: 'wx_h5', // PC/H5 场景,后端已兼容无 openid
})
if (!response) {
ElMessage.error('请先完成企业认证后再进行充值操作')
return
}
// 处理微信支付响应
// prepay_data 可能包含 code_url (扫码支付) 或 pay_url (H5支付)
if (response.data && response.data.prepay_data) {
const prepayData = response.data.prepay_data
// 扫码支付:显示二维码
if (prepayData.code_url) {
// 保存订单号用于轮询(从响应中获取订单号)
if (response.data.out_trade_no) {
currentWechatOrderNo.value = response.data.out_trade_no
await showQrCode(prepayData.code_url)
// 开始轮询订单状态
startWechatOrderPolling()
} else {
ElMessage.error('获取订单号失败,请重新支付')
}
}
// H5支付跳转到支付页面
else if (prepayData.pay_url || response.data.pay_url) {
ElMessage.success('正在跳转到微信支付...')
window.location.href = prepayData.pay_url || response.data.pay_url
}
// 小程序或APP支付
else if (prepayData.prepay_id || response.data.prepay_id) {
ElMessage.success('请使用微信扫码支付')
// 这里可以根据实际返回的数据进行处理
} else {
console.warn('微信支付返回数据格式异常:', response.data)
ElMessage.warning('支付数据格式异常,请联系客服')
}
} else if (response.data && response.data.pay_url) {
// 兼容旧的返回格式(直接返回 pay_url
ElMessage.success('正在跳转到微信支付...')
window.location.href = response.data.pay_url
} else {
console.warn('微信支付返回数据异常:', response.data)
ElMessage.warning('获取支付信息失败,请稍后重试')
}
} catch (error) {
// 如果是用户取消,不显示错误信息
if (error === 'cancel' || error === 'close') {
return
}
console.error('微信充值失败:', error)
if (canCallAPI.value) {
ElMessage.error('微信充值失败,请稍后重试')
}
} finally {
wechatLoading.value = false
}
}
// 显示二维码
const showQrCode = async (codeUrl) => {
try {
showQrCodeDialog.value = true
// 等待DOM更新
await nextTick()
if (qrCodeCanvas.value) {
// 生成二维码
await QRCode.toCanvas(qrCodeCanvas.value, codeUrl, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
})
}
} catch (error) {
console.error('生成二维码失败:', error)
ElMessage.error('生成二维码失败,请稍后重试')
showQrCodeDialog.value = false
}
}
// 关闭二维码弹窗(用户主动关闭,跳转处理页继续轮询)
const closeQrCodeDialog = () => {
showQrCodeDialog.value = false
}
const handleQrCodeDialogClose = () => {
if (wechatPaymentResolved.value) {
wechatPaymentResolved.value = false
stopWechatOrderPolling()
currentWechatOrderNo.value = null
isCheckingPayment.value = false
wechatDialogPollCount.value = 0
return
}
if (skipQrCloseRedirect.value) {
skipQrCloseRedirect.value = false
isCheckingPayment.value = false
wechatDialogPollCount.value = 0
return
}
const orderNo = currentWechatOrderNo.value
const orderAmount = wechatForm.amount
stopWechatOrderPolling()
currentWechatOrderNo.value = null
isCheckingPayment.value = false
wechatDialogPollCount.value = 0
if (orderNo) {
router.push({
path: '/finance/wallet/processing',
query: {
out_trade_no: orderNo,
amount: orderAmount,
payment_type: 'wechat',
},
})
}
}
const redirectToWechatProcessingPage = () => {
const orderNo = currentWechatOrderNo.value
const orderAmount = wechatForm.amount
if (!orderNo) return
stopWechatOrderPolling()
skipQrCloseRedirect.value = true
currentWechatOrderNo.value = null
wechatDialogPollCount.value = 0
showQrCodeDialog.value = false
isCheckingPayment.value = false
router.push({
path: '/finance/wallet/processing',
query: {
out_trade_no: orderNo,
amount: orderAmount,
payment_type: 'wechat',
},
})
}
// 开始轮询微信订单状态
const startWechatOrderPolling = () => {
stopWechatOrderPolling()
wechatDialogPollCount.value = 0
checkWechatOrderStatus()
wechatOrderPollTimer = setInterval(() => {
wechatDialogPollCount.value++
checkWechatOrderStatus()
}, WECHAT_POLL_INTERVAL_MS)
}
// 停止轮询
const stopWechatOrderPolling = () => {
if (wechatOrderPollTimer) {
clearInterval(wechatOrderPollTimer)
wechatOrderPollTimer = null
}
}
// 检查微信订单状态
const checkWechatOrderStatus = async () => {
if (!currentWechatOrderNo.value) {
return
}
if (wechatDialogPollCount.value >= WECHAT_DIALOG_MAX_POLL_COUNT) {
redirectToWechatProcessingPage()
return
}
try {
isCheckingPayment.value = true
const response = await financeApi.getWechatOrderStatus({
out_trade_no: currentWechatOrderNo.value
})
if (!response?.data) {
isCheckingPayment.value = false
return
}
const orderStatus = response.data.status
if (orderStatus === 'success') {
wechatPaymentResolved.value = true
stopWechatOrderPolling()
isCheckingPayment.value = false
showQrCodeDialog.value = false
ElMessage.success('充值成功!')
await loadWalletInfo()
wechatForm.amount = ''
selectedPresetAmount.value = null
currentWechatOrderNo.value = null
wechatDialogPollCount.value = 0
} else if (orderStatus === 'failed' || orderStatus === 'closed') {
wechatPaymentResolved.value = true
stopWechatOrderPolling()
isCheckingPayment.value = false
showQrCodeDialog.value = false
ElMessage.error('支付失败,请重新支付')
currentWechatOrderNo.value = null
wechatDialogPollCount.value = 0
} else {
isCheckingPayment.value = false
}
} catch (error) {
console.error('查询微信订单状态失败:', error)
isCheckingPayment.value = false
}
}
// 组件卸载时清理定时器
onBeforeUnmount(() => {
stopWechatOrderPolling()
})
</script>
<style scoped>
/* 钱包信息区域 */
.wallet-info-section {
margin-bottom: 32px;
}
.wallet-balance-card {
display: flex;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 24px;
color: white;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
}
.list-page-wrapper {
padding: 32px;
}
.balance-icon {
font-size: 48px;
margin-right: 20px;
opacity: 0.9;
}
.balance-content {
flex: 1;
}
.balance-label {
font-size: 16px;
opacity: 0.9;
margin-bottom: 8px;
}
.balance-amount {
font-size: 36px;
font-weight: 700;
line-height: 1;
}
.balance-status {
font-size: 14px;
opacity: 0.8;
margin-top: 8px;
}
/* 充值优惠提示 */
.recharge-benefit-alert {
margin: 16px 0;
}
.benefit-content {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.consultation-btn {
flex-shrink: 0;
}
/* 余额状态卡片样式 */
.balance-card-arrears {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
}
.balance-card-low {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important;
}
/* 余额状态提示 */
.balance-alert {
margin-top: 16px;
}
.arrears-alert {
margin-bottom: 16px;
}
/* 确认框样式 */
:deep(.custom-message-box) {
.el-message-box__header {
padding: 20px 20px 10px;
}
.el-message-box__content {
padding: 20px;
font-size: 16px;
line-height: 1.6;
}
.el-message-box__btns {
padding: 10px 20px 20px;
}
.el-button--warning {
background-color: #e6a23c;
border-color: #e6a23c;
}
.el-button--warning:hover {
background-color: #cf9236;
border-color: #cf9236;
}
}
.low-balance-alert {
margin-bottom: 16px;
}
/* 充值方式选择 */
.recharge-methods-section {
margin-bottom: 32px;
}
.recharge-methods-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin-top: 20px;
}
.recharge-method-card {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.8);
border: 2px solid rgba(226, 232, 240, 0.6);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.recharge-method-card:hover {
border-color: rgba(59, 130, 246, 0.4);
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.recharge-method-card.active {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.05);
}
.method-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-right: 16px;
color: white;
}
.alipay-icon {
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
}
.wechat-icon {
background: linear-gradient(135deg, #07c160 0%, #06ad56 100%);
}
.transfer-icon {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
}
.method-content {
flex: 1;
}
.method-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin-bottom: 4px;
}
.method-description {
font-size: 14px;
color: #64748b;
}
.method-check {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
transition: background 0.2s;
}
.method-check.checked {
background: #3b82f6;
color: #fff;
}
.method-check.unchecked {
background: #e5e7eb;
color: #94a3b8;
}
/* 预设金额选择 */
.preset-amounts-section {
margin-bottom: 24px;
}
.preset-title {
font-size: 16px;
font-weight: 600;
color: #374151;
margin-bottom: 16px;
}
.preset-amounts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 8px;
}
.preset-amount-card {
background: rgba(248, 250, 252, 0.8);
border: 2px solid rgba(226, 232, 240, 0.6);
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.preset-amount-card:hover {
border-color: rgba(59, 130, 246, 0.4);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.preset-amount-card.active {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.preset-amount-main {
margin-bottom: 6px;
}
.preset-amount-value {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin-bottom: 2px;
}
.preset-bonus-info {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
font-size: 14px;
color: #64748b;
}
.bonus-label {
color: #10b981;
font-weight: 500;
}
.bonus-amount {
color: #10b981;
font-weight: 600;
}
.preset-amount-total {
font-size: 14px;
color: #374151;
font-weight: 600;
background: rgba(16, 185, 129, 0.1);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.custom-amount-card {
border-style: dashed;
}
.custom-amount-card .preset-amount-value {
color: #6b7280;
}
/* 赠送信息提示 */
.bonus-info {
margin-top: 12px;
}
.bonus-detail {
font-size: 14px;
font-weight: 600;
color: #10b981;
}
/* 充值表单 */
.recharge-form-section,
.transfer-info-section {
margin-top: 32px;
}
.recharge-form,
.transfer-form {
max-width: 500px;
margin-top: 20px;
}
.amount-input {
width: 100%;
}
.form-tip {
font-size: 12px;
color: #64748b;
margin-top: 4px;
}
.submit-btn {
width: 100%;
height: 48px;
font-size: 16px;
font-weight: 600;
}
/* 对公转账信息 */
.transfer-info-card {
background: rgba(248, 250, 252, 0.8);
border: 1px solid rgba(226, 232, 240, 0.6);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.transfer-info-item {
display: flex;
align-items: center;
margin-bottom: 16px;
padding: 12px 0;
border-bottom: 1px solid rgba(226, 232, 240, 0.4);
}
.transfer-info-item:last-child {
margin-bottom: 0;
border-bottom: none;
}
.info-label {
width: 120px;
font-size: 14px;
font-weight: 500;
color: #64748b;
}
.info-value {
flex: 1;
font-size: 16px;
color: #1e293b;
font-weight: 600;
}
.info-value.account-number {
font-family: 'Courier New', monospace;
background: rgba(255, 255, 255, 0.8);
padding: 8px 12px;
border-radius: 6px;
border: 1px solid rgba(226, 232, 240, 0.6);
margin-right: 12px;
}
.info-value.amount {
color: #dc2626;
font-weight: 700;
}
.copy-btn {
margin-left: 12px;
}
/* 转账说明 */
.transfer-notice {
margin-bottom: 32px;
}
.notice-list {
margin: 0;
padding-left: 20px;
line-height: 1.6;
}
.notice-list li {
margin-bottom: 8px;
color: #475569;
}
.notice-list li:last-child {
margin-bottom: 0;
}
/* 调试信息样式 */
.debug-info {
margin-top: 32px;
padding: 16px;
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
font-size: 14px;
color: #495057;
white-space: pre-wrap; /* 保持换行 */
word-break: break-all; /* 允许单词换行 */
}
.debug-info h4 {
margin-top: 0;
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
color: #212529;
}
/* 响应式设计 */
@media (max-width: 768px) {
.wallet-balance-card {
padding: 20px;
}
.balance-icon {
font-size: 36px;
margin-right: 16px;
}
.balance-amount {
font-size: 28px;
}
.benefit-content {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.consultation-btn {
align-self: flex-end;
}
.recharge-methods-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.recharge-method-card {
padding: 16px;
}
.method-icon {
width: 40px;
height: 40px;
font-size: 20px;
margin-right: 12px;
}
.method-title {
font-size: 16px;
}
.transfer-info-item {
flex-direction: column;
align-items: flex-start;
}
.info-label {
width: auto;
margin-bottom: 8px;
}
.info-value.account-number {
margin-right: 0;
margin-bottom: 8px;
width: 100%;
}
.copy-btn {
margin-left: 0;
align-self: flex-start;
}
.benefits-list {
grid-template-columns: 1fr;
}
.qr-code-wrapper {
width: 160px;
height: 160px;
}
/* 预设金额选择响应式 */
.preset-amounts-grid {
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.preset-amount-card {
padding: 8px;
}
.preset-amount-value {
font-size: 14px;
}
.preset-bonus-info {
font-size: 9px;
}
.preset-amount-total {
font-size: 9px;
padding: 2px 4px;
}
}
/* 二维码弹窗样式 */
.qr-code-dialog :deep(.el-dialog) {
border-radius: 16px;
}
.qr-code-dialog :deep(.el-dialog__header) {
padding: 20px 20px 10px;
text-align: center;
}
.qr-code-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: 600;
}
.qr-code-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
}
.qr-code-wrapper {
width: 256px;
height: 256px;
display: flex;
align-items: center;
justify-content: center;
background: #ffffff;
border: 2px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.qr-code-canvas {
max-width: 100%;
max-height: 100%;
}
.qr-code-tip {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
text-align: center;
}
.qr-code-amount {
font-size: 16px;
font-weight: 600;
color: #dc2626;
margin-bottom: 12px;
text-align: center;
}
.qr-code-checking {
font-size: 14px;
color: #3b82f6;
margin-bottom: 24px;
text-align: center;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
.close-qr-btn {
width: 120px;
height: 40px;
}
</style>