From c0c46dca2ff04bd42313d1b2109d1ee75312141b Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Tue, 1 Sep 2026 16:33:10 +0800 Subject: [PATCH] feat(account): show WeChat QR payment dialog with status refresh --- src/views/AccountView/AccountView.vue | 183 ++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 9 deletions(-) diff --git a/src/views/AccountView/AccountView.vue b/src/views/AccountView/AccountView.vue index 21eb346..63a83cc 100644 --- a/src/views/AccountView/AccountView.vue +++ b/src/views/AccountView/AccountView.vue @@ -309,6 +309,41 @@ >创建并支付 + +
+
+ 应付金额 + {{ payDialog.amountText }} +
+
+
业务
{{ payDialog.businessType === 'sim_recharge_order' ? '通信卡充值' : '云媒体流量' }}
+
订单号
{{ payDialog.businessOrderId || '--' }}
+
支付单
{{ payDialog.transactionId || '--' }}
+
状态
{{ payDialog.statusText || '--' }}
+
+
+ 支付二维码 + 请使用微信扫码支付 +
+

{{ payDialog.error || '暂无二维码' }}

+
+ +
@@ -352,6 +387,20 @@ const simRecharging = ref(false) const pendingSimOrdersByCard = ref({}) const payingPendingSimId = ref(0) const cancellingPendingSimId = ref(0) +const payDialogOpen = ref(false) +const payDialogRefreshing = ref(false) +const payDialog = ref({ + title: '微信支付', + businessType: '', + businessOrderId: 0, + transactionId: 0, + amountText: '--', + status: '', + statusText: '', + qrSrc: '', + error: '' +}) + const editOpen = ref(false); const saving = ref(false); const editForm = ref({ name: '', email: '' }) const orderColumns = [ @@ -449,11 +498,100 @@ function payStatusName(status) { function isTrafficOrderActionable(row) { return row?.payStatus === 'unpaid' || row?.payStatus === 'processing' } -function describePayment(payment) { - if (!payment) return '支付已创建' - const amount = moneyFromFen(payment.totalFeeFen) - if (payment.providerPayload) return `支付已创建(${amount}),请按返回凭证完成支付` - return `支付已创建(${amount}),状态:${payment.status || '--'}` +function paymentStatusName(status) { + return ({ + unpaid: '待支付', + processing: '支付中', + paid: '已支付', + success: '已支付', + failed: '支付失败', + cancelled: '已取消', + closed: '已关闭' + })[status] || status || '--' +} +function looksLikeBase64(value) { + const text = String(value || '').replace(/\s+/g, '') + return text.length > 32 && /^[A-Za-z0-9+/=]+$/.test(text) +} +function resolvePaymentQrSrc(payload) { + if (payload == null) return '' + if (typeof payload !== 'string') { + if (typeof payload === 'object') { + const nested = payload.qrCode || payload.qrUrl || payload.image || payload.codeUrl || '' + return resolvePaymentQrSrc(nested) + } + return '' + } + const text = payload.trim() + if (!text) return '' + if (text.startsWith('data:image/')) return text + if (/^https?:\/\//i.test(text) || text.startsWith('/')) return text + if (text.startsWith('weixin://')) return '' + if (looksLikeBase64(text)) return `data:image/png;base64,${text.replace(/\s+/g, '')}` + // JSON 字符串里混入了 PNG 二进制时,尝试从 latin1 字节转 base64 + try { + const bytes = Array.from(text, (ch) => ch.charCodeAt(0) & 0xff) + let binary = '' + for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]) + if (bytes[0] === 0x89 && bytes[1] === 0x50) { + return `data:image/png;base64,${btoa(binary)}` + } + } catch (_) {} + return '' +} +function openPaymentDialog(payment, meta = {}) { + const qrSrc = resolvePaymentQrSrc(payment?.providerPayload) + payDialog.value = { + title: meta.title || '微信支付', + businessType: meta.businessType || payment?.businessType || '', + businessOrderId: meta.businessOrderId || payment?.businessOrderId || 0, + transactionId: payment?.transactionId || 0, + amountText: moneyFromFen(payment?.totalFeeFen), + status: payment?.status || '', + statusText: paymentStatusName(payment?.status), + qrSrc, + error: qrSrc ? '' : '支付已创建,但二维码无法展示;可关闭后点击「去支付」重试' + } + payDialogOpen.value = true + return payment +} +async function refreshPaymentStatus() { + const id = payDialog.value.transactionId + if (!id) { + ui.toast('缺少支付单号') + return + } + payDialogRefreshing.value = true + try { + const payment = await request.get(urls.PAYMENT(id)) + const qrSrc = resolvePaymentQrSrc(payment?.providerPayload) || payDialog.value.qrSrc + payDialog.value = { + ...payDialog.value, + status: payment?.status || payDialog.value.status, + statusText: paymentStatusName(payment?.status || payDialog.value.status), + amountText: moneyFromFen(payment?.totalFeeFen) || payDialog.value.amountText, + qrSrc, + error: qrSrc ? '' : payDialog.value.error + } + const paid = payment?.status === 'paid' || payment?.status === 'success' + await Promise.all([ + loadOrders(), + request.get(urls.TRAFFIC_BALANCE).then((data) => { + balance.value = data?.balanceGb == null ? '--' : Number(data.balanceGb).toFixed(1) + }).catch(() => {}), + loadSim().catch(() => {}) + ]) + if (paid) { + ui.toast('支付成功,流量/订单已刷新') + payDialogOpen.value = false + } else { + ui.toast(`当前状态:${paymentStatusName(payment?.status)},如已扫码请稍后再试`) + } + } catch (error) { + ui.toast(error.message || '刷新支付状态失败') + } finally { + payDialogRefreshing.value = false + } } async function createPayment(businessType, businessOrderId) { return request.post(urls.PAYMENTS, { businessType, businessOrderId }) @@ -529,7 +667,11 @@ async function createTrafficOrderAndPay() { orderCreated = true orderPageNum.value = 1 const payment = await createPayment('traffic_order', order.id) - ui.toast(describePayment(payment)) + openPaymentDialog(payment, { + title: '云媒体流量支付', + businessType: 'traffic_order', + businessOrderId: order.id + }) await Promise.all([loadOrders(), request.get(urls.TRAFFIC_BALANCE).then((data) => { balance.value = data?.balanceGb == null ? '--' : Number(data.balanceGb).toFixed(1) })]) @@ -547,7 +689,11 @@ async function payTrafficOrder(row) { payingOrderId.value = row.id try { const payment = await createPayment('traffic_order', row.id) - ui.toast(describePayment(payment)) + openPaymentDialog(payment, { + title: '云媒体流量支付', + businessType: 'traffic_order', + businessOrderId: row.id + }) await loadOrders() } catch (error) { ui.toast(error.message || '发起支付失败') @@ -617,7 +763,11 @@ async function payPendingSimOrder(order) { payingPendingSimId.value = order.id try { const payment = await createPayment('sim_recharge_order', order.id) - ui.toast(describePayment(payment)) + openPaymentDialog(payment, { + title: '通信卡充值支付', + businessType: 'sim_recharge_order', + businessOrderId: order.id + }) await refreshPendingSimOrder(order.id, order.simCardId) await loadSim() } catch (error) { @@ -660,7 +810,11 @@ async function submitSimRecharge() { orderCreated = true upsertPendingSimOrder(order) const payment = await createPayment('sim_recharge_order', order.id) - ui.toast(describePayment(payment)) + openPaymentDialog(payment, { + title: '通信卡充值支付', + businessType: 'sim_recharge_order', + businessOrderId: order.id + }) simRechargeOpen.value = false await refreshPendingSimOrder(order.id, order.simCardId) await loadSim() @@ -843,6 +997,17 @@ onMounted(load) .package-card-grid :deep(.t-is-disabled) { opacity: .6; } +.pay-qr-panel { display: flex; flex-direction: column; gap: 14px; } +.pay-qr-amount span { display: block; color: var(--muted); font-size: 10px; } +.pay-qr-amount strong { display: block; margin-top: 4px; color: #246fae; font-size: 28px; font-weight: 600; } +.pay-qr-meta { margin: 0; display: grid; gap: 6px; } +.pay-qr-meta > div { display: flex; gap: 10px; font-size: 12px; } +.pay-qr-meta dt { width: 48px; color: #74838d; } +.pay-qr-meta dd { margin: 0; color: #304552; word-break: break-all; } +.pay-qr-code { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 12px; border: 1px solid #d7e4ee; border-radius: 5px; background: #f5f9fc; } +.pay-qr-code img { width: 220px; height: 220px; object-fit: contain; background: #fff; } +.pay-qr-code small { color: #74838d; font-size: 11px; } +.pay-qr-error { margin: 0; padding: 12px; border-radius: 5px; background: #fff5f5; color: #bd4046; font-size: 12px; }