feat: 首页更新

This commit is contained in:
2025-11-13 16:47:10 +08:00
parent 9ef9233797
commit 54bf84b19b
1244 changed files with 3507 additions and 951 deletions

View File

@@ -21,7 +21,6 @@ import { useColorScheme } from '@/hooks';
import { styles } from './styles';
import useMsgStore from '@/stores/msgStore';
interface BannerSwiperProps {}
const { width } = Dimensions.get('window');
@@ -38,7 +37,6 @@ export default function BannerSwiper({}: BannerSwiperProps) {
const autoPlayTimerRef = useRef<any>(null);
const { homeBanner } = useMsgStore();
// 加载轮播图数据
useEffect(() => {
// 如果有传入的 banners 数据,直接使用
@@ -127,11 +125,7 @@ export default function BannerSwiper({}: BannerSwiperProps) {
onPress={() => onBannerPress(banner)}
activeOpacity={0.9}
>
<Image
source={{ uri: banner.subject }}
style={s.image}
resizeMode="cover"
/>
<Image source={{ uri: banner.subject }} style={s.image} resizeMode="cover" />
</TouchableOpacity>
))}
</ScrollView>
@@ -139,16 +133,9 @@ export default function BannerSwiper({}: BannerSwiperProps) {
{/* 指示器 */}
<View style={s.indicatorContainer}>
{homeBanner.map((_, index) => (
<View
key={index}
style={[
s.indicator,
index === currentIndex && s.indicatorActive,
]}
/>
<View key={index} style={[s.indicator, index === currentIndex && s.indicatorActive]} />
))}
</View>
</View>
);
}

View File

