forked from gxwebsoft/mp-10550
- 添加水票核销扫码页面,支持扫描加密和明文二维码 - 实现水票验证逻辑,包括余额检查和核销确认 - 添加核销记录展示,最多保留最近10条记录 - 在骑手端界面增加水票核销入口 - 新增获取用户水票总数的API接口 - 优化首页轮播图加载,增加缓存和懒加载机制 - 添加门店选择功能,支持订单确认页切换门店 - 修复物流信息类型安全问题 - 更新用户中心门店相关文案显示
173 lines
5.4 KiB
TypeScript
173 lines
5.4 KiB
TypeScript
import {useEffect, useState} from 'react'
|
||
import {View} from '@tarojs/components'
|
||
import {Swiper} from '@nutui/nutui-react-taro'
|
||
import {CmsAd} from "@/api/cms/cmsAd/model";
|
||
import {Image} from '@nutui/nutui-react-taro'
|
||
import {getCmsAdByCode} from "@/api/cms/cmsAd";
|
||
import navTo from "@/utils/common";
|
||
import {pageCmsArticle} from "@/api/cms/cmsArticle";
|
||
import Taro from '@tarojs/taro'
|
||
|
||
type AdImage = {
|
||
url?: string
|
||
path?: string
|
||
title?: string
|
||
// Compatible keys (some backends use different fields)
|
||
src?: string
|
||
imageUrl?: string
|
||
}
|
||
|
||
function normalizeAdImages(ad?: CmsAd): AdImage[] {
|
||
const list = ad?.imageList
|
||
if (Array.isArray(list) && list.length) return list as AdImage[]
|
||
|
||
// Some APIs only return `images` as a JSON string.
|
||
const raw = ad?.images
|
||
if (!raw) return []
|
||
try {
|
||
const parsed = JSON.parse(raw)
|
||
return Array.isArray(parsed) ? (parsed as AdImage[]) : []
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
|
||
function toNumberPx(input: unknown, fallback: number) {
|
||
const n = typeof input === 'number' ? input : Number.parseInt(String(input ?? ''), 10)
|
||
return Number.isFinite(n) ? n : fallback
|
||
}
|
||
|
||
const MyPage = () => {
|
||
const [carouselData, setCarouselData] = useState<CmsAd>()
|
||
const [loading, setLoading] = useState(true)
|
||
// const [disableSwiper, setDisableSwiper] = useState(false)
|
||
|
||
const CACHE_KEY = 'home_banner_mp-ad'
|
||
|
||
// 用于记录触摸开始位置
|
||
// const touchStartRef = useRef({x: 0, y: 0})
|
||
|
||
// 加载数据
|
||
const loadData = async (opts?: {silent?: boolean}) => {
|
||
if (!opts?.silent) setLoading(true)
|
||
try {
|
||
// 只阻塞 banner 自己的数据;其他数据预热不应影响首屏展示速度
|
||
const flash = await getCmsAdByCode('mp-ad')
|
||
setCarouselData(flash)
|
||
void Taro.setStorage({ key: CACHE_KEY, data: flash }).catch(() => {})
|
||
} catch (error) {
|
||
console.error('Banner数据加载失败:', error)
|
||
} finally {
|
||
if (!opts?.silent) setLoading(false)
|
||
}
|
||
|
||
// 后台预热(不阻塞 banner 渲染)
|
||
void getCmsAdByCode('hot_today').catch(() => {})
|
||
void pageCmsArticle({ limit: 1, recommend: 1 }).catch(() => {})
|
||
}
|
||
|
||
useEffect(() => {
|
||
const cached = Taro.getStorageSync(CACHE_KEY) as CmsAd | undefined
|
||
// 有缓存则先渲染缓存,避免首屏等待;再静默刷新
|
||
if (cached && normalizeAdImages(cached).length) {
|
||
setCarouselData(cached)
|
||
setLoading(false)
|
||
void loadData({ silent: true })
|
||
return
|
||
}
|
||
void loadData()
|
||
}, [])
|
||
|
||
// 轮播图高度,默认300px
|
||
const carouselHeight = toNumberPx(carouselData?.height, 300)
|
||
const carouselImages = normalizeAdImages(carouselData)
|
||
|
||
// 骨架屏组件
|
||
const BannerSkeleton = () => (
|
||
<View className="flex p-2 justify-between" style={{height: `${carouselHeight}px`}}>
|
||
{/* 左侧轮播图骨架屏 */}
|
||
<View style={{width: '50%', height: '100%'}}>
|
||
<View
|
||
className="bg-gray-200 rounded animate-pulse"
|
||
style={{height: `${carouselHeight}px`}}
|
||
/>
|
||
</View>
|
||
|
||
{/* 右侧骨架屏 */}
|
||
<View className="flex flex-col" style={{width: '50%', height: '100%'}}>
|
||
{/* 上层骨架屏 */}
|
||
<View className="ml-2 bg-white rounded-lg">
|
||
<View className="px-3 my-2">
|
||
<View className="bg-gray-200 h-4 w-16 rounded animate-pulse"/>
|
||
</View>
|
||
<View className="px-3 flex" style={{height: '110px'}}>
|
||
{[1, 2].map(i => (
|
||
<View key={i} className="item flex flex-col mr-4">
|
||
<View className="bg-gray-200 rounded animate-pulse" style={{width: 70, height: 70}}/>
|
||
<View className="bg-gray-200 h-3 w-16 rounded mt-2 animate-pulse"/>
|
||
</View>
|
||
))}
|
||
</View>
|
||
</View>
|
||
|
||
{/* 下层骨架屏 */}
|
||
<View className="ml-2 bg-white rounded-lg mt-3">
|
||
<View className="px-3 my-2">
|
||
<View className="bg-gray-200 h-4 w-20 rounded animate-pulse"/>
|
||
</View>
|
||
<View className="rounded-lg px-3 pb-3">
|
||
<View className="bg-gray-200 rounded animate-pulse" style={{width: '100%', height: 106}}/>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
)
|
||
|
||
// 如果正在加载,显示骨架屏
|
||
if (loading) {
|
||
return <BannerSkeleton />
|
||
}
|
||
|
||
return (
|
||
<Swiper
|
||
defaultValue={0}
|
||
height={carouselHeight}
|
||
indicator
|
||
autoPlay
|
||
duration={3000}
|
||
style={{
|
||
height: `${carouselHeight}px`,
|
||
touchAction: 'pan-y' // 关键修改:允许垂直滑动
|
||
}}
|
||
disableTouch={false}
|
||
direction="horizontal"
|
||
className="custom-swiper"
|
||
>
|
||
{carouselImages.map((img, index) => {
|
||
const src = img.url || img.src || img.imageUrl
|
||
if (!src) return null
|
||
return (
|
||
<Swiper.Item key={index} style={{ touchAction: 'pan-x pan-y' }}>
|
||
<Image
|
||
width="100%"
|
||
height="100%"
|
||
src={src}
|
||
mode={'scaleToFill'}
|
||
onClick={() => (img.path ? navTo(`${img.path}`) : undefined)}
|
||
// 首张图优先加载,其余按需懒加载,避免并发图片请求拖慢首屏可见
|
||
lazyLoad={index !== 0}
|
||
style={{
|
||
height: `${carouselHeight}px`,
|
||
borderRadius: '4px',
|
||
touchAction: 'manipulation' // 关键修改:优化触摸操作
|
||
}}
|
||
/>
|
||
</Swiper.Item>
|
||
)
|
||
})}
|
||
</Swiper>
|
||
)
|
||
}
|
||
|
||
export default MyPage
|