Files
hfb_sys/frontend/src/features/orders/composables/useOrderPaymentCashier.ts
T
yml bd9c88d659 重构订单详情双视图: 抽取 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 全部通过。
2026-06-14 10:59:18 +08:00

180 lines
5.7 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。 */
}
/**
* 支付收银台 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 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,
}
}