优化支付退款钱包链路

This commit is contained in:
yml
2026-06-03 21:47:04 +08:00
parent 04193ae687
commit c80d3f960e
26 changed files with 1342 additions and 504 deletions
+19 -201
View File
@@ -1,18 +1,14 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { CircleCheck, CreditCard, Loading, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
import QRCode from 'qrcode'
import { CircleCheck, Lock, Money, Refresh, Tickets, Wallet as WalletIcon } from '@element-plus/icons-vue'
import {
fetchWalletBalance,
fetchWalletLedger,
queryWalletRechargePayment,
startWalletRechargePayment,
type WalletAccount,
type WalletLedger,
} from '@/api/wallet'
import type { PaymentOrder } from '@/api/orders'
import { balanceTypeLabel, ledgerDirectionLabel, walletStatusLabel } from '@/utils/statusLabels'
import { formatDateTime } from '@/utils/time'
@@ -22,16 +18,6 @@ const ledger = ref<WalletLedger[]>([])
const currentPage = ref(1)
const currentPageSize = ref(20)
const total = ref(0)
const rechargeAmount = ref(100)
const recharging = ref(false)
const rechargeDialogVisible = ref(false)
const activeRechargePayment = ref<PaymentOrder | null>(null)
const rechargeQRCodeURL = ref('')
const qrGenerating = ref(false)
const checkingRecharge = ref(false)
let rechargePollingTimer: number | undefined
const quickRechargeAmounts = [50, 100, 200, 500]
const walletMetrics = computed(() => {
if (!account.value) {
@@ -41,14 +27,14 @@ const walletMetrics = computed(() => {
{
label: '可用余额',
value: formatMoney(account.value.available_balance),
hint: '可用于支付租号订单',
hint: '卖家结算收入累计到此账户',
icon: WalletIcon,
tone: 'available',
},
{
label: '冻结余额',
value: formatMoney(account.value.frozen_balance),
hint: '订单押金与待结算金额',
hint: '当前暂无冻结资金使用',
icon: Lock,
tone: 'frozen',
},
@@ -63,7 +49,6 @@ const walletMetrics = computed(() => {
})
onMounted(loadWallet)
onBeforeUnmount(stopRechargePolling)
async function loadWallet() {
loading.value = true
@@ -86,128 +71,14 @@ function loadLedgerPage() {
loadWallet()
}
async function handleRecharge() {
recharging.value = true
try {
const payment = await startWalletRechargePayment(rechargeAmount.value)
if (payment.paid) {
ElMessage.success('充值成功')
} else {
showRechargePaymentDialog(payment)
}
await loadWallet()
} catch (error) {
ElMessage.error(readError(error, '充值失败'))
} finally {
recharging.value = false
}
}
function handleWithdraw() {
ElMessage.info('提现功能待实现')
}
function showRechargePaymentDialog(payment: PaymentOrder) {
activeRechargePayment.value = payment
rechargeDialogVisible.value = true
void renderRechargeQRCode(payment)
startRechargePolling()
}
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
}
qrGenerating.value = true
try {
rechargeQRCodeURL.value = await QRCode.toDataURL(payURL, {
width: 240,
margin: 1,
errorCorrectionLevel: 'M',
color: {
dark: '#111827',
light: '#ffffff',
},
})
} catch {
ElMessage.error('二维码生成失败,请复制链接后扫码支付')
} finally {
qrGenerating.value = false
}
}
function startRechargePolling() {
stopRechargePolling()
rechargePollingTimer = window.setInterval(() => {
void refreshRechargePayment(true)
}, 3000)
}
function stopRechargePolling() {
if (rechargePollingTimer !== undefined) {
window.clearInterval(rechargePollingTimer)
rechargePollingTimer = undefined
}
}
async function refreshRechargePayment(silent = false) {
const paymentID = activeRechargePayment.value?.id
if (!paymentID || checkingRecharge.value) {
return
}
checkingRecharge.value = true
const currentPayURL = rechargePayURL()
try {
const payment = await queryWalletRechargePayment(paymentID)
if (payment.paid) {
stopRechargePolling()
ElMessage.success('充值成功')
rechargeDialogVisible.value = false
await loadWallet()
} else {
activeRechargePayment.value = payment
if (rechargePayURL(payment) !== currentPayURL) {
await renderRechargeQRCode(payment)
}
if (!silent) {
ElMessage.info('支付未完成')
}
}
} catch (error) {
if (!silent) {
ElMessage.error(readError(error, '刷新支付状态失败'))
}
} finally {
checkingRecharge.value = false
}
}
async function handleRefreshRechargePayment() {
await refreshRechargePayment(false)
}
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)}`
}
function pickQuickRecharge(amount: number) {
rechargeAmount.value = amount
}
function walletBizTypeLabel(type: string) {
const map: Record<string, string> = {
dev_recharge: '测试充值',
@@ -227,6 +98,10 @@ function walletBizTypeLabel(type: string) {
arbitration_release_frozen: '仲裁解冻',
arbitration_renter_refund: '仲裁退款',
arbitration_owner_income: '仲裁收入',
cancel_refund: '取消退款',
checkout_refund: '结账退款',
channel_deposit_refund: '押金退还',
withdraw_apply: '申请提现',
}
return map[type] || type || '-'
}
@@ -254,12 +129,15 @@ function amountPrefix(direction: string) {
<div class="page-header">
<p class="eyebrow">我的钱包</p>
<h1>资金账户</h1>
<p>查看余额、充值和每一笔资金变化,订单押金与租金都会记录在这里。</p>
<p>查看卖家结算收入、可提现余额和每一笔资金变化。</p>
</div>
<div class="wallet-hero-action">
<span>当前可用</span>
<strong>{{ account ? formatMoney(account.available_balance) : '¥0.00' }}</strong>
<el-button class="withdraw-button" :icon="Money" @click="handleWithdraw">申请提现</el-button>
<el-button class="withdraw-button" :icon="Money" disabled @click="handleWithdraw">
申请提现
<el-tag size="small" type="info" effect="plain" class="withdraw-tag">待开发</el-tag>
</el-button>
</div>
</div>
@@ -277,39 +155,6 @@ function amountPrefix(direction: string) {
</div>
<div class="wallet-workspace">
<section class="recharge-panel">
<div class="panel-title">
<div class="panel-title-icon">
<el-icon><CreditCard /></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: rechargeAmount === amount }"
@click="pickQuickRecharge(amount)"
>
{{ formatMoney(amount) }}
</button>
</div>
<div class="recharge-action-row">
<el-input-number
v-model="rechargeAmount"
:min="0.01"
:step="0.01"
:precision="2"
controls-position="right"
/>
<el-button type="primary" :icon="CreditCard" :loading="recharging" @click="handleRecharge">发起充值</el-button>
</div>
</section>
<section class="ledger-summary-card">
<div class="panel-title">
<div class="panel-title-icon is-blue">
@@ -370,38 +215,6 @@ function amountPrefix(direction: string) {
/>
</div>
<el-dialog
v-model="rechargeDialogVisible"
title="支付充值"
width="520px"
append-to-body
:z-index="4000"
class="wallet-pay-dialog"
@closed="stopRechargePolling"
>
<div v-if="activeRechargePayment" class="pay-dialog-body">
<div class="pay-summary">
<span>充值金额</span>
<strong>{{ formatMoney(activeRechargePayment.amount_cent / 100) }}</strong>
</div>
<div v-if="rechargePayURL()" class="pay-qr-panel">
<div class="pay-qr-box">
<el-icon v-if="qrGenerating" class="is-loading" :size="32"><Loading /></el-icon>
<img v-else-if="rechargeQRCodeURL" :src="rechargeQRCodeURL" alt="充值支付二维码" />
</div>
<div class="pay-scan-copy">
<strong>请使用微信或支付宝扫码支付</strong>
<span>不要在电脑浏览器直接打开该链接。扫码完成后将自动刷新,也可手动确认。</span>
</div>
</div>
<p v-else class="pay-hint">充值支付单已创建,请完成付款后刷新状态。</p>
</div>
<template #footer>
<div class="pay-dialog-footer">
<el-button type="primary" :loading="checkingRecharge" @click="handleRefreshRechargePayment">我已支付,刷新状态</el-button>
</div>
</template>
</el-dialog>
</section>
</template>
@@ -457,6 +270,11 @@ function amountPrefix(direction: string) {
margin-top: 14px;
}
.withdraw-tag {
margin-left: 8px;
vertical-align: middle;
}
.wallet-metric-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -528,7 +346,7 @@ function amountPrefix(direction: string) {
.wallet-workspace {
display: grid;
grid-template-columns: minmax(420px, 1.1fr) minmax(320px, 0.9fr);
grid-template-columns: minmax(0, 1fr);
gap: 16px;
}