Compare commits
2 Commits
230191f181
...
9ef9233797
| Author | SHA1 | Date |
|---|---|---|
|
|
9ef9233797 | 1 month ago |
|
|
b48cce06f4 | 1 month ago |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@ -0,0 +1,165 @@
|
||||
/** |
||||
* 首页 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> |
||||
); |
||||
} |
||||
@ -0,0 +1,182 @@
|
||||
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, |
||||
}; |
||||
@ -0,0 +1,6 @@
|
||||
// theme enum
|
||||
export enum ThemeEnum { |
||||
LIGHT = 'light', |
||||
DARK = 'dark', |
||||
ORANGE = 'orange', |
||||
} |
||||
@ -0,0 +1,8 @@
|
||||
// 用户注册方式
|
||||
export enum RegisterWayEnum { |
||||
SMS_REGISTRATION = 1, // 短信注册
|
||||
ACCOUNT_REGISTRATION = 2, // 账号注册
|
||||
EMAIL_REGISTRATION = 3, // 邮箱注册
|
||||
AGENT_ACCOUNT_OPENING_REGISTRATION = 4, // 代理开户注册
|
||||
BULK_ACCOUNT_OPENING_REGISTRATION = 5, // 批量开户注册
|
||||
} |
||||
@ -0,0 +1,155 @@
|
||||
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, |
||||
}; |
||||
}; |
||||
@ -0,0 +1,154 @@
|
||||
/** |
||||
* 轮播图组件 |
||||
* |
||||
* 展示首页轮播图,支持自动播放和手动滑动 |
||||
*/ |
||||
|
||||
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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { createThemeStyles } from '@/theme'; |
||||
import { Dimensions } from 'react-native'; |
||||
|
||||
const { width } = Dimensions.get('window'); |
||||
const BANNER_HEIGHT = width * 0.32534; // 保持 32.534% 的宽高比
|
||||
|
||||
/** |
||||
* 创建主题样式 |
||||
*/ |
||||
export const styles = createThemeStyles((colors) => ({ |
||||
container: { |
||||
width: '100%', |
||||
height: BANNER_HEIGHT, |
||||
backgroundColor: colors.backgroundSecondary, |
||||
borderRadius: 12, |
||||
overflow: 'hidden', |
||||
marginHorizontal: 12, |
||||
marginBottom: 12, |
||||
elevation: 3, |
||||
shadowColor: colors.cardShadow, |
||||
shadowOffset: { width: 0, height: 2 }, |
||||
shadowOpacity: 0.15, |
||||
shadowRadius: 6, |
||||
}, |
||||
scrollView: { |
||||
width: '100%', |
||||
height: '100%', |
||||
}, |
||||
image: { |
||||
width: width - 24, |
||||
height: BANNER_HEIGHT, |
||||
}, |
||||
indicatorContainer: { |
||||
position: 'absolute', |
||||
bottom: 12, |
||||
left: 0, |
||||
right: 0, |
||||
flexDirection: 'row', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
}, |
||||
indicator: { |
||||
width: 8, |
||||
height: 8, |
||||
borderRadius: 4, |
||||
backgroundColor: 'rgba(255, 255, 255, 0.5)', |
||||
marginHorizontal: 4, |
||||
}, |
||||
indicatorActive: { |
||||
width: 12, |
||||
height: 8, |
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)', |
||||
}, |
||||
loadingContainer: { |
||||
width: '100%', |
||||
height: '100%', |
||||
justifyContent: 'center', |
||||
alignItems: 'center', |
||||
backgroundColor: colors.backgroundSecondary, |
||||
}, |
||||
})); |
||||
@ -0,0 +1,126 @@
|
||||
/** |
||||
* 首页底部 Tabs 导航组件 |
||||
*/ |
||||
|
||||
import React, { useState, useCallback } from 'react'; |
||||
import { |
||||
View, |
||||
Text, |
||||
StyleSheet, |
||||
TouchableOpacity, |
||||
Dimensions, |
||||
} from 'react-native'; |
||||
import { useColorScheme } from '@/hooks'; |
||||
import { createThemeStyles } from '@/theme'; |
||||
import { Colors } from '@/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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,143 @@
|
||||
/** |
||||
* 快速底部导航组件 |
||||
* |
||||
* 深色主题特有,提供快速导航,使用真实数据 |
||||
*/ |
||||
|
||||
import React, { useState, useEffect } from 'react'; |
||||
import { |
||||
View, |
||||
Text, |
||||
StyleSheet, |
||||
TouchableOpacity, |
||||
ScrollView, |
||||
Animated, |
||||
} from 'react-native'; |
||||
import { createThemeStyles } from '@/theme'; |
||||
import { 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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,219 @@
|
||||
/** |
||||
* 游戏分类菜单组件 |
||||
* |
||||
* 展示游戏分类,支持切换,使用真实数据 |
||||
*/ |
||||
|
||||
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> |
||||
); |
||||
} |
||||
@ -0,0 +1,70 @@
|
||||
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: '', |
||||
}, |
||||
}); |
||||
@ -0,0 +1,177 @@
|
||||
/** |
||||
* 首页 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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,164 @@
|
||||
/** |
||||
* 高奖金游戏组件 |
||||
* |
||||
* 深色主题特有,展示高奖金游戏,使用真实数据 |
||||
*/ |
||||
|
||||
import React, { useState, useEffect } from 'react'; |
||||
import { |
||||
View, |
||||
Text, |
||||
StyleSheet, |
||||
ScrollView, |
||||
TouchableOpacity, |
||||
Animated, |
||||
Image, |
||||
} from 'react-native'; |
||||
import { createThemeStyles } from '@/theme'; |
||||
import { 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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,208 @@
|
||||
/** |
||||
* 游戏大厅组件 |
||||
* |
||||
* 展示游戏列表,使用真实数据 |
||||
*/ |
||||
|
||||
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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,171 @@
|
||||
/** |
||||
* 公告栏组件 |
||||
* |
||||
* 展示滚动公告,使用真实数据 |
||||
*/ |
||||
|
||||
import React, { useState, useEffect, useRef } from 'react'; |
||||
import { |
||||
View, |
||||
Text, |
||||
StyleSheet, |
||||
Animated, |
||||
TouchableOpacity, |
||||
Dimensions, |
||||
} from 'react-native'; |
||||
import { createThemeStyles } from '@/theme'; |
||||
import { 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> |
||||
); |
||||
} |
||||
|
||||
@ -0,0 +1,26 @@
|
||||
/** |
||||
* 首页组件统一导出 |
||||
* |
||||
* 所有组件都使用 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); |
||||
|
||||
@ -0,0 +1,157 @@
|
||||
/** |
||||
* 完整首页容器 |
||||
* 包含 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,6 +80,9 @@ importers:
|
||||
react-native: |
||||
specifier: 0.81.5 |
||||
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: |
||||
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]) |
||||
@ -2985,6 +2988,12 @@ packages:
|
||||
react: '*' |
||||
react-native: '*' |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==} |
||||
peerDependencies: |
||||
react: '*' |
||||
react-native: '*' |
||||
|
||||
[email protected]: |
||||
resolution: {integrity: sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==} |
||||
peerDependencies: |
||||
@ -7208,6 +7217,11 @@ snapshots:
|
||||
react: 19.1.0 |
||||
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]): |
||||
dependencies: |
||||
'@callstack/react-theme-provider': 3.0.9([email protected]) |
||||
|
||||
@ -0,0 +1,37 @@
|
||||
/** |
||||
* 游戏服务 |
||||
* 处理租户相关的 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; |
||||
@ -0,0 +1,233 @@
|
||||
/** |
||||
* 首页 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); |
||||
}; |
||||
|
||||
@ -0,0 +1,267 @@
|
||||
/** |
||||
* 游戏状态管理 |
||||
* 使用 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; |
||||
@ -0,0 +1,72 @@
|
||||
/** |
||||
* 消息状态管理 |
||||
* 使用 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; |
||||
@ -0,0 +1,121 @@
|
||||
/** |
||||
* 首页相关的数据类型定义 |
||||
*/ |
||||
|
||||
/** |
||||
* 轮播图数据 |
||||
*/ |
||||
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; |
||||
} |
||||
|
||||