feat(aggregate): 支付宝客户端内扫码 URL 直跳 + 支付完成关闭 webview

- 支付宝客户端内 precreate 返回的 qrCode 为可拉起 URL, 直接 location.href 跳转, 不再渲染二维码

- 支付中 redirect/form 结果置 redirecting 标志, 跳转前不重置 paying 避免闪现订单卡

- 新增 closeOnPaidWithoutReturn, 支付成功无商户 returnUrl 时关闭 webview 回到宿主钱包
This commit is contained in:
bootx
2026-07-20 15:31:32 +08:00
parent 61d3fbf6e9
commit 741fcf29ac
2 changed files with 69 additions and 1 deletions

View File

@@ -0,0 +1,34 @@
/**
* 关闭当前 webview回到宿主钱包
*
* 适用场景:聚合支付完成后无商户 returnUrl 时,关闭页面回到支付宝/微信钱包。
* - 支付宝AlipayJSBridge.call('closeWebview')
* - 微信WeixinJSBridge.call('closeWindow')
* - 云闪付/抖音/浏览器window.close() 兜底(多数 webview 不生效,但无副作用)
*
* 与 [jsapi.ts] 的桥判断模式对称:宿主桥未注入时静默降级,不抛错
* (关闭是 fire-and-forget不能因为关不掉页面而报错影响已完成的支付体验
*/
export function closeWebview(): void {
if (typeof window === 'undefined') {
return
}
const w = window as any
// 支付宝:优先用 AlipayJSBridge
if (w.AlipayJSBridge?.call) {
w.AlipayJSBridge.call('closeWebview')
return
}
// 微信:用 WeixinJSBridge
if (w.WeixinJSBridge?.call) {
w.WeixinJSBridge.call('closeWindow')
return
}
// 兜底:云闪付/抖音/浏览器(多数不生效,无副作用)
if (typeof w.close === 'function') {
w.close()
}
}

View File

@@ -12,6 +12,7 @@ import {
getGatewayOrder,
} from '@/shared/api/gateway'
import { useGatewayOrderPoll } from '@/shared/hooks/use-gateway-order-poll'
import { closeWebview } from '@/shared/pay/close-webview'
import { invokeJsapiByEnv } from '@/shared/pay/jsapi'
import { buildAggregateEnvPath } from '@/shared/utils/client-env'
import { fenToYuan } from '@/shared/utils/pay-amount'
@@ -31,6 +32,12 @@ export interface UseAggregatePayOptions {
onError?: (message: string) => void
/** i18n 文案回调 */
t: (key: string) => string
/**
* 支付成功且无商户 returnUrl 时,自动关闭 webview 回到宿主钱包
* 聚合扫码场景建议开启(用户没有业务回跳地址,付完应回到支付宝/微信钱包)
* 与 returnUrl 跳转互斥:有 returnUrl 走跳转,无 returnUrl 且开关开启才关闭
*/
closeOnPaidWithoutReturn?: boolean
}
/**
@@ -54,11 +61,14 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
onPaid,
onError,
t,
closeOnPaidWithoutReturn = false,
} = options
const loading = ref(true)
const paying = ref(false)
const authorizing = ref(false)
// 跳转类支付结果标志: location.href 触发后页面卸载前 finally 不重置 paying, 避免闪现订单卡
let redirecting = false
const loadError = ref('')
const order = ref<GatewayOrderInfo>({})
const meta = ref<AggregatePayMeta>({})
@@ -96,9 +106,18 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
function redirectIfNeeded() {
if (order.value.returnUrl) {
// 有商户回跳地址:延迟跳转,让 toast 先显示
setTimeout(() => {
window.location.href = order.value.returnUrl!
}, 1200)
return
}
// 无 returnUrl 且开启自动关闭:延迟关闭 webview 回到宿主钱包
// 比 returnUrl 跳转稍晚,让成功 toast 显示完
if (closeOnPaidWithoutReturn) {
setTimeout(() => {
closeWebview()
}, 1500)
}
}
@@ -176,14 +195,25 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
redirectIfNeeded()
break
case 'redirect':
// 跳转类结果: 保持 paying, 避免 finally 重置导致跳转前闪现订单卡
redirecting = true
redirectToPayUrl(action.url)
break
case 'qrcode':
// 支付宝客户端内: precreate 返回的 qrCode 是可拉起的 URL, 直接跳转而非显示二维码
if (clientEnv === 'alipay' && /^https?:\/\//i.test(action.content)) {
redirecting = true
redirectToPayUrl(action.content)
startPoll(orderNo)
break
}
qrContent.value = action.content
showQrcode.value = true
startPoll(orderNo)
break
case 'form':
// 表单提交会导航离开本页, 同属跳转类
redirecting = true
submitPayForm(action.html)
startPoll(orderNo)
break
@@ -220,6 +250,7 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
return
}
paying.value = true
redirecting = false
showQrcode.value = false
qrContent.value = ''
try {
@@ -236,7 +267,10 @@ export function useAggregatePay(options: UseAggregatePayOptions) {
onError?.(e?.message || t('aggregate.payFail'))
}
finally {
paying.value = false
// 跳转类结果保持 paying=true, 让 loading 稳定显示到页面卸载
if (!redirecting) {
paying.value = false
}
}
}