diff --git a/.cursor/skills/dukang-v3/reference-acc.md b/.cursor/skills/dukang-v3/reference-acc.md index 7cc930e..0443082 100644 --- a/.cursor/skills/dukang-v3/reference-acc.md +++ b/.cursor/skills/dukang-v3/reference-acc.md @@ -51,7 +51,7 @@ |-----|------|----------| | ACC-010 | 工单 | 四类型;总部决策;补发/退款流转 | | ACC-011 | 发票 | 四组合申请;2 工作日回传;可下载 | -| ACC-012 | 代下单 | W3:手机号建用户;权益自动发 | +| ACC-012 | 代下单 | W3:客户+合伙人双短信;手机号建用户(来源 PARTNER_PROXY);线下确认后已付款已完成并发权益;C 端展示代下单人 | | ACC-013 | 问卷评价 | 成交问卷无激励;核销评价;总部可看 | | ACC-014 | 推广分享 | 推广码归因;商品分享带渠道 | | ACC-018 | 话术 | OPT-012 3 日内对齐 | diff --git a/apps/h5-partner/src/PartnerAppRoutes.tsx b/apps/h5-partner/src/PartnerAppRoutes.tsx index 1a89802..29c3ce6 100644 --- a/apps/h5-partner/src/PartnerAppRoutes.tsx +++ b/apps/h5-partner/src/PartnerAppRoutes.tsx @@ -15,6 +15,7 @@ import BillsPage from './pages/BillsPage'; import SettlementPage from './pages/SettlementPage'; import WeeklyReportPage from './pages/WeeklyReportPage'; import ProxyOrderPage from './pages/ProxyOrderPage'; +import ProxyOrderProductsPage from './pages/ProxyOrderProductsPage'; import StaffListPage from './pages/StaffListPage'; import StaffCreatePage from './pages/StaffCreatePage'; import LeaderboardPage from './pages/LeaderboardPage'; @@ -33,6 +34,7 @@ function PrimaryRoutes() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/apps/h5-partner/src/pages/ProxyOrderPage.tsx b/apps/h5-partner/src/pages/ProxyOrderPage.tsx index 338cb4f..6ea9ed3 100644 --- a/apps/h5-partner/src/pages/ProxyOrderPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderPage.tsx @@ -1,11 +1,12 @@ import { useEffect, useMemo, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; 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 { toastError, toastSuccess } from '../lib/toast'; import type { + PartnerProxyDeliveryMode, PartnerProxyOrderCreateRequest, PartnerProxyOrderOptions, PartnerProxyOrderPreviewResult, @@ -17,19 +18,26 @@ function fmtMoney(n: number) { export default function ProxyOrderPage() { const navigate = useNavigate(); + const [searchParams] = useSearchParams(); const [options, setOptions] = useState(null); const [loadingOptions, setLoadingOptions] = useState(true); const [phone, setPhone] = useState(''); - const [smsCode, setSmsCode] = useState(''); + const [customerSmsCode, setCustomerSmsCode] = useState(''); + const [partnerSmsCode, setPartnerSmsCode] = useState(''); const [receiverName, setReceiverName] = useState(''); const [regionCodes, setRegionCodes] = useState([]); const [addressDetail, setAddressDetail] = useState(''); const [productId, setProductId] = useState(''); const [quantity, setQuantity] = useState(2); const [promoCodeId, setPromoCodeId] = useState(''); + const [deliveryMode, setDeliveryMode] = useState('ADDRESS'); + const [storeId, setStoreId] = useState(''); + const [autoReceive, setAutoReceive] = useState(false); + const [confirmStep, setConfirmStep] = useState(false); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); - const [smsCooldown, setSmsCooldown] = useState(0); + const [customerCooldown, setCustomerCooldown] = useState(0); + const [partnerCooldown, setPartnerCooldown] = useState(0); const [submitting, setSubmitting] = useState(false); const [msg, setMsg] = useState(''); @@ -39,12 +47,27 @@ export default function ProxyOrderPage() { useEffect(() => { request('PARTNER_H5', '/partner/proxy-orders/options') .then((data) => { - setOptions(data); - if (data.products[0]) setProductId(data.products[0].id); + 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); + } else if (data.products[0] && !productId) { + setProductId(data.products[0].id); + } + if (data.stores?.[0] && !storeId) setStoreId(data.stores[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); + }, [searchParams]); useEffect(() => { if (!productId || quantity < 1) { @@ -58,8 +81,10 @@ export default function ProxyOrderPage() { body: JSON.stringify({ productId, quantity, - receiverCity: region.city || undefined, - receiverDistrict: region.district || undefined, + deliveryMode, + storeId: deliveryMode === 'ON_SITE_PICKUP' ? storeId || undefined : undefined, + receiverCity: deliveryMode === 'ADDRESS' ? region.city || undefined : undefined, + receiverDistrict: deliveryMode === 'ADDRESS' ? region.district || undefined : undefined, }), silent: true, }) @@ -68,67 +93,110 @@ export default function ProxyOrderPage() { .finally(() => setPreviewLoading(false)); }, 300); return () => clearTimeout(timer); - }, [productId, quantity, region.city, region.district]); + }, [productId, quantity, deliveryMode, storeId, region.city, region.district]); - async function sendSms() { + 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-sms', { - method: 'POST', - body: JSON.stringify({ phone: phone.trim() }), - silent: true, - }); - setMsg(`验证码已发送至 ${res.maskedPhone}`); - setSmsCooldown(60); - const timer = setInterval(() => { - setSmsCooldown((s) => { - if (s <= 1) { - clearInterval(timer); - return 0; - } - return s - 1; - }); - }, 1000); + 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 === 'ON_SITE_PICKUP') { + if (!storeId) return '请选择提货门店'; + } else { + if (!region?.province || !region.city || !region.district) return '请选择省市区'; + if (!addressDetail.trim()) return '请填写详细地址'; + if (!autoReceive) return '配送到址须勾选同意自动收货'; + } + if (!preview) return '请等待费用计算完成'; + return null; + } + + async function openConfirmStep() { + setMsg(''); + const err = validateForm(); + if (err) { + setMsg(err); + return; + } + setConfirmStep(true); + setPartnerSmsCode(''); + await sendPartnerSms(); + } + async function submit() { setMsg(''); - if (!/^1\d{10}$/.test(phone.trim())) { - setMsg('请输入有效手机号'); + const err = validateForm(); + if (err) { + setMsg(err); return; } - if (!smsCode.trim()) { - setMsg('请输入验证码'); - return; - } - if (!region?.province || !region.city || !region.district) { - setMsg('请选择省市区'); - return; - } - if (!addressDetail.trim()) { - setMsg('请填写详细地址'); - return; - } - if (!productId) { - setMsg('请选择商品'); + if (!partnerSmsCode.trim()) { + setMsg('请输入合伙人确认验证码'); return; } const payload: PartnerProxyOrderCreateRequest = { phone: phone.trim(), - smsCode: smsCode.trim(), + customerSmsCode: customerSmsCode.trim(), + partnerSmsCode: partnerSmsCode.trim(), + deliveryMode, + autoReceive: deliveryMode === 'ADDRESS' ? true : undefined, + storeId: deliveryMode === 'ON_SITE_PICKUP' ? storeId : undefined, receiverName: receiverName.trim() || undefined, - province: region.province, - city: region.city, - district: region.district, - addressDetail: addressDetail.trim(), + 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, @@ -151,6 +219,12 @@ export default function ProxyOrderPage() { } const selectedProduct = options?.products.find((p) => p.id === productId); + const deliveryLabel = + preview?.deliveryType === 'ON_SITE_PICKUP' + ? '现场提货' + : preview?.deliveryType === 'CROSS_CITY' + ? '跨城配送' + : '同城配送'; return (
@@ -158,7 +232,7 @@ export default function ProxyOrderPage() {
{loadingOptions ? ( -

加载商品与推广码…

+

加载商品与门店…

) : ( <>
@@ -175,70 +249,49 @@ export default function ProxyOrderPage() {
- +
setSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} + placeholder="发至客户手机" + value={customerSmsCode} + onChange={(e) => setCustomerSmsCode(e.target.value.replace(/\D/g, '').slice(0, 6))} />
- -
- setReceiverName(e.target.value)} - /> -
-
- -
- - - {regionLabel &&

{regionLabel}

} -
- -
- -
-