重构订单详情双视图: 抽取 useOrderActions + useOrderPaymentCashier 消除重复

OrderDetailView (PC) 与 MobileOrderDetailView 共 13 个 handler + 9 个 computed 逐字重复, 差异只在 UI 反馈原语 (ElMessage vs showToast)、二次确认交互、支付外壳。本次按 usePublishForm 范式抽取共享 composable, 业务逻辑收敛、UI 反馈靠 options 注入。

新增:
- useOrderActions: 订单详情业务逻辑 (load/cancel/pay/handoff/confirm/checkout 五件套/dispute 两件套/evidence upload), 持有状态/表单/computed, UI 反馈与二次确认通过 options 注入, 复用 useOrderSnapshot 的纯函数与 useOrderCheckoutSnapshot
- useOrderPaymentCashier: 通用支付收银台 (二维码渲染 + 轮询 + App 浏览器跳转兜底), 反馈靠 options 注入, 轮询间隔/二维码宽度/外部跳转参数化

改造:
- useMobilePaymentCashier 改为 useOrderPaymentCashier 的薄封装, 注入 vant toast/showDialog + 微信/支付宝内嵌浏览器跳转, 对外 API 名不变
- OrderDetailView 1505→1120, MobileOrderDetailView 1253→896, 视图净减 742 行重复代码
- 修复 PC 缺失的 rejectReason/disputeDescription 非空校验 (现网 PC 遗漏, 与 Mobile 对齐)

关键不变式:
- handleRejectCheckout 用现网 createDispute(type:'checkout_dispute') 语义, 未引入死代码 useSettlement 的 counterCheckout bug
- 轮询间隔 PC 3000ms / Mobile 2500ms 通过 pollIntervalMs 注入, 行为不变
- 二次确认 Mobile 4 项 (cancel/confirmCheckout/acceptCheckout/cancelDispute) 走 showDialog, PC 直接放行
- autoPay (route.query.pay==='1') 触发逻辑保留

chatPathPrefix 移入 options 并加 onCounterCheckoutSuccess/onRejectCheckoutSuccess/onCreateDisputeSuccess 三个可选 UI 回调: 修复 PC template @click="openOrderChat" 误把 PointerEvent 当路由前缀 (跳成 [object PointerEvent]), 以及 Mobile 修改结账/拒绝修正/提交申诉成功后 popup 不关闭的回归。

vue-tsc / eslint / vite build 全部通过。
This commit is contained in:
yml
2026-06-14 10:59:18 +08:00
parent 6ff7692f3e
commit bd9c88d659
5 changed files with 1007 additions and 1055 deletions
@@ -1,150 +1,56 @@
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'
import {
paymentPayURL,
useOrderPaymentCashier,
} from '@/features/orders/composables/useOrderPaymentCashier'
type PaidHandler = () => Promise<void> | 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 { paymentPayURL as mobilePaymentPayURL }
/**
* 移动端支付收银台 Composable。
*
* 在通用 {@link useOrderPaymentCashier} 基础上注入 vant 的 toast 提示,
* 并处理微信 / 支付宝内嵌浏览器的直接跳转拉起支付。对外 API 名保持不变,
* Mobile 视图无需改动。
*/
export function useMobilePaymentCashier() {
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 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: '知道了',
})
function isInAppPaymentBrowser(): boolean {
if (typeof navigator === 'undefined') return false
const ua = navigator.userAgent || ''
return /MicroMessenger|AlipayClient/i.test(ua)
}
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',
},
const cashier = useOrderPaymentCashier({
notifySuccess: message => showToast({ message, icon: 'passed' }),
notifyError: message => showToast({ message, icon: 'cross' }),
notifyInfo: message => showToast({ message, icon: 'info-o' }),
notifyFallback: message => {
void showDialog({
title: '订单支付',
message,
confirmButtonText: '知道了',
})
} 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
},
paidMessage: '支付成功,等待号主交接',
pollIntervalMs: 2500,
qrWidth: 220,
openExternalURL: url => {
if (isHTTPURL(url) && isInAppPaymentBrowser()) {
window.location.href = url
return true
}
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()
return false
},
})
onBeforeUnmount(stopPaymentPolling)
return {
paymentPopupVisible,
activePayment,
payURL,
paymentQRCodeURL,
qrGenerating,
checkingPayment,
openMobilePaymentCashier,
refreshPaymentStatus,
stopPaymentPolling,
...cashier,
// 保持 Mobile 视图既有调用名。
openMobilePaymentCashier: cashier.openPaymentCashier,
}
}
function isHTTPURL(value: string): boolean {
return /^https?:\/\//i.test(value)
}