import { computed, onBeforeUnmount, ref, watch } from 'vue' import QRCode from 'qrcode' import { queryOrderPayment, type PaymentOrder } from '@/features/orders/api/orders' type PaidHandler = () => Promise | 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。 */ } /** * 支付收银台 Composable(PC / Mobile 共用)。 * * 封装支付单创建后的 UI 流程:HTTP URL 直接跳转 / 二维码渲染 + 轮询查询 / 兜底提示。 * UI 反馈通过 options 注入;轮询间隔、二维码宽度、外部跳转等差异通过参数控制。 */ export function useOrderPaymentCashier(options: UseOrderPaymentCashierOptions) { const paymentPopupVisible = ref(false) const activePayment = ref(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 payment = await queryOrderPayment(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, } }