feat(partner): complete proxy order with dual SMS and pickup
CI / verify (pull_request) Has been cancelled
CI / verify (pull_request) Has been cancelled
Add partner/customer SMS confirm, address auto-receive or on-site pickup, proxy placer fields, PARTNER_PROXY user source, and C-end badge. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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() {
|
||||
<Route path="/center/staff" element={<StaffListPage />} />
|
||||
<Route path="/center/staff/new" element={<StaffCreatePage />} />
|
||||
<Route path="/proxy-order" element={<ProxyOrderPage />} />
|
||||
<Route path="/proxy-order/products" element={<ProxyOrderProductsPage />} />
|
||||
<Route path="/reports/weekly" element={<WeeklyReportPage />} />
|
||||
<Route path="/leaderboard" element={<LeaderboardPage />} />
|
||||
<Route path="/stores/:id" element={<StoreDetailPage />} />
|
||||
|
||||
@@ -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<PartnerProxyOrderOptions | null>(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<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 [storeId, setStoreId] = useState('');
|
||||
const [autoReceive, setAutoReceive] = useState(false);
|
||||
const [confirmStep, setConfirmStep] = useState(false);
|
||||
const [preview, setPreview] = useState<PartnerProxyOrderPreviewResult | null>(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<PartnerProxyOrderOptions>('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 (
|
||||
<div className="page partner-proxy-order-page">
|
||||
@@ -158,7 +232,7 @@ export default function ProxyOrderPage() {
|
||||
|
||||
<main className="partner-form-card" style={{ margin: '0 16px 24px' }}>
|
||||
{loadingOptions ? (
|
||||
<p className="label-md text-muted">加载商品与推广码…</p>
|
||||
<p className="label-md text-muted">加载商品与门店…</p>
|
||||
) : (
|
||||
<>
|
||||
<section className="partner-form-section">
|
||||
@@ -175,70 +249,49 @@ export default function ProxyOrderPage() {
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
disabled={smsCooldown > 0}
|
||||
onClick={() => void sendSms()}
|
||||
disabled={customerCooldown > 0}
|
||||
onClick={() => void sendCustomerSms()}
|
||||
>
|
||||
{smsCooldown > 0 ? `${smsCooldown}s` : '获取验证码'}
|
||||
{customerCooldown > 0 ? `${customerCooldown}s` : '获取验证码'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">短信验证码</label>
|
||||
<label className="partner-form-label">客户验证码</label>
|
||||
<div className="partner-input-wrap">
|
||||
<input
|
||||
className="partner-input"
|
||||
placeholder="线下代发货确认码"
|
||||
value={smsCode}
|
||||
onChange={(e) => 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))}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>}
|
||||
</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>
|
||||
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">商品</label>
|
||||
<select
|
||||
className="partner-input"
|
||||
value={productId}
|
||||
onChange={(e) => setProductId(e.target.value)}
|
||||
style={{ width: '100%', padding: '12px 14px', borderRadius: 12, border: '1px solid var(--color-border)' }}
|
||||
<label className="partner-form-label">酒品</label>
|
||||
<button
|
||||
type="button"
|
||||
className="partner-proxy-product-picker"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/proxy-order/products${productId ? `?selected=${encodeURIComponent(productId)}` : ''}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
{(options?.products ?? []).map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}({p.spec})¥{fmtMoney(p.price)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{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>
|
||||
|
||||
<section className="partner-form-section">
|
||||
@@ -254,6 +307,99 @@ export default function ProxyOrderPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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${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>
|
||||
</section>
|
||||
|
||||
{deliveryMode === 'ON_SITE_PICKUP' ? (
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">提货门店</label>
|
||||
{(options?.stores.length ?? 0) === 0 ? (
|
||||
<p className="label-md text-muted">暂无名下门店,请先录入门店</p>
|
||||
) : (
|
||||
<select
|
||||
className="partner-input"
|
||||
value={storeId}
|
||||
onChange={(e) => setStoreId(e.target.value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
{options!.stores.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.name}({s.district}
|
||||
{s.address})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{(options?.promoCodes.length ?? 0) > 0 && (
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">绑定推广码(选填)</label>
|
||||
@@ -261,7 +407,12 @@ export default function ProxyOrderPage() {
|
||||
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)' }}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '12px 14px',
|
||||
borderRadius: 12,
|
||||
border: '1px solid var(--color-border)',
|
||||
}}
|
||||
>
|
||||
<option value="">不绑定</option>
|
||||
{options!.promoCodes.map((p) => (
|
||||
@@ -288,8 +439,8 @@ export default function ProxyOrderPage() {
|
||||
<span className="body-md">×{quantity}</span>
|
||||
</div>
|
||||
<div className="partner-proxy-fee-row">
|
||||
<span className="label-md text-muted">配送类型</span>
|
||||
<span className="body-md">{preview.deliveryType === 'LOCAL' ? '同城' : '跨城'}</span>
|
||||
<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>
|
||||
@@ -302,25 +453,78 @@ export default function ProxyOrderPage() {
|
||||
</>
|
||||
) : (
|
||||
<p className="label-md text-muted">
|
||||
{selectedProduct ? '请确认数量与地址后查看费用' : '请选择商品'}
|
||||
{selectedProduct ? '请确认数量与履约信息后查看费用' : '请选择商品'}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{msg && <p className="partner-form-error" role="alert">{msg}</p>}
|
||||
{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}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-block"
|
||||
disabled={submitting || !preview}
|
||||
onClick={() => void submit()}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
{submitting ? '提交中…' : '验证码确认并代下单'}
|
||||
</button>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<p className="label-md text-muted" style={{ marginTop: 12, lineHeight: 1.5 }}>
|
||||
提交后将自动创建/关联用户,订单类型为「线下代下单」,状态直接标记为已收货,并发放对应权益。
|
||||
线下已收款确认后,将自动创建/关联用户,订单标记为代下单并直接完成,同时发放对应权益。客户订单列表会显示您的姓名。
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import { request } from '../lib/api';
|
||||
import { toastError } from '../lib/toast';
|
||||
import type { PartnerProxyOrderOptions, PartnerProxyOrderProductOption } from '@dukang/shared-types';
|
||||
|
||||
function fmtMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
export default function ProxyOrderProductsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const selectedId = searchParams.get('selected') || '';
|
||||
const [products, setProducts] = useState<PartnerProxyOrderProductOption[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
request<PartnerProxyOrderOptions>('PARTNER_H5', '/partner/proxy-orders/options')
|
||||
.then((data) => setProducts(Array.isArray(data.products) ? data.products : []))
|
||||
.catch((e) => toastError(e instanceof Error ? e.message : '加载失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
function pick(product: PartnerProxyOrderProductOption) {
|
||||
navigate(`/proxy-order?productId=${encodeURIComponent(product.id)}`, { replace: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page partner-proxy-order-page">
|
||||
<PageHeader title="选择酒品" onBack={() => navigate(-1)} />
|
||||
<main className="partner-proxy-product-list">
|
||||
{loading ? <p className="label-md text-muted">加载中…</p> : null}
|
||||
{!loading && products.length === 0 ? (
|
||||
<p className="label-md text-muted">暂无可售商品</p>
|
||||
) : null}
|
||||
{products.map((p) => {
|
||||
const benefit = p.benefitAmount != null ? Number(p.benefitAmount) : Number(p.price);
|
||||
const active = p.id === selectedId;
|
||||
return (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
className={`partner-proxy-product-card${active ? ' partner-proxy-product-card--active' : ''}`}
|
||||
onClick={() => pick(p)}
|
||||
>
|
||||
<div className="partner-proxy-product-thumb">
|
||||
{p.coverUrl ? (
|
||||
<img src={p.coverUrl} alt="" />
|
||||
) : (
|
||||
<span className="partner-proxy-product-thumb-empty" />
|
||||
)}
|
||||
</div>
|
||||
<div className="partner-proxy-product-main">
|
||||
<div className="partner-proxy-product-row">
|
||||
<span className="partner-proxy-product-name">{p.name}</span>
|
||||
<span className="partner-proxy-product-price">¥{fmtMoney(p.price)}</span>
|
||||
</div>
|
||||
<p className="partner-proxy-product-spec">{p.spec}</p>
|
||||
<p className="partner-proxy-product-benefit">好客权益 ¥{fmtMoney(benefit)}</p>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3315,6 +3315,145 @@ body {
|
||||
border-top: 1px dashed rgba(166, 29, 36, 0.15);
|
||||
}
|
||||
|
||||
.partner-proxy-mode-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partner-proxy-mode-tab {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
font-size: 14px;
|
||||
color: var(--color-ink-black);
|
||||
}
|
||||
|
||||
.partner-proxy-mode-tab.is-active {
|
||||
border-color: var(--color-heritage-red);
|
||||
color: var(--color-heritage-red);
|
||||
background: rgba(166, 29, 36, 0.06);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.partner-proxy-auto-receive {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin: 4px 0 16px;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--color-ink-black);
|
||||
}
|
||||
|
||||
.partner-proxy-auto-receive input {
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.partner-proxy-product-picker {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 12px 36px 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.partner-proxy-product-picker-arrow {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 22px;
|
||||
color: var(--color-muted, #999);
|
||||
}
|
||||
|
||||
.partner-proxy-product-list {
|
||||
padding: 0 16px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.partner-proxy-product-card {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.partner-proxy-product-card--active {
|
||||
border-color: var(--color-heritage-red);
|
||||
box-shadow: 0 0 0 1px rgba(166, 29, 36, 0.2);
|
||||
}
|
||||
|
||||
.partner-proxy-product-thumb {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #f3f1ee;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.partner-proxy-product-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.partner-proxy-product-thumb-empty {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #f3f1ee, #e8e4de);
|
||||
}
|
||||
|
||||
.partner-proxy-product-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.partner-proxy-product-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.partner-proxy-product-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--color-ink-black);
|
||||
}
|
||||
|
||||
.partner-proxy-product-price {
|
||||
color: var(--color-heritage-red);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.partner-proxy-product-spec,
|
||||
.partner-proxy-product-benefit {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-muted, #888);
|
||||
}
|
||||
|
||||
.partner-proxy-confirm {
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.partner-warehouse-denied {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user