Files
template-10584/src/pages/index/Banner.tsx
赵忠林 8679b26f74 feat(rider): 新增水票核销功能
- 添加水票核销扫码页面,支持扫描加密和明文二维码
- 实现水票验证逻辑,包括余额检查和核销确认
- 添加核销记录展示,最多保留最近10条记录
- 在骑手端界面增加水票核销入口
- 新增获取用户水票总数的API接口
- 优化首页轮播图加载,增加缓存和懒加载机制
- 添加门店选择功能,支持订单确认页切换门店
- 修复物流信息类型安全问题
- 更新用户中心门店相关文案显示
2026-02-05 01:08:37 +08:00

173 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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