785 lines
29 KiB
TypeScript
785 lines
29 KiB
TypeScript
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<PartnerProxyOrderOptions | null>(null);
|
||
const [loadingOptions, setLoadingOptions] = useState(true);
|
||
const [phone, setPhone] = useState(draft0.phone);
|
||
const [receiverName, setReceiverName] = useState(draft0.receiverName);
|
||
const [regionCodes, setRegionCodes] = useState<string[]>(draft0.regionCodes);
|
||
const [addressDetail, setAddressDetail] = useState(draft0.addressDetail);
|
||
const [productId, setProductId] = useState(draft0.productId);
|
||
const [skuId, setSkuId] = useState('');
|
||
const [quantity, setQuantity] = useState(draft0.quantity);
|
||
const [promoCodeId, setPromoCodeId] = useState(draft0.promoCodeId);
|
||
const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>(draft0.deliveryMode);
|
||
const [autoReceive, setAutoReceive] = useState(draft0.autoReceive);
|
||
const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(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<ProxyOrderCreateResponse | null>(null);
|
||
const [payMethod, setPayMethod] = useState<ProxyPayMethod>('NATIVE');
|
||
const [codeUrl, setCodeUrl] = useState<string | null>(null);
|
||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||
const [paying, setPaying] = useState(false);
|
||
const [cancelling, setCancelling] = useState(false);
|
||
const [lockedPayMethod, setLockedPayMethod] = useState<ProxyPayMethod | null>(null);
|
||
const pollRef = useRef<number | null>(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<ProxyOrderDraft>) {
|
||
saveProxyOrderDraft({
|
||
phone,
|
||
receiverName,
|
||
regionCodes,
|
||
addressDetail,
|
||
productId,
|
||
quantity,
|
||
promoCodeId,
|
||
deliveryMode,
|
||
autoReceive,
|
||
...overrides,
|
||
});
|
||
}
|
||
|
||
useEffect(() => {
|
||
request<PartnerProxyOrderOptions>('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 skuOptions = (selectedProduct?.skus ?? []).filter((s) => s.status === 'ON_SALE');
|
||
const selectedSku =
|
||
skuOptions.find((s) => s.id === skuId) ||
|
||
skuOptions.find((s) => s.id === selectedProduct?.defaultSkuId) ||
|
||
skuOptions[0];
|
||
const allowOnline = selectedSku
|
||
? selectedSku.allowOnlinePurchase !== false
|
||
: selectedProduct
|
||
? selectedProduct.allowOnlinePurchase !== false
|
||
: true;
|
||
const allowOnSite = selectedSku
|
||
? !!selectedSku.allowOnSitePickup
|
||
: !!selectedProduct?.allowOnSitePickup;
|
||
const allowCrossCity = selectedSku
|
||
? selectedSku.allowCrossCityDelivery !== false
|
||
: selectedProduct
|
||
? selectedProduct.allowCrossCityDelivery !== false
|
||
: true;
|
||
|
||
useEffect(() => {
|
||
if (!selectedProduct) return;
|
||
const def =
|
||
skuOptions.find((s) => s.id === selectedProduct.defaultSkuId) ||
|
||
skuOptions.find((s) => s.isDefault) ||
|
||
skuOptions[0];
|
||
if (def && (!skuId || !skuOptions.some((s) => s.id === skuId))) {
|
||
setSkuId(def.id);
|
||
if (def.saleUnit === 'BOX') setQuantity(1);
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [productId, selectedProduct?.id, options]);
|
||
|
||
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<PartnerProxyOrderPreviewResult>('PARTNER_H5', '/partner/proxy-orders/preview', {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
productId,
|
||
quantity,
|
||
deliveryMode,
|
||
skuId: skuId || undefined,
|
||
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, skuId, 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<ProxyOrderPayResponse>(
|
||
'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,
|
||
skuId: skuId || undefined,
|
||
};
|
||
|
||
setSubmitting(true);
|
||
try {
|
||
const order = await request<ProxyOrderCreateResponse>('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 (
|
||
<div className="page partner-proxy-order-page">
|
||
<PageHeader title={step === 'pay' ? '代下单支付' : '代下单'} onBack={() => navigate(-1)} />
|
||
|
||
<main className="partner-form-card" style={{ margin: '0 16px 24px' }}>
|
||
{step === 'pay' && created ? (
|
||
<>
|
||
<p className="body-md">
|
||
订单 {created.orderNo} · 应付 ¥{fmtMoney(created.payAmount)}
|
||
</p>
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
{created.deliveryType === 'ON_SITE_PICKUP'
|
||
? '支付成功后订单即为已完成,并发放好客权益'
|
||
: '支付成功后进入待发货,由总部履约'}
|
||
</p>
|
||
|
||
<section className="partner-form-section" style={{ marginTop: 16 }}>
|
||
<label className="partner-form-label">支付方式</label>
|
||
<div className="partner-proxy-mode-tabs">
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
|
||
disabled={!!lockedPayMethod && lockedPayMethod !== 'NATIVE'}
|
||
onClick={() => switchPayMethod('NATIVE')}
|
||
>
|
||
收款码
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
|
||
disabled={!!lockedPayMethod && lockedPayMethod !== 'JSAPI'}
|
||
onClick={() => switchPayMethod('JSAPI')}
|
||
>
|
||
微信代付
|
||
</button>
|
||
</div>
|
||
{lockedPayMethod ? (
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付
|
||
</p>
|
||
) : null}
|
||
</section>
|
||
|
||
{payMethod === 'NATIVE' ? (
|
||
<div style={{ textAlign: 'center', marginTop: 16 }}>
|
||
{qrDataUrl ? (
|
||
<img src={qrDataUrl} alt="收款码" width={220} height={220} style={{ margin: '0 auto' }} />
|
||
) : (
|
||
<p className="label-md text-muted">{paying ? '生成收款码中…' : '暂无收款码'}</p>
|
||
)}
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
请使用微信扫码支付,支付成功后自动跳转
|
||
</p>
|
||
<button
|
||
type="button"
|
||
className="btn btn-block"
|
||
style={{ marginTop: 16 }}
|
||
disabled={paying}
|
||
onClick={() => void startPay(created.id, 'NATIVE')}
|
||
>
|
||
{paying ? '生成中…' : '重新生成收款码'}
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div style={{ marginTop: 16 }}>
|
||
<p className="label-md text-muted">将调起微信支付,由合伙人微信完成代付</p>
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-block"
|
||
style={{ marginTop: 12 }}
|
||
disabled={paying}
|
||
onClick={() => void startPay(created.id, 'JSAPI')}
|
||
>
|
||
{paying ? '支付中…' : '重新调起微信代付'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{msg ? (
|
||
<p className="partner-form-error" role="alert" style={{ marginTop: 12 }}>
|
||
{msg}
|
||
</p>
|
||
) : null}
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-block"
|
||
style={{ marginTop: 16 }}
|
||
disabled={cancelling || paying}
|
||
onClick={() => void cancelPay(created.id)}
|
||
>
|
||
{cancelling ? '取消中…' : '取消支付'}
|
||
</button>
|
||
</>
|
||
) : loadingOptions ? (
|
||
<p className="label-md text-muted">加载商品…</p>
|
||
) : (
|
||
<>
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">用户手机号</label>
|
||
<div className="partner-input-wrap">
|
||
<input
|
||
className="partner-input"
|
||
placeholder="11 位手机号"
|
||
value={phone}
|
||
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">酒品</label>
|
||
<button
|
||
type="button"
|
||
className="partner-proxy-product-picker"
|
||
onClick={() => {
|
||
persistDraft();
|
||
navigate(
|
||
`/proxy-order/products${productId ? `?selected=${encodeURIComponent(productId)}` : ''}`,
|
||
);
|
||
}}
|
||
>
|
||
{selectedProduct ? (
|
||
<>
|
||
<span className="partner-proxy-product-name">{selectedProduct.name}</span>
|
||
<span className="label-md text-muted">
|
||
{selectedProduct.spec} · ¥{fmtMoney(selectedProduct.price)}
|
||
</span>
|
||
</>
|
||
) : (
|
||
<span className="label-md text-muted">点击选择酒品</span>
|
||
)}
|
||
<span className="partner-proxy-product-picker-arrow">›</span>
|
||
</button>
|
||
</section>
|
||
|
||
{skuOptions.length > 1 || selectedProduct?.specEnabled ? (
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">规格</label>
|
||
<div className="partner-input-wrap">
|
||
<select
|
||
className="partner-input"
|
||
value={skuId}
|
||
onChange={(e) => {
|
||
const id = e.target.value;
|
||
setSkuId(id);
|
||
const sku = skuOptions.find((s) => s.id === id);
|
||
if (sku?.saleUnit === 'BOX') setQuantity(1);
|
||
}}
|
||
>
|
||
{skuOptions.map((s) => (
|
||
<option key={s.id} value={s.id}>
|
||
{s.specText || '默认'} · ¥{fmtMoney(s.price)} ·{' '}
|
||
{s.saleUnit === 'BOX' ? `${s.bottlesPerUnit}瓶/箱` : '瓶'}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
</section>
|
||
) : null}
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">
|
||
{selectedSku?.saleUnit === 'BOX' ? '数量(箱)' : '数量(瓶)'}
|
||
</label>
|
||
<div className="partner-input-wrap">
|
||
<input
|
||
className="partner-input"
|
||
type="number"
|
||
min={1}
|
||
value={quantity}
|
||
onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
{(options?.promoCodes.length ?? 0) > 0 && (
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">绑定推广码(选填)</label>
|
||
<select
|
||
className="partner-input"
|
||
value={promoCodeId}
|
||
onChange={(e) => setPromoCodeId(e.target.value)}
|
||
style={{
|
||
width: '100%',
|
||
padding: '12px 14px',
|
||
borderRadius: 12,
|
||
border: '1px solid var(--color-border)',
|
||
}}
|
||
>
|
||
<option value="">不绑定</option>
|
||
{options!.promoCodes.map((p) => (
|
||
<option key={p.id} value={p.id}>
|
||
{p.name}({p.code})
|
||
</option>
|
||
))}
|
||
</select>
|
||
</section>
|
||
)}
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">履约方式</label>
|
||
{allowOnline && allowOnSite ? (
|
||
<div className="partner-proxy-mode-tabs">
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${deliveryMode === 'ADDRESS' ? ' is-active' : ''}`}
|
||
onClick={() => setDeliveryMode('ADDRESS')}
|
||
>
|
||
配送到址
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${deliveryMode === 'ON_SITE_PICKUP' ? ' is-active' : ''}`}
|
||
onClick={() => setDeliveryMode('ON_SITE_PICKUP')}
|
||
>
|
||
现场提货
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<p className="label-md text-muted">
|
||
{!allowOnline && allowOnSite
|
||
? '该商品仅支持现场提货'
|
||
: allowOnline && !allowOnSite
|
||
? allowCrossCity
|
||
? '该商品仅支持配送到址(含跨城)'
|
||
: '该商品仅支持配送到址(不可跨城)'
|
||
: '该商品暂无可选履约方式'}
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
{deliveryMode === 'ADDRESS' && allowOnline ? (
|
||
<>
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">收货人(选填)</label>
|
||
<div className="partner-input-wrap">
|
||
<input
|
||
className="partner-input"
|
||
placeholder="默认:用户+手机尾号"
|
||
value={receiverName}
|
||
onChange={(e) => setReceiverName(e.target.value)}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">收货地区</label>
|
||
<ChinaRegionPicker value={regionCodes} onChange={setRegionCodes} />
|
||
{regionLabel ? (
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
{regionLabel}
|
||
</p>
|
||
) : null}
|
||
</section>
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">详细地址</label>
|
||
<div className="partner-field-input partner-field-input--block">
|
||
<textarea
|
||
rows={3}
|
||
placeholder="街道、门牌号等"
|
||
value={addressDetail}
|
||
onChange={(e) => setAddressDetail(e.target.value)}
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<label className="partner-proxy-auto-receive">
|
||
<input
|
||
type="checkbox"
|
||
checked={autoReceive}
|
||
onChange={(e) => setAutoReceive(e.target.checked)}
|
||
/>
|
||
<span>同意自动收货(配送到址必选)</span>
|
||
</label>
|
||
</>
|
||
) : null}
|
||
|
||
<section className="partner-form-section">
|
||
<label className="partner-form-label">支付方式</label>
|
||
<div className="partner-proxy-mode-tabs">
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
|
||
onClick={() => setPayMethod('NATIVE')}
|
||
>
|
||
收款码
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
|
||
onClick={() => setPayMethod('JSAPI')}
|
||
>
|
||
微信代付
|
||
</button>
|
||
</div>
|
||
<p className="label-md text-muted" style={{ marginTop: 8, lineHeight: 1.5 }}>
|
||
{payMethod === 'NATIVE'
|
||
? '提交后展示商家收款码,客户或现场扫码支付'
|
||
: '提交后在微信内由您代客户完成支付(需已绑定微信)'}
|
||
</p>
|
||
</section>
|
||
|
||
<section className="partner-proxy-fee-card">
|
||
<h3 className="headline-md">费用明细</h3>
|
||
{previewLoading ? (
|
||
<p className="label-md text-muted">计算中…</p>
|
||
) : preview ? (
|
||
<>
|
||
<div className="partner-proxy-fee-row">
|
||
<span className="label-md text-muted">商品单价</span>
|
||
<span className="body-md">¥{fmtMoney(preview.unitPrice)}</span>
|
||
</div>
|
||
<div className="partner-proxy-fee-row">
|
||
<span className="label-md text-muted">数量</span>
|
||
<span className="body-md">×{quantity}</span>
|
||
</div>
|
||
<div className="partner-proxy-fee-row">
|
||
<span className="label-md text-muted">履约类型</span>
|
||
<span className="body-md">{deliveryLabel}</span>
|
||
</div>
|
||
<div className="partner-proxy-fee-row">
|
||
<span className="label-md text-muted">权益额</span>
|
||
<span className="body-md">¥{fmtMoney(preview.benefitAmount)}</span>
|
||
</div>
|
||
<div className="partner-proxy-fee-row partner-proxy-fee-row--total">
|
||
<span className="headline-md">应付金额</span>
|
||
<span className="amount-lg text-primary">¥{fmtMoney(preview.payAmount)}</span>
|
||
</div>
|
||
</>
|
||
) : (
|
||
<p className="label-md text-muted">
|
||
{selectedProduct ? '请确认数量与履约信息后查看费用' : '请选择商品'}
|
||
</p>
|
||
)}
|
||
</section>
|
||
|
||
{msg ? (
|
||
<p className="partner-form-error" role="alert">
|
||
{msg}
|
||
</p>
|
||
) : null}
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-block"
|
||
disabled={submitting || paying || !preview}
|
||
onClick={() => void submit()}
|
||
style={{ marginTop: 16 }}
|
||
>
|
||
{submitting || paying ? '处理中…' : '提交并支付'}
|
||
</button>
|
||
|
||
<p className="label-md text-muted" style={{ marginTop: 12, lineHeight: 1.5 }}>
|
||
提交后进入在线支付;支付成功后发放权益。配送单将进入待发货由总部履约,现场提货走自提闭环。
|
||
</p>
|
||
</>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}
|