feat: add scroll top mask, unify page top spacing and docs
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
.scroll-top-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 40;
|
||||
pointer-events: none;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.scroll-top-blur,
|
||||
.scroll-top-gradient {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scroll-top-gradient {
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.scroll-top-title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 38%;
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scroll-top-back {
|
||||
position: absolute;
|
||||
left: 30rpx;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
backdrop-filter: blur(32px) saturate(1.2);
|
||||
-webkit-backdrop-filter: blur(32px) saturate(1.2);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.18), 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.theme-dark .scroll-top-back {
|
||||
background: rgba(25, 25, 25, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.14);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.5), 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.scroll-top-back-arrow {
|
||||
font-size: 44rpx;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.scroll-top-blur-mid {
|
||||
backdrop-filter: blur(12rpx);
|
||||
-webkit-backdrop-filter: blur(12rpx);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 42%,
|
||||
rgba(0, 0, 0, 0) 76%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.8) 42%,
|
||||
rgba(0, 0, 0, 0) 76%
|
||||
);
|
||||
}
|
||||
|
||||
.scroll-top-blur-soft {
|
||||
backdrop-filter: blur(6rpx);
|
||||
-webkit-backdrop-filter: blur(6rpx);
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.65) 52%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 0%,
|
||||
rgba(0, 0, 0, 0.65) 52%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.theme-light .scroll-top-gradient {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(250, 247, 242, 0.94) 0%,
|
||||
rgba(250, 247, 242, 0.9) 50%,
|
||||
rgba(250, 247, 242, 0.58) 78%,
|
||||
rgba(250, 247, 242, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.theme-dark .scroll-top-gradient {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(25, 25, 25, 0.94) 0%,
|
||||
rgba(25, 25, 25, 0.9) 50%,
|
||||
rgba(25, 25, 25, 0.58) 78%,
|
||||
rgba(25, 25, 25, 0) 100%
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { View, Text } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { getTopMaskProgress, getTopMaskStartY } from '../../utils/homeTopMask'
|
||||
import './index.scss'
|
||||
|
||||
interface ScrollTopMaskProps {
|
||||
title: string
|
||||
targetSelector?: string
|
||||
showBack?: boolean
|
||||
}
|
||||
|
||||
export default function ScrollTopMask({ title, targetSelector = '.page-header', showBack = false }: ScrollTopMaskProps) {
|
||||
const safe = useSafeArea()
|
||||
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
|
||||
const fallbackTitleMetrics = useMemo(() => ({
|
||||
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
|
||||
rangeY: Math.max(48, safe.headerPaddingTop * 0.45)
|
||||
}), [safe.headerPaddingTop, safe.statusBarHeight])
|
||||
const titleMetricsRef = useRef(fallbackTitleMetrics)
|
||||
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const latestScrollTopRef = useRef(0)
|
||||
|
||||
const applyTopMaskProgress = useCallback((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(() => {
|
||||
let cancelled = false
|
||||
let measureTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
titleMetricsRef.current = fallbackTitleMetrics
|
||||
|
||||
type HeaderRect = Taro.NodesRef.BoundingClientRectCallbackResult
|
||||
|
||||
const measureTitle = () => {
|
||||
if (cancelled) return
|
||||
Taro.createSelectorQuery()
|
||||
.select(targetSelector)
|
||||
.boundingClientRect((rect: HeaderRect | HeaderRect[]) => {
|
||||
if (cancelled) return
|
||||
if (!rect || Array.isArray(rect) || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
|
||||
titleMetricsRef.current = fallbackTitleMetrics
|
||||
applyTopMaskProgress(latestScrollTopRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
const startY = getTopMaskStartY({
|
||||
viewportBottom: rect.bottom,
|
||||
scrollTop: latestScrollTopRef.current,
|
||||
statusBarHeight: safe.statusBarHeight
|
||||
})
|
||||
const rangeY = Math.max(32, rect.height * 0.7)
|
||||
titleMetricsRef.current = { startY, rangeY }
|
||||
applyTopMaskProgress(latestScrollTopRef.current)
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
|
||||
Taro.nextTick(() => {
|
||||
if (cancelled) return
|
||||
measureTimer = setTimeout(measureTitle, 80)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (measureTimer !== null) {
|
||||
clearTimeout(measureTimer)
|
||||
}
|
||||
if (scrollFrameRef.current !== null) {
|
||||
clearTimeout(scrollFrameRef.current)
|
||||
}
|
||||
scrollFrameRef.current = null
|
||||
}
|
||||
}, [applyTopMaskProgress, fallbackTitleMetrics, targetSelector])
|
||||
|
||||
Taro.usePageScroll((e) => {
|
||||
applyTopMaskProgress(e.scrollTop)
|
||||
})
|
||||
|
||||
return (
|
||||
<View
|
||||
className='scroll-top-mask'
|
||||
style={{
|
||||
height: `${Math.max(safe.menuButtonTop + safe.menuButtonHeight + 18, safe.statusBarHeight + 52) + 24}px`,
|
||||
opacity: topMaskOpacity
|
||||
}}
|
||||
>
|
||||
<View className='scroll-top-blur scroll-top-blur-mid' />
|
||||
<View className='scroll-top-blur scroll-top-blur-soft' />
|
||||
<View className='scroll-top-gradient' />
|
||||
{showBack && (
|
||||
<View
|
||||
className='scroll-top-back'
|
||||
style={{ top: `${safe.menuButtonTop}px` }}
|
||||
onTap={() => Taro.navigateBack()}
|
||||
>
|
||||
<Text className='scroll-top-back-arrow'>←</Text>
|
||||
</View>
|
||||
)}
|
||||
<Text
|
||||
className='scroll-top-title'
|
||||
style={{
|
||||
opacity: Math.max(0, Math.min(1, (topMaskOpacity - 0.5) / 0.5))
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -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 ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../components/BottomActionBar'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
@@ -97,9 +98,10 @@ export default function AddressPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="收货地址" targetSelector=".page-header" showBack />
|
||||
<View className='address-page'>
|
||||
|
||||
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
|
||||
export default function AgreementPage() {
|
||||
const { theme, resolvedTheme } = useThemeContext()
|
||||
@@ -36,8 +37,9 @@ export default function AgreementPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="定制协议" targetSelector=".agreement-title" />
|
||||
<View className='agreement-page'>
|
||||
<View className='agreement-content' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='agreement-content' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className='agreement-title'>定制协议</Text>
|
||||
{SECTIONS.map((sec, idx) => (
|
||||
<View key={idx} className='agreement-section surface-card'>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../components/BottomActionBar'
|
||||
|
||||
export default function CheckoutPage() {
|
||||
@@ -67,8 +68,9 @@ export default function CheckoutPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="设计效果确认" targetSelector=".page-header" showBack />
|
||||
<View className='checkout-page'>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
@@ -94,9 +96,10 @@ export default function CheckoutPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="设计效果确认" targetSelector=".page-header" showBack />
|
||||
<View className='checkout-page'>
|
||||
|
||||
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.design-page {
|
||||
padding: 0 32rpx 24rpx;
|
||||
padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import LoginGuard from '../../components/LoginGuard'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ code: 'all', label: '全部' },
|
||||
@@ -120,9 +121,10 @@ export default function DesignListPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="我的设计清单" targetSelector=".page-header.design-header" />
|
||||
<LoginGuard>
|
||||
<View className='design-page'>
|
||||
<View className='page-header design-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header design-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='header-spacer' />
|
||||
<Text className='header-title'>我的设计清单</Text>
|
||||
<View className='header-manage' onTap={handleManage}>
|
||||
|
||||
@@ -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 ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../components/BottomActionBar'
|
||||
|
||||
export default function DIYPage() {
|
||||
@@ -264,6 +265,7 @@ export default function DIYPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
|
||||
<View className='diy-page'>
|
||||
<Text className='page-title'>加载中...</Text>
|
||||
</View>
|
||||
@@ -274,9 +276,10 @@ export default function DIYPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="设计工作台" targetSelector=".page-header.surface-card" showBack />
|
||||
<View className='diy-page'>
|
||||
|
||||
<View className='page-header surface-card mt-20' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header surface-card mt-20' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='flex-between' style={{ width: '100%' }}>
|
||||
<Text className='back-btn' onTap={goBack}>←</Text>
|
||||
<Text className='page-title'>设计工作台</Text>
|
||||
|
||||
@@ -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 ScrollTopMask from '../../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../../components/BottomActionBar'
|
||||
|
||||
/* ============================================================
|
||||
@@ -311,9 +312,10 @@ export default function StickerEditPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="编辑贴纸" targetSelector=".edit-header" showBack />
|
||||
<View className='sticker-edit-page'>
|
||||
{/* Header */}
|
||||
<View className='edit-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='edit-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className='edit-back' onTap={() => Taro.navigateBack()}>←</Text>
|
||||
<Text className='edit-title'>编辑贴纸</Text>
|
||||
<Text className='edit-save' onTap={handleSave}>保存</Text>
|
||||
|
||||
+10
-34
@@ -1,5 +1,6 @@
|
||||
.index-page {
|
||||
padding: 0 32rpx 24rpx;
|
||||
padding-bottom: calc(220rpx + env(safe-area-inset-bottom));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@
|
||||
border-radius: 999rpx;
|
||||
background: var(--bg-input);
|
||||
border: 1rpx solid transparent;
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
@@ -54,7 +56,12 @@
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.search-input-placeholder {
|
||||
color: var(--text-secondary);
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
@@ -118,10 +125,10 @@
|
||||
max-width: 78%;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.6);
|
||||
border: 1rpx solid var(--glass-line);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
@@ -252,34 +259,3 @@
|
||||
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);
|
||||
}
|
||||
|
||||
.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%
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { View, Text, Image, Input, Button, Swiper, SwiperItem } from '@tarojs/components'
|
||||
import Taro from '@tarojs/taro'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import './index.scss'
|
||||
import { CATEGORIES } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
@@ -8,7 +8,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import { getTopMaskProgress, getTopMaskStartY } from '../../utils/homeTopMask'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
|
||||
// 成品展示配图(从 img 里取对应实物图)
|
||||
const SHOWCASE_LIST = [
|
||||
@@ -71,81 +71,6 @@ export default function Index() {
|
||||
useStatusBar(resolvedTheme)
|
||||
const [searchKey, setSearchKey] = useState('')
|
||||
|
||||
const [topMaskOpacity, setTopMaskOpacity] = useState(0)
|
||||
const fallbackTitleMetrics = useMemo(() => ({
|
||||
startY: Math.max(0, safe.headerPaddingTop - safe.statusBarHeight),
|
||||
rangeY: Math.max(48, safe.headerPaddingTop * 0.45)
|
||||
}), [safe.headerPaddingTop, safe.statusBarHeight])
|
||||
const titleMetricsRef = useRef(fallbackTitleMetrics)
|
||||
const scrollFrameRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const latestScrollTopRef = useRef(0)
|
||||
|
||||
const applyTopMaskProgress = useCallback((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(() => {
|
||||
let cancelled = false
|
||||
let measureTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
titleMetricsRef.current = fallbackTitleMetrics
|
||||
|
||||
type HomeHeaderRect = Taro.NodesRef.BoundingClientRectCallbackResult
|
||||
|
||||
const measureTitle = () => {
|
||||
if (cancelled) return
|
||||
Taro.createSelectorQuery()
|
||||
.select('.home-header')
|
||||
.boundingClientRect((rect: HomeHeaderRect | HomeHeaderRect[]) => {
|
||||
if (cancelled) return
|
||||
if (!rect || Array.isArray(rect) || typeof rect.bottom !== 'number' || typeof rect.height !== 'number') {
|
||||
titleMetricsRef.current = fallbackTitleMetrics
|
||||
applyTopMaskProgress(latestScrollTopRef.current)
|
||||
return
|
||||
}
|
||||
|
||||
const startY = getTopMaskStartY({
|
||||
viewportBottom: rect.bottom,
|
||||
scrollTop: latestScrollTopRef.current,
|
||||
statusBarHeight: safe.statusBarHeight
|
||||
})
|
||||
const rangeY = Math.max(32, rect.height * 0.7)
|
||||
titleMetricsRef.current = { startY, rangeY }
|
||||
applyTopMaskProgress(latestScrollTopRef.current)
|
||||
})
|
||||
.exec()
|
||||
}
|
||||
|
||||
Taro.nextTick(() => {
|
||||
if (cancelled) return
|
||||
measureTimer = setTimeout(measureTitle, 80)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (measureTimer !== null) {
|
||||
clearTimeout(measureTimer)
|
||||
}
|
||||
if (scrollFrameRef.current !== null) {
|
||||
clearTimeout(scrollFrameRef.current)
|
||||
}
|
||||
scrollFrameRef.current = null
|
||||
}
|
||||
}, [applyTopMaskProgress, fallbackTitleMetrics])
|
||||
|
||||
Taro.usePageScroll((e) => {
|
||||
applyTopMaskProgress(e.scrollTop)
|
||||
})
|
||||
|
||||
const navigateToProduct = (categoryId: string) => {
|
||||
Taro.navigateTo({ url: `/pages/product/index?id=${categoryId}` })
|
||||
}
|
||||
@@ -174,17 +99,11 @@ export default function Index() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<View
|
||||
className='home-top-mask'
|
||||
style={{
|
||||
height: `${Math.max(safe.menuButtonTop + safe.menuButtonHeight + 18, safe.statusBarHeight + 52)}px`,
|
||||
opacity: topMaskOpacity
|
||||
}}
|
||||
/>
|
||||
<ScrollTopMask title='智绘微刻' targetSelector='.home-header' />
|
||||
<View className='index-page'>
|
||||
|
||||
{/* 页头 */}
|
||||
<View className='home-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='home-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className='home-title'>智绘微刻</Text>
|
||||
</View>
|
||||
|
||||
@@ -194,7 +113,8 @@ export default function Index() {
|
||||
<Input
|
||||
className='search-input'
|
||||
type='text'
|
||||
placeholder='搜索定制品类:笔记本、杯垫、书灯...'
|
||||
placeholder='搜索定制品类,如笔记本、杯垫、书灯'
|
||||
placeholderClass='search-input-placeholder'
|
||||
value={searchKey}
|
||||
onInput={handleSearch}
|
||||
confirmType='search'
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { getOrderList, getDefaultAddress, getAddressList, type AddressItem, type OrderItem } from '../../utils/store'
|
||||
import { PRODUCT_ICON_MAP } from '../../utils/productConfig'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
@@ -55,8 +56,9 @@ export default function OrderDetailPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="订单详情" targetSelector=".page-header" showBack />
|
||||
<View className='order-detail-page'>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
@@ -73,9 +75,10 @@ export default function OrderDetailPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="订单详情" targetSelector=".page-header" showBack />
|
||||
<View className='order-detail-page'>
|
||||
|
||||
<View className='page-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.orders-page {
|
||||
padding: 0 32rpx 48rpx;
|
||||
padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import LoginGuard from '../../components/LoginGuard'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
|
||||
const STATUS_TABS = [
|
||||
{ code: 'all', label: '全部' },
|
||||
@@ -84,10 +85,11 @@ export default function OrdersPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="我的订单" targetSelector=".orders-page .page-header" />
|
||||
<LoginGuard>
|
||||
<View className='orders-page'>
|
||||
|
||||
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className='page-title'>我的订单</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -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 ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../components/BottomActionBar'
|
||||
import { getProductIconImg } from '../../utils/productConfig'
|
||||
|
||||
@@ -27,8 +28,9 @@ export default function ProductPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="商品详情" targetSelector=".page-header" showBack />
|
||||
<View className='product-page'>
|
||||
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
@@ -72,9 +74,10 @@ export default function ProductPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="商品详情" targetSelector=".page-header" showBack />
|
||||
<View className='product-page'>
|
||||
|
||||
<View className='page-header' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='page-header-inner'>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.profile-page {
|
||||
padding: 0 32rpx 48rpx;
|
||||
padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { login, getMe, updateProfile, getToken } from '../../utils/api'
|
||||
import { safeHideLoading } from '../../utils/request'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
@@ -279,10 +280,11 @@ export default function ProfilePage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="我的" targetSelector=".profile-header" />
|
||||
<View className='profile-page'>
|
||||
|
||||
{/* 用户信息头部 */}
|
||||
<View className='profile-header surface-card' style={{ marginTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='profile-header surface-card' style={{ marginTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='user-info'>
|
||||
{isLoggedIn && userInfo.avatarUrl ? (
|
||||
<Image className='avatar' src={userInfo.avatarUrl} mode='aspectFill' />
|
||||
|
||||
@@ -9,19 +9,56 @@
|
||||
}
|
||||
|
||||
.service-page .page-header.service-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin: 0;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.service-header .back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1rpx solid var(--border-strong);
|
||||
box-shadow: var(--shadow-card);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.service-header .back-btn:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
.service-header .back-arrow {
|
||||
font-size: 36rpx;
|
||||
line-height: 1;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.service-header .page-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.service-header .header-spacer {
|
||||
width: 88rpx;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
/* 客服主入口 */
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
const FAQ_ITEMS = [
|
||||
@@ -22,9 +23,14 @@ export default function ServicePage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="联系客服" targetSelector=".page-header.service-header" showBack />
|
||||
<View className='service-page'>
|
||||
<View className='page-header service-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header service-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
</View>
|
||||
<Text className='page-title'>联系客服</Text>
|
||||
<View className='header-spacer' />
|
||||
</View>
|
||||
|
||||
{/* 客服入口 */}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { getUserInfo, setUserInfo, clearUserInfo, getAddressList, getDefaultAddress, type AddressItem } from '../../utils/store'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
@@ -69,10 +70,11 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`} style={{ minHeight: '100vh' }}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="设置" targetSelector=".page-header.settings-header" showBack />
|
||||
<View className='settings-page'>
|
||||
<View className='settings-area'>
|
||||
|
||||
<View className='page-header settings-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header settings-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='back-btn' onTap={() => Taro.navigateBack()}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
</View>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useSafeArea } from '../../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../../hooks/useStatusBar'
|
||||
import { useThemeContext } from '../../../context/ThemeContext'
|
||||
import ThemedPageMeta from '../../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../../components/ScrollTopMask'
|
||||
import BottomActionBar from '../../../components/BottomActionBar'
|
||||
|
||||
const clamp = (v: number, min: number, max: number) => Math.min(Math.max(v, min), max)
|
||||
@@ -111,6 +112,7 @@ export default function ShopDetailPage() {
|
||||
return (
|
||||
<View className="shop-detail-page theme-dark">
|
||||
<ThemedPageMeta darkTop />
|
||||
<ScrollTopMask title="商品详情" targetSelector=".shop-back-btn" showBack />
|
||||
<Text style={{ color: '#fff', padding: '40rpx' }}>商品不存在</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -119,6 +121,7 @@ export default function ShopDetailPage() {
|
||||
return (
|
||||
<View className="shop-detail-page theme-dark">
|
||||
<ThemedPageMeta darkTop />
|
||||
<ScrollTopMask title="商品详情" targetSelector=".shop-back-btn" showBack />
|
||||
{/* Fixed Hero */}
|
||||
<View className="shop-hero-fixed">
|
||||
<View
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/* --- List Layer --- */
|
||||
.shop-list {
|
||||
padding: 0 32rpx 24rpx;
|
||||
padding: 0 32rpx calc(220rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.shop-header {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import './index.scss'
|
||||
import { PRODUCTS } from '../../utils/productConfig'
|
||||
|
||||
@@ -19,8 +20,9 @@ export default function ShopPage() {
|
||||
return (
|
||||
<View className={`shop-page theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="智绘精选" targetSelector=".shop-header" />
|
||||
<View className="shop-list">
|
||||
<View className="shop-header" style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className="shop-header" style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className="shop-header-title">智绘精选</Text>
|
||||
<Text className="shop-header-sub">激光微雕定制好物</Text>
|
||||
</View>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
export default function UserDatabasePage() {
|
||||
@@ -86,7 +87,8 @@ export default function UserDatabasePage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<View className='udb-page udb-lock-page' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<ScrollTopMask title="本地用户数据库" targetSelector=".page-header.udb-header" />
|
||||
<View className='udb-page udb-lock-page' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='udb-lock-card surface-card'>
|
||||
<Image className='udb-lock-icon' src={assetUrl('/icon/账号管理.png')} mode='aspectFit' />
|
||||
<Text className='udb-lock-title'>管理员验证</Text>
|
||||
@@ -114,9 +116,10 @@ export default function UserDatabasePage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="本地用户数据库" targetSelector=".page-header.udb-header" />
|
||||
<View className='udb-page'>
|
||||
|
||||
<View className='page-header udb-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header udb-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<Text className='page-title'>本地用户数据库</Text>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useThemeContext } from '../../context/ThemeContext'
|
||||
import { useSafeArea } from '../../hooks/useSafeArea'
|
||||
import { useStatusBar } from '../../hooks/useStatusBar'
|
||||
import ThemedPageMeta from '../../components/ThemedPageMeta'
|
||||
import ScrollTopMask from '../../components/ScrollTopMask'
|
||||
import { assetUrl } from '../../utils/asset'
|
||||
|
||||
export default function WordCloudPage() {
|
||||
@@ -64,9 +65,10 @@ export default function WordCloudPage() {
|
||||
return (
|
||||
<View className={`theme-${resolvedTheme}`}>
|
||||
<ThemedPageMeta />
|
||||
<ScrollTopMask title="AI词云生成" targetSelector=".page-header.cloud-header" showBack />
|
||||
<View className='wordcloud-page'>
|
||||
|
||||
<View className='page-header cloud-header' style={{ paddingTop: `${safe.headerPaddingTop}px` }}>
|
||||
<View className='page-header cloud-header' style={{ paddingTop: `${safe.statusBarHeight + 12}px` }}>
|
||||
<View className='back-btn' onTap={goBack}>
|
||||
<Text className='back-arrow'>←</Text>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user