feat(catalog): add product fulfillment flags and auto SKU
Enable online/cross-city/on-site purchase switches with trade gates, HQ and proxy UI, and server-generated DK SKUs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -151,6 +151,22 @@ export default function ProxyOrderPage() {
|
||||
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);
|
||||
@@ -169,8 +185,14 @@ export default function ProxyOrderPage() {
|
||||
}),
|
||||
silent: true,
|
||||
})
|
||||
.then(setPreview)
|
||||
.catch(() => setPreview(null))
|
||||
.then((data) => {
|
||||
setPreview(data);
|
||||
setMsg('');
|
||||
})
|
||||
.catch((e) => {
|
||||
setPreview(null);
|
||||
setMsg(e instanceof Error ? e.message : '费用预览失败');
|
||||
})
|
||||
.finally(() => setPreviewLoading(false));
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
@@ -180,11 +202,14 @@ export default function ProxyOrderPage() {
|
||||
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 '请等待费用计算完成';
|
||||
if (!preview) return msg || '请等待费用计算完成';
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -305,7 +330,6 @@ export default function ProxyOrderPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const selectedProduct = options?.products.find((p) => p.id === productId);
|
||||
const deliveryLabel =
|
||||
preview?.deliveryType === 'ON_SITE_PICKUP'
|
||||
? '现场提货'
|
||||
@@ -477,25 +501,37 @@ export default function ProxyOrderPage() {
|
||||
|
||||
<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>
|
||||
{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' ? (
|
||||
{deliveryMode === 'ADDRESS' && allowOnline ? (
|
||||
<>
|
||||
<section className="partner-form-section">
|
||||
<label className="partner-form-label">收货人(选填)</label>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||
import { request } from '../lib/api';
|
||||
@@ -9,12 +9,30 @@ function fmtMoney(n: number) {
|
||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
type FulfillmentFilter = 'ALL' | 'ONLINE' | 'CROSS_CITY' | 'ON_SITE';
|
||||
|
||||
const FILTERS: Array<{ key: FulfillmentFilter; label: string }> = [
|
||||
{ key: 'ALL', label: '全部' },
|
||||
{ key: 'ONLINE', label: '线上' },
|
||||
{ key: 'CROSS_CITY', label: '跨城' },
|
||||
{ key: 'ON_SITE', label: '现场' },
|
||||
];
|
||||
|
||||
function matchFilter(p: PartnerProxyOrderProductOption, filter: FulfillmentFilter): boolean {
|
||||
if (filter === 'ALL') return true;
|
||||
if (filter === 'ONLINE') return p.allowOnlinePurchase !== false;
|
||||
if (filter === 'CROSS_CITY') return p.allowCrossCityDelivery !== false;
|
||||
return !!p.allowOnSitePickup;
|
||||
}
|
||||
|
||||
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);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [filter, setFilter] = useState<FulfillmentFilter>('ALL');
|
||||
|
||||
useEffect(() => {
|
||||
request<PartnerProxyOrderOptions>('PARTNER_H5', '/partner/proxy-orders/options')
|
||||
@@ -23,6 +41,15 @@ export default function ProxyOrderProductsPage() {
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
return products.filter((p) => {
|
||||
if (!matchFilter(p, filter)) return false;
|
||||
if (!q) return true;
|
||||
return `${p.name} ${p.spec}`.toLowerCase().includes(q);
|
||||
});
|
||||
}, [products, keyword, filter]);
|
||||
|
||||
function pick(product: PartnerProxyOrderProductOption) {
|
||||
navigate(`/proxy-order?productId=${encodeURIComponent(product.id)}`, { replace: true });
|
||||
}
|
||||
@@ -30,12 +57,32 @@ export default function ProxyOrderProductsPage() {
|
||||
return (
|
||||
<div className="page partner-proxy-order-page">
|
||||
<PageHeader title="选择酒品" onBack={() => navigate(-1)} />
|
||||
<div className="partner-proxy-product-toolbar">
|
||||
<input
|
||||
className="partner-input"
|
||||
placeholder="搜索酒品名称 / 规格"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
<div className="partner-proxy-product-filters">
|
||||
{FILTERS.map((f) => (
|
||||
<button
|
||||
key={f.key}
|
||||
type="button"
|
||||
className={`partner-proxy-product-filter${filter === f.key ? ' is-active' : ''}`}
|
||||
onClick={() => setFilter(f.key)}
|
||||
>
|
||||
{f.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
{!loading && filtered.length === 0 ? (
|
||||
<p className="label-md text-muted">暂无符合条件的商品</p>
|
||||
) : null}
|
||||
{products.map((p) => {
|
||||
{filtered.map((p) => {
|
||||
const benefit = p.benefitAmount != null ? Number(p.benefitAmount) : Number(p.price);
|
||||
const active = p.id === selectedId;
|
||||
return (
|
||||
@@ -59,6 +106,11 @@ export default function ProxyOrderProductsPage() {
|
||||
</div>
|
||||
<p className="partner-proxy-product-spec">{p.spec}</p>
|
||||
<p className="partner-proxy-product-benefit">好客权益 ¥{fmtMoney(benefit)}</p>
|
||||
<div className="partner-proxy-product-tags">
|
||||
{p.allowOnlinePurchase !== false ? <span className="partner-proxy-tag">线上</span> : null}
|
||||
{p.allowCrossCityDelivery !== false ? <span className="partner-proxy-tag">跨城</span> : null}
|
||||
{p.allowOnSitePickup ? <span className="partner-proxy-tag">现场</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -3374,6 +3374,50 @@ body {
|
||||
color: var(--color-muted, #999);
|
||||
}
|
||||
|
||||
.partner-proxy-product-toolbar {
|
||||
padding: 0 16px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.partner-proxy-product-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.partner-proxy-product-filter {
|
||||
border: 1px solid var(--color-border);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--color-muted, #888);
|
||||
}
|
||||
|
||||
.partner-proxy-product-filter.is-active {
|
||||
border-color: var(--color-heritage-red);
|
||||
color: var(--color-heritage-red);
|
||||
background: rgba(166, 29, 36, 0.06);
|
||||
}
|
||||
|
||||
.partner-proxy-product-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.partner-proxy-tag {
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--color-muted, #666);
|
||||
}
|
||||
|
||||
.partner-proxy-product-list {
|
||||
padding: 0 16px 24px;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user