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() {
+
+