feat: 首页更新
This commit is contained in:
180
pages/HomeScreen/HomeScreenComplete.tsx
Normal file
180
pages/HomeScreen/HomeScreenComplete.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 完整首页容器
|
||||
* 包含 Header、内容区域、BottomTabs
|
||||
* 支持主题切换和真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, ScrollView, RefreshControl, StyleSheet, SafeAreaView, Alert } from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import Header from './components/Header';
|
||||
import BannerSwiper from './components/BannerSwiper';
|
||||
import NoticeBar from './components/NoticeBar';
|
||||
import GameMainMenus from './components/GameMainMenus';
|
||||
import Lobby from './components/Lobby';
|
||||
import HighPrizeGame from './components/HighPrizeGame';
|
||||
import FastFootNav from './components/FastFootNav';
|
||||
import { requestHomePageData } from '@/stores/gameStore';
|
||||
import { useTenantLoad } from '@/stores/tenantStore';
|
||||
import type {
|
||||
Banner,
|
||||
Notice,
|
||||
GameCategory,
|
||||
Game,
|
||||
HighPrizeGame as HighPrizeGameType,
|
||||
} from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
interface HomeScreenCompleteProps {
|
||||
theme?: 'light' | 'dark';
|
||||
isDarkTheme?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整首页容器
|
||||
*/
|
||||
export default function HomeScreenComplete({
|
||||
theme = 'light',
|
||||
isDarkTheme = false,
|
||||
}: HomeScreenCompleteProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(0);
|
||||
const tenantLoad = useTenantLoad();
|
||||
|
||||
// 加载首页数据
|
||||
const loadHomePageData = useCallback(async () => {
|
||||
try {
|
||||
await requestHomePageData();
|
||||
} catch (error) {
|
||||
console.error('加载首页数据失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
console.log('租户数据加载完成:', tenantLoad);
|
||||
if (tenantLoad) {
|
||||
loadHomePageData();
|
||||
}
|
||||
}, [loadHomePageData, tenantLoad]);
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadHomePageData();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [loadHomePageData]);
|
||||
|
||||
// 处理分类选择
|
||||
const handleCategorySelect = useCallback((categoryId: number) => {
|
||||
setSelectedCategory(categoryId);
|
||||
// 这里可以根据分类过滤游戏
|
||||
}, []);
|
||||
|
||||
// 处理游戏点击
|
||||
const handleGamePress = useCallback((game: Game) => {
|
||||
Alert.alert('游戏', `点击了: ${game.play_up_name}`);
|
||||
// 这里可以添加打开游戏的逻辑
|
||||
}, []);
|
||||
|
||||
// 处理底部 Tab 点击
|
||||
const handleTabPress = useCallback((tabId: string, action: string) => {
|
||||
Alert.alert('导航', `点击了: ${tabId}`);
|
||||
// 这里可以添加导航逻辑
|
||||
}, []);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = useCallback((keyword: string) => {
|
||||
Alert.alert('搜索', `搜索关键词: ${keyword}`);
|
||||
// 这里可以添加搜索逻辑
|
||||
}, []);
|
||||
|
||||
// 根据主题选择要显示的组件
|
||||
const renderContent = () => {
|
||||
if (isDarkTheme || actualTheme === 'dark') {
|
||||
// 深色主题布局
|
||||
return (
|
||||
<>
|
||||
<GameMainMenus
|
||||
theme={actualTheme}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
<BannerSwiper theme={actualTheme} />
|
||||
<NoticeBar theme={actualTheme} />
|
||||
<HighPrizeGame theme={actualTheme} onGamePress={handleGamePress} />
|
||||
<Lobby theme={actualTheme} onGamePress={handleGamePress} />
|
||||
<FastFootNav theme={actualTheme} onTabPress={handleTabPress} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// 浅色主题布局
|
||||
return (
|
||||
<>
|
||||
<BannerSwiper theme={actualTheme} />
|
||||
<NoticeBar theme={actualTheme} />
|
||||
<GameMainMenus
|
||||
theme={actualTheme}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
<Lobby theme={actualTheme} onGamePress={handleGamePress} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={s.container}>
|
||||
{/* Header */}
|
||||
<Header
|
||||
theme={actualTheme}
|
||||
onSearch={handleSearch}
|
||||
onMessagePress={() => Alert.alert('消息', '消息功能')}
|
||||
onUserPress={() => Alert.alert('用户', '用户中心')}
|
||||
unreadCount={3}
|
||||
/>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<View style={s.contentContainer}>
|
||||
<ScrollView
|
||||
style={s.contentContainer}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={Colors[actualTheme].primary}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={s.scrollContent}>{renderContent()}</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
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,
|
||||
},
|
||||
}));
|
||||
126
pages/HomeScreen/components/BottomTabs.tsx
Normal file
126
pages/HomeScreen/components/BottomTabs.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 首页底部 Tabs 导航组件
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.card,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 8,
|
||||
paddingTop: 8,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
tabIcon: {
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 12,
|
||||
color: colors.text + '80',
|
||||
fontWeight: '500',
|
||||
},
|
||||
tabLabelActive: {
|
||||
color: colors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
}));
|
||||
|
||||
interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface BottomTabsProps {
|
||||
theme?: 'light' | 'dark';
|
||||
activeTab?: string;
|
||||
onTabPress?: (tabId: string, action: string) => void;
|
||||
items?: TabItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认 Tab 项
|
||||
*/
|
||||
const DEFAULT_TABS: TabItem[] = [
|
||||
{ id: 'recharge', label: '充值', icon: '💰', action: 'recharge' },
|
||||
{ id: 'withdraw', label: '提现', icon: '💳', action: 'withdraw' },
|
||||
{ id: 'activity', label: '活动', icon: '🎉', action: 'activity' },
|
||||
{ id: 'service', label: '客服', icon: '🎧', action: 'service' },
|
||||
{ id: 'help', label: '帮助', icon: '❓', action: 'help' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 底部 Tabs 导航组件
|
||||
*/
|
||||
export default function BottomTabs({
|
||||
theme = 'light',
|
||||
activeTab = 'recharge',
|
||||
onTabPress,
|
||||
items = DEFAULT_TABS,
|
||||
}: BottomTabsProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
const colors = Colors[actualTheme];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(activeTab);
|
||||
|
||||
const handleTabPress = useCallback(
|
||||
(tabId: string, action: string) => {
|
||||
setSelectedTab(tabId);
|
||||
onTabPress?.(tabId, action);
|
||||
},
|
||||
[onTabPress]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
{items.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={s.tabItem}
|
||||
onPress={() => handleTabPress(item.id, item.action)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.tabIcon}>{item.icon}</Text>
|
||||
<Text
|
||||
style={[
|
||||
s.tabLabel,
|
||||
selectedTab === item.id && s.tabLabelActive,
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
143
pages/HomeScreen/components/FastFootNav.tsx
Normal file
143
pages/HomeScreen/components/FastFootNav.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 快速底部导航组件
|
||||
*
|
||||
* 深色主题特有,提供快速导航,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockNavItems } from '@/services/mockHomeService';
|
||||
import type { NavItem } from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
scrollView: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
navItem: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
marginRight: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.background,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 75,
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
navItemActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
navIcon: {
|
||||
fontSize: 22,
|
||||
marginBottom: 4,
|
||||
},
|
||||
navText: {
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
navTextActive: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
}));
|
||||
|
||||
interface FastFootNavProps {
|
||||
theme: 'light' | 'dark';
|
||||
items?: NavItem[];
|
||||
onTabPress?: (tabId: string, action: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速底部导航组件
|
||||
*/
|
||||
export default function FastFootNav({ theme, items: propItems, onTabPress }: FastFootNavProps) {
|
||||
const s = styles[theme];
|
||||
const [items, setItems] = useState<NavItem[]>(propItems || []);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 加载导航项数据
|
||||
useEffect(() => {
|
||||
if (propItems && propItems.length > 0) {
|
||||
setItems(propItems);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadItems = async () => {
|
||||
try {
|
||||
// const data = await getNavItems();
|
||||
// setItems(data.length > 0 ? data : mockNavItems);
|
||||
} catch (error) {
|
||||
console.error('加载导航项失败:', error);
|
||||
setItems(mockNavItems);
|
||||
}
|
||||
};
|
||||
loadItems();
|
||||
}, [propItems]);
|
||||
|
||||
const handleNavPress = (item: NavItem) => {
|
||||
setSelectedId(item.id);
|
||||
onTabPress?.(item.id, item.action);
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
s.navItem,
|
||||
selectedId === item.id && s.navItemActive,
|
||||
]}
|
||||
onPress={() => handleNavPress(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.navIcon}>{item.icon || '🎮'}</Text>
|
||||
<Text
|
||||
style={[
|
||||
s.navText,
|
||||
selectedId === item.id && s.navTextActive,
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
112
pages/HomeScreen/components/GameMainMenus/index.tsx
Normal file
112
pages/HomeScreen/components/GameMainMenus/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 游戏分类菜单组件
|
||||
*
|
||||
* 展示游戏分类,支持切换,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Animated } from 'react-native';
|
||||
// import type { GameCategory } from '@/types/home';
|
||||
import { styles } from './styles';
|
||||
import { useGameMainMenus, useMenuDataLoaded } from '@/hooks/useGameMenus';
|
||||
// import useGameStore from '@/stores/gameStore';
|
||||
import { ThemeEnum } from '@/constants/theme';
|
||||
|
||||
|
||||
|
||||
interface GameMainMenuProps {
|
||||
theme: ThemeEnum;
|
||||
selectedCategory?: string;
|
||||
onCategorySelect?: (categoryId: string) => void;
|
||||
topHeight?: number;
|
||||
showSubMenus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏分类菜单组件
|
||||
*/
|
||||
export default function GameMainMenu({
|
||||
theme,
|
||||
selectedCategory = '103',
|
||||
onCategorySelect,
|
||||
topHeight = 0,
|
||||
showSubMenus = true,
|
||||
}: GameMainMenuProps) {
|
||||
const s = styles[theme];
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const gameMenus = useGameMainMenus(theme);
|
||||
|
||||
// 检查数据加载完成
|
||||
const isDataLoaded = useMenuDataLoaded();
|
||||
|
||||
// 使用 useMemo 缓存找到的索引,避免每次都重新计算
|
||||
const selectedIndex = useMemo(() => {
|
||||
return gameMenus.findIndex((cat) => cat.key === selectedCategory);
|
||||
}, [selectedCategory, gameMenus]);
|
||||
|
||||
// 当分类改变时,滚动到该分类
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0) {
|
||||
scrollViewRef.current?.scrollTo({
|
||||
x: selectedIndex * 100,
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 使用 useCallback 稳定 onPress 回调
|
||||
const handleCategoryPress = useCallback((categoryKey: string) => {
|
||||
onCategorySelect?.(categoryKey);
|
||||
}, [onCategorySelect]);
|
||||
|
||||
// 骨架屏 - 显示加载中的占位符
|
||||
const renderSkeleton = () => (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<View
|
||||
key={`skeleton-${index}`}
|
||||
style={[s.categoryItem, { backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0' }]}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 如果动态数据还未加载,显示骨架屏
|
||||
if (!isDataLoaded) {
|
||||
return renderSkeleton();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{gameMenus.map((menu) => (
|
||||
<TouchableOpacity
|
||||
key={menu.key}
|
||||
style={[s.menuItem, selectedCategory === menu.key && s.menuItemActive]}
|
||||
onPress={() => handleCategoryPress(menu.key)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[s.menuText, selectedCategory === menu.key && s.menuTextActive]}
|
||||
>
|
||||
{menu.icon || '🎮'} {menu.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
47
pages/HomeScreen/components/GameMainMenus/styles.ts
Normal file
47
pages/HomeScreen/components/GameMainMenus/styles.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
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: {
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
scrollView: {
|
||||
paddingHorizontal: 12,
|
||||
},
|
||||
menuItem: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
marginRight: 8,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
elevation: 1,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
menuItemActive: {
|
||||
backgroundColor: colors.primary,
|
||||
elevation: 2,
|
||||
shadowOpacity: 0.15,
|
||||
},
|
||||
menuText: {
|
||||
fontSize: 13,
|
||||
color: colors.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
menuTextActive: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
}));
|
||||
180
pages/HomeScreen/components/Header.tsx
Normal file
180
pages/HomeScreen/components/Header.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 首页 Header 组件
|
||||
* 包含搜索、用户信息、消息等功能
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
Image,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
logo: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.primary,
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
height: 36,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
color: colors.text + '80',
|
||||
},
|
||||
iconButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 8,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
badgeText: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
interface HeaderProps {
|
||||
theme?: 'light' | 'dark';
|
||||
onSearch?: (keyword: string) => void;
|
||||
onMessagePress?: () => void;
|
||||
onUserPress?: () => void;
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header 组件
|
||||
*/
|
||||
export default function Header({
|
||||
theme = 'light',
|
||||
onSearch,
|
||||
onMessagePress,
|
||||
onUserPress,
|
||||
unreadCount = 0,
|
||||
}: HeaderProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
const colors = Colors[actualTheme];
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (searchText.trim()) {
|
||||
onSearch?.(searchText);
|
||||
}
|
||||
}, [searchText, onSearch]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchText('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
{/* 顶部栏 */}
|
||||
<View style={s.header}>
|
||||
{/* Logo */}
|
||||
<Text style={s.logo}>🎮 游戏大厅</Text>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<View style={s.searchContainer}>
|
||||
<Text style={{ color: colors.text + '60', fontSize: 16 }}>🔍</Text>
|
||||
<TextInput
|
||||
style={[s.searchInput, s.searchPlaceholder]}
|
||||
placeholder="搜索游戏..."
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onSubmitEditing={handleSearch}
|
||||
returnKeyType="search"
|
||||
/>
|
||||
{searchText ? (
|
||||
<TouchableOpacity onPress={handleClearSearch}>
|
||||
<Text style={{ fontSize: 16 }}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 消息按钮 */}
|
||||
<TouchableOpacity
|
||||
style={s.iconButton}
|
||||
onPress={onMessagePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>💬</Text>
|
||||
{unreadCount > 0 && (
|
||||
<View style={s.badge}>
|
||||
<Text style={s.badgeText}>
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 用户按钮 */}
|
||||
<TouchableOpacity
|
||||
style={s.iconButton}
|
||||
onPress={onUserPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>👤</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
164
pages/HomeScreen/components/HighPrizeGame.tsx
Normal file
164
pages/HomeScreen/components/HighPrizeGame.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 高奖金游戏组件
|
||||
*
|
||||
* 深色主题特有,展示高奖金游戏,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Animated,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockHighPrizeGames } from '@/services/mockHomeService';
|
||||
import type { HighPrizeGame as HighPrizeGameType } from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
color: colors.text,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollView: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
gameItem: {
|
||||
width: 110,
|
||||
height: 110,
|
||||
marginRight: 10,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.card,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
elevation: 3,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
gameIcon: {
|
||||
fontSize: 44,
|
||||
marginBottom: 4,
|
||||
},
|
||||
gameName: {
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
prizeTag: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: colors.error,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
prizeText: {
|
||||
fontSize: 10,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}));
|
||||
|
||||
interface HighPrizeGameProps {
|
||||
theme: 'light' | 'dark';
|
||||
games?: HighPrizeGameType[];
|
||||
onGamePress?: (game: HighPrizeGameType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高奖金游戏组件
|
||||
*/
|
||||
export default function HighPrizeGame({ theme, games: propGames, onGamePress }: HighPrizeGameProps) {
|
||||
const s = styles[theme];
|
||||
const [games, setGames] = useState<HighPrizeGameType[]>(propGames || []);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 加载高奖金游戏数据
|
||||
useEffect(() => {
|
||||
if (propGames && propGames.length > 0) {
|
||||
setGames(propGames);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadGames = async () => {
|
||||
try {
|
||||
// const data = await getHighPrizeGames();
|
||||
// setGames(data.length > 0 ? data : mockHighPrizeGames);
|
||||
} catch (error) {
|
||||
console.error('加载高奖金游戏失败:', error);
|
||||
setGames(mockHighPrizeGames);
|
||||
}
|
||||
};
|
||||
loadGames();
|
||||
}, [propGames]);
|
||||
|
||||
if (games.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<Text style={s.title}>🏆 实时爆奖</Text>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{games.map((game) => (
|
||||
<TouchableOpacity
|
||||
key={game.id}
|
||||
style={s.gameItem}
|
||||
onPress={() => {
|
||||
setSelectedId(game.id);
|
||||
onGamePress?.(game);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{game.icon ? (
|
||||
<Image
|
||||
source={{ uri: game.icon }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<Text style={s.gameIcon}>🎰</Text>
|
||||
)}
|
||||
<Text style={s.gameName} numberOfLines={2}>
|
||||
{game.play_up_name}
|
||||
</Text>
|
||||
<View style={s.prizeTag}>
|
||||
<Text style={s.prizeText}>¥{Math.floor(game.payout_amount / 1000)}k</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
209
pages/HomeScreen/components/Lobby.tsx
Normal file
209
pages/HomeScreen/components/Lobby.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 游戏大厅组件
|
||||
*
|
||||
* 展示游戏列表,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { getMockGamesByCategory } from '@/services/mockHomeService';
|
||||
import type { Game } from '@/types/home';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
gameGrid: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
gameCard: {
|
||||
flex: 1,
|
||||
margin: 6,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.card,
|
||||
elevation: 3,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
gameCardPressed: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
gameImage: {
|
||||
width: '100%',
|
||||
height: 140,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
gameIcon: {
|
||||
fontSize: 48,
|
||||
},
|
||||
gameInfo: {
|
||||
padding: 10,
|
||||
},
|
||||
gameName: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 6,
|
||||
},
|
||||
gameButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 6,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
},
|
||||
gameButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 40,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 12,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
interface LobbyProps {
|
||||
theme: 'light' | 'dark';
|
||||
games?: Game[];
|
||||
selectedCategory?: number;
|
||||
onGamePress?: (game: Game) => void;
|
||||
topHeight?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏大厅组件
|
||||
*/
|
||||
export default function Lobby({
|
||||
theme,
|
||||
games: propGames,
|
||||
selectedCategory = 0,
|
||||
onGamePress,
|
||||
topHeight = 0,
|
||||
}: LobbyProps) {
|
||||
const s = styles[theme];
|
||||
const [games, setGames] = useState<Game[]>(propGames || []);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 加载游戏数据
|
||||
useEffect(() => {
|
||||
if (propGames && propGames.length > 0) {
|
||||
setGames(propGames);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadGames = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// const response = await getGames(selectedCategory);
|
||||
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(selectedCategory));
|
||||
} catch (error) {
|
||||
console.error('加载游戏失败:', error);
|
||||
setGames(getMockGamesByCategory(selectedCategory));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadGames();
|
||||
}, [propGames, selectedCategory]);
|
||||
|
||||
const renderGameCard = ({ item }: { item: Game }) => (
|
||||
<TouchableOpacity
|
||||
style={s.gameCard}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => onGamePress?.(item)}
|
||||
>
|
||||
<View style={s.gameImage}>
|
||||
{item.icon ? (
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<Text style={s.gameIcon}>🎮</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={s.gameInfo}>
|
||||
<Text style={s.gameName} numberOfLines={2}>
|
||||
{item.play_up_name}
|
||||
{item.play_cname ? ` - ${item.play_cname}` : ''}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={s.gameButton}
|
||||
onPress={() => onGamePress?.(item)}
|
||||
>
|
||||
<Text style={s.gameButtonText}>进入游戏</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={s.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={Colors[theme].primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (games.length === 0) {
|
||||
return (
|
||||
<View style={s.emptyContainer}>
|
||||
<Text style={{ fontSize: 40 }}>🎮</Text>
|
||||
<Text style={s.emptyText}>暂无游戏</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<FlatList
|
||||
data={games}
|
||||
renderItem={renderGameCard}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={2}
|
||||
scrollEnabled={false}
|
||||
contentContainerStyle={s.gameGrid}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
171
pages/HomeScreen/components/NoticeBar.tsx
Normal file
171
pages/HomeScreen/components/NoticeBar.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 公告栏组件
|
||||
*
|
||||
* 展示滚动公告,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockNotices } from '@/services/mockHomeService';
|
||||
import type { Notice } from '@/types/home';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginHorizontal: 12,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginRight: 8,
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
closeButton: {
|
||||
marginLeft: 8,
|
||||
padding: 4,
|
||||
},
|
||||
closeText: {
|
||||
fontSize: 16,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
}));
|
||||
|
||||
interface NoticeBarProps {
|
||||
theme: 'light' | 'dark';
|
||||
notices?: Notice[];
|
||||
onNoticePress?: (notice: Notice) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公告栏组件
|
||||
*/
|
||||
export default function NoticeBar({ theme, notices: propNotices, onNoticePress }: NoticeBarProps) {
|
||||
const s = styles[theme];
|
||||
const [notices, setNotices] = useState<Notice[]>(propNotices || []);
|
||||
const [currentNotice, setCurrentNotice] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const animatedValue = useRef(new Animated.Value(1)).current;
|
||||
|
||||
// 加载公告数据
|
||||
useEffect(() => {
|
||||
if (propNotices && propNotices.length > 0) {
|
||||
setNotices(propNotices);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadNotices = async () => {
|
||||
try {
|
||||
// const data = await getNotices();
|
||||
// setNotices(data.length > 0 ? data : mockNotices);
|
||||
} catch (error) {
|
||||
console.error('加载公告失败:', error);
|
||||
setNotices(mockNotices);
|
||||
}
|
||||
};
|
||||
loadNotices();
|
||||
}, [propNotices]);
|
||||
|
||||
// 自动切换公告
|
||||
useEffect(() => {
|
||||
if (notices.length === 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCurrentNotice((prev) => (prev + 1) % notices.length);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [notices.length]);
|
||||
|
||||
// 处理关闭公告
|
||||
const handleClose = () => {
|
||||
Animated.timing(animatedValue, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setVisible(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 处理公告点击
|
||||
const handleNoticePress = () => {
|
||||
if (notices.length > 0) {
|
||||
onNoticePress?.(notices[currentNotice]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!visible || notices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentNoticeData = notices[currentNotice];
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
s.container,
|
||||
{
|
||||
opacity: animatedValue,
|
||||
transform: [
|
||||
{
|
||||
scaleY: animatedValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={s.label}>📢</Text>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={handleNoticePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.content} numberOfLines={1}>
|
||||
{currentNoticeData.title || currentNoticeData.content}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={s.closeButton}
|
||||
onPress={handleClose}
|
||||
>
|
||||
<Text style={s.closeText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
26
pages/HomeScreen/components/index.ts
Normal file
26
pages/HomeScreen/components/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 首页组件统一导出
|
||||
*
|
||||
* 所有组件都使用 React.memo 进行性能优化
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import BannerSwiperComponent from './BannerSwiper';
|
||||
import NoticeBarComponent from './NoticeBar';
|
||||
import GameCategoryMenuComponent from './GameCategoryMenu';
|
||||
import LobbyComponent from './Lobby';
|
||||
import HighPrizeGameComponent from './HighPrizeGame';
|
||||
import FastFootNavComponent from './FastFootNav';
|
||||
import HeaderComponent from './Header';
|
||||
import BottomTabsComponent from './BottomTabs';
|
||||
|
||||
// 使用 React.memo 优化组件性能,避免不必要的重新渲染
|
||||
export const BannerSwiper = React.memo(BannerSwiperComponent);
|
||||
export const NoticeBar = React.memo(NoticeBarComponent);
|
||||
export const GameCategoryMenu = React.memo(GameCategoryMenuComponent);
|
||||
export const Lobby = React.memo(LobbyComponent);
|
||||
export const HighPrizeGame = React.memo(HighPrizeGameComponent);
|
||||
export const FastFootNav = React.memo(FastFootNavComponent);
|
||||
export const Header = React.memo(HeaderComponent);
|
||||
export const BottomTabs = React.memo(BottomTabsComponent);
|
||||
|
||||
59
pages/HomeScreen/index.tsx
Normal file
59
pages/HomeScreen/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 首页主容器组件
|
||||
*
|
||||
* 支持浅色/深色主题,包含完整的首页功能:
|
||||
* - Header(搜索、用户信息)
|
||||
* - 轮播图
|
||||
* - 游戏分类菜单
|
||||
* - 游戏大厅
|
||||
* - 公告栏
|
||||
* - 高奖金游戏(深色主题)
|
||||
* - 快速导航(深色主题)
|
||||
* - BottomTabs(底部导航)
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { ScrollView, StyleSheet, RefreshControl } from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import HomeScreenComplete from './HomeScreenComplete';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* 首页主容器组件
|
||||
*/
|
||||
export default function HomeScreen() {
|
||||
const theme = useColorScheme();
|
||||
const s = styles[theme];
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
// 下拉刷新处理
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
// 模拟刷新延迟
|
||||
setTimeout(() => {
|
||||
setRefreshing(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HomeScreenComplete
|
||||
theme={theme}
|
||||
isDarkTheme={theme === 'dark'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user