pay: submit.php 直出收银台页(new-api 易支付对接)
- submit.php 验签建单后不再 302,直接渲染自包含收银台 HTML, 支付宝二维码由服务端 skip2/go-qrcode 生成 PNG data URL 内嵌 - 新增公开轮询端点 /cashier-status(页面经 /api 前缀访问), 与 PaySvc.CashierStatus 共用 cashierStatusReply - Cashier.vue 收银台页同步改版:取景框二维码、倒计时进度条, 去掉跳上游收银台的外链 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { NButton, NIcon, NSpin } from 'naive-ui'
|
||||
import { NIcon, NSpin } from 'naive-ui'
|
||||
import { CheckmarkCircleOutline, TimeOutline, CloseCircleOutline } from '@vicons/ionicons5'
|
||||
import QRCode from 'qrcode'
|
||||
import { client } from '../lib/api'
|
||||
@@ -16,8 +16,11 @@ const info = ref<{
|
||||
} | null>(null)
|
||||
const qrDataUrl = ref('')
|
||||
const notFound = ref(false)
|
||||
const redirecting = ref(false)
|
||||
const now = ref(Math.floor(Date.now() / 1000))
|
||||
const abort = new AbortController()
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let clockTimer: ReturnType<typeof setInterval> | null = null
|
||||
function stopPoll() {
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
}
|
||||
@@ -28,7 +31,7 @@ async function applyStatus(status: number, returnUrl?: string) {
|
||||
if (status === 1) {
|
||||
const d = await client.paySvc.cashierInfo({ tradeNo })
|
||||
if (d.data) info.value.cards = d.data.cards
|
||||
if (returnUrl) setTimeout(() => { location.href = returnUrl }, 1500)
|
||||
if (returnUrl) { redirecting.value = true; setTimeout(() => { location.href = returnUrl }, 1500) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +50,17 @@ const statusLabel = computed(() => {
|
||||
return { 0: '等待支付', 1: '支付成功', 3: '订单已关闭' }[info.value.status] ?? '订单已关闭'
|
||||
})
|
||||
|
||||
const remainSec = computed(() => {
|
||||
if (!info.value || !info.value.expiredAt) return 0
|
||||
return Math.max(0, info.value.expiredAt - now.value)
|
||||
})
|
||||
const remainText = computed(() => {
|
||||
const m = Math.floor(remainSec.value / 60)
|
||||
const s = remainSec.value % 60
|
||||
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
|
||||
})
|
||||
const remainPct = computed(() => Math.min(100, (remainSec.value / 1800) * 100))
|
||||
|
||||
async function load() {
|
||||
const r = await client.paySvc.cashierInfo({ tradeNo })
|
||||
if (r.status !== 0 || !r.data) { notFound.value = true; return }
|
||||
@@ -72,8 +86,11 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
onUnmounted(() => { abort.abort(); stopPoll() })
|
||||
onMounted(() => {
|
||||
load()
|
||||
clockTimer = setInterval(() => { now.value = Math.floor(Date.now() / 1000) }, 1000)
|
||||
})
|
||||
onUnmounted(() => { abort.abort(); stopPoll(); if (clockTimer) clearInterval(clockTimer) })
|
||||
|
||||
function fen2yuan(fen: number): string { return (fen / 100).toFixed(2) }
|
||||
</script>
|
||||
@@ -94,21 +111,30 @@ function fen2yuan(fen: number): string { return (fen / 100).toFixed(2) }
|
||||
|
||||
<template v-else>
|
||||
<div class="cash-subject">{{ info!.subject || '商品订单' }}</div>
|
||||
<div class="cash-amount">¥{{ fen2yuan(info!.money) }}</div>
|
||||
<div class="cash-amount"><span class="cash-cny">¥</span>{{ fen2yuan(info!.money) }}</div>
|
||||
|
||||
<div v-if="info!.status === 0" class="cash-qr-area">
|
||||
<img v-if="qrDataUrl" :src="qrDataUrl" class="cash-qr" alt="支付宝扫码" />
|
||||
<div class="cash-qr-frame">
|
||||
<img v-if="qrDataUrl" :src="qrDataUrl" class="cash-qr" alt="支付宝扫码" />
|
||||
<div v-else class="cash-qr-empty">二维码生成中…</div>
|
||||
<div class="cash-qr-corner tl" /><div class="cash-qr-corner tr" />
|
||||
<div class="cash-qr-corner bl" /><div class="cash-qr-corner br" />
|
||||
</div>
|
||||
<div class="cash-qr-tip">
|
||||
<n-icon><TimeOutline /></n-icon> 请使用 <b>支付宝</b> 扫码支付
|
||||
<span class="cash-alipay-badge">支</span>
|
||||
打开<b>支付宝</b>扫一扫,向「池易」付款
|
||||
</div>
|
||||
<div class="cash-countdown">
|
||||
<n-icon><TimeOutline /></n-icon>
|
||||
支付剩余时间 {{ remainText }}
|
||||
<div class="cash-countdown-bar"><i :style="{ width: remainPct + '%' }" /></div>
|
||||
</div>
|
||||
<n-button v-if="info!.payurl" size="small" quaternary tag="a" :href="info!.payurl" target="_blank">
|
||||
在支付宝收银台继续 →
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="info!.status === 1" class="cash-state">
|
||||
<n-icon :size="56" color="#18a058"><CheckmarkCircleOutline /></n-icon>
|
||||
<div class="cash-ok">{{ statusLabel }}</div>
|
||||
<div v-if="redirecting" class="cash-redirect">正在跳回商家页面…</div>
|
||||
<pre v-if="info!.cards" class="cash-cards">{{ info!.cards }}</pre>
|
||||
</div>
|
||||
|
||||
@@ -135,12 +161,50 @@ function fen2yuan(fen: number): string { return (fen / 100).toFixed(2) }
|
||||
.cash-logo { font-size: 20px; font-weight: 900; }
|
||||
.cash-sub { font-size: 11px; color: var(--cb-ink-3, #8a8577); font-family: Consolas, monospace; margin-top: 4px; }
|
||||
.cash-subject { font-size: 14px; font-weight: 700; }
|
||||
.cash-amount { font-size: 34px; font-weight: 900; font-family: Consolas, monospace; margin: 8px 0 18px; }
|
||||
.cash-qr-area { display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
.cash-qr { border: 3px solid var(--cb-ink, #1c1b1a); border-radius: 12px; }
|
||||
.cash-qr-tip { font-size: 13px; color: var(--cb-ink-3, #8a8577); display: flex; align-items: center; gap: 6px; }
|
||||
.cash-amount { font-size: 40px; font-weight: 900; font-family: Consolas, monospace; margin: 8px 0 18px; }
|
||||
.cash-cny { font-size: 22px; vertical-align: 6px; margin-right: 2px; }
|
||||
.cash-qr-area { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
||||
.cash-qr-frame {
|
||||
position: relative; padding: 14px; background: #fff;
|
||||
border: 3px solid var(--cb-ink, #1c1b1a); border-radius: 16px;
|
||||
box-shadow: 5px 5px 0 var(--cb-ink, #1c1b1a);
|
||||
}
|
||||
.cash-qr { display: block; border-radius: 8px; }
|
||||
.cash-qr-empty {
|
||||
width: 240px; height: 240px; display: flex; align-items: center; justify-content: center;
|
||||
color: var(--cb-ink-3, #8a8577); font-size: 13px;
|
||||
}
|
||||
.cash-qr-corner {
|
||||
position: absolute; width: 18px; height: 18px; border: 3px solid #1677ff;
|
||||
}
|
||||
.cash-qr-corner.tl { top: -6px; left: -6px; border-right: none; border-bottom: none; border-radius: 8px 0 0 0; }
|
||||
.cash-qr-corner.tr { top: -6px; right: -6px; border-left: none; border-bottom: none; border-radius: 0 8px 0 0; }
|
||||
.cash-qr-corner.bl { bottom: -6px; left: -6px; border-right: none; border-top: none; border-radius: 0 0 0 8px; }
|
||||
.cash-qr-corner.br { bottom: -6px; right: -6px; border-left: none; border-top: none; border-radius: 0 0 8px 0; }
|
||||
.cash-qr-tip {
|
||||
font-size: 14px; font-weight: 700; color: var(--cb-ink, #1c1b1a);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.cash-alipay-badge {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
width: 24px; height: 24px; background: #1677ff; color: #fff;
|
||||
border-radius: 6px; font-size: 14px; font-weight: 900;
|
||||
border: 2px solid var(--cb-ink, #1c1b1a);
|
||||
}
|
||||
.cash-countdown {
|
||||
font-size: 12px; color: var(--cb-ink-3, #8a8577);
|
||||
display: flex; align-items: center; gap: 6px; width: 100%;
|
||||
}
|
||||
.cash-countdown-bar {
|
||||
flex: 1; height: 8px; background: var(--cb-cream-2, #e5e0d5);
|
||||
border: 2px solid var(--cb-ink, #1c1b1a); border-radius: 6px; overflow: hidden;
|
||||
}
|
||||
.cash-countdown-bar i {
|
||||
display: block; height: 100%; background: #1677ff; transition: width 1s linear;
|
||||
}
|
||||
.cash-state { display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 30px 0; font-size: 15px; font-weight: 700; }
|
||||
.cash-ok { color: #18a058; font-size: 18px; }
|
||||
.cash-redirect { font-size: 12px; color: var(--cb-ink-3, #8a8577); font-weight: 400; }
|
||||
.cash-cards {
|
||||
text-align: left; font-size: 12px; font-family: Consolas, monospace; max-height: 200px;
|
||||
overflow: auto; background: var(--cb-cream, #f7f4ec); border-radius: 8px; padding: 10px; margin: 0;
|
||||
|
||||
Reference in New Issue
Block a user