diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index a088de7..2f7690f 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -29,42 +29,28 @@ export default function TabLayout() { , - headerRight: () => ( - - - {({ pressed }) => ( - - )} - - - ), + title: '首页', + tabBarIcon: ({ color }) => , }} /> , }} /> , }} /> , }} /> diff --git a/app/(tabs)/demo.tsx b/app/(tabs)/demo.tsx index af28749..3399871 100644 --- a/app/(tabs)/demo.tsx +++ b/app/(tabs)/demo.tsx @@ -24,10 +24,8 @@ import { useRouter } from 'expo-router'; // 工具函数 import { - Storage, + storageManager, STORAGE_KEYS, - SessionStorage, - SESSION_KEYS, formatDate, formatRelativeTime, formatChatTime @@ -43,17 +41,13 @@ import { useLanguage, useHapticsEnabled, useSettingsActions, - useTenantStates, - useTenantInfo, } from '@/stores'; +import { useTenantLoad, useTenantInfo } from '@/stores/tenantStore'; // 验证规则 import { loginSchema } from '@/schemas'; import type { LoginFormData } from '@/schemas'; -// API 服务 -import { authService } from '@/services'; - // 自定义 Hooks import { useDebounce, useThrottle, useHaptics } from '@/hooks'; @@ -71,7 +65,7 @@ export default function DemoScreen() { const login = useUserStore((state) => state.login); const logout = useUserStore((state) => state.logout); - const { tenantLoad } = useTenantStates(); + const tenantLoad = useTenantLoad(); const tenantInfo = useTenantInfo(); // 设置状态 @@ -189,7 +183,7 @@ export default function DemoScreen() { counter, }; - await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData); + storageManager.session.setItem(STORAGE_KEYS.USER_PREFERENCES, testData); haptics.success(); Alert.alert('成功', '数据已保存到本地存储'); } catch (error) { @@ -201,7 +195,7 @@ export default function DemoScreen() { const handleLoadFromStorage = async () => { try { haptics.light(); - const data = await Storage.getObject(STORAGE_KEYS.USER_PREFERENCES); + const data = storageManager.session.getItem(STORAGE_KEYS.USER_PREFERENCES); if (data) { setStorageValue(JSON.stringify(data, null, 2)); @@ -229,7 +223,7 @@ export default function DemoScreen() { counter: Math.floor(Math.random() * 100), }; - SessionStorage.setObject(SESSION_KEYS.FORM_DRAFT, testData); + storageManager.session.setItem(STORAGE_KEYS.FORM_DRAFT, testData); haptics.success(); Alert.alert('成功', '数据已保存到会话存储(应用重启后会丢失)'); } catch (error) { @@ -241,7 +235,7 @@ export default function DemoScreen() { const handleLoadFromSession = () => { try { haptics.light(); - const data = SessionStorage.getObject(SESSION_KEYS.FORM_DRAFT); + const data = storageManager.session.getItem(STORAGE_KEYS.FORM_DRAFT); if (data) { setSessionValue(JSON.stringify(data, null, 2)); @@ -259,7 +253,7 @@ export default function DemoScreen() { const handleClearSession = () => { try { haptics.light(); - SessionStorage.clear(); + storageManager.session.clear(); setSessionValue(''); haptics.success(); Alert.alert('成功', '会话存储已清空'); diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index b9185e5..f01fbf1 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,187 +1,23 @@ -import { useState, useEffect } from 'react'; -import { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native'; -import * as Updates from 'expo-updates'; +/** + * 首页 - 游戏大厅 + * + * 重构自 xinyong-web 项目的首页 + * 支持浅色/深色主题,包含轮播图、分类菜单、游戏大厅等功能 + */ -import { Text, View } from '@/components/Themed'; +import { Stack } from 'expo-router'; +import HomeScreen from '@/pages/HomeScreen'; export default function TabOneScreen() { - const [isChecking, setIsChecking] = useState(false); - const [updateInfo, setUpdateInfo] = useState(''); - - const checkForUpdates = async () => { - if (__DEV__) { - Alert.alert('提示', '开发模式下无法检查更新,请使用生产构建测试热更新功能'); - return; - } - - setIsChecking(true); - setUpdateInfo('正在检查更新...'); - - try { - const update = await Updates.checkForUpdateAsync(); - - if (update.isAvailable) { - setUpdateInfo('发现新版本,正在下载...'); - await Updates.fetchUpdateAsync(); - - Alert.alert('更新完成', '新版本已下载完成,是否立即重启应用?', [ - { - text: '稍后', - style: 'cancel', - onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'), - }, - { - text: '立即重启', - onPress: async () => { - await Updates.reloadAsync(); - }, - }, - ]); - } else { - setUpdateInfo('当前已是最新版本'); - } - } catch (error) { - setUpdateInfo('检查更新失败: ' + (error as Error).message); - Alert.alert('错误', '检查更新失败,请稍后重试'); - } finally { - setIsChecking(false); - } - }; - - const getUpdateInfo = () => { - const { isEmbeddedLaunch, isEmergencyLaunch, updateId, channel, runtimeVersion } = - Updates.useUpdates(); - - return ` -运行模式: ${__DEV__ ? '开发模式' : '生产模式'} -是否为内嵌启动: ${isEmbeddedLaunch ? '是' : '否'} -是否为紧急启动: ${isEmergencyLaunch ? '是' : '否'} -更新 ID: ${updateId || '无'} -更新通道: ${channel || '无'} -运行时版本: ${runtimeVersion || '无'} - `.trim(); - }; - - useEffect(() => { - console.log('=== TabOneScreen 组件已渲染 ==='); - }, []); - return ( - - 🚀 热更新演示 - - - - 当前版本信息: - {getUpdateInfo()} - - - - {isChecking ? ( - - ) : ( - 检查更新 - )} - - - {updateInfo ? ( - - {updateInfo} - - ) : null} - - - 📝 使用说明: - - 1. 使用 EAS Build 构建生产版本{'\n'} - 2. 修改代码后运行 eas update 发布更新{'\n'} - 3. 打开应用点击"检查更新"按钮{'\n'} - 4. 应用会自动下载并提示重启 - - - + <> + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - padding: 20, - }, - title: { - fontSize: 28, - fontWeight: 'bold', - marginBottom: 10, - }, - separator: { - marginVertical: 20, - height: 1, - width: '80%', - }, - infoContainer: { - backgroundColor: 'rgba(0, 122, 255, 0.1)', - padding: 15, - borderRadius: 10, - marginBottom: 20, - width: '100%', - }, - infoTitle: { - fontSize: 16, - fontWeight: 'bold', - marginBottom: 10, - }, - infoText: { - fontSize: 12, - fontFamily: 'monospace', - lineHeight: 18, - }, - button: { - backgroundColor: '#007AFF', - paddingHorizontal: 30, - paddingVertical: 15, - borderRadius: 10, - marginBottom: 20, - minWidth: 200, - alignItems: 'center', - }, - buttonDisabled: { - backgroundColor: '#999', - }, - buttonText: { - color: '#fff', - fontSize: 16, - fontWeight: 'bold', - }, - updateInfoContainer: { - backgroundColor: 'rgba(52, 199, 89, 0.1)', - padding: 15, - borderRadius: 10, - marginBottom: 20, - width: '100%', - }, - updateInfoText: { - fontSize: 14, - textAlign: 'center', - }, - instructionsContainer: { - backgroundColor: 'rgba(255, 149, 0, 0.1)', - padding: 15, - borderRadius: 10, - width: '100%', - }, - instructionsTitle: { - fontSize: 16, - fontWeight: 'bold', - marginBottom: 10, - }, - instructionsText: { - fontSize: 13, - lineHeight: 20, - }, -}); diff --git a/app/_layout.tsx b/app/_layout.tsx index d6307ea..00d503d 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -12,7 +12,8 @@ import { PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper'; // ✅ 从 hooks 目录导入 import { useColorScheme } from '@/hooks'; // ✅ 从 stores 目录导入 -import { restoreUserState, restoreSettingsState, useTenantActions } from '@/stores'; +import { restoreUserState, restoreSettingsState } from '@/stores'; +import { requestTenantInfo } from '@/stores/tenantStore'; export { // Catch any errors thrown by the Layout component. @@ -32,7 +33,6 @@ export default function RootLayout() { SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), ...FontAwesome.font, }); - const { requestTenantInfo } = useTenantActions(); // Expo Router uses Error Boundaries to catch errors in the navigation tree. useEffect(() => { diff --git a/constants/Colors.ts b/constants/Colors.ts index 98fc02f..450604a 100644 --- a/constants/Colors.ts +++ b/constants/Colors.ts @@ -1,7 +1,6 @@ /** * 主题颜色配置 * - * 支持 light 和 dark 两种主题 * 可以通过 settingsStore 切换主题 */ @@ -59,6 +58,56 @@ export default { // 覆盖层 overlay: 'rgba(0, 0, 0, 0.5)', }, + orange: { + // 文本颜色 + text: '#000000', + textSecondary: '#666666', + textTertiary: '#999999', + textInverse: '#FFFFFF', + + // 背景颜色 + background: '#FFFFFF', + backgroundSecondary: '#F5F5F5', + backgroundTertiary: '#E5E5E5', + + // 主题色 + tint: tintColorLight, + primary: '#007AFF', + secondary: '#5856D6', + success: '#34C759', + warning: '#FF9500', + error: '#FF3B30', + info: '#5AC8FA', + + // 边框颜色 + border: '#E5E5E5', + borderSecondary: '#D1D1D6', + + // Tab 图标 + tabIconDefault: '#8E8E93', + tabIconSelected: tintColorLight, + + // 卡片 + card: '#FFFFFF', + cardShadow: 'rgba(0, 0, 0, 0.1)', + + // 输入框 + inputBackground: '#FFFFFF', + inputBorder: '#D1D1D6', + inputPlaceholder: '#C7C7CC', + + // 按钮 + buttonPrimary: '#007AFF', + buttonSecondary: '#5856D6', + buttonDisabled: '#E5E5E5', + buttonText: '#FFFFFF', + + // 分隔线 + separator: '#E5E5E5', + + // 覆盖层 + overlay: 'rgba(0, 0, 0, 0.5)', + }, dark: { // 文本颜色 text: '#FFFFFF', diff --git a/constants/game.ts b/constants/game.ts new file mode 100644 index 0000000..cde8e88 --- /dev/null +++ b/constants/game.ts @@ -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; +})(); + +// 默认首页游戏分类菜单 +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, +}; diff --git a/constants/theme.ts b/constants/theme.ts new file mode 100644 index 0000000..13f70cd --- /dev/null +++ b/constants/theme.ts @@ -0,0 +1,6 @@ +// theme enum +export enum ThemeEnum { + LIGHT = 'light', + DARK = 'dark', + ORANGE = 'orange', +} diff --git a/constants/user.ts b/constants/user.ts new file mode 100644 index 0000000..424e966 --- /dev/null +++ b/constants/user.ts @@ -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, // 批量开户注册 +} diff --git a/hooks/useGameMenus.ts b/hooks/useGameMenus.ts new file mode 100644 index 0000000..68ecd12 --- /dev/null +++ b/hooks/useGameMenus.ts @@ -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) => { + 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); diff --git a/pages/HomeScreen/HomeScreenComplete.tsx b/pages/HomeScreen/HomeScreenComplete.tsx new file mode 100644 index 0000000..1026499 --- /dev/null +++ b/pages/HomeScreen/HomeScreenComplete.tsx @@ -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(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 ( + <> + + + + + + + + ); + } else { + // 浅色主题布局 + return ( + <> + + + + + + ); + } + }; + + return ( + + {/* Header */} +
Alert.alert('消息', '消息功能')} + onUserPress={() => Alert.alert('用户', '用户中心')} + unreadCount={3} + /> + + {/* 内容区域 */} + + + } + showsVerticalScrollIndicator={false} + > + {renderContent()} + + + + ); +} diff --git a/pages/HomeScreen/components/BannerSwiper/index.tsx b/pages/HomeScreen/components/BannerSwiper/index.tsx new file mode 100644 index 0000000..5c9500d --- /dev/null +++ b/pages/HomeScreen/components/BannerSwiper/index.tsx @@ -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(null); + const autoPlayTimerRef = useRef(null); + const { homeBanner } = useMsgStore(); + + + // 加载轮播图数据 + useEffect(() => { + // 如果有传入的 banners 数据,直接使用 + if (homeBanner.length > 0) { + setLoading(false); + return; + } + // 如果没有数据,保持 loading 状态显示骨架屏 + }, [homeBanner]); + + // 处理 Banner 点击 + const onBannerPress = useCallback((banner: Record) => { + Alert.alert('轮播图', `点击了: ${banner.title || banner.id}`); + // 这里可以添加导航逻辑 + }, []); + + // 处理滚动事件 + const handleScroll = (event: NativeSyntheticEvent) => { + 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 ( + + + + ); + } + + return ( + + + {homeBanner.map((banner) => ( + onBannerPress(banner)} + activeOpacity={0.9} + > + + + ))} + + + {/* 指示器 */} + + {homeBanner.map((_, index) => ( + + ))} + + + ); +} + diff --git a/pages/HomeScreen/components/BannerSwiper/styles.ts b/pages/HomeScreen/components/BannerSwiper/styles.ts new file mode 100644 index 0000000..071c248 --- /dev/null +++ b/pages/HomeScreen/components/BannerSwiper/styles.ts @@ -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, + }, +})); diff --git a/pages/HomeScreen/components/BottomTabs.tsx b/pages/HomeScreen/components/BottomTabs.tsx new file mode 100644 index 0000000..b20eef0 --- /dev/null +++ b/pages/HomeScreen/components/BottomTabs.tsx @@ -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 ( + + {items.map((item) => ( + handleTabPress(item.id, item.action)} + activeOpacity={0.7} + > + {item.icon} + + {item.label} + + + ))} + + ); +} + diff --git a/pages/HomeScreen/components/FastFootNav.tsx b/pages/HomeScreen/components/FastFootNav.tsx new file mode 100644 index 0000000..8a6a7a6 --- /dev/null +++ b/pages/HomeScreen/components/FastFootNav.tsx @@ -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(propItems || []); + const [selectedId, setSelectedId] = useState(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 ( + + + {items.map((item) => ( + handleNavPress(item)} + activeOpacity={0.7} + > + {item.icon || '🎮'} + + {item.name} + + + ))} + + + ); +} + diff --git a/pages/HomeScreen/components/GameMainMenus/index.tsx b/pages/HomeScreen/components/GameMainMenus/index.tsx new file mode 100644 index 0000000..70b76a9 --- /dev/null +++ b/pages/HomeScreen/components/GameMainMenus/index.tsx @@ -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(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 = () => ( + + + {[1, 2, 3, 4, 5].map((index) => ( + + ))} + + + ); + + // 如果动态数据还未加载,显示骨架屏 + if (!isDataLoaded) { + return renderSkeleton(); + } + + return ( + + + {gameMenus.map((menu) => ( + handleCategoryPress(menu.key)} + activeOpacity={0.7} + > + + {menu.icon || '🎮'} {menu.name} + + + ))} + + + ); +} diff --git a/pages/HomeScreen/components/GameMainMenus/styles.ts b/pages/HomeScreen/components/GameMainMenus/styles.ts new file mode 100644 index 0000000..a1095b5 --- /dev/null +++ b/pages/HomeScreen/components/GameMainMenus/styles.ts @@ -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', + }, +})); diff --git a/pages/HomeScreen/components/Header.tsx b/pages/HomeScreen/components/Header.tsx new file mode 100644 index 0000000..219e17c --- /dev/null +++ b/pages/HomeScreen/components/Header.tsx @@ -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 ( + + {/* 顶部栏 */} + + {/* Logo */} + 🎮 游戏大厅 + + {/* 搜索框 */} + + 🔍 + + {searchText ? ( + + + + ) : null} + + + {/* 消息按钮 */} + + 💬 + {unreadCount > 0 && ( + + + {unreadCount > 99 ? '99+' : unreadCount} + + + )} + + + {/* 用户按钮 */} + + 👤 + + + + ); +} + diff --git a/pages/HomeScreen/components/HighPrizeGame.tsx b/pages/HomeScreen/components/HighPrizeGame.tsx new file mode 100644 index 0000000..3993782 --- /dev/null +++ b/pages/HomeScreen/components/HighPrizeGame.tsx @@ -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(propGames || []); + const [selectedId, setSelectedId] = useState(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 ( + + 🏆 实时爆奖 + + {games.map((game) => ( + { + setSelectedId(game.id); + onGamePress?.(game); + }} + activeOpacity={0.7} + > + {game.icon ? ( + + ) : ( + 🎰 + )} + + {game.play_up_name} + + + ¥{Math.floor(game.payout_amount / 1000)}k + + + ))} + + + ); +} + diff --git a/pages/HomeScreen/components/Lobby.tsx b/pages/HomeScreen/components/Lobby.tsx new file mode 100644 index 0000000..83b2be0 --- /dev/null +++ b/pages/HomeScreen/components/Lobby.tsx @@ -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(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 }) => ( + onGamePress?.(item)} + > + + {item.icon ? ( + + ) : ( + 🎮 + )} + + + + {item.play_up_name} + {item.play_cname ? ` - ${item.play_cname}` : ''} + + onGamePress?.(item)} + > + 进入游戏 + + + + ); + + if (loading) { + return ( + + + + ); + } + + if (games.length === 0) { + return ( + + 🎮 + 暂无游戏 + + ); + } + + return ( + + item.id} + numColumns={2} + scrollEnabled={false} + contentContainerStyle={s.gameGrid} + /> + + ); +} + diff --git a/pages/HomeScreen/components/NoticeBar.tsx b/pages/HomeScreen/components/NoticeBar.tsx new file mode 100644 index 0000000..b0738ff --- /dev/null +++ b/pages/HomeScreen/components/NoticeBar.tsx @@ -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(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 ( + + 📢 + + + {currentNoticeData.title || currentNoticeData.content} + + + + + + + ); +} + diff --git a/pages/HomeScreen/components/index.ts b/pages/HomeScreen/components/index.ts new file mode 100644 index 0000000..f76d05b --- /dev/null +++ b/pages/HomeScreen/components/index.ts @@ -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); + diff --git a/pages/HomeScreen/index.tsx b/pages/HomeScreen/index.tsx new file mode 100644 index 0000000..cbda9f3 --- /dev/null +++ b/pages/HomeScreen/index.tsx @@ -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 ( + + ); +} + diff --git a/pages/index.ts b/pages/index.ts index 4e18ee8..e38333a 100644 --- a/pages/index.ts +++ b/pages/index.ts @@ -37,4 +37,5 @@ // 导出业务页面组件 export { default as TestPage } from './TestPage'; +export { default as HomeScreen } from './HomeScreen'; diff --git a/scripts/proxy-server.js b/scripts/proxy-server.js index 3c0e0cd..af73add 100644 --- a/scripts/proxy-server.js +++ b/scripts/proxy-server.js @@ -47,7 +47,7 @@ app.use(cors({ const API_TARGET = process.env.API_TARGET || 'https://51zhh5.notbug.org'; // 代理路径列表 -const PROXY_PATHS = ['/api/v1', '/api/v2', '/api/v3']; +const PROXY_PATHS = ['/api/v2']; // 为每个路径配置代理 PROXY_PATHS.forEach((path) => { diff --git a/services/gameService.ts b/services/gameService.ts new file mode 100644 index 0000000..300bcd1 --- /dev/null +++ b/services/gameService.ts @@ -0,0 +1,37 @@ +/** + * 游戏服务 + * 处理租户相关的 API 请求 + */ + +import { request } from '@/utils/network/api'; + +/** + * API 响应接口 + */ +interface ApiResponse { + code: number; + message: string; + data: T; +} + +/** + * tenant 服务类 + */ +class GameService { + /** + * 获取首页数据 + */ + getHomePageData(data?: Record): Promise { + return request.post('/v2', data, { + headers: { + cmdId: 381119, + paramType: 1, + apiName: 'getHomePageData', + }, + }); + } +} + +// 导出单例 +export const gameService = new GameService(); +export default gameService; diff --git a/services/index.ts b/services/index.ts index f1cb430..05d17b3 100644 --- a/services/index.ts +++ b/services/index.ts @@ -5,4 +5,5 @@ export { default as authService } from './authService'; export { default as userService } from './userService'; export { default as tenantService } from './tenantService'; +export { default as gameService } from './gameService'; diff --git a/services/mockHomeService.ts b/services/mockHomeService.ts new file mode 100644 index 0000000..232fd34 --- /dev/null +++ b/services/mockHomeService.ts @@ -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); +}; + diff --git a/services/tenantService.ts b/services/tenantService.ts index d85f77b..260e16a 100644 --- a/services/tenantService.ts +++ b/services/tenantService.ts @@ -4,13 +4,12 @@ */ import { request } from '@/utils/network/api'; -// import type { User, UpdateProfileFormData } from '@/schemas/user'; /** * API 响应接口 */ interface ApiResponse { - code: number; + success: string; message: string; data: T; } @@ -32,7 +31,6 @@ class TenantService { cmdId: 371130, headerType: 1, apiName: 'getPlatformData', - tid: '', }, }); } diff --git a/stores/gameStore.ts b/stores/gameStore.ts new file mode 100644 index 0000000..68a63e1 --- /dev/null +++ b/stores/gameStore.ts @@ -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[]; + rebateGameSort: Record[]; + originalGames: Record[]; + blockchainGames: Record[]; + homeHotGames: Record[]; + gamesTry: Record[]; + gamesTryPlayIds: number[]; + smallClassGames: Record; + gameBigClass: Record; +} + +// 操作 +interface Actions { + setHomePageData: (data: Record) => void; + setOriginalGames: (data: Record[]) => void; + setBlockchainGames: (data: Record[]) => void; + setGamesTry: (data: Record[]) => void; + setHomeHotGames: (data: Record[]) => void; + setSmallClassGame: (data: Record) => void; + setGameBigClass: (data: Record) => void; + // requestHomePageData: (data?: Record) => Promise; +} + +/** + * 租户状态 Store + */ +const useGameStore = create()((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[]) => { + const originalGames = map(list, (item: any) => ({ + ...item, + isOriginal: true, + })).sort((a: any, b: any) => a.hotVal - b.hotVal); + set({ originalGames }); + }, + + setBlockchainGames: (list: Record[]) => { + set({ blockchainGames: list || [] }); + }, + + setGamesTry: (list: Record[]) => { + 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[]) => { + set({ homeHotGames: list || [] }); + }, + + setSmallClassGame: (data: Record) => { + 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) => { + 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; diff --git a/stores/index.ts b/stores/index.ts index 0df2589..9ce2233 100644 --- a/stores/index.ts +++ b/stores/index.ts @@ -29,8 +29,12 @@ export type { Theme, Language } from './settingsStore'; // Tenant Store export { default as useTenantStore, - useTenantInfo, - useTenantStates, - useTenantActions, restoreTenantState, } from './tenantStore'; + +// Game Store +export { + default as useGameStore, + restoreGameState, +} from './gameStore'; + diff --git a/stores/msgStore.ts b/stores/msgStore.ts new file mode 100644 index 0000000..2911e78 --- /dev/null +++ b/stores/msgStore.ts @@ -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[]; + homeBanner: Record[]; + mineBanner: Record[]; +} + +/** + * 操作 + */ +interface Actions { + setNotices: (list: Record[]) => void; + setBanners: (list: Record[]) => void; +} + +/** + * 租户状态 Store + */ +const useMsgStore = create()((set, get) => ({ + // state + notices: [], + homeBanner: [], + mineBanner: [], + + // actions + setNotices: (list: Record[]) => { + set({ notices: list }); + + if (__DEV__) { + console.log('💾 notices saved:', list); + } + }, + setBanners: (list: Record[]) => { + 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; diff --git a/stores/settingsStore.ts b/stores/settingsStore.ts index 3256f0c..b6eea74 100644 --- a/stores/settingsStore.ts +++ b/stores/settingsStore.ts @@ -5,7 +5,7 @@ import { create } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import AsyncStorage from '@react-native-async-storage/async-storage'; +import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; /** * 主题类型 @@ -59,7 +59,7 @@ export const useSettingsStore = create()((set, get) => ({ setTheme: (theme) => { set({ theme }); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(get())); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('🎨 Theme changed:', theme); } @@ -69,7 +69,7 @@ export const useSettingsStore = create()((set, get) => ({ setLanguage: (language) => { set({ language }); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(get())); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('🌐 Language changed:', language); } @@ -79,7 +79,7 @@ export const useSettingsStore = create()((set, get) => ({ setNotificationsEnabled: (enabled) => { set({ notificationsEnabled: enabled }); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(get())); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled'); } @@ -89,7 +89,7 @@ export const useSettingsStore = create()((set, get) => ({ setSoundEnabled: (enabled) => { set({ soundEnabled: enabled }); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(get())); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled'); } @@ -99,7 +99,7 @@ export const useSettingsStore = create()((set, get) => ({ setHapticsEnabled: (enabled) => { set({ hapticsEnabled: enabled }); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(get())); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled'); } @@ -109,7 +109,7 @@ export const useSettingsStore = create()((set, get) => ({ resetSettings: () => { set(DEFAULT_SETTINGS); // 手动持久化 - AsyncStorage.setItem('settings-storage', JSON.stringify(DEFAULT_SETTINGS)); + storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get()); if (__DEV__) { console.log('🔄 Settings reset to default'); } @@ -119,7 +119,7 @@ export const useSettingsStore = create()((set, get) => ({ // 从 AsyncStorage 恢复状态的函数 export const restoreSettingsState = async () => { try { - const stored = await AsyncStorage.getItem('settings-storage'); + const stored = await storageManager.local.getItem(STORAGE_KEYS.SETTINGS_STORE); if (stored) { const state = JSON.parse(stored); useSettingsStore.setState(state); diff --git a/stores/tenantStore.ts b/stores/tenantStore.ts index e2ffd49..ae9ef7d 100644 --- a/stores/tenantStore.ts +++ b/stores/tenantStore.ts @@ -4,41 +4,36 @@ */ import { create } from 'zustand'; -import { useShallow } from 'zustand/react/shallow'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { STORAGE_KEYS } from '@/utils/storage'; -import { tenantService } from '@/services/tenantService'; -import { useEffect } from 'react'; +// import { useShallow } from 'zustand/react/shallow'; +import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; +import { tenantService } from '@/services'; + /** * 租户信息接口 */ -// export interface Tenant { -// id: string; -// username: string; -// email: string; -// avatar?: string; -// nickname?: string; -// phone?: string; -// createdAt?: string; -// } +export interface Tenant { + tid: number; + proxy: number; + create_time: string; + domain_addr: string +} -/** - * 租户状态接口 - */ -interface TenantState { - // 状态 - tenantInfo: Record | null; +// 状态 +interface State { + tenantInfo: Tenant | null; +} - // 操作 +// 操作 +interface Actions { setTenantInfo: (data: Record) => void; - requestTenantInfo: (data?: Record) => Promise; } + /** * 租户状态 Store */ -const useTenantStore = create()((set, get) => ({ +const useTenantStore = create()((set, get) => ({ // 初始状态 tenantInfo: null, @@ -47,39 +42,19 @@ const useTenantStore = create()((set, get) => ({ setTenantInfo: (data: any) => { set({ tenantInfo: data }); // 手动持久化 - // AsyncStorage.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data })); + storageManager.session.setItem(STORAGE_KEYS.TENANT_STORE, get()); + storageManager.session.setItem(STORAGE_KEYS.TENANT_TID, data?.tid ?? ''); if (__DEV__) { console.log('💾 Tenant info saved:', data); } }, - - // 获取租户信息(调用 API 并使用 setTenantInfo 保存) - requestTenantInfo: async () => { - try { - const params = { - domain_addr: 'https://51zhh5.notbug.org', - }; - const { data } = await tenantService.getPlatformData(params); - - // 调用 setTenantInfo 来保存数据,避免重复代码 - get().setTenantInfo(data); - - if (__DEV__) { - console.log('✅ Tenant info loaded:', data); - } - return Promise.resolve(data); - } catch (error) { - console.error('Failed to request tenant info:', error); - return Promise.reject(error); - } - }, })); // 从 AsyncStorage 恢复状态的函数 export const restoreTenantState = async () => { try { - const stored = await AsyncStorage.getItem(STORAGE_KEYS.TENANT_STORE); + const stored = storageManager.session.getItem(STORAGE_KEYS.TENANT_STORE); if (stored) { const state = JSON.parse(stored); useTenantStore.setState(state); @@ -99,24 +74,29 @@ export const restoreTenantState = async () => { // 获取用户信息 export const useTenantInfo = () => useTenantStore((state) => state.tenantInfo); +// 租户数据是否加载完成 +export const useTenantLoad = () => useTenantStore((state) => !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time); + +// 获取租户信息 +export const requestTenantInfo = async (): Promise => { + try { + // 使用 getState() 而不是 hook + const { setTenantInfo } = useTenantStore.getState(); + const params = { + domain_addr: 'https://51zhh5.notbug.org', + }; + const { data } = await tenantService.getPlatformData(params); + + setTenantInfo(data); -// 获取租户状态 -export const useTenantStates = () => - useTenantStore( - useShallow((state) => ({ - tenantInfo: state.tenantInfo, - tenantLoad: !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time, - })) - ); - -// 获取租户操作方法 -// 使用 useShallow 避免每次渲染都返回新对象 -export const useTenantActions = () => - useTenantStore( - useShallow((state) => ({ - setTenantInfo: state.setTenantInfo, - requestTenantInfo: state.requestTenantInfo, - })) - ); + if (__DEV__) { + console.log('✅ Tenant info loaded:', data); + } + return Promise.resolve(data); + } catch (error) { + console.error('Failed to request tenant info:', error); + return Promise.reject(error); + } +}; export default useTenantStore; diff --git a/stores/userStore.ts b/stores/userStore.ts index bb8f823..535156a 100644 --- a/stores/userStore.ts +++ b/stores/userStore.ts @@ -5,7 +5,7 @@ import { create } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import AsyncStorage from '@react-native-async-storage/async-storage'; +import storageManager, { STORAGE_KEYS } from '@/utils/storageManager'; /** * 用户信息接口 @@ -20,16 +20,15 @@ export interface User { createdAt?: string; } -/** - * 用户状态接口 - */ -interface UserState { - // 状态 +// 状态 +interface State { user: User | null; isLoggedIn: boolean; token: string | null; +} - // 操作 +// 操作 +interface Actions { setUser: (user: User) => void; setToken: (token: string) => void; login: (user: User, token: string) => void; @@ -40,7 +39,7 @@ interface UserState { /** * 用户状态 Store */ -export const useUserStore = create()((set, get) => ({ +export const useUserStore = create()((set, get) => ({ // 初始状态 user: null, isLoggedIn: false, @@ -51,7 +50,7 @@ export const useUserStore = create()((set, get) => ({ const newState = { user, isLoggedIn: true }; set(newState); // 手动持久化 - AsyncStorage.setItem('user-storage', JSON.stringify(newState)); + storageManager.session.setItem(STORAGE_KEYS.USER_STORE, newState); }, // 设置 token @@ -59,8 +58,7 @@ export const useUserStore = create()((set, get) => ({ set({ token }); // 手动持久化 - 延迟执行以确保状态已更新 setTimeout(() => { - const state = get(); - AsyncStorage.setItem('user-storage', JSON.stringify(state)); + storageManager.session.setItem(STORAGE_KEYS.USER_STORE, get()); }, 0); }, @@ -74,9 +72,9 @@ export const useUserStore = create()((set, get) => ({ set(newState); // 同时保存 token 到 AsyncStorage(用于 API 请求) - AsyncStorage.setItem('auth_token', token); + storageManager.session.setItem('auth_token', token); // 手动持久化整个状态 - AsyncStorage.setItem('user-storage', JSON.stringify(newState)); + storageManager.session.setItem(STORAGE_KEYS.USER_STORE, newState); if (__DEV__) { console.log('✅ User logged in:', user.username); @@ -93,9 +91,9 @@ export const useUserStore = create()((set, get) => ({ set(newState); // 清除 AsyncStorage 中的 token - AsyncStorage.removeItem('auth_token'); + storageManager.session.removeItem('auth_token'); // 清除持久化状态 - AsyncStorage.removeItem('user-storage'); + storageManager.session.removeItem('user-storage'); if (__DEV__) { console.log('👋 User logged out'); @@ -109,7 +107,7 @@ export const useUserStore = create()((set, get) => ({ const newUser = { ...currentUser, ...updates }; set({ user: newUser }); // 手动持久化 - AsyncStorage.setItem('user-storage', JSON.stringify({ ...get(), user: newUser })); + storageManager.session.setItem(STORAGE_KEYS.USER_STORE, { ...get(), user: newUser }); if (__DEV__) { console.log('📝 User updated:', updates); @@ -121,7 +119,7 @@ export const useUserStore = create()((set, get) => ({ // 从 AsyncStorage 恢复状态的函数 export const restoreUserState = async () => { try { - const stored = await AsyncStorage.getItem('user-storage'); + const stored = await storageManager.session.getItem(STORAGE_KEYS.USER_STORE); if (stored) { const state = JSON.parse(stored); useUserStore.setState(state); diff --git a/theme/styles.ts b/theme/styles.ts index 1c8e119..2284eb0 100644 --- a/theme/styles.ts +++ b/theme/styles.ts @@ -1,30 +1,32 @@ /** * 主题样式工厂 - * + * * 提供创建主题感知样式的工具函数 - * + * * React Native 不支持 CSS 类名,但可以通过样式工厂函数实现类似效果 */ import { StyleSheet, TextStyle, ViewStyle } from 'react-native'; import Colors from '@/constants/Colors'; +import { ThemeEnum } from '@/constants/theme'; /** * 主题样式类型 */ export type ThemeStyles = { - light: any; - dark: any; + [ThemeEnum.LIGHT]: any; + [ThemeEnum.DARK]: any; + [ThemeEnum.ORANGE]: any; }; /** * 创建主题样式 - * + * * 类似于 CSS 类名的概念,但使用函数式方法 - * + * * @param createStyles - 样式创建函数,接收颜色对象 * @returns 主题样式对象 - * + * * @example * ```tsx * const styles = createThemeStyles((colors) => ({ @@ -37,7 +39,7 @@ export type ThemeStyles = { * fontSize: 16, * }, * })); - * + * * // 使用 * const theme = useColorScheme(); * @@ -46,23 +48,25 @@ export type ThemeStyles = { * ``` */ export function createThemeStyles>( - createStyles: (colors: typeof Colors.light) => T + createStyles: (colors: typeof Colors.light & typeof Colors.dark & typeof Colors.orange) => T ): ThemeStyles { return { light: StyleSheet.create(createStyles(Colors.light)), dark: StyleSheet.create(createStyles(Colors.dark)), + orange: StyleSheet.create(createStyles(Colors.light)), }; } /** * 创建响应式主题样式 - * + * * 允许为不同主题定义完全不同的样式 - * + * * @param lightStyles - 浅色主题样式 * @param darkStyles - 深色主题样式 + * @param orangeStyles - 橙色主题样式 * @returns 主题样式对象 - * + * * @example * ```tsx * const styles = createResponsiveThemeStyles( @@ -79,17 +83,19 @@ export function createThemeStyles>( */ export function createResponsiveThemeStyles>( lightStyles: T, - darkStyles: T + darkStyles: T, + orangeStyles: T ): ThemeStyles { return { light: StyleSheet.create(lightStyles), dark: StyleSheet.create(darkStyles), + orange: StyleSheet.create(lightStyles), }; } /** * 预定义的通用样式类 - * + * * 类似于 Tailwind CSS 的工具类 */ export const commonStyles = createThemeStyles((colors) => ({ @@ -109,7 +115,7 @@ export const commonStyles = createThemeStyles((colors) => ({ justifyContent: 'center', alignItems: 'center', }, - + // 卡片样式 card: { backgroundColor: colors.card, @@ -128,7 +134,7 @@ export const commonStyles = createThemeStyles((colors) => ({ shadowRadius: 4, elevation: 3, }, - + // 文本样式 textPrimary: { color: colors.text, @@ -152,7 +158,7 @@ export const commonStyles = createThemeStyles((colors) => ({ fontSize: 18, fontWeight: '600', } as TextStyle, - + // 按钮样式 button: { backgroundColor: colors.buttonPrimary, @@ -182,7 +188,7 @@ export const commonStyles = createThemeStyles((colors) => ({ fontSize: 16, fontWeight: '600', } as TextStyle, - + // 输入框样式 input: { backgroundColor: colors.inputBackground, @@ -204,7 +210,7 @@ export const commonStyles = createThemeStyles((colors) => ({ fontSize: 16, color: colors.text, } as TextStyle, - + // 分隔线 separator: { height: 1, @@ -214,14 +220,14 @@ export const commonStyles = createThemeStyles((colors) => ({ width: 1, backgroundColor: colors.separator, } as ViewStyle, - + // 间距 spacingXs: { height: 4 } as ViewStyle, spacingSm: { height: 8 } as ViewStyle, spacingMd: { height: 16 } as ViewStyle, spacingLg: { height: 24 } as ViewStyle, spacingXl: { height: 32 } as ViewStyle, - + // 布局 row: { flexDirection: 'row', @@ -243,11 +249,11 @@ export const commonStyles = createThemeStyles((colors) => ({ /** * 获取主题样式 - * + * * @param styles - 主题样式对象 * @param theme - 当前主题 * @returns 当前主题的样式 - * + * * @example * ```tsx * const theme = useColorScheme(); @@ -255,7 +261,7 @@ export const commonStyles = createThemeStyles((colors) => ({ * * ``` */ -export function getThemeStyle(styles: ThemeStyles, theme: 'light' | 'dark'): T { +export function getThemeStyle(styles: ThemeStyles, theme: ThemeEnum): T { return styles[theme]; } diff --git a/theme/utils.ts b/theme/utils.ts index 315e934..2494970 100644 --- a/theme/utils.ts +++ b/theme/utils.ts @@ -1,43 +1,45 @@ /** * 主题工具函数 - * + * * 提供主题相关的辅助函数 */ import Colors from '@/constants/Colors'; +import { ThemeEnum } from '@/constants/theme'; /** * 根据主题获取颜色 - * - * @param theme - 主题类型 'light' | 'dark' + * + * @param theme - 主题类型 ThemeEnum * @param colorName - 颜色名称 * @returns 颜色值 */ export function getThemeColor( - theme: 'light' | 'dark', - colorName: keyof typeof Colors.light & keyof typeof Colors.dark + theme: ThemeEnum, + colorName: keyof typeof Colors.light & keyof typeof Colors.dark & keyof typeof Colors.orange ): string { return Colors[theme][colorName]; } /** * 根据主题获取所有颜色 - * - * @param theme - 主题类型 'light' | 'dark' + * + * @param theme - 主题类型 ThemeEnum * @returns 颜色对象 */ -export function getThemeColors(theme: 'light' | 'dark') { +export function getThemeColors(theme: ThemeEnum) { return Colors[theme]; } /** * 创建主题感知的样式 - * + * * @param lightStyle - 浅色主题样式 * @param darkStyle - 深色主题样式 + * @param orangeStyle - 橙色主题样式 * @param theme - 当前主题 * @returns 合并后的样式 - * + * * @example * ```tsx * const style = createThemedStyle( @@ -50,19 +52,30 @@ export function getThemeColors(theme: 'light' | 'dark') { export function createThemedStyle( lightStyle: T, darkStyle: T, - theme: 'light' | 'dark' + orangeStyle: T, + theme: ThemeEnum ): T { - return theme === 'dark' ? darkStyle : lightStyle; + switch (theme) { + case ThemeEnum.LIGHT: + return lightStyle; + case ThemeEnum.DARK: + return darkStyle; + case ThemeEnum.ORANGE: + return orangeStyle; + default: + return lightStyle; + } } /** * 根据主题选择值 - * + * * @param lightValue - 浅色主题值 * @param darkValue - 深色主题值 + * @param orangeValue - 深色主题值 * @param theme - 当前主题 * @returns 选中的值 - * + * * @example * ```tsx * const fontSize = selectByTheme(14, 16, theme); @@ -71,18 +84,28 @@ export function createThemedStyle( export function selectByTheme( lightValue: T, darkValue: T, - theme: 'light' | 'dark' + orangeValue: T, + theme: ThemeEnum ): T { - return theme === 'dark' ? darkValue : lightValue; + switch (theme) { + case ThemeEnum.LIGHT: + return lightValue; + case ThemeEnum.DARK: + return darkValue; + case ThemeEnum.ORANGE: + return orangeValue; + default: + return lightValue; + } } /** * 颜色透明度调整 - * + * * @param color - 十六进制颜色值 * @param opacity - 透明度 0-1 * @returns 带透明度的颜色值 - * + * * @example * ```tsx * const color = withOpacity('#000000', 0.5); // rgba(0, 0, 0, 0.5) @@ -91,32 +114,42 @@ export function selectByTheme( export function withOpacity(color: string, opacity: number): string { // 移除 # 号 const hex = color.replace('#', ''); - + // 转换为 RGB const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); - + return `rgba(${r}, ${g}, ${b}, ${opacity})`; } /** * 判断是否为深色主题 - * + * * @param theme - 主题类型 * @returns 是否为深色主题 */ -export function isDarkTheme(theme: 'light' | 'dark'): boolean { - return theme === 'dark'; +export function isDarkTheme(theme: ThemeEnum): boolean { + return theme === ThemeEnum.DARK; } /** * 判断是否为浅色主题 - * + * * @param theme - 主题类型 * @returns 是否为浅色主题 */ -export function isLightTheme(theme: 'light' | 'dark'): boolean { - return theme === 'light'; +export function isLightTheme(theme: ThemeEnum): boolean { + return theme === ThemeEnum.LIGHT; +} + +/** + * 判断是否为橙色主题 + * + * @param theme - 主题类型 + * @returns 是否为橙色主题 + */ +export function isOrangeTheme(theme: ThemeEnum): boolean { + return theme === ThemeEnum.ORANGE; } diff --git a/types/home.ts b/types/home.ts new file mode 100644 index 0000000..df2b8cd --- /dev/null +++ b/types/home.ts @@ -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 { + 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; +} + diff --git a/utils/index.ts b/utils/index.ts index c5034cd..10e6b0b 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -13,10 +13,7 @@ export { export type { ApiResponse, ApiError, RequestConfig } from './network/api'; // Storage -export { default as Storage, STORAGE_KEYS } from './storage'; -export { default as SessionStorage, SESSION_KEYS } from './sessionStorage'; -export { default as StorageManager } from './storageManager'; -export type { StorageType, StorageOptions } from './storageManager'; +export { default as storageManager, STORAGE_KEYS } from './storageManager'; // Config export { default as config, printConfig } from './config'; @@ -26,13 +23,7 @@ export { formatDate, formatRelativeTime, formatChatTime, - parseDate, isToday, isYesterday, - isSameDay, - addDays, - subtractDays, - startOfDay, - endOfDay, } from './date'; diff --git a/utils/network/api.ts b/utils/network/api.ts index 1a3a1ff..368b04c 100644 --- a/utils/network/api.ts +++ b/utils/network/api.ts @@ -24,7 +24,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { router } from 'expo-router'; import { config } from '../config'; import { transformRequest, parseResponse } from './helper'; -import { cloneDeep, pick } from 'lodash-es'; +import { cloneDeep, pick, includes } from 'lodash-es'; import md5 from 'md5'; /** @@ -304,6 +304,10 @@ api.interceptors.response.use( }); } + if (includes([500, 502, 503], error.response?.status) && includes(['371130'], `${error.config?.headers?.cmdId}`)) { + router.replace('/maintenance' as any); + } + // 处理不同的错误状态码 if (error.response) { const { status, data } = error.response; diff --git a/utils/network/helper.ts b/utils/network/helper.ts index ce8eb11..629e85f 100644 --- a/utils/network/helper.ts +++ b/utils/network/helper.ts @@ -1,6 +1,7 @@ import { HmacMD5 } from 'crypto-js'; import Base64 from 'crypto-js/enc-base64'; import Latin1 from 'crypto-js/enc-latin1'; +import { Platform } from 'react-native'; import md5 from 'md5'; import { AxiosResponse } from 'axios'; import * as des from './des'; @@ -8,11 +9,11 @@ import NetworkError from './error'; import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es'; import { NetworkTypeEnum } from '@/constants/network'; import appConfig from '../config'; +// import storageManager, { STORAGE_KEYS } from '../storageManager'; // import NetworkError from './error' -// import { storeToRefs, useTenantStore, useUserStore, useAppStore, start } from '../index'; -// import { isMobile, getBetPlatform } from '@star/utils'; -// import { langToNum } from '@star/languages'; + +type PlatformType = 'IOS' | 'ANDROID' | 'H5_IOS'; // 请求到的数据返回 export type NetworkResponse = { @@ -20,38 +21,16 @@ export type NetworkResponse = { data: T; }; -export const getBetPlatform = (isReturnIndex = false) => { +const getBetPlatform = (): PlatformType => { // 5=PC; 7=HOMESCREEN_IOS; 6=HOMESCREEN_ANDROID; 4=H5_IOS 3=IOS 2=H5_ANDROID; 1=ANDROID 8=马甲包 - return 'H5_IOS'; - // const platform = new URLSearchParams(window.location.search).get('platform'); - // if (platform) { - // return platform?.includes('IOS') ? 'IOS' : platform?.includes('ANDROID') ? 'ANDROID' : ''; - // } - // if (isAppMJB()) { - // return 'APPS_ANDROID'; - // } - // if (isPWA()) { - // if (isIOS()) { - // return isReturnIndex ? 4 : 'HS_IOS'; - // } else { - // return isReturnIndex ? 2 : 'HS_ANDROID'; - // } - // } - // if (isAndroid()) { - // if (BASE_CONFIG.appVersion > 0) { - // return isReturnIndex ? 1 : 'ANDROID'; - // } else { - // return isReturnIndex ? 2 : 'H5_ANDROID'; - // } - // } - // if (isIOS()) { - // if (BASE_CONFIG.appVersion > 0) { - // return isReturnIndex ? 3 : 'IOS'; - // } else { - // return isReturnIndex ? 4 : 'H5_IOS'; - // } - // } - // return isReturnIndex ? 5 : 'PC'; + switch (Platform.OS) { + case 'ios': + return 'IOS'; + case 'android': + return 'ANDROID'; + default: + return 'H5_IOS'; + } }; const uuid = (len: number, radix: number) => { @@ -85,7 +64,7 @@ const uuid = (len: number, radix: number) => { }; // 格式化要发送的数据 -export const formatSendData = (data: any, type: number = 0) => { +const formatSendData = (data: any, type: number = 0) => { // url code if (type === 0) { const arr: any[] = []; @@ -107,44 +86,44 @@ export const formatSendData = (data: any, type: number = 0) => { return JSON.stringify(data); }; -export const getP = (p: any) => { +const getP = (p: any) => { return HmacMD5(p, '7NEkojNzfkk=').toString(); }; -export const enD = (rk: string, str: string) => { +const enD = (rk: string, str: string) => { const enc = des.des(rk, str, 1, 0, null, 1); return Base64.stringify(Latin1.parse(enc)); }; -export const dnD = (rk: string, str: string) => { +const dnD = (rk: string, str: string) => { const s = Latin1.stringify(Base64.parse(str)); const d = des.des(rk, s, 0, 0, null, 1); return d; }; -export const enP = (rk: string, vk: string, t: number) => { +const enP = (rk: string, vk: string, t: number) => { const enc = des.des(vk, rk + t, 1, 0, null, 1); return Base64.stringify(Latin1.parse(enc)); }; -export const dnP = (vk: string, str: string) => { +const dnP = (vk: string, str: string) => { const s = Latin1.stringify(Base64.parse(str)); const p = des.des(vk, s, 0, 0, null, 1); return p; }; -export const enC = (rk: string, vk: string, m: string) => { +const enC = (rk: string, vk: string, m: string) => { const enc = HmacMD5(m + rk, vk); return Base64.stringify(enc); }; -export const getRequestKey = (cmdId: number, data: any) => { +const getRequestKey = (cmdId: number, data: any) => { return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`; }; // 加工请求数据 export const transformRequest = (config: any) => { - const { headerType = 2, paramType = 0, cmdId, tid, ...reset } = config.headers; + const { headerType = 2, paramType = 0, cmdId, ...reset } = config.headers; const headers: Record = {}; // const { tenantInfo } = storeToRefs(useTenantStore()); // const { userInfo } = storeToRefs(useUserStore()); @@ -153,10 +132,8 @@ export const transformRequest = (config: any) => { const rk = md5(toString(Math.random() + t)).substring(0, 8); const vk = appConfig.app.vk as string; const pwds = enP(rk, vk, t); + // const tid = cmdId !== '371130' ? storageManager.session.getItem(STORAGE_KEYS.TENANT_TID) : ''; - const tenantInfo = { - tid: 3, - }; let userInfo = { cust_id: '', cust_name: '', @@ -221,7 +198,7 @@ export const transformRequest = (config: any) => { headers.cmdId = cmdId; headers.aseqId = appConfig.app.aseqId; headers.nc = appConfig.app.nc; - headers.tid = tid ?? tenantInfo.tid ?? ''; + headers.tid = 3; // 试玩游戏cust_id=0 header需要保持一致 headers.custId = (config.data?.cust_id === 0 ? '0' : '') || userInfo?.cust_id || ''; headers.reqId = uuid(32, 16); diff --git a/utils/sessionStorage.ts b/utils/sessionStorage.ts deleted file mode 100644 index 33baed2..0000000 --- a/utils/sessionStorage.ts +++ /dev/null @@ -1,220 +0,0 @@ -/** - * Session Storage 实现 - * - * React Native 没有原生的 sessionStorage,这里提供一个内存实现 - * 数据只在应用运行期间保存,应用关闭后会丢失 - * - * 特点: - * - 数据存储在内存中 - * - 应用重启后数据丢失 - * - 适用于临时数据、会话数据 - * - API 与 localStorage 类似 - */ - -/** - * Session Storage 键名常量 - */ -export enum SESSION_KEYS { - TEMP_DATA = 'temp_data', - FORM_DRAFT = 'form_draft', - SEARCH_HISTORY = 'search_history', - CURRENT_TAB = 'current_tab', - SCROLL_POSITION = 'scroll_position', - FILTER_STATE = 'filter_state', -} - -/** - * Session Storage 类 - * - * 使用 Map 实现内存存储 - */ -class SessionStorage { - private static storage: Map = new Map(); - - /** - * 存储字符串 - */ - static setString(key: string, value: string): void { - try { - this.storage.set(key, value); - if (__DEV__) { - console.log(`💾 SessionStorage set: ${key}`); - } - } catch (error) { - console.error(`SessionStorage setString error for key "${key}":`, error); - throw error; - } - } - - /** - * 获取字符串 - */ - static getString(key: string): string | null { - try { - const value = this.storage.get(key) ?? null; - if (__DEV__) { - console.log(`📖 SessionStorage get: ${key}`, value ? '✓' : '✗'); - } - return value; - } catch (error) { - console.error(`SessionStorage getString error for key "${key}":`, error); - return null; - } - } - - /** - * 存储对象(自动序列化为 JSON) - */ - static setObject(key: string, value: T): void { - try { - const jsonValue = JSON.stringify(value); - this.storage.set(key, jsonValue); - if (__DEV__) { - console.log(`💾 SessionStorage set object: ${key}`); - } - } catch (error) { - console.error(`SessionStorage setObject error for key "${key}":`, error); - throw error; - } - } - - /** - * 获取对象(自动反序列化 JSON) - */ - static getObject(key: string): T | null { - try { - const jsonValue = this.storage.get(key); - if (jsonValue === undefined) { - return null; - } - const value = JSON.parse(jsonValue) as T; - if (__DEV__) { - console.log(`📖 SessionStorage get object: ${key} ✓`); - } - return value; - } catch (error) { - console.error(`SessionStorage getObject error for key "${key}":`, error); - return null; - } - } - - /** - * 删除指定键 - */ - static remove(key: string): void { - try { - this.storage.delete(key); - if (__DEV__) { - console.log(`🗑️ SessionStorage remove: ${key}`); - } - } catch (error) { - console.error(`SessionStorage remove error for key "${key}":`, error); - throw error; - } - } - - /** - * 清空所有存储 - */ - static clear(): void { - try { - this.storage.clear(); - if (__DEV__) { - console.log('🗑️ SessionStorage cleared all'); - } - } catch (error) { - console.error('SessionStorage clear error:', error); - throw error; - } - } - - /** - * 获取所有键名 - */ - static getAllKeys(): string[] { - try { - const keys = Array.from(this.storage.keys()); - if (__DEV__) { - console.log('🔑 SessionStorage all keys:', keys); - } - return keys; - } catch (error) { - console.error('SessionStorage getAllKeys error:', error); - return []; - } - } - - /** - * 获取存储项数量 - */ - static get length(): number { - return this.storage.size; - } - - /** - * 检查键是否存在 - */ - static has(key: string): boolean { - return this.storage.has(key); - } - - /** - * 批量获取 - */ - static multiGet(keys: string[]): [string, string | null][] { - try { - return keys.map((key) => [key, this.storage.get(key) ?? null]); - } catch (error) { - console.error('SessionStorage multiGet error:', error); - return []; - } - } - - /** - * 批量设置 - */ - static multiSet(keyValuePairs: [string, string][]): void { - try { - keyValuePairs.forEach(([key, value]) => { - this.storage.set(key, value); - }); - if (__DEV__) { - console.log(`💾 SessionStorage multiSet: ${keyValuePairs.length} items`); - } - } catch (error) { - console.error('SessionStorage multiSet error:', error); - throw error; - } - } - - /** - * 批量删除 - */ - static multiRemove(keys: string[]): void { - try { - keys.forEach((key) => { - this.storage.delete(key); - }); - if (__DEV__) { - console.log(`🗑️ SessionStorage multiRemove: ${keys.length} items`); - } - } catch (error) { - console.error('SessionStorage multiRemove error:', error); - throw error; - } - } - - /** - * 获取所有数据(调试用) - */ - static getAll(): Record { - const result: Record = {}; - this.storage.forEach((value, key) => { - result[key] = value; - }); - return result; - } -} - -export default SessionStorage; - diff --git a/utils/storage.ts b/utils/storage.ts deleted file mode 100644 index 88632b5..0000000 --- a/utils/storage.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * AsyncStorage 封装工具 - * 提供类型安全的本地存储操作 - */ - -import AsyncStorage from '@react-native-async-storage/async-storage'; - -/** - * 存储键名常量 - */ -export enum STORAGE_KEYS { - AUTH_TOKEN = 'auth_token', - USER_INFO = 'user_info', - SETTINGS = 'settings', - THEME = 'theme', - LANGUAGE = 'language', - USER_PREFERENCES = 'user_preferences', - TENANT_STORE = 'tenant_storage', - USER_STORE = 'user_storage', - SETTINGS_STORE = 'settings_storage', -} - -/** - * Storage 工具类 - */ -class Storage { - /** - * 存储字符串 - */ - static async setString(key: string, value: string): Promise { - try { - await AsyncStorage.setItem(key, value); - if (__DEV__) { - console.log(`💾 Storage set: ${key}`); - } - } catch (error) { - console.error(`Storage setString error for key "${key}":`, error); - throw error; - } - } - - /** - * 获取字符串 - */ - static async getString(key: string): Promise { - try { - const value = await AsyncStorage.getItem(key); - if (__DEV__) { - console.log(`📖 Storage get: ${key}`, value ? '✓' : '✗'); - } - return value; - } catch (error) { - console.error(`Storage getString error for key "${key}":`, error); - return null; - } - } - - /** - * 存储对象(自动序列化为 JSON) - */ - static async setObject(key: string, value: T): Promise { - try { - const jsonValue = JSON.stringify(value); - await AsyncStorage.setItem(key, jsonValue); - if (__DEV__) { - console.log(`💾 Storage set object: ${key}`); - } - } catch (error) { - console.error(`Storage setObject error for key "${key}":`, error); - throw error; - } - } - - /** - * 获取对象(自动反序列化 JSON) - */ - static async getObject(key: string): Promise { - try { - const jsonValue = await AsyncStorage.getItem(key); - if (jsonValue === null) { - return null; - } - const value = JSON.parse(jsonValue) as T; - if (__DEV__) { - console.log(`📖 Storage get object: ${key} ✓`); - } - return value; - } catch (error) { - console.error(`Storage getObject error for key "${key}":`, error); - return null; - } - } - - /** - * 删除指定键 - */ - static async remove(key: string): Promise { - try { - await AsyncStorage.removeItem(key); - if (__DEV__) { - console.log(`🗑️ Storage remove: ${key}`); - } - } catch (error) { - console.error(`Storage remove error for key "${key}":`, error); - throw error; - } - } - - /** - * 清空所有存储 - */ - static async clear(): Promise { - try { - await AsyncStorage.clear(); - if (__DEV__) { - console.log('🗑️ Storage cleared all'); - } - } catch (error) { - console.error('Storage clear error:', error); - throw error; - } - } - - /** - * 获取所有键名 - */ - static async getAllKeys(): Promise { - try { - const keys = await AsyncStorage.getAllKeys(); - if (__DEV__) { - console.log('🔑 Storage all keys:', keys); - } - return keys; - } catch (error) { - console.error('Storage getAllKeys error:', error); - return []; - } - } - - /** - * 批量获取 - */ - static async multiGet(keys: string[]): Promise<[string, string | null][]> { - try { - const values = await AsyncStorage.multiGet(keys); - return values; - } catch (error) { - console.error('Storage multiGet error:', error); - return []; - } - } - - /** - * 批量设置 - */ - static async multiSet(keyValuePairs: [string, string][]): Promise { - try { - await AsyncStorage.multiSet(keyValuePairs); - if (__DEV__) { - console.log(`💾 Storage multiSet: ${keyValuePairs.length} items`); - } - } catch (error) { - console.error('Storage multiSet error:', error); - throw error; - } - } - - /** - * 批量删除 - */ - static async multiRemove(keys: string[]): Promise { - try { - await AsyncStorage.multiRemove(keys); - if (__DEV__) { - console.log(`🗑️ Storage multiRemove: ${keys.length} items`); - } - } catch (error) { - console.error('Storage multiRemove error:', error); - throw error; - } - } -} - -export default Storage; diff --git a/utils/storageManager.ts b/utils/storageManager.ts index 4762b7b..e76a36c 100644 --- a/utils/storageManager.ts +++ b/utils/storageManager.ts @@ -1,242 +1,307 @@ /** * 统一存储管理器 - * + * * 提供统一的接口来使用 localStorage (AsyncStorage) 或 sessionStorage - * + * * 使用场景: - * - localStorage: 持久化数据,应用重启后仍然存在 - * - sessionStorage: 临时数据,应用重启后丢失 - * + * - local: 持久化数据,应用重启后仍然存在 + * - session: 临时数据,应用重启后丢失 + * * 示例: * ```typescript - * // 使用 localStorage(默认) - * await StorageManager.set('user', userData); - * + * // 使用 localStorage + * await storageManager.local.setItem('user', userData); + * const user = await storageManager.local.getItem('user'); + * * // 使用 sessionStorage - * await StorageManager.set('temp', tempData, { type: 'session' }); - * - * // 获取数据(自动从正确的存储中读取) - * const user = await StorageManager.get('user'); - * const temp = await StorageManager.get('temp', { type: 'session' }); + * storageManager.session.setItem('temp', tempData); + * const temp = storageManager.session.getItem('temp'); * ``` */ -import Storage from './storage'; -import SessionStorage from './sessionStorage'; +import AsyncStorage from '@react-native-async-storage/async-storage'; /** - * 存储类型 + * 存储键名常量 */ -export type StorageType = 'local' | 'session'; +export enum STORAGE_KEYS { + AUTH_TOKEN = 'auth_token', + THEME = 'theme', + LANGUAGE = 'language', + USER_PREFERENCES = 'user_preferences', + TENANT_STORE = 'tenant_storage', + + USER_STORE = 'user_storage', + USER_INFO = 'user_info', + + // 游戏相关 + GAME_STORE = 'game_storage', + GAME_TRY = 'game_try', + + SETTINGS_STORE = 'settings_storage', + MSG_STORE = 'msg_storage', + TEMP_DATA = 'temp_data', + FORM_DRAFT = 'form_draft', + SEARCH_HISTORY = 'search_history', + CURRENT_TAB = 'current_tab', + SCROLL_POSITION = 'scroll_position', + FILTER_STATE = 'filter_state', + TENANT_TID = 'tenant_tid', + + APP_CONFIG = 'app_config', + APP_THEME = 'app_theme', // 主题 + APP_PLATFORM = 'app_platform', // 平台 + APP_TEMPLATE = 'app_template', // 模板 + APP_LANGUAGE = 'app_language', // 语言 + APP_ACTIVE_FOOTER_TAB_MENU = 'app_active_footer_tab_menu', // 底部菜单 + APP_MAIN_MENU = 'app_main_menu', // 首页一级菜单原始数据 + APP_ACTIVE_MAIN_MENU_TAB = 'app_active_main_menu_tab', // 游戏列表页主菜单 + APP_ACTIVE_SUB_MENU_TAB = 'app_active_sub_menu_tab', // 游戏列表页二级菜单 + APP_ACTIVE_CHILD_MENU_TAB = 'app_active_child_menu_tab', // 游戏列表页三级菜单 + APP_USER_INFO = 'app_user_info', // 登录用户信息 + POPUP_MODAL_LIST = 'app_popup_modal_list', // 首页弹窗列表 + POPUP_MODAL_STATUS_MAP = 'popup_modal_status_map', // 首页弹窗状态 + RECOVER_PASSWORD = 'recover_password', // 找回密码 + CDN_PAGE_DATA_LOADED = 'cdn_page_data_loaded', // CDN首页数据是否已加载 +} /** - * 存储选项 + * 数据类型标记 */ -export interface StorageOptions { - /** - * 存储类型 - * - 'local': 持久化存储(AsyncStorage) - * - 'session': 会话存储(内存) - */ - type?: StorageType; +enum DataType { + STRING = 'string', + NUMBER = 'number', + BOOLEAN = 'boolean', + OBJECT = 'object', + ARRAY = 'array', + NULL = 'null', } /** - * 统一存储管理器 + * 带类型标记的存储值 */ -class StorageManager { - /** - * 存储字符串 - */ - static async setString( - key: string, - value: string, - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - SessionStorage.setString(key, value); - } else { - await Storage.setString(key, value); - } - } - - /** - * 获取字符串 - */ - static async getString( - key: string, - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.getString(key); - } else { - return await Storage.getString(key); - } - } +interface TypedValue { + type: DataType; + value: string; +} +/** + * 存储工具基类 + * 提供公共的序列化、反序列化和类型转换方法 + */ +abstract class StorageBase { /** - * 存储对象 + * 获取数据类型 */ - static async setObject( - key: string, - value: T, - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - SessionStorage.setObject(key, value); - } else { - await Storage.setObject(key, value); - } + protected static getDataType(value: any): DataType { + if (value === null) return DataType.NULL; + if (Array.isArray(value)) return DataType.ARRAY; + const typeStr = typeof value; + const dataType = DataType[typeStr.toUpperCase() as keyof typeof DataType]; + return dataType || DataType.OBJECT; } /** - * 获取对象 + * 序列化值(带类型标记) */ - static async getObject( - key: string, - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.getObject(key); - } else { - return await Storage.getObject(key); - } + protected static serializeValue(value: any): string { + const type = this.getDataType(value); + const typedValue: TypedValue = { + type, + value: typeof value === 'string' ? value : JSON.stringify(value), + }; + return JSON.stringify(typedValue); } /** - * 删除指定键 + * 反序列化值(自动转换类型) */ - static async remove(key: string, options: StorageOptions = {}): Promise { - const { type = 'local' } = options; + protected static deserializeValue(data: string): any { + try { + const typedValue: TypedValue = JSON.parse(data); + const { type, value } = typedValue; - if (type === 'session') { - SessionStorage.remove(key); - } else { - await Storage.remove(key); + switch (type) { + case DataType.STRING: + return value; + case DataType.NUMBER: + return Number(value); + case DataType.BOOLEAN: + return value === 'true'; + case DataType.NULL: + return null; + case DataType.OBJECT: + case DataType.ARRAY: + return JSON.parse(value); + default: + return value; + } + } catch (error) { + console.error('StorageBase deserialize error:', error); + return null; } } +} +/** + * 本地存储类(AsyncStorage) + * 继承 StorageBase,使用其公共的序列化和反序列化方法 + */ +class LocalStorage extends StorageBase { /** - * 清空指定类型的所有存储 + * 存储数据(自动序列化和类型标记) */ - static async clear(options: StorageOptions = {}): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - SessionStorage.clear(); - } else { - await Storage.clear(); + static async setItem(key: string, value: any): Promise { + try { + const serialized = this.serializeValue(value); + await AsyncStorage.setItem(key, serialized); + if (__DEV__) { + console.log(`💾 LocalStorage set: ${key}`); + } + } catch (error) { + console.error(`LocalStorage setItem error for key "${key}":`, error); + throw error; } } /** - * 获取所有键名 + * 获取数据(自动反序列化和类型转换) */ - static async getAllKeys(options: StorageOptions = {}): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.getAllKeys(); - } else { - return await Storage.getAllKeys(); + static async getItem(key: string): Promise { + try { + const value = await AsyncStorage.getItem(key); + if (value === null) { + if (__DEV__) { + console.log(`📖 LocalStorage get: ${key} ✗`); + } + return null; + } + const result = this.deserializeValue(value); + if (__DEV__) { + console.log(`📖 LocalStorage get: ${key} ✓`); + } + return result; + } catch (error) { + console.error(`LocalStorage getItem error for key "${key}":`, error); + return null; } } /** - * 检查键是否存在 + * 删除指定键 */ - static async has(key: string, options: StorageOptions = {}): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.has(key); - } else { - const value = await Storage.getString(key); - return value !== null; + static async removeItem(key: string): Promise { + try { + await AsyncStorage.removeItem(key); + if (__DEV__) { + console.log(`🗑️ LocalStorage remove: ${key}`); + } + } catch (error) { + console.error(`LocalStorage removeItem error for key "${key}":`, error); + throw error; } } /** - * 批量获取 + * 清空所有存储 */ - static async multiGet( - keys: string[], - options: StorageOptions = {} - ): Promise<[string, string | null][]> { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.multiGet(keys); - } else { - return await Storage.multiGet(keys); + static async clear(): Promise { + try { + await AsyncStorage.clear(); + if (__DEV__) { + console.log('🗑️ LocalStorage cleared all'); + } + } catch (error) { + console.error('LocalStorage clear error:', error); + throw error; } } +} + +/** + * 会话存储类(内存实现) + * 继承 StorageBase,使用其公共的序列化和反序列化方法 + */ +class SessionStorage extends StorageBase { + private static storage: Map = new Map(); /** - * 批量设置 + * 存储数据(自动序列化和类型标记) */ - static async multiSet( - keyValuePairs: [string, string][], - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - SessionStorage.multiSet(keyValuePairs); - } else { - await Storage.multiSet(keyValuePairs); + static setItem(key: string, value: any): void { + try { + const serialized = this.serializeValue(value); + this.storage.set(key, serialized); + if (__DEV__) { + console.log(`💾 SessionStorage set: ${key}`); + } + } catch (error) { + console.error(`SessionStorage setItem error for key "${key}":`, error); + throw error; } } /** - * 批量删除 + * 获取数据(自动反序列化和类型转换) */ - static async multiRemove( - keys: string[], - options: StorageOptions = {} - ): Promise { - const { type = 'local' } = options; - - if (type === 'session') { - SessionStorage.multiRemove(keys); - } else { - await Storage.multiRemove(keys); + static getItem(key: string): any { + try { + const value = this.storage.get(key); + if (value === undefined) { + if (__DEV__) { + console.log(`📖 SessionStorage get: ${key} ✗`); + } + return null; + } + const result = this.deserializeValue(value); + if (__DEV__) { + console.log(`📖 SessionStorage get: ${key} ✓`); + } + return result; + } catch (error) { + console.error(`SessionStorage getItem error for key "${key}":`, error); + return null; } } /** - * 获取存储大小(仅 session storage) + * 删除指定键 */ - static getSize(options: StorageOptions = {}): number { - const { type = 'local' } = options; - - if (type === 'session') { - return SessionStorage.length; - } else { - // AsyncStorage 不支持直接获取大小 - return -1; + static removeItem(key: string): void { + try { + this.storage.delete(key); + if (__DEV__) { + console.log(`🗑️ SessionStorage remove: ${key}`); + } + } catch (error) { + console.error(`SessionStorage removeItem error for key "${key}":`, error); + throw error; } } /** - * 清空所有存储(local + session) + * 清空所有存储 */ - static async clearAll(): Promise { - await Storage.clear(); - SessionStorage.clear(); - if (__DEV__) { - console.log('🗑️ All storage cleared (local + session)'); + static clear(): void { + try { + this.storage.clear(); + if (__DEV__) { + console.log('🗑️ SessionStorage cleared all'); + } + } catch (error) { + console.error('SessionStorage clear error:', error); + throw error; } } } -export default StorageManager; +/** + * 统一存储管理器 + */ +const storageManager = { + local: LocalStorage, + session: SessionStorage, +}; + +export default storageManager;