43 changed files with 3147 additions and 990 deletions
@ -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,87 @@ |
|||||||
|
import { useEffect, useState, useMemo } 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'; |
||||||
|
|
||||||
|
// 有子菜单的游戏类型
|
||||||
|
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, |
||||||
|
key: `${item.key}`, |
||||||
|
}) |
||||||
|
); |
||||||
|
}, [theme, isLogin, menuSort, gameBigClass]); |
||||||
|
}; |
||||||
|
|
||||||
|
export const useMenuDataLoaded = () => useGameStore((state) => state.menuSort?.length > 0); |
||||||
@ -0,0 +1,180 @@ |
|||||||
|
/** |
||||||
|
* 完整首页容器 |
||||||
|
* 包含 Header、内容区域、BottomTabs |
||||||
|
* 支持主题切换和真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react'; |
||||||
|
import { View, ScrollView, RefreshControl, StyleSheet, SafeAreaView, Alert } from 'react-native'; |
||||||
|
import { useColorScheme } from '@/hooks'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import Header from './components/Header'; |
||||||
|
import BannerSwiper from './components/BannerSwiper'; |
||||||
|
import NoticeBar from './components/NoticeBar'; |
||||||
|
import GameMainMenus from './components/GameMainMenus'; |
||||||
|
import Lobby from './components/Lobby'; |
||||||
|
import HighPrizeGame from './components/HighPrizeGame'; |
||||||
|
import FastFootNav from './components/FastFootNav'; |
||||||
|
import { requestHomePageData } from '@/stores/gameStore'; |
||||||
|
import { useTenantLoad } from '@/stores/tenantStore'; |
||||||
|
import type { |
||||||
|
Banner, |
||||||
|
Notice, |
||||||
|
GameCategory, |
||||||
|
Game, |
||||||
|
HighPrizeGame as HighPrizeGameType, |
||||||
|
} from '@/types/home'; |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
flex: 1, |
||||||
|
backgroundColor: colors.background, |
||||||
|
}, |
||||||
|
contentContainer: { |
||||||
|
flex: 1, |
||||||
|
}, |
||||||
|
scrollContent: { |
||||||
|
paddingBottom: 20, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface HomeScreenCompleteProps { |
||||||
|
theme?: 'light' | 'dark'; |
||||||
|
isDarkTheme?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 完整首页容器 |
||||||
|
*/ |
||||||
|
export default function HomeScreenComplete({ |
||||||
|
theme = 'light', |
||||||
|
isDarkTheme = false, |
||||||
|
}: HomeScreenCompleteProps) { |
||||||
|
const colorScheme = useColorScheme(); |
||||||
|
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme; |
||||||
|
const s = styles[actualTheme]; |
||||||
|
|
||||||
|
const [refreshing, setRefreshing] = useState(false); |
||||||
|
const [selectedCategory, setSelectedCategory] = useState<number>(0); |
||||||
|
const tenantLoad = useTenantLoad(); |
||||||
|
|
||||||
|
// 加载首页数据
|
||||||
|
const loadHomePageData = useCallback(async () => { |
||||||
|
try { |
||||||
|
await requestHomePageData(); |
||||||
|
} catch (error) { |
||||||
|
console.error('加载首页数据失败:', error); |
||||||
|
} |
||||||
|
}, []); |
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
useEffect(() => { |
||||||
|
console.log('租户数据加载完成:', tenantLoad); |
||||||
|
if (tenantLoad) { |
||||||
|
loadHomePageData(); |
||||||
|
} |
||||||
|
}, [loadHomePageData, tenantLoad]); |
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const handleRefresh = useCallback(async () => { |
||||||
|
setRefreshing(true); |
||||||
|
try { |
||||||
|
await loadHomePageData(); |
||||||
|
} finally { |
||||||
|
setRefreshing(false); |
||||||
|
} |
||||||
|
}, [loadHomePageData]); |
||||||
|
|
||||||
|
// 处理分类选择
|
||||||
|
const handleCategorySelect = useCallback((categoryId: number) => { |
||||||
|
setSelectedCategory(categoryId); |
||||||
|
// 这里可以根据分类过滤游戏
|
||||||
|
}, []); |
||||||
|
|
||||||
|
// 处理游戏点击
|
||||||
|
const handleGamePress = useCallback((game: Game) => { |
||||||
|
Alert.alert('游戏', `点击了: ${game.play_up_name}`); |
||||||
|
// 这里可以添加打开游戏的逻辑
|
||||||
|
}, []); |
||||||
|
|
||||||
|
// 处理底部 Tab 点击
|
||||||
|
const handleTabPress = useCallback((tabId: string, action: string) => { |
||||||
|
Alert.alert('导航', `点击了: ${tabId}`); |
||||||
|
// 这里可以添加导航逻辑
|
||||||
|
}, []); |
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = useCallback((keyword: string) => { |
||||||
|
Alert.alert('搜索', `搜索关键词: ${keyword}`); |
||||||
|
// 这里可以添加搜索逻辑
|
||||||
|
}, []); |
||||||
|
|
||||||
|
// 根据主题选择要显示的组件
|
||||||
|
const renderContent = () => { |
||||||
|
if (isDarkTheme || actualTheme === 'dark') { |
||||||
|
// 深色主题布局
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<GameMainMenus |
||||||
|
theme={actualTheme} |
||||||
|
selectedCategory={selectedCategory} |
||||||
|
onCategorySelect={handleCategorySelect} |
||||||
|
/> |
||||||
|
<BannerSwiper theme={actualTheme} /> |
||||||
|
<NoticeBar theme={actualTheme} /> |
||||||
|
<HighPrizeGame theme={actualTheme} onGamePress={handleGamePress} /> |
||||||
|
<Lobby theme={actualTheme} onGamePress={handleGamePress} /> |
||||||
|
<FastFootNav theme={actualTheme} onTabPress={handleTabPress} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
} else { |
||||||
|
// 浅色主题布局
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<BannerSwiper theme={actualTheme} /> |
||||||
|
<NoticeBar theme={actualTheme} /> |
||||||
|
<GameMainMenus |
||||||
|
theme={actualTheme} |
||||||
|
selectedCategory={selectedCategory} |
||||||
|
onCategorySelect={handleCategorySelect} |
||||||
|
/> |
||||||
|
<Lobby theme={actualTheme} onGamePress={handleGamePress} /> |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<SafeAreaView style={s.container}> |
||||||
|
{/* Header */} |
||||||
|
<Header |
||||||
|
theme={actualTheme} |
||||||
|
onSearch={handleSearch} |
||||||
|
onMessagePress={() => Alert.alert('消息', '消息功能')} |
||||||
|
onUserPress={() => Alert.alert('用户', '用户中心')} |
||||||
|
unreadCount={3} |
||||||
|
/> |
||||||
|
|
||||||
|
{/* 内容区域 */} |
||||||
|
<View style={s.contentContainer}> |
||||||
|
<ScrollView |
||||||
|
style={s.contentContainer} |
||||||
|
refreshControl={ |
||||||
|
<RefreshControl |
||||||
|
refreshing={refreshing} |
||||||
|
onRefresh={handleRefresh} |
||||||
|
tintColor={Colors[actualTheme].primary} |
||||||
|
/> |
||||||
|
} |
||||||
|
showsVerticalScrollIndicator={false} |
||||||
|
> |
||||||
|
<View style={s.scrollContent}>{renderContent()}</View> |
||||||
|
</ScrollView> |
||||||
|
</View> |
||||||
|
</SafeAreaView> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,156 @@ |
|||||||
|
/** |
||||||
|
* 轮播图组件 |
||||||
|
* |
||||||
|
* 展示首页轮播图,支持自动播放和手动滑动 |
||||||
|
* 使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Image, |
||||||
|
TouchableOpacity, |
||||||
|
ScrollView, |
||||||
|
NativeScrollEvent, |
||||||
|
NativeSyntheticEvent, |
||||||
|
ActivityIndicator, |
||||||
|
Dimensions, |
||||||
|
Alert, |
||||||
|
} from 'react-native'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
// import type { Banner } from '@/types/home';
|
||||||
|
import { styles } from './styles'; |
||||||
|
import useMsgStore from '@/stores/msgStore'; |
||||||
|
|
||||||
|
|
||||||
|
interface BannerSwiperProps { |
||||||
|
theme: 'light' | 'dark'; |
||||||
|
} |
||||||
|
|
||||||
|
const { width } = Dimensions.get('window'); |
||||||
|
|
||||||
|
/** |
||||||
|
* 轮播图组件 |
||||||
|
*/ |
||||||
|
export default function BannerSwiper({ theme }: BannerSwiperProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const [currentIndex, setCurrentIndex] = useState(0); |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
const scrollViewRef = useRef<ScrollView>(null); |
||||||
|
const autoPlayTimerRef = useRef<any>(null); |
||||||
|
const { homeBanner } = useMsgStore(); |
||||||
|
|
||||||
|
|
||||||
|
// 加载轮播图数据
|
||||||
|
useEffect(() => { |
||||||
|
// 如果有传入的 banners 数据,直接使用
|
||||||
|
if (homeBanner.length > 0) { |
||||||
|
setLoading(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
// 如果没有数据,保持 loading 状态显示骨架屏
|
||||||
|
}, [homeBanner]); |
||||||
|
|
||||||
|
// 处理 Banner 点击
|
||||||
|
const onBannerPress = useCallback((banner: Record<string, any>) => { |
||||||
|
Alert.alert('轮播图', `点击了: ${banner.title || banner.id}`); |
||||||
|
// 这里可以添加导航逻辑
|
||||||
|
}, []); |
||||||
|
|
||||||
|
// 处理滚动事件
|
||||||
|
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => { |
||||||
|
const contentOffsetX = event.nativeEvent.contentOffset.x; |
||||||
|
const index = Math.round(contentOffsetX / (width - 24)); |
||||||
|
setCurrentIndex(Math.min(index, homeBanner.length - 1)); |
||||||
|
}; |
||||||
|
|
||||||
|
// 启动自动播放
|
||||||
|
const startAutoPlay = useCallback(() => { |
||||||
|
if (homeBanner.length <= 1) return; |
||||||
|
autoPlayTimerRef.current = setInterval(() => { |
||||||
|
setCurrentIndex((prev) => { |
||||||
|
const nextIndex = (prev + 1) % homeBanner.length; |
||||||
|
scrollViewRef.current?.scrollTo({ |
||||||
|
x: nextIndex * (width - 24), |
||||||
|
animated: true, |
||||||
|
}); |
||||||
|
return nextIndex; |
||||||
|
}); |
||||||
|
}, 5000); |
||||||
|
}, [homeBanner.length]); |
||||||
|
|
||||||
|
// 停止自动播放
|
||||||
|
const stopAutoPlay = useCallback(() => { |
||||||
|
if (autoPlayTimerRef.current) { |
||||||
|
clearInterval(autoPlayTimerRef.current); |
||||||
|
} |
||||||
|
}, []); |
||||||
|
|
||||||
|
// 自动播放
|
||||||
|
useEffect(() => { |
||||||
|
if (!loading && homeBanner.length > 0) { |
||||||
|
startAutoPlay(); |
||||||
|
} |
||||||
|
return () => stopAutoPlay(); |
||||||
|
}, [loading, homeBanner.length, startAutoPlay, stopAutoPlay]); |
||||||
|
|
||||||
|
// 骨架屏 - 加载中显示占位符
|
||||||
|
if (loading || homeBanner.length === 0) { |
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<View |
||||||
|
style={[ |
||||||
|
s.image, |
||||||
|
{ |
||||||
|
backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0', |
||||||
|
}, |
||||||
|
]} |
||||||
|
/> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<ScrollView |
||||||
|
ref={scrollViewRef} |
||||||
|
style={s.scrollView} |
||||||
|
horizontal |
||||||
|
pagingEnabled |
||||||
|
scrollEventThrottle={16} |
||||||
|
onScroll={handleScroll} |
||||||
|
showsHorizontalScrollIndicator={false} |
||||||
|
onMomentumScrollBegin={stopAutoPlay} |
||||||
|
onMomentumScrollEnd={startAutoPlay} |
||||||
|
> |
||||||
|
{homeBanner.map((banner) => ( |
||||||
|
<TouchableOpacity |
||||||
|
key={banner.id} |
||||||
|
onPress={() => onBannerPress(banner)} |
||||||
|
activeOpacity={0.9} |
||||||
|
> |
||||||
|
<Image |
||||||
|
source={{ uri: banner.subject }} |
||||||
|
style={s.image} |
||||||
|
resizeMode="cover" |
||||||
|
/> |
||||||
|
</TouchableOpacity> |
||||||
|
))} |
||||||
|
</ScrollView> |
||||||
|
|
||||||
|
{/* 指示器 */} |
||||||
|
<View style={s.indicatorContainer}> |
||||||
|
{homeBanner.map((_, index) => ( |
||||||
|
<View |
||||||
|
key={index} |
||||||
|
style={[ |
||||||
|
s.indicator, |
||||||
|
index === currentIndex && s.indicatorActive, |
||||||
|
]} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -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 '@/constants/Colors'; |
||||||
|
|
||||||
|
const { width } = Dimensions.get('window'); |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.card, |
||||||
|
borderTopWidth: 1, |
||||||
|
borderTopColor: colors.border, |
||||||
|
flexDirection: 'row', |
||||||
|
justifyContent: 'space-around', |
||||||
|
alignItems: 'center', |
||||||
|
paddingBottom: 8, |
||||||
|
paddingTop: 8, |
||||||
|
}, |
||||||
|
tabItem: { |
||||||
|
flex: 1, |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center', |
||||||
|
paddingVertical: 8, |
||||||
|
}, |
||||||
|
tabIcon: { |
||||||
|
fontSize: 24, |
||||||
|
marginBottom: 4, |
||||||
|
}, |
||||||
|
tabLabel: { |
||||||
|
fontSize: 12, |
||||||
|
color: colors.text + '80', |
||||||
|
fontWeight: '500', |
||||||
|
}, |
||||||
|
tabLabelActive: { |
||||||
|
color: colors.primary, |
||||||
|
fontWeight: '600', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface TabItem { |
||||||
|
id: string; |
||||||
|
label: string; |
||||||
|
icon: string; |
||||||
|
action: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface BottomTabsProps { |
||||||
|
theme?: 'light' | 'dark'; |
||||||
|
activeTab?: string; |
||||||
|
onTabPress?: (tabId: string, action: string) => void; |
||||||
|
items?: TabItem[]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 默认 Tab 项 |
||||||
|
*/ |
||||||
|
const DEFAULT_TABS: TabItem[] = [ |
||||||
|
{ id: 'recharge', label: '充值', icon: '💰', action: 'recharge' }, |
||||||
|
{ id: 'withdraw', label: '提现', icon: '💳', action: 'withdraw' }, |
||||||
|
{ id: 'activity', label: '活动', icon: '🎉', action: 'activity' }, |
||||||
|
{ id: 'service', label: '客服', icon: '🎧', action: 'service' }, |
||||||
|
{ id: 'help', label: '帮助', icon: '❓', action: 'help' }, |
||||||
|
]; |
||||||
|
|
||||||
|
/** |
||||||
|
* 底部 Tabs 导航组件 |
||||||
|
*/ |
||||||
|
export default function BottomTabs({ |
||||||
|
theme = 'light', |
||||||
|
activeTab = 'recharge', |
||||||
|
onTabPress, |
||||||
|
items = DEFAULT_TABS, |
||||||
|
}: BottomTabsProps) { |
||||||
|
const colorScheme = useColorScheme(); |
||||||
|
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme; |
||||||
|
const s = styles[actualTheme]; |
||||||
|
const colors = Colors[actualTheme]; |
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState(activeTab); |
||||||
|
|
||||||
|
const handleTabPress = useCallback( |
||||||
|
(tabId: string, action: string) => { |
||||||
|
setSelectedTab(tabId); |
||||||
|
onTabPress?.(tabId, action); |
||||||
|
}, |
||||||
|
[onTabPress] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
{items.map((item) => ( |
||||||
|
<TouchableOpacity |
||||||
|
key={item.id} |
||||||
|
style={s.tabItem} |
||||||
|
onPress={() => handleTabPress(item.id, item.action)} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text style={s.tabIcon}>{item.icon}</Text> |
||||||
|
<Text |
||||||
|
style={[ |
||||||
|
s.tabLabel, |
||||||
|
selectedTab === item.id && s.tabLabelActive, |
||||||
|
]} |
||||||
|
> |
||||||
|
{item.label} |
||||||
|
</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
))} |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,143 @@ |
|||||||
|
/** |
||||||
|
* 快速底部导航组件 |
||||||
|
* |
||||||
|
* 深色主题特有,提供快速导航,使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Text, |
||||||
|
StyleSheet, |
||||||
|
TouchableOpacity, |
||||||
|
ScrollView, |
||||||
|
Animated, |
||||||
|
} from 'react-native'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import { mockNavItems } from '@/services/mockHomeService'; |
||||||
|
import type { NavItem } from '@/types/home'; |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.backgroundSecondary, |
||||||
|
paddingVertical: 12, |
||||||
|
paddingHorizontal: 12, |
||||||
|
borderTopWidth: 1, |
||||||
|
borderTopColor: colors.border, |
||||||
|
}, |
||||||
|
scrollView: { |
||||||
|
paddingHorizontal: 0, |
||||||
|
}, |
||||||
|
navItem: { |
||||||
|
paddingHorizontal: 14, |
||||||
|
paddingVertical: 10, |
||||||
|
marginRight: 10, |
||||||
|
borderRadius: 8, |
||||||
|
backgroundColor: colors.background, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
minWidth: 75, |
||||||
|
elevation: 2, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 1 }, |
||||||
|
shadowOpacity: 0.1, |
||||||
|
shadowRadius: 2, |
||||||
|
}, |
||||||
|
navItemActive: { |
||||||
|
backgroundColor: colors.primary, |
||||||
|
}, |
||||||
|
navIcon: { |
||||||
|
fontSize: 22, |
||||||
|
marginBottom: 4, |
||||||
|
}, |
||||||
|
navText: { |
||||||
|
fontSize: 12, |
||||||
|
color: colors.text, |
||||||
|
textAlign: 'center', |
||||||
|
fontWeight: '500', |
||||||
|
}, |
||||||
|
navTextActive: { |
||||||
|
color: '#FFFFFF', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface FastFootNavProps { |
||||||
|
theme: 'light' | 'dark'; |
||||||
|
items?: NavItem[]; |
||||||
|
onTabPress?: (tabId: string, action: string) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 快速底部导航组件 |
||||||
|
*/ |
||||||
|
export default function FastFootNav({ theme, items: propItems, onTabPress }: FastFootNavProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const [items, setItems] = useState<NavItem[]>(propItems || []); |
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null); |
||||||
|
|
||||||
|
// 加载导航项数据
|
||||||
|
useEffect(() => { |
||||||
|
if (propItems && propItems.length > 0) { |
||||||
|
setItems(propItems); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const loadItems = async () => { |
||||||
|
try { |
||||||
|
// const data = await getNavItems();
|
||||||
|
// setItems(data.length > 0 ? data : mockNavItems);
|
||||||
|
} catch (error) { |
||||||
|
console.error('加载导航项失败:', error); |
||||||
|
setItems(mockNavItems); |
||||||
|
} |
||||||
|
}; |
||||||
|
loadItems(); |
||||||
|
}, [propItems]); |
||||||
|
|
||||||
|
const handleNavPress = (item: NavItem) => { |
||||||
|
setSelectedId(item.id); |
||||||
|
onTabPress?.(item.id, item.action); |
||||||
|
}; |
||||||
|
|
||||||
|
if (items.length === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<ScrollView |
||||||
|
style={s.scrollView} |
||||||
|
horizontal |
||||||
|
showsHorizontalScrollIndicator={false} |
||||||
|
scrollEventThrottle={16} |
||||||
|
> |
||||||
|
{items.map((item) => ( |
||||||
|
<TouchableOpacity |
||||||
|
key={item.id} |
||||||
|
style={[ |
||||||
|
s.navItem, |
||||||
|
selectedId === item.id && s.navItemActive, |
||||||
|
]} |
||||||
|
onPress={() => handleNavPress(item)} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text style={s.navIcon}>{item.icon || '🎮'}</Text> |
||||||
|
<Text |
||||||
|
style={[ |
||||||
|
s.navText, |
||||||
|
selectedId === item.id && s.navTextActive, |
||||||
|
]} |
||||||
|
> |
||||||
|
{item.name} |
||||||
|
</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
))} |
||||||
|
</ScrollView> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,112 @@ |
|||||||
|
/** |
||||||
|
* 游戏分类菜单组件 |
||||||
|
* |
||||||
|
* 展示游戏分类,支持切换,使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react'; |
||||||
|
import { View, Text, ScrollView, TouchableOpacity, Animated } from 'react-native'; |
||||||
|
// import type { GameCategory } from '@/types/home';
|
||||||
|
import { styles } from './styles'; |
||||||
|
import { useGameMainMenus, useMenuDataLoaded } from '@/hooks/useGameMenus'; |
||||||
|
// import useGameStore from '@/stores/gameStore';
|
||||||
|
import { ThemeEnum } from '@/constants/theme'; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
interface GameMainMenuProps { |
||||||
|
theme: ThemeEnum; |
||||||
|
selectedCategory?: string; |
||||||
|
onCategorySelect?: (categoryId: string) => void; |
||||||
|
topHeight?: number; |
||||||
|
showSubMenus?: boolean; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 游戏分类菜单组件 |
||||||
|
*/ |
||||||
|
export default function GameMainMenu({ |
||||||
|
theme, |
||||||
|
selectedCategory = '103', |
||||||
|
onCategorySelect, |
||||||
|
topHeight = 0, |
||||||
|
showSubMenus = true, |
||||||
|
}: GameMainMenuProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const scrollViewRef = useRef<ScrollView>(null); |
||||||
|
const gameMenus = useGameMainMenus(theme); |
||||||
|
|
||||||
|
// 检查数据加载完成
|
||||||
|
const isDataLoaded = useMenuDataLoaded(); |
||||||
|
|
||||||
|
// 使用 useMemo 缓存找到的索引,避免每次都重新计算
|
||||||
|
const selectedIndex = useMemo(() => { |
||||||
|
return gameMenus.findIndex((cat) => cat.key === selectedCategory); |
||||||
|
}, [selectedCategory, gameMenus]); |
||||||
|
|
||||||
|
// 当分类改变时,滚动到该分类
|
||||||
|
useEffect(() => { |
||||||
|
if (selectedIndex >= 0) { |
||||||
|
scrollViewRef.current?.scrollTo({ |
||||||
|
x: selectedIndex * 100, |
||||||
|
animated: true, |
||||||
|
}); |
||||||
|
} |
||||||
|
}, [selectedIndex]); |
||||||
|
|
||||||
|
// 使用 useCallback 稳定 onPress 回调
|
||||||
|
const handleCategoryPress = useCallback((categoryKey: string) => { |
||||||
|
onCategorySelect?.(categoryKey); |
||||||
|
}, [onCategorySelect]); |
||||||
|
|
||||||
|
// 骨架屏 - 显示加载中的占位符
|
||||||
|
const renderSkeleton = () => ( |
||||||
|
<View style={s.container}> |
||||||
|
<ScrollView |
||||||
|
style={s.scrollView} |
||||||
|
horizontal |
||||||
|
showsHorizontalScrollIndicator={false} |
||||||
|
scrollEventThrottle={16} |
||||||
|
> |
||||||
|
{[1, 2, 3, 4, 5].map((index) => ( |
||||||
|
<View |
||||||
|
key={`skeleton-${index}`} |
||||||
|
style={[s.categoryItem, { backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0' }]} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</ScrollView> |
||||||
|
</View> |
||||||
|
); |
||||||
|
|
||||||
|
// 如果动态数据还未加载,显示骨架屏
|
||||||
|
if (!isDataLoaded) { |
||||||
|
return renderSkeleton(); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<ScrollView |
||||||
|
ref={scrollViewRef} |
||||||
|
style={s.scrollView} |
||||||
|
horizontal |
||||||
|
showsHorizontalScrollIndicator={false} |
||||||
|
scrollEventThrottle={16} |
||||||
|
> |
||||||
|
{gameMenus.map((menu) => ( |
||||||
|
<TouchableOpacity |
||||||
|
key={menu.key} |
||||||
|
style={[s.menuItem, selectedCategory === menu.key && s.menuItemActive]} |
||||||
|
onPress={() => handleCategoryPress(menu.key)} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text |
||||||
|
style={[s.menuText, selectedCategory === menu.key && s.menuTextActive]} |
||||||
|
> |
||||||
|
{menu.icon || '🎮'} {menu.name} |
||||||
|
</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
))} |
||||||
|
</ScrollView> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
@ -0,0 +1,47 @@ |
|||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import { Dimensions } from 'react-native'; |
||||||
|
|
||||||
|
// const { width } = Dimensions.get('window');
|
||||||
|
// const BANNER_HEIGHT = width * 0.32534; // 保持 32.534% 的宽高比
|
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
export const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.background, |
||||||
|
paddingVertical: 10, |
||||||
|
borderBottomWidth: 1, |
||||||
|
borderBottomColor: colors.border, |
||||||
|
}, |
||||||
|
scrollView: { |
||||||
|
paddingHorizontal: 12, |
||||||
|
}, |
||||||
|
menuItem: { |
||||||
|
paddingHorizontal: 16, |
||||||
|
paddingVertical: 10, |
||||||
|
marginRight: 8, |
||||||
|
borderRadius: 22, |
||||||
|
backgroundColor: colors.backgroundSecondary, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
elevation: 1, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 1 }, |
||||||
|
shadowOpacity: 0.1, |
||||||
|
shadowRadius: 2, |
||||||
|
}, |
||||||
|
menuItemActive: { |
||||||
|
backgroundColor: colors.primary, |
||||||
|
elevation: 2, |
||||||
|
shadowOpacity: 0.15, |
||||||
|
}, |
||||||
|
menuText: { |
||||||
|
fontSize: 13, |
||||||
|
color: colors.text, |
||||||
|
fontWeight: '600', |
||||||
|
}, |
||||||
|
menuTextActive: { |
||||||
|
color: '#FFFFFF', |
||||||
|
}, |
||||||
|
})); |
||||||
@ -0,0 +1,180 @@ |
|||||||
|
/** |
||||||
|
* 首页 Header 组件 |
||||||
|
* 包含搜索、用户信息、消息等功能 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Text, |
||||||
|
StyleSheet, |
||||||
|
TouchableOpacity, |
||||||
|
TextInput, |
||||||
|
Image, |
||||||
|
Dimensions, |
||||||
|
} from 'react-native'; |
||||||
|
import { useColorScheme } from '@/hooks'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
|
||||||
|
const { width } = Dimensions.get('window'); |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.background, |
||||||
|
paddingHorizontal: 12, |
||||||
|
paddingVertical: 8, |
||||||
|
borderBottomWidth: 1, |
||||||
|
borderBottomColor: colors.border, |
||||||
|
}, |
||||||
|
header: { |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'space-between', |
||||||
|
paddingVertical: 8, |
||||||
|
}, |
||||||
|
logo: { |
||||||
|
fontSize: 18, |
||||||
|
fontWeight: '600', |
||||||
|
color: colors.primary, |
||||||
|
}, |
||||||
|
searchContainer: { |
||||||
|
flex: 1, |
||||||
|
marginHorizontal: 12, |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: 'center', |
||||||
|
backgroundColor: colors.card, |
||||||
|
borderRadius: 20, |
||||||
|
paddingHorizontal: 12, |
||||||
|
height: 36, |
||||||
|
}, |
||||||
|
searchInput: { |
||||||
|
flex: 1, |
||||||
|
marginLeft: 8, |
||||||
|
fontSize: 14, |
||||||
|
color: colors.text, |
||||||
|
}, |
||||||
|
searchPlaceholder: { |
||||||
|
color: colors.text + '80', |
||||||
|
}, |
||||||
|
iconButton: { |
||||||
|
width: 36, |
||||||
|
height: 36, |
||||||
|
borderRadius: 18, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
marginLeft: 8, |
||||||
|
}, |
||||||
|
badge: { |
||||||
|
position: 'absolute', |
||||||
|
top: -4, |
||||||
|
right: -4, |
||||||
|
backgroundColor: colors.primary, |
||||||
|
borderRadius: 8, |
||||||
|
minWidth: 16, |
||||||
|
height: 16, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
}, |
||||||
|
badgeText: { |
||||||
|
color: '#fff', |
||||||
|
fontSize: 10, |
||||||
|
fontWeight: '600', |
||||||
|
textAlign: 'center', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface HeaderProps { |
||||||
|
theme?: 'light' | 'dark'; |
||||||
|
onSearch?: (keyword: string) => void; |
||||||
|
onMessagePress?: () => void; |
||||||
|
onUserPress?: () => void; |
||||||
|
unreadCount?: number; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Header 组件 |
||||||
|
*/ |
||||||
|
export default function Header({ |
||||||
|
theme = 'light', |
||||||
|
onSearch, |
||||||
|
onMessagePress, |
||||||
|
onUserPress, |
||||||
|
unreadCount = 0, |
||||||
|
}: HeaderProps) { |
||||||
|
const colorScheme = useColorScheme(); |
||||||
|
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme; |
||||||
|
const s = styles[actualTheme]; |
||||||
|
const colors = Colors[actualTheme]; |
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState(''); |
||||||
|
const [isSearching, setIsSearching] = useState(false); |
||||||
|
|
||||||
|
const handleSearch = useCallback(() => { |
||||||
|
if (searchText.trim()) { |
||||||
|
onSearch?.(searchText); |
||||||
|
} |
||||||
|
}, [searchText, onSearch]); |
||||||
|
|
||||||
|
const handleClearSearch = useCallback(() => { |
||||||
|
setSearchText(''); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
{/* 顶部栏 */} |
||||||
|
<View style={s.header}> |
||||||
|
{/* Logo */} |
||||||
|
<Text style={s.logo}>🎮 游戏大厅</Text> |
||||||
|
|
||||||
|
{/* 搜索框 */} |
||||||
|
<View style={s.searchContainer}> |
||||||
|
<Text style={{ color: colors.text + '60', fontSize: 16 }}>🔍</Text> |
||||||
|
<TextInput |
||||||
|
style={[s.searchInput, s.searchPlaceholder]} |
||||||
|
placeholder="搜索游戏..." |
||||||
|
placeholderTextColor={colors.text + '60'} |
||||||
|
value={searchText} |
||||||
|
onChangeText={setSearchText} |
||||||
|
onSubmitEditing={handleSearch} |
||||||
|
returnKeyType="search" |
||||||
|
/> |
||||||
|
{searchText ? ( |
||||||
|
<TouchableOpacity onPress={handleClearSearch}> |
||||||
|
<Text style={{ fontSize: 16 }}>✕</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
) : null} |
||||||
|
</View> |
||||||
|
|
||||||
|
{/* 消息按钮 */} |
||||||
|
<TouchableOpacity |
||||||
|
style={s.iconButton} |
||||||
|
onPress={onMessagePress} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text style={{ fontSize: 18 }}>💬</Text> |
||||||
|
{unreadCount > 0 && ( |
||||||
|
<View style={s.badge}> |
||||||
|
<Text style={s.badgeText}> |
||||||
|
{unreadCount > 99 ? '99+' : unreadCount} |
||||||
|
</Text> |
||||||
|
</View> |
||||||
|
)} |
||||||
|
</TouchableOpacity> |
||||||
|
|
||||||
|
{/* 用户按钮 */} |
||||||
|
<TouchableOpacity |
||||||
|
style={s.iconButton} |
||||||
|
onPress={onUserPress} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text style={{ fontSize: 18 }}>👤</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
</View> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,164 @@ |
|||||||
|
/** |
||||||
|
* 高奖金游戏组件 |
||||||
|
* |
||||||
|
* 深色主题特有,展示高奖金游戏,使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Text, |
||||||
|
StyleSheet, |
||||||
|
ScrollView, |
||||||
|
TouchableOpacity, |
||||||
|
Animated, |
||||||
|
Image, |
||||||
|
} from 'react-native'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import { mockHighPrizeGames } from '@/services/mockHomeService'; |
||||||
|
import type { HighPrizeGame as HighPrizeGameType } from '@/types/home'; |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.background, |
||||||
|
paddingVertical: 12, |
||||||
|
paddingHorizontal: 12, |
||||||
|
marginBottom: 12, |
||||||
|
}, |
||||||
|
title: { |
||||||
|
fontSize: 15, |
||||||
|
fontWeight: 'bold', |
||||||
|
color: colors.text, |
||||||
|
marginBottom: 10, |
||||||
|
}, |
||||||
|
scrollView: { |
||||||
|
paddingHorizontal: 0, |
||||||
|
}, |
||||||
|
gameItem: { |
||||||
|
width: 110, |
||||||
|
height: 110, |
||||||
|
marginRight: 10, |
||||||
|
borderRadius: 12, |
||||||
|
backgroundColor: colors.card, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
overflow: 'hidden', |
||||||
|
elevation: 3, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 2 }, |
||||||
|
shadowOpacity: 0.15, |
||||||
|
shadowRadius: 4, |
||||||
|
}, |
||||||
|
gameIcon: { |
||||||
|
fontSize: 44, |
||||||
|
marginBottom: 4, |
||||||
|
}, |
||||||
|
gameName: { |
||||||
|
fontSize: 12, |
||||||
|
color: colors.text, |
||||||
|
textAlign: 'center', |
||||||
|
fontWeight: '500', |
||||||
|
}, |
||||||
|
prizeTag: { |
||||||
|
position: 'absolute', |
||||||
|
top: 6, |
||||||
|
right: 6, |
||||||
|
backgroundColor: colors.error, |
||||||
|
paddingHorizontal: 6, |
||||||
|
paddingVertical: 3, |
||||||
|
borderRadius: 4, |
||||||
|
elevation: 2, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 1 }, |
||||||
|
shadowOpacity: 0.2, |
||||||
|
shadowRadius: 2, |
||||||
|
}, |
||||||
|
prizeText: { |
||||||
|
fontSize: 10, |
||||||
|
color: '#FFFFFF', |
||||||
|
fontWeight: 'bold', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface HighPrizeGameProps { |
||||||
|
theme: 'light' | 'dark'; |
||||||
|
games?: HighPrizeGameType[]; |
||||||
|
onGamePress?: (game: HighPrizeGameType) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 高奖金游戏组件 |
||||||
|
*/ |
||||||
|
export default function HighPrizeGame({ theme, games: propGames, onGamePress }: HighPrizeGameProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const [games, setGames] = useState<HighPrizeGameType[]>(propGames || []); |
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null); |
||||||
|
|
||||||
|
// 加载高奖金游戏数据
|
||||||
|
useEffect(() => { |
||||||
|
if (propGames && propGames.length > 0) { |
||||||
|
setGames(propGames); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const loadGames = async () => { |
||||||
|
try { |
||||||
|
// const data = await getHighPrizeGames();
|
||||||
|
// setGames(data.length > 0 ? data : mockHighPrizeGames);
|
||||||
|
} catch (error) { |
||||||
|
console.error('加载高奖金游戏失败:', error); |
||||||
|
setGames(mockHighPrizeGames); |
||||||
|
} |
||||||
|
}; |
||||||
|
loadGames(); |
||||||
|
}, [propGames]); |
||||||
|
|
||||||
|
if (games.length === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<Text style={s.title}>🏆 实时爆奖</Text> |
||||||
|
<ScrollView |
||||||
|
style={s.scrollView} |
||||||
|
horizontal |
||||||
|
showsHorizontalScrollIndicator={false} |
||||||
|
scrollEventThrottle={16} |
||||||
|
> |
||||||
|
{games.map((game) => ( |
||||||
|
<TouchableOpacity |
||||||
|
key={game.id} |
||||||
|
style={s.gameItem} |
||||||
|
onPress={() => { |
||||||
|
setSelectedId(game.id); |
||||||
|
onGamePress?.(game); |
||||||
|
}} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
{game.icon ? ( |
||||||
|
<Image |
||||||
|
source={{ uri: game.icon }} |
||||||
|
style={{ width: '100%', height: '100%' }} |
||||||
|
resizeMode="cover" |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<Text style={s.gameIcon}>🎰</Text> |
||||||
|
)} |
||||||
|
<Text style={s.gameName} numberOfLines={2}> |
||||||
|
{game.play_up_name} |
||||||
|
</Text> |
||||||
|
<View style={s.prizeTag}> |
||||||
|
<Text style={s.prizeText}>¥{Math.floor(game.payout_amount / 1000)}k</Text> |
||||||
|
</View> |
||||||
|
</TouchableOpacity> |
||||||
|
))} |
||||||
|
</ScrollView> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,209 @@ |
|||||||
|
/** |
||||||
|
* 游戏大厅组件 |
||||||
|
* |
||||||
|
* 展示游戏列表,使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Text, |
||||||
|
StyleSheet, |
||||||
|
FlatList, |
||||||
|
TouchableOpacity, |
||||||
|
Image, |
||||||
|
ActivityIndicator, |
||||||
|
Dimensions, |
||||||
|
} from 'react-native'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import { getMockGamesByCategory } from '@/services/mockHomeService'; |
||||||
|
import type { Game } from '@/types/home'; |
||||||
|
|
||||||
|
const { width } = Dimensions.get('window'); |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
flex: 1, |
||||||
|
backgroundColor: colors.background, |
||||||
|
paddingHorizontal: 12, |
||||||
|
paddingVertical: 12, |
||||||
|
}, |
||||||
|
gameGrid: { |
||||||
|
paddingBottom: 20, |
||||||
|
}, |
||||||
|
gameCard: { |
||||||
|
flex: 1, |
||||||
|
margin: 6, |
||||||
|
borderRadius: 12, |
||||||
|
overflow: 'hidden', |
||||||
|
backgroundColor: colors.card, |
||||||
|
elevation: 3, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 3 }, |
||||||
|
shadowOpacity: 0.15, |
||||||
|
shadowRadius: 6, |
||||||
|
}, |
||||||
|
gameCardPressed: { |
||||||
|
opacity: 0.8, |
||||||
|
}, |
||||||
|
gameImage: { |
||||||
|
width: '100%', |
||||||
|
height: 140, |
||||||
|
backgroundColor: colors.backgroundSecondary, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
}, |
||||||
|
gameIcon: { |
||||||
|
fontSize: 48, |
||||||
|
}, |
||||||
|
gameInfo: { |
||||||
|
padding: 10, |
||||||
|
}, |
||||||
|
gameName: { |
||||||
|
fontSize: 13, |
||||||
|
fontWeight: '600', |
||||||
|
color: colors.text, |
||||||
|
marginBottom: 6, |
||||||
|
}, |
||||||
|
gameButton: { |
||||||
|
backgroundColor: colors.primary, |
||||||
|
paddingVertical: 8, |
||||||
|
borderRadius: 6, |
||||||
|
alignItems: 'center', |
||||||
|
marginTop: 6, |
||||||
|
}, |
||||||
|
gameButtonText: { |
||||||
|
color: '#FFFFFF', |
||||||
|
fontSize: 12, |
||||||
|
fontWeight: '600', |
||||||
|
}, |
||||||
|
emptyContainer: { |
||||||
|
flex: 1, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
paddingVertical: 40, |
||||||
|
}, |
||||||
|
emptyText: { |
||||||
|
fontSize: 14, |
||||||
|
color: colors.textSecondary, |
||||||
|
marginTop: 12, |
||||||
|
}, |
||||||
|
loadingContainer: { |
||||||
|
flex: 1, |
||||||
|
justifyContent: 'center', |
||||||
|
alignItems: 'center', |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface LobbyProps { |
||||||
|
theme: 'light' | 'dark'; |
||||||
|
games?: Game[]; |
||||||
|
selectedCategory?: number; |
||||||
|
onGamePress?: (game: Game) => void; |
||||||
|
topHeight?: number; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 游戏大厅组件 |
||||||
|
*/ |
||||||
|
export default function Lobby({ |
||||||
|
theme, |
||||||
|
games: propGames, |
||||||
|
selectedCategory = 0, |
||||||
|
onGamePress, |
||||||
|
topHeight = 0, |
||||||
|
}: LobbyProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const [games, setGames] = useState<Game[]>(propGames || []); |
||||||
|
const [loading, setLoading] = useState(true); |
||||||
|
|
||||||
|
// 加载游戏数据
|
||||||
|
useEffect(() => { |
||||||
|
if (propGames && propGames.length > 0) { |
||||||
|
setGames(propGames); |
||||||
|
setLoading(false); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const loadGames = async () => { |
||||||
|
try { |
||||||
|
setLoading(true); |
||||||
|
// const response = await getGames(selectedCategory);
|
||||||
|
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(selectedCategory));
|
||||||
|
} catch (error) { |
||||||
|
console.error('加载游戏失败:', error); |
||||||
|
setGames(getMockGamesByCategory(selectedCategory)); |
||||||
|
} finally { |
||||||
|
setLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
loadGames(); |
||||||
|
}, [propGames, selectedCategory]); |
||||||
|
|
||||||
|
const renderGameCard = ({ item }: { item: Game }) => ( |
||||||
|
<TouchableOpacity |
||||||
|
style={s.gameCard} |
||||||
|
activeOpacity={0.7} |
||||||
|
onPress={() => onGamePress?.(item)} |
||||||
|
> |
||||||
|
<View style={s.gameImage}> |
||||||
|
{item.icon ? ( |
||||||
|
<Image |
||||||
|
source={{ uri: item.icon }} |
||||||
|
style={{ width: '100%', height: '100%' }} |
||||||
|
resizeMode="cover" |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<Text style={s.gameIcon}>🎮</Text> |
||||||
|
)} |
||||||
|
</View> |
||||||
|
<View style={s.gameInfo}> |
||||||
|
<Text style={s.gameName} numberOfLines={2}> |
||||||
|
{item.play_up_name} |
||||||
|
{item.play_cname ? ` - ${item.play_cname}` : ''} |
||||||
|
</Text> |
||||||
|
<TouchableOpacity |
||||||
|
style={s.gameButton} |
||||||
|
onPress={() => onGamePress?.(item)} |
||||||
|
> |
||||||
|
<Text style={s.gameButtonText}>进入游戏</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
</View> |
||||||
|
</TouchableOpacity> |
||||||
|
); |
||||||
|
|
||||||
|
if (loading) { |
||||||
|
return ( |
||||||
|
<View style={s.loadingContainer}> |
||||||
|
<ActivityIndicator size="large" color={Colors[theme].primary} /> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (games.length === 0) { |
||||||
|
return ( |
||||||
|
<View style={s.emptyContainer}> |
||||||
|
<Text style={{ fontSize: 40 }}>🎮</Text> |
||||||
|
<Text style={s.emptyText}>暂无游戏</Text> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<View style={s.container}> |
||||||
|
<FlatList |
||||||
|
data={games} |
||||||
|
renderItem={renderGameCard} |
||||||
|
keyExtractor={(item) => item.id} |
||||||
|
numColumns={2} |
||||||
|
scrollEnabled={false} |
||||||
|
contentContainerStyle={s.gameGrid} |
||||||
|
/> |
||||||
|
</View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,171 @@ |
|||||||
|
/** |
||||||
|
* 公告栏组件 |
||||||
|
* |
||||||
|
* 展示滚动公告,使用真实数据 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef } from 'react'; |
||||||
|
import { |
||||||
|
View, |
||||||
|
Text, |
||||||
|
StyleSheet, |
||||||
|
Animated, |
||||||
|
TouchableOpacity, |
||||||
|
Dimensions, |
||||||
|
} from 'react-native'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import { mockNotices } from '@/services/mockHomeService'; |
||||||
|
import type { Notice } from '@/types/home'; |
||||||
|
|
||||||
|
const { width } = Dimensions.get('window'); |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
backgroundColor: colors.backgroundSecondary, |
||||||
|
paddingVertical: 10, |
||||||
|
paddingHorizontal: 12, |
||||||
|
marginHorizontal: 12, |
||||||
|
marginBottom: 12, |
||||||
|
borderRadius: 8, |
||||||
|
flexDirection: 'row', |
||||||
|
alignItems: 'center', |
||||||
|
elevation: 2, |
||||||
|
shadowColor: colors.cardShadow, |
||||||
|
shadowOffset: { width: 0, height: 1 }, |
||||||
|
shadowOpacity: 0.1, |
||||||
|
shadowRadius: 3, |
||||||
|
}, |
||||||
|
label: { |
||||||
|
fontSize: 12, |
||||||
|
fontWeight: 'bold', |
||||||
|
color: '#FFFFFF', |
||||||
|
marginRight: 8, |
||||||
|
backgroundColor: colors.primary, |
||||||
|
paddingHorizontal: 8, |
||||||
|
paddingVertical: 4, |
||||||
|
borderRadius: 4, |
||||||
|
}, |
||||||
|
content: { |
||||||
|
flex: 1, |
||||||
|
fontSize: 12, |
||||||
|
color: colors.text, |
||||||
|
overflow: 'hidden', |
||||||
|
}, |
||||||
|
closeButton: { |
||||||
|
marginLeft: 8, |
||||||
|
padding: 4, |
||||||
|
}, |
||||||
|
closeText: { |
||||||
|
fontSize: 16, |
||||||
|
color: colors.textSecondary, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
interface NoticeBarProps { |
||||||
|
theme: 'light' | 'dark'; |
||||||
|
notices?: Notice[]; |
||||||
|
onNoticePress?: (notice: Notice) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 公告栏组件 |
||||||
|
*/ |
||||||
|
export default function NoticeBar({ theme, notices: propNotices, onNoticePress }: NoticeBarProps) { |
||||||
|
const s = styles[theme]; |
||||||
|
const [notices, setNotices] = useState<Notice[]>(propNotices || []); |
||||||
|
const [currentNotice, setCurrentNotice] = useState(0); |
||||||
|
const [visible, setVisible] = useState(true); |
||||||
|
const animatedValue = useRef(new Animated.Value(1)).current; |
||||||
|
|
||||||
|
// 加载公告数据
|
||||||
|
useEffect(() => { |
||||||
|
if (propNotices && propNotices.length > 0) { |
||||||
|
setNotices(propNotices); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const loadNotices = async () => { |
||||||
|
try { |
||||||
|
// const data = await getNotices();
|
||||||
|
// setNotices(data.length > 0 ? data : mockNotices);
|
||||||
|
} catch (error) { |
||||||
|
console.error('加载公告失败:', error); |
||||||
|
setNotices(mockNotices); |
||||||
|
} |
||||||
|
}; |
||||||
|
loadNotices(); |
||||||
|
}, [propNotices]); |
||||||
|
|
||||||
|
// 自动切换公告
|
||||||
|
useEffect(() => { |
||||||
|
if (notices.length === 0) return; |
||||||
|
|
||||||
|
const timer = setInterval(() => { |
||||||
|
setCurrentNotice((prev) => (prev + 1) % notices.length); |
||||||
|
}, 5000); |
||||||
|
|
||||||
|
return () => clearInterval(timer); |
||||||
|
}, [notices.length]); |
||||||
|
|
||||||
|
// 处理关闭公告
|
||||||
|
const handleClose = () => { |
||||||
|
Animated.timing(animatedValue, { |
||||||
|
toValue: 0, |
||||||
|
duration: 300, |
||||||
|
useNativeDriver: true, |
||||||
|
}).start(() => { |
||||||
|
setVisible(false); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
// 处理公告点击
|
||||||
|
const handleNoticePress = () => { |
||||||
|
if (notices.length > 0) { |
||||||
|
onNoticePress?.(notices[currentNotice]); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
if (!visible || notices.length === 0) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
const currentNoticeData = notices[currentNotice]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Animated.View |
||||||
|
style={[ |
||||||
|
s.container, |
||||||
|
{ |
||||||
|
opacity: animatedValue, |
||||||
|
transform: [ |
||||||
|
{ |
||||||
|
scaleY: animatedValue, |
||||||
|
}, |
||||||
|
], |
||||||
|
}, |
||||||
|
]} |
||||||
|
> |
||||||
|
<Text style={s.label}>📢</Text> |
||||||
|
<TouchableOpacity |
||||||
|
style={{ flex: 1 }} |
||||||
|
onPress={handleNoticePress} |
||||||
|
activeOpacity={0.7} |
||||||
|
> |
||||||
|
<Text style={s.content} numberOfLines={1}> |
||||||
|
{currentNoticeData.title || currentNoticeData.content} |
||||||
|
</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
<TouchableOpacity |
||||||
|
style={s.closeButton} |
||||||
|
onPress={handleClose} |
||||||
|
> |
||||||
|
<Text style={s.closeText}>✕</Text> |
||||||
|
</TouchableOpacity> |
||||||
|
</Animated.View> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,26 @@ |
|||||||
|
/** |
||||||
|
* 首页组件统一导出 |
||||||
|
* |
||||||
|
* 所有组件都使用 React.memo 进行性能优化 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React from 'react'; |
||||||
|
import BannerSwiperComponent from './BannerSwiper'; |
||||||
|
import NoticeBarComponent from './NoticeBar'; |
||||||
|
import GameCategoryMenuComponent from './GameCategoryMenu'; |
||||||
|
import LobbyComponent from './Lobby'; |
||||||
|
import HighPrizeGameComponent from './HighPrizeGame'; |
||||||
|
import FastFootNavComponent from './FastFootNav'; |
||||||
|
import HeaderComponent from './Header'; |
||||||
|
import BottomTabsComponent from './BottomTabs'; |
||||||
|
|
||||||
|
// 使用 React.memo 优化组件性能,避免不必要的重新渲染
|
||||||
|
export const BannerSwiper = React.memo(BannerSwiperComponent); |
||||||
|
export const NoticeBar = React.memo(NoticeBarComponent); |
||||||
|
export const GameCategoryMenu = React.memo(GameCategoryMenuComponent); |
||||||
|
export const Lobby = React.memo(LobbyComponent); |
||||||
|
export const HighPrizeGame = React.memo(HighPrizeGameComponent); |
||||||
|
export const FastFootNav = React.memo(FastFootNavComponent); |
||||||
|
export const Header = React.memo(HeaderComponent); |
||||||
|
export const BottomTabs = React.memo(BottomTabsComponent); |
||||||
|
|
||||||
@ -0,0 +1,59 @@ |
|||||||
|
/** |
||||||
|
* 首页主容器组件 |
||||||
|
* |
||||||
|
* 支持浅色/深色主题,包含完整的首页功能: |
||||||
|
* - Header(搜索、用户信息) |
||||||
|
* - 轮播图 |
||||||
|
* - 游戏分类菜单 |
||||||
|
* - 游戏大厅 |
||||||
|
* - 公告栏 |
||||||
|
* - 高奖金游戏(深色主题) |
||||||
|
* - 快速导航(深色主题) |
||||||
|
* - BottomTabs(底部导航) |
||||||
|
*/ |
||||||
|
|
||||||
|
import React, { useMemo, useCallback } from 'react'; |
||||||
|
import { ScrollView, StyleSheet, RefreshControl } from 'react-native'; |
||||||
|
import { useColorScheme } from '@/hooks'; |
||||||
|
import { createThemeStyles } from '@/theme'; |
||||||
|
import Colors from '@/constants/Colors'; |
||||||
|
import HomeScreenComplete from './HomeScreenComplete'; |
||||||
|
|
||||||
|
/** |
||||||
|
* 创建主题样式 |
||||||
|
*/ |
||||||
|
const styles = createThemeStyles((colors) => ({ |
||||||
|
container: { |
||||||
|
flex: 1, |
||||||
|
backgroundColor: colors.background, |
||||||
|
}, |
||||||
|
scrollView: { |
||||||
|
flex: 1, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
/** |
||||||
|
* 首页主容器组件 |
||||||
|
*/ |
||||||
|
export default function HomeScreen() { |
||||||
|
const theme = useColorScheme(); |
||||||
|
const s = styles[theme]; |
||||||
|
const [refreshing, setRefreshing] = React.useState(false); |
||||||
|
|
||||||
|
// 下拉刷新处理
|
||||||
|
const onRefresh = useCallback(() => { |
||||||
|
setRefreshing(true); |
||||||
|
// 模拟刷新延迟
|
||||||
|
setTimeout(() => { |
||||||
|
setRefreshing(false); |
||||||
|
}, 1000); |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<HomeScreenComplete |
||||||
|
theme={theme} |
||||||
|
isDarkTheme={theme === 'dark'} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
@ -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: number) => { |
||||||
|
if (categoryId === 0) { |
||||||
|
return mockGames; // 推荐分类返回所有游戏
|
||||||
|
} |
||||||
|
return mockGames.filter((game) => game.big_type === categoryId); |
||||||
|
}; |
||||||
|
|
||||||
@ -0,0 +1,258 @@ |
|||||||
|
/** |
||||||
|
* 游戏状态管理 |
||||||
|
* 使用 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>; |
||||||
|
} |
||||||
|
|
||||||
|
// 操作
|
||||||
|
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; |
||||||
|
// 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: {}, |
||||||
|
|
||||||
|
|
||||||
|
// 保存首页数据
|
||||||
|
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] || [], |
||||||
|
} }); |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
// 从 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; |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue