小程序商品列表上下滚动

This commit is contained in:
2026-08-02 17:54:42 +08:00
parent 5deffbdbfb
commit 49a5c057db
2 changed files with 166 additions and 57 deletions
+140 -57
View File
@@ -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 Taro, { useDidShow, usePullDownRefresh, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import Taro, {
useDidShow,
usePageScroll,
usePullDownRefresh,
useShareAppMessage,
useShareTimeline,
} from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import TabMainHeader from '../../components/TabMainHeader';
import CouponBadge from '../../components/CouponBadge';
@@ -42,13 +48,25 @@ const AROMA_TABS = [
{ key: 'NONGXIANG', label: '浓香型' },
] 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() {
const [tab, setTab] = useState('QINGXIANG');
const [activeAroma, setActiveAroma] = useState<AromaKey>('QINGXIANG');
const [products, setProducts] = useState<Product[]>([]);
const [loading, setLoading] = useState(true);
const [displayCity, setDisplayCity] = useState('郑州市');
const [cityCode, setCityCode] = useState('410100');
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(() => {
return request<{ miniHome?: MiniHomeConfig }>('/common/client-config')
@@ -126,7 +144,19 @@ export default function HomePage() {
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 footerUrl = miniHome.footerUrl;
@@ -147,6 +177,100 @@ export default function HomePage() {
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 (
<PageShell variant="tab" className="home-page no-tab-header">
<WechatShareReady payload={sharePayload} />
@@ -175,8 +299,8 @@ export default function HomePage() {
{AROMA_TABS.map((t) => (
<Text
key={t.key}
className={`home-aroma-tab${tab === t.key ? ' home-aroma-tab--active' : ''}`}
onClick={() => setTab(t.key)}
className={`home-aroma-tab${activeAroma === t.key ? ' home-aroma-tab--active' : ''}`}
onClick={() => scrollToAroma(t.key)}
>
{t.label}
</Text>
@@ -190,59 +314,18 @@ export default function HomePage() {
{!loading && products.length === 0 ? (
<View className="home-empty"></View>
) : null}
{!loading && products.length > 0 && filtered.length === 0 ? (
<View className="home-empty">线</View>
) : null}
{!loading &&
filtered.map((p) => {
const thumb = getProductMainImage(p);
const spec = p.subtitle || p.spec || '';
products.length > 0 &&
AROMA_TABS.map((t) => {
const list = productsByAroma[t.key];
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 key={t.key} id={aromaSectionId(t.key)} className="home-aroma-section">
<Text className="home-aroma-section-title">{t.label}</Text>
{list.length === 0 ? (
<View className="home-empty home-empty--section">线</View>
) : (
list.map((p) => renderProductCard(p))
)}
</View>
);
})}