diff --git a/src/pages/index/index.scss b/src/pages/index/index.scss index bbcb1bc..c12ef4c 100644 --- a/src/pages/index/index.scss +++ b/src/pages/index/index.scss @@ -252,3 +252,35 @@ font-size: 24rpx; color: var(--text-secondary); } + +.home-top-mask { + position: fixed; + top: 0; + left: 0; + right: 0; + z-index: 40; + pointer-events: none; + transform: translateZ(0); + will-change: opacity; +} + +.theme-light .home-top-mask { + background: linear-gradient( + to bottom, + rgba(250, 247, 242, 0.98) 0%, + rgba(250, 247, 242, 0.92) 38%, + rgba(250, 247, 242, 0.48) 70%, + rgba(250, 247, 242, 0) 100% + ); +} + +.theme-dark .home-top-mask { + background: linear-gradient( + to bottom, + rgba(25, 25, 25, 0.98) 0%, + rgba(25, 25, 25, 0.92) 38%, + rgba(25, 25, 25, 0.48) 70%, + rgba(25, 25, 25, 0) 100% + ); +} + diff --git a/src/pages/index/index.tsx b/src/pages/index/index.tsx index 29dcdc7..64f829b 100644 --- a/src/pages/index/index.tsx +++ b/src/pages/index/index.tsx @@ -1,6 +1,6 @@ import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components' import Taro from '@tarojs/taro' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import './index.scss' import { CATEGORIES } from '../../utils/productConfig' import { assetUrl } from '../../utils/asset' @@ -8,6 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext' import { useSafeArea } from '../../hooks/useSafeArea' import { useStatusBar } from '../../hooks/useStatusBar' import ThemedPageMeta from '../../components/ThemedPageMeta' +import { getTopMaskProgress } from '../../utils/homeTopMask' // 成品展示配图(从 img 里取对应实物图) const SHOWCASE_LIST = [ @@ -70,6 +71,63 @@ export default function Index() { useStatusBar(resolvedTheme) const [searchKey, setSearchKey] = useState('') + const [topMaskOpacity, setTopMaskOpacity] = useState(0) + const titleMetricsRef = useRef({ startY: 0, rangeY: 48 }) + const scrollFrameRef = useRef | null>(null) + const latestScrollTopRef = useRef(0) + + const applyTopMaskProgress = (scrollTop: number) => { + latestScrollTopRef.current = scrollTop + if (scrollFrameRef.current !== null) { + return + } + + scrollFrameRef.current = setTimeout(() => { + scrollFrameRef.current = null + const { startY, rangeY } = titleMetricsRef.current + setTopMaskOpacity(getTopMaskProgress({ scrollTop: latestScrollTopRef.current, startY, rangeY })) + }, 16) + } + + useEffect(() => { + const measureTitle = () => { + Taro.createSelectorQuery() + .select('.home-header') + .boundingClientRect((rect: { top?: number; bottom?: number; height?: number } | null) => { + if (!rect || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') { + const fallbackRange = Math.max(48, safe.headerPaddingTop * 0.45) + titleMetricsRef.current = { + startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight), + rangeY: fallbackRange + } + applyTopMaskProgress(latestScrollTopRef.current) + return + } + + const startY = Math.max(0, rect.bottom - safe.statusBarHeight - 8) + const rangeY = Math.max(32, rect.height * 0.7) + titleMetricsRef.current = { startY, rangeY } + applyTopMaskProgress(latestScrollTopRef.current) + }) + .exec() + } + + Taro.nextTick(() => { + setTimeout(measureTitle, 80) + }) + + return () => { + if (scrollFrameRef.current !== null) { + clearTimeout(scrollFrameRef.current) + } + scrollFrameRef.current = null + } + }, [safe.headerPaddingTop, safe.statusBarHeight]) + + Taro.usePageScroll((e) => { + applyTopMaskProgress(e.scrollTop) + }) + const navigateToProduct = (categoryId: string) => { Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` }) } @@ -98,6 +156,13 @@ export default function Index() { return ( + {/* 页头 */}