import { useEffect, useMemo, useRef, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; import QRCode from 'qrcode'; import PageHeader from '@dukang/shared-ui/PageHeader'; import { invokeWechatPay } from '@dukang/weixin-sdk'; import ChinaRegionPicker from '../components/ChinaRegionPicker'; import { getToken, request } from '../lib/api'; import { formatRegionLabel, parseRegionCodes } from '../lib/china-region'; import { clearProxyOrderDraft, loadProxyOrderDraft, saveProxyOrderDraft, type ProxyOrderDraft, } from '../lib/proxyOrderDraft'; import { toastError, toastSuccess } from '../lib/toast'; import { isWechatEnv } from '../lib/weixin'; import { fetchPartnerProfile, partnerHasWechatBinding } from '../lib/wechat-auth'; import type { PartnerProxyDeliveryMode, PartnerProxyOrderCreateRequest, PartnerProxyOrderOptions, PartnerProxyOrderPreviewResult, ProxyOrderCreateResponse, ProxyOrderPayResponse, ProxyPayMethod, } from '@dukang/shared-types'; import { usePartnerPageView } from '../lib/usePageView'; function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function initialDraft(): ProxyOrderDraft { return ( loadProxyOrderDraft() ?? { phone: '', receiverName: '', regionCodes: [], addressDetail: '', productId: '', quantity: 2, promoCodeId: '', deliveryMode: 'ADDRESS', autoReceive: false, } ); } export default function ProxyOrderPage() { usePartnerPageView('partner_proxy_order_view'); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const draft0 = useMemo(() => initialDraft(), []); const [options, setOptions] = useState(null); const [loadingOptions, setLoadingOptions] = useState(true); const [phone, setPhone] = useState(draft0.phone); const [receiverName, setReceiverName] = useState(draft0.receiverName); const [regionCodes, setRegionCodes] = useState(draft0.regionCodes); const [addressDetail, setAddressDetail] = useState(draft0.addressDetail); const [productId, setProductId] = useState(draft0.productId); const [quantity, setQuantity] = useState(draft0.quantity); const [promoCodeId, setPromoCodeId] = useState(draft0.promoCodeId); const [deliveryMode, setDeliveryMode] = useState(draft0.deliveryMode); const [autoReceive, setAutoReceive] = useState(draft0.autoReceive); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [msg, setMsg] = useState(''); const [step, setStep] = useState<'form' | 'pay'>('form'); const [created, setCreated] = useState(null); const [payMethod, setPayMethod] = useState('NATIVE'); const [codeUrl, setCodeUrl] = useState(null); const [qrDataUrl, setQrDataUrl] = useState(null); const [paying, setPaying] = useState(false); const [cancelling, setCancelling] = useState(false); const [lockedPayMethod, setLockedPayMethod] = useState(null); const pollRef = useRef(null); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); const regionLabel = region ? formatRegionLabel(region) : ''; function stopPoll() { if (pollRef.current != null) { window.clearInterval(pollRef.current); pollRef.current = null; } } useEffect(() => () => stopPoll(), []); useEffect(() => { if (!codeUrl) { setQrDataUrl(null); return; } void QRCode.toDataURL(codeUrl, { width: 220, margin: 1 }).then(setQrDataUrl).catch(() => setQrDataUrl(null)); }, [codeUrl]); function persistDraft(overrides?: Partial) { saveProxyOrderDraft({ phone, receiverName, regionCodes, addressDetail, productId, quantity, promoCodeId, deliveryMode, autoReceive, ...overrides, }); } useEffect(() => { request('PARTNER_H5', '/partner/proxy-orders/options') .then((data) => { setOptions({ ...data, stores: Array.isArray(data.stores) ? data.stores : [], }); const fromQuery = searchParams.get('productId'); if (fromQuery && data.products.some((p) => p.id === fromQuery)) { setProductId(fromQuery); persistDraft({ productId: fromQuery }); } else if (data.products[0] && !productId) { setProductId(data.products[0].id); } }) .catch((e) => toastError(e instanceof Error ? e.message : '加载失败')) .finally(() => setLoadingOptions(false)); // eslint-disable-next-line react-hooks/exhaustive-deps -- hydrate once; productId from query }, [searchParams]); useEffect(() => { const fromQuery = searchParams.get('productId'); if (fromQuery) { setProductId(fromQuery); persistDraft({ productId: fromQuery }); } // eslint-disable-next-line react-hooks/exhaustive-deps -- only sync productId from URL }, [searchParams]); useEffect(() => { persistDraft(); // eslint-disable-next-line react-hooks/exhaustive-deps -- keep draft in sync for product-picker roundtrip }, [ phone, receiverName, regionCodes, addressDetail, productId, quantity, promoCodeId, deliveryMode, autoReceive, ]); const selectedProduct = options?.products.find((p) => p.id === productId); const allowOnline = selectedProduct ? selectedProduct.allowOnlinePurchase !== false : true; const allowOnSite = !!selectedProduct?.allowOnSitePickup; const allowCrossCity = selectedProduct ? selectedProduct.allowCrossCityDelivery !== false : true; useEffect(() => { if (!selectedProduct) return; if (!allowOnline && allowOnSite && deliveryMode !== 'ON_SITE_PICKUP') { setDeliveryMode('ON_SITE_PICKUP'); return; } if (allowOnline && !allowOnSite && deliveryMode !== 'ADDRESS') { setDeliveryMode('ADDRESS'); } }, [selectedProduct, allowOnline, allowOnSite, deliveryMode]); useEffect(() => { if (!productId || quantity < 1) { setPreview(null); return; } const timer = setTimeout(() => { setPreviewLoading(true); request('PARTNER_H5', '/partner/proxy-orders/preview', { method: 'POST', body: JSON.stringify({ productId, quantity, deliveryMode, receiverCity: deliveryMode === 'ADDRESS' ? region?.city || undefined : undefined, receiverDistrict: deliveryMode === 'ADDRESS' ? region?.district || undefined : undefined, }), silent: true, }) .then((data) => { setPreview(data); setMsg(''); }) .catch((e) => { setPreview(null); setMsg(e instanceof Error ? e.message : '费用预览失败'); }) .finally(() => setPreviewLoading(false)); }, 300); return () => clearTimeout(timer); }, [productId, quantity, deliveryMode, region?.city, region?.district]); function validateForm(): string | null { if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号'; if (!productId) return '请选择商品'; if (deliveryMode === 'ADDRESS') { if (!allowOnline) return '该商品不支持线上购买'; if (!region?.province || !region?.city || !region?.district) return '请选择省市区'; if (!addressDetail.trim()) return '请填写详细地址'; if (!autoReceive) return '配送到址须勾选同意自动收货'; } else if (!allowOnSite) { return '该商品不支持现场提货'; } if (!preview) return msg || '请等待费用计算完成'; return null; } function startPoll(orderId: string) { stopPoll(); pollRef.current = window.setInterval(() => { void request<{ payStatus: string; orderNo: string }>( 'PARTNER_H5', `/partner/proxy-orders/${orderId}/pay-status`, { silent: true }, ) .then((st) => { if (st.payStatus === 'PAID') { stopPoll(); toastSuccess(`支付成功:${st.orderNo}`); clearProxyOrderDraft(); navigate(`/center/proxy-orders/${orderId}`, { replace: true }); } }) .catch(() => undefined); }, 2000); } async function startPay(orderId: string, method: ProxyPayMethod) { if (lockedPayMethod && lockedPayMethod !== method) { setMsg( lockedPayMethod === 'JSAPI' ? '该订单已发起微信代付,请先取消支付后重新下单' : '该订单已生成收款码,请先取消支付后重新下单', ); return; } setPaying(true); setMsg(''); setPayMethod(method); try { if (method === 'JSAPI') { if (!isWechatEnv()) { throw new Error('请在微信内打开合伙人端以使用微信代付'); } const profile = await fetchPartnerProfile(); if (!partnerHasWechatBinding(profile)) { throw new Error('请先绑定微信后再代付'); } } const pay = await request( 'PARTNER_H5', `/partner/proxy-orders/${orderId}/pay`, { method: 'POST', body: JSON.stringify({ payMethod: method }), silent: true, }, ); if (pay.mode === 'mock') { toastSuccess('支付成功'); clearProxyOrderDraft(); navigate(`/center/proxy-orders/${orderId}`, { replace: true }); return; } if (pay.mode === 'native' && pay.codeUrl) { setCodeUrl(pay.codeUrl); setLockedPayMethod(method); startPoll(orderId); return; } if (pay.mode === 'jsapi' && pay.prepay) { setCodeUrl(null); setLockedPayMethod(method); await invokeWechatPay(pay.prepay, { apiBase: '/api/v1', clientApp: 'PARTNER_H5', getAccessToken: getToken, platform: 'wechat-h5', }); startPoll(orderId); return; } throw new Error('支付发起失败'); } catch (e) { setMsg(e instanceof Error ? e.message : '支付失败'); } finally { setPaying(false); } } async function cancelPay(orderId: string) { setCancelling(true); setMsg(''); stopPoll(); try { await request('PARTNER_H5', `/partner/proxy-orders/${orderId}/cancel-pay`, { method: 'POST', body: '{}', }); toastSuccess('已取消支付,可重新下单并选择其他支付方式'); setStep('form'); setCreated(null); setCodeUrl(null); setLockedPayMethod(null); } catch (e) { setMsg(e instanceof Error ? e.message : '取消失败'); } finally { setCancelling(false); } } function switchPayMethod(method: ProxyPayMethod) { if (lockedPayMethod && lockedPayMethod !== method) { setMsg( lockedPayMethod === 'JSAPI' ? '该订单已发起微信代付,请先取消支付后重新下单' : '该订单已生成收款码,请先取消支付后重新下单', ); return; } setPayMethod(method); setMsg(''); } async function submit() { setMsg(''); const err = validateForm(); if (err) { setMsg(err); return; } const payload: PartnerProxyOrderCreateRequest = { phone: phone.trim(), deliveryMode, autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, receiverName: receiverName.trim() || undefined, province: deliveryMode === 'ADDRESS' ? region?.province : undefined, city: deliveryMode === 'ADDRESS' ? region?.city : undefined, district: deliveryMode === 'ADDRESS' ? region?.district : undefined, addressDetail: deliveryMode === 'ADDRESS' ? addressDetail.trim() : undefined, productId, quantity, promoCodeId: promoCodeId || undefined, }; setSubmitting(true); try { const order = await request('PARTNER_H5', '/partner/proxy-orders', { method: 'POST', body: JSON.stringify(payload), silent: true, }); clearProxyOrderDraft(); setCreated(order); setStep('pay'); setCodeUrl(null); setLockedPayMethod(null); await startPay(order.id, payMethod); } catch (e) { setMsg(e instanceof Error ? e.message : '下单失败'); } finally { setSubmitting(false); } } const deliveryLabel = preview?.deliveryType === 'ON_SITE_PICKUP' ? '现场提货' : preview?.deliveryType === 'CROSS_CITY' ? '跨城配送' : '同城配送'; return (
navigate(-1)} />
{step === 'pay' && created ? ( <>

订单 {created.orderNo} · 应付 ¥{fmtMoney(created.payAmount)}

{created.deliveryType === 'ON_SITE_PICKUP' ? '支付成功后订单即为已完成,并发放好客权益' : '支付成功后进入待发货,由总部履约'}

{lockedPayMethod ? (

已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付

) : null}
{payMethod === 'NATIVE' ? (
{qrDataUrl ? ( 收款码 ) : (

{paying ? '生成收款码中…' : '暂无收款码'}

)}

请使用微信扫码支付,支付成功后自动跳转

) : (

将调起微信支付,由合伙人微信完成代付

)} {msg ? (

{msg}

) : null} ) : loadingOptions ? (

加载商品…

) : ( <>
setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))} />
setQuantity(Math.max(1, Number(e.target.value) || 1))} />
{(options?.promoCodes.length ?? 0) > 0 && (
)}
{allowOnline && allowOnSite ? (
) : (

{!allowOnline && allowOnSite ? '该商品仅支持现场提货' : allowOnline && !allowOnSite ? allowCrossCity ? '该商品仅支持配送到址(含跨城)' : '该商品仅支持配送到址(不可跨城)' : '该商品暂无可选履约方式'}

)}
{deliveryMode === 'ADDRESS' && allowOnline ? ( <>
setReceiverName(e.target.value)} />
{regionLabel ? (

{regionLabel}

) : null}