mirror of
https://gitee.com/bootx/dax-pay-h5
synced 2026-08-13 07:45:42 +08:00
refactor(aggregate): 环境页路由合并至父级并清理 EnvPage
- 各环境页(alipay/wechat/union-pay/douyin)合并至 Aggregate 父级子路由, 避免 entry 到环境页跳转触发顶层 transition remount 白屏 - 删除通用 EnvPage.vue(已由各环境专属页替代) - defaultTitle 由聚合支付改为订单支付 - 码牌 CodePayShell 注释对齐
This commit is contained in:
@@ -47,78 +47,48 @@ const routeModuleList: Array<RouteRecordRaw> = [
|
||||
},
|
||||
],
|
||||
},
|
||||
// 聚合扫码:环境页须先于入口注册(避免 wechat 等被 :orderNo 吞掉)
|
||||
{
|
||||
path: RoutePath.AGGREGATE_WECHAT,
|
||||
name: 'AggregateWechat',
|
||||
component: Layout,
|
||||
meta: { title: '聚合支付' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AggregateWechatPage',
|
||||
component: () => import('@/mobile/views/aggregate/wechat/Index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: RoutePath.AGGREGATE_ALIPAY,
|
||||
name: 'AggregateAlipay',
|
||||
component: Layout,
|
||||
meta: { title: '聚合支付' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AggregateAlipayPage',
|
||||
component: () => import('@/mobile/views/aggregate/alipay/Index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: RoutePath.AGGREGATE_UNION,
|
||||
name: 'AggregateUnion',
|
||||
component: Layout,
|
||||
meta: { title: '聚合支付' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AggregateUnionPage',
|
||||
component: () => import('@/mobile/views/aggregate/union-pay/Index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: RoutePath.AGGREGATE_DOUYIN,
|
||||
name: 'AggregateDouyin',
|
||||
component: Layout,
|
||||
meta: { title: '聚合支付' },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'AggregateDouyinPage',
|
||||
component: () => import('@/mobile/views/aggregate/douyin/Index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
// 聚合扫码:入口 + 非宿主提示 统一在 Aggregate 父级下,避免跨顶层路由跳转
|
||||
// 触发 App.vue <transition mode="out-in"> 整页 remount Layout 与 Entry.vue 的
|
||||
// onBeforeMount router.replace 竞争(曾导致 /aggregate/unsupported 首次白屏、刷新才正常)
|
||||
// 注:环境页 wechat/alipay/union-pay/douyin 仍为独立顶层路由(仅宿主内访问,无此问题)
|
||||
// 聚合扫码:入口 + 非宿主提示 + 各宿主环境页 统一在 Aggregate 父级下作为子路由
|
||||
// 让 App.vue 顶层 transition key(matched[0].name)在 entry → 环境页跳转时保持不变(都是 Aggregate),
|
||||
// 避免整页 remount 与 Entry.vue 的 onBeforeMount router.replace 竞争导致白屏
|
||||
// (曾发生在支付宝 webview:onBeforeMount 跨顶层 replace 与 <transition mode="out-in" appear> 的 enter 死锁)
|
||||
// 注意:静态段(unsupported/alipay/wechat/union-pay/douyin)必须排在 :orderNo 之前,否则会被当订单号吞掉
|
||||
// URL 契约不变,仍是 /aggregate/:orderNo、/aggregate/unsupported、/aggregate/{env}/:orderNo
|
||||
{
|
||||
path: RoutePath.AGGREGATE_GROUP,
|
||||
name: 'Aggregate',
|
||||
component: Layout,
|
||||
meta: {
|
||||
title: '聚合支付',
|
||||
},
|
||||
children: [
|
||||
// 非宿主提示:静态 path 段,须在 :orderNo 之前注册,避免 'unsupported' 被当 orderNo 吞掉
|
||||
// 非宿主提示:静态 path 段
|
||||
{
|
||||
path: 'unsupported',
|
||||
name: 'AggregateUnsupportedPage',
|
||||
component: () => import('@/mobile/views/aggregate/unsupported.vue'),
|
||||
},
|
||||
// 入口:UA 探测后 replace 到环境页
|
||||
// 微信环境页
|
||||
{
|
||||
path: 'wechat/:orderNo',
|
||||
name: 'AggregateWechatPage',
|
||||
component: () => import('@/mobile/views/aggregate/wechat/Index.vue'),
|
||||
},
|
||||
// 支付宝环境页
|
||||
{
|
||||
path: 'alipay/:orderNo',
|
||||
name: 'AggregateAlipayPage',
|
||||
component: () => import('@/mobile/views/aggregate/alipay/Index.vue'),
|
||||
},
|
||||
// 云闪付环境页
|
||||
{
|
||||
path: 'union-pay/:orderNo',
|
||||
name: 'AggregateUnionPage',
|
||||
component: () => import('@/mobile/views/aggregate/union-pay/Index.vue'),
|
||||
},
|
||||
// 抖音环境页
|
||||
{
|
||||
path: 'douyin/:orderNo',
|
||||
name: 'AggregateDouyinPage',
|
||||
component: () => import('@/mobile/views/aggregate/douyin/Index.vue'),
|
||||
},
|
||||
// 入口:UA 探测后 replace 到环境页(静态段已注册完毕,:orderNo 兜底匹配订单号)
|
||||
{
|
||||
path: ':orderNo',
|
||||
name: 'AggregateEntry',
|
||||
|
||||
@@ -12,12 +12,12 @@ defineOptions({ name: 'AggregateEntry' })
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
/** clientEnv → 路由 name */
|
||||
/** clientEnv → 路由 name(指向 Aggregate 父级下的环境页子路由,不跨顶层以避免 transition 竞争) */
|
||||
const ENV_ROUTE_NAME: Record<string, string> = {
|
||||
wechat: 'AggregateWechat',
|
||||
alipay: 'AggregateAlipay',
|
||||
union_pay: 'AggregateUnion',
|
||||
douyin: 'AggregateDouyin',
|
||||
wechat: 'AggregateWechatPage',
|
||||
alipay: 'AggregateAlipayPage',
|
||||
union_pay: 'AggregateUnionPage',
|
||||
douyin: 'AggregateDouyinPage',
|
||||
}
|
||||
|
||||
// onBeforeMount 探测并 replace:挂载前跳走,避免与 App.vue <transition mode="out-in" appear> 的 enter 动画
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 聚合扫码环境页(wechat/alipay/union_pay/douyin 共用)
|
||||
* clientEnv 由路由注入,不二次猜 UA(OAuth 回跳后仍稳定)
|
||||
*/
|
||||
import type { AggregateClientEnv } from '@/shared/utils/client-env'
|
||||
import { showNotify, showSuccessToast } from 'vant'
|
||||
import { onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
import QrCodeDisplay from '@/shared/components/pay/QrCodeDisplay.vue'
|
||||
import { useAggregatePay } from '@/shared/pay/use-aggregate-pay'
|
||||
import { isAggregateClientEnv } from '@/shared/utils/client-env'
|
||||
|
||||
defineOptions({ name: 'AggregateEnvPage' })
|
||||
|
||||
const props = defineProps<{
|
||||
/** 固定客户端环境 */
|
||||
clientEnv: AggregateClientEnv
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const orderNo = route.params.orderNo as string
|
||||
|
||||
if (!isAggregateClientEnv(props.clientEnv)) {
|
||||
throw new Error(`invalid aggregate clientEnv: ${props.clientEnv}`)
|
||||
}
|
||||
|
||||
const {
|
||||
loading,
|
||||
paying,
|
||||
authorizing,
|
||||
loadError,
|
||||
order,
|
||||
meta,
|
||||
payResult,
|
||||
showQrcode,
|
||||
qrContent,
|
||||
amountYuan,
|
||||
paid,
|
||||
terminal,
|
||||
expired,
|
||||
countdown,
|
||||
remainSeconds,
|
||||
bootstrap,
|
||||
doPay,
|
||||
} = useAggregatePay({
|
||||
orderNo,
|
||||
clientEnv: props.clientEnv,
|
||||
device: 'mobile',
|
||||
t,
|
||||
onPaid() {
|
||||
showSuccessToast(t('aggregate.paid'))
|
||||
},
|
||||
onError(message) {
|
||||
showNotify({ type: 'danger', message })
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 环境展示名
|
||||
*/
|
||||
function clientEnvLabel() {
|
||||
if (props.clientEnv === 'alipay') {
|
||||
return t('aggregate.clientEnv.alipay')
|
||||
}
|
||||
if (props.clientEnv === 'union_pay') {
|
||||
return t('aggregate.clientEnv.union')
|
||||
}
|
||||
if (props.clientEnv === 'douyin') {
|
||||
return t('aggregate.clientEnv.douyin')
|
||||
}
|
||||
return t('aggregate.clientEnv.wechat')
|
||||
}
|
||||
|
||||
/**
|
||||
* 终态文案
|
||||
*/
|
||||
function terminalMessage() {
|
||||
const s = order.value.status
|
||||
if (s === 'paid') {
|
||||
return t('aggregate.paid')
|
||||
}
|
||||
if (s === 'expired') {
|
||||
return t('aggregate.expired')
|
||||
}
|
||||
if (s === 'closed') {
|
||||
return t('aggregate.closed')
|
||||
}
|
||||
if (s === 'failed') {
|
||||
return t('aggregate.failed')
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
bootstrap()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="aggregate">
|
||||
<div class="aggregate__brand" />
|
||||
|
||||
<div v-if="loading || authorizing" class="aggregate__card">
|
||||
<van-loading color="#5d9dfe" size="24px" />
|
||||
<p v-if="authorizing" class="aggregate__tip">
|
||||
{{ t('aggregate.authorizing') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="loadError" class="aggregate__card">
|
||||
<p class="aggregate__error">
|
||||
{{ loadError }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="aggregate__card">
|
||||
<div class="aggregate__title">
|
||||
{{ order.title || t('aggregate.defaultTitle') }}
|
||||
</div>
|
||||
<div class="aggregate__amount">
|
||||
<span class="aggregate__currency">¥</span>{{ amountYuan }}
|
||||
</div>
|
||||
<div v-if="order.description" class="aggregate__desc">
|
||||
{{ order.description }}
|
||||
</div>
|
||||
<div class="aggregate__meta">
|
||||
<div>{{ t('aggregate.orderNo') }}: {{ order.orderNo }}</div>
|
||||
<div v-if="remainSeconds > 0 && !paid">
|
||||
{{ t('aggregate.countdown') }}: {{ countdown }}
|
||||
</div>
|
||||
<div>{{ t('aggregate.clientEnvLabel') }}: {{ clientEnvLabel() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="paid" class="aggregate__card aggregate__success">
|
||||
{{ t('aggregate.paid') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="terminal" class="aggregate__card">
|
||||
<p class="aggregate__error">
|
||||
{{ terminalMessage() }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="expired" class="aggregate__card">
|
||||
<p class="aggregate__error">
|
||||
{{ t('aggregate.expired') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="showQrcode && qrContent" class="aggregate__card">
|
||||
<p class="aggregate__tip">
|
||||
{{ t('aggregate.scanQr') }}
|
||||
</p>
|
||||
<div class="aggregate__qr">
|
||||
<QrCodeDisplay :content="qrContent" :size="200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="payResult?.payBody && payResult.payBodyType !== 'link' && payResult.payBodyType !== 'jsapi'"
|
||||
class="aggregate__card"
|
||||
>
|
||||
<div class="aggregate__pay-tip">
|
||||
{{ t('aggregate.useAppPay') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!paid && !terminal && !expired" class="aggregate__actions">
|
||||
<van-button
|
||||
type="primary"
|
||||
round
|
||||
block
|
||||
:loading="paying || authorizing"
|
||||
@click="doPay"
|
||||
>
|
||||
{{
|
||||
paying
|
||||
? t('aggregate.paying')
|
||||
: (meta.autoLaunch ? t('aggregate.retry') : t('aggregate.payNow'))
|
||||
}}
|
||||
</van-button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.aggregate {
|
||||
min-height: 100%;
|
||||
padding-bottom: 40px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.aggregate__brand {
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #5d9dfe, #4787f7);
|
||||
}
|
||||
|
||||
.aggregate__card {
|
||||
margin: -48px 16px 16px;
|
||||
padding: 24px 20px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 6%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.aggregate__title {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aggregate__amount {
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.aggregate__currency {
|
||||
font-size: 20px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.aggregate__desc {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.aggregate__meta {
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: left;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.aggregate__error {
|
||||
color: #ee0a24;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.aggregate__success {
|
||||
color: #07c160;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.aggregate__tip,
|
||||
.aggregate__pay-tip {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.aggregate__qr {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.aggregate__actions {
|
||||
margin: 0 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 聚合扫码:非微信/支付宝/云闪付/抖音宿主提示
|
||||
* 结构对齐 EnvPage(品牌条 + 上浮卡);四钱包仅图标、无标签,减少与描述重复
|
||||
* 结构对齐各环境页(品牌条 + 上浮卡);四钱包仅图标、无标签,减少与描述重复
|
||||
*/
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* 码牌收款共用 UI
|
||||
*
|
||||
* 视觉对齐聚合 EnvPage(上浮卡 + 灰阶 token)与收银结果态;
|
||||
* 视觉对齐聚合各环境页(上浮卡 + 灰阶 token)与收银结果态;
|
||||
* 通道品牌色由端页注入(微信绿 / 支付宝蓝)。
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -31,14 +31,12 @@ const routes: RouteRecordRaw[] = [
|
||||
path: RoutePath.AGGREGATE_UNSUPPORTED,
|
||||
name: 'PcAggregateUnsupported',
|
||||
component: () => import('@/pc/views/aggregate/Unsupported.vue'),
|
||||
meta: { title: '聚合支付' },
|
||||
},
|
||||
// 聚合扫码支付(跨端)
|
||||
{
|
||||
path: RoutePath.AGGREGATE,
|
||||
name: 'PcAggregate',
|
||||
component: () => import('@/pc/views/aggregate/Index.vue'),
|
||||
meta: { title: '聚合支付' },
|
||||
},
|
||||
// 商户对账单(PC 独占:移动端由注册表派生 device-only 存根)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "Aggregate Pay",
|
||||
"defaultTitle": "Pay Order",
|
||||
"orderNo": "Order No",
|
||||
"countdown": "Time left",
|
||||
"clientEnvLabel": "Scene",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "Aggregate Pay",
|
||||
"defaultTitle": "Pembayaran Pesanan",
|
||||
"orderNo": "Order No",
|
||||
"countdown": "Time left",
|
||||
"clientEnvLabel": "Scene",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "統合決済",
|
||||
"defaultTitle": "注文のお支払い",
|
||||
"orderNo": "注文番号",
|
||||
"countdown": "残り時間",
|
||||
"clientEnvLabel": "決済環境",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "통합 결제",
|
||||
"defaultTitle": "주문 결제",
|
||||
"orderNo": "주문번호",
|
||||
"countdown": "남은 시간",
|
||||
"clientEnvLabel": "결제 환경",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "Aggregate Pay",
|
||||
"defaultTitle": "Bayaran Pesanan",
|
||||
"orderNo": "Order No",
|
||||
"countdown": "Time left",
|
||||
"clientEnvLabel": "Scene",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "Aggregate Pay",
|
||||
"defaultTitle": "ชำระคำสั่งซื้อ",
|
||||
"orderNo": "Order No",
|
||||
"countdown": "Time left",
|
||||
"clientEnvLabel": "Scene",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "Aggregate Pay",
|
||||
"defaultTitle": "Thanh toán đơn hàng",
|
||||
"orderNo": "Order No",
|
||||
"countdown": "Time left",
|
||||
"clientEnvLabel": "Scene",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "聚合支付",
|
||||
"defaultTitle": "订单支付",
|
||||
"orderNo": "订单号",
|
||||
"countdown": "剩余时间",
|
||||
"clientEnvLabel": "支付环境",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "聚合支付",
|
||||
"defaultTitle": "訂單付款",
|
||||
"orderNo": "訂單號",
|
||||
"countdown": "剩餘時間",
|
||||
"clientEnvLabel": "支付環境",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"defaultTitle": "聚合支付",
|
||||
"defaultTitle": "訂單支付",
|
||||
"orderNo": "訂單號",
|
||||
"countdown": "剩餘時間",
|
||||
"clientEnvLabel": "支付環境",
|
||||
|
||||
@@ -33,22 +33,15 @@ export const RoutePath = {
|
||||
AGGREGATE: '/aggregate/:orderNo',
|
||||
/**
|
||||
* 聚合扫码父级分组(仅 Mobile 路由注册用)
|
||||
* Mobile 把入口 + unsupported 提示合并到此父级下作为子路由,让 App.vue
|
||||
* 顶层 transition key(matched[0].name)在 entry → unsupported 跳转时保持
|
||||
* 不变,避免整页 remount 与 onBeforeMount 内 router.replace 竞争导致白屏。
|
||||
* URL 契约不变,仍是 /aggregate/:orderNo 与 /aggregate/unsupported。
|
||||
* Mobile 把入口 + 非宿主提示 + 各宿主环境页(wechat/alipay/union-pay/douyin)全部
|
||||
* 合并到此父级下作为子路由,让 App.vue 顶层 transition key(matched[0].name)在
|
||||
* entry → 环境页跳转时保持不变(都是 Aggregate),避免整页 remount 与 Entry.vue 的
|
||||
* onBeforeMount router.replace 竞争导致白屏(曾发生在支付宝 webview)。
|
||||
* URL 契约不变:/aggregate/:orderNo、/aggregate/unsupported、/aggregate/{env}/:orderNo。
|
||||
* PC 仍用 AGGREGATE(/aggregate/:orderNo)作为单层路由。
|
||||
*/
|
||||
AGGREGATE_GROUP: '/aggregate',
|
||||
/** 聚合-微信环境页(Mobile) */
|
||||
AGGREGATE_WECHAT: '/aggregate/wechat/:orderNo',
|
||||
/** 聚合-支付宝环境页(Mobile) */
|
||||
AGGREGATE_ALIPAY: '/aggregate/alipay/:orderNo',
|
||||
/** 聚合-云闪付环境页(Mobile) */
|
||||
AGGREGATE_UNION: '/aggregate/union-pay/:orderNo',
|
||||
/** 聚合-抖音环境页(Mobile) */
|
||||
AGGREGATE_DOUYIN: '/aggregate/douyin/:orderNo',
|
||||
/** 聚合-不支持的宿主环境提示(Mobile) */
|
||||
/** 聚合-不支持的宿主环境提示(Mobile + PC 注册用) */
|
||||
AGGREGATE_UNSUPPORTED: '/aggregate/unsupported',
|
||||
/** 码牌支付入口(跨端:Mobile 分发/收款,PC 扫码引导;path 与后端 getCodeLink /h/{code} 一致) */
|
||||
CODE_PAY: '/h/:code',
|
||||
|
||||
Reference in New Issue
Block a user