feat: 首页更新
This commit is contained in:
@@ -29,42 +29,28 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Tab One',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
headerRight: () => (
|
||||
<Link href="/modal" asChild>
|
||||
<Pressable>
|
||||
{({ pressed }) => (
|
||||
<FontAwesome
|
||||
name="info-circle"
|
||||
size={25}
|
||||
color={Colors[colorScheme ?? 'light'].text}
|
||||
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
</Link>
|
||||
),
|
||||
title: '首页',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="home" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="two"
|
||||
options={{
|
||||
title: 'Tab Two',
|
||||
title: '充值',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="demo"
|
||||
options={{
|
||||
title: '完整示例',
|
||||
title: '活动',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="rocket" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="paper"
|
||||
options={{
|
||||
title: 'Paper UI',
|
||||
title: '我的',
|
||||
tabBarIcon: ({ color }) => <TabBarIcon name="paint-brush" color={color} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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<any>(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<any>(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('成功', '会话存储已清空');
|
||||
|
||||
@@ -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<string>('');
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
if (__DEV__) {
|
||||
Alert.alert('提示', '开发模式下无法检查更新,请使用生产构建测试热更新功能');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(true);
|
||||
setUpdateInfo('正在检查更新...');
|
||||
|
||||
try {
|
||||
const update = await Updates.checkForUpdateAsync();
|
||||
|
||||
if (update.isAvailable) {
|
||||
setUpdateInfo('发现新版本,正在下载...');
|
||||
await Updates.fetchUpdateAsync();
|
||||
|
||||
Alert.alert('更新完成', '新版本已下载完成,是否立即重启应用?', [
|
||||
{
|
||||
text: '稍后',
|
||||
style: 'cancel',
|
||||
onPress: () => setUpdateInfo('更新已下载,稍后重启应用即可应用'),
|
||||
},
|
||||
{
|
||||
text: '立即重启',
|
||||
onPress: async () => {
|
||||
await Updates.reloadAsync();
|
||||
},
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setUpdateInfo('当前已是最新版本');
|
||||
}
|
||||
} catch (error) {
|
||||
setUpdateInfo('检查更新失败: ' + (error as Error).message);
|
||||
Alert.alert('错误', '检查更新失败,请稍后重试');
|
||||
} finally {
|
||||
setIsChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getUpdateInfo = () => {
|
||||
const { isEmbeddedLaunch, isEmergencyLaunch, updateId, channel, runtimeVersion } =
|
||||
Updates.useUpdates();
|
||||
|
||||
return `
|
||||
运行模式: ${__DEV__ ? '开发模式' : '生产模式'}
|
||||
是否为内嵌启动: ${isEmbeddedLaunch ? '是' : '否'}
|
||||
是否为紧急启动: ${isEmergencyLaunch ? '是' : '否'}
|
||||
更新 ID: ${updateId || '无'}
|
||||
更新通道: ${channel || '无'}
|
||||
运行时版本: ${runtimeVersion || '无'}
|
||||
`.trim();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log('=== TabOneScreen 组件已渲染 ===');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>🚀 热更新演示</Text>
|
||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
||||
|
||||
<View style={styles.infoContainer}>
|
||||
<Text style={styles.infoTitle}>当前版本信息:</Text>
|
||||
<Text style={styles.infoText}>{getUpdateInfo()}</Text>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isChecking && styles.buttonDisabled]}
|
||||
onPress={checkForUpdates}
|
||||
disabled={isChecking}
|
||||
>
|
||||
{isChecking ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text style={styles.buttonText}>检查更新</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{updateInfo ? (
|
||||
<View style={styles.updateInfoContainer}>
|
||||
<Text style={styles.updateInfoText}>{updateInfo}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.instructionsContainer}>
|
||||
<Text style={styles.instructionsTitle}>📝 使用说明:</Text>
|
||||
<Text style={styles.instructionsText}>
|
||||
1. 使用 EAS Build 构建生产版本{'\n'}
|
||||
2. 修改代码后运行 eas update 发布更新{'\n'}
|
||||
3. 打开应用点击"检查更新"按钮{'\n'}
|
||||
4. 应用会自动下载并提示重启
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: '首页',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<HomeScreen />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
182
constants/game.ts
Normal file
182
constants/game.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { filter, toNumber, isNaN, forEach } from 'lodash-es';
|
||||
|
||||
// 1:棋牌 2:彩票 3:电子 4:捕鱼 5:真人 6:体育 7:红包 8:原创 10: 区块链
|
||||
export enum GameMainTypesEnum {
|
||||
CHESS = 1,
|
||||
LOTTERY = 2,
|
||||
ELECTRONIC = 3,
|
||||
FISHING = 4,
|
||||
LIVE = 5,
|
||||
SPORTS = 6,
|
||||
BLOCK = 8,
|
||||
BLOCK_THIRD = 39,
|
||||
|
||||
// 自定义
|
||||
LOBBY = 100, // 大厅
|
||||
COLLECT = 101, // 收藏
|
||||
RECENT = 102, // 最近
|
||||
RECOMMEND = 103, // 推荐
|
||||
HIGH_HIT = 104, // 高暴奖
|
||||
TRIAL = 105, // 试玩
|
||||
HOT = 0, // 热门
|
||||
HOT_GAME = 10000, // 热门游戏
|
||||
HOT_CHESS = 10001, // 热门棋牌
|
||||
HOT_LOTTERY = 10002, // 热门彩票
|
||||
HOT_ELECTRONIC = 10003, // 热门电子
|
||||
HOT_FISHING = 10004, // 热门捕鱼
|
||||
HOT_LIVE = 10005, // 热门真人
|
||||
HOT_SPORTS = 10006, // 热门体育
|
||||
HOT_BLOCK = 10008, // 热门原创
|
||||
HOT_BLOCK_THIRD = 10039, // 热门区块链
|
||||
}
|
||||
|
||||
export enum GameBigTypesEnum {
|
||||
CHESS = 1,
|
||||
ELECTRONIC = 2,
|
||||
FISHING = 3,
|
||||
LIVE = 4,
|
||||
SPORTS = 5,
|
||||
LOTTERY = 7,
|
||||
BLOCK_THIRD = 10,
|
||||
}
|
||||
|
||||
// 首页三级菜单
|
||||
export enum GameChildTypesEnum {
|
||||
ALL = '1', // 所有
|
||||
HOT = '2', // 热门
|
||||
RECENT = '3', // 最近
|
||||
COLLECT = '4', // 收藏
|
||||
}
|
||||
|
||||
export enum LotteryTypeKeysEnum {
|
||||
tianCheng = 15,
|
||||
GPI = 16,
|
||||
SBCP = 30,
|
||||
DB = 31,
|
||||
SGWIN = 32,
|
||||
}
|
||||
|
||||
export enum LotteryTypeIdEnum {
|
||||
GPI = 504,
|
||||
DB = 1385,
|
||||
SBCP = 1326,
|
||||
SGWIN = 1410,
|
||||
tianCheng = 468,
|
||||
}
|
||||
|
||||
export const LotteryNameByKey = {
|
||||
GPI: 'GPI',
|
||||
DB: 'DB',
|
||||
SGWIN: 'SGWin',
|
||||
tianCheng: '天成',
|
||||
};
|
||||
|
||||
export enum GameMainKeysEnum {
|
||||
CHESS = 'chess',
|
||||
LOTTERY = 'lottery', // 热门彩票
|
||||
LOTTERY_ALL = 'lotteryAll', // 全部彩票
|
||||
ELECTRONIC = 'electronic',
|
||||
FISHING = 'fishing',
|
||||
BLOCK_THIRD = 'blockThird',
|
||||
LIVE = 'live',
|
||||
SPORTS = 'sports',
|
||||
BLOCK = 'block',
|
||||
|
||||
LOBBY = 'lobby', // 大厅
|
||||
COLLECT = 'collect', // 收藏
|
||||
RECENT = 'recent', // 最近
|
||||
RECOMMEND = 'recommend', // 推荐
|
||||
HIGH_HIT = 'highHit', // 高暴奖
|
||||
TRIAL = 'trial', // 试玩
|
||||
HOT = 'hot', // 热门
|
||||
HOT_GAME = 'hotGame', // 热门游戏
|
||||
HOT_CHESS = 'hotChess', // 热门棋牌
|
||||
HOT_LOTTERY = 'hotLottery', // 热门彩票
|
||||
HOT_ELECTRONIC = 'hotElectronic', // 热门电子
|
||||
HOT_FISHING = 'hotFishing', // 热门捕鱼
|
||||
HOT_LIVE = 'hotLive', // 热门真人
|
||||
HOT_SPORTS = 'hotSports', // 热门体育
|
||||
HOT_BLOCK = 'hotBlock', // 热门原创
|
||||
HOT_BLOCK_THIRD = 'hotBlockThird', // 热门区块链
|
||||
}
|
||||
|
||||
export const gameMainTypesMap = (() => {
|
||||
const result = {};
|
||||
forEach(
|
||||
filter(Object.keys(GameMainTypesEnum), val => isNaN(toNumber(val))),
|
||||
(key) => {
|
||||
if (GameMainKeysEnum[key]) {
|
||||
result[GameMainTypesEnum[key]] = GameMainKeysEnum[key];
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log('resultresultresult', result);
|
||||
return result as Record<GameMainTypesEnum, GameMainKeysEnum>;
|
||||
})();
|
||||
|
||||
// 默认首页游戏分类菜单
|
||||
export const defaultHomeGameTabMenus = [
|
||||
{ name: '推荐', key: GameMainTypesEnum.RECOMMEND, icon: 'recommend' },
|
||||
// { name: '试玩', key: GameMainTypesEnum.TRIAL, icon: 'try' },
|
||||
{ name: '收藏', key: GameMainTypesEnum.COLLECT, icon: 'home-star-solid' },
|
||||
{ name: '最近', key: GameMainTypesEnum.RECENT, icon: 'clock-solid' },
|
||||
];
|
||||
|
||||
export const gameTypeName = name => {
|
||||
console.log(name);
|
||||
let iconName = '';
|
||||
switch (name) {
|
||||
case '棋牌':
|
||||
iconName = 'chess';
|
||||
break;
|
||||
case '电子':
|
||||
iconName = 'electronic';
|
||||
break;
|
||||
case '捕鱼':
|
||||
iconName = 'fishing';
|
||||
break;
|
||||
case '真人':
|
||||
iconName = 'live';
|
||||
break;
|
||||
case '体育':
|
||||
iconName = 'sports';
|
||||
break;
|
||||
case '彩票':
|
||||
iconName = 'lottery';
|
||||
break;
|
||||
case '原创':
|
||||
iconName = 'block';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return iconName;
|
||||
};
|
||||
|
||||
export const bigTypeNameConfig = {
|
||||
1: GameMainKeysEnum.CHESS,
|
||||
2: GameMainKeysEnum.ELECTRONIC,
|
||||
3: GameMainKeysEnum.FISHING,
|
||||
4: GameMainKeysEnum.LIVE,
|
||||
5: GameMainKeysEnum.SPORTS,
|
||||
7: GameMainKeysEnum.LOTTERY,
|
||||
10: GameMainKeysEnum.BLOCK_THIRD,
|
||||
};
|
||||
|
||||
export const serverImgPathMap = {
|
||||
[GameBigTypesEnum.CHESS]: 'qipai',
|
||||
[GameBigTypesEnum.ELECTRONIC]: 'dianzi',
|
||||
[GameBigTypesEnum.FISHING]: 'buyu',
|
||||
[GameBigTypesEnum.BLOCK_THIRD]: 'qukuailian',
|
||||
};
|
||||
|
||||
export const bigTypeToMainTypeMap = {
|
||||
1: GameMainTypesEnum.CHESS,
|
||||
2: GameMainTypesEnum.ELECTRONIC,
|
||||
3: GameMainTypesEnum.FISHING,
|
||||
4: GameMainTypesEnum.LIVE,
|
||||
5: GameMainTypesEnum.SPORTS,
|
||||
7: GameMainTypesEnum.LOTTERY,
|
||||
10: GameMainTypesEnum.BLOCK_THIRD,
|
||||
};
|
||||
6
constants/theme.ts
Normal file
6
constants/theme.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// theme enum
|
||||
export enum ThemeEnum {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
ORANGE = 'orange',
|
||||
}
|
||||
8
constants/user.ts
Normal file
8
constants/user.ts
Normal file
@@ -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, // 批量开户注册
|
||||
}
|
||||
87
hooks/useGameMenus.ts
Normal file
87
hooks/useGameMenus.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import useGameStore from '@/stores/gameStore';
|
||||
import { GameMainTypesEnum, defaultHomeGameTabMenus, gameMainTypesMap } from '@/constants/game';
|
||||
import { ThemeEnum } from '@/constants/theme';
|
||||
import { forEach, cloneDeep, map, filter } from 'lodash-es';
|
||||
import { useIsLoggedIn } from '@/stores/userStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
// 有子菜单的游戏类型
|
||||
const hasSubGameMainTypes = [
|
||||
GameMainTypesEnum.CHESS,
|
||||
GameMainTypesEnum.ELECTRONIC,
|
||||
GameMainTypesEnum.FISHING,
|
||||
GameMainTypesEnum.BLOCK_THIRD,
|
||||
];
|
||||
|
||||
export const useGameMainMenus = (theme: ThemeEnum) => {
|
||||
// 在 hook 顶层调用 useIsLoggedIn
|
||||
const isLogin = useIsLoggedIn();
|
||||
|
||||
// 从 store 获取必要的数据 - 直接获取,不使用 useShallow
|
||||
const menuSort = useGameStore((state) => state.menuSort);
|
||||
const gameBigClass = useGameStore((state) => state.gameBigClass);
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('🎮 useGameMainMenus - menuSort:', menuSort, 'length:', menuSort?.length);
|
||||
}
|
||||
// 使用 useMemo 缓存计算结果,避免每次都创建新对象
|
||||
return useMemo(() => {
|
||||
const defaultMenus = cloneDeep(defaultHomeGameTabMenus);
|
||||
|
||||
if (theme === ThemeEnum.DARK) {
|
||||
forEach(defaultMenus, (item) => {
|
||||
if (item.key === GameMainTypesEnum.RECENT) {
|
||||
item.icon = 'clock-solid_dark';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const gameMenu = map(menuSort, (item: Record<string, any>) => {
|
||||
const typeName = gameMainTypesMap[item.type as GameMainTypesEnum];
|
||||
|
||||
const children = hasSubGameMainTypes.includes(item.type)
|
||||
? map(gameBigClass?.[typeName], (it) => {
|
||||
return {
|
||||
name: it.play_cname,
|
||||
key: `${it.play_id}`,
|
||||
play_sort: it.play_sort,
|
||||
darkImgSrc: `/images/game/${typeName}/dark_${it.play_id}.png`,
|
||||
lightImgSrc: `/images/game/${typeName}/light_${it.play_id}.png`,
|
||||
colorImgSrc: it.logo3_img_url || `/images/game/${typeName}/color_${it.play_id}.png`,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
name: item.play_name,
|
||||
key: `${item.type}`,
|
||||
icon: typeName,
|
||||
logo: item.logo_url1,
|
||||
children,
|
||||
};
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
console.log(gameMenu, 'gameMenu');
|
||||
}
|
||||
|
||||
return map(
|
||||
[
|
||||
...filter(defaultMenus, (item) => [GameMainTypesEnum.RECOMMEND].includes(item.key)),
|
||||
...(isLogin
|
||||
? filter(gameMenu, (item) => Number(item.key) !== GameMainTypesEnum.TRIAL)
|
||||
: gameMenu),
|
||||
...filter(defaultMenus, (item) =>
|
||||
[GameMainTypesEnum.COLLECT, GameMainTypesEnum.RECENT].includes(item.key)
|
||||
),
|
||||
],
|
||||
(item) => ({
|
||||
...item,
|
||||
key: `${item.key}`,
|
||||
})
|
||||
);
|
||||
}, [theme, isLogin, menuSort, gameBigClass]);
|
||||
};
|
||||
|
||||
export const useMenuDataLoaded = () => useGameStore((state) => state.menuSort?.length > 0);
|
||||
180
pages/HomeScreen/HomeScreenComplete.tsx
Normal file
180
pages/HomeScreen/HomeScreenComplete.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 完整首页容器
|
||||
* 包含 Header、内容区域、BottomTabs
|
||||
* 支持主题切换和真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { View, ScrollView, RefreshControl, StyleSheet, SafeAreaView, Alert } from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import Header from './components/Header';
|
||||
import BannerSwiper from './components/BannerSwiper';
|
||||
import NoticeBar from './components/NoticeBar';
|
||||
import GameMainMenus from './components/GameMainMenus';
|
||||
import Lobby from './components/Lobby';
|
||||
import HighPrizeGame from './components/HighPrizeGame';
|
||||
import FastFootNav from './components/FastFootNav';
|
||||
import { requestHomePageData } from '@/stores/gameStore';
|
||||
import { useTenantLoad } from '@/stores/tenantStore';
|
||||
import type {
|
||||
Banner,
|
||||
Notice,
|
||||
GameCategory,
|
||||
Game,
|
||||
HighPrizeGame as HighPrizeGameType,
|
||||
} from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
interface HomeScreenCompleteProps {
|
||||
theme?: 'light' | 'dark';
|
||||
isDarkTheme?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整首页容器
|
||||
*/
|
||||
export default function HomeScreenComplete({
|
||||
theme = 'light',
|
||||
isDarkTheme = false,
|
||||
}: HomeScreenCompleteProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [selectedCategory, setSelectedCategory] = useState<number>(0);
|
||||
const tenantLoad = useTenantLoad();
|
||||
|
||||
// 加载首页数据
|
||||
const loadHomePageData = useCallback(async () => {
|
||||
try {
|
||||
await requestHomePageData();
|
||||
} catch (error) {
|
||||
console.error('加载首页数据失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 初始化加载
|
||||
useEffect(() => {
|
||||
console.log('租户数据加载完成:', tenantLoad);
|
||||
if (tenantLoad) {
|
||||
loadHomePageData();
|
||||
}
|
||||
}, [loadHomePageData, tenantLoad]);
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
try {
|
||||
await loadHomePageData();
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
}
|
||||
}, [loadHomePageData]);
|
||||
|
||||
// 处理分类选择
|
||||
const handleCategorySelect = useCallback((categoryId: number) => {
|
||||
setSelectedCategory(categoryId);
|
||||
// 这里可以根据分类过滤游戏
|
||||
}, []);
|
||||
|
||||
// 处理游戏点击
|
||||
const handleGamePress = useCallback((game: Game) => {
|
||||
Alert.alert('游戏', `点击了: ${game.play_up_name}`);
|
||||
// 这里可以添加打开游戏的逻辑
|
||||
}, []);
|
||||
|
||||
// 处理底部 Tab 点击
|
||||
const handleTabPress = useCallback((tabId: string, action: string) => {
|
||||
Alert.alert('导航', `点击了: ${tabId}`);
|
||||
// 这里可以添加导航逻辑
|
||||
}, []);
|
||||
|
||||
// 处理搜索
|
||||
const handleSearch = useCallback((keyword: string) => {
|
||||
Alert.alert('搜索', `搜索关键词: ${keyword}`);
|
||||
// 这里可以添加搜索逻辑
|
||||
}, []);
|
||||
|
||||
// 根据主题选择要显示的组件
|
||||
const renderContent = () => {
|
||||
if (isDarkTheme || actualTheme === 'dark') {
|
||||
// 深色主题布局
|
||||
return (
|
||||
<>
|
||||
<GameMainMenus
|
||||
theme={actualTheme}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
<BannerSwiper theme={actualTheme} />
|
||||
<NoticeBar theme={actualTheme} />
|
||||
<HighPrizeGame theme={actualTheme} onGamePress={handleGamePress} />
|
||||
<Lobby theme={actualTheme} onGamePress={handleGamePress} />
|
||||
<FastFootNav theme={actualTheme} onTabPress={handleTabPress} />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// 浅色主题布局
|
||||
return (
|
||||
<>
|
||||
<BannerSwiper theme={actualTheme} />
|
||||
<NoticeBar theme={actualTheme} />
|
||||
<GameMainMenus
|
||||
theme={actualTheme}
|
||||
selectedCategory={selectedCategory}
|
||||
onCategorySelect={handleCategorySelect}
|
||||
/>
|
||||
<Lobby theme={actualTheme} onGamePress={handleGamePress} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={s.container}>
|
||||
{/* Header */}
|
||||
<Header
|
||||
theme={actualTheme}
|
||||
onSearch={handleSearch}
|
||||
onMessagePress={() => Alert.alert('消息', '消息功能')}
|
||||
onUserPress={() => Alert.alert('用户', '用户中心')}
|
||||
unreadCount={3}
|
||||
/>
|
||||
|
||||
{/* 内容区域 */}
|
||||
<View style={s.contentContainer}>
|
||||
<ScrollView
|
||||
style={s.contentContainer}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={Colors[actualTheme].primary}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={s.scrollContent}>{renderContent()}</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
156
pages/HomeScreen/components/BannerSwiper/index.tsx
Normal file
156
pages/HomeScreen/components/BannerSwiper/index.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* 轮播图组件
|
||||
*
|
||||
* 展示首页轮播图,支持自动播放和手动滑动
|
||||
* 使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import Colors from '@/constants/Colors';
|
||||
// import type { Banner } from '@/types/home';
|
||||
import { styles } from './styles';
|
||||
import useMsgStore from '@/stores/msgStore';
|
||||
|
||||
|
||||
interface BannerSwiperProps {
|
||||
theme: 'light' | 'dark';
|
||||
}
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 轮播图组件
|
||||
*/
|
||||
export default function BannerSwiper({ theme }: BannerSwiperProps) {
|
||||
const s = styles[theme];
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const autoPlayTimerRef = useRef<any>(null);
|
||||
const { homeBanner } = useMsgStore();
|
||||
|
||||
|
||||
// 加载轮播图数据
|
||||
useEffect(() => {
|
||||
// 如果有传入的 banners 数据,直接使用
|
||||
if (homeBanner.length > 0) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
// 如果没有数据,保持 loading 状态显示骨架屏
|
||||
}, [homeBanner]);
|
||||
|
||||
// 处理 Banner 点击
|
||||
const onBannerPress = useCallback((banner: Record<string, any>) => {
|
||||
Alert.alert('轮播图', `点击了: ${banner.title || banner.id}`);
|
||||
// 这里可以添加导航逻辑
|
||||
}, []);
|
||||
|
||||
// 处理滚动事件
|
||||
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const contentOffsetX = event.nativeEvent.contentOffset.x;
|
||||
const index = Math.round(contentOffsetX / (width - 24));
|
||||
setCurrentIndex(Math.min(index, homeBanner.length - 1));
|
||||
};
|
||||
|
||||
// 启动自动播放
|
||||
const startAutoPlay = useCallback(() => {
|
||||
if (homeBanner.length <= 1) return;
|
||||
autoPlayTimerRef.current = setInterval(() => {
|
||||
setCurrentIndex((prev) => {
|
||||
const nextIndex = (prev + 1) % homeBanner.length;
|
||||
scrollViewRef.current?.scrollTo({
|
||||
x: nextIndex * (width - 24),
|
||||
animated: true,
|
||||
});
|
||||
return nextIndex;
|
||||
});
|
||||
}, 5000);
|
||||
}, [homeBanner.length]);
|
||||
|
||||
// 停止自动播放
|
||||
const stopAutoPlay = useCallback(() => {
|
||||
if (autoPlayTimerRef.current) {
|
||||
clearInterval(autoPlayTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 自动播放
|
||||
useEffect(() => {
|
||||
if (!loading && homeBanner.length > 0) {
|
||||
startAutoPlay();
|
||||
}
|
||||
return () => stopAutoPlay();
|
||||
}, [loading, homeBanner.length, startAutoPlay, stopAutoPlay]);
|
||||
|
||||
// 骨架屏 - 加载中显示占位符
|
||||
if (loading || homeBanner.length === 0) {
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<View
|
||||
style={[
|
||||
s.image,
|
||||
{
|
||||
backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
scrollEventThrottle={16}
|
||||
onScroll={handleScroll}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollBegin={stopAutoPlay}
|
||||
onMomentumScrollEnd={startAutoPlay}
|
||||
>
|
||||
{homeBanner.map((banner) => (
|
||||
<TouchableOpacity
|
||||
key={banner.id}
|
||||
onPress={() => onBannerPress(banner)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: banner.subject }}
|
||||
style={s.image}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
{/* 指示器 */}
|
||||
<View style={s.indicatorContainer}>
|
||||
{homeBanner.map((_, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
s.indicator,
|
||||
index === currentIndex && s.indicatorActive,
|
||||
]}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
61
pages/HomeScreen/components/BannerSwiper/styles.ts
Normal file
61
pages/HomeScreen/components/BannerSwiper/styles.ts
Normal file
@@ -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,
|
||||
},
|
||||
}));
|
||||
126
pages/HomeScreen/components/BottomTabs.tsx
Normal file
126
pages/HomeScreen/components/BottomTabs.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 首页底部 Tabs 导航组件
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.card,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
paddingBottom: 8,
|
||||
paddingTop: 8,
|
||||
},
|
||||
tabItem: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
tabIcon: {
|
||||
fontSize: 24,
|
||||
marginBottom: 4,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 12,
|
||||
color: colors.text + '80',
|
||||
fontWeight: '500',
|
||||
},
|
||||
tabLabelActive: {
|
||||
color: colors.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
}));
|
||||
|
||||
interface TabItem {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface BottomTabsProps {
|
||||
theme?: 'light' | 'dark';
|
||||
activeTab?: string;
|
||||
onTabPress?: (tabId: string, action: string) => void;
|
||||
items?: TabItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认 Tab 项
|
||||
*/
|
||||
const DEFAULT_TABS: TabItem[] = [
|
||||
{ id: 'recharge', label: '充值', icon: '💰', action: 'recharge' },
|
||||
{ id: 'withdraw', label: '提现', icon: '💳', action: 'withdraw' },
|
||||
{ id: 'activity', label: '活动', icon: '🎉', action: 'activity' },
|
||||
{ id: 'service', label: '客服', icon: '🎧', action: 'service' },
|
||||
{ id: 'help', label: '帮助', icon: '❓', action: 'help' },
|
||||
];
|
||||
|
||||
/**
|
||||
* 底部 Tabs 导航组件
|
||||
*/
|
||||
export default function BottomTabs({
|
||||
theme = 'light',
|
||||
activeTab = 'recharge',
|
||||
onTabPress,
|
||||
items = DEFAULT_TABS,
|
||||
}: BottomTabsProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
const colors = Colors[actualTheme];
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState(activeTab);
|
||||
|
||||
const handleTabPress = useCallback(
|
||||
(tabId: string, action: string) => {
|
||||
setSelectedTab(tabId);
|
||||
onTabPress?.(tabId, action);
|
||||
},
|
||||
[onTabPress]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
{items.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={s.tabItem}
|
||||
onPress={() => handleTabPress(item.id, item.action)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.tabIcon}>{item.icon}</Text>
|
||||
<Text
|
||||
style={[
|
||||
s.tabLabel,
|
||||
selectedTab === item.id && s.tabLabelActive,
|
||||
]}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
143
pages/HomeScreen/components/FastFootNav.tsx
Normal file
143
pages/HomeScreen/components/FastFootNav.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 快速底部导航组件
|
||||
*
|
||||
* 深色主题特有,提供快速导航,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockNavItems } from '@/services/mockHomeService';
|
||||
import type { NavItem } from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.border,
|
||||
},
|
||||
scrollView: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
navItem: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
marginRight: 10,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.background,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
minWidth: 75,
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
navItemActive: {
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
navIcon: {
|
||||
fontSize: 22,
|
||||
marginBottom: 4,
|
||||
},
|
||||
navText: {
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
navTextActive: {
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
}));
|
||||
|
||||
interface FastFootNavProps {
|
||||
theme: 'light' | 'dark';
|
||||
items?: NavItem[];
|
||||
onTabPress?: (tabId: string, action: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速底部导航组件
|
||||
*/
|
||||
export default function FastFootNav({ theme, items: propItems, onTabPress }: FastFootNavProps) {
|
||||
const s = styles[theme];
|
||||
const [items, setItems] = useState<NavItem[]>(propItems || []);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 加载导航项数据
|
||||
useEffect(() => {
|
||||
if (propItems && propItems.length > 0) {
|
||||
setItems(propItems);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadItems = async () => {
|
||||
try {
|
||||
// const data = await getNavItems();
|
||||
// setItems(data.length > 0 ? data : mockNavItems);
|
||||
} catch (error) {
|
||||
console.error('加载导航项失败:', error);
|
||||
setItems(mockNavItems);
|
||||
}
|
||||
};
|
||||
loadItems();
|
||||
}, [propItems]);
|
||||
|
||||
const handleNavPress = (item: NavItem) => {
|
||||
setSelectedId(item.id);
|
||||
onTabPress?.(item.id, item.action);
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
s.navItem,
|
||||
selectedId === item.id && s.navItemActive,
|
||||
]}
|
||||
onPress={() => handleNavPress(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.navIcon}>{item.icon || '🎮'}</Text>
|
||||
<Text
|
||||
style={[
|
||||
s.navText,
|
||||
selectedId === item.id && s.navTextActive,
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
112
pages/HomeScreen/components/GameMainMenus/index.tsx
Normal file
112
pages/HomeScreen/components/GameMainMenus/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 游戏分类菜单组件
|
||||
*
|
||||
* 展示游戏分类,支持切换,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { View, Text, ScrollView, TouchableOpacity, Animated } from 'react-native';
|
||||
// import type { GameCategory } from '@/types/home';
|
||||
import { styles } from './styles';
|
||||
import { useGameMainMenus, useMenuDataLoaded } from '@/hooks/useGameMenus';
|
||||
// import useGameStore from '@/stores/gameStore';
|
||||
import { ThemeEnum } from '@/constants/theme';
|
||||
|
||||
|
||||
|
||||
interface GameMainMenuProps {
|
||||
theme: ThemeEnum;
|
||||
selectedCategory?: string;
|
||||
onCategorySelect?: (categoryId: string) => void;
|
||||
topHeight?: number;
|
||||
showSubMenus?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏分类菜单组件
|
||||
*/
|
||||
export default function GameMainMenu({
|
||||
theme,
|
||||
selectedCategory = '103',
|
||||
onCategorySelect,
|
||||
topHeight = 0,
|
||||
showSubMenus = true,
|
||||
}: GameMainMenuProps) {
|
||||
const s = styles[theme];
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const gameMenus = useGameMainMenus(theme);
|
||||
|
||||
// 检查数据加载完成
|
||||
const isDataLoaded = useMenuDataLoaded();
|
||||
|
||||
// 使用 useMemo 缓存找到的索引,避免每次都重新计算
|
||||
const selectedIndex = useMemo(() => {
|
||||
return gameMenus.findIndex((cat) => cat.key === selectedCategory);
|
||||
}, [selectedCategory, gameMenus]);
|
||||
|
||||
// 当分类改变时,滚动到该分类
|
||||
useEffect(() => {
|
||||
if (selectedIndex >= 0) {
|
||||
scrollViewRef.current?.scrollTo({
|
||||
x: selectedIndex * 100,
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
}, [selectedIndex]);
|
||||
|
||||
// 使用 useCallback 稳定 onPress 回调
|
||||
const handleCategoryPress = useCallback((categoryKey: string) => {
|
||||
onCategorySelect?.(categoryKey);
|
||||
}, [onCategorySelect]);
|
||||
|
||||
// 骨架屏 - 显示加载中的占位符
|
||||
const renderSkeleton = () => (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{[1, 2, 3, 4, 5].map((index) => (
|
||||
<View
|
||||
key={`skeleton-${index}`}
|
||||
style={[s.categoryItem, { backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0' }]}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
|
||||
// 如果动态数据还未加载,显示骨架屏
|
||||
if (!isDataLoaded) {
|
||||
return renderSkeleton();
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{gameMenus.map((menu) => (
|
||||
<TouchableOpacity
|
||||
key={menu.key}
|
||||
style={[s.menuItem, selectedCategory === menu.key && s.menuItemActive]}
|
||||
onPress={() => handleCategoryPress(menu.key)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[s.menuText, selectedCategory === menu.key && s.menuTextActive]}
|
||||
>
|
||||
{menu.icon || '🎮'} {menu.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
47
pages/HomeScreen/components/GameMainMenus/styles.ts
Normal file
47
pages/HomeScreen/components/GameMainMenus/styles.ts
Normal file
@@ -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',
|
||||
},
|
||||
}));
|
||||
180
pages/HomeScreen/components/Header.tsx
Normal file
180
pages/HomeScreen/components/Header.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 首页 Header 组件
|
||||
* 包含搜索、用户信息、消息等功能
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TextInput,
|
||||
Image,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingVertical: 8,
|
||||
},
|
||||
logo: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: colors.primary,
|
||||
},
|
||||
searchContainer: {
|
||||
flex: 1,
|
||||
marginHorizontal: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.card,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
height: 36,
|
||||
},
|
||||
searchInput: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
fontSize: 14,
|
||||
color: colors.text,
|
||||
},
|
||||
searchPlaceholder: {
|
||||
color: colors.text + '80',
|
||||
},
|
||||
iconButton: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 8,
|
||||
},
|
||||
badge: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
backgroundColor: colors.primary,
|
||||
borderRadius: 8,
|
||||
minWidth: 16,
|
||||
height: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
badgeText: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
interface HeaderProps {
|
||||
theme?: 'light' | 'dark';
|
||||
onSearch?: (keyword: string) => void;
|
||||
onMessagePress?: () => void;
|
||||
onUserPress?: () => void;
|
||||
unreadCount?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header 组件
|
||||
*/
|
||||
export default function Header({
|
||||
theme = 'light',
|
||||
onSearch,
|
||||
onMessagePress,
|
||||
onUserPress,
|
||||
unreadCount = 0,
|
||||
}: HeaderProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||
const s = styles[actualTheme];
|
||||
const colors = Colors[actualTheme];
|
||||
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const handleSearch = useCallback(() => {
|
||||
if (searchText.trim()) {
|
||||
onSearch?.(searchText);
|
||||
}
|
||||
}, [searchText, onSearch]);
|
||||
|
||||
const handleClearSearch = useCallback(() => {
|
||||
setSearchText('');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
{/* 顶部栏 */}
|
||||
<View style={s.header}>
|
||||
{/* Logo */}
|
||||
<Text style={s.logo}>🎮 游戏大厅</Text>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<View style={s.searchContainer}>
|
||||
<Text style={{ color: colors.text + '60', fontSize: 16 }}>🔍</Text>
|
||||
<TextInput
|
||||
style={[s.searchInput, s.searchPlaceholder]}
|
||||
placeholder="搜索游戏..."
|
||||
placeholderTextColor={colors.text + '60'}
|
||||
value={searchText}
|
||||
onChangeText={setSearchText}
|
||||
onSubmitEditing={handleSearch}
|
||||
returnKeyType="search"
|
||||
/>
|
||||
{searchText ? (
|
||||
<TouchableOpacity onPress={handleClearSearch}>
|
||||
<Text style={{ fontSize: 16 }}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* 消息按钮 */}
|
||||
<TouchableOpacity
|
||||
style={s.iconButton}
|
||||
onPress={onMessagePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>💬</Text>
|
||||
{unreadCount > 0 && (
|
||||
<View style={s.badge}>
|
||||
<Text style={s.badgeText}>
|
||||
{unreadCount > 99 ? '99+' : unreadCount}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 用户按钮 */}
|
||||
<TouchableOpacity
|
||||
style={s.iconButton}
|
||||
onPress={onUserPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={{ fontSize: 18 }}>👤</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
164
pages/HomeScreen/components/HighPrizeGame.tsx
Normal file
164
pages/HomeScreen/components/HighPrizeGame.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 高奖金游戏组件
|
||||
*
|
||||
* 深色主题特有,展示高奖金游戏,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Animated,
|
||||
Image,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockHighPrizeGames } from '@/services/mockHomeService';
|
||||
import type { HighPrizeGame as HighPrizeGameType } from '@/types/home';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.background,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 12,
|
||||
marginBottom: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
color: colors.text,
|
||||
marginBottom: 10,
|
||||
},
|
||||
scrollView: {
|
||||
paddingHorizontal: 0,
|
||||
},
|
||||
gameItem: {
|
||||
width: 110,
|
||||
height: 110,
|
||||
marginRight: 10,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.card,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
elevation: 3,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
gameIcon: {
|
||||
fontSize: 44,
|
||||
marginBottom: 4,
|
||||
},
|
||||
gameName: {
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
prizeTag: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: colors.error,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.2,
|
||||
shadowRadius: 2,
|
||||
},
|
||||
prizeText: {
|
||||
fontSize: 10,
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}));
|
||||
|
||||
interface HighPrizeGameProps {
|
||||
theme: 'light' | 'dark';
|
||||
games?: HighPrizeGameType[];
|
||||
onGamePress?: (game: HighPrizeGameType) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高奖金游戏组件
|
||||
*/
|
||||
export default function HighPrizeGame({ theme, games: propGames, onGamePress }: HighPrizeGameProps) {
|
||||
const s = styles[theme];
|
||||
const [games, setGames] = useState<HighPrizeGameType[]>(propGames || []);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
// 加载高奖金游戏数据
|
||||
useEffect(() => {
|
||||
if (propGames && propGames.length > 0) {
|
||||
setGames(propGames);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadGames = async () => {
|
||||
try {
|
||||
// const data = await getHighPrizeGames();
|
||||
// setGames(data.length > 0 ? data : mockHighPrizeGames);
|
||||
} catch (error) {
|
||||
console.error('加载高奖金游戏失败:', error);
|
||||
setGames(mockHighPrizeGames);
|
||||
}
|
||||
};
|
||||
loadGames();
|
||||
}, [propGames]);
|
||||
|
||||
if (games.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<Text style={s.title}>🏆 实时爆奖</Text>
|
||||
<ScrollView
|
||||
style={s.scrollView}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
scrollEventThrottle={16}
|
||||
>
|
||||
{games.map((game) => (
|
||||
<TouchableOpacity
|
||||
key={game.id}
|
||||
style={s.gameItem}
|
||||
onPress={() => {
|
||||
setSelectedId(game.id);
|
||||
onGamePress?.(game);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{game.icon ? (
|
||||
<Image
|
||||
source={{ uri: game.icon }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<Text style={s.gameIcon}>🎰</Text>
|
||||
)}
|
||||
<Text style={s.gameName} numberOfLines={2}>
|
||||
{game.play_up_name}
|
||||
</Text>
|
||||
<View style={s.prizeTag}>
|
||||
<Text style={s.prizeText}>¥{Math.floor(game.payout_amount / 1000)}k</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
209
pages/HomeScreen/components/Lobby.tsx
Normal file
209
pages/HomeScreen/components/Lobby.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 游戏大厅组件
|
||||
*
|
||||
* 展示游戏列表,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { getMockGamesByCategory } from '@/services/mockHomeService';
|
||||
import type { Game } from '@/types/home';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12,
|
||||
},
|
||||
gameGrid: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
gameCard: {
|
||||
flex: 1,
|
||||
margin: 6,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.card,
|
||||
elevation: 3,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 3 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 6,
|
||||
},
|
||||
gameCardPressed: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
gameImage: {
|
||||
width: '100%',
|
||||
height: 140,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
gameIcon: {
|
||||
fontSize: 48,
|
||||
},
|
||||
gameInfo: {
|
||||
padding: 10,
|
||||
},
|
||||
gameName: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
color: colors.text,
|
||||
marginBottom: 6,
|
||||
},
|
||||
gameButton: {
|
||||
backgroundColor: colors.primary,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 6,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
},
|
||||
gameButtonText: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 40,
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 12,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
}));
|
||||
|
||||
interface LobbyProps {
|
||||
theme: 'light' | 'dark';
|
||||
games?: Game[];
|
||||
selectedCategory?: number;
|
||||
onGamePress?: (game: Game) => void;
|
||||
topHeight?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏大厅组件
|
||||
*/
|
||||
export default function Lobby({
|
||||
theme,
|
||||
games: propGames,
|
||||
selectedCategory = 0,
|
||||
onGamePress,
|
||||
topHeight = 0,
|
||||
}: LobbyProps) {
|
||||
const s = styles[theme];
|
||||
const [games, setGames] = useState<Game[]>(propGames || []);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// 加载游戏数据
|
||||
useEffect(() => {
|
||||
if (propGames && propGames.length > 0) {
|
||||
setGames(propGames);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadGames = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// const response = await getGames(selectedCategory);
|
||||
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(selectedCategory));
|
||||
} catch (error) {
|
||||
console.error('加载游戏失败:', error);
|
||||
setGames(getMockGamesByCategory(selectedCategory));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadGames();
|
||||
}, [propGames, selectedCategory]);
|
||||
|
||||
const renderGameCard = ({ item }: { item: Game }) => (
|
||||
<TouchableOpacity
|
||||
style={s.gameCard}
|
||||
activeOpacity={0.7}
|
||||
onPress={() => onGamePress?.(item)}
|
||||
>
|
||||
<View style={s.gameImage}>
|
||||
{item.icon ? (
|
||||
<Image
|
||||
source={{ uri: item.icon }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<Text style={s.gameIcon}>🎮</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={s.gameInfo}>
|
||||
<Text style={s.gameName} numberOfLines={2}>
|
||||
{item.play_up_name}
|
||||
{item.play_cname ? ` - ${item.play_cname}` : ''}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={s.gameButton}
|
||||
onPress={() => onGamePress?.(item)}
|
||||
>
|
||||
<Text style={s.gameButtonText}>进入游戏</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={s.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={Colors[theme].primary} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (games.length === 0) {
|
||||
return (
|
||||
<View style={s.emptyContainer}>
|
||||
<Text style={{ fontSize: 40 }}>🎮</Text>
|
||||
<Text style={s.emptyText}>暂无游戏</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={s.container}>
|
||||
<FlatList
|
||||
data={games}
|
||||
renderItem={renderGameCard}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={2}
|
||||
scrollEnabled={false}
|
||||
contentContainerStyle={s.gameGrid}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
171
pages/HomeScreen/components/NoticeBar.tsx
Normal file
171
pages/HomeScreen/components/NoticeBar.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 公告栏组件
|
||||
*
|
||||
* 展示滚动公告,使用真实数据
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import { mockNotices } from '@/services/mockHomeService';
|
||||
import type { Notice } from '@/types/home';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginHorizontal: 12,
|
||||
marginBottom: 12,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: colors.cardShadow,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3,
|
||||
},
|
||||
label: {
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
color: '#FFFFFF',
|
||||
marginRight: 8,
|
||||
backgroundColor: colors.primary,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
fontSize: 12,
|
||||
color: colors.text,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
closeButton: {
|
||||
marginLeft: 8,
|
||||
padding: 4,
|
||||
},
|
||||
closeText: {
|
||||
fontSize: 16,
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
}));
|
||||
|
||||
interface NoticeBarProps {
|
||||
theme: 'light' | 'dark';
|
||||
notices?: Notice[];
|
||||
onNoticePress?: (notice: Notice) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公告栏组件
|
||||
*/
|
||||
export default function NoticeBar({ theme, notices: propNotices, onNoticePress }: NoticeBarProps) {
|
||||
const s = styles[theme];
|
||||
const [notices, setNotices] = useState<Notice[]>(propNotices || []);
|
||||
const [currentNotice, setCurrentNotice] = useState(0);
|
||||
const [visible, setVisible] = useState(true);
|
||||
const animatedValue = useRef(new Animated.Value(1)).current;
|
||||
|
||||
// 加载公告数据
|
||||
useEffect(() => {
|
||||
if (propNotices && propNotices.length > 0) {
|
||||
setNotices(propNotices);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadNotices = async () => {
|
||||
try {
|
||||
// const data = await getNotices();
|
||||
// setNotices(data.length > 0 ? data : mockNotices);
|
||||
} catch (error) {
|
||||
console.error('加载公告失败:', error);
|
||||
setNotices(mockNotices);
|
||||
}
|
||||
};
|
||||
loadNotices();
|
||||
}, [propNotices]);
|
||||
|
||||
// 自动切换公告
|
||||
useEffect(() => {
|
||||
if (notices.length === 0) return;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
setCurrentNotice((prev) => (prev + 1) % notices.length);
|
||||
}, 5000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [notices.length]);
|
||||
|
||||
// 处理关闭公告
|
||||
const handleClose = () => {
|
||||
Animated.timing(animatedValue, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}).start(() => {
|
||||
setVisible(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 处理公告点击
|
||||
const handleNoticePress = () => {
|
||||
if (notices.length > 0) {
|
||||
onNoticePress?.(notices[currentNotice]);
|
||||
}
|
||||
};
|
||||
|
||||
if (!visible || notices.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentNoticeData = notices[currentNotice];
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
s.container,
|
||||
{
|
||||
opacity: animatedValue,
|
||||
transform: [
|
||||
{
|
||||
scaleY: animatedValue,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text style={s.label}>📢</Text>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={handleNoticePress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={s.content} numberOfLines={1}>
|
||||
{currentNoticeData.title || currentNoticeData.content}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={s.closeButton}
|
||||
onPress={handleClose}
|
||||
>
|
||||
<Text style={s.closeText}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
26
pages/HomeScreen/components/index.ts
Normal file
26
pages/HomeScreen/components/index.ts
Normal file
@@ -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);
|
||||
|
||||
59
pages/HomeScreen/index.tsx
Normal file
59
pages/HomeScreen/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 首页主容器组件
|
||||
*
|
||||
* 支持浅色/深色主题,包含完整的首页功能:
|
||||
* - Header(搜索、用户信息)
|
||||
* - 轮播图
|
||||
* - 游戏分类菜单
|
||||
* - 游戏大厅
|
||||
* - 公告栏
|
||||
* - 高奖金游戏(深色主题)
|
||||
* - 快速导航(深色主题)
|
||||
* - BottomTabs(底部导航)
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react';
|
||||
import { ScrollView, StyleSheet, RefreshControl } from 'react-native';
|
||||
import { useColorScheme } from '@/hooks';
|
||||
import { createThemeStyles } from '@/theme';
|
||||
import Colors from '@/constants/Colors';
|
||||
import HomeScreenComplete from './HomeScreenComplete';
|
||||
|
||||
/**
|
||||
* 创建主题样式
|
||||
*/
|
||||
const styles = createThemeStyles((colors) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* 首页主容器组件
|
||||
*/
|
||||
export default function HomeScreen() {
|
||||
const theme = useColorScheme();
|
||||
const s = styles[theme];
|
||||
const [refreshing, setRefreshing] = React.useState(false);
|
||||
|
||||
// 下拉刷新处理
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
// 模拟刷新延迟
|
||||
setTimeout(() => {
|
||||
setRefreshing(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<HomeScreenComplete
|
||||
theme={theme}
|
||||
isDarkTheme={theme === 'dark'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,4 +37,5 @@
|
||||
|
||||
// 导出业务页面组件
|
||||
export { default as TestPage } from './TestPage';
|
||||
export { default as HomeScreen } from './HomeScreen';
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
37
services/gameService.ts
Normal file
37
services/gameService.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 游戏服务
|
||||
* 处理租户相关的 API 请求
|
||||
*/
|
||||
|
||||
import { request } from '@/utils/network/api';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
*/
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* tenant 服务类
|
||||
*/
|
||||
class GameService {
|
||||
/**
|
||||
* 获取首页数据
|
||||
*/
|
||||
getHomePageData(data?: Record<string, any>): Promise<ApiResponse> {
|
||||
return request.post('/v2', data, {
|
||||
headers: {
|
||||
cmdId: 381119,
|
||||
paramType: 1,
|
||||
apiName: 'getHomePageData',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const gameService = new GameService();
|
||||
export default gameService;
|
||||
@@ -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';
|
||||
|
||||
|
||||
233
services/mockHomeService.ts
Normal file
233
services/mockHomeService.ts
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
*/
|
||||
|
||||
import { request } from '@/utils/network/api';
|
||||
// import type { User, UpdateProfileFormData } from '@/schemas/user';
|
||||
|
||||
/**
|
||||
* API 响应接口
|
||||
*/
|
||||
interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
success: string;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
@@ -32,7 +31,6 @@ class TenantService {
|
||||
cmdId: 371130,
|
||||
headerType: 1,
|
||||
apiName: 'getPlatformData',
|
||||
tid: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
258
stores/gameStore.ts
Normal file
258
stores/gameStore.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* 游戏状态管理
|
||||
* 使用 Zustand + AsyncStorage 持久化
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
// import { useShallow } from 'zustand/react/shallow';
|
||||
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||
import gameService from '@/services/gameService';
|
||||
import appConfig from '@/utils/config';
|
||||
import useTenantStore from '@/stores/tenantStore';
|
||||
import useMsgStore from '@/stores/msgStore';
|
||||
import { filter, map, concat, cloneDeep, groupBy } from 'lodash-es';
|
||||
import { GameMainKeysEnum, defaultHomeGameTabMenus } from '@/constants/game';
|
||||
|
||||
// 状态
|
||||
interface State {
|
||||
appLoginPopType: number;
|
||||
receiveAwardPopType: number;
|
||||
appLoginPop: boolean;
|
||||
menuSort: Record<string, any>[];
|
||||
rebateGameSort: Record<string, any>[];
|
||||
originalGames: Record<string, any>[];
|
||||
blockchainGames: Record<string, any>[];
|
||||
homeHotGames: Record<string, any>[];
|
||||
gamesTry: Record<string, any>[];
|
||||
gamesTryPlayIds: number[];
|
||||
smallClassGames: Record<string, any>;
|
||||
gameBigClass: Record<string, any>;
|
||||
}
|
||||
|
||||
// 操作
|
||||
interface Actions {
|
||||
setHomePageData: (data: Record<string, any>) => void;
|
||||
setOriginalGames: (data: Record<string, any>[]) => void;
|
||||
setBlockchainGames: (data: Record<string, any>[]) => void;
|
||||
setGamesTry: (data: Record<string, any>[]) => void;
|
||||
setHomeHotGames: (data: Record<string, any>[]) => void;
|
||||
setSmallClassGame: (data: Record<string, any>) => void;
|
||||
setGameBigClass: (data: Record<string, any>) => void;
|
||||
// requestHomePageData: (data?: Record<string, any>) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户状态 Store
|
||||
*/
|
||||
const useGameStore = create<State & Actions>()((set, get) => ({
|
||||
// 初始状态
|
||||
appLoginPopType: 0,
|
||||
receiveAwardPopType: 0,
|
||||
appLoginPop: false,
|
||||
menuSort: [], // 游戏主菜单
|
||||
rebateGameSort: [],
|
||||
originalGames: [], // 原创游戏
|
||||
blockchainGames: [], // 区块链游戏
|
||||
homeHotGames: [], // 热门游戏
|
||||
gamesTry: [], // 试玩游戏
|
||||
gamesTryPlayIds: [], // 试玩游戏id列表
|
||||
smallClassGames: {},
|
||||
gameBigClass: {},
|
||||
|
||||
|
||||
// 保存首页数据
|
||||
setHomePageData: (data: any) => {
|
||||
if (data) {
|
||||
const { setNotices, setBanners } = useMsgStore.getState();
|
||||
|
||||
// 设置注册弹窗
|
||||
const appLoginPopType = data.appLoginPopType || 0;
|
||||
const receiveAwardPopType = data.receiveAwardPopType || 0;
|
||||
const appLoginPop = data.appLoginPop || false;
|
||||
|
||||
// 菜单排序
|
||||
const menuSort = filter(data.version_type, item => item.sort_v !== 0 && item.state == 1).sort((a, b) => {
|
||||
return b.sort_v - a.sort_v;
|
||||
});
|
||||
console.log(menuSort, 'menuSort 1');
|
||||
// version_shuffle
|
||||
const rebateGameSort = data.version_shuffle || [];
|
||||
|
||||
// 所有游戏销售状态
|
||||
// gameStore.setGameAllStates(res.data.gameState);
|
||||
|
||||
// 公告
|
||||
setNotices(data.news?.data || []);
|
||||
|
||||
// 轮播图
|
||||
setBanners(data.banners?.data || []);
|
||||
|
||||
set({ appLoginPopType, receiveAwardPopType, appLoginPop, menuSort, rebateGameSort });
|
||||
|
||||
// 原创游戏
|
||||
get().setOriginalGames(data.originalGames);
|
||||
|
||||
// 区块链游戏
|
||||
get().setBlockchainGames(data.hsGames);
|
||||
|
||||
// 试玩游戏
|
||||
get().setGamesTry(data.gamesTry);
|
||||
|
||||
// 首页热门游戏
|
||||
get().setHomeHotGames(data.homeHotGames?.[1] || []);
|
||||
get().setSmallClassGame(data.homeHotGames);
|
||||
|
||||
// 三方游戏
|
||||
get().setGameBigClass(data.thirdGames);
|
||||
|
||||
// 手动持久化
|
||||
// storageManager.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data }));
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.log('💾 Tenant info saved:', data);
|
||||
}
|
||||
},
|
||||
|
||||
setOriginalGames: (list: Record<string, any>[]) => {
|
||||
const originalGames = map(list, (item: any) => ({
|
||||
...item,
|
||||
isOriginal: true,
|
||||
})).sort((a: any, b: any) => a.hotVal - b.hotVal);
|
||||
set({ originalGames });
|
||||
},
|
||||
|
||||
setBlockchainGames: (list: Record<string, any>[]) => {
|
||||
set({ blockchainGames: list || [] });
|
||||
},
|
||||
|
||||
setGamesTry: (list: Record<string, any>[]) => {
|
||||
set({ gamesTry: list || [] });
|
||||
storageManager.session.setItem(STORAGE_KEYS.GAME_TRY, list);
|
||||
const gamesTryPlayIds = concat(...map(list, item => map(item.subList, subItem => Number(subItem.play_id))));
|
||||
set({ gamesTryPlayIds });
|
||||
},
|
||||
|
||||
setHomeHotGames: (list: Record<string, any>[]) => {
|
||||
set({ homeHotGames: list || [] });
|
||||
},
|
||||
|
||||
setSmallClassGame: (data: Record<string, any>) => {
|
||||
set({ smallClassGames: {
|
||||
[GameMainKeysEnum.HOT_ELECTRONIC]: data?.[2] || [],
|
||||
[GameMainKeysEnum.HOT_FISHING]: data?.[3] || [],
|
||||
[GameMainKeysEnum.HOT_CHESS]: data?.[5] || [],
|
||||
[GameMainKeysEnum.HOT_BLOCK_THIRD]: data?.[8] || [],
|
||||
} });
|
||||
},
|
||||
|
||||
setGameBigClass: (data: Record<string, any>) => {
|
||||
const groupByType = cloneDeep(groupBy(data, item => item.big_type));
|
||||
set({ gameBigClass: {
|
||||
[GameMainKeysEnum.CHESS]: groupByType?.[1] || [],
|
||||
[GameMainKeysEnum.ELECTRONIC]: groupByType?.[2] || [],
|
||||
[GameMainKeysEnum.FISHING]: groupByType?.[3] || [],
|
||||
[GameMainKeysEnum.LIVE]: groupByType?.[4] || [],
|
||||
[GameMainKeysEnum.SPORTS]: groupByType?.[5] || [],
|
||||
[GameMainKeysEnum.LOTTERY]: groupByType?.[7] || [],
|
||||
[GameMainKeysEnum.BLOCK_THIRD]: groupByType?.[10] || [],
|
||||
} });
|
||||
},
|
||||
}));
|
||||
|
||||
// 从 AsyncStorage 恢复状态的函数
|
||||
export const restoreGameState = async () => {
|
||||
try {
|
||||
const stored = await storageManager.session.getItem(STORAGE_KEYS.TENANT_STORE);
|
||||
if (stored) {
|
||||
const state = JSON.parse(stored);
|
||||
useGameStore.setState(state);
|
||||
if (__DEV__) {
|
||||
console.log('✅ Tenant state restored from storage');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore tenant state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取首页数据
|
||||
export const requestHomePageData = async () => {
|
||||
try {
|
||||
const { tenantInfo } = useTenantStore.getState();
|
||||
const params = {
|
||||
tid: tenantInfo?.tid,
|
||||
aseq: {
|
||||
aseq: appConfig.app.aseqId,
|
||||
},
|
||||
version_type: {
|
||||
version_type: 3, // 1 是棋牌, 2 是网赚 3, 是综合彩票
|
||||
},
|
||||
news: {
|
||||
page_start: 1,
|
||||
num_per_page: 999,
|
||||
chan_con: 8,
|
||||
state: 1,
|
||||
message_id: '10,13', //10公告 13 跑马灯
|
||||
sort_flag: 1,
|
||||
vip_level: 0, //userInfo.value.cust_level || 0,
|
||||
proxy: tenantInfo?.proxy,
|
||||
apply: 8, // 1 棋牌;2 网赚;3 综合彩票;3 老版彩票; (apply后端设置默认为1,为可选参数)
|
||||
language: 0, //Number(window.localStorage.getItem('languageNum') || '0'),
|
||||
},
|
||||
game_news: {
|
||||
page_start: 1,
|
||||
num_per_page: 9999,
|
||||
state: 1,
|
||||
message_id: 17,
|
||||
sort_flag: 1,
|
||||
tid: tenantInfo?.tid,
|
||||
proxy: tenantInfo?.proxy,
|
||||
chan_con: 0,
|
||||
apply: 8,
|
||||
ma_id: 10,
|
||||
// vip_level: userInfo.value.cust_level || 0,
|
||||
vip_level: 0,
|
||||
type: 'MWEB',
|
||||
language: 0,
|
||||
},
|
||||
banners: {
|
||||
page_start: 1,
|
||||
num_per_page: 999,
|
||||
chan_con: 8,
|
||||
state: 1,
|
||||
message_id: 12,
|
||||
sort_flag: 1,
|
||||
tid: tenantInfo?.tid,
|
||||
proxy: tenantInfo?.proxy,
|
||||
apply: 8,
|
||||
location: '1',
|
||||
type: 'MWEB',
|
||||
language: Number(window.localStorage.getItem('languageNum') || '0'),
|
||||
},
|
||||
homeHotGames: {
|
||||
// cust_id: userInfo.value.cust_id || '0',
|
||||
cust_id: '0',
|
||||
},
|
||||
proxyConfig: {
|
||||
proxy: tenantInfo?.proxy,
|
||||
},
|
||||
hotGames: {
|
||||
size: 12,
|
||||
},
|
||||
};
|
||||
const { data } = await gameService.getHomePageData(params);
|
||||
|
||||
// ✅ 直接调用 setHomePageData action
|
||||
useGameStore.getState().setHomePageData(data);
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('✅ Home-data info loaded:', data);
|
||||
}
|
||||
return Promise.resolve(data);
|
||||
} catch (error) {
|
||||
console.error('Failed to request home-data info:', error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
export default useGameStore;
|
||||
@@ -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';
|
||||
|
||||
|
||||
72
stores/msgStore.ts
Normal file
72
stores/msgStore.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* 消息状态管理
|
||||
* 使用 Zustand + AsyncStorage 持久化
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||
import { filter } from 'lodash-es';
|
||||
// import { useShallow } from 'zustand/react/shallow';
|
||||
// import { tenantService } from '@/services';
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
interface State {
|
||||
notices: Record<string, any>[];
|
||||
homeBanner: Record<string, any>[];
|
||||
mineBanner: Record<string, any>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作
|
||||
*/
|
||||
interface Actions {
|
||||
setNotices: (list: Record<string, any>[]) => void;
|
||||
setBanners: (list: Record<string, any>[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户状态 Store
|
||||
*/
|
||||
const useMsgStore = create<State & Actions>()((set, get) => ({
|
||||
// state
|
||||
notices: [],
|
||||
homeBanner: [],
|
||||
mineBanner: [],
|
||||
|
||||
// actions
|
||||
setNotices: (list: Record<string, any>[]) => {
|
||||
set({ notices: list });
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('💾 notices saved:', list);
|
||||
}
|
||||
},
|
||||
setBanners: (list: Record<string, any>[]) => {
|
||||
const homeBanner = filter(list, (item) => item.content1 && item.content1.includes('1'));
|
||||
const mineBanner = filter(list, (item) => item.content1 && item.content1.includes('2'));
|
||||
set({ homeBanner, mineBanner });
|
||||
|
||||
if (__DEV__) {
|
||||
console.log('💾 banners saved:', list);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// 从 AsyncStorage 恢复状态的函数
|
||||
export const restoreMsgState = async () => {
|
||||
try {
|
||||
const stored = storageManager.session.getItem(STORAGE_KEYS.MSG_STORE);
|
||||
if (stored) {
|
||||
useMsgStore.setState(stored);
|
||||
if (__DEV__) {
|
||||
console.log('✅ Msg state restored from storage');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to restore msg state:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export default useMsgStore;
|
||||
@@ -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<SettingsState>()((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<SettingsState>()((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<SettingsState>()((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<SettingsState>()((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<SettingsState>()((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<SettingsState>()((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<SettingsState>()((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);
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
/**
|
||||
* 租户状态接口
|
||||
*/
|
||||
interface TenantState {
|
||||
// 状态
|
||||
tenantInfo: Record<string, any> | null;
|
||||
|
||||
// 操作
|
||||
setTenantInfo: (data: Record<string, any>) => void;
|
||||
requestTenantInfo: (data?: Record<string, any>) => Promise<any>;
|
||||
export interface Tenant {
|
||||
tid: number;
|
||||
proxy: number;
|
||||
create_time: string;
|
||||
domain_addr: string
|
||||
}
|
||||
|
||||
// 状态
|
||||
interface State {
|
||||
tenantInfo: Tenant | null;
|
||||
}
|
||||
|
||||
// 操作
|
||||
interface Actions {
|
||||
setTenantInfo: (data: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 租户状态 Store
|
||||
*/
|
||||
const useTenantStore = create<TenantState>()((set, get) => ({
|
||||
const useTenantStore = create<State & Actions>()((set, get) => ({
|
||||
// 初始状态
|
||||
tenantInfo: null,
|
||||
|
||||
@@ -47,39 +42,19 @@ const useTenantStore = create<TenantState>()((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 useTenantStates = () =>
|
||||
useTenantStore(
|
||||
useShallow((state) => ({
|
||||
tenantInfo: state.tenantInfo,
|
||||
tenantLoad: !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time,
|
||||
}))
|
||||
);
|
||||
// 获取租户信息
|
||||
export const requestTenantInfo = async (): Promise<Tenant> => {
|
||||
try {
|
||||
// 使用 getState() 而不是 hook
|
||||
const { setTenantInfo } = useTenantStore.getState();
|
||||
const params = {
|
||||
domain_addr: 'https://51zhh5.notbug.org',
|
||||
};
|
||||
const { data } = await tenantService.getPlatformData(params);
|
||||
|
||||
// 获取租户操作方法
|
||||
// 使用 useShallow 避免每次渲染都返回新对象
|
||||
export const useTenantActions = () =>
|
||||
useTenantStore(
|
||||
useShallow((state) => ({
|
||||
setTenantInfo: state.setTenantInfo,
|
||||
requestTenantInfo: state.requestTenantInfo,
|
||||
}))
|
||||
);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
export default useTenantStore;
|
||||
|
||||
@@ -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<UserState>()((set, get) => ({
|
||||
export const useUserStore = create<State & Actions>()((set, get) => ({
|
||||
// 初始状态
|
||||
user: null,
|
||||
isLoggedIn: false,
|
||||
@@ -51,7 +50,7 @@ export const useUserStore = create<UserState>()((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<UserState>()((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<UserState>()((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<UserState>()((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<UserState>()((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<UserState>()((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);
|
||||
|
||||
@@ -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();
|
||||
* <View style={styles[theme].container}>
|
||||
@@ -46,23 +48,25 @@ export type ThemeStyles = {
|
||||
* ```
|
||||
*/
|
||||
export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
||||
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<T extends StyleSheet.NamedStyles<T>>(
|
||||
*/
|
||||
export function createResponsiveThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
||||
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) => ({
|
||||
* <View style={style.container} />
|
||||
* ```
|
||||
*/
|
||||
export function getThemeStyle<T>(styles: ThemeStyles, theme: 'light' | 'dark'): T {
|
||||
export function getThemeStyle<T>(styles: ThemeStyles, theme: ThemeEnum): T {
|
||||
return styles[theme];
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T>(
|
||||
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<T>(
|
||||
export function selectByTheme<T>(
|
||||
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<T>(
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
121
types/home.ts
Normal file
121
types/home.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 首页相关的数据类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 轮播图数据
|
||||
*/
|
||||
export interface Banner {
|
||||
id: string;
|
||||
subject: string; // 图片URL
|
||||
link_type: number; // 1: 外链, 2: 内部路由
|
||||
content: string; // 链接内容
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公告数据
|
||||
*/
|
||||
export interface Notice {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
content_type: number; // 1: 文本, 2: 图片, 3: 链接
|
||||
create_time: string;
|
||||
formatDate?: string;
|
||||
is_save_pic?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏分类
|
||||
*/
|
||||
export interface GameCategory {
|
||||
id: number;
|
||||
key: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
logo?: string;
|
||||
big_type?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏数据
|
||||
*/
|
||||
export interface Game {
|
||||
id: string;
|
||||
play_id: number;
|
||||
upper_play_id?: number;
|
||||
play_up_name: string; // 游戏平台名称
|
||||
play_cname?: string; // 游戏子类名称
|
||||
logo3_img_url?: string; // 游戏图标
|
||||
icon?: string;
|
||||
big_type: number; // 游戏大类型
|
||||
mainType?: number;
|
||||
index?: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高奖金游戏数据
|
||||
*/
|
||||
export interface HighPrizeGame extends Game {
|
||||
payout_amount: number; // 派彩金额
|
||||
bet_amount: number; // 下注金额
|
||||
odds: number; // 赔率
|
||||
cust_name: string; // 用户名
|
||||
avatar?: string; // 用户头像
|
||||
update_time: string; // 更新时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速导航项
|
||||
*/
|
||||
export interface NavItem {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
action: string; // 导航动作
|
||||
}
|
||||
|
||||
/**
|
||||
* 首页数据响应
|
||||
*/
|
||||
export interface HomePageData {
|
||||
banners: Banner[];
|
||||
notices: Notice[];
|
||||
categories: GameCategory[];
|
||||
games: Game[];
|
||||
highPrizeGames: HighPrizeGame[];
|
||||
navItems: NavItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API 响应格式
|
||||
*/
|
||||
export interface ApiResponse<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
type?: 'success' | 'error';
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
page_size: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏列表响应
|
||||
*/
|
||||
export interface GameListResponse {
|
||||
games: Game[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T> = {
|
||||
@@ -20,38 +21,16 @@ export type NetworkResponse<T> = {
|
||||
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<string, any> = {};
|
||||
// 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);
|
||||
|
||||
@@ -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<string, string> = 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<T>(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<T>(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<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
this.storage.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionStorage;
|
||||
|
||||
184
utils/storage.ts
184
utils/storage.ts
@@ -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<void> {
|
||||
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<string | null> {
|
||||
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<T>(key: string, value: T): Promise<void> {
|
||||
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<T>(key: string): Promise<T | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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;
|
||||
@@ -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',
|
||||
|
||||
/**
|
||||
* 存储选项
|
||||
*/
|
||||
export interface StorageOptions {
|
||||
/**
|
||||
* 存储类型
|
||||
* - 'local': 持久化存储(AsyncStorage)
|
||||
* - 'session': 会话存储(内存)
|
||||
*/
|
||||
type?: StorageType;
|
||||
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首页数据是否已加载
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一存储管理器
|
||||
* 数据类型标记
|
||||
*/
|
||||
class StorageManager {
|
||||
/**
|
||||
* 存储字符串
|
||||
*/
|
||||
static async setString(
|
||||
key: string,
|
||||
value: string,
|
||||
options: StorageOptions = {}
|
||||
): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
enum DataType {
|
||||
STRING = 'string',
|
||||
NUMBER = 'number',
|
||||
BOOLEAN = 'boolean',
|
||||
OBJECT = 'object',
|
||||
ARRAY = 'array',
|
||||
NULL = 'null',
|
||||
}
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.setString(key, value);
|
||||
} else {
|
||||
await Storage.setString(key, value);
|
||||
/**
|
||||
* 带类型标记的存储值
|
||||
*/
|
||||
interface TypedValue {
|
||||
type: DataType;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储工具基类
|
||||
* 提供公共的序列化、反序列化和类型转换方法
|
||||
*/
|
||||
abstract class StorageBase {
|
||||
/**
|
||||
* 获取数据类型
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 序列化值(带类型标记)
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 反序列化值(自动转换类型)
|
||||
*/
|
||||
protected static deserializeValue(data: string): any {
|
||||
try {
|
||||
const typedValue: TypedValue = JSON.parse(data);
|
||||
const { type, value } = typedValue;
|
||||
|
||||
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 setItem(key: string, value: any): Promise<void> {
|
||||
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 getString(
|
||||
key: string,
|
||||
options: StorageOptions = {}
|
||||
): Promise<string | null> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
return SessionStorage.getString(key);
|
||||
} else {
|
||||
return await Storage.getString(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 存储对象
|
||||
*/
|
||||
static async setObject<T>(
|
||||
key: string,
|
||||
value: T,
|
||||
options: StorageOptions = {}
|
||||
): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.setObject(key, value);
|
||||
} else {
|
||||
await Storage.setObject(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象
|
||||
*/
|
||||
static async getObject<T>(
|
||||
key: string,
|
||||
options: StorageOptions = {}
|
||||
): Promise<T | null> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
return SessionStorage.getObject<T>(key);
|
||||
} else {
|
||||
return await Storage.getObject<T>(key);
|
||||
static async getItem(key: string): Promise<any> {
|
||||
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 remove(key: string, options: StorageOptions = {}): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.remove(key);
|
||||
} else {
|
||||
await Storage.remove(key);
|
||||
static async removeItem(key: string): Promise<void> {
|
||||
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 clear(options: StorageOptions = {}): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.clear();
|
||||
} else {
|
||||
await Storage.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键名
|
||||
*/
|
||||
static async getAllKeys(options: StorageOptions = {}): Promise<string[]> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
return SessionStorage.getAllKeys();
|
||||
} else {
|
||||
return await Storage.getAllKeys();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
*/
|
||||
static async has(key: string, options: StorageOptions = {}): Promise<boolean> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
return SessionStorage.has(key);
|
||||
} else {
|
||||
const value = await Storage.getString(key);
|
||||
return value !== null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取
|
||||
*/
|
||||
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 multiSet(
|
||||
keyValuePairs: [string, string][],
|
||||
options: StorageOptions = {}
|
||||
): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.multiSet(keyValuePairs);
|
||||
} else {
|
||||
await Storage.multiSet(keyValuePairs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除
|
||||
*/
|
||||
static async multiRemove(
|
||||
keys: string[],
|
||||
options: StorageOptions = {}
|
||||
): Promise<void> {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
SessionStorage.multiRemove(keys);
|
||||
} else {
|
||||
await Storage.multiRemove(keys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储大小(仅 session storage)
|
||||
*/
|
||||
static getSize(options: StorageOptions = {}): number {
|
||||
const { type = 'local' } = options;
|
||||
|
||||
if (type === 'session') {
|
||||
return SessionStorage.length;
|
||||
} else {
|
||||
// AsyncStorage 不支持直接获取大小
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有存储(local + session)
|
||||
*/
|
||||
static async clearAll(): Promise<void> {
|
||||
await Storage.clear();
|
||||
SessionStorage.clear();
|
||||
if (__DEV__) {
|
||||
console.log('🗑️ All storage cleared (local + session)');
|
||||
static async clear(): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.clear();
|
||||
if (__DEV__) {
|
||||
console.log('🗑️ LocalStorage cleared all');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('LocalStorage clear error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StorageManager;
|
||||
/**
|
||||
* 会话存储类(内存实现)
|
||||
* 继承 StorageBase,使用其公共的序列化和反序列化方法
|
||||
*/
|
||||
class SessionStorage extends StorageBase {
|
||||
private static storage: Map<string, string> = new Map();
|
||||
|
||||
/**
|
||||
* 存储数据(自动序列化和类型标记)
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定键
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有存储
|
||||
*/
|
||||
static clear(): void {
|
||||
try {
|
||||
this.storage.clear();
|
||||
if (__DEV__) {
|
||||
console.log('🗑️ SessionStorage cleared all');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('SessionStorage clear error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一存储管理器
|
||||
*/
|
||||
const storageManager = {
|
||||
local: LocalStorage,
|
||||
session: SessionStorage,
|
||||
};
|
||||
|
||||
export default storageManager;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user