feat: 首页更新

This commit is contained in:
2025-11-11 18:48:54 +08:00
parent 230191f181
commit b48cce06f4
43 changed files with 3186 additions and 1029 deletions

View 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>
);
}

View 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>
);
}

View 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,
},
}));

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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',
},
}));

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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);

View 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'}
/>
);
}