支持拉卡拉多渠道支付和测试充值

This commit is contained in:
yml
2026-06-06 21:46:41 +08:00
parent 4d40883da6
commit ba4c56c0cc
16 changed files with 1917 additions and 157 deletions
@@ -2,14 +2,18 @@
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { CircleCheck, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
import { CircleCheck, Coin, Loading, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
import QRCode from 'qrcode'
import {
fetchWalletBalance,
fetchWalletLedger,
queryWalletRechargePayment,
startWalletRechargePayment,
type WalletAccount,
type WalletLedger,
} from '@/features/wallet'
import type { PaymentOrder } from '@/features/orders'
import { balanceTypeLabel, ledgerDirectionLabel, walletStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
@@ -20,6 +24,15 @@ const ledger = ref<WalletLedger[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const isDev = import.meta.env.DEV
const quickRechargeAmounts = [0.01, 1, 10, 100]
const devRechargeAmount = ref(0.01)
const devRecharging = ref(false)
const rechargeDialogVisible = ref(false)
const activeRechargePayment = ref<PaymentOrder | null>(null)
const rechargeQRCodeURL = ref('')
const rechargeQRGenerating = ref(false)
const checkingRecharge = ref(false)
const walletMetrics = computed(() => {
if (!account.value) {
@@ -77,6 +90,96 @@ function handleWithdraw() {
router.push('/wallet/withdrawal')
}
function selectDevRechargeAmount(amount: number) {
devRechargeAmount.value = amount
}
function rechargePayURL(payment = activeRechargePayment.value) {
return payment?.jspay_url || payment?.td_code || payment?.jspay_info || ''
}
async function renderRechargeQRCode(payment = activeRechargePayment.value) {
const payURL = rechargePayURL(payment)
rechargeQRCodeURL.value = ''
if (!payURL) return
rechargeQRGenerating.value = true
try {
rechargeQRCodeURL.value = await QRCode.toDataURL(payURL, {
width: 220,
margin: 1,
errorCorrectionLevel: 'M',
color: {
dark: '#111827',
light: '#ffffff',
},
})
} catch {
ElMessage.error('二维码生成失败')
} finally {
rechargeQRGenerating.value = false
}
}
async function handleDevRecharge() {
if (!isDev) return
if (devRechargeAmount.value <= 0) {
ElMessage.warning('请输入充值金额')
return
}
devRecharging.value = true
try {
const payment = await startWalletRechargePayment(devRechargeAmount.value)
activeRechargePayment.value = payment
rechargeDialogVisible.value = true
await renderRechargeQRCode(payment)
if (payment.paid) {
ElMessage.success('充值成功')
await loadWallet()
} else {
ElMessage.success('测试支付单已创建')
}
} catch (error) {
ElMessage.error(readError(error, '创建测试充值失败'))
} finally {
devRecharging.value = false
}
}
async function checkDevRechargeStatus() {
if (!activeRechargePayment.value || checkingRecharge.value) return
checkingRecharge.value = true
try {
const payment = await queryWalletRechargePayment(activeRechargePayment.value.id)
activeRechargePayment.value = payment
await renderRechargeQRCode(payment)
if (payment.paid) {
ElMessage.success('充值成功')
rechargeDialogVisible.value = false
await loadWallet()
} else {
ElMessage.info('支付暂未完成')
}
} catch (error) {
ElMessage.error(readError(error, '查询支付状态失败'))
} finally {
checkingRecharge.value = false
}
}
function openRechargePayURL() {
const payURL = rechargePayURL()
if (!payURL) return
window.open(payURL, '_blank', 'noopener,noreferrer')
}
function readError(error: unknown, fallback: string) {
if (typeof error === 'object' && error && 'response' in error) {
const response = (error as { response?: { data?: { message?: string } } }).response
return response?.data?.message || fallback
}
return fallback
}
function formatMoney(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
}
@@ -156,6 +259,35 @@ function amountPrefix(direction: string) {
</div>
<div class="wallet-workspace">
<section v-if="isDev" class="recharge-panel">
<div class="panel-title">
<div class="panel-title-icon is-orange">
<el-icon><Coin /></el-icon>
</div>
<div>
<h2>测试充值</h2>
<p>开发环境可见用于快速验证当前支付渠道配置</p>
</div>
</div>
<div class="quick-amounts">
<button
v-for="amount in quickRechargeAmounts"
:key="amount"
type="button"
:class="{ active: devRechargeAmount === amount }"
@click="selectDevRechargeAmount(amount)"
>
{{ formatMoney(amount) }}
</button>
</div>
<div class="recharge-action-row">
<el-input-number v-model="devRechargeAmount" :min="0.01" :precision="2" :step="1" />
<el-button type="primary" :icon="Coin" :loading="devRecharging" @click="handleDevRecharge">
创建测试支付
</el-button>
</div>
</section>
<section class="ledger-summary-card">
<div class="panel-title">
<div class="panel-title-icon is-blue">
@@ -216,6 +348,38 @@ function amountPrefix(direction: string) {
/>
</div>
<el-dialog v-model="rechargeDialogVisible" title="测试充值支付" width="520px">
<div class="recharge-cashier">
<div class="cashier-amount">
<span>支付金额</span>
<strong>{{ activeRechargePayment ? formatMoney(activeRechargePayment.amount_cent / 100) : formatMoney(devRechargeAmount) }}</strong>
</div>
<div v-if="rechargePayURL()" class="cashier-qr">
<div class="qr-box">
<el-icon v-if="rechargeQRGenerating" class="qr-loading"><Loading /></el-icon>
<img v-else-if="rechargeQRCodeURL" :src="rechargeQRCodeURL" alt="测试充值支付二维码" />
<span v-else>支付链接已生成</span>
</div>
<el-button text type="primary" @click="openRechargePayURL">打开支付链接</el-button>
</div>
<el-alert
v-else
title="支付单已创建,但渠道未返回可展示的支付链接"
type="info"
:closable="false"
show-icon
/>
<div class="cashier-meta" v-if="activeRechargePayment">
<span>支付单号{{ activeRechargePayment.payment_no }}</span>
<span>渠道{{ activeRechargePayment.provider }}</span>
<span>状态{{ activeRechargePayment.status }}</span>
</div>
</div>
<template #footer>
<el-button @click="rechargeDialogVisible = false">关闭</el-button>
<el-button type="primary" :loading="checkingRecharge" @click="checkDevRechargeStatus">查询支付状态</el-button>
</template>
</el-dialog>
</section>
</template>
@@ -394,6 +558,11 @@ function amountPrefix(direction: string) {
color: #2563eb;
}
.panel-title-icon.is-orange {
background: #fff4ec;
color: #ff6b00;
}
.panel-title h2 {
margin: 0;
color: #111827;
@@ -437,6 +606,70 @@ function amountPrefix(direction: string) {
gap: 12px;
}
.recharge-cashier {
display: grid;
gap: 18px;
}
.cashier-amount {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border: 1px solid #e6eaf2;
border-radius: 8px;
background: #f8fafc;
}
.cashier-amount span,
.cashier-meta {
color: #64748b;
font-size: 13px;
}
.cashier-amount strong {
color: #111a44;
font-size: 24px;
}
.cashier-qr {
display: grid;
justify-items: center;
gap: 10px;
}
.qr-box {
display: grid;
width: 236px;
height: 236px;
place-items: center;
border: 1px solid #e6eaf2;
border-radius: 8px;
background: #ffffff;
}
.qr-box img {
width: 220px;
height: 220px;
}
.qr-loading {
color: #64748b;
font-size: 28px;
animation: spin 0.9s linear infinite;
}
.cashier-meta {
display: grid;
gap: 6px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.wallet-ledger-table {
overflow-x: auto;
border: 1px solid #e6eaf2;