优化支付退款钱包链路

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
+239 -10
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import { ChatDotRound } from '@element-plus/icons-vue'
import { computed, onMounted, ref } from 'vue'
import { ChatDotRound, Loading } from '@element-plus/icons-vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import QRCode from 'qrcode'
import { fetchOrderChat } from '@/api/chats'
import { createDispute } from '@/api/disputes'
@@ -15,11 +16,13 @@ import {
counterCheckout,
fetchHandoffRecords,
fetchOrder,
payOrder,
queryOrderPayment,
startOrderPayment,
submitCheckout,
submitHandoff,
type HandoffRecord,
type Order,
type PaymentOrder,
} from '@/api/orders'
import { useSessionStore } from '@/stores/session'
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
@@ -30,7 +33,7 @@ const router = useRouter()
const session = useSessionStore()
const loading = ref(false)
const cancelling = ref(false)
const paying = ref(false)
const startingPayment = ref(false)
const handoffing = ref(false)
const confirming = ref(false)
const returning = ref(false)
@@ -43,6 +46,12 @@ const uploadingEvidence = ref(false)
const order = ref<Order | null>(null)
const handoffRecords = ref<HandoffRecord[]>([])
const handoffContent = ref('')
const paymentDialogVisible = ref(false)
const activePayment = ref<PaymentOrder | null>(null)
const paymentQRCodeURL = ref('')
const qrGenerating = ref(false)
const checkingPayment = ref(false)
let paymentPollingTimer: number | undefined
const checkoutForm = ref({
content: '',
consumable_amount: 0,
@@ -64,6 +73,7 @@ const disputeType = ref('cannot_login')
const disputeDescription = ref('')
const disputeEvidenceText = ref('')
const openingChat = ref(false)
let autoPayHandled = false
const isOwner = computed(() => order.value?.owner_id === session.userId)
const isRenter = computed(() => order.value?.renter_id === session.userId)
@@ -97,6 +107,7 @@ const remainingHafCoinM = computed(() => {
})
onMounted(loadOrder)
onBeforeUnmount(stopPaymentPolling)
async function loadOrder() {
loading.value = true
@@ -105,6 +116,16 @@ async function loadOrder() {
handoffRecords.value = await fetchHandoffRecords(String(route.params.id))
hydrateResourceUsage()
hydrateCounterForm()
if (
!autoPayHandled &&
route.query.pay === '1' &&
order.value?.status === 'pending_payment' &&
order.value?.renter_id === session.userId
) {
autoPayHandled = true
void router.replace({ path: route.path })
void handlePay()
}
} finally {
loading.value = false
}
@@ -126,18 +147,106 @@ async function handleCancel() {
async function handlePay() {
if (!order.value) return
paying.value = true
startingPayment.value = true
try {
await payOrder(order.value.id)
ElMessage.success('支付成功,等待号主交接')
await loadOrder()
const payment = await startOrderPayment(order.value.id)
if (payment.paid) {
ElMessage.success('支付成功,等待号主交接')
await loadOrder()
} else {
showPaymentDialog(payment)
}
} catch (error) {
ElMessage.error(readError(error, '支付失败'))
} finally {
paying.value = false
startingPayment.value = false
}
}
function showPaymentDialog(payment: PaymentOrder) {
activePayment.value = payment
paymentDialogVisible.value = true
void renderPaymentQRCode(payment)
startPaymentPolling()
}
function paymentPayURL(payment = activePayment.value) {
return payment?.jspay_url || payment?.td_code || payment?.jspay_info || ''
}
async function renderPaymentQRCode(payment = activePayment.value) {
const payURL = paymentPayURL(payment)
paymentQRCodeURL.value = ''
if (!payURL) {
return
}
qrGenerating.value = true
try {
paymentQRCodeURL.value = await QRCode.toDataURL(payURL, {
width: 240,
margin: 1,
errorCorrectionLevel: 'M',
color: {
dark: '#111827',
light: '#ffffff',
},
})
} catch {
ElMessage.error('二维码生成失败')
} finally {
qrGenerating.value = false
}
}
function startPaymentPolling() {
stopPaymentPolling()
paymentPollingTimer = window.setInterval(() => {
void refreshPaymentStatus(true)
}, 3000)
}
function stopPaymentPolling() {
if (paymentPollingTimer !== undefined) {
window.clearInterval(paymentPollingTimer)
paymentPollingTimer = undefined
}
}
async function refreshPaymentStatus(silent = false) {
if (!order.value || checkingPayment.value) {
return
}
checkingPayment.value = true
const currentPayURL = paymentPayURL()
try {
const payment = await queryOrderPayment(order.value.id)
if (payment.paid) {
stopPaymentPolling()
ElMessage.success('支付成功')
paymentDialogVisible.value = false
await loadOrder()
} else {
activePayment.value = payment
if (paymentPayURL(payment) !== currentPayURL) {
await renderPaymentQRCode(payment)
}
if (!silent) {
ElMessage.info('支付未完成')
}
}
} catch (error) {
if (!silent) {
ElMessage.error(readError(error, '刷新支付状态失败'))
}
} finally {
checkingPayment.value = false
}
}
async function handleRefreshPayment() {
await refreshPaymentStatus(false)
}
async function handleSubmitHandoff() {
if (!order.value) return
handoffing.value = true
@@ -511,7 +620,7 @@ function linesToList(value: string) {
<p>开始{{ formatDateTime(orderRentedAt(), '未开始') }}</p>
<p>预计截止{{ formatDateTime(orderEstimatedEndAt(), '未设置') }}</p>
<p v-if="order.status === 'pending_payment'">订单待支付支付后账号进入交接流程</p>
<el-button v-if="order.status === 'pending_payment' && isRenter" type="primary" :loading="paying" @click="handlePay">
<el-button v-if="order.status === 'pending_payment' && isRenter" type="primary" :loading="startingPayment" @click="handlePay">
支付订单
</el-button>
<el-button v-if="['pending_payment', 'pending_handoff'].includes(order.status)" type="danger" :loading="cancelling" @click="handleCancel">
@@ -662,6 +771,39 @@ function linesToList(value: string) {
{{ isCheckoutDisputeStage ? '提交结账争议' : '提交申诉' }}
</el-button>
</div>
<el-dialog
v-model="paymentDialogVisible"
title="订单支付"
width="520px"
append-to-body
:z-index="4000"
class="order-pay-dialog"
@closed="stopPaymentPolling"
>
<div v-if="activePayment" class="pay-dialog-body">
<div class="pay-summary">
<span>支付金额</span>
<strong>¥{{ (activePayment.amount_cent / 100).toFixed(2) }}</strong>
</div>
<div v-if="paymentPayURL()" 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="paymentQRCodeURL" :src="paymentQRCodeURL" 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="checkingPayment" @click="handleRefreshPayment">我已支付刷新状态</el-button>
</div>
</template>
</el-dialog>
</section>
</template>
@@ -722,4 +864,91 @@ function linesToList(value: string) {
display: block;
margin-top: 6px;
}
.pay-dialog-body {
display: grid;
gap: 14px;
}
.pay-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px;
border-radius: 8px;
background: #f7f9fc;
color: #64748b;
}
.pay-summary strong {
color: #111a44;
font-size: 22px;
}
.pay-qr-panel {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
align-items: center;
gap: 20px;
padding: 18px;
border-radius: 8px;
background: #f8fafc;
}
.pay-qr-box {
width: 240px;
height: 240px;
display: grid;
place-items: center;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #ffffff;
}
.pay-qr-box img {
width: 220px;
height: 220px;
display: block;
}
.pay-scan-copy {
display: grid;
gap: 8px;
color: #334155;
}
.pay-scan-copy strong {
color: #111a44;
font-size: 18px;
}
.pay-scan-copy span {
color: #64748b;
line-height: 1.7;
}
.pay-hint {
margin: 0;
color: #64748b;
}
.pay-dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
:global(.order-pay-dialog) {
position: relative;
z-index: 4001;
}
@media (max-width: 720px) {
.pay-qr-panel {
grid-template-columns: 1fr;
justify-items: center;
text-align: center;
}
}
</style>