Compare commits
No commits in common. '9ef9233797b4574f83915e1ceb47b7dde1e6d520' and '230191f181d9da56a4d178553f337c0f34b83ed5' have entirely different histories.
9ef9233797
...
230191f181
@ -1,23 +1,187 @@ |
|||||||
/** |
import { useState, useEffect } from 'react'; |
||||||
* 首页 - 游戏大厅 |
import { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; |
||||||
* |
import * as Updates from 'expo-updates'; |
||||||
* 重构自 xinyong-web 项目的首页 |
|
||||||
* 支持浅色/深色主题,包含轮播图、分类菜单、游戏大厅等功能 |
|
||||||
*/ |
|
||||||
|
|
||||||
import { Stack } from 'expo-router'; |
import { Text, View } from '@/components/Themed'; |
||||||
import HomeScreen from '@/pages/HomeScreen'; |
|
||||||
|
export default function TabOneScreen() { |
||||||
|
const [isChecking, setIsChecking] = useState(false); |
||||||
|
const [updateInfo, setUpdateInfo] = useState<string>(''); |
||||||
|
|
||||||
|
const checkForUpdates = async () => { |
||||||
|
if (__DEV__) { |
||||||
|
Alert.alert('提示', '开发模式下无法检查更新,请使用生产构建测试热更新功能'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setIsChecking(true); |
||||||
|
setUpdateInfo('正在检查更新...'); |
||||||
|
|
||||||
|
try { |
||||||
|
const update = await Updates.checkForUpdateAsync(); |
||||||
|
|
||||||
|
if (update.isAvailable) { |
||||||
|
setUpdateInfo('发现新版本,正在下载...'); |
||||||
|
await Updates.fetchUpdateAsync(); |
||||||
|
|
||||||
|
Alert.alert('更新完成', '新版本已下载完成,是否立即重启应用?', [ |
||||||
|
{ |
||||||
|
text: '稍后', |
||||||
|
style: 'cancel', |
||||||
|
onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), |
||||||
|
}, |
||||||
|
{ |
||||||
|
text: '立即重启', |
||||||
|
onPress: async () => { |
||||||
|
await Updates.reloadAsync(); |
||||||
|
}, |
||||||
|
}, |
||||||
|
]); |
||||||
|
} else { |
||||||
|
setUpdateInfo('当前已是最新版本'); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
setUpdateInfo('检查更新失败: ' + (error as Error).message); |
||||||
|
Alert.alert('错误', '检查更新失败,请稍后重试'); |
||||||
|
} finally { |
||||||
|
setIsChecking(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const getUpdateInfo = () => { |
||||||
|
const { isEmbeddedLaunch, isEmergencyLaunch, updateId, channel, runtimeVersion } = |
||||||
|
Updates.useUpdates(); |
||||||
|
|
||||||
|
return ` |
||||||
|
运行模式: ${__DEV__ ? '开发模式' : '生产模式'} |
||||||
|
是否为内嵌启动: ${isEmbeddedLaunch ? '是' : '否'} |
||||||
|
是否为紧急启动: ${isEmergencyLaunch ? '是' : '否'} |
||||||
|
更新 ID: ${updateId || '无'} |
||||||
|
更新通道: ${channel || '无'} |
||||||
|
运行时版本: ${runtimeVersion || '无'} |
||||||
|
`.trim();
|
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
console.log('=== TabOneScreen 组件已渲染 ==='); |
||||||
|
}, []); |
||||||
|
|
||||||
export default function TabHoneScreen() { |
|
||||||
return ( |
return ( |
||||||
<> |
<View style={styles.container}> |
||||||
<Stack.Screen |
<Text style={styles.title}>🚀 热更新演示</Text> |
||||||
options={{ |
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" /> |
||||||
title: '首页', |
|
||||||
headerShown: false, |
<View style={styles.infoContainer}> |
||||||
}} |
<Text style={styles.infoTitle}>当前版本信息:</Text> |
||||||
/> |
<Text style={styles.infoText}>{getUpdateInfo()}</Text> |
||||||
<HomeScreen /> |
</View> |
||||||
</> |
|
||||||
|
<TouchableOpacity |
||||||
|
style={[styles.button, isChecking && styles.buttonDisabled]} |
||||||
|
onPress={checkForUpdates} |
||||||
|
disabled={isChecking} |
||||||
|
> |
||||||
|
{isChecking ? ( |
||||||
|
<ActivityIndicator color="#fff" /> |
||||||
|
) : ( |
||||||
|
<Text style={styles.buttonText}>检查更新</Text> |
||||||
|
)} |
||||||
|
</TouchableOpacity> |
||||||
|
|
||||||
|
{updateInfo ? ( |
||||||
|
<View style={styles.updateInfoContainer}> |
||||||
|
<Text style={styles.updateInfoText}>{updateInfo}</Text> |
||||||
|
</View> |
||||||
|
) : null} |
||||||
|
|
||||||
|
<View style={styles.instructionsContainer}> |
||||||
|
<Text style={styles.instructionsTitle}>📝 使用说明:</Text> |
||||||
|
<Text style={styles.instructionsText}> |
||||||
|
1. 使用 EAS Build 构建生产版本{'\n'} |
||||||
|
2. 修改代码后运行 eas update 发布更新{'\n'} |
||||||
|
3. 打开应用点击"检查更新"按钮{'\n'} |
||||||
|
4. 应用会自动下载并提示重启 |
||||||
|
</Text> |
||||||
|
</View> |
||||||
|
</View> |
||||||
); |
); |
||||||
} |
} |
||||||
|
|
||||||
|
const styles = StyleSheet.create({ |
||||||
|
container: { |
||||||
|
flex: 1, |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center', |
||||||
|
padding: 20, |
||||||
|
}, |
||||||
|
title: { |
||||||
|
fontSize: 28, |
||||||
|
fontWeight: 'bold', |
||||||
|
marginBottom: 10, |
||||||
|
}, |
||||||
|
separator: { |
||||||
|
marginVertical: 20, |
||||||
|
height: 1, |
||||||
|
width: '80%', |
||||||
|
}, |
||||||
|
infoContainer: { |
||||||
|
backgroundColor: 'rgba(0, 122, 255, 0.1)', |
||||||
|
padding: 15, |
||||||
|
borderRadius: 10, |
||||||
|
marginBottom: 20, |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
infoTitle: { |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: 'bold', |
||||||
|
marginBottom: 10, |
||||||
|
}, |
||||||
|
infoText: { |
||||||
|
fontSize: 12, |
||||||
|
fontFamily: 'monospace', |
||||||
|
lineHeight: 18, |
||||||
|
}, |
||||||
|
button: { |
||||||
|
backgroundColor: '#007AFF', |
||||||
|
paddingHorizontal: 30, |
||||||
|
paddingVertical: 15, |
||||||
|
borderRadius: 10, |
||||||
|
marginBottom: 20, |
||||||
|
minWidth: 200, |
||||||
|
alignItems: 'center', |
||||||
|
}, |
||||||
|
buttonDisabled: { |
||||||
|
backgroundColor: '#999', |
||||||
|
}, |
||||||
|
buttonText: { |
||||||
|
color: '#fff', |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: 'bold', |
||||||
|
}, |
||||||
|
updateInfoContainer: { |
||||||
|
backgroundColor: 'rgba(52, 199, 89, 0.1)', |
||||||
|
padding: 15, |
||||||
|
borderRadius: 10, |
||||||
|
marginBottom: 20, |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
updateInfoText: { |
||||||
|
fontSize: 14, |
||||||
|
textAlign: 'center', |
||||||
|
}, |
||||||
|
instructionsContainer: { |
||||||
|
backgroundColor: 'rgba(255, 149, 0, 0.1)', |
||||||
|
padding: 15, |
||||||
|
borderRadius: 10, |
||||||
|
width: '100%', |
||||||
|
}, |
||||||
|
instructionsTitle: { |
||||||
|
fontSize: 16, |
||||||
|
fontWeight: 'bold', |
||||||
|
marginBottom: 10, |
||||||
|
}, |
||||||
|
instructionsText: { |
||||||
|
fontSize: 13, |
||||||
|
lineHeight: 20, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.5 KiB |
|
Before Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB |
@ -1,165 +0,0 @@ |
|||||||
/** |
|
||||||
* 首页 Header 组件 |
|
||||||
* 包含搜索、用户信息、消息等功能 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react'; |
|
||||||
import { |
|
||||||
View, |
|
||||||
Text, |
|
||||||
StyleSheet, |
|
||||||
TouchableOpacity, |
|
||||||
TextInput, |
|
||||||
Image, |
|
||||||
Dimensions, |
|
||||||
} from 'react-native'; |
|
||||||
import { createThemeStyles, useColorScheme, useThemeInfo } 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 theme = useColorScheme(); |
|
||||||
const s = styles[theme]; |
|
||||||
const { colors } = useThemeInfo(); |
|
||||||
|
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,182 +0,0 @@ |
|||||||
import { filter, toNumber, isNaN, forEach } from 'lodash-es'; |
|
||||||
|
|
||||||
// 1:棋牌 2:彩票 3:电子 4:捕鱼 5:真人 6:体育 7:红包 8:原创 10: 区块链
|
|
||||||
export enum GameMainTypesEnum { |
|
||||||
CHESS = 1, |
|
||||||
LOTTERY = 2, |
|
||||||
ELECTRONIC = 3, |
|
||||||
FISHING = 4, |
|
||||||
LIVE = 5, |
|
||||||
SPORTS = 6, |
|
||||||
BLOCK = 8, |
|
||||||
BLOCK_THIRD = 39, |
|
||||||
|
|
||||||
// 自定义
|
|
||||||
LOBBY = 100, // 大厅
|
|
||||||
COLLECT = 101, // 收藏
|
|
||||||
RECENT = 102, // 最近
|
|
||||||
RECOMMEND = 103, // 推荐
|
|
||||||
HIGH_HIT = 104, // 高暴奖
|
|
||||||
TRIAL = 105, // 试玩
|
|
||||||
HOT = 0, // 热门
|
|
||||||
HOT_GAME = 10000, // 热门游戏
|
|
||||||
HOT_CHESS = 10001, // 热门棋牌
|
|
||||||
HOT_LOTTERY = 10002, // 热门彩票
|
|
||||||
HOT_ELECTRONIC = 10003, // 热门电子
|
|
||||||
HOT_FISHING = 10004, // 热门捕鱼
|
|
||||||
HOT_LIVE = 10005, // 热门真人
|
|
||||||
HOT_SPORTS = 10006, // 热门体育
|
|
||||||
HOT_BLOCK = 10008, // 热门原创
|
|
||||||
HOT_BLOCK_THIRD = 10039, // 热门区块链
|
|
||||||
} |
|
||||||
|
|
||||||
export enum GameBigTypesEnum { |
|
||||||
CHESS = 1, |
|
||||||
ELECTRONIC = 2, |
|
||||||
FISHING = 3, |
|
||||||
LIVE = 4, |
|
||||||
SPORTS = 5, |
|
||||||
LOTTERY = 7, |
|
||||||
BLOCK_THIRD = 10, |
|
||||||
} |
|
||||||
|
|
||||||
// 首页三级菜单
|
|
||||||
export enum GameChildTypesEnum { |
|
||||||
ALL = '1', // 所有
|
|
||||||
HOT = '2', // 热门
|
|
||||||
RECENT = '3', // 最近
|
|
||||||
COLLECT = '4', // 收藏
|
|
||||||
} |
|
||||||
|
|
||||||
export enum LotteryTypeKeysEnum { |
|
||||||
tianCheng = 15, |
|
||||||
GPI = 16, |
|
||||||
SBCP = 30, |
|
||||||
DB = 31, |
|
||||||
SGWIN = 32, |
|
||||||
} |
|
||||||
|
|
||||||
export enum LotteryTypeIdEnum { |
|
||||||
GPI = 504, |
|
||||||
DB = 1385, |
|
||||||
SBCP = 1326, |
|
||||||
SGWIN = 1410, |
|
||||||
tianCheng = 468, |
|
||||||
} |
|
||||||
|
|
||||||
export const LotteryNameByKey = { |
|
||||||
GPI: 'GPI', |
|
||||||
DB: 'DB', |
|
||||||
SGWIN: 'SGWin', |
|
||||||
tianCheng: '天成', |
|
||||||
}; |
|
||||||
|
|
||||||
export enum GameMainKeysEnum { |
|
||||||
CHESS = 'chess', |
|
||||||
LOTTERY = 'lottery', // 热门彩票
|
|
||||||
LOTTERY_ALL = 'lotteryAll', // 全部彩票
|
|
||||||
ELECTRONIC = 'electronic', |
|
||||||
FISHING = 'fishing', |
|
||||||
BLOCK_THIRD = 'blockThird', |
|
||||||
LIVE = 'live', |
|
||||||
SPORTS = 'sports', |
|
||||||
BLOCK = 'block', |
|
||||||
|
|
||||||
LOBBY = 'lobby', // 大厅
|
|
||||||
COLLECT = 'collect', // 收藏
|
|
||||||
RECENT = 'recent', // 最近
|
|
||||||
RECOMMEND = 'recommend', // 推荐
|
|
||||||
HIGH_HIT = 'highHit', // 高暴奖
|
|
||||||
TRIAL = 'trial', // 试玩
|
|
||||||
HOT = 'hot', // 热门
|
|
||||||
HOT_GAME = 'hotGame', // 热门游戏
|
|
||||||
HOT_CHESS = 'hotChess', // 热门棋牌
|
|
||||||
HOT_LOTTERY = 'hotLottery', // 热门彩票
|
|
||||||
HOT_ELECTRONIC = 'hotElectronic', // 热门电子
|
|
||||||
HOT_FISHING = 'hotFishing', // 热门捕鱼
|
|
||||||
HOT_LIVE = 'hotLive', // 热门真人
|
|
||||||
HOT_SPORTS = 'hotSports', // 热门体育
|
|
||||||
HOT_BLOCK = 'hotBlock', // 热门原创
|
|
||||||
HOT_BLOCK_THIRD = 'hotBlockThird', // 热门区块链
|
|
||||||
} |
|
||||||
|
|
||||||
export const gameMainTypesMap = (() => { |
|
||||||
const result = {}; |
|
||||||
forEach( |
|
||||||
filter(Object.keys(GameMainTypesEnum), val => isNaN(toNumber(val))), |
|
||||||
(key) => { |
|
||||||
if (GameMainKeysEnum[key]) { |
|
||||||
result[GameMainTypesEnum[key]] = GameMainKeysEnum[key]; |
|
||||||
} |
|
||||||
} |
|
||||||
); |
|
||||||
console.log('resultresultresult', result); |
|
||||||
return result as Record<GameMainTypesEnum, GameMainKeysEnum>; |
|
||||||
})(); |
|
||||||
|
|
||||||
// 默认首页游戏分类菜单
|
|
||||||
export const defaultHomeGameTabMenus = [ |
|
||||||
{ name: '推荐', key: GameMainTypesEnum.RECOMMEND, icon: 'recommend' }, |
|
||||||
// { name: '试玩', key: GameMainTypesEnum.TRIAL, icon: 'try' },
|
|
||||||
{ name: '收藏', key: GameMainTypesEnum.COLLECT, icon: 'home-star-solid' }, |
|
||||||
{ name: '最近', key: GameMainTypesEnum.RECENT, icon: 'clock-solid' }, |
|
||||||
]; |
|
||||||
|
|
||||||
export const gameTypeName = name => { |
|
||||||
console.log(name); |
|
||||||
let iconName = ''; |
|
||||||
switch (name) { |
|
||||||
case '棋牌': |
|
||||||
iconName = 'chess'; |
|
||||||
break; |
|
||||||
case '电子': |
|
||||||
iconName = 'electronic'; |
|
||||||
break; |
|
||||||
case '捕鱼': |
|
||||||
iconName = 'fishing'; |
|
||||||
break; |
|
||||||
case '真人': |
|
||||||
iconName = 'live'; |
|
||||||
break; |
|
||||||
case '体育': |
|
||||||
iconName = 'sports'; |
|
||||||
break; |
|
||||||
case '彩票': |
|
||||||
iconName = 'lottery'; |
|
||||||
break; |
|
||||||
case '原创': |
|
||||||
iconName = 'block'; |
|
||||||
break; |
|
||||||
|
|
||||||
default: |
|
||||||
break; |
|
||||||
} |
|
||||||
return iconName; |
|
||||||
}; |
|
||||||
|
|
||||||
export const bigTypeNameConfig = { |
|
||||||
1: GameMainKeysEnum.CHESS, |
|
||||||
2: GameMainKeysEnum.ELECTRONIC, |
|
||||||
3: GameMainKeysEnum.FISHING, |
|
||||||
4: GameMainKeysEnum.LIVE, |
|
||||||
5: GameMainKeysEnum.SPORTS, |
|
||||||
7: GameMainKeysEnum.LOTTERY, |
|
||||||
10: GameMainKeysEnum.BLOCK_THIRD, |
|
||||||
}; |
|
||||||
|
|
||||||
export const serverImgPathMap = { |
|
||||||
[GameBigTypesEnum.CHESS]: 'qipai', |
|
||||||
[GameBigTypesEnum.ELECTRONIC]: 'dianzi', |
|
||||||
[GameBigTypesEnum.FISHING]: 'buyu', |
|
||||||
[GameBigTypesEnum.BLOCK_THIRD]: 'qukuailian', |
|
||||||
}; |
|
||||||
|
|
||||||
export const bigTypeToMainTypeMap = { |
|
||||||
1: GameMainTypesEnum.CHESS, |
|
||||||
2: GameMainTypesEnum.ELECTRONIC, |
|
||||||
3: GameMainTypesEnum.FISHING, |
|
||||||
4: GameMainTypesEnum.LIVE, |
|
||||||
5: GameMainTypesEnum.SPORTS, |
|
||||||
7: GameMainTypesEnum.LOTTERY, |
|
||||||
10: GameMainTypesEnum.BLOCK_THIRD, |
|
||||||
}; |
|
||||||
@ -1,6 +0,0 @@ |
|||||||
// theme enum
|
|
||||||
export enum ThemeEnum { |
|
||||||
LIGHT = 'light', |
|
||||||
DARK = 'dark', |
|
||||||
ORANGE = 'orange', |
|
||||||
} |
|
||||||
@ -1,8 +0,0 @@ |
|||||||
// 用户注册方式
|
|
||||||
export enum RegisterWayEnum { |
|
||||||
SMS_REGISTRATION = 1, // 短信注册
|
|
||||||
ACCOUNT_REGISTRATION = 2, // 账号注册
|
|
||||||
EMAIL_REGISTRATION = 3, // 邮箱注册
|
|
||||||
AGENT_ACCOUNT_OPENING_REGISTRATION = 4, // 代理开户注册
|
|
||||||
BULK_ACCOUNT_OPENING_REGISTRATION = 5, // 批量开户注册
|
|
||||||
} |
|
||||||
@ -1,155 +0,0 @@ |
|||||||
import { useEffect, useState, useMemo, useCallback } from 'react'; |
|
||||||
import useGameStore from '@/stores/gameStore'; |
|
||||||
import { GameMainTypesEnum, defaultHomeGameTabMenus, gameMainTypesMap } from '@/constants/game'; |
|
||||||
import { ThemeEnum } from '@/constants/theme'; |
|
||||||
import { forEach, cloneDeep, map, filter } from 'lodash-es'; |
|
||||||
import { useIsLoggedIn } from '@/stores/userStore'; |
|
||||||
import { useShallow } from 'zustand/react/shallow'; |
|
||||||
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; |
|
||||||
|
|
||||||
|
|
||||||
type GameMenu = { |
|
||||||
name: string; |
|
||||||
key: string; |
|
||||||
icon?: string; |
|
||||||
logo?: string; |
|
||||||
children?: GameMenu[]; |
|
||||||
}; |
|
||||||
|
|
||||||
// 有子菜单的游戏类型
|
|
||||||
const hasSubGameMainTypes = [ |
|
||||||
GameMainTypesEnum.CHESS, |
|
||||||
GameMainTypesEnum.ELECTRONIC, |
|
||||||
GameMainTypesEnum.FISHING, |
|
||||||
GameMainTypesEnum.BLOCK_THIRD, |
|
||||||
]; |
|
||||||
|
|
||||||
export const useGameMainMenus = (theme: ThemeEnum) => { |
|
||||||
// 在 hook 顶层调用 useIsLoggedIn
|
|
||||||
const isLogin = useIsLoggedIn(); |
|
||||||
|
|
||||||
// 从 store 获取必要的数据 - 直接获取,不使用 useShallow
|
|
||||||
const menuSort = useGameStore((state) => state.menuSort); |
|
||||||
const gameBigClass = useGameStore((state) => state.gameBigClass); |
|
||||||
|
|
||||||
if (__DEV__) { |
|
||||||
console.log('🎮 useGameMainMenus - menuSort:', menuSort, 'length:', menuSort?.length); |
|
||||||
} |
|
||||||
// 使用 useMemo 缓存计算结果,避免每次都创建新对象
|
|
||||||
return useMemo(() => { |
|
||||||
const defaultMenus = cloneDeep(defaultHomeGameTabMenus); |
|
||||||
|
|
||||||
if (theme === ThemeEnum.DARK) { |
|
||||||
forEach(defaultMenus, (item) => { |
|
||||||
if (item.key === GameMainTypesEnum.RECENT) { |
|
||||||
item.icon = 'clock-solid_dark'; |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
const gameMenu = map(menuSort, (item: Record<string, any>) => { |
|
||||||
const typeName = gameMainTypesMap[item.type as GameMainTypesEnum]; |
|
||||||
|
|
||||||
const children = hasSubGameMainTypes.includes(item.type) |
|
||||||
? map(gameBigClass?.[typeName], (it) => { |
|
||||||
return { |
|
||||||
name: it.play_cname, |
|
||||||
key: `${it.play_id}`, |
|
||||||
play_sort: it.play_sort, |
|
||||||
darkImgSrc: `/images/game/${typeName}/dark_${it.play_id}.png`, |
|
||||||
lightImgSrc: `/images/game/${typeName}/light_${it.play_id}.png`, |
|
||||||
colorImgSrc: it.logo3_img_url || `/images/game/${typeName}/color_${it.play_id}.png`, |
|
||||||
}; |
|
||||||
}) |
|
||||||
: []; |
|
||||||
|
|
||||||
return { |
|
||||||
name: item.play_name, |
|
||||||
key: `${item.type}`, |
|
||||||
icon: typeName, |
|
||||||
logo: item.logo_url1, |
|
||||||
children, |
|
||||||
}; |
|
||||||
}); |
|
||||||
|
|
||||||
if (__DEV__) { |
|
||||||
console.log(gameMenu, 'gameMenu'); |
|
||||||
} |
|
||||||
|
|
||||||
return map( |
|
||||||
[ |
|
||||||
...filter(defaultMenus, (item) => [GameMainTypesEnum.RECOMMEND].includes(item.key)), |
|
||||||
...(isLogin |
|
||||||
? filter(gameMenu, (item) => Number(item.key) !== GameMainTypesEnum.TRIAL) |
|
||||||
: gameMenu), |
|
||||||
...filter(defaultMenus, (item) => |
|
||||||
[GameMainTypesEnum.COLLECT, GameMainTypesEnum.RECENT].includes(item.key) |
|
||||||
), |
|
||||||
], |
|
||||||
(item) => ({ |
|
||||||
...item, |
|
||||||
// 为了在 React Native 中正确加载本地图片,使用 require 的方式
|
|
||||||
// 这里保留原始的 icon 名称,在组件中使用 require 加载
|
|
||||||
icon: item.icon, |
|
||||||
key: `${item.key}`, |
|
||||||
}) |
|
||||||
) as GameMenu[]; |
|
||||||
}, [theme, isLogin, menuSort, gameBigClass]); |
|
||||||
}; |
|
||||||
|
|
||||||
export const useMenuDataLoaded = () => useGameStore((state) => state.menuSort?.length > 0); |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏分类选择 Hook(统一管理) |
|
||||||
* |
|
||||||
* 管理当前选中的游戏分类,支持: |
|
||||||
* - 从 gameStore 获取当前选中分类 |
|
||||||
* - 更新选中分类并保存到 session storage |
|
||||||
* - 页面刷新后恢复选中分类 |
|
||||||
* |
|
||||||
* @returns {Object} 包含 selectedCategory 和 setSelectedCategory 的对象 |
|
||||||
* |
|
||||||
* @example |
|
||||||
* ```typescript
|
|
||||||
* const { selectedCategory, setSelectedCategory } = useSelectedCategory(); |
|
||||||
* |
|
||||||
* // 获取当前选中分类
|
|
||||||
* console.log(selectedCategory); // '103'
|
|
||||||
* |
|
||||||
* // 更新选中分类
|
|
||||||
* setSelectedCategory('1'); |
|
||||||
* ``` |
|
||||||
*/ |
|
||||||
export const useSelectedCategory = () => { |
|
||||||
const selectedCategory = useGameStore((state) => state.selectedCategory); |
|
||||||
const setSelectedCategoryInStore = useGameStore((state) => state.setSelectedCategory); |
|
||||||
|
|
||||||
// 初始化时从 session storage 恢复选中分类
|
|
||||||
useEffect(() => { |
|
||||||
const initializeSelectedCategory = async () => { |
|
||||||
try { |
|
||||||
const savedCategory = storageManager.session.getItem(STORAGE_KEYS.APP_ACTIVE_MAIN_MENU_TAB); |
|
||||||
if (savedCategory) { |
|
||||||
setSelectedCategoryInStore(savedCategory); |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error('Failed to restore selected category:', error); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
initializeSelectedCategory(); |
|
||||||
}, [setSelectedCategoryInStore]); |
|
||||||
|
|
||||||
// 更新选中分类的回调函数
|
|
||||||
const setSelectedCategory = useCallback( |
|
||||||
(categoryId: string) => { |
|
||||||
setSelectedCategoryInStore(categoryId); |
|
||||||
}, |
|
||||||
[setSelectedCategoryInStore] |
|
||||||
); |
|
||||||
|
|
||||||
return { |
|
||||||
selectedCategory, |
|
||||||
setSelectedCategory, |
|
||||||
}; |
|
||||||
}; |
|
||||||
@ -1,154 +0,0 @@ |
|||||||
/** |
|
||||||
* 轮播图组件 |
|
||||||
* |
|
||||||
* 展示首页轮播图,支持自动播放和手动滑动 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react'; |
|
||||||
import { |
|
||||||
View, |
|
||||||
Image, |
|
||||||
TouchableOpacity, |
|
||||||
ScrollView, |
|
||||||
NativeScrollEvent, |
|
||||||
NativeSyntheticEvent, |
|
||||||
ActivityIndicator, |
|
||||||
Dimensions, |
|
||||||
Alert, |
|
||||||
} from 'react-native'; |
|
||||||
import { useColorScheme } from '@/hooks'; |
|
||||||
// import type { Banner } from '@/types/home';
|
|
||||||
import { styles } from './styles'; |
|
||||||
import useMsgStore from '@/stores/msgStore'; |
|
||||||
|
|
||||||
|
|
||||||
interface BannerSwiperProps {} |
|
||||||
|
|
||||||
const { width } = Dimensions.get('window'); |
|
||||||
|
|
||||||
/** |
|
||||||
* 轮播图组件 |
|
||||||
*/ |
|
||||||
export default function BannerSwiper({}: BannerSwiperProps) { |
|
||||||
const colorScheme = useColorScheme(); |
|
||||||
const s = styles[colorScheme]; |
|
||||||
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: colorScheme === '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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,61 +0,0 @@ |
|||||||
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, |
|
||||||
}, |
|
||||||
})); |
|
||||||
@ -1,126 +0,0 @@ |
|||||||
/** |
|
||||||
* 首页底部 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 '@/theme'; |
|
||||||
|
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,143 +0,0 @@ |
|||||||
/** |
|
||||||
* 快速底部导航组件 |
|
||||||
* |
|
||||||
* 深色主题特有,提供快速导航,使用真实数据 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'; |
|
||||||
import { |
|
||||||
View, |
|
||||||
Text, |
|
||||||
StyleSheet, |
|
||||||
TouchableOpacity, |
|
||||||
ScrollView, |
|
||||||
Animated, |
|
||||||
} from 'react-native'; |
|
||||||
import { createThemeStyles } from '@/theme'; |
|
||||||
import { useColorScheme } from '@/hooks'; |
|
||||||
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 { |
|
||||||
items?: NavItem[]; |
|
||||||
onTabPress?: (tabId: string, action: string) => void; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 快速底部导航组件 |
|
||||||
*/ |
|
||||||
export default function FastFootNav({ items: propItems, onTabPress }: FastFootNavProps) { |
|
||||||
const colorScheme = useColorScheme(); |
|
||||||
const s = styles[colorScheme]; |
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,219 +0,0 @@ |
|||||||
/** |
|
||||||
* 游戏分类菜单组件 |
|
||||||
* |
|
||||||
* 展示游戏分类,支持切换,使用真实数据 |
|
||||||
*/ |
|
||||||
|
|
||||||
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 useGameStore from '@/stores/gameStore';
|
|
||||||
// import { ThemeEnum } from '@/constants/theme';
|
|
||||||
import { Colors, useColorScheme } from '@/theme'; |
|
||||||
|
|
||||||
// 条件导入 LinearGradient - 仅在非 Web 平台使用
|
|
||||||
let LinearGradient: any = null; |
|
||||||
if (Platform.OS !== 'web') { |
|
||||||
LinearGradient = require('react-native-linear-gradient').default; |
|
||||||
} |
|
||||||
|
|
||||||
// 游戏菜单图片映射 - 使用 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'), |
|
||||||
'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'), |
|
||||||
}; |
|
||||||
|
|
||||||
interface GameMainMenuProps { |
|
||||||
topHeight?: number; |
|
||||||
showSubMenus?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏分类菜单组件 |
|
||||||
*/ |
|
||||||
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 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) => { |
|
||||||
setSelectedCategory(categoryKey); |
|
||||||
}, |
|
||||||
[setSelectedCategory] |
|
||||||
); |
|
||||||
|
|
||||||
// 骨架屏 - 显示加载中的占位符
|
|
||||||
const renderSkeleton = () => ( |
|
||||||
<View style={s.container}> |
|
||||||
<ScrollView |
|
||||||
style={s.scrollView} |
|
||||||
horizontal |
|
||||||
showsHorizontalScrollIndicator={false} |
|
||||||
scrollEventThrottle={16} |
|
||||||
> |
|
||||||
{Array.from({ length: 6 }).map((_, index) => ( |
|
||||||
<View |
|
||||||
key={`skeleton-${index}`} |
|
||||||
style={[s.menuItem, { backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0' }]} |
|
||||||
/> |
|
||||||
))} |
|
||||||
</ScrollView> |
|
||||||
</View> |
|
||||||
); |
|
||||||
console.log('isDataLoaded', isDataLoaded); |
|
||||||
// 如果动态数据还未加载,显示骨架屏
|
|
||||||
if (!isDataLoaded) { |
|
||||||
return renderSkeleton(); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<View style={s.container}> |
|
||||||
<ScrollView |
|
||||||
ref={scrollViewRef} |
|
||||||
style={s.scrollView} |
|
||||||
horizontal |
|
||||||
showsHorizontalScrollIndicator={false} |
|
||||||
scrollEventThrottle={16} |
|
||||||
> |
|
||||||
{gameMenus.map((menu) => { |
|
||||||
// 处理图片源 - 优先使用 logo(URL),其次使用 icon(本地资源)
|
|
||||||
let imageSource: any = null; |
|
||||||
|
|
||||||
if (menu.logo) { |
|
||||||
// logo 是 URL,直接使用
|
|
||||||
imageSource = { uri: menu.logo }; |
|
||||||
} else if (menu.icon) { |
|
||||||
// icon 是本地资源名称,从映射表中获取
|
|
||||||
imageSource = MENU_ICON_MAP[menu.icon]; |
|
||||||
} |
|
||||||
|
|
||||||
const isActive = selectedCategory === menu.key; |
|
||||||
const themeColors = Colors[theme]; |
|
||||||
|
|
||||||
// 获取渐变色 - 从主题色到透明
|
|
||||||
const gradientStart = `${themeColors.tint}40`; // 主题色 + 40% 透明度
|
|
||||||
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> |
|
||||||
</> |
|
||||||
); |
|
||||||
|
|
||||||
// 如果是选中状态,使用 LinearGradient 包装(非 Web 平台)或 CSS 渐变(Web 平台)
|
|
||||||
if (isActive) { |
|
||||||
// Web 平台使用 CSS 渐变
|
|
||||||
if (Platform.OS === 'web') { |
|
||||||
const webStyle = { |
|
||||||
...s.menuItem, |
|
||||||
...s.menuItemActive, |
|
||||||
background: `linear-gradient(to top, ${gradientStart}, ${gradientEnd})`, |
|
||||||
backgroundColor: undefined, // 移除 backgroundColor,使用 background
|
|
||||||
} as any; |
|
||||||
|
|
||||||
return ( |
|
||||||
<TouchableOpacity |
|
||||||
key={menu.key} |
|
||||||
style={webStyle} |
|
||||||
onPress={() => handleCategoryPress(menu.key)} |
|
||||||
activeOpacity={0.7} |
|
||||||
> |
|
||||||
{menuContent} |
|
||||||
</TouchableOpacity> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 非 Web 平台使用 LinearGradient
|
|
||||||
if (LinearGradient) { |
|
||||||
return ( |
|
||||||
<LinearGradient |
|
||||||
key={menu.key} |
|
||||||
colors={[gradientStart, gradientEnd]} |
|
||||||
start={{ x: 0.5, y: 1 }} // 从下往上
|
|
||||||
end={{ x: 0.5, y: 0 }} |
|
||||||
style={[s.menuItem, s.menuItemActive]} |
|
||||||
> |
|
||||||
<TouchableOpacity |
|
||||||
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }} |
|
||||||
onPress={() => handleCategoryPress(menu.key)} |
|
||||||
activeOpacity={0.7} |
|
||||||
> |
|
||||||
{menuContent} |
|
||||||
</TouchableOpacity> |
|
||||||
</LinearGradient> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 备用方案:如果 LinearGradient 不可用,使用纯色
|
|
||||||
return ( |
|
||||||
<TouchableOpacity |
|
||||||
key={menu.key} |
|
||||||
style={[s.menuItem, s.menuItemActive]} |
|
||||||
onPress={() => handleCategoryPress(menu.key)} |
|
||||||
activeOpacity={0.7} |
|
||||||
> |
|
||||||
{menuContent} |
|
||||||
</TouchableOpacity> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
// 未选中状态,使用普通 TouchableOpacity
|
|
||||||
return ( |
|
||||||
<TouchableOpacity |
|
||||||
key={menu.key} |
|
||||||
style={s.menuItem} |
|
||||||
onPress={() => handleCategoryPress(menu.key)} |
|
||||||
activeOpacity={0.7} |
|
||||||
> |
|
||||||
{menuContent} |
|
||||||
</TouchableOpacity> |
|
||||||
); |
|
||||||
})} |
|
||||||
</ScrollView> |
|
||||||
</View> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -1,70 +0,0 @@ |
|||||||
import { createThemeStyles, createResponsiveThemeStyles } 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, |
|
||||||
paddingTop: 5, |
|
||||||
borderBottomWidth: 1, |
|
||||||
borderBottomColor: colors.border, |
|
||||||
}, |
|
||||||
scrollView: { |
|
||||||
paddingHorizontal: 12, |
|
||||||
}, |
|
||||||
menuItem: { |
|
||||||
paddingHorizontal: 12, |
|
||||||
paddingVertical: 5, |
|
||||||
marginRight: 8, |
|
||||||
borderRadius: 0, |
|
||||||
backgroundColor: 'transparent', |
|
||||||
justifyContent: 'center', |
|
||||||
alignItems: 'center', |
|
||||||
elevation: 1, |
|
||||||
shadowColor: colors.cardShadow, |
|
||||||
shadowOffset: { width: 0, height: 1 }, |
|
||||||
shadowOpacity: 0.1, |
|
||||||
shadowRadius: 2, |
|
||||||
borderBottomColor: 'transparent', |
|
||||||
borderBottomWidth: 2, |
|
||||||
}, |
|
||||||
menuItemActive: { |
|
||||||
// backgroundColor: `${colors.tint}15`, // 主题色 + 20% 透明度
|
|
||||||
elevation: 2, |
|
||||||
shadowOpacity: 0.15, |
|
||||||
borderBottomColor: colors.tint, |
|
||||||
borderRadius: 0, |
|
||||||
}, |
|
||||||
menuText: { |
|
||||||
fontSize: 14, |
|
||||||
color: colors.text, |
|
||||||
fontWeight: '600', |
|
||||||
}, |
|
||||||
menuTextActive: { |
|
||||||
color: colors.tint, |
|
||||||
}, |
|
||||||
menuIcon: { |
|
||||||
width: 30, |
|
||||||
height: 30, |
|
||||||
marginBottom: 4, |
|
||||||
}, |
|
||||||
})); |
|
||||||
|
|
||||||
export const themeStyles = createResponsiveThemeStyles({ |
|
||||||
menuItemActive: { |
|
||||||
backgroundColor: '', |
|
||||||
}, |
|
||||||
}, { |
|
||||||
menuItemActive: { |
|
||||||
backgroundColor: '', |
|
||||||
}, |
|
||||||
}, { |
|
||||||
menuItemActive: { |
|
||||||
backgroundColor: '', |
|
||||||
}, |
|
||||||
}); |
|
||||||
@ -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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,164 +0,0 @@ |
|||||||
/** |
|
||||||
* 高奖金游戏组件 |
|
||||||
* |
|
||||||
* 深色主题特有,展示高奖金游戏,使用真实数据 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'; |
|
||||||
import { |
|
||||||
View, |
|
||||||
Text, |
|
||||||
StyleSheet, |
|
||||||
ScrollView, |
|
||||||
TouchableOpacity, |
|
||||||
Animated, |
|
||||||
Image, |
|
||||||
} from 'react-native'; |
|
||||||
import { createThemeStyles } from '@/theme'; |
|
||||||
import { useColorScheme } from '@/hooks'; |
|
||||||
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 { |
|
||||||
games?: HighPrizeGameType[]; |
|
||||||
onGamePress?: (game: HighPrizeGameType) => void; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 高奖金游戏组件 |
|
||||||
*/ |
|
||||||
export default function HighPrizeGame({ games: propGames, onGamePress }: HighPrizeGameProps) { |
|
||||||
const colorScheme = useColorScheme(); |
|
||||||
const s = styles[colorScheme]; |
|
||||||
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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,208 +0,0 @@ |
|||||||
/** |
|
||||||
* 游戏大厅组件 |
|
||||||
* |
|
||||||
* 展示游戏列表,使用真实数据 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React, { useState, useEffect, useMemo } from 'react'; |
|
||||||
import { |
|
||||||
View, |
|
||||||
Text, |
|
||||||
StyleSheet, |
|
||||||
FlatList, |
|
||||||
TouchableOpacity, |
|
||||||
Image, |
|
||||||
ActivityIndicator, |
|
||||||
Dimensions, |
|
||||||
} from 'react-native'; |
|
||||||
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme'; |
|
||||||
import { useSelectedCategory } from '@/hooks/useGameMenus'; |
|
||||||
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 { |
|
||||||
games?: Game[]; |
|
||||||
onGamePress?: (game: Game) => void; |
|
||||||
topHeight?: number; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏大厅组件 |
|
||||||
*/ |
|
||||||
export default function Lobby({ |
|
||||||
games: propGames, |
|
||||||
onGamePress, |
|
||||||
topHeight = 0, |
|
||||||
}: LobbyProps) { |
|
||||||
const colorScheme = useColorScheme(); |
|
||||||
const s = styles[colorScheme]; |
|
||||||
const { colors } = useThemeInfo(); |
|
||||||
const { selectedCategory } = useSelectedCategory(); |
|
||||||
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.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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -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> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,26 +0,0 @@ |
|||||||
/** |
|
||||||
* 首页组件统一导出 |
|
||||||
* |
|
||||||
* 所有组件都使用 React.memo 进行性能优化 |
|
||||||
*/ |
|
||||||
|
|
||||||
import React from 'react'; |
|
||||||
import BannerSwiperComponent from './BannerSwiper'; |
|
||||||
import NoticeBarComponent from './NoticeBar'; |
|
||||||
import GameMainMenusComponent from './GameMainMenus'; |
|
||||||
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 GameMainMenus = React.memo(GameMainMenusComponent); |
|
||||||
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); |
|
||||||
|
|
||||||
@ -1,157 +0,0 @@ |
|||||||
/** |
|
||||||
* 完整首页容器 |
|
||||||
* 包含 Header、内容区域、BottomTabs |
|
||||||
* 支持主题切换和真实数据 |
|
||||||
*/ |
|
||||||
import React, { useState, useEffect, useCallback } from 'react'; |
|
||||||
import { View, ScrollView, RefreshControl, Alert } from 'react-native'; |
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context'; |
|
||||||
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme'; |
|
||||||
import { |
|
||||||
Header, |
|
||||||
BannerSwiper, |
|
||||||
NoticeBar, |
|
||||||
GameMainMenus, |
|
||||||
Lobby, |
|
||||||
HighPrizeGame, |
|
||||||
FastFootNav, |
|
||||||
} from './components'; |
|
||||||
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, |
|
||||||
}, |
|
||||||
})); |
|
||||||
|
|
||||||
|
|
||||||
/** |
|
||||||
* 完整首页容器 |
|
||||||
*/ |
|
||||||
export default function HomePage() { |
|
||||||
const colorScheme = useColorScheme(); |
|
||||||
const s = styles[colorScheme]; |
|
||||||
const { isDark: isDarkTheme, colors } = useThemeInfo(); |
|
||||||
|
|
||||||
const [refreshing, setRefreshing] = useState(false); |
|
||||||
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 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) { |
|
||||||
// 深色主题布局
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<GameMainMenus /> |
|
||||||
<BannerSwiper /> |
|
||||||
<NoticeBar /> |
|
||||||
<HighPrizeGame onGamePress={handleGamePress} /> |
|
||||||
<Lobby onGamePress={handleGamePress} /> |
|
||||||
<FastFootNav onTabPress={handleTabPress} /> |
|
||||||
</> |
|
||||||
); |
|
||||||
} else { |
|
||||||
// 浅色主题布局
|
|
||||||
return ( |
|
||||||
<> |
|
||||||
<BannerSwiper /> |
|
||||||
<NoticeBar /> |
|
||||||
<GameMainMenus /> |
|
||||||
<Lobby onGamePress={handleGamePress} /> |
|
||||||
</> |
|
||||||
); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<SafeAreaView style={s.container}> |
|
||||||
{/* Header */} |
|
||||||
<Header |
|
||||||
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.primary} |
|
||||||
/> |
|
||||||
} |
|
||||||
showsVerticalScrollIndicator={false} |
|
||||||
> |
|
||||||
<View style={s.scrollContent}>{renderContent()}</View> |
|
||||||
</ScrollView> |
|
||||||
</View> |
|
||||||
</SafeAreaView> |
|
||||||
); |
|
||||||
} |
|
||||||
@ -80,9 +80,6 @@ importers: |
|||||||
react-native: |
react-native: |
||||||
specifier: 0.81.5 |
specifier: 0.81.5 |
||||||
version: 0.81.5(@babel/[email protected])(@types/[email protected])([email protected]) |
version: 0.81.5(@babel/[email protected])(@types/[email protected])([email protected]) |
||||||
react-native-linear-gradient: |
|
||||||
specifier: ^2.8.3 |
|
||||||
version: 2.8.3([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]) |
|
||||||
react-native-paper: |
react-native-paper: |
||||||
specifier: ^5.14.5 |
specifier: ^5.14.5 |
||||||
version: 5.14.5([email protected]([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]))([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]) |
version: 5.14.5([email protected]([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]))([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]) |
||||||
@ -2988,12 +2985,6 @@ packages: |
|||||||
react: '*' |
react: '*' |
||||||
react-native: '*' |
react-native: '*' |
||||||
|
|
||||||
[email protected]: |
|
||||||
resolution: {integrity: sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==} |
|
||||||
peerDependencies: |
|
||||||
react: '*' |
|
||||||
react-native: '*' |
|
||||||
|
|
||||||
[email protected]: |
[email protected]: |
||||||
resolution: {integrity: sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==} |
resolution: {integrity: sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==} |
||||||
peerDependencies: |
peerDependencies: |
||||||
@ -7217,11 +7208,6 @@ snapshots: |
|||||||
react: 19.1.0 |
react: 19.1.0 |
||||||
react-native: 0.81.5(@babel/[email protected])(@types/[email protected])([email protected]) |
react-native: 0.81.5(@babel/[email protected])(@types/[email protected])([email protected]) |
||||||
|
|
||||||
[email protected]([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]): |
|
||||||
dependencies: |
|
||||||
react: 19.1.0 |
|
||||||
react-native: 0.81.5(@babel/[email protected])(@types/[email protected])([email protected]) |
|
||||||
|
|
||||||
[email protected]([email protected]([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]))([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]): |
[email protected]([email protected]([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]))([email protected](@babel/[email protected])(@types/[email protected])([email protected]))([email protected]): |
||||||
dependencies: |
dependencies: |
||||||
'@callstack/react-theme-provider': 3.0.9([email protected]) |
'@callstack/react-theme-provider': 3.0.9([email protected]) |
||||||
|
|||||||
@ -1,37 +0,0 @@ |
|||||||
/** |
|
||||||
* 游戏服务 |
|
||||||
* 处理租户相关的 API 请求 |
|
||||||
*/ |
|
||||||
|
|
||||||
import { request } from '@/utils/network/api'; |
|
||||||
|
|
||||||
/** |
|
||||||
* API 响应接口 |
|
||||||
*/ |
|
||||||
interface ApiResponse<T = any> { |
|
||||||
code: number; |
|
||||||
message: string; |
|
||||||
data: T; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* tenant 服务类 |
|
||||||
*/ |
|
||||||
class GameService { |
|
||||||
/** |
|
||||||
* 获取首页数据 |
|
||||||
*/ |
|
||||||
getHomePageData(data?: Record<string, any>): Promise<ApiResponse> { |
|
||||||
return request.post('/v2', data, { |
|
||||||
headers: { |
|
||||||
cmdId: 381119, |
|
||||||
paramType: 1, |
|
||||||
apiName: 'getHomePageData', |
|
||||||
}, |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// 导出单例
|
|
||||||
export const gameService = new GameService(); |
|
||||||
export default gameService; |
|
||||||
@ -1,233 +0,0 @@ |
|||||||
/** |
|
||||||
* 首页 Mock 数据服务 |
|
||||||
* 用于开发和测试,模拟真实 API 响应 |
|
||||||
*/ |
|
||||||
|
|
||||||
import type { |
|
||||||
Banner, |
|
||||||
Notice, |
|
||||||
GameCategory, |
|
||||||
Game, |
|
||||||
HighPrizeGame, |
|
||||||
NavItem, |
|
||||||
} from '@/types/home'; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 轮播图数据 |
|
||||||
*/ |
|
||||||
export const mockBanners: Banner[] = [ |
|
||||||
{ |
|
||||||
id: '1', |
|
||||||
subject: 'https://via.placeholder.com/1080x350/FF6B6B/FFFFFF?text=Banner+1', |
|
||||||
link_type: 1, |
|
||||||
content: 'https://example.com', |
|
||||||
title: '新年大优惠', |
|
||||||
description: '充值送彩金', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '2', |
|
||||||
subject: 'https://via.placeholder.com/1080x350/4ECDC4/FFFFFF?text=Banner+2', |
|
||||||
link_type: 1, |
|
||||||
content: 'https://example.com', |
|
||||||
title: '周末狂欢', |
|
||||||
description: '返水最高50%', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '3', |
|
||||||
subject: 'https://via.placeholder.com/1080x350/45B7D1/FFFFFF?text=Banner+3', |
|
||||||
link_type: 1, |
|
||||||
content: 'https://example.com', |
|
||||||
title: '限时活动', |
|
||||||
description: '邀请好友送奖金', |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 公告数据 |
|
||||||
*/ |
|
||||||
export const mockNotices: Notice[] = [ |
|
||||||
{ |
|
||||||
id: '1', |
|
||||||
title: '系统维护通知', |
|
||||||
content: '系统将于今晚22:00-23:00进行维护,期间无法正常使用', |
|
||||||
content_type: 1, |
|
||||||
create_time: '2025-11-08 10:00:00', |
|
||||||
formatDate: '2025-11-08', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '2', |
|
||||||
title: '新游戏上线', |
|
||||||
content: '全新游戏《幸运转盘》已上线,欢迎体验', |
|
||||||
content_type: 1, |
|
||||||
create_time: '2025-11-07 15:30:00', |
|
||||||
formatDate: '2025-11-07', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '3', |
|
||||||
title: '活动规则更新', |
|
||||||
content: '详见活动页面', |
|
||||||
content_type: 3, |
|
||||||
create_time: '2025-11-06 09:00:00', |
|
||||||
formatDate: '2025-11-06', |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 游戏分类 |
|
||||||
*/ |
|
||||||
export const mockGameCategories: GameCategory[] = [ |
|
||||||
{ id: 0, key: 'recommend', name: '推荐', icon: 'star', big_type: 0 }, |
|
||||||
{ id: 1, key: 'chess', name: '棋牌', icon: 'chess', big_type: 1 }, |
|
||||||
{ id: 2, key: 'electronic', name: '电子', icon: 'game', big_type: 2 }, |
|
||||||
{ id: 3, key: 'fishing', name: '捕鱼', icon: 'fish', big_type: 3 }, |
|
||||||
{ id: 4, key: 'sports', name: '体育', icon: 'sports', big_type: 4 }, |
|
||||||
{ id: 5, key: 'lottery', name: '彩票', icon: 'lottery', big_type: 5 }, |
|
||||||
{ id: 6, key: 'live', name: '直播', icon: 'live', big_type: 6 }, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 游戏数据 |
|
||||||
*/ |
|
||||||
export const mockGames: Game[] = [ |
|
||||||
{ |
|
||||||
id: '1', |
|
||||||
play_id: 1001, |
|
||||||
play_up_name: '真人百家乐', |
|
||||||
play_cname: '标准版', |
|
||||||
icon: 'https://via.placeholder.com/100/FF6B6B/FFFFFF?text=Game+1', |
|
||||||
big_type: 1, |
|
||||||
index: 1, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '2', |
|
||||||
play_id: 1002, |
|
||||||
play_up_name: '电子老虎机', |
|
||||||
play_cname: '幸运转盘', |
|
||||||
icon: 'https://via.placeholder.com/100/4ECDC4/FFFFFF?text=Game+2', |
|
||||||
big_type: 2, |
|
||||||
index: 2, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '3', |
|
||||||
play_id: 1003, |
|
||||||
play_up_name: '捕鱼达人', |
|
||||||
play_cname: '经典版', |
|
||||||
icon: 'https://via.placeholder.com/100/45B7D1/FFFFFF?text=Game+3', |
|
||||||
big_type: 3, |
|
||||||
index: 3, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '4', |
|
||||||
play_id: 1004, |
|
||||||
play_up_name: '体育竞技', |
|
||||||
play_cname: '足球', |
|
||||||
icon: 'https://via.placeholder.com/100/96CEB4/FFFFFF?text=Game+4', |
|
||||||
big_type: 4, |
|
||||||
index: 4, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '5', |
|
||||||
play_id: 1005, |
|
||||||
play_up_name: '彩票游戏', |
|
||||||
play_cname: '双色球', |
|
||||||
icon: 'https://via.placeholder.com/100/FFEAA7/FFFFFF?text=Game+5', |
|
||||||
big_type: 5, |
|
||||||
index: 5, |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '6', |
|
||||||
play_id: 1006, |
|
||||||
play_up_name: '真人直播', |
|
||||||
play_cname: '美女荷官', |
|
||||||
icon: 'https://via.placeholder.com/100/DFE6E9/FFFFFF?text=Game+6', |
|
||||||
big_type: 6, |
|
||||||
index: 6, |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 高奖金游戏数据 |
|
||||||
*/ |
|
||||||
export const mockHighPrizeGames: HighPrizeGame[] = [ |
|
||||||
{ |
|
||||||
id: '1', |
|
||||||
play_id: 1001, |
|
||||||
play_up_name: '真人百家乐', |
|
||||||
play_cname: '标准版', |
|
||||||
icon: 'https://via.placeholder.com/100/FF6B6B/FFFFFF?text=Prize+1', |
|
||||||
big_type: 1, |
|
||||||
index: 1, |
|
||||||
payout_amount: 50000, |
|
||||||
bet_amount: 1000, |
|
||||||
odds: 50000, |
|
||||||
cust_name: '幸运用户***', |
|
||||||
avatar: 'https://via.placeholder.com/50/FF6B6B/FFFFFF?text=User', |
|
||||||
update_time: '2025-11-08 14:30:00', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '2', |
|
||||||
play_id: 1002, |
|
||||||
play_up_name: '电子老虎机', |
|
||||||
play_cname: '幸运转盘', |
|
||||||
icon: 'https://via.placeholder.com/100/4ECDC4/FFFFFF?text=Prize+2', |
|
||||||
big_type: 2, |
|
||||||
index: 2, |
|
||||||
payout_amount: 100000, |
|
||||||
bet_amount: 500, |
|
||||||
odds: 200000, |
|
||||||
cust_name: '幸运玩家***', |
|
||||||
avatar: 'https://via.placeholder.com/50/4ECDC4/FFFFFF?text=User', |
|
||||||
update_time: '2025-11-08 13:15:00', |
|
||||||
}, |
|
||||||
{ |
|
||||||
id: '3', |
|
||||||
play_id: 1003, |
|
||||||
play_up_name: '捕鱼达人', |
|
||||||
play_cname: '经典版', |
|
||||||
icon: 'https://via.placeholder.com/100/45B7D1/FFFFFF?text=Prize+3', |
|
||||||
big_type: 3, |
|
||||||
index: 3, |
|
||||||
payout_amount: 75000, |
|
||||||
bet_amount: 800, |
|
||||||
odds: 93750, |
|
||||||
cust_name: '大奖得主***', |
|
||||||
avatar: 'https://via.placeholder.com/50/45B7D1/FFFFFF?text=User', |
|
||||||
update_time: '2025-11-08 12:00:00', |
|
||||||
}, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* Mock 快速导航项 |
|
||||||
*/ |
|
||||||
export const mockNavItems: NavItem[] = [ |
|
||||||
{ id: '1', name: '充值', icon: 'recharge', action: 'recharge' }, |
|
||||||
{ id: '2', name: '提现', icon: 'withdraw', action: 'withdraw' }, |
|
||||||
{ id: '3', name: '活动', icon: 'activity', action: 'activity' }, |
|
||||||
{ id: '4', name: '客服', icon: 'service', action: 'service' }, |
|
||||||
{ id: '5', name: '帮助', icon: 'help', action: 'help' }, |
|
||||||
]; |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取 Mock 首页数据 |
|
||||||
*/ |
|
||||||
export const getMockHomePageData = () => { |
|
||||||
return { |
|
||||||
banners: mockBanners, |
|
||||||
notices: mockNotices, |
|
||||||
categories: mockGameCategories, |
|
||||||
games: mockGames, |
|
||||||
highPrizeGames: mockHighPrizeGames, |
|
||||||
navItems: mockNavItems, |
|
||||||
}; |
|
||||||
}; |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取 Mock 游戏列表(支持分类过滤) |
|
||||||
*/ |
|
||||||
export const getMockGamesByCategory = (categoryId: string | number) => { |
|
||||||
if (categoryId === 0) { |
|
||||||
return mockGames; // 推荐分类返回所有游戏
|
|
||||||
} |
|
||||||
return mockGames.filter((game) => game.big_type === categoryId); |
|
||||||
}; |
|
||||||
|
|
||||||
@ -1,267 +0,0 @@ |
|||||||
/** |
|
||||||
* 游戏状态管理 |
|
||||||
* 使用 Zustand + AsyncStorage 持久化 |
|
||||||
*/ |
|
||||||
|
|
||||||
import { create } from 'zustand'; |
|
||||||
// import { useShallow } from 'zustand/react/shallow';
|
|
||||||
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; |
|
||||||
import gameService from '@/services/gameService'; |
|
||||||
import appConfig from '@/utils/config'; |
|
||||||
import useTenantStore from '@/stores/tenantStore'; |
|
||||||
import useMsgStore from '@/stores/msgStore'; |
|
||||||
import { filter, map, concat, cloneDeep, groupBy } from 'lodash-es'; |
|
||||||
import { GameMainKeysEnum, defaultHomeGameTabMenus } from '@/constants/game'; |
|
||||||
|
|
||||||
// 状态
|
|
||||||
interface State { |
|
||||||
appLoginPopType: number; |
|
||||||
receiveAwardPopType: number; |
|
||||||
appLoginPop: boolean; |
|
||||||
menuSort: Record<string, any>[]; |
|
||||||
rebateGameSort: Record<string, any>[]; |
|
||||||
originalGames: Record<string, any>[]; |
|
||||||
blockchainGames: Record<string, any>[]; |
|
||||||
homeHotGames: Record<string, any>[]; |
|
||||||
gamesTry: Record<string, any>[]; |
|
||||||
gamesTryPlayIds: number[]; |
|
||||||
smallClassGames: Record<string, any>; |
|
||||||
gameBigClass: Record<string, any>; |
|
||||||
selectedCategory: string; // 当前选中的游戏分类
|
|
||||||
} |
|
||||||
|
|
||||||
// 操作
|
|
||||||
interface Actions { |
|
||||||
setHomePageData: (data: Record<string, any>) => void; |
|
||||||
setOriginalGames: (data: Record<string, any>[]) => void; |
|
||||||
setBlockchainGames: (data: Record<string, any>[]) => void; |
|
||||||
setGamesTry: (data: Record<string, any>[]) => void; |
|
||||||
setHomeHotGames: (data: Record<string, any>[]) => void; |
|
||||||
setSmallClassGame: (data: Record<string, any>) => void; |
|
||||||
setGameBigClass: (data: Record<string, any>) => void; |
|
||||||
setSelectedCategory: (categoryId: string) => void; // 设置选中的游戏分类
|
|
||||||
// requestHomePageData: (data?: Record<string, any>) => Promise<any>;
|
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 租户状态 Store |
|
||||||
*/ |
|
||||||
const useGameStore = create<State & Actions>()((set, get) => ({ |
|
||||||
// 初始状态
|
|
||||||
appLoginPopType: 0, |
|
||||||
receiveAwardPopType: 0, |
|
||||||
appLoginPop: false, |
|
||||||
menuSort: [], // 游戏主菜单
|
|
||||||
rebateGameSort: [], |
|
||||||
originalGames: [], // 原创游戏
|
|
||||||
blockchainGames: [], // 区块链游戏
|
|
||||||
homeHotGames: [], // 热门游戏
|
|
||||||
gamesTry: [], // 试玩游戏
|
|
||||||
gamesTryPlayIds: [], // 试玩游戏id列表
|
|
||||||
smallClassGames: {}, |
|
||||||
gameBigClass: {}, |
|
||||||
selectedCategory: '103', // 默认选中推荐分类
|
|
||||||
|
|
||||||
|
|
||||||
// 保存首页数据
|
|
||||||
setHomePageData: (data: any) => { |
|
||||||
if (data) { |
|
||||||
const { setNotices, setBanners } = useMsgStore.getState(); |
|
||||||
|
|
||||||
// 设置注册弹窗
|
|
||||||
const appLoginPopType = data.appLoginPopType || 0; |
|
||||||
const receiveAwardPopType = data.receiveAwardPopType || 0; |
|
||||||
const appLoginPop = data.appLoginPop || false; |
|
||||||
|
|
||||||
// 菜单排序
|
|
||||||
const menuSort = filter(data.version_type, item => item.sort_v !== 0 && item.state == 1).sort((a, b) => { |
|
||||||
return b.sort_v - a.sort_v; |
|
||||||
}); |
|
||||||
console.log(menuSort, 'menuSort 1'); |
|
||||||
// version_shuffle
|
|
||||||
const rebateGameSort = data.version_shuffle || []; |
|
||||||
|
|
||||||
// 所有游戏销售状态
|
|
||||||
// gameStore.setGameAllStates(res.data.gameState);
|
|
||||||
|
|
||||||
// 公告
|
|
||||||
setNotices(data.news?.data || []); |
|
||||||
|
|
||||||
// 轮播图
|
|
||||||
setBanners(data.banners?.data || []); |
|
||||||
|
|
||||||
set({ appLoginPopType, receiveAwardPopType, appLoginPop, menuSort, rebateGameSort }); |
|
||||||
|
|
||||||
// 原创游戏
|
|
||||||
get().setOriginalGames(data.originalGames); |
|
||||||
|
|
||||||
// 区块链游戏
|
|
||||||
get().setBlockchainGames(data.hsGames); |
|
||||||
|
|
||||||
// 试玩游戏
|
|
||||||
get().setGamesTry(data.gamesTry); |
|
||||||
|
|
||||||
// 首页热门游戏
|
|
||||||
get().setHomeHotGames(data.homeHotGames?.[1] || []); |
|
||||||
get().setSmallClassGame(data.homeHotGames); |
|
||||||
|
|
||||||
// 三方游戏
|
|
||||||
get().setGameBigClass(data.thirdGames); |
|
||||||
|
|
||||||
// 手动持久化
|
|
||||||
// storageManager.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data }));
|
|
||||||
} |
|
||||||
if (__DEV__) { |
|
||||||
console.log('💾 Tenant info saved:', data); |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
setOriginalGames: (list: Record<string, any>[]) => { |
|
||||||
const originalGames = map(list, (item: any) => ({ |
|
||||||
...item, |
|
||||||
isOriginal: true, |
|
||||||
})).sort((a: any, b: any) => a.hotVal - b.hotVal); |
|
||||||
set({ originalGames }); |
|
||||||
}, |
|
||||||
|
|
||||||
setBlockchainGames: (list: Record<string, any>[]) => { |
|
||||||
set({ blockchainGames: list || [] }); |
|
||||||
}, |
|
||||||
|
|
||||||
setGamesTry: (list: Record<string, any>[]) => { |
|
||||||
set({ gamesTry: list || [] }); |
|
||||||
storageManager.session.setItem(STORAGE_KEYS.GAME_TRY, list); |
|
||||||
const gamesTryPlayIds = concat(...map(list, item => map(item.subList, subItem => Number(subItem.play_id)))); |
|
||||||
set({ gamesTryPlayIds }); |
|
||||||
}, |
|
||||||
|
|
||||||
setHomeHotGames: (list: Record<string, any>[]) => { |
|
||||||
set({ homeHotGames: list || [] }); |
|
||||||
}, |
|
||||||
|
|
||||||
setSmallClassGame: (data: Record<string, any>) => { |
|
||||||
set({ smallClassGames: { |
|
||||||
[GameMainKeysEnum.HOT_ELECTRONIC]: data?.[2] || [], |
|
||||||
[GameMainKeysEnum.HOT_FISHING]: data?.[3] || [], |
|
||||||
[GameMainKeysEnum.HOT_CHESS]: data?.[5] || [], |
|
||||||
[GameMainKeysEnum.HOT_BLOCK_THIRD]: data?.[8] || [], |
|
||||||
} }); |
|
||||||
}, |
|
||||||
|
|
||||||
setGameBigClass: (data: Record<string, any>) => { |
|
||||||
const groupByType = cloneDeep(groupBy(data, item => item.big_type)); |
|
||||||
set({ gameBigClass: { |
|
||||||
[GameMainKeysEnum.CHESS]: groupByType?.[1] || [], |
|
||||||
[GameMainKeysEnum.ELECTRONIC]: groupByType?.[2] || [], |
|
||||||
[GameMainKeysEnum.FISHING]: groupByType?.[3] || [], |
|
||||||
[GameMainKeysEnum.LIVE]: groupByType?.[4] || [], |
|
||||||
[GameMainKeysEnum.SPORTS]: groupByType?.[5] || [], |
|
||||||
[GameMainKeysEnum.LOTTERY]: groupByType?.[7] || [], |
|
||||||
[GameMainKeysEnum.BLOCK_THIRD]: groupByType?.[10] || [], |
|
||||||
} }); |
|
||||||
}, |
|
||||||
|
|
||||||
setSelectedCategory: (categoryId: string) => { |
|
||||||
set({ selectedCategory: categoryId }); |
|
||||||
// 保存到 session storage,页面刷新后仍然保留
|
|
||||||
storageManager.session.setItem(STORAGE_KEYS.APP_ACTIVE_MAIN_MENU_TAB, categoryId); |
|
||||||
}, |
|
||||||
})); |
|
||||||
|
|
||||||
// 从 AsyncStorage 恢复状态的函数
|
|
||||||
export const restoreGameState = async () => { |
|
||||||
try { |
|
||||||
const stored = await storageManager.session.getItem(STORAGE_KEYS.TENANT_STORE); |
|
||||||
if (stored) { |
|
||||||
const state = JSON.parse(stored); |
|
||||||
useGameStore.setState(state); |
|
||||||
if (__DEV__) { |
|
||||||
console.log('✅ Tenant state restored from storage'); |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error('Failed to restore tenant state:', error); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
// 获取首页数据
|
|
||||||
export const requestHomePageData = async () => { |
|
||||||
try { |
|
||||||
const { tenantInfo } = useTenantStore.getState(); |
|
||||||
const params = { |
|
||||||
tid: tenantInfo?.tid, |
|
||||||
aseq: { |
|
||||||
aseq: appConfig.app.aseqId, |
|
||||||
}, |
|
||||||
version_type: { |
|
||||||
version_type: 3, // 1 是棋牌, 2 是网赚 3, 是综合彩票
|
|
||||||
}, |
|
||||||
news: { |
|
||||||
page_start: 1, |
|
||||||
num_per_page: 999, |
|
||||||
chan_con: 8, |
|
||||||
state: 1, |
|
||||||
message_id: '10,13', //10公告 13 跑马灯
|
|
||||||
sort_flag: 1, |
|
||||||
vip_level: 0, //userInfo.value.cust_level || 0,
|
|
||||||
proxy: tenantInfo?.proxy, |
|
||||||
apply: 8, // 1 棋牌;2 网赚;3 综合彩票;3 老版彩票; (apply后端设置默认为1,为可选参数)
|
|
||||||
language: 0, //Number(window.localStorage.getItem('languageNum') || '0'),
|
|
||||||
}, |
|
||||||
game_news: { |
|
||||||
page_start: 1, |
|
||||||
num_per_page: 9999, |
|
||||||
state: 1, |
|
||||||
message_id: 17, |
|
||||||
sort_flag: 1, |
|
||||||
tid: tenantInfo?.tid, |
|
||||||
proxy: tenantInfo?.proxy, |
|
||||||
chan_con: 0, |
|
||||||
apply: 8, |
|
||||||
ma_id: 10, |
|
||||||
// vip_level: userInfo.value.cust_level || 0,
|
|
||||||
vip_level: 0, |
|
||||||
type: 'MWEB', |
|
||||||
language: 0, |
|
||||||
}, |
|
||||||
banners: { |
|
||||||
page_start: 1, |
|
||||||
num_per_page: 999, |
|
||||||
chan_con: 8, |
|
||||||
state: 1, |
|
||||||
message_id: 12, |
|
||||||
sort_flag: 1, |
|
||||||
tid: tenantInfo?.tid, |
|
||||||
proxy: tenantInfo?.proxy, |
|
||||||
apply: 8, |
|
||||||
location: '1', |
|
||||||
type: 'MWEB', |
|
||||||
language: Number(window.localStorage.getItem('languageNum') || '0'), |
|
||||||
}, |
|
||||||
homeHotGames: { |
|
||||||
// cust_id: userInfo.value.cust_id || '0',
|
|
||||||
cust_id: '0', |
|
||||||
}, |
|
||||||
proxyConfig: { |
|
||||||
proxy: tenantInfo?.proxy, |
|
||||||
}, |
|
||||||
hotGames: { |
|
||||||
size: 12, |
|
||||||
}, |
|
||||||
}; |
|
||||||
const { data } = await gameService.getHomePageData(params); |
|
||||||
|
|
||||||
// ✅ 直接调用 setHomePageData action
|
|
||||||
useGameStore.getState().setHomePageData(data); |
|
||||||
|
|
||||||
if (__DEV__) { |
|
||||||
console.log('✅ Home-data info loaded:', data); |
|
||||||
} |
|
||||||
return Promise.resolve(data); |
|
||||||
} catch (error) { |
|
||||||
console.error('Failed to request home-data info:', error); |
|
||||||
return Promise.reject(error); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
export default useGameStore; |
|
||||||
@ -1,72 +0,0 @@ |
|||||||
/** |
|
||||||
* 消息状态管理 |
|
||||||
* 使用 Zustand + AsyncStorage 持久化 |
|
||||||
*/ |
|
||||||
|
|
||||||
import { create } from 'zustand'; |
|
||||||
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; |
|
||||||
import { filter } from 'lodash-es'; |
|
||||||
// import { useShallow } from 'zustand/react/shallow';
|
|
||||||
// import { tenantService } from '@/services';
|
|
||||||
|
|
||||||
/** |
|
||||||
* 状态 |
|
||||||
*/ |
|
||||||
interface State { |
|
||||||
notices: Record<string, any>[]; |
|
||||||
homeBanner: Record<string, any>[]; |
|
||||||
mineBanner: Record<string, any>[]; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 操作 |
|
||||||
*/ |
|
||||||
interface Actions { |
|
||||||
setNotices: (list: Record<string, any>[]) => void; |
|
||||||
setBanners: (list: Record<string, any>[]) => void; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 租户状态 Store |
|
||||||
*/ |
|
||||||
const useMsgStore = create<State & Actions>()((set, get) => ({ |
|
||||||
// state
|
|
||||||
notices: [], |
|
||||||
homeBanner: [], |
|
||||||
mineBanner: [], |
|
||||||
|
|
||||||
// actions
|
|
||||||
setNotices: (list: Record<string, any>[]) => { |
|
||||||
set({ notices: list }); |
|
||||||
|
|
||||||
if (__DEV__) { |
|
||||||
console.log('💾 notices saved:', list); |
|
||||||
} |
|
||||||
}, |
|
||||||
setBanners: (list: Record<string, any>[]) => { |
|
||||||
const homeBanner = filter(list, (item) => item.content1 && item.content1.includes('1')); |
|
||||||
const mineBanner = filter(list, (item) => item.content1 && item.content1.includes('2')); |
|
||||||
set({ homeBanner, mineBanner }); |
|
||||||
|
|
||||||
if (__DEV__) { |
|
||||||
console.log('💾 banners saved:', list); |
|
||||||
} |
|
||||||
}, |
|
||||||
})); |
|
||||||
|
|
||||||
// 从 AsyncStorage 恢复状态的函数
|
|
||||||
export const restoreMsgState = async () => { |
|
||||||
try { |
|
||||||
const stored = storageManager.session.getItem(STORAGE_KEYS.MSG_STORE); |
|
||||||
if (stored) { |
|
||||||
useMsgStore.setState(stored); |
|
||||||
if (__DEV__) { |
|
||||||
console.log('✅ Msg state restored from storage'); |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (error) { |
|
||||||
console.error('Failed to restore msg state:', error); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
export default useMsgStore; |
|
||||||
@ -1,121 +0,0 @@ |
|||||||
/** |
|
||||||
* 首页相关的数据类型定义 |
|
||||||
*/ |
|
||||||
|
|
||||||
/** |
|
||||||
* 轮播图数据 |
|
||||||
*/ |
|
||||||
export interface Banner { |
|
||||||
id: string; |
|
||||||
subject: string; // 图片URL
|
|
||||||
link_type: number; // 1: 外链, 2: 内部路由
|
|
||||||
content: string; // 链接内容
|
|
||||||
title?: string; |
|
||||||
description?: string; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 公告数据 |
|
||||||
*/ |
|
||||||
export interface Notice { |
|
||||||
id: string; |
|
||||||
title: string; |
|
||||||
content: string; |
|
||||||
content_type: number; // 1: 文本, 2: 图片, 3: 链接
|
|
||||||
create_time: string; |
|
||||||
formatDate?: string; |
|
||||||
is_save_pic?: boolean; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏分类 |
|
||||||
*/ |
|
||||||
export interface GameCategory { |
|
||||||
id: number; |
|
||||||
key: string; |
|
||||||
name: string; |
|
||||||
icon: string; |
|
||||||
logo?: string; |
|
||||||
big_type?: number; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏数据 |
|
||||||
*/ |
|
||||||
export interface Game { |
|
||||||
id: string; |
|
||||||
play_id: number; |
|
||||||
upper_play_id?: number; |
|
||||||
play_up_name: string; // 游戏平台名称
|
|
||||||
play_cname?: string; // 游戏子类名称
|
|
||||||
logo3_img_url?: string; // 游戏图标
|
|
||||||
icon?: string; |
|
||||||
big_type: number; // 游戏大类型
|
|
||||||
mainType?: number; |
|
||||||
index?: number; |
|
||||||
description?: string; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 高奖金游戏数据 |
|
||||||
*/ |
|
||||||
export interface HighPrizeGame extends Game { |
|
||||||
payout_amount: number; // 派彩金额
|
|
||||||
bet_amount: number; // 下注金额
|
|
||||||
odds: number; // 赔率
|
|
||||||
cust_name: string; // 用户名
|
|
||||||
avatar?: string; // 用户头像
|
|
||||||
update_time: string; // 更新时间
|
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 快速导航项 |
|
||||||
*/ |
|
||||||
export interface NavItem { |
|
||||||
id: string; |
|
||||||
name: string; |
|
||||||
icon: string; |
|
||||||
action: string; // 导航动作
|
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 首页数据响应 |
|
||||||
*/ |
|
||||||
export interface HomePageData { |
|
||||||
banners: Banner[]; |
|
||||||
notices: Notice[]; |
|
||||||
categories: GameCategory[]; |
|
||||||
games: Game[]; |
|
||||||
highPrizeGames: HighPrizeGame[]; |
|
||||||
navItems: NavItem[]; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* API 响应格式 |
|
||||||
*/ |
|
||||||
export interface ApiResponse<T = any> { |
|
||||||
code: number; |
|
||||||
message: string; |
|
||||||
data: T; |
|
||||||
type?: 'success' | 'error'; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 分页参数 |
|
||||||
*/ |
|
||||||
export interface PaginationParams { |
|
||||||
page: number; |
|
||||||
page_size: number; |
|
||||||
[key: string]: any; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 游戏列表响应 |
|
||||||
*/ |
|
||||||
export interface GameListResponse { |
|
||||||
games: Game[]; |
|
||||||
total: number; |
|
||||||
page: number; |
|
||||||
page_size: number; |
|
||||||
} |
|
||||||
|
|
||||||