feat: 首页更新
This commit is contained in:
156
pages/HomeScreen/components/BannerSwiper/index.tsx
Normal file
156
pages/HomeScreen/components/BannerSwiper/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 轮播图组件
|
||||
*
|
||||
* 展示首页轮播图,支持自动播放和手动滑动
|
||||
* 使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import Colors from '@/constants/Colors';
|
||||
// import type { Banner } from '@/types/home';
|
||||
import { styles } from './styles';
|
||||
import useMsgStore from '@/stores/msgStore';
|
||||
|
||||
|
||||
interface BannerSwiperProps {
|
||||
theme: 'light' | 'dark';
|
||||
}
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 轮播图组件
|
||||
*/
|
||||
export default function BannerSwiper({ theme }: BannerSwiperProps) {
|
||||
const s = styles[theme];
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const autoPlayTimerRef = useRef<any>(null);
|
||||
const { homeBanner } = useMsgStore();
|
||||
|
||||
|
||||
// 加载轮播图数据
|
||||
useEffect(() => {
|
||||
// 如果有传入的 banners 数据,直接使用
|
||||
if (homeBanner.length > 0) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
// 如果没有数据,保持 loading 状态显示骨架屏
|
||||
}, [homeBanner]);
|
||||
|
||||
// 处理 Banner 点击
|
||||
const onBannerPress = useCallback((banner: Record<string, any>) => {
|
||||
Alert.alert('轮播图', `点击了: ${banner.title || banner.id}`);
|
||||
// 这里可以添加导航逻辑
|
||||
}, []);
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const contentOffsetX = event.nativeEvent.contentOffset.x;
|
||||
const index = Math.round(contentOffsetX / (width - 24));
|
||||
setCurrentIndex(Math.min(index, homeBanner.length - 1));
|
||||
};
|
||||
|
||||
// 启动自动播放
|
||||
const startAutoPlay = useCallback(() => {
|
||||
if (homeBanner.length <= 1) return;
|
||||
autoPlayTimerRef.current = setInterval(() => {
|
||||
setCurrentIndex((prev) => {
|
||||
const nextIndex = (prev + 1) % homeBanner.length;
|
||||
scrollViewRef.current?.scrollTo({
|
||||
x: nextIndex * (width - 24),
|
||||
animated: true,
|
||||
});
|
||||
return nextIndex;
|
||||
});
|
||||
}, 5000);
|
||||
}, [homeBanner.length]);
|
||||
|
||||
// 停止自动播放
|
||||
const stopAutoPlay = useCallback(() => {
|
||||
if (autoPlayTimerRef.current) {
|
||||
clearInterval(autoPlayTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 自动播放
|
||||
useEffect(() => {
|
||||
if (!loading && homeBanner.length > 0) {
|
||||
startAutoPlay();
|
||||
}
|
||||
return () => stopAutoPlay();
|
||||
}, [loading, homeBanner.length, startAutoPlay, stopAutoPlay]);
|
||||
|
||||
// 骨架屏 - 加载中显示占位符
|
||||
if (loading || homeBanner.length === 0) {
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<View
|
||||
style={[
|
||||
s.image,
|
||||
{
|
||||
backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
scrollEventThrottle={16}
|
||||
onScroll={handleScroll}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollBegin={stopAutoPlay}
|
||||
onMomentumScrollEnd={startAutoPlay}
|
||||
>
|
||||
{homeBanner.map((banner) => (
|
||||
<TouchableOpacity
|
||||
key={banner.id}
|
||||
onPress={() => onBannerPress(banner)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: banner.subject }}
|
||||
style={s.image}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* 指示器 */}
|
||||
<View style={s.indicatorContainer}>
|
||||
{homeBanner.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
s.indicator,
|
||||
index === currentIndex && s.indicatorActive,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
61
pages/HomeScreen/components/BannerSwiper/styles.ts
Normal file
61
pages/HomeScreen/components/BannerSwiper/styles.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import { Dimensions } from 'react-native';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const BANNER_HEIGHT = width * 0.32534; // 保持 32.534% 的宽高比
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
export const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
height: BANNER_HEIGHT,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
marginHorizontal: 12,
|
||||
marginBottom: 12,
|
||||
elevation: 3,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
scrollView: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
image: {
|
||||
width: width - 24,
|
||||
height: BANNER_HEIGHT,
|
||||
},
|
||||
indicatorContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
indicator: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.5)',
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
indicatorActive: {
|
||||
width: 12,
|
||||
height: 8,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
},
|
||||
loadingContainer: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user