Files
hfb_sys/frontend/src/features/orders/composables/useOrderPaymentCashier.ts
T
yml2213 a4189db4ca 业务更名为撞车并切换crash路由
用户可见文案与页面/API路径改为撞车与crash,保留mohong表与权限码兼容;移动端入口去掉租/撞图标。
2026-07-16 21:19:35 +08:00

186 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
}
}