186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||
import QRCode from 'qrcode'
|
||
|
||
import { queryOrderPayment, type PaymentOrder } from '@/features/orders/api/orders'
|
||
|
||
type PaidHandler = () => Promise<void> | void
|
||
|
||
/**
|
||
* 从支付单提取实际支付 URL。顺序与现网 PC/Mobile 一致:jspay_url → td_code → jspay_info。
|
||
*/
|
||
export function paymentPayURL(payment?: PaymentOrder | null): string {
|
||
return payment?.jspay_url || payment?.td_code || payment?.jspay_info || ''
|
||
}
|
||
|
||
function isHTTPURL(value: string): boolean {
|
||
return /^https?:\/\//i.test(value)
|
||
}
|
||
|
||
export interface UseOrderPaymentCashierOptions {
|
||
/** 成功提示。 */
|
||
notifySuccess: (message: string) => void
|
||
/** 错误提示(如二维码生成失败、查询支付状态失败)。 */
|
||
notifyError: (message: string) => void
|
||
/** 非静默查询未完成时的轻量提示("支付暂未完成")。 */
|
||
notifyInfo: (message: string) => void
|
||
/**
|
||
* 兜底提示:支付 URL 非 HTTP 或为空、无法渲染二维码时展示。
|
||
* Mobile 用 vant dialog(带"知道了"按钮)做强提示,PC 用普通提示即可。
|
||
*/
|
||
notifyFallback: (message: string) => void
|
||
/** 支付完成的提示文案,默认 "支付成功,正在进入群消息"。 */
|
||
paidMessage?: string
|
||
/** 轮询间隔,PC 3000 / Mobile 2500。 */
|
||
pollIntervalMs?: number
|
||
/** 二维码宽度,PC 240 / Mobile 220。 */
|
||
qrWidth?: number
|
||
/**
|
||
* 拦截 HTTP 支付 URL 的外部打开方式。
|
||
* Mobile(微信/支付宝内嵌浏览器)注入 `window.location.href = url` 直接拉起支付;
|
||
* PC 不注入(直接渲染二维码弹窗)。返回 true 表示已处理(不再走弹窗),false 走默认弹窗流程。
|
||
*/
|
||
openExternalURL?: (url: string) => boolean
|
||
/** 弹窗显隐控制方式。PC 用 el-dialog(v-model 一个 ref),Mobile 用 van-popup。返回 true 表示用 ref,false 表示 composable 内部 ref。 */
|
||
/**
|
||
* 自定义支付状态查询。默认走租号订单 `/orders/:id/query-payment`。
|
||
* 撞车等独立业务可注入自己的查询函数。
|
||
*/
|
||
queryPayment?: (orderId: number) => Promise<PaymentOrder>
|
||
}
|
||
|
||
/**
|
||
* 支付收银台 Composable(PC / Mobile 共用)。
|
||
*
|
||
* 封装支付单创建后的 UI 流程:HTTP URL 直接跳转 / 二维码渲染 + 轮询查询 / 兜底提示。
|
||
* UI 反馈通过 options 注入;轮询间隔、二维码宽度、外部跳转等差异通过参数控制。
|
||
*/
|
||
export function useOrderPaymentCashier(options: UseOrderPaymentCashierOptions) {
|
||
const paymentPopupVisible = ref(false)
|
||
const activePayment = ref<PaymentOrder | null>(null)
|
||
const paymentQRCodeURL = ref('')
|
||
const qrGenerating = ref(false)
|
||
const checkingPayment = ref(false)
|
||
let paymentPollingTimer: number | undefined
|
||
let paidHandler: PaidHandler | undefined
|
||
|
||
const pollIntervalMs = options.pollIntervalMs ?? 2500
|
||
const qrWidth = options.qrWidth ?? 220
|
||
const paidMessage = options.paidMessage ?? '支付成功,正在进入群消息'
|
||
|
||
const payURL = computed(() => paymentPayURL(activePayment.value))
|
||
|
||
async function openPaymentCashier(payment: PaymentOrder, onPaid?: PaidHandler) {
|
||
activePayment.value = payment
|
||
paidHandler = onPaid
|
||
|
||
if (payment.paid) {
|
||
await handlePaid()
|
||
return
|
||
}
|
||
|
||
const currentPayURL = paymentPayURL(payment)
|
||
if (currentPayURL && options.openExternalURL?.(currentPayURL)) {
|
||
return
|
||
}
|
||
|
||
if (currentPayURL && isHTTPURL(currentPayURL)) {
|
||
paymentPopupVisible.value = true
|
||
await renderPaymentQRCode(payment)
|
||
startPaymentPolling()
|
||
return
|
||
}
|
||
|
||
// 非 HTTP URL(或空):无法渲染二维码,仅展示兜底提示。
|
||
options.notifyFallback(currentPayURL || '支付单已创建,请稍后刷新订单状态。')
|
||
}
|
||
|
||
async function renderPaymentQRCode(payment = activePayment.value) {
|
||
const currentPayURL = paymentPayURL(payment)
|
||
paymentQRCodeURL.value = ''
|
||
if (!currentPayURL || !isHTTPURL(currentPayURL)) return
|
||
|
||
qrGenerating.value = true
|
||
try {
|
||
paymentQRCodeURL.value = await QRCode.toDataURL(currentPayURL, {
|
||
width: qrWidth,
|
||
margin: 1,
|
||
errorCorrectionLevel: 'M',
|
||
color: {
|
||
dark: '#111827',
|
||
light: '#ffffff',
|
||
},
|
||
})
|
||
} catch {
|
||
options.notifyError('二维码生成失败')
|
||
} finally {
|
||
qrGenerating.value = false
|
||
}
|
||
}
|
||
|
||
function startPaymentPolling() {
|
||
stopPaymentPolling()
|
||
paymentPollingTimer = window.setInterval(() => {
|
||
void refreshPaymentStatus(true)
|
||
}, pollIntervalMs)
|
||
}
|
||
|
||
function stopPaymentPolling() {
|
||
if (paymentPollingTimer === undefined) return
|
||
window.clearInterval(paymentPollingTimer)
|
||
paymentPollingTimer = undefined
|
||
}
|
||
|
||
async function refreshPaymentStatus(silent = false) {
|
||
if (!activePayment.value || checkingPayment.value) return
|
||
|
||
checkingPayment.value = true
|
||
const previousPayURL = payURL.value
|
||
try {
|
||
const query = options.queryPayment || queryOrderPayment
|
||
const payment = await query(activePayment.value.order_id)
|
||
activePayment.value = payment
|
||
if (payment.paid) {
|
||
await handlePaid()
|
||
return
|
||
}
|
||
if (paymentPayURL(payment) !== previousPayURL) {
|
||
await renderPaymentQRCode(payment)
|
||
}
|
||
if (!silent) {
|
||
options.notifyInfo('支付暂未完成')
|
||
}
|
||
} catch {
|
||
if (!silent) {
|
||
options.notifyError('查询支付状态失败')
|
||
}
|
||
} finally {
|
||
checkingPayment.value = false
|
||
}
|
||
}
|
||
|
||
async function handlePaid() {
|
||
stopPaymentPolling()
|
||
paymentPopupVisible.value = false
|
||
options.notifySuccess(paidMessage)
|
||
await paidHandler?.()
|
||
}
|
||
|
||
watch(paymentPopupVisible, visible => {
|
||
if (!visible) stopPaymentPolling()
|
||
})
|
||
|
||
onBeforeUnmount(stopPaymentPolling)
|
||
|
||
return {
|
||
paymentPopupVisible,
|
||
activePayment,
|
||
payURL,
|
||
paymentQRCodeURL,
|
||
qrGenerating,
|
||
checkingPayment,
|
||
openPaymentCashier,
|
||
refreshPaymentStatus,
|
||
stopPaymentPolling,
|
||
}
|
||
}
|