mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-21 20:19:31 +00:00
feat(payment): standalone result page, method order & QR logos
- Payment result page is now a standalone page without business layout - Support EasyPay return_url params (out_trade_no, trade_status) - Payment methods sorted: EasyPay → Alipay → WeChat → Stripe - Alipay/WeChat use official brand SVG icons - QR code displays Alipay/WeChat logo in center with scan prompt
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
</label>
|
||||
<div class="grid grid-cols-2 gap-3 sm:flex">
|
||||
<button
|
||||
v-for="method in methods"
|
||||
v-for="method in sortedMethods"
|
||||
:key="method.type"
|
||||
type="button"
|
||||
:disabled="!method.available"
|
||||
@@ -20,11 +20,30 @@
|
||||
@click="method.available && emit('select', method.type)"
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
<span :class="['flex h-8 w-8 items-center justify-center rounded-md text-white', iconBgClass(method.type)]">
|
||||
<Icon v-if="isStripe(method.type)" name="creditCard" size="sm" />
|
||||
<span v-else class="text-sm font-bold">
|
||||
{{ iconLabel(method.type) }}
|
||||
</span>
|
||||
<!-- Alipay official logo -->
|
||||
<span v-if="method.type.includes('alipay')" class="flex h-8 w-8 items-center justify-center">
|
||||
<svg viewBox="0 0 1024 1024" class="h-8 w-8">
|
||||
<path d="M230.4 0h563.2A230.4 230.4 0 0 1 1024 230.4v563.2A230.4 230.4 0 0 1 793.6 1024H230.4A230.4 230.4 0 0 1 0 793.6V230.4A230.4 230.4 0 0 1 230.4 0z" fill="#1677FF"/>
|
||||
<path d="M716.8 604.16c-60.42-24.58-128.26-51.2-128.26-51.2s35.84-91.65 49.66-156.67c13.82-65.02 5.12-102.4-40.96-117.76-46.08-15.36-81.92 7.17-97.28 56.32-15.36 49.15-15.36 122.88 5.12 194.56-30.72 56.32-71.68 122.88-112.64 179.2-40.96 56.32-71.68 92.16-71.68 92.16s-97.28 35.84-143.36 76.8c-46.08 40.96-56.32 76.8-40.96 102.4 15.36 25.6 56.32 30.72 112.64-5.12 56.32-35.84 107.52-107.52 148.48-168.96 0 0 81.92-30.72 128-43.01 46.08-12.29 81.92-20.48 81.92-20.48s71.68 66.56 148.48 92.16c76.8 25.6 133.12 15.36 138.24-20.48 5.12-35.84-30.72-61.44-46.08-71.68-41.98-27.65-81.92-46.08-131.32-66.05zM293.63 855.04c-35.84 25.6-66.56 15.36-56.32-10.24 10.24-25.6 46.08-61.44 92.16-86.02 0 0 10.24-5.12 30.72-15.36-30.72 51.2-66.56 112.64-66.56 111.62zm225.28-491.52c-5.12-56.32 10.24-86.02 35.84-86.02s35.84 25.6 25.6 76.8c-10.24 51.2-30.72 107.52-30.72 107.52s-25.6-42.0-30.72-98.3zm-71.68 296.96c25.6-46.08 51.2-97.28 71.68-143.36 0 0 61.44 25.6 97.28 35.84-30.72 12.8-102.4 71.68-168.96 107.52z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
</span>
|
||||
<!-- WeChat Pay official logo -->
|
||||
<span v-else-if="method.type.includes('wxpay')" class="flex h-8 w-8 items-center justify-center">
|
||||
<svg viewBox="0 0 1024 1024" class="h-8 w-8">
|
||||
<path d="M230.4 0h563.2A230.4 230.4 0 0 1 1024 230.4v563.2A230.4 230.4 0 0 1 793.6 1024H230.4A230.4 230.4 0 0 1 0 793.6V230.4A230.4 230.4 0 0 1 230.4 0z" fill="#07C160"/>
|
||||
<path d="M408.06 429.06a25.6 25.6 0 1 0 0-51.2 25.6 25.6 0 0 0 0 51.2zm-152.06 0a25.6 25.6 0 1 0 0-51.2 25.6 25.6 0 0 0 0 51.2z" fill="#FFFFFF"/>
|
||||
<path d="M332 271c-119.3 0-216 79.7-216 178 0 55.1 28.9 104.9 74.2 138.7l-18.5 55.7 64.4-33.2c26.6 8.9 55.6 13.9 86 13.9 7.3 0 14.5-0.3 21.6-0.9-9-27.6-14.2-57-14.2-87.5C329.5 408.9 422.2 332.7 535 332.7c7.3 0 14.5 0.4 21.6 1-23.6-36.5-67.8-62.7-118.6-62.7H332z" fill="#FFFFFF"/>
|
||||
<path d="M710.4 593.9a20.5 20.5 0 1 0 0-41 20.5 20.5 0 0 0 0 41zm-121.6 0a20.5 20.5 0 1 0 0-41 20.5 20.5 0 0 0 0 41z" fill="#FFFFFF"/>
|
||||
<path d="M649.6 365.7c-99.1 0-179.5 69.3-179.5 154.8s80.4 154.8 179.5 154.8c21.3 0 41.8-3.2 60.8-9.2l53.5 27.5-15.4-46.2c37.6-28.1 61.1-69.4 61.1-115.0-0.1-85.4-80.5-154.7-160-166.7z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
</span>
|
||||
<!-- Stripe icon -->
|
||||
<span v-else-if="isStripe(method.type)" :class="['flex h-8 w-8 items-center justify-center rounded-md text-white', iconBgClass(method.type)]">
|
||||
<Icon name="creditCard" size="sm" />
|
||||
</span>
|
||||
<!-- EasyPay icon -->
|
||||
<span v-else :class="['flex h-8 w-8 items-center justify-center rounded-md text-white', iconBgClass(method.type)]">
|
||||
<span class="text-sm font-bold">{{ iconLabel(method.type) }}</span>
|
||||
</span>
|
||||
<span class="flex flex-col items-start leading-none">
|
||||
<span class="text-base font-semibold">{{ t(`payment.methods.${method.type}`) }}</span>
|
||||
@@ -42,6 +61,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
|
||||
@@ -51,7 +71,7 @@ export interface PaymentMethodOption {
|
||||
available: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
methods: PaymentMethodOption[]
|
||||
selected: string
|
||||
}>()
|
||||
@@ -62,28 +82,35 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
/** Fixed display order for payment methods */
|
||||
const METHOD_ORDER = ['easypay', 'alipay', 'wxpay', 'stripe']
|
||||
|
||||
const sortedMethods = computed(() => {
|
||||
return [...props.methods].sort((a, b) => {
|
||||
const ai = METHOD_ORDER.indexOf(a.type)
|
||||
const bi = METHOD_ORDER.indexOf(b.type)
|
||||
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi)
|
||||
})
|
||||
})
|
||||
|
||||
function isStripe(type: string) {
|
||||
return type === 'stripe'
|
||||
}
|
||||
|
||||
function iconBgClass(type: string): string {
|
||||
if (type === 'easypay') return 'bg-[#FF6B35]'
|
||||
if (type.includes('alipay')) return 'bg-[#00AEEF]'
|
||||
if (type.includes('wxpay')) return 'bg-[#07C160]'
|
||||
if (type === 'stripe') return 'bg-[#635bff]'
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
|
||||
function iconLabel(type: string): string {
|
||||
if (type === 'easypay') return '易'
|
||||
if (type.includes('alipay')) return '\u652f'
|
||||
if (type.includes('wxpay')) return '\u5fae'
|
||||
return type[0]?.toUpperCase() || '?'
|
||||
}
|
||||
|
||||
function methodSelectedClass(type: string): string {
|
||||
if (type === 'easypay') return 'border-[#FF6B35] bg-orange-50 text-gray-900 shadow-sm dark:bg-orange-950 dark:text-gray-100'
|
||||
if (type.includes('alipay')) return 'border-[#00AEEF] bg-blue-50 text-gray-900 shadow-sm dark:bg-blue-950 dark:text-gray-100'
|
||||
if (type.includes('alipay')) return 'border-[#1677FF] bg-blue-50 text-gray-900 shadow-sm dark:bg-blue-950 dark:text-gray-100'
|
||||
if (type.includes('wxpay')) return 'border-[#07C160] bg-green-50 text-gray-900 shadow-sm dark:bg-green-950 dark:text-gray-100'
|
||||
if (type === 'stripe') return 'border-[#635bff] bg-indigo-50 text-gray-900 shadow-sm dark:bg-indigo-950 dark:text-gray-100'
|
||||
return 'border-primary-500 bg-primary-50 text-gray-900 shadow-sm dark:bg-primary-950 dark:text-gray-100'
|
||||
|
||||
@@ -5095,6 +5095,10 @@ export default {
|
||||
},
|
||||
qr: {
|
||||
scanToPay: 'Scan to Pay',
|
||||
scanAlipay: 'Alipay QR Payment',
|
||||
scanWxpay: 'WeChat QR Payment',
|
||||
scanAlipayHint: 'Open Alipay on your phone and scan the QR code to pay',
|
||||
scanWxpayHint: 'Open WeChat on your phone and scan the QR code to pay',
|
||||
payInNewWindow: 'Complete Payment in New Window',
|
||||
payInNewWindowHint: 'The payment page has opened in a new window. Please complete the payment there and return to this page.',
|
||||
openPayWindow: 'Reopen Payment Page',
|
||||
|
||||
@@ -5292,6 +5292,10 @@ export default {
|
||||
},
|
||||
qr: {
|
||||
scanToPay: '请扫码支付',
|
||||
scanAlipay: '支付宝扫码支付',
|
||||
scanWxpay: '微信扫码支付',
|
||||
scanAlipayHint: '请使用手机打开支付宝,扫描二维码完成支付',
|
||||
scanWxpayHint: '请使用手机打开微信,扫描二维码完成支付',
|
||||
payInNewWindow: '请在新窗口中完成支付',
|
||||
payInNewWindowHint: '支付页面已在新窗口打开,请在新窗口中完成支付后返回此页面',
|
||||
openPayWindow: '重新打开支付页面',
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
<AppLayout>
|
||||
<div class="mx-auto flex max-w-md flex-col items-center space-y-6 py-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{{ qrUrl ? t('payment.qr.scanToPay') : t('payment.qr.payInNewWindow') }}
|
||||
{{ qrUrl ? scanTitle : t('payment.qr.payInNewWindow') }}
|
||||
</h2>
|
||||
<div v-if="qrUrl" class="rounded-2xl bg-white p-6 shadow-lg dark:bg-dark-800">
|
||||
<canvas ref="qrCanvas" class="mx-auto"></canvas>
|
||||
</div>
|
||||
<!-- Scan prompt for QR code -->
|
||||
<p v-if="qrUrl && !expired && scanHint" class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ scanHint }}
|
||||
</p>
|
||||
<div v-if="expired" class="text-center">
|
||||
<p class="text-lg font-medium text-red-500">{{ t('payment.qr.expired') }}</p>
|
||||
<button class="btn btn-primary mt-4" @click="router.push('/purchase')">{{ t('payment.result.backToRecharge') }}</button>
|
||||
@@ -52,6 +56,7 @@ const orderId = ref(0)
|
||||
const remainingSeconds = ref(0)
|
||||
const expired = ref(false)
|
||||
const cancelling = ref(false)
|
||||
const paymentType = ref('')
|
||||
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null
|
||||
@@ -62,10 +67,71 @@ const countdownDisplay = computed(() => {
|
||||
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
|
||||
})
|
||||
|
||||
const isAlipay = computed(() => paymentType.value.includes('alipay'))
|
||||
const isWxpay = computed(() => paymentType.value.includes('wxpay'))
|
||||
|
||||
const scanTitle = computed(() => {
|
||||
if (isAlipay.value) return t('payment.qr.scanAlipay')
|
||||
if (isWxpay.value) return t('payment.qr.scanWxpay')
|
||||
return t('payment.qr.scanToPay')
|
||||
})
|
||||
|
||||
const scanHint = computed(() => {
|
||||
if (isAlipay.value) return t('payment.qr.scanAlipayHint')
|
||||
if (isWxpay.value) return t('payment.qr.scanWxpayHint')
|
||||
return ''
|
||||
})
|
||||
|
||||
// Alipay logo SVG as data URI (blue brand mark)
|
||||
const ALIPAY_LOGO = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#1677FF"/><text x="32" y="44" font-family="Arial,sans-serif" font-size="32" font-weight="bold" fill="white" text-anchor="middle">支</text></svg>')}`
|
||||
|
||||
// WeChat Pay logo SVG as data URI (green brand mark)
|
||||
const WXPAY_LOGO = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#07C160"/><path d="M20 22c-8 0-14.5 5.4-14.5 12.1 0 3.8 2 7.2 5.1 9.5l-1.3 3.8 4.4-2.3c1.8.6 3.8 1 5.9 1 .5 0 1 0 1.5-.1-.6-1.9-1-3.9-1-6C20.1 33 27 27 35.5 27c.5 0 1 0 1.5.1C35.4 23.4 28.2 22 20 22zm-5 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm10 0a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" fill="white"/><path d="M50.5 40c0-5.8-5.5-10.5-12.2-10.5S26 34.2 26 40s5.5 10.5 12.3 10.5c1.5 0 2.8-.2 4.1-.6l3.6 2-1-3.2c2.6-2 4.1-4.8 4.1-7.9zm-16.2-1.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm8 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z" fill="white"/></svg>')}`
|
||||
|
||||
function getLogoForType(): string | null {
|
||||
if (isAlipay.value) return ALIPAY_LOGO
|
||||
if (isWxpay.value) return WXPAY_LOGO
|
||||
return null
|
||||
}
|
||||
|
||||
async function renderQR() {
|
||||
await nextTick()
|
||||
if (qrCanvas.value && qrUrl.value) {
|
||||
await QRCode.toCanvas(qrCanvas.value, qrUrl.value, { width: 256, margin: 2 })
|
||||
if (!qrCanvas.value || !qrUrl.value) return
|
||||
|
||||
// Use high error correction to support logo overlay
|
||||
const logoSrc = getLogoForType()
|
||||
await QRCode.toCanvas(qrCanvas.value, qrUrl.value, {
|
||||
width: 256,
|
||||
margin: 2,
|
||||
errorCorrectionLevel: logoSrc ? 'H' : 'M',
|
||||
})
|
||||
|
||||
if (!logoSrc) return
|
||||
|
||||
// Draw logo in center of QR code
|
||||
const canvas = qrCanvas.value
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
const img = new Image()
|
||||
img.src = logoSrc
|
||||
img.onload = () => {
|
||||
const logoSize = 48
|
||||
const x = (canvas.width - logoSize) / 2
|
||||
const y = (canvas.height - logoSize) / 2
|
||||
// White background with rounded corners
|
||||
const pad = 5
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
const r = 6
|
||||
ctx.moveTo(x - pad + r, y - pad)
|
||||
ctx.arcTo(x + logoSize + pad, y - pad, x + logoSize + pad, y + logoSize + pad, r)
|
||||
ctx.arcTo(x + logoSize + pad, y + logoSize + pad, x - pad, y + logoSize + pad, r)
|
||||
ctx.arcTo(x - pad, y + logoSize + pad, x - pad, y - pad, r)
|
||||
ctx.arcTo(x - pad, y - pad, x + logoSize + pad, y - pad, r)
|
||||
ctx.fill()
|
||||
// Draw logo
|
||||
ctx.drawImage(img, x, y, logoSize, logoSize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +188,7 @@ onMounted(() => {
|
||||
orderId.value = Number(route.query.order_id) || 0
|
||||
qrUrl.value = (route.query.qr as string) || ''
|
||||
payUrl.value = (route.query.pay_url as string) || ''
|
||||
paymentType.value = (route.query.payment_type as string) || ''
|
||||
|
||||
// Calculate countdown from expiresAt
|
||||
const expiresAtStr = route.query.expires_at as string
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
<template>
|
||||
<AppLayout>
|
||||
<div class="mx-auto flex max-w-md flex-col items-center space-y-6 py-16">
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 dark:bg-dark-900">
|
||||
<div class="w-full max-w-md space-y-6">
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-20">
|
||||
<div class="h-8 w-8 animate-spin rounded-full border-4 border-primary-500 border-t-transparent"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div v-if="isSuccess" class="text-center">
|
||||
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<svg class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<!-- Status Icon -->
|
||||
<div class="text-center">
|
||||
<div v-if="isSuccess"
|
||||
class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/30">
|
||||
<svg class="h-10 w-10 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-4 text-2xl font-bold text-gray-900 dark:text-white">{{ t('payment.result.success') }}</h2>
|
||||
</div>
|
||||
<div v-else class="text-center">
|
||||
<div class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
|
||||
<div v-else
|
||||
class="mx-auto flex h-20 w-20 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/30">
|
||||
<svg class="h-10 w-10 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-4 text-2xl font-bold text-gray-900 dark:text-white">{{ t('payment.result.failed') }}</h2>
|
||||
<h2 class="mt-4 text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{{ isSuccess ? t('payment.result.success') : t('payment.result.failed') }}
|
||||
</h2>
|
||||
</div>
|
||||
<div v-if="order" class="w-full rounded-xl bg-gray-50 p-4 dark:bg-dark-800">
|
||||
<div class="space-y-2 text-sm">
|
||||
<!-- Order Info -->
|
||||
<div v-if="order" class="rounded-xl bg-white p-5 shadow-sm dark:bg-dark-800">
|
||||
<div class="space-y-3 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">#{{ order.id }}</span>
|
||||
@@ -41,20 +46,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 pt-4">
|
||||
<button class="btn btn-secondary" @click="router.push('/purchase')">{{ t('payment.result.backToRecharge') }}</button>
|
||||
<button class="btn btn-primary" @click="router.push('/orders')">{{ t('payment.result.viewOrders') }}</button>
|
||||
<!-- EasyPay return info (when no order loaded) -->
|
||||
<div v-else-if="returnInfo" class="rounded-xl bg-white p-5 shadow-sm dark:bg-dark-800">
|
||||
<div class="space-y-3 text-sm">
|
||||
<div v-if="returnInfo.outTradeNo" class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.orderId') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ returnInfo.outTradeNo }}</span>
|
||||
</div>
|
||||
<div v-if="returnInfo.money" class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.amount') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">¥{{ returnInfo.money }}</span>
|
||||
</div>
|
||||
<div v-if="returnInfo.type" class="flex justify-between">
|
||||
<span class="text-gray-500 dark:text-gray-400">{{ t('payment.orders.paymentMethod') }}</span>
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ t('payment.methods.' + returnInfo.type, returnInfo.type) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-3">
|
||||
<button class="btn btn-secondary flex-1" @click="router.push('/purchase')">{{ t('payment.result.backToRecharge') }}</button>
|
||||
<button class="btn btn-primary flex-1" @click="router.push('/orders')">{{ t('payment.result.viewOrders') }}</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AppLayout from '@/components/layout/AppLayout.vue'
|
||||
import OrderStatusBadge from '@/components/payment/OrderStatusBadge.vue'
|
||||
import { usePaymentStore } from '@/stores/payment'
|
||||
import type { PaymentOrder } from '@/types/payment'
|
||||
@@ -67,16 +89,55 @@ const paymentStore = usePaymentStore()
|
||||
const order = ref<PaymentOrder | null>(null)
|
||||
const loading = ref(true)
|
||||
|
||||
interface ReturnInfo {
|
||||
outTradeNo: string
|
||||
money: string
|
||||
type: string
|
||||
tradeStatus: string
|
||||
}
|
||||
const returnInfo = ref<ReturnInfo | null>(null)
|
||||
|
||||
const isSuccess = computed(() => {
|
||||
// Internal flow: explicit status=success
|
||||
if (route.query.status === 'success') return true
|
||||
if (!order.value) return false
|
||||
return order.value.status === 'COMPLETED' || order.value.status === 'PAID'
|
||||
// EasyPay return flow: trade_status=TRADE_SUCCESS
|
||||
if (route.query.trade_status === 'TRADE_SUCCESS') return true
|
||||
// Order-based check
|
||||
if (order.value) {
|
||||
return order.value.status === 'COMPLETED' || order.value.status === 'PAID'
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
/** Extract numeric order ID from out_trade_no like "sub2_46" → 46 */
|
||||
function parseOutTradeNo(outTradeNo: string): number {
|
||||
const match = outTradeNo.match(/_(\d+)$/)
|
||||
return match ? Number(match[1]) : 0
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const orderId = Number(route.query.order_id)
|
||||
// Try order_id first (internal navigation from QRCode/Stripe pages)
|
||||
let orderId = Number(route.query.order_id) || 0
|
||||
|
||||
// Fallback: EasyPay return URL with out_trade_no
|
||||
if (!orderId && route.query.out_trade_no) {
|
||||
const outTradeNo = route.query.out_trade_no as string
|
||||
orderId = parseOutTradeNo(outTradeNo)
|
||||
// Store return info for display when order lookup fails
|
||||
returnInfo.value = {
|
||||
outTradeNo,
|
||||
money: (route.query.money as string) || '',
|
||||
type: (route.query.type as string) || '',
|
||||
tradeStatus: (route.query.trade_status as string) || '',
|
||||
}
|
||||
}
|
||||
|
||||
if (orderId) {
|
||||
order.value = await paymentStore.pollOrderStatus(orderId)
|
||||
try {
|
||||
order.value = await paymentStore.pollOrderStatus(orderId)
|
||||
} catch {
|
||||
// Order lookup failed, will show returnInfo fallback
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
@@ -233,10 +233,10 @@ async function createOrder(orderAmount: number, orderType: string, planId?: numb
|
||||
if (result.client_secret) {
|
||||
router.push({ path: '/payment/stripe', query: { order_id: String(result.order_id), client_secret: result.client_secret } })
|
||||
} else if (result.qr_code) {
|
||||
router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), qr: result.qr_code || '', pay_url: result.pay_url || '', expires_at: result.expires_at || '' } })
|
||||
router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), qr: result.qr_code || '', pay_url: result.pay_url || '', expires_at: result.expires_at || '', payment_type: selectedMethod.value } })
|
||||
} else if (result.pay_url) {
|
||||
window.open(result.pay_url, '_blank')
|
||||
router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), pay_url: result.pay_url, expires_at: result.expires_at || '' } })
|
||||
router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), pay_url: result.pay_url, expires_at: result.expires_at || '', payment_type: selectedMethod.value } })
|
||||
} else {
|
||||
errorMessage.value = t('payment.result.failed')
|
||||
appStore.showError(errorMessage.value)
|
||||
@@ -272,7 +272,15 @@ onMounted(async () => {
|
||||
const limitsRes = await paymentAPI.getLimits()
|
||||
methodLimits.value = limitsRes.data
|
||||
} catch (e) { /* limits endpoint may not exist */ }
|
||||
if (enabledMethods.value.length) selectedMethod.value = enabledMethods.value[0]
|
||||
if (enabledMethods.value.length) {
|
||||
const METHOD_ORDER = ['easypay', 'alipay', 'wxpay', 'stripe']
|
||||
const sorted = [...enabledMethods.value].sort((a, b) => {
|
||||
const ai = METHOD_ORDER.indexOf(a)
|
||||
const bi = METHOD_ORDER.indexOf(b)
|
||||
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi)
|
||||
})
|
||||
selectedMethod.value = sorted[0]
|
||||
}
|
||||
if (config.value?.balance_disabled) {
|
||||
activeTab.value = 'subscription'
|
||||
await loadPlans()
|
||||
|
||||
Reference in New Issue
Block a user