import { useCallback, useMemo, useRef, useState } from 'react'; import { View, Text, Image, Swiper, SwiperItem } from '@tarojs/components'; 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'; import WechatShareReady from '../../components/WechatShareReady'; import UserTabBar, { shouldRenderPageTabBar, syncTabBarSelected } from '../../components/UserTabBar'; import { goLogin } from '../../lib/auth-nav'; import { getToken, isLoggedIn, request, toast } from '../../lib/api'; import { getHomeCatalogCache, isHomeCatalogBootstrapped, setHomeCatalogCache, } from '../../lib/home-catalog-session'; import { ensurePayReady } from '../../lib/pay-ready'; import { capturePromoSceneAndTouchScan } from '../../lib/promo'; import { getProductMainImage } from '../../lib/product-images'; import { canBuyOnline, canCrossCity, canPickupOnSite, normalizeFulfillmentFlags, } from '../../lib/product-fulfillment'; import { getCityCodeForCatalog, resolveUserCity } from '../../lib/user-location'; import { DEFAULT_SHARE_DESC, DEFAULT_SHARE_TITLE, toWeappShareMessage, } from '../../lib/wechat-share'; type Product = { id: string; name: string; subtitle?: string; spec?: string; price: number; benefitDisplay?: number; mainImageUrl?: string | null; carouselUrls?: string[] | null; aromaType: string; allowOnSitePickup?: boolean; allowOnlinePurchase?: boolean; allowCrossCityDelivery?: boolean; }; type FulfillmentFilter = 'ALL' | 'ONLINE' | 'CROSS_CITY' | 'ON_SITE'; const FULFILLMENT_FILTERS: Array<{ key: FulfillmentFilter; label: string }> = [ { key: 'ALL', label: '全部' }, { key: 'ONLINE', label: '线上' }, { key: 'CROSS_CITY', label: '跨城' }, { key: 'ON_SITE', label: '现场' }, ]; function matchFulfillmentFilter(p: Product, filter: FulfillmentFilter): boolean { if (filter === 'ALL') return true; if (filter === 'ONLINE') return canBuyOnline(p); if (filter === 'CROSS_CITY') return canCrossCity(p); return canPickupOnSite(p); } type MiniHomeConfig = { banners: string[]; footerUrl: string | null; }; const AROMA_TABS = [ { key: 'QINGXIANG', label: '清香型' }, { key: 'JIANGXIANG', label: '酱香型' }, { 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 [activeAroma, setActiveAroma] = useState('QINGXIANG'); const [fulfillmentFilter, setFulfillmentFilter] = useState('ALL'); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(true); const [displayCity, setDisplayCity] = useState('郑州市'); const [cityCode, setCityCode] = useState('410100'); const [miniHome, setMiniHome] = useState({ banners: [], footerUrl: null }); const scrollingToRef = useRef(null); const scrollLockTimerRef = useRef | null>(null); const lastScrollSyncAtRef = useRef(0); const loadMiniHome = useCallback(() => { return request<{ miniHome?: MiniHomeConfig }>('/common/client-config') .then((cfg) => { const banners = Array.isArray(cfg.miniHome?.banners) ? cfg.miniHome!.banners.filter((u) => typeof u === 'string' && !!u.trim()) : []; const footerUrl = typeof cfg.miniHome?.footerUrl === 'string' && cfg.miniHome.footerUrl.trim() ? cfg.miniHome.footerUrl.trim() : null; setMiniHome({ banners, footerUrl }); }) .catch(() => { /* 首页装饰图失败不阻断商品列表 */ }); }, []); const applyProductList = useCallback((list: Product[], nextCode: string, authKey: string) => { const normalized = Array.isArray(list) ? list.map((p) => normalizeFulfillmentFlags(p)) : []; setProducts(normalized); setHomeCatalogCache({ cityCode: nextCode, authKey, products: normalized }); }, []); const fetchProducts = useCallback( (nextCode: string, authKey: string) => { setLoading(true); return request(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`) .then((list) => applyProductList(list, nextCode, authKey)) .catch((e) => toast(e instanceof Error ? e.message : '加载失败')) .finally(() => setLoading(false)); }, [applyProductList], ); /** * 首次进入 / 城市或登录态变化:拉商品。 * 同次再切 tab:只同步选中态,不重复请求(对齐门店页)。 */ useDidShow(() => { syncTabBarSelected(0); void capturePromoSceneAndTouchScan(); void loadMiniHome(); const authKey = getToken() || ''; void (async () => { const resolved = await resolveUserCity(); const nextCode = getCityCodeForCatalog(resolved); setDisplayCity(resolved.displayCity); setCityCode(nextCode); const cache = getHomeCatalogCache(); if ( isHomeCatalogBootstrapped() && cache && cache.cityCode === nextCode && cache.authKey === authKey && Array.isArray(cache.products) ) { setProducts(cache.products as Product[]); setLoading(false); return; } await fetchProducts(nextCode, authKey); })(); }); usePullDownRefresh(() => { void (async () => { try { const authKey = getToken() || ''; const resolved = await resolveUserCity(); setDisplayCity(resolved.displayCity); const nextCode = getCityCodeForCatalog(resolved); setCityCode(nextCode); setLoading(true); const [list] = await Promise.all([ request(`/catalog/products?cityCode=${encodeURIComponent(nextCode)}`), loadMiniHome(), ]); applyProductList( Array.isArray(list) ? list : [], nextCode, authKey, ); } catch (e) { toast(e instanceof Error ? e.message : '加载失败'); } finally { setLoading(false); Taro.stopPullDownRefresh(); } })(); }); function openProductDetail(id: string) { Taro.navigateTo({ url: `/pages/product-detail/index?id=${id}` }); } async function goOnSitePickup(productId: string) { const returnPath = `/pages/order-confirm-pickup/index?productId=${productId}&qty=1`; if (!isLoggedIn()) { goLogin(returnPath); return; } const ready = await ensurePayReady(returnPath); if (!ready) return; Taro.navigateTo({ url: returnPath }); } const filteredProducts = useMemo( () => products.filter((p) => matchFulfillmentFilter(p, fulfillmentFilter)), [products, fulfillmentFilter], ); const productsByAroma = useMemo(() => { const map: Record = { QINGXIANG: [], JIANGXIANG: [], NONGXIANG: [], }; for (const p of filteredProducts) { const key = p.aromaType as AromaKey; if (key in map) map[key].push(p); } return map; }, [filteredProducts]); const banners = miniHome.banners; const footerUrl = miniHome.footerUrl; const sharePayload = useMemo( () => ({ title: DEFAULT_SHARE_TITLE, desc: DEFAULT_SHARE_DESC, path: '/pages/home/index', imgUrl: banners[0] || undefined, }), [banners], ); useShareAppMessage(() => toWeappShareMessage(sharePayload)); useShareTimeline(() => ({ title: sharePayload.title || DEFAULT_SHARE_TITLE, query: '', 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 ( openProductDetail(p.id)}> {thumb ? ( ) : ( )} {p.name} ¥{Number(p.price).toFixed(0)} {spec ? {spec} : null} {canPickupOnSite(p) ? ( { e.stopPropagation?.(); void goOnSitePickup(p.id); }} > 现场取货 ) : null} {canBuyOnline(p) ? ( { e.stopPropagation?.(); openProductDetail(p.id); }} > 立即购买 ) : null} ); } return ( {banners.length > 0 ? ( 1} autoplay={banners.length > 1} circular={banners.length > 1} interval={2500} > {banners.map((url) => ( ))} ) : null} {AROMA_TABS.map((t) => ( scrollToAroma(t.key)} > {t.label} ))} {displayCity} {FULFILLMENT_FILTERS.map((f) => ( setFulfillmentFilter(f.key)} > {f.label} ))} {loading ? 加载中… : null} {!loading && products.length === 0 ? ( 当前城市暂无在售商品 ) : null} {!loading && products.length > 0 && filteredProducts.length === 0 ? ( 暂无符合履约方式的商品 ) : null} {!loading && filteredProducts.length > 0 && AROMA_TABS.map((t) => { const list = productsByAroma[t.key]; return ( {t.label} {list.length === 0 ? ( 该香型暂未上线 ) : ( list.map((p) => renderProductCard(p)) )} ); })} {footerUrl ? ( ) : null} {shouldRenderPageTabBar() ? : null} ); }