diff --git a/src/mobile/views/cashier/CashierEnvPage.vue b/src/mobile/views/cashier/CashierEnvPage.vue index 0f88f33..6e35b1e 100644 --- a/src/mobile/views/cashier/CashierEnvPage.vue +++ b/src/mobile/views/cashier/CashierEnvPage.vue @@ -4,8 +4,8 @@ * 负责订单展示、支付项选择、发起支付与 payBody 分发 */ import type { CashierItemPublic, GatewayOrderInfo } from '@/shared/api/gateway' -import { showNotify, showSuccessToast } from 'vant' -import { computed, onMounted, onUnmounted, ref } from 'vue' +import { showNotify } from 'vant' +import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useRoute, useRouter } from 'vue-router' import { @@ -21,6 +21,7 @@ import { useGatewayOrderPoll } from '@/shared/hooks/use-gateway-order-poll' import { closeWebview } from '@/shared/pay/close-webview' import { useGatewayAuth } from '@/shared/pay/use-gateway-auth' import { detectClientEnv, isValidH5ClientEnv } from '@/shared/utils/client-env' +import { formatDateTime } from '@/shared/utils/datetime' import { cacheOrder, clearCachedOrder, getCachedOrder } from '@/shared/utils/order-cache' import { fenToYuan } from '@/shared/utils/pay-amount' import { invokeJsapiByEnv } from '@/shared/utils/pay-jsapi' @@ -72,13 +73,17 @@ const isTerminal = computed(() => paid.value || closed.value || failed.value || order.value.status === 'expired' || expired.value, ) -// 结果态类型(关单/失败/过期/加载失败),非空时渲染结果卡片 -type ResultState = 'closed' | 'failed' | 'expired' | 'loadError' +// 结果态类型(成功/关单/失败/过期/加载失败),非空时渲染结果卡片 +type ResultState = 'paid' | 'closed' | 'failed' | 'expired' | 'loadError' const resultState = computed(() => { // 订单加载失败优先(订单不存在/网络异常) if (loadError.value) { return 'loadError' } + // 支付成功(优先于其他终态,展示完整成功卡片) + if (paid.value) { + return 'paid' + } if (closed.value) { return 'closed' } @@ -94,6 +99,9 @@ const resultState = computed(() => { // 结果态展示元数据(图标/标题/副文案/主题色) const resultMeta = computed(() => { switch (resultState.value) { + case 'paid': + // 支付成功: 微信绿(与码牌/聚合一致) + return { icon: 'success', titleKey: 'cashier.paid', tipKey: 'cashier.paidTip', color: '#07c160' } case 'closed': return { icon: 'lock', titleKey: 'cashier.closed', tipKey: 'cashier.closedTip', color: '#fa8c16' } case 'failed': @@ -118,9 +126,8 @@ const { startPoll, stopPoll } = useGatewayOrderPoll({ order.value = latest }, onPaid(latest) { + // 轮询检测到已支付: 仅更新订单,成功卡片由 resultState 渲染、倒计时跳转由 watch 触发 order.value = latest - showSuccessToast(t('cashier.paid')) - redirectIfNeeded() }, }) @@ -148,16 +155,59 @@ function selectPayMethod(itemId: string) { } /** - * 支付成功后跳转商户 returnUrl + * 支付成功后跳转商户 returnUrl - 倒计时方案 + * + * 进入 paid 终态后,若有 returnUrl,展示 3 秒倒计时,归零自动 location.href; + * 用户也可点「返回商户」按钮立即跳转。替代原 1.2s setTimeout 闪跳, + * 让用户能看清成功卡片再跳。 */ -function redirectIfNeeded() { - if (order.value.returnUrl) { - setTimeout(() => { - window.location.href = order.value.returnUrl! - }, 1200) +const REDIRECT_COUNTDOWN_SECONDS = 3 +const redirectCountdown = ref(0) +let redirectTimer: ReturnType | null = null + +function clearRedirectTimer() { + if (redirectTimer) { + clearInterval(redirectTimer) + redirectTimer = null } } +/** + * 启动可视化倒计时,由 watch(resultState) 在进入 paid 终态时触发 + */ +function startRedirectCountdown() { + if (!order.value.returnUrl || redirectTimer) { + return + } + redirectCountdown.value = REDIRECT_COUNTDOWN_SECONDS + redirectTimer = setInterval(() => { + redirectCountdown.value-- + if (redirectCountdown.value <= 0) { + clearRedirectTimer() + if (order.value.returnUrl) { + window.location.href = order.value.returnUrl + } + } + }, 1000) +} + +/** + * 用户点击「返回商户」立即跳转(不等倒计时) + */ +function redirectNow() { + clearRedirectTimer() + if (order.value.returnUrl) { + window.location.href = order.value.returnUrl + } +} + +// 进入 paid 终态时启动倒计时跳转(由 status 变化驱动,所有触发 paid 的路径统一在此处理) +watch(resultState, (state) => { + if (state === 'paid') { + startRedirectCountdown() + } +}) + /** * 启动过期倒计时 */ @@ -324,9 +374,8 @@ async function ensureOpenIdOrRedirect(): Promise { async function handleJsapi(payload: string) { const status = await invokeJsapiByEnv(clientEnvParam, payload) if (status === 'ok') { + // JSAPI 调起支付成功: 仅更新状态,成功卡片由 resultState 渲染、倒计时跳转由 watch 触发 order.value.status = 'paid' - showSuccessToast(t('cashier.paid')) - redirectIfNeeded() return } if (status === 'cancel') { @@ -375,10 +424,9 @@ async function pay() { switch (action.type) { case 'success': order.value.status = 'paid' - // 支付成功后订单状态已变,清除缓存避免回显未付态 + // 支付成功后订单状态已变,清除缓存避免回显未付态 clearCachedOrder(orderNo) - showSuccessToast(t('cashier.paid')) - redirectIfNeeded() + // 成功卡片由 resultState 渲染、倒计时跳转由 watch 触发 break case 'redirect': redirectToPayUrl(action.url) @@ -421,6 +469,7 @@ onUnmounted(() => { if (countdownTimer) { clearInterval(countdownTimer) } + clearRedirectTimer() stopPoll() }) @@ -433,13 +482,51 @@ onUnmounted(() => { :tip-key="paying ? 'cashier.paying' : ''" /> - +
+
- + + + + + + + + + + + +
{{ t(resultMeta.titleKey) }} @@ -461,8 +548,23 @@ onUnmounted(() => { {{ t('cashier.payableAmount') }} ¥{{ amountYuan }}
+ +
+ {{ t('cashier.payTime') }} + {{ formatDateTime(order.payTime) }} +
- @@ -496,15 +598,8 @@ onUnmounted(() => { - -
-
- {{ t('cashier.paid') }} -
-
- -
+
{{ t('cashier.qrcodePay') }}
@@ -548,8 +643,8 @@ onUnmounted(() => {
- -