diff --git a/frontend/src/features/orders/components/MobilePaymentCashierPopup.vue b/frontend/src/features/orders/components/MobilePaymentCashierPopup.vue new file mode 100644 index 0000000..e0de6aa --- /dev/null +++ b/frontend/src/features/orders/components/MobilePaymentCashierPopup.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/features/orders/composables/useMobilePaymentCashier.ts b/frontend/src/features/orders/composables/useMobilePaymentCashier.ts new file mode 100644 index 0000000..35d4a96 --- /dev/null +++ b/frontend/src/features/orders/composables/useMobilePaymentCashier.ts @@ -0,0 +1,150 @@ +import { computed, onBeforeUnmount, ref, watch } from 'vue' +import QRCode from 'qrcode' +import { showDialog, showToast } from 'vant' + +import { queryOrderPayment, type PaymentOrder } from '@/features/orders/api/orders' + +type PaidHandler = () => Promise | void + +export function mobilePaymentPayURL(payment?: PaymentOrder | null) { + return payment?.jspay_url || payment?.td_code || payment?.jspay_info || '' +} + +function isInAppPaymentBrowser() { + if (typeof navigator === 'undefined') return false + const ua = navigator.userAgent || '' + return /MicroMessenger|AlipayClient/i.test(ua) +} + +function isHTTPURL(value: string) { + return /^https?:\/\//i.test(value) +} + +export function useMobilePaymentCashier() { + 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 payURL = computed(() => mobilePaymentPayURL(activePayment.value)) + + async function openMobilePaymentCashier(payment: PaymentOrder, onPaid?: PaidHandler) { + activePayment.value = payment + paidHandler = onPaid + + if (payment.paid) { + await handlePaid() + return + } + + const currentPayURL = mobilePaymentPayURL(payment) + if (currentPayURL && isHTTPURL(currentPayURL) && isInAppPaymentBrowser()) { + window.location.href = currentPayURL + return + } + + if (currentPayURL && isHTTPURL(currentPayURL)) { + paymentPopupVisible.value = true + await renderPaymentQRCode(payment) + startPaymentPolling() + return + } + + await showDialog({ + title: '订单支付', + message: currentPayURL || '支付单已创建,请稍后刷新订单状态。', + confirmButtonText: '知道了', + }) + } + + async function renderPaymentQRCode(payment = activePayment.value) { + const currentPayURL = mobilePaymentPayURL(payment) + paymentQRCodeURL.value = '' + if (!currentPayURL || !isHTTPURL(currentPayURL)) return + + qrGenerating.value = true + try { + paymentQRCodeURL.value = await QRCode.toDataURL(currentPayURL, { + width: 220, + margin: 1, + errorCorrectionLevel: 'M', + color: { + dark: '#111827', + light: '#ffffff', + }, + }) + } catch { + showToast({ message: '二维码生成失败', icon: 'cross' }) + } finally { + qrGenerating.value = false + } + } + + function startPaymentPolling() { + stopPaymentPolling() + paymentPollingTimer = window.setInterval(() => { + void refreshPaymentStatus(true) + }, 2500) + } + + 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 (mobilePaymentPayURL(payment) !== previousPayURL) { + await renderPaymentQRCode(payment) + } + if (!silent) { + showToast({ message: '支付暂未完成', icon: 'info-o' }) + } + } catch { + if (!silent) { + showToast({ message: '查询支付状态失败', icon: 'cross' }) + } + } finally { + checkingPayment.value = false + } + } + + async function handlePaid() { + stopPaymentPolling() + paymentPopupVisible.value = false + showToast({ message: '支付成功,等待号主交接', icon: 'passed' }) + await paidHandler?.() + } + + watch(paymentPopupVisible, visible => { + if (!visible) stopPaymentPolling() + }) + + onBeforeUnmount(stopPaymentPolling) + + return { + paymentPopupVisible, + activePayment, + payURL, + paymentQRCodeURL, + qrGenerating, + checkingPayment, + openMobilePaymentCashier, + refreshPaymentStatus, + stopPaymentPolling, + } +} diff --git a/frontend/src/features/orders/views/MobileOrderDetailView.vue b/frontend/src/features/orders/views/MobileOrderDetailView.vue index 490a19b..1ed5e2f 100644 --- a/frontend/src/features/orders/views/MobileOrderDetailView.vue +++ b/frontend/src/features/orders/views/MobileOrderDetailView.vue @@ -23,8 +23,9 @@ import { OrderResourceUsageEditor, type HandoffRecord, type Order, - type PaymentOrder, } from '@/features/orders' +import MobilePaymentCashierPopup from '@/features/orders/components/MobilePaymentCashierPopup.vue' +import { useMobilePaymentCashier } from '@/features/orders/composables/useMobilePaymentCashier' import { amountYuan, hydrateCounterFormFromCheckout, @@ -92,6 +93,17 @@ const showCounterPopup = ref(false) const showRejectPopup = ref(false) let autoPayHandled = false +const { + paymentPopupVisible, + activePayment, + payURL, + paymentQRCodeURL, + qrGenerating, + checkingPayment, + openMobilePaymentCashier, + refreshPaymentStatus, +} = useMobilePaymentCashier() + const isOwner = computed(() => order.value?.owner_id === session.userId) const isRenter = computed(() => order.value?.renter_id === session.userId) const orderAmountLabel = computed(() => (isOwner.value ? '预计租金' : '支付租金')) @@ -186,7 +198,7 @@ async function handlePay() { showToast({ message: '支付成功,等待号主交接', icon: 'passed' }) await loadOrder() } else { - openPaymentCashier(payment) + await openMobilePaymentCashier(payment, loadOrder) } } catch (error) { showToast({ message: readError(error, '支付失败'), icon: 'cross' }) @@ -195,19 +207,6 @@ async function handlePay() { } } -function openPaymentCashier(payment: PaymentOrder) { - const payURL = payment.jspay_url || payment.td_code || payment.jspay_info || '' - if (payURL && /^https?:\/\//i.test(payURL)) { - window.location.href = payURL - return - } - showDialog({ - title: '订单支付', - message: payURL || '支付单已创建,请稍后刷新订单状态。', - confirmButtonText: '知道了', - }) -} - async function handleSubmitHandoff() { if (!order.value) return if (!handoffContent.value.trim()) { @@ -764,6 +763,16 @@ async function copyListingCode() { + +