@@ -3,13 +3,7 @@
*/
import React, { useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import { useColorScheme } from '@/hooks';
import { createThemeStyles } from '@/theme';
import { Colors } from '@/theme';
@@ -110,12 +104,7 @@ export default function BottomTabs({
activeOpacity={0.7}
>
<Text style={s.tabIcon}>{item.icon}</Text>
<Text
style={[
s.tabLabel,
selectedTab === item.id && s.tabLabelActive,
]}
>
<Text style={[s.tabLabel, selectedTab === item.id && s.tabLabelActive]}>
{item.label}
</Text>
</TouchableOpacity>
@@ -123,4 +112,3 @@ export default function BottomTabs({
</View>
);
}

View File

@@ -5,14 +5,7 @@
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Animated,
} from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Animated } from 'react-native';
import { createThemeStyles } from '@/theme';
import { useColorScheme } from '@/hooks';
import { mockNavItems } from '@/services/mockHomeService';
@@ -118,26 +111,15 @@ export default function FastFootNav({ items: propItems, onTabPress }: FastFootNa
{items.map((item) => (
<TouchableOpacity
key={item.id}
style={[
s.navItem,
selectedId === item.id && s.navItemActive,
]}
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>
<Text style={[s.navText, selectedId === item.id && s.navTextActive]}>{item.name}</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
}

View File

@@ -8,7 +8,7 @@ import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'
import { View, Text, ScrollView, TouchableOpacity, Animated, Image, Platform } from 'react-native';
// import type { GameCategory } from '@/types/home';
import { styles } from './styles';
import { useGameMainMenus, useMenuDataLoaded, useSelectedCategory } from '@/hooks/useGameMenus';
import { useGameMainMenus, useMenuDataLoaded, useGameMenuTabs } from '@/hooks/useGameMenus';
// import useGameStore from '@/stores/gameStore';
// import { ThemeEnum } from '@/constants/theme';
import { Colors, useColorScheme } from '@/theme';
@@ -21,18 +21,18 @@ if (Platform.OS !== 'web') {
// 游戏菜单图片映射 - 使用 require 加载本地资源
const MENU_ICON_MAP: Record<string, any> = {
'recommend': require('../../../../assets/images/game/menu/recommend.png'),
'chess': require('../../../../assets/images/game/menu/chess.png'),
'electronic': require('../../../../assets/images/game/menu/electronic.png'),
'fishing': require('../../../../assets/images/game/menu/fishing.png'),
'lottery': require('../../../../assets/images/game/menu/lottery.png'),
'sports': require('../../../../assets/images/game/menu/sports.png'),
'trial': require('../../../../assets/images/game/menu/trial.png'),
'blockThird': require('../../../../assets/images/game/menu/blockThird.png'),
recommend: require('../../../../assets/images/game/menu/recommend.png'),
chess: require('../../../../assets/images/game/menu/chess.png'),
electronic: require('../../../../assets/images/game/menu/electronic.png'),
fishing: require('../../../../assets/images/game/menu/fishing.png'),
lottery: require('../../../../assets/images/game/menu/lottery.png'),
sports: require('../../../../assets/images/game/menu/sports.png'),
trial: require('../../../../assets/images/game/menu/trial.png'),
blockThird: require('../../../../assets/images/game/menu/blockThird.png'),
'clock-solid': require('../../../../assets/images/game/menu/clock-solid.png'),
'clock-solid_dark': require('../../../../assets/images/game/menu/clock-solid_dark.png'),
'home-star-solid': require('../../../../assets/images/game/menu/home-star-solid.png'),
'live': require('../../../../assets/images/game/menu/live.png'),
live: require('../../../../assets/images/game/menu/live.png'),
};
interface GameMainMenuProps {
@@ -43,25 +43,22 @@ interface GameMainMenuProps {
/**
* 游戏分类菜单组件
*/
export default function GameMainMenu({
topHeight = 0,
showSubMenus = true,
}: GameMainMenuProps) {
export default function GameMainMenu({ topHeight = 0, showSubMenus = true }: GameMainMenuProps) {
const theme = useColorScheme();
const s = styles[theme];
const scrollViewRef = useRef<ScrollView>(null);
const gameMenus = useGameMainMenus(theme);
// 从 hook 获取选中分类和更新方法
const { selectedCategory, setSelectedCategory } = useSelectedCategory();
const { activeMainMenuTab, setActiveMainMenuTab } = useGameMenuTabs();
// 检查数据加载完成
const isDataLoaded = useMenuDataLoaded();
// 使用 useMemo 缓存找到的索引,避免每次都重新计算
const selectedIndex = useMemo(() => {
return gameMenus.findIndex((cat) => cat.key === selectedCategory);
}, [selectedCategory, gameMenus]);
return gameMenus.findIndex((cat) => cat.key === activeMainMenuTab);
}, [activeMainMenuTab, gameMenus]);
// 当分类改变时,滚动到该分类
useEffect(() => {
@@ -76,9 +73,9 @@ export default function GameMainMenu({
// 使用 useCallback 稳定 onPress 回调
const handleCategoryPress = useCallback(
(categoryKey: string) => {
setSelectedCategory(categoryKey);
setActiveMainMenuTab(categoryKey);
},
[setSelectedCategory]
[setActiveMainMenuTab]
);
// 骨架屏 - 显示加载中的占位符
@@ -126,21 +123,19 @@ export default function GameMainMenu({
imageSource = MENU_ICON_MAP[menu.icon];
}
const isActive = selectedCategory === menu.key;
const isActive = activeMainMenuTab === menu.key;
const themeColors = Colors[theme];
// 获取渐变色 - 从主题色到透明
const gradientStart = `${themeColors.tint}40`; // 主题色 + 40% 透明度
const gradientEnd = `${themeColors.tint}00`; // 完全透明
const gradientEnd = `${themeColors.tint}00`; // 完全透明
const menuContent = (
<>
{imageSource && (
<Image source={imageSource} style={s.menuIcon} resizeMode="contain" />
)}
<Text style={[s.menuText, isActive && s.menuTextActive]}>
{menu.name}
</Text>
<Text style={[s.menuText, isActive && s.menuTextActive]}>{menu.name}</Text>
</>
);

View File

@@ -12,7 +12,7 @@ export const styles = createThemeStyles((colors) => ({
backgroundColor: colors.background,
paddingTop: 5,
borderBottomWidth: 1,
borderBottomColor: colors.border,
borderBottomColor: colors.borderSecondary,
},
scrollView: {
paddingHorizontal: 12,
@@ -55,16 +55,20 @@ export const styles = createThemeStyles((colors) => ({
},
}));
export const themeStyles = createResponsiveThemeStyles({
menuItemActive: {
backgroundColor: '',
export const themeStyles = createResponsiveThemeStyles(
{
menuItemActive: {
backgroundColor: '',
},
},
}, {
menuItemActive: {
backgroundColor: '',
{
menuItemActive: {
backgroundColor: '',
},
},
}, {
menuItemActive: {
backgroundColor: '',
},
});
{
menuItemActive: {
backgroundColor: '',
},
}
);

View File

@@ -0,0 +1,231 @@
/**
* 游戏子菜单组件
* 基于 xinyong-web 的 SubGameCategoryMenu 组件重建
* 功能包括:
* - 水平/竖直菜单切换
* - 菜单项滚动和自动定位
* - 弹窗选择菜单
* - 主题适配
*/
import React, { useState, useCallback, useMemo, useRef, useEffect } from 'react';
import {
View,
Text,
ScrollView,
TouchableOpacity,
Modal,
FlatList,
Dimensions,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useColorScheme, useThemeColors } from '@/theme';
import { styles } from './styles';
import { useGameMainMenus, useGameMenuTabs } from '@/hooks/useGameMenus';
import { Image } from '@/components';
const { width } = Dimensions.get('window');
interface GameSubMenusProps {
vertical?: boolean;
onSubMenuChange?: (menuKey: string) => void;
}
/**
* 游戏子菜单组件
*/
export default function GameSubMenus({ vertical = false, onSubMenuChange }: GameSubMenusProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
const themeColors = useThemeColors();
// 获取主菜单数据
const gameMenus = useGameMainMenus(colorScheme);
const { activeMainMenuTab, activeSubMenuTab, setActiveSubMenuTab } = useGameMenuTabs();
// 获取当前选中的主菜单
const currentMenu = useMemo(() => {
return gameMenus.find((menu) => menu.key === activeMainMenuTab);
}, [gameMenus, activeMainMenuTab]);
// 获取当前选中的子菜单
const subMenus = useMemo((): Record<string, any>[] => {
return currentMenu?.children || [];
}, [currentMenu]);
// 弹窗状态
const [showPopup, setShowPopup] = useState(false);
// 滚动视图引用
const scrollViewRef = useRef<ScrollView>(null);
// 当前选中的子菜单索引
const selectedSubMenuIndex = useMemo(() => {
return subMenus.findIndex((menu) => menu.key === activeSubMenuTab);
}, [subMenus, activeSubMenuTab]);
// 处理子菜单选择
const handleSubMenuPress = useCallback(
(menuKey: string) => {
setActiveSubMenuTab(menuKey);
onSubMenuChange?.(menuKey);
},
[setActiveSubMenuTab, onSubMenuChange]
);
// 处理打开弹窗
const handleOpenPopup = useCallback(() => {
setShowPopup(true);
}, []);
// 处理关闭弹窗
const handleClosePopup = useCallback(() => {
setShowPopup(false);
}, []);
// 处理弹窗中的菜单选择
const handlePopupMenuSelect = useCallback(
(menuKey: string) => {
handleSubMenuPress(menuKey);
handleClosePopup();
},
[handleSubMenuPress, handleClosePopup]
);
// 自动滚动到选中项
useEffect(() => {
if (selectedSubMenuIndex >= 0 && !vertical) {
const scrollPosition = selectedSubMenuIndex * 112; // 100 (width) + 12 (margin)
scrollViewRef.current?.scrollTo({
x: scrollPosition,
animated: true,
});
}
}, [selectedSubMenuIndex, vertical]);
// 如果没有子菜单,返回空
if (subMenus.length === 0) {
return null;
}
// 渲染菜单项
const renderMenuItem = (item: any, isActive: boolean) => (
<TouchableOpacity
style={[s.menuItem, vertical && s.menuItemVertical, isActive && s.menuItemActive]}
onPress={() => handleSubMenuPress(item.key)}
activeOpacity={0.7}
>
{item.colorImgSrc && (
<Image
source={item.colorImgSrc}
style={[s.menuIcon]}
adaptiveMode="height"
autoMeasure={true}
/>
)}
<Text style={[s.menuText, isActive && s.menuTextActive]} numberOfLines={2}>
{item.name}
</Text>
</TouchableOpacity>
);
// 水平布局
if (!vertical) {
return (
<View style={s.horizontalContainer}>
<ScrollView
ref={scrollViewRef}
horizontal
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
style={s.horizontalScrollView}
>
{subMenus.map((item) => (
<View key={item.key}>{renderMenuItem(item, item.key === activeSubMenuTab)}</View>
))}
{/* 更多菜单按钮 */}
<TouchableOpacity
style={[s.menuItem, { marginLeft: 6 }]}
onPress={handleOpenPopup}
activeOpacity={0.7}
>
<Ionicons name="list" size={32} color={themeColors.text + '80'} />
<Text style={s.menuText}></Text>
</TouchableOpacity>
</ScrollView>
</View>
);
}
// 竖直布局
return (
<View style={s.verticalContainer}>
<ScrollView
ref={scrollViewRef}
showsVerticalScrollIndicator={false}
scrollEventThrottle={16}
style={s.verticalScrollView}
>
{subMenus.map((item) => (
<View key={item.key}>{renderMenuItem(item, item.key === activeSubMenuTab)}</View>
))}
</ScrollView>
{/* 弹窗 */}
<Modal
visible={showPopup}
transparent
animationType="slide"
onRequestClose={handleClosePopup}
>
<View style={s.modalOverlay}>
<TouchableOpacity style={{ flex: 1 }} activeOpacity={1} onPress={handleClosePopup} />
<View style={s.modalContent}>
{/* 弹窗头部 */}
<View style={s.modalHeader}>
<Text style={s.modalTitle}></Text>
<TouchableOpacity style={s.modalCloseButton} onPress={handleClosePopup}>
<Ionicons name="close" size={24} color={themeColors.text} />
</TouchableOpacity>
</View>
{/* 弹窗内容 - 网格布局 */}
<FlatList
data={subMenus}
renderItem={({ item }) => (
<TouchableOpacity
style={[s.modalGridItem, item.key === activeSubMenuTab && s.modalGridItemActive]}
onPress={() => handlePopupMenuSelect(item.key)}
activeOpacity={0.7}
>
{item.colorImgSrc && (
<Image
source={item.colorImgSrc}
style={s.modalGridIcon}
resizeMode="contain"
adaptiveMode="height"
autoMeasure={true}
/>
)}
<Text
style={[
s.modalGridText,
item.key === activeSubMenuTab && s.modalGridTextActive,
]}
numberOfLines={2}
>
{item.name}
</Text>
</TouchableOpacity>
)}
keyExtractor={(item) => item.key}
numColumns={2}
scrollEnabled={false}
contentContainerStyle={s.modalGrid}
/>
</View>
</View>
</Modal>
</View>
);
}

View File

@@ -0,0 +1,174 @@
/**
* GameSubMenus 组件样式
* 支持水平和竖直两种布局
*/
import { createThemeStyles } from '@/theme';
export const styles = createThemeStyles((colors) => ({
// ========== 容器样式 ==========
container: {
flex: 1,
backgroundColor: colors.background,
},
// ========== 水平布局样式 ==========
horizontalContainer: {
paddingVertical: 12,
paddingHorizontal: 8,
backgroundColor: colors.background,
},
horizontalScrollView: {
flexGrow: 0,
},
// ========== 竖直布局样式 ==========
verticalContainer: {
paddingHorizontal: 12,
paddingVertical: 12,
backgroundColor: colors.background,
},
verticalScrollView: {
flexGrow: 0,
},
// ========== 菜单项样式 ==========
menuItem: {
minWidth: 90,
height: 33,
marginHorizontal: 6,
paddingHorizontal: 8,
// paddingVertical: 8,
borderRadius: 6,
backgroundColor: '#f6f6f7',
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
},
menuItemActive: {
backgroundColor: colors.primary,
},
menuItemVertical: {
width: 120,
height: 120,
marginBottom: 12,
marginHorizontal: 0,
},
menuIcon: {
height: 16,
width: 48,
// 宽度由 Image 组件根据 autoMeasure 自动计算
resizeMode: 'contain',
},
menuText: {
fontSize: 12,
fontWeight: '500',
color: colors.text + '80',
textAlign: 'center',
// marginTop: 4,
},
menuTextActive: {
color: '#fff',
fontWeight: '600',
},
// ========== 弹窗样式 ==========
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.background,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingTop: 16,
paddingBottom: 24,
maxHeight: '70%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingBottom: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
modalTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.text,
},
modalCloseButton: {
padding: 8,
},
modalGrid: {
paddingHorizontal: 12,
paddingVertical: 12,
},
modalGridItem: {
flex: 1,
margin: 6,
height: 120,
borderRadius: 12,
backgroundColor: colors.card,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
},
modalGridItemActive: {
backgroundColor: colors.primary,
},
modalGridIcon: {
// 只设置高度,宽度由 Image 组件根据 adaptiveMode="height" 自动计算
height: 60,
marginBottom: 8,
// 注意resizeMode 不应该在样式中,应该作为 Image 组件的 prop
},
modalGridText: {
fontSize: 12,
fontWeight: '500',
color: colors.text + '80',
textAlign: 'center',
},
modalGridTextActive: {
color: '#fff',
fontWeight: '600',
},
// ========== 加载状态 ==========
skeleton: {
backgroundColor: colors.card,
borderRadius: 12,
},
skeletonHorizontal: {
width: 100,
height: 96,
marginHorizontal: 6,
},
skeletonVertical: {
width: 120,
height: 120,
marginBottom: 12,
},
}));

View File

@@ -1,177 +0,0 @@
/**
* 首页 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 '@/theme';
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 {
onSearch?: (keyword: string) => void;
onMessagePress?: () => void;
onUserPress?: () => void;
unreadCount?: number;
}
/**
* Header 组件
*/
export default function Header({
onSearch,
onMessagePress,
onUserPress,
unreadCount = 0,
}: HeaderProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
const colors = Colors[colorScheme];
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

@@ -161,4 +161,3 @@ export default function HighPrizeGame({ games: propGames, onGamePress }: HighPri
</View>
);
}

View File

@@ -16,7 +16,7 @@ import {
Dimensions,
} from 'react-native';
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme';
import { useSelectedCategory } from '@/hooks/useGameMenus';
import { useGameMenuTabs } from '@/hooks/useGameMenus';
import { getMockGamesByCategory } from '@/services/mockHomeService';
import type { Game } from '@/types/home';
@@ -108,15 +108,11 @@ interface LobbyProps {
/**
* 游戏大厅组件
*/
export default function Lobby({
games: propGames,
onGamePress,
topHeight = 0,
}: LobbyProps) {
export default function Lobby({ games: propGames, onGamePress, topHeight = 0 }: LobbyProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
const { colors } = useThemeInfo();
const { selectedCategory } = useSelectedCategory();
const { activeMainMenuTab, activeSubMenuTab } = useGameMenuTabs();
const [games, setGames] = useState<Game[]>(propGames || []);
const [loading, setLoading] = useState(true);
@@ -131,24 +127,20 @@ export default function Lobby({
const loadGames = async () => {
try {
setLoading(true);
// const response = await getGames(selectedCategory);
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(selectedCategory));
// const response = await getGames(activeMainMenuTab);
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(activeMainMenuTab));
} catch (error) {
console.error('加载游戏失败:', error);
setGames(getMockGamesByCategory(selectedCategory));
setGames(getMockGamesByCategory(activeMainMenuTab));
} finally {
setLoading(false);
}
};
loadGames();
}, [propGames, selectedCategory]);
}, [propGames, activeMainMenuTab]);
const renderGameCard = ({ item }: { item: Game }) => (
<TouchableOpacity
style={s.gameCard}
activeOpacity={0.7}
onPress={() => onGamePress?.(item)}
>
<TouchableOpacity style={s.gameCard} activeOpacity={0.7} onPress={() => onGamePress?.(item)}>
<View style={s.gameImage}>
{item.icon ? (
<Image
@@ -165,10 +157,7 @@ export default function Lobby({
{item.play_up_name}
{item.play_cname ? ` - ${item.play_cname}` : ''}
</Text>
<TouchableOpacity
style={s.gameButton}
onPress={() => onGamePress?.(item)}
>
<TouchableOpacity style={s.gameButton} onPress={() => onGamePress?.(item)}>
<Text style={s.gameButtonText}></Text>
</TouchableOpacity>
</View>
@@ -205,4 +194,3 @@ export default function Lobby({
</View>
);
}

View File

@@ -1,171 +0,0 @@
/**
* 公告栏组件
*
* 展示滚动公告,使用真实数据
*/
import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
TouchableOpacity,
Dimensions,
} from 'react-native';
import { createThemeStyles } from '@/theme';
import { useColorScheme } from '@/hooks';
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 {
notices?: Notice[];
onNoticePress?: (notice: Notice) => void;
}
/**
* 公告栏组件
*/
export default function NoticeBar({ notices: propNotices, onNoticePress }: NoticeBarProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
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,239 @@
/**
* 公告栏组件 - 增强版
*
* 基于 xinyong-web 的 NoticeBar 组件重建
* 功能包括:
* - 顶部通知栏(自动轮播)
* - 消息和客服按钮
* - 公告详情弹窗(支持文本、图片、链接)
* - 主题适配light/dark/orange
*/
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
Animated,
TouchableOpacity,
Dimensions,
ScrollView,
Modal,
Linking,
Image as RNImage,
ActivityIndicator,
} from 'react-native';
// import { Button } from 'react-native-paper';
import { useThemeColors, useColorScheme } from '@/theme';
// import { mockNotices } from '@/services/mockHomeService';
// import type { Notice } from '@/types/home';
import useMsgStore, { useUnreadMessageTotal } from '@/stores/msgStore';
import { filter, map } from 'lodash-es';
import { styles } from './styles';
import { Ionicons } from '@expo/vector-icons';
interface NoticeBarProps {
onNoticePress?: (notice: Record<string, any>) => void;
onMessagePress?: () => void;
onServicePress?: () => void;
unreadCount?: number;
}
/**
* 公告栏组件 - 增强版
*/
export default function NoticeBar({
onNoticePress,
onMessagePress,
onServicePress,
unreadCount = 0,
}: NoticeBarProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
const themeColors = useThemeColors();
const [currentNotice, setCurrentNotice] = useState(0);
const [visible, setVisible] = useState(true);
const [showDetailModal, setShowDetailModal] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
const animatedValue = useRef(new Animated.Value(1)).current;
const { notices } = useMsgStore();
const unreadMessageTotal = useUnreadMessageTotal();
const noticeList = useMemo(() => {
return map(
filter(notices, (item) => item.message_id == 13 && !!item.content),
(notice) => {
return {
...notice,
content: notice.content.replace(
/<(\/?)(p|span|div|br|strong|em|b|i|u|s|ul|li|ol|h1|h2|h3|h4|h5|h6|a|img|table|tr|td|th|tbody|thead|tfoot|style|script)[^>]*>/gi,
''
),
};
}
);
}, [notices]);
// 自动切换公告
useEffect(() => {
if (noticeList.length === 0) return;
const timer = setInterval(() => {
setCurrentNotice((prev) => (prev + 1) % noticeList.length);
}, 5000);
return () => clearInterval(timer);
}, [noticeList.length]);
// 处理关闭公告
const handleClose = useCallback(() => {
Animated.timing(animatedValue, {
toValue: 0,
duration: 300,
useNativeDriver: true,
}).start(() => {
setVisible(false);
});
}, [animatedValue]);
// 处理公告点击 - 打开详情弹窗
const handleNoticePress = useCallback(() => {
if (noticeList.length > 0) {
setShowDetailModal(true);
onNoticePress?.(noticeList[currentNotice]);
}
}, [noticeList, currentNotice, onNoticePress]);
// 处理链接点击
const handleLinkPress = useCallback((url: string) => {
Linking.openURL(url).catch((err) => {
console.error('打开链接失败:', err);
});
}, []);
if (!visible || noticeList.length === 0) {
return null;
}
const currentNoticeData = noticeList[currentNotice] as Record<string, any>;
return (
<>
{/* 顶部通知栏 */}
<Animated.View
style={[
s.container,
{
opacity: animatedValue,
transform: [
{
scaleY: animatedValue,
},
],
},
]}
>
<Ionicons
name="volume-medium-outline"
size={24}
color={themeColors.tint}
style={s.volumeIcon}
/>
<TouchableOpacity style={{ flex: 1 }} onPress={handleNoticePress} activeOpacity={0.7}>
<Text style={s.content} numberOfLines={1}>
{currentNoticeData.content}
</Text>
</TouchableOpacity>
{/* 右侧按钮组 */}
<View style={s.rightButtonsContainer}>
<TouchableOpacity style={s.actionButton} onPress={onMessagePress} activeOpacity={0.7}>
<Ionicons name="volume-medium-outline" size={18} color={themeColors.tint} />
{/*{unreadCount > 0 && (*/}
{/* <Text style={[s.actionButtonText, { marginLeft: 4 }]}>{unreadCount}</Text>*/}
{/*)}*/}
<Text style={[s.actionButtonText, { marginLeft: 4 }]}></Text>
</TouchableOpacity>
<TouchableOpacity style={s.actionButton} onPress={onServicePress} activeOpacity={0.7}>
<Ionicons name="headset" size={18} color={themeColors.tint} />
<Text style={s.actionButtonText}></Text>
</TouchableOpacity>
{/*<TouchableOpacity style={s.closeButton} onPress={handleClose}>*/}
{/* <Text style={s.closeText}>✕</Text>*/}
{/*</TouchableOpacity>*/}
</View>
</Animated.View>
{/* 公告详情弹窗 */}
<Modal
visible={showDetailModal}
transparent
animationType="fade"
onRequestClose={() => setShowDetailModal(false)}
>
<View style={s.modalOverlay}>
<View style={s.modalContent}>
{/* 弹窗头部 */}
<View style={s.modalHeader}>
<Text style={s.modalTitle}></Text>
<TouchableOpacity
style={s.modalCloseButton}
onPress={() => setShowDetailModal(false)}
>
<Text style={s.modalCloseText}></Text>
</TouchableOpacity>
</View>
{/* 弹窗内容 */}
<ScrollView style={s.modalBody}>
{currentNoticeData.title && (
<Text style={s.noticeTitle}>{currentNoticeData.title}</Text>
)}
{currentNoticeData.create_time && (
<Text style={s.noticeDate}>
{currentNoticeData.formatDate || currentNoticeData.create_time}
</Text>
)}
{/* 根据内容类型渲染不同的内容 */}
{currentNoticeData.content_type === 1 && (
<Text style={s.noticeTextContent}>{currentNoticeData.content}</Text>
)}
{currentNoticeData.content_type === 2 && currentNoticeData.content && (
<View style={s.noticeImageContainer}>
{imageLoading && (
<ActivityIndicator
size="large"
color={themeColors.primary}
style={s.imageLoader}
/>
)}
<RNImage
source={{ uri: currentNoticeData.content }}
style={s.noticeImage}
resizeMode="contain"
onLoadStart={() => {
setImageLoading(true);
}}
onLoadEnd={() => {
setImageLoading(false);
}}
onError={(error) => {
console.error('图片加载失败:', error);
setImageLoading(false);
}}
/>
</View>
)}
{currentNoticeData.content_type === 3 && (
<TouchableOpacity onPress={() => handleLinkPress(currentNoticeData.content)}>
<Text style={s.noticeLink}>{currentNoticeData.content}</Text>
</TouchableOpacity>
)}
</ScrollView>
</View>
</View>
</Modal>
</>
);
}

View File

@@ -0,0 +1,156 @@
import { Dimensions } from 'react-native';
import { createThemeStyles } from '@/theme';
const { width, height } = Dimensions.get('window');
/**
* 创建主题样式
*/
export const styles = createThemeStyles((colors) => ({
// 顶部通知栏
container: {
backgroundColor: colors.background,
paddingVertical: 10,
paddingHorizontal: 12,
marginHorizontal: 0,
marginBottom: 0,
borderRadius: 0,
flexDirection: 'row',
alignItems: 'center',
elevation: 2,
shadowColor: colors.cardShadow,
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 3,
},
volumeIcon: {
marginRight: 8,
},
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,
},
// 右侧按钮组
rightButtonsContainer: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 8,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 6,
// paddingVertical: 6,
marginLeft: 8,
borderRadius: 3,
borderWidth: 1,
borderColor: colors.border,
height: 20,
// backgroundColor: colors.primary,
},
actionButtonText: {
fontSize: 10,
color: colors.text,
fontWeight: '500',
marginLeft: 4,
},
// 弹窗样式
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
},
modalContent: {
backgroundColor: colors.card,
borderRadius: 12,
width: width * 0.9,
maxHeight: height * 0.8,
overflow: 'hidden',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.border,
},
modalTitle: {
fontSize: 16,
fontWeight: 'bold',
color: colors.text,
},
modalCloseButton: {
padding: 8,
},
modalCloseText: {
fontSize: 20,
color: colors.textSecondary,
},
modalBody: {
padding: 16,
},
noticeTitle: {
fontSize: 14,
fontWeight: 'bold',
color: colors.text,
marginBottom: 8,
},
noticeDate: {
fontSize: 12,
color: colors.textSecondary,
marginBottom: 12,
},
noticeTextContent: {
fontSize: 13,
color: colors.text,
lineHeight: 20,
marginBottom: 12,
},
noticeImageContainer: {
width: '100%',
height: 200,
borderRadius: 8,
marginBottom: 12,
backgroundColor: colors.backgroundSecondary,
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
},
noticeImage: {
width: '100%',
height: 200,
borderRadius: 8,
},
imageLoader: {
position: 'absolute',
},
noticeLink: {
fontSize: 13,
color: colors.primary,
textDecorationLine: 'underline',
marginBottom: 12,
},
}));

View File

@@ -8,19 +8,20 @@ import React from 'react';
import BannerSwiperComponent from './BannerSwiper';
import NoticeBarComponent from './NoticeBar';
import GameMainMenusComponent from './GameMainMenus';
import GameSubMenusComponent from './GameSubMenus';
import LobbyComponent from './Lobby';
import HighPrizeGameComponent from './HighPrizeGame';
import FastFootNavComponent from './FastFootNav';
import HeaderComponent from './Header';
import HeaderComponent from '@/components/Header';
import BottomTabsComponent from './BottomTabs';
// 使用 React.memo 优化组件性能,避免不必要的重新渲染
export const BannerSwiper = React.memo(BannerSwiperComponent);
export const NoticeBar = React.memo(NoticeBarComponent);
export const GameMainMenus = React.memo(GameMainMenusComponent);
export const GameSubMenus = React.memo(GameSubMenusComponent);
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

@@ -12,6 +12,7 @@ import {
BannerSwiper,
NoticeBar,
GameMainMenus,
GameSubMenus,
Lobby,
HighPrizeGame,
FastFootNav,
@@ -42,7 +43,6 @@ const styles = createThemeStyles((colors) => ({
},
}));
/**
* 完整首页容器
*/
@@ -99,6 +99,18 @@ export default function HomePage() {
// 这里可以添加搜索逻辑
}, []);
// 处理消息按钮点击
const handleMessagePress = useCallback(() => {
Alert.alert('消息', '进入消息中心');
// 这里可以添加导航到消息页面的逻辑
}, []);
// 处理客服按钮点击
const handleServicePress = useCallback(() => {
Alert.alert('客服', '联系客服');
// 这里可以添加导航到客服页面的逻辑
}, []);
// 根据主题选择要显示的组件
const renderContent = () => {
if (isDarkTheme) {
@@ -107,7 +119,11 @@ export default function HomePage() {
<>
<GameMainMenus />
<BannerSwiper />
<NoticeBar />
<NoticeBar
onMessagePress={handleMessagePress}
onServicePress={handleServicePress}
unreadCount={3}
/>
<HighPrizeGame onGamePress={handleGamePress} />
<Lobby onGamePress={handleGamePress} />
<FastFootNav onTabPress={handleTabPress} />
@@ -118,8 +134,13 @@ export default function HomePage() {
return (
<>
<BannerSwiper />
<NoticeBar />
<NoticeBar
onMessagePress={handleMessagePress}
onServicePress={handleServicePress}
unreadCount={3}
/>
<GameMainMenus />
<GameSubMenus />
<Lobby onGamePress={handleGamePress} />
</>
);
@@ -129,12 +150,7 @@ export default function HomePage() {
return (
<SafeAreaView style={s.container}>
{/* Header */}
<Header
onSearch={handleSearch}
onMessagePress={() => Alert.alert('消息', '消息功能')}
onUserPress={() => Alert.alert('用户', '用户中心')}
unreadCount={3}
/>
<Header />
{/* 内容区域 */}
<View style={s.contentContainer}>