fix(trade): simplify proxy order and respect product whitelist
Drop SMS verification for HQ/partner proxy orders; keep form draft across product picker; hide whitelist-only products from partner catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,12 @@ import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import ChinaRegionPicker from '../components/ChinaRegionPicker';
|
||||
import { 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 type {
|
||||
PartnerProxyDeliveryMode,
|
||||
@@ -16,33 +22,60 @@ 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() {
|
||||
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('');
|
||||
const [customerSmsCode, setCustomerSmsCode] = useState('');
|
||||
const [partnerSmsCode, setPartnerSmsCode] = useState('');
|
||||
const [receiverName, setReceiverName] = useState('');
|
||||
const [regionCodes, setRegionCodes] = useState<string[]>([]);
|
||||
const [addressDetail, setAddressDetail] = useState('');
|
||||
const [productId, setProductId] = useState('');
|
||||
const [quantity, setQuantity] = useState(2);
|
||||
const [promoCodeId, setPromoCodeId] = useState('');
|
||||
const [deliveryMode, setDeliveryMode] = useState<PartnerProxyDeliveryMode>('ADDRESS');
|
||||
const [autoReceive, setAutoReceive] = useState(false);
|
||||
const [confirmStep, setConfirmStep] = useState(false);
|
||||
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 [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 [customerCooldown, setCustomerCooldown] = useState(0);
|
||||
const [partnerCooldown, setPartnerCooldown] = useState(0);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [msg, setMsg] = useState('');
|
||||
|
||||
const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]);
|
||||
const regionLabel = region ? formatRegionLabel(region) : '';
|
||||
|
||||
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) => {
|
||||
@@ -53,6 +86,7 @@ export default function ProxyOrderPage() {
|
||||
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);
|
||||
}
|
||||
@@ -64,9 +98,28 @@ export default function ProxyOrderPage() {
|
||||
|
||||
useEffect(() => {
|
||||
const fromQuery = searchParams.get('productId');
|
||||
if (fromQuery) setProductId(fromQuery);
|
||||
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,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!productId || quantity < 1) {
|
||||
setPreview(null);
|
||||
@@ -92,64 +145,10 @@ export default function ProxyOrderPage() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [productId, quantity, deliveryMode, region?.city, region?.district]);
|
||||
|
||||
function startCooldown(setter: (n: number | ((s: number) => number)) => void) {
|
||||
setter(60);
|
||||
const timer = setInterval(() => {
|
||||
setter((s) => {
|
||||
if (s <= 1) {
|
||||
clearInterval(timer);
|
||||
return 0;
|
||||
}
|
||||
return s - 1;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function sendCustomerSms() {
|
||||
setMsg('');
|
||||
if (!/^1\d{10}$/.test(phone.trim())) {
|
||||
setMsg('请输入有效手机号');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await request<{ maskedPhone: string }>(
|
||||
'PARTNER_H5',
|
||||
'/partner/proxy-orders/send-customer-sms',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ phone: phone.trim() }),
|
||||
silent: true,
|
||||
},
|
||||
);
|
||||
setMsg(`客户验证码已发送至 ${res.maskedPhone}`);
|
||||
startCooldown(setCustomerCooldown);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPartnerSms() {
|
||||
setMsg('');
|
||||
try {
|
||||
const res = await request<{ maskedPhone: string }>(
|
||||
'PARTNER_H5',
|
||||
'/partner/proxy-orders/send-partner-sms',
|
||||
{ method: 'POST', body: '{}', silent: true },
|
||||
);
|
||||
setMsg(`确认验证码已发送至合伙人手机 ${res.maskedPhone}`);
|
||||
startCooldown(setPartnerCooldown);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm(): string | null {
|
||||
if (!/^1\d{10}$/.test(phone.trim())) return '请输入有效手机号';
|
||||
if (!customerSmsCode.trim()) return '请输入客户验证码';
|
||||
if (!productId) return '请选择商品';
|
||||
if (deliveryMode !== 'ADDRESS') {
|
||||
// 现场提货不需要地址与门店
|
||||
} else {
|
||||
if (deliveryMode === 'ADDRESS') {
|
||||
if (!region?.province || !region?.city || !region?.district) return '请选择省市区';
|
||||
if (!addressDetail.trim()) return '请填写详细地址';
|
||||
if (!autoReceive) return '配送到址须勾选同意自动收货';
|
||||
@@ -158,18 +157,6 @@ export default function ProxyOrderPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function openConfirmStep() {
|
||||
setMsg('');
|
||||
const err = validateForm();
|
||||
if (err) {
|
||||
setMsg(err);
|
||||
return;
|
||||
}
|
||||
setConfirmStep(true);
|
||||
setPartnerSmsCode('');
|
||||
await sendPartnerSms();
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
setMsg('');
|
||||
const err = validateForm();
|
||||
@@ -177,15 +164,9 @@ export default function ProxyOrderPage() {
|
||||
setMsg(err);
|
||||
return;
|
||||
}
|
||||
if (!partnerSmsCode.trim()) {
|
||||
setMsg('请输入合伙人确认验证码');
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: PartnerProxyOrderCreateRequest = {
|
||||
phone: phone.trim(),
|
||||
customerSmsCode: customerSmsCode.trim(),
|
||||
partnerSmsCode: partnerSmsCode.trim(),
|
||||
deliveryMode,
|
||||
autoReceive: deliveryMode === 'ADDRESS' ? true : undefined,
|
||||
receiverName: receiverName.trim() || undefined,
|
||||
@@ -206,6 +187,7 @@ export default function ProxyOrderPage() {
|
||||
silent: true,
|
||||
});
|
||||
toastSuccess(`代下单成功:${order.orderNo}`);
|
||||
clearProxyOrderDraft();
|
||||
navigate(`/orders/${order.id}`);
|
||||
} catch (e) {
|
||||
setMsg(e instanceof Error ? e.message : '下单失败');
|
||||
@@ -233,34 +215,12 @@ export default function ProxyOrderPage() {
|
||||
<>
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">用户手机号</label>
|
||||
<div className="partner-input-row">
|
||||
<div className="partner-input-wrap" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="partner-input"
|
||||
placeholder="11 位手机号"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={customerCooldown > 0}
|
||||
onClick={() => void sendCustomerSms()}
|
||||
>
|
||||
{customerCooldown > 0 ? `${customerCooldown}s` : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">客户验证码</label>
|
||||
<div className="partner-input-wrap">
|
||||
<input
|
||||
className="partner-input"
|
||||
placeholder="发至客户手机"
|
||||
value={customerSmsCode}
|
||||
onChange={(e) => setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
placeholder="11 位手机号"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value.replace(/\D/g, '').slice(0, 11))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -270,11 +230,12 @@ export default function ProxyOrderPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="partner-proxy-product-picker"
|
||||
onClick={() =>
|
||||
onClick={() => {
|
||||
persistDraft();
|
||||
navigate(
|
||||
`/proxy-order/products${productId ? `?selected=${encodeURIComponent(productId)}` : ''}`,
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
{selectedProduct ? (
|
||||
<>
|
||||
@@ -428,73 +389,24 @@ export default function ProxyOrderPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
{confirmStep ? (
|
||||
<section className="partner-form-section partner-proxy-confirm">
|
||||
<label className="partner-form-label">合伙人确认验证码</label>
|
||||
<p className="label-md text-muted" style={{ marginBottom: 8 }}>
|
||||
确认线下已收款后,将向您的登录手机号发送验证码
|
||||
</p>
|
||||
<div className="partner-input-row">
|
||||
<div className="partner-input-wrap" style={{ flex: 1 }}>
|
||||
<input
|
||||
className="partner-input"
|
||||
placeholder="发至合伙人手机"
|
||||
value={partnerSmsCode}
|
||||
onChange={(e) => setPartnerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={partnerCooldown > 0}
|
||||
onClick={() => void sendPartnerSms()}
|
||||
>
|
||||
{partnerCooldown > 0 ? `${partnerCooldown}s` : '重新发送'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{msg ? (
|
||||
<p className="partner-form-error" role="alert">
|
||||
{msg}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{!confirmStep ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={!preview}
|
||||
onClick={() => void openConfirmStep()}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
确认代下单
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: 8, marginTop: 16 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ flex: 1 }}
|
||||
onClick={() => setConfirmStep(false)}
|
||||
>
|
||||
返回修改
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
style={{ flex: 1 }}
|
||||
disabled={submitting || !preview}
|
||||
onClick={() => void submit()}
|
||||
>
|
||||
{submitting ? '提交中…' : '验证并提交'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={submitting || !preview}
|
||||
onClick={() => void submit()}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
{submitting ? '提交中…' : '确认代下单'}
|
||||
</button>
|
||||
|
||||
<p className="label-md text-muted" style={{ marginTop: 12, lineHeight: 1.5 }}>
|
||||
线下已收款确认后,将自动创建/关联用户,订单标记为代下单并直接完成,同时发放对应权益。客户订单列表会显示您的姓名。
|
||||
确认线下已收款后提交:将自动创建/关联用户,订单标记为代下单并直接完成,同时发放对应权益。客户订单列表会显示您的姓名。
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user