feat: 首页更新
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
231
pages/HomeScreen/components/GameSubMenus/index.tsx
Normal file
231
pages/HomeScreen/components/GameSubMenus/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
174
pages/HomeScreen/components/GameSubMenus/styles.ts
Normal file
174
pages/HomeScreen/components/GameSubMenus/styles.ts
Normal 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,
|
||||
},
|
||||
}));
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,4 +161,3 @@ export default function HighPrizeGame({ games: propGames, onGamePress }: HighPri
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
239
pages/HomeScreen/components/NoticeBar/index.tsx
Normal file
239
pages/HomeScreen/components/NoticeBar/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
156
pages/HomeScreen/components/NoticeBar/styles.ts
Normal file
156
pages/HomeScreen/components/NoticeBar/styles.ts
Normal 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,
|
||||
},
|
||||
}));
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -56,22 +56,16 @@ export default function TestPage() {
|
||||
|
||||
<ThemedView style={styles.section}>
|
||||
<ThemedText type="subtitle">路由说明</ThemedText>
|
||||
<ThemedText style={styles.item}>
|
||||
文件路径: app/test-page.tsx
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.item}>
|
||||
访问路径: /test-page
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.item}>
|
||||
跳转方式: router.push('/test-page')
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.item}>文件路径: app/test-page.tsx</ThemedText>
|
||||
<ThemedText style={styles.item}>访问路径: /test-page</ThemedText>
|
||||
<ThemedText style={styles.item}>跳转方式: router.push('/test-page')</ThemedText>
|
||||
</ThemedView>
|
||||
|
||||
<ThemedView style={styles.infoBox}>
|
||||
<ThemedText type="defaultSemiBold">💡 提示</ThemedText>
|
||||
<ThemedText style={styles.infoText}>
|
||||
对于复杂的业务页面,建议在 screens/ 目录下创建独立的组件,
|
||||
然后在 app/ 目录下的路由文件中引用。
|
||||
对于复杂的业务页面,建议在 screens/ 目录下创建独立的组件, 然后在 app/
|
||||
目录下的路由文件中引用。
|
||||
</ThemedText>
|
||||
</ThemedView>
|
||||
</ThemedView>
|
||||
|
||||
@@ -38,4 +38,3 @@
|
||||
// 导出业务页面组件
|
||||
export { default as TestPage } from './TestPage';
|
||||
export { default as HomeScreen } from './HomeScreen';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user