小程序商品列表上下滚动
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
|
import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components';
|
||||||
import Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
import Taro, {
|
||||||
|
useDidShow,
|
||||||
|
usePageScroll,
|
||||||
|
usePullDownRefresh,
|
||||||
|
useShareAppMessage,
|
||||||
|
useShareTimeline,
|
||||||
|
} from '@tarojs/taro';
|
||||||
import PageShell from '../../components/PageShell';
|
import PageShell from '../../components/PageShell';
|
||||||
import TabMainHeader from '../../components/TabMainHeader';
|
import TabMainHeader from '../../components/TabMainHeader';
|
||||||
import CouponBadge from '../../components/CouponBadge';
|
import CouponBadge from '../../components/CouponBadge';
|
||||||
@@ -42,13 +48,25 @@ const AROMA_TABS = [
|
|||||||
{ key: 'NONGXIANG', label: '浓香型' },
|
{ key: 'NONGXIANG', label: '浓香型' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
type AromaKey = (typeof AROMA_TABS)[number]['key'];
|
||||||
|
|
||||||
|
/** sticky 香型导航高度(与 CSS 大致一致),锚点滚动时预留 */
|
||||||
|
const AROMA_NAV_OFFSET_PX = 44;
|
||||||
|
|
||||||
|
function aromaSectionId(key: AromaKey) {
|
||||||
|
return `aroma-section-${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [tab, setTab] = useState('QINGXIANG');
|
const [activeAroma, setActiveAroma] = useState<AromaKey>('QINGXIANG');
|
||||||
const [products, setProducts] = useState<Product[]>([]);
|
const [products, setProducts] = useState<Product[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [displayCity, setDisplayCity] = useState('郑州市');
|
const [displayCity, setDisplayCity] = useState('郑州市');
|
||||||
const [cityCode, setCityCode] = useState('410100');
|
const [cityCode, setCityCode] = useState('410100');
|
||||||
const [miniHome, setMiniHome] = useState<MiniHomeConfig>({ banners: [], footerUrl: null });
|
const [miniHome, setMiniHome] = useState<MiniHomeConfig>({ banners: [], footerUrl: null });
|
||||||
|
const scrollingToRef = useRef<AromaKey | null>(null);
|
||||||
|
const scrollLockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const lastScrollSyncAtRef = useRef(0);
|
||||||
|
|
||||||
const loadMiniHome = useCallback(() => {
|
const loadMiniHome = useCallback(() => {
|
||||||
return request<{ miniHome?: MiniHomeConfig }>('/common/client-config')
|
return request<{ miniHome?: MiniHomeConfig }>('/common/client-config')
|
||||||
@@ -126,7 +144,19 @@ export default function HomePage() {
|
|||||||
Taro.navigateTo({ url: returnPath });
|
Taro.navigateTo({ url: returnPath });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = products.filter((p) => p.aromaType === tab);
|
const productsByAroma = useMemo(() => {
|
||||||
|
const map: Record<AromaKey, Product[]> = {
|
||||||
|
QINGXIANG: [],
|
||||||
|
JIANGXIANG: [],
|
||||||
|
NONGXIANG: [],
|
||||||
|
};
|
||||||
|
for (const p of products) {
|
||||||
|
const key = p.aromaType as AromaKey;
|
||||||
|
if (key in map) map[key].push(p);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [products]);
|
||||||
|
|
||||||
const banners = miniHome.banners;
|
const banners = miniHome.banners;
|
||||||
const footerUrl = miniHome.footerUrl;
|
const footerUrl = miniHome.footerUrl;
|
||||||
|
|
||||||
@@ -147,6 +177,100 @@ export default function HomePage() {
|
|||||||
imageUrl: sharePayload.imgUrl,
|
imageUrl: sharePayload.imgUrl,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
function scrollToAroma(key: AromaKey) {
|
||||||
|
setActiveAroma(key);
|
||||||
|
scrollingToRef.current = key;
|
||||||
|
if (scrollLockTimerRef.current) clearTimeout(scrollLockTimerRef.current);
|
||||||
|
scrollLockTimerRef.current = setTimeout(() => {
|
||||||
|
scrollingToRef.current = null;
|
||||||
|
}, 450);
|
||||||
|
|
||||||
|
const query = Taro.createSelectorQuery();
|
||||||
|
query.select(`#${aromaSectionId(key)}`).boundingClientRect();
|
||||||
|
query.selectViewport().scrollOffset();
|
||||||
|
query.exec((res) => {
|
||||||
|
const rect = res?.[0] as { top?: number } | undefined;
|
||||||
|
const viewport = res?.[1] as { scrollTop?: number } | undefined;
|
||||||
|
if (rect?.top == null || viewport?.scrollTop == null) return;
|
||||||
|
const scrollTop = Math.max(0, viewport.scrollTop + rect.top - AROMA_NAV_OFFSET_PX);
|
||||||
|
void Taro.pageScrollTo({ scrollTop, duration: 280 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
usePageScroll(() => {
|
||||||
|
if (scrollingToRef.current) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastScrollSyncAtRef.current < 80) return;
|
||||||
|
lastScrollSyncAtRef.current = now;
|
||||||
|
const query = Taro.createSelectorQuery();
|
||||||
|
AROMA_TABS.forEach((t) => {
|
||||||
|
query.select(`#${aromaSectionId(t.key)}`).boundingClientRect();
|
||||||
|
});
|
||||||
|
query.exec((rects) => {
|
||||||
|
if (!Array.isArray(rects) || rects.length === 0) return;
|
||||||
|
let next: AromaKey = AROMA_TABS[0].key;
|
||||||
|
for (let i = 0; i < AROMA_TABS.length; i++) {
|
||||||
|
const rect = rects[i] as { top?: number } | null;
|
||||||
|
if (!rect || rect.top == null) continue;
|
||||||
|
// 区块顶进入导航下方一带时视为当前香型
|
||||||
|
if (rect.top <= AROMA_NAV_OFFSET_PX + 24) {
|
||||||
|
next = AROMA_TABS[i].key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveAroma((prev) => (prev === next ? prev : next));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderProductCard(p: Product) {
|
||||||
|
const thumb = getProductMainImage(p);
|
||||||
|
const spec = p.subtitle || p.spec || '';
|
||||||
|
return (
|
||||||
|
<View key={p.id} className="home-product-card">
|
||||||
|
<View className="home-product-card-inner" onClick={() => openProductDetail(p.id)}>
|
||||||
|
<View className="home-product-thumb-wrap">
|
||||||
|
{thumb ? (
|
||||||
|
<Image className="home-product-thumb" src={thumb} mode="aspectFill" />
|
||||||
|
) : (
|
||||||
|
<View className="home-product-thumb home-product-thumb--empty" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View className="home-product-main">
|
||||||
|
<View className="home-product-row">
|
||||||
|
<Text className="home-product-name">{p.name}</Text>
|
||||||
|
<Text className="home-product-price">¥{Number(p.price).toFixed(0)}</Text>
|
||||||
|
</View>
|
||||||
|
{spec ? <Text className="home-product-sub">{spec}</Text> : null}
|
||||||
|
<View className="home-product-footer">
|
||||||
|
<CouponBadge amount={p.benefitDisplay ?? p.price} label="好客权益" />
|
||||||
|
</View>
|
||||||
|
<View className="home-product-actions">
|
||||||
|
{p.allowOnSitePickup ? (
|
||||||
|
<Text
|
||||||
|
className="home-pickup-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation?.();
|
||||||
|
void goOnSitePickup(p.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
现场取货
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text
|
||||||
|
className="home-buy-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation?.();
|
||||||
|
openProductDetail(p.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
立即购买
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="tab" className="home-page no-tab-header">
|
<PageShell variant="tab" className="home-page no-tab-header">
|
||||||
<WechatShareReady payload={sharePayload} />
|
<WechatShareReady payload={sharePayload} />
|
||||||
@@ -175,8 +299,8 @@ export default function HomePage() {
|
|||||||
{AROMA_TABS.map((t) => (
|
{AROMA_TABS.map((t) => (
|
||||||
<Text
|
<Text
|
||||||
key={t.key}
|
key={t.key}
|
||||||
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}`}
|
className={`home-aroma-tab${activeAroma === t.key ? ' home-aroma-tab--active' : ''}`}
|
||||||
onClick={() => setTab(t.key)}
|
onClick={() => scrollToAroma(t.key)}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
</Text>
|
</Text>
|
||||||
@@ -190,59 +314,18 @@ export default function HomePage() {
|
|||||||
{!loading && products.length === 0 ? (
|
{!loading && products.length === 0 ? (
|
||||||
<View className="home-empty">当前城市暂无在售商品</View>
|
<View className="home-empty">当前城市暂无在售商品</View>
|
||||||
) : null}
|
) : null}
|
||||||
{!loading && products.length > 0 && filtered.length === 0 ? (
|
|
||||||
<View className="home-empty">该香型暂未上线</View>
|
|
||||||
) : null}
|
|
||||||
{!loading &&
|
{!loading &&
|
||||||
filtered.map((p) => {
|
products.length > 0 &&
|
||||||
const thumb = getProductMainImage(p);
|
AROMA_TABS.map((t) => {
|
||||||
const spec = p.subtitle || p.spec || '';
|
const list = productsByAroma[t.key];
|
||||||
return (
|
return (
|
||||||
<View key={p.id} className="home-product-card">
|
<View key={t.key} id={aromaSectionId(t.key)} className="home-aroma-section">
|
||||||
<View
|
<Text className="home-aroma-section-title">{t.label}</Text>
|
||||||
className="home-product-card-inner"
|
{list.length === 0 ? (
|
||||||
onClick={() => openProductDetail(p.id)}
|
<View className="home-empty home-empty--section">该香型暂未上线</View>
|
||||||
>
|
) : (
|
||||||
<View className="home-product-thumb-wrap">
|
list.map((p) => renderProductCard(p))
|
||||||
{thumb ? (
|
)}
|
||||||
<Image className="home-product-thumb" src={thumb} mode="aspectFill" />
|
|
||||||
) : (
|
|
||||||
<View className="home-product-thumb home-product-thumb--empty" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View className="home-product-main">
|
|
||||||
<View className="home-product-row">
|
|
||||||
<Text className="home-product-name">{p.name}</Text>
|
|
||||||
<Text className="home-product-price">¥{Number(p.price).toFixed(0)}</Text>
|
|
||||||
</View>
|
|
||||||
{spec ? <Text className="home-product-sub">{spec}</Text> : null}
|
|
||||||
<View className="home-product-footer">
|
|
||||||
<CouponBadge amount={p.benefitDisplay ?? p.price} label="好客权益" />
|
|
||||||
</View>
|
|
||||||
<View className="home-product-actions">
|
|
||||||
{p.allowOnSitePickup ? (
|
|
||||||
<Text
|
|
||||||
className="home-pickup-btn"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation?.();
|
|
||||||
void goOnSitePickup(p.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
现场取货
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
<Text
|
|
||||||
className="home-buy-btn"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation?.();
|
|
||||||
openProductDetail(p.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
立即购买
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -110,6 +110,32 @@
|
|||||||
padding: 8px var(--space-page) 12px;
|
padding: 8px var(--space-page) 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-aroma-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
scroll-margin-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-aroma-section + .home-aroma-section {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-aroma-section-title {
|
||||||
|
font-family: var(--font-headline);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
color: var(--color-on-surface);
|
||||||
|
padding: 2px 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-empty--section {
|
||||||
|
padding: 16px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.home-product-card {
|
.home-product-card {
|
||||||
background: var(--color-card);
|
background: var(--color-card);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user