优化支付退款钱包链路
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user