Compare commits
2 Commits
230191f181
...
9ef9233797
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ef9233797 | |||
| b48cce06f4 |
@@ -3,7 +3,7 @@ import FontAwesome from '@expo/vector-icons/FontAwesome';
|
|||||||
import { Link, Tabs } from 'expo-router';
|
import { Link, Tabs } from 'expo-router';
|
||||||
import { Pressable } from 'react-native';
|
import { Pressable } from 'react-native';
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
import { useColorScheme, useClientOnlyValue } from '@/hooks';
|
import { useColorScheme, useClientOnlyValue } from '@/hooks';
|
||||||
|
|
||||||
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
|
||||||
@@ -29,42 +29,28 @@ export default function TabLayout() {
|
|||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="index"
|
name="index"
|
||||||
options={{
|
options={{
|
||||||
title: 'Tab One',
|
title: '首页',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="home" 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>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="two"
|
name="two"
|
||||||
options={{
|
options={{
|
||||||
title: 'Tab Two',
|
title: '充值',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="demo"
|
name="demo"
|
||||||
options={{
|
options={{
|
||||||
title: '完整示例',
|
title: '活动',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="rocket" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="rocket" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tabs.Screen
|
<Tabs.Screen
|
||||||
name="paper"
|
name="paper"
|
||||||
options={{
|
options={{
|
||||||
title: 'Paper UI',
|
title: '我的',
|
||||||
tabBarIcon: ({ color }) => <TabBarIcon name="paint-brush" color={color} />,
|
tabBarIcon: ({ color }) => <TabBarIcon name="paint-brush" color={color} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,10 +24,8 @@ import { useRouter } from 'expo-router';
|
|||||||
|
|
||||||
// 工具函数
|
// 工具函数
|
||||||
import {
|
import {
|
||||||
Storage,
|
storageManager,
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SessionStorage,
|
|
||||||
SESSION_KEYS,
|
|
||||||
formatDate,
|
formatDate,
|
||||||
formatRelativeTime,
|
formatRelativeTime,
|
||||||
formatChatTime
|
formatChatTime
|
||||||
@@ -42,18 +40,13 @@ import {
|
|||||||
useTheme,
|
useTheme,
|
||||||
useLanguage,
|
useLanguage,
|
||||||
useHapticsEnabled,
|
useHapticsEnabled,
|
||||||
useSettingsActions,
|
|
||||||
useTenantStates,
|
|
||||||
useTenantInfo,
|
|
||||||
} from '@/stores';
|
} from '@/stores';
|
||||||
|
import { useTenantLoad, useTenantInfo } from '@/stores/tenantStore';
|
||||||
|
|
||||||
// 验证规则
|
// 验证规则
|
||||||
import { loginSchema } from '@/schemas';
|
import { loginSchema } from '@/schemas';
|
||||||
import type { LoginFormData } from '@/schemas';
|
import type { LoginFormData } from '@/schemas';
|
||||||
|
|
||||||
// API 服务
|
|
||||||
import { authService } from '@/services';
|
|
||||||
|
|
||||||
// 自定义 Hooks
|
// 自定义 Hooks
|
||||||
import { useDebounce, useThrottle, useHaptics } from '@/hooks';
|
import { useDebounce, useThrottle, useHaptics } from '@/hooks';
|
||||||
|
|
||||||
@@ -71,14 +64,14 @@ export default function DemoScreen() {
|
|||||||
const login = useUserStore((state) => state.login);
|
const login = useUserStore((state) => state.login);
|
||||||
const logout = useUserStore((state) => state.logout);
|
const logout = useUserStore((state) => state.logout);
|
||||||
|
|
||||||
const { tenantLoad } = useTenantStates();
|
const tenantLoad = useTenantLoad();
|
||||||
const tenantInfo = useTenantInfo();
|
const tenantInfo = useTenantInfo();
|
||||||
|
|
||||||
// 设置状态
|
// 设置状态
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const language = useLanguage();
|
const language = useLanguage();
|
||||||
const hapticsEnabled = useHapticsEnabled();
|
const hapticsEnabled = useHapticsEnabled();
|
||||||
const { setTheme, setLanguage, setHapticsEnabled } = useSettingsActions();
|
const { setTheme, setLanguage, setHapticsEnabled } = useSettingsStore();
|
||||||
// const setTheme = useSettingsStore((state) => state.setTheme);
|
// const setTheme = useSettingsStore((state) => state.setTheme);
|
||||||
// const setLanguage = useSettingsStore((state) => state.setLanguage);
|
// const setLanguage = useSettingsStore((state) => state.setLanguage);
|
||||||
// const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
|
// const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
|
||||||
@@ -189,7 +182,7 @@ export default function DemoScreen() {
|
|||||||
counter,
|
counter,
|
||||||
};
|
};
|
||||||
|
|
||||||
await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData);
|
storageManager.session.setItem(STORAGE_KEYS.USER_PREFERENCES, testData);
|
||||||
haptics.success();
|
haptics.success();
|
||||||
Alert.alert('成功', '数据已保存到本地存储');
|
Alert.alert('成功', '数据已保存到本地存储');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -201,7 +194,7 @@ export default function DemoScreen() {
|
|||||||
const handleLoadFromStorage = async () => {
|
const handleLoadFromStorage = async () => {
|
||||||
try {
|
try {
|
||||||
haptics.light();
|
haptics.light();
|
||||||
const data = await Storage.getObject<any>(STORAGE_KEYS.USER_PREFERENCES);
|
const data = storageManager.session.getItem(STORAGE_KEYS.USER_PREFERENCES);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setStorageValue(JSON.stringify(data, null, 2));
|
setStorageValue(JSON.stringify(data, null, 2));
|
||||||
@@ -229,7 +222,7 @@ export default function DemoScreen() {
|
|||||||
counter: Math.floor(Math.random() * 100),
|
counter: Math.floor(Math.random() * 100),
|
||||||
};
|
};
|
||||||
|
|
||||||
SessionStorage.setObject(SESSION_KEYS.FORM_DRAFT, testData);
|
storageManager.session.setItem(STORAGE_KEYS.FORM_DRAFT, testData);
|
||||||
haptics.success();
|
haptics.success();
|
||||||
Alert.alert('成功', '数据已保存到会话存储(应用重启后会丢失)');
|
Alert.alert('成功', '数据已保存到会话存储(应用重启后会丢失)');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -241,7 +234,7 @@ export default function DemoScreen() {
|
|||||||
const handleLoadFromSession = () => {
|
const handleLoadFromSession = () => {
|
||||||
try {
|
try {
|
||||||
haptics.light();
|
haptics.light();
|
||||||
const data = SessionStorage.getObject<any>(SESSION_KEYS.FORM_DRAFT);
|
const data = storageManager.session.getItem(STORAGE_KEYS.FORM_DRAFT);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setSessionValue(JSON.stringify(data, null, 2));
|
setSessionValue(JSON.stringify(data, null, 2));
|
||||||
@@ -259,7 +252,7 @@ export default function DemoScreen() {
|
|||||||
const handleClearSession = () => {
|
const handleClearSession = () => {
|
||||||
try {
|
try {
|
||||||
haptics.light();
|
haptics.light();
|
||||||
SessionStorage.clear();
|
storageManager.session.clear();
|
||||||
setSessionValue('');
|
setSessionValue('');
|
||||||
haptics.success();
|
haptics.success();
|
||||||
Alert.alert('成功', '会话存储已清空');
|
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 组件已渲染 ===');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
|
export default function TabHoneScreen() {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<>
|
||||||
<Text style={styles.title}>🚀 热更新演示</Text>
|
<Stack.Screen
|
||||||
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
|
options={{
|
||||||
|
title: '首页',
|
||||||
<View style={styles.infoContainer}>
|
headerShown: false,
|
||||||
<Text style={styles.infoTitle}>当前版本信息:</Text>
|
}}
|
||||||
<Text style={styles.infoText}>{getUpdateInfo()}</Text>
|
/>
|
||||||
</View>
|
<HomeScreen />
|
||||||
|
</>
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.button, isChecking && styles.buttonDisabled]}
|
|
||||||
onPress={checkForUpdates}
|
|
||||||
disabled={isChecking}
|
|
||||||
>
|
|
||||||
{isChecking ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<Text style={styles.buttonText}>检查更新</Text>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{updateInfo ? (
|
|
||||||
<View style={styles.updateInfoContainer}>
|
|
||||||
<Text style={styles.updateInfoText}>{updateInfo}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<View style={styles.instructionsContainer}>
|
|
||||||
<Text style={styles.instructionsTitle}>📝 使用说明:</Text>
|
|
||||||
<Text style={styles.instructionsText}>
|
|
||||||
1. 使用 EAS Build 构建生产版本{'\n'}
|
|
||||||
2. 修改代码后运行 eas update 发布更新{'\n'}
|
|
||||||
3. 打开应用点击"检查更新"按钮{'\n'}
|
|
||||||
4. 应用会自动下载并提示重启
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: 20,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginVertical: 20,
|
|
||||||
height: 1,
|
|
||||||
width: '80%',
|
|
||||||
},
|
|
||||||
infoContainer: {
|
|
||||||
backgroundColor: 'rgba(0, 122, 255, 0.1)',
|
|
||||||
padding: 15,
|
|
||||||
borderRadius: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
infoTitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
infoText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
lineHeight: 18,
|
|
||||||
},
|
|
||||||
button: {
|
|
||||||
backgroundColor: '#007AFF',
|
|
||||||
paddingHorizontal: 30,
|
|
||||||
paddingVertical: 15,
|
|
||||||
borderRadius: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
minWidth: 200,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
buttonDisabled: {
|
|
||||||
backgroundColor: '#999',
|
|
||||||
},
|
|
||||||
buttonText: {
|
|
||||||
color: '#fff',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
},
|
|
||||||
updateInfoContainer: {
|
|
||||||
backgroundColor: 'rgba(52, 199, 89, 0.1)',
|
|
||||||
padding: 15,
|
|
||||||
borderRadius: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
updateInfoText: {
|
|
||||||
fontSize: 14,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
instructionsContainer: {
|
|
||||||
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
|
||||||
padding: 15,
|
|
||||||
borderRadius: 10,
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
instructionsTitle: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
instructionsText: {
|
|
||||||
fontSize: 13,
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { PaperProvider, MD3DarkTheme, MD3LightTheme } from 'react-native-paper';
|
|||||||
// ✅ 从 hooks 目录导入
|
// ✅ 从 hooks 目录导入
|
||||||
import { useColorScheme } from '@/hooks';
|
import { useColorScheme } from '@/hooks';
|
||||||
// ✅ 从 stores 目录导入
|
// ✅ 从 stores 目录导入
|
||||||
import { restoreUserState, restoreSettingsState, useTenantActions } from '@/stores';
|
import { restoreUserState, restoreSettingsState } from '@/stores';
|
||||||
|
import { requestTenantInfo } from '@/stores/tenantStore';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
// Catch any errors thrown by the Layout component.
|
// Catch any errors thrown by the Layout component.
|
||||||
@@ -32,7 +33,6 @@ export default function RootLayout() {
|
|||||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||||
...FontAwesome.font,
|
...FontAwesome.font,
|
||||||
});
|
});
|
||||||
const { requestTenantInfo } = useTenantActions();
|
|
||||||
|
|
||||||
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { StyleSheet, ScrollView, TouchableOpacity, View, Text, useColorScheme as useSystemColorScheme } from 'react-native';
|
import { StyleSheet, ScrollView, TouchableOpacity, View, Text, useColorScheme as useSystemColorScheme } from 'react-native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useTheme, useSettingsActions } from '@/stores';
|
import { useTheme, useSettingsStore } from '@/stores';
|
||||||
import { useHaptics } from '@/hooks';
|
import { useHaptics } from '@/hooks';
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
|
|
||||||
export default function ThemeTestScreen() {
|
export default function ThemeTestScreen() {
|
||||||
const currentTheme = useTheme();
|
const currentTheme = useTheme();
|
||||||
const { setTheme } = useSettingsActions();
|
const { setTheme } = useSettingsStore();
|
||||||
const haptics = useHaptics();
|
const haptics = useHaptics();
|
||||||
const systemColorScheme = useSystemColorScheme();
|
const systemColorScheme = useSystemColorScheme();
|
||||||
|
|
||||||
|
|||||||
BIN
assets/images/game/menu/blockThird.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
assets/images/game/menu/chess.png
Normal file
|
After Width: | Height: | Size: 8.5 KiB |
BIN
assets/images/game/menu/clock-solid.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/images/game/menu/clock-solid_dark.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/images/game/menu/electronic.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
assets/images/game/menu/fishing.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
assets/images/game/menu/home-star-solid.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
assets/images/game/menu/live.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
assets/images/game/menu/lottery.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
assets/images/game/menu/recommend.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
assets/images/game/menu/sports.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
assets/images/game/menu/trial.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
@@ -5,7 +5,7 @@ import { ExternalLink } from './ExternalLink';
|
|||||||
import { MonoText } from './StyledText';
|
import { MonoText } from './StyledText';
|
||||||
import { Text, View } from './Themed';
|
import { Text, View } from './Themed';
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
|
|
||||||
export default function EditScreenInfo({ path }: { path: string }) {
|
export default function EditScreenInfo({ path }: { path: string }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
165
components/Header/index.tsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/**
|
||||||
|
* 首页 Header 组件
|
||||||
|
* 包含搜索、用户信息、消息等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
TextInput,
|
||||||
|
Image,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme';
|
||||||
|
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
searchContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
height: 36,
|
||||||
|
},
|
||||||
|
searchInput: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
color: colors.text + '80',
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 8,
|
||||||
|
minWidth: 16,
|
||||||
|
height: 16,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onSearch?: (keyword: string) => void;
|
||||||
|
onMessagePress?: () => void;
|
||||||
|
onUserPress?: () => void;
|
||||||
|
unreadCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header 组件
|
||||||
|
*/
|
||||||
|
export default function Header({
|
||||||
|
onSearch,
|
||||||
|
onMessagePress,
|
||||||
|
onUserPress,
|
||||||
|
unreadCount = 0,
|
||||||
|
}: HeaderProps) {
|
||||||
|
const theme = useColorScheme();
|
||||||
|
const s = styles[theme];
|
||||||
|
const { colors } = useThemeInfo();
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
if (searchText.trim()) {
|
||||||
|
onSearch?.(searchText);
|
||||||
|
}
|
||||||
|
}, [searchText, onSearch]);
|
||||||
|
|
||||||
|
const handleClearSearch = useCallback(() => {
|
||||||
|
setSearchText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
{/* 顶部栏 */}
|
||||||
|
<View style={s.header}>
|
||||||
|
{/* Logo */}
|
||||||
|
<Text style={s.logo}>🎮 游戏大厅</Text>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<View style={s.searchContainer}>
|
||||||
|
<Text style={{ color: colors.text + '60', fontSize: 16 }}>🔍</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.searchInput, s.searchPlaceholder]}
|
||||||
|
placeholder="搜索游戏..."
|
||||||
|
placeholderTextColor={colors.text + '60'}
|
||||||
|
value={searchText}
|
||||||
|
onChangeText={setSearchText}
|
||||||
|
onSubmitEditing={handleSearch}
|
||||||
|
returnKeyType="search"
|
||||||
|
/>
|
||||||
|
{searchText ? (
|
||||||
|
<TouchableOpacity onPress={handleClearSearch}>
|
||||||
|
<Text style={{ fontSize: 16 }}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 消息按钮 */}
|
||||||
|
<TouchableOpacity style={s.iconButton} onPress={onMessagePress} activeOpacity={0.7}>
|
||||||
|
<Text style={{ fontSize: 18 }}>💬</Text>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<View style={s.badge}>
|
||||||
|
<Text style={s.badgeText}>{unreadCount > 99 ? '99+' : unreadCount}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 用户按钮 */}
|
||||||
|
<TouchableOpacity style={s.iconButton} onPress={onUserPress} activeOpacity={0.7}>
|
||||||
|
<Text style={{ fontSize: 18 }}>👤</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StyleSheet, View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
import { StyleSheet, View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
||||||
import { ThemedText, ThemedView, useThemeColor } from './Themed';
|
import { ThemedText, ThemedView, useThemeColor } from './Themed';
|
||||||
import { useTheme, useSettingsActions } from '@/stores';
|
import { useTheme, useSettingsStore } from '@/stores';
|
||||||
import { useHaptics } from '@/hooks';
|
import { useHaptics } from '@/hooks';
|
||||||
|
|
||||||
export function ThemeDemo() {
|
export function ThemeDemo() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { setTheme } = useSettingsActions();
|
const { setTheme } = useSettingsStore();
|
||||||
const haptics = useHaptics();
|
const haptics = useHaptics();
|
||||||
|
|
||||||
// 获取主题颜色
|
// 获取主题颜色
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import { Text as DefaultText, View as DefaultView, TextStyle } from 'react-native';
|
import { Text as DefaultText, View as DefaultView, TextStyle } from 'react-native';
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
import { useColorScheme } from '@/hooks/useTheme';
|
import { useColorScheme } from '@/hooks/useTheme';
|
||||||
|
|
||||||
type ThemeProps = {
|
type ThemeProps = {
|
||||||
|
|||||||
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
@@ -0,0 +1,6 @@
|
|||||||
|
// theme enum
|
||||||
|
export enum ThemeEnum {
|
||||||
|
LIGHT = 'light',
|
||||||
|
DARK = 'dark',
|
||||||
|
ORANGE = 'orange',
|
||||||
|
}
|
||||||
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, // 批量开户注册
|
||||||
|
}
|
||||||
@@ -24,3 +24,6 @@ export {
|
|||||||
|
|
||||||
// Client-only value (for SSR/Web compatibility)
|
// Client-only value (for SSR/Web compatibility)
|
||||||
export { useClientOnlyValue } from './useClientOnlyValue';
|
export { useClientOnlyValue } from './useClientOnlyValue';
|
||||||
|
|
||||||
|
// Game Menus
|
||||||
|
export { useGameMainMenus, useMenuDataLoaded, useSelectedCategory } from './useGameMenus';
|
||||||
|
|||||||
155
hooks/useGameMenus.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
|
import useGameStore from '@/stores/gameStore';
|
||||||
|
import { GameMainTypesEnum, defaultHomeGameTabMenus, gameMainTypesMap } from '@/constants/game';
|
||||||
|
import { ThemeEnum } from '@/constants/theme';
|
||||||
|
import { forEach, cloneDeep, map, filter } from 'lodash-es';
|
||||||
|
import { useIsLoggedIn } from '@/stores/userStore';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||||
|
|
||||||
|
|
||||||
|
type GameMenu = {
|
||||||
|
name: string;
|
||||||
|
key: string;
|
||||||
|
icon?: string;
|
||||||
|
logo?: string;
|
||||||
|
children?: GameMenu[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 有子菜单的游戏类型
|
||||||
|
const hasSubGameMainTypes = [
|
||||||
|
GameMainTypesEnum.CHESS,
|
||||||
|
GameMainTypesEnum.ELECTRONIC,
|
||||||
|
GameMainTypesEnum.FISHING,
|
||||||
|
GameMainTypesEnum.BLOCK_THIRD,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const useGameMainMenus = (theme: ThemeEnum) => {
|
||||||
|
// 在 hook 顶层调用 useIsLoggedIn
|
||||||
|
const isLogin = useIsLoggedIn();
|
||||||
|
|
||||||
|
// 从 store 获取必要的数据 - 直接获取,不使用 useShallow
|
||||||
|
const menuSort = useGameStore((state) => state.menuSort);
|
||||||
|
const gameBigClass = useGameStore((state) => state.gameBigClass);
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log('🎮 useGameMainMenus - menuSort:', menuSort, 'length:', menuSort?.length);
|
||||||
|
}
|
||||||
|
// 使用 useMemo 缓存计算结果,避免每次都创建新对象
|
||||||
|
return useMemo(() => {
|
||||||
|
const defaultMenus = cloneDeep(defaultHomeGameTabMenus);
|
||||||
|
|
||||||
|
if (theme === ThemeEnum.DARK) {
|
||||||
|
forEach(defaultMenus, (item) => {
|
||||||
|
if (item.key === GameMainTypesEnum.RECENT) {
|
||||||
|
item.icon = 'clock-solid_dark';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const gameMenu = map(menuSort, (item: Record<string, any>) => {
|
||||||
|
const typeName = gameMainTypesMap[item.type as GameMainTypesEnum];
|
||||||
|
|
||||||
|
const children = hasSubGameMainTypes.includes(item.type)
|
||||||
|
? map(gameBigClass?.[typeName], (it) => {
|
||||||
|
return {
|
||||||
|
name: it.play_cname,
|
||||||
|
key: `${it.play_id}`,
|
||||||
|
play_sort: it.play_sort,
|
||||||
|
darkImgSrc: `/images/game/${typeName}/dark_${it.play_id}.png`,
|
||||||
|
lightImgSrc: `/images/game/${typeName}/light_${it.play_id}.png`,
|
||||||
|
colorImgSrc: it.logo3_img_url || `/images/game/${typeName}/color_${it.play_id}.png`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.play_name,
|
||||||
|
key: `${item.type}`,
|
||||||
|
icon: typeName,
|
||||||
|
logo: item.logo_url1,
|
||||||
|
children,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log(gameMenu, 'gameMenu');
|
||||||
|
}
|
||||||
|
|
||||||
|
return map(
|
||||||
|
[
|
||||||
|
...filter(defaultMenus, (item) => [GameMainTypesEnum.RECOMMEND].includes(item.key)),
|
||||||
|
...(isLogin
|
||||||
|
? filter(gameMenu, (item) => Number(item.key) !== GameMainTypesEnum.TRIAL)
|
||||||
|
: gameMenu),
|
||||||
|
...filter(defaultMenus, (item) =>
|
||||||
|
[GameMainTypesEnum.COLLECT, GameMainTypesEnum.RECENT].includes(item.key)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
(item) => ({
|
||||||
|
...item,
|
||||||
|
// 为了在 React Native 中正确加载本地图片,使用 require 的方式
|
||||||
|
// 这里保留原始的 icon 名称,在组件中使用 require 加载
|
||||||
|
icon: item.icon,
|
||||||
|
key: `${item.key}`,
|
||||||
|
})
|
||||||
|
) as GameMenu[];
|
||||||
|
}, [theme, isLogin, menuSort, gameBigClass]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useMenuDataLoaded = () => useGameStore((state) => state.menuSort?.length > 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏分类选择 Hook(统一管理)
|
||||||
|
*
|
||||||
|
* 管理当前选中的游戏分类,支持:
|
||||||
|
* - 从 gameStore 获取当前选中分类
|
||||||
|
* - 更新选中分类并保存到 session storage
|
||||||
|
* - 页面刷新后恢复选中分类
|
||||||
|
*
|
||||||
|
* @returns {Object} 包含 selectedCategory 和 setSelectedCategory 的对象
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const { selectedCategory, setSelectedCategory } = useSelectedCategory();
|
||||||
|
*
|
||||||
|
* // 获取当前选中分类
|
||||||
|
* console.log(selectedCategory); // '103'
|
||||||
|
*
|
||||||
|
* // 更新选中分类
|
||||||
|
* setSelectedCategory('1');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const useSelectedCategory = () => {
|
||||||
|
const selectedCategory = useGameStore((state) => state.selectedCategory);
|
||||||
|
const setSelectedCategoryInStore = useGameStore((state) => state.setSelectedCategory);
|
||||||
|
|
||||||
|
// 初始化时从 session storage 恢复选中分类
|
||||||
|
useEffect(() => {
|
||||||
|
const initializeSelectedCategory = async () => {
|
||||||
|
try {
|
||||||
|
const savedCategory = storageManager.session.getItem(STORAGE_KEYS.APP_ACTIVE_MAIN_MENU_TAB);
|
||||||
|
if (savedCategory) {
|
||||||
|
setSelectedCategoryInStore(savedCategory);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore selected category:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializeSelectedCategory();
|
||||||
|
}, [setSelectedCategoryInStore]);
|
||||||
|
|
||||||
|
// 更新选中分类的回调函数
|
||||||
|
const setSelectedCategory = useCallback(
|
||||||
|
(categoryId: string) => {
|
||||||
|
setSelectedCategoryInStore(categoryId);
|
||||||
|
},
|
||||||
|
[setSelectedCategoryInStore]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedCategory,
|
||||||
|
setSelectedCategory,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,43 +7,44 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useColorScheme as useSystemColorScheme } from 'react-native';
|
import { useColorScheme as useSystemColorScheme } from 'react-native';
|
||||||
import { useTheme as useThemeStore } from '@/stores';
|
import { useTheme as useThemeStore } from '@/stores';
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
|
import { ThemeEnum } from '@/constants/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前颜色方案(light | dark)
|
* 获取当前颜色方案(light | dark | orange)
|
||||||
*
|
*
|
||||||
* 从 settingsStore 读取用户设置的主题
|
* 从 settingsStore 读取用户设置的主题
|
||||||
* 支持 'light' | 'dark' | 'auto' 三种模式
|
* 支持 'light' | 'dark' | 'orange' | 'auto' 四种模式
|
||||||
*/
|
*/
|
||||||
export function useColorScheme(): 'light' | 'dark' {
|
export function useColorScheme(): ThemeEnum {
|
||||||
const userTheme = useThemeStore();
|
const userTheme = useThemeStore();
|
||||||
const systemTheme = useSystemColorScheme();
|
const systemTheme = useSystemColorScheme();
|
||||||
|
|
||||||
// 如果用户选择了 'auto',则使用系统主题
|
// 如果用户选择了 'auto',则使用系统主题
|
||||||
if (userTheme === 'auto') {
|
if (userTheme === 'auto') {
|
||||||
return systemTheme === 'dark' ? 'dark' : 'light';
|
return systemTheme === 'dark' ? ThemeEnum.DARK : ThemeEnum.LIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 否则使用用户选择的主题
|
// 否则使用用户选择的主题
|
||||||
return userTheme;
|
return userTheme as ThemeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主题颜色
|
* 获取主题颜色
|
||||||
*
|
*
|
||||||
* @param props - 可选的自定义颜色 { light?: string; dark?: string }
|
* @param props - 可选的自定义颜色 { light?: string; dark?: string; orange?: string }
|
||||||
* @param colorName - Colors 中定义的颜色名称
|
* @param colorName - Colors 中定义的颜色名称
|
||||||
* @returns 当前主题对应的颜色值
|
* @returns 当前主题对应的颜色值
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* const textColor = useThemeColor({}, 'text');
|
* const textColor = useThemeColor({}, 'text');
|
||||||
* const customColor = useThemeColor({ light: '#000', dark: '#fff' }, 'text');
|
* const customColor = useThemeColor({ light: '#000', dark: '#fff', orange: '#f90' }, 'text');
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useThemeColor(
|
export function useThemeColor(
|
||||||
props: { light?: string; dark?: string },
|
props: { light?: string; dark?: string; orange?: string },
|
||||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
colorName: keyof typeof Colors.light & keyof typeof Colors.dark & keyof typeof Colors.orange
|
||||||
): string {
|
): string {
|
||||||
const theme = useColorScheme();
|
const theme = useColorScheme();
|
||||||
const colorFromProps = props[theme];
|
const colorFromProps = props[theme];
|
||||||
@@ -83,7 +84,7 @@ export function useThemeColors() {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
* const { theme, colors, isDark } = useThemeInfo();
|
* const { theme, colors, isDark, isLight, isOrange } = useThemeInfo();
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function useThemeInfo() {
|
export function useThemeInfo() {
|
||||||
@@ -93,8 +94,9 @@ export function useThemeInfo() {
|
|||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
theme,
|
theme,
|
||||||
colors,
|
colors,
|
||||||
isDark: theme === 'dark',
|
isDark: theme === ThemeEnum.DARK,
|
||||||
isLight: theme === 'light',
|
isLight: theme === ThemeEnum.LIGHT,
|
||||||
|
isOrange: theme === ThemeEnum.ORANGE,
|
||||||
}), [theme, colors]);
|
}), [theme, colors]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.66.0",
|
"react-hook-form": "^7.66.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
|
"react-native-linear-gradient": "^2.8.3",
|
||||||
"react-native-paper": "^5.14.5",
|
"react-native-paper": "^5.14.5",
|
||||||
"react-native-reanimated": "~4.1.1",
|
"react-native-reanimated": "~4.1.1",
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
|
|||||||
154
pages/HomeScreen/components/BannerSwiper/index.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* 轮播图组件
|
||||||
|
*
|
||||||
|
* 展示首页轮播图,支持自动播放和手动滑动
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Image,
|
||||||
|
TouchableOpacity,
|
||||||
|
ScrollView,
|
||||||
|
NativeScrollEvent,
|
||||||
|
NativeSyntheticEvent,
|
||||||
|
ActivityIndicator,
|
||||||
|
Dimensions,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useColorScheme } from '@/hooks';
|
||||||
|
// import type { Banner } from '@/types/home';
|
||||||
|
import { styles } from './styles';
|
||||||
|
import useMsgStore from '@/stores/msgStore';
|
||||||
|
|
||||||
|
|
||||||
|
interface BannerSwiperProps {}
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮播图组件
|
||||||
|
*/
|
||||||
|
export default function BannerSwiper({}: BannerSwiperProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
const autoPlayTimerRef = useRef<any>(null);
|
||||||
|
const { homeBanner } = useMsgStore();
|
||||||
|
|
||||||
|
|
||||||
|
// 加载轮播图数据
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果有传入的 banners 数据,直接使用
|
||||||
|
if (homeBanner.length > 0) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果没有数据,保持 loading 状态显示骨架屏
|
||||||
|
}, [homeBanner]);
|
||||||
|
|
||||||
|
// 处理 Banner 点击
|
||||||
|
const onBannerPress = useCallback((banner: Record<string, any>) => {
|
||||||
|
Alert.alert('轮播图', `点击了: ${banner.title || banner.id}`);
|
||||||
|
// 这里可以添加导航逻辑
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理滚动事件
|
||||||
|
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
|
const contentOffsetX = event.nativeEvent.contentOffset.x;
|
||||||
|
const index = Math.round(contentOffsetX / (width - 24));
|
||||||
|
setCurrentIndex(Math.min(index, homeBanner.length - 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启动自动播放
|
||||||
|
const startAutoPlay = useCallback(() => {
|
||||||
|
if (homeBanner.length <= 1) return;
|
||||||
|
autoPlayTimerRef.current = setInterval(() => {
|
||||||
|
setCurrentIndex((prev) => {
|
||||||
|
const nextIndex = (prev + 1) % homeBanner.length;
|
||||||
|
scrollViewRef.current?.scrollTo({
|
||||||
|
x: nextIndex * (width - 24),
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
return nextIndex;
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
}, [homeBanner.length]);
|
||||||
|
|
||||||
|
// 停止自动播放
|
||||||
|
const stopAutoPlay = useCallback(() => {
|
||||||
|
if (autoPlayTimerRef.current) {
|
||||||
|
clearInterval(autoPlayTimerRef.current);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 自动播放
|
||||||
|
useEffect(() => {
|
||||||
|
if (!loading && homeBanner.length > 0) {
|
||||||
|
startAutoPlay();
|
||||||
|
}
|
||||||
|
return () => stopAutoPlay();
|
||||||
|
}, [loading, homeBanner.length, startAutoPlay, stopAutoPlay]);
|
||||||
|
|
||||||
|
// 骨架屏 - 加载中显示占位符
|
||||||
|
if (loading || homeBanner.length === 0) {
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
s.image,
|
||||||
|
{
|
||||||
|
backgroundColor: colorScheme === 'dark' ? '#333' : '#e0e0e0',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={s.scrollView}
|
||||||
|
horizontal
|
||||||
|
pagingEnabled
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
onMomentumScrollBegin={stopAutoPlay}
|
||||||
|
onMomentumScrollEnd={startAutoPlay}
|
||||||
|
>
|
||||||
|
{homeBanner.map((banner) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={banner.id}
|
||||||
|
onPress={() => onBannerPress(banner)}
|
||||||
|
activeOpacity={0.9}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{ uri: banner.subject }}
|
||||||
|
style={s.image}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* 指示器 */}
|
||||||
|
<View style={s.indicatorContainer}>
|
||||||
|
{homeBanner.map((_, index) => (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
s.indicator,
|
||||||
|
index === currentIndex && s.indicatorActive,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* 首页底部 Tabs 导航组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useColorScheme } from '@/hooks';
|
||||||
|
import { createThemeStyles } from '@/theme';
|
||||||
|
import { Colors } from '@/theme';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingBottom: 8,
|
||||||
|
paddingTop: 8,
|
||||||
|
},
|
||||||
|
tabItem: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
tabIcon: {
|
||||||
|
fontSize: 24,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
tabLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.text + '80',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
tabLabelActive: {
|
||||||
|
color: colors.primary,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BottomTabsProps {
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
|
activeTab?: string;
|
||||||
|
onTabPress?: (tabId: string, action: string) => void;
|
||||||
|
items?: TabItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 Tab 项
|
||||||
|
*/
|
||||||
|
const DEFAULT_TABS: TabItem[] = [
|
||||||
|
{ id: 'recharge', label: '充值', icon: '💰', action: 'recharge' },
|
||||||
|
{ id: 'withdraw', label: '提现', icon: '💳', action: 'withdraw' },
|
||||||
|
{ id: 'activity', label: '活动', icon: '🎉', action: 'activity' },
|
||||||
|
{ id: 'service', label: '客服', icon: '🎧', action: 'service' },
|
||||||
|
{ id: 'help', label: '帮助', icon: '❓', action: 'help' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 底部 Tabs 导航组件
|
||||||
|
*/
|
||||||
|
export default function BottomTabs({
|
||||||
|
theme = 'light',
|
||||||
|
activeTab = 'recharge',
|
||||||
|
onTabPress,
|
||||||
|
items = DEFAULT_TABS,
|
||||||
|
}: BottomTabsProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const actualTheme = theme === 'light' || theme === 'dark' ? theme : colorScheme;
|
||||||
|
const s = styles[actualTheme];
|
||||||
|
const colors = Colors[actualTheme];
|
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState(activeTab);
|
||||||
|
|
||||||
|
const handleTabPress = useCallback(
|
||||||
|
(tabId: string, action: string) => {
|
||||||
|
setSelectedTab(tabId);
|
||||||
|
onTabPress?.(tabId, action);
|
||||||
|
},
|
||||||
|
[onTabPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
{items.map((item) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.id}
|
||||||
|
style={s.tabItem}
|
||||||
|
onPress={() => handleTabPress(item.id, item.action)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={s.tabIcon}>{item.icon}</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
s.tabLabel,
|
||||||
|
selectedTab === item.id && s.tabLabelActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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 { useColorScheme } from '@/hooks';
|
||||||
|
import { mockNavItems } from '@/services/mockHomeService';
|
||||||
|
import type { NavItem } from '@/types/home';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
},
|
||||||
|
navItem: {
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
marginRight: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minWidth: 75,
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
},
|
||||||
|
navItemActive: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
},
|
||||||
|
navIcon: {
|
||||||
|
fontSize: 22,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
navText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
navTextActive: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface FastFootNavProps {
|
||||||
|
items?: NavItem[];
|
||||||
|
onTabPress?: (tabId: string, action: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速底部导航组件
|
||||||
|
*/
|
||||||
|
export default function FastFootNav({ items: propItems, onTabPress }: FastFootNavProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const [items, setItems] = useState<NavItem[]>(propItems || []);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 加载导航项数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (propItems && propItems.length > 0) {
|
||||||
|
setItems(propItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadItems = async () => {
|
||||||
|
try {
|
||||||
|
// const data = await getNavItems();
|
||||||
|
// setItems(data.length > 0 ? data : mockNavItems);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载导航项失败:', error);
|
||||||
|
setItems(mockNavItems);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadItems();
|
||||||
|
}, [propItems]);
|
||||||
|
|
||||||
|
const handleNavPress = (item: NavItem) => {
|
||||||
|
setSelectedId(item.id);
|
||||||
|
onTabPress?.(item.id, item.action);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<ScrollView
|
||||||
|
style={s.scrollView}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
>
|
||||||
|
{items.map((item) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={item.id}
|
||||||
|
style={[
|
||||||
|
s.navItem,
|
||||||
|
selectedId === item.id && s.navItemActive,
|
||||||
|
]}
|
||||||
|
onPress={() => handleNavPress(item)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={s.navIcon}>{item.icon || '🎮'}</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
s.navText,
|
||||||
|
selectedId === item.id && s.navTextActive,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
219
pages/HomeScreen/components/GameMainMenus/index.tsx
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/**
|
||||||
|
* 游戏分类菜单组件
|
||||||
|
*
|
||||||
|
* 展示游戏分类,支持切换,使用真实数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useRef, useEffect, useState, useCallback, useMemo } from 'react';
|
||||||
|
import { View, Text, ScrollView, TouchableOpacity, Animated, Image, Platform } from 'react-native';
|
||||||
|
// import type { GameCategory } from '@/types/home';
|
||||||
|
import { styles } from './styles';
|
||||||
|
import { useGameMainMenus, useMenuDataLoaded, useSelectedCategory } from '@/hooks/useGameMenus';
|
||||||
|
// import useGameStore from '@/stores/gameStore';
|
||||||
|
// import { ThemeEnum } from '@/constants/theme';
|
||||||
|
import { Colors, useColorScheme } from '@/theme';
|
||||||
|
|
||||||
|
// 条件导入 LinearGradient - 仅在非 Web 平台使用
|
||||||
|
let LinearGradient: any = null;
|
||||||
|
if (Platform.OS !== 'web') {
|
||||||
|
LinearGradient = require('react-native-linear-gradient').default;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏菜单图片映射 - 使用 require 加载本地资源
|
||||||
|
const MENU_ICON_MAP: Record<string, any> = {
|
||||||
|
'recommend': require('../../../../assets/images/game/menu/recommend.png'),
|
||||||
|
'chess': require('../../../../assets/images/game/menu/chess.png'),
|
||||||
|
'electronic': require('../../../../assets/images/game/menu/electronic.png'),
|
||||||
|
'fishing': require('../../../../assets/images/game/menu/fishing.png'),
|
||||||
|
'lottery': require('../../../../assets/images/game/menu/lottery.png'),
|
||||||
|
'sports': require('../../../../assets/images/game/menu/sports.png'),
|
||||||
|
'trial': require('../../../../assets/images/game/menu/trial.png'),
|
||||||
|
'blockThird': require('../../../../assets/images/game/menu/blockThird.png'),
|
||||||
|
'clock-solid': require('../../../../assets/images/game/menu/clock-solid.png'),
|
||||||
|
'clock-solid_dark': require('../../../../assets/images/game/menu/clock-solid_dark.png'),
|
||||||
|
'home-star-solid': require('../../../../assets/images/game/menu/home-star-solid.png'),
|
||||||
|
'live': require('../../../../assets/images/game/menu/live.png'),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface GameMainMenuProps {
|
||||||
|
topHeight?: number;
|
||||||
|
showSubMenus?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏分类菜单组件
|
||||||
|
*/
|
||||||
|
export default function GameMainMenu({
|
||||||
|
topHeight = 0,
|
||||||
|
showSubMenus = true,
|
||||||
|
}: GameMainMenuProps) {
|
||||||
|
const theme = useColorScheme();
|
||||||
|
const s = styles[theme];
|
||||||
|
const scrollViewRef = useRef<ScrollView>(null);
|
||||||
|
const gameMenus = useGameMainMenus(theme);
|
||||||
|
|
||||||
|
// 从 hook 获取选中分类和更新方法
|
||||||
|
const { selectedCategory, setSelectedCategory } = useSelectedCategory();
|
||||||
|
|
||||||
|
// 检查数据加载完成
|
||||||
|
const isDataLoaded = useMenuDataLoaded();
|
||||||
|
|
||||||
|
// 使用 useMemo 缓存找到的索引,避免每次都重新计算
|
||||||
|
const selectedIndex = useMemo(() => {
|
||||||
|
return gameMenus.findIndex((cat) => cat.key === selectedCategory);
|
||||||
|
}, [selectedCategory, gameMenus]);
|
||||||
|
|
||||||
|
// 当分类改变时,滚动到该分类
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedIndex >= 0) {
|
||||||
|
scrollViewRef.current?.scrollTo({
|
||||||
|
x: selectedIndex * 100,
|
||||||
|
animated: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
|
// 使用 useCallback 稳定 onPress 回调
|
||||||
|
const handleCategoryPress = useCallback(
|
||||||
|
(categoryKey: string) => {
|
||||||
|
setSelectedCategory(categoryKey);
|
||||||
|
},
|
||||||
|
[setSelectedCategory]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 骨架屏 - 显示加载中的占位符
|
||||||
|
const renderSkeleton = () => (
|
||||||
|
<View style={s.container}>
|
||||||
|
<ScrollView
|
||||||
|
style={s.scrollView}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
|
<View
|
||||||
|
key={`skeleton-${index}`}
|
||||||
|
style={[s.menuItem, { backgroundColor: theme === 'dark' ? '#333' : '#e0e0e0' }]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
console.log('isDataLoaded', isDataLoaded);
|
||||||
|
// 如果动态数据还未加载,显示骨架屏
|
||||||
|
if (!isDataLoaded) {
|
||||||
|
return renderSkeleton();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<ScrollView
|
||||||
|
ref={scrollViewRef}
|
||||||
|
style={s.scrollView}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
>
|
||||||
|
{gameMenus.map((menu) => {
|
||||||
|
// 处理图片源 - 优先使用 logo(URL),其次使用 icon(本地资源)
|
||||||
|
let imageSource: any = null;
|
||||||
|
|
||||||
|
if (menu.logo) {
|
||||||
|
// logo 是 URL,直接使用
|
||||||
|
imageSource = { uri: menu.logo };
|
||||||
|
} else if (menu.icon) {
|
||||||
|
// icon 是本地资源名称,从映射表中获取
|
||||||
|
imageSource = MENU_ICON_MAP[menu.icon];
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = selectedCategory === menu.key;
|
||||||
|
const themeColors = Colors[theme];
|
||||||
|
|
||||||
|
// 获取渐变色 - 从主题色到透明
|
||||||
|
const gradientStart = `${themeColors.tint}40`; // 主题色 + 40% 透明度
|
||||||
|
const gradientEnd = `${themeColors.tint}00`; // 完全透明
|
||||||
|
|
||||||
|
const menuContent = (
|
||||||
|
<>
|
||||||
|
{imageSource && (
|
||||||
|
<Image source={imageSource} style={s.menuIcon} resizeMode="contain" />
|
||||||
|
)}
|
||||||
|
<Text style={[s.menuText, isActive && s.menuTextActive]}>
|
||||||
|
{menu.name}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果是选中状态,使用 LinearGradient 包装(非 Web 平台)或 CSS 渐变(Web 平台)
|
||||||
|
if (isActive) {
|
||||||
|
// Web 平台使用 CSS 渐变
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
const webStyle = {
|
||||||
|
...s.menuItem,
|
||||||
|
...s.menuItemActive,
|
||||||
|
background: `linear-gradient(to top, ${gradientStart}, ${gradientEnd})`,
|
||||||
|
backgroundColor: undefined, // 移除 backgroundColor,使用 background
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={menu.key}
|
||||||
|
style={webStyle}
|
||||||
|
onPress={() => handleCategoryPress(menu.key)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{menuContent}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非 Web 平台使用 LinearGradient
|
||||||
|
if (LinearGradient) {
|
||||||
|
return (
|
||||||
|
<LinearGradient
|
||||||
|
key={menu.key}
|
||||||
|
colors={[gradientStart, gradientEnd]}
|
||||||
|
start={{ x: 0.5, y: 1 }} // 从下往上
|
||||||
|
end={{ x: 0.5, y: 0 }}
|
||||||
|
style={[s.menuItem, s.menuItemActive]}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
|
||||||
|
onPress={() => handleCategoryPress(menu.key)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{menuContent}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用方案:如果 LinearGradient 不可用,使用纯色
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={menu.key}
|
||||||
|
style={[s.menuItem, s.menuItemActive]}
|
||||||
|
onPress={() => handleCategoryPress(menu.key)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{menuContent}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未选中状态,使用普通 TouchableOpacity
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={menu.key}
|
||||||
|
style={s.menuItem}
|
||||||
|
onPress={() => handleCategoryPress(menu.key)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{menuContent}
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
70
pages/HomeScreen/components/GameMainMenus/styles.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { createThemeStyles, createResponsiveThemeStyles } from '@/theme';
|
||||||
|
import { Dimensions } from 'react-native';
|
||||||
|
|
||||||
|
// const { width } = Dimensions.get('window');
|
||||||
|
// const BANNER_HEIGHT = width * 0.32534; // 保持 32.534% 的宽高比
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
export const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingTop: 5,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
menuItem: {
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 5,
|
||||||
|
marginRight: 8,
|
||||||
|
borderRadius: 0,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
elevation: 1,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 2,
|
||||||
|
borderBottomColor: 'transparent',
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
},
|
||||||
|
menuItemActive: {
|
||||||
|
// backgroundColor: `${colors.tint}15`, // 主题色 + 20% 透明度
|
||||||
|
elevation: 2,
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
borderBottomColor: colors.tint,
|
||||||
|
borderRadius: 0,
|
||||||
|
},
|
||||||
|
menuText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
menuTextActive: {
|
||||||
|
color: colors.tint,
|
||||||
|
},
|
||||||
|
menuIcon: {
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const themeStyles = createResponsiveThemeStyles({
|
||||||
|
menuItemActive: {
|
||||||
|
backgroundColor: '',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
menuItemActive: {
|
||||||
|
backgroundColor: '',
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
menuItemActive: {
|
||||||
|
backgroundColor: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
177
pages/HomeScreen/components/Header.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* 首页 Header 组件
|
||||||
|
* 包含搜索、用户信息、消息等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TouchableOpacity,
|
||||||
|
TextInput,
|
||||||
|
Image,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { useColorScheme } from '@/hooks';
|
||||||
|
import { createThemeStyles } from '@/theme';
|
||||||
|
import { Colors } from '@/theme';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
logo: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
searchContainer: {
|
||||||
|
flex: 1,
|
||||||
|
marginHorizontal: 12,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
borderRadius: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
height: 36,
|
||||||
|
},
|
||||||
|
searchInput: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.text,
|
||||||
|
},
|
||||||
|
searchPlaceholder: {
|
||||||
|
color: colors.text + '80',
|
||||||
|
},
|
||||||
|
iconButton: {
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
badge: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: -4,
|
||||||
|
right: -4,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 8,
|
||||||
|
minWidth: 16,
|
||||||
|
height: 16,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
badgeText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
onSearch?: (keyword: string) => void;
|
||||||
|
onMessagePress?: () => void;
|
||||||
|
onUserPress?: () => void;
|
||||||
|
unreadCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Header 组件
|
||||||
|
*/
|
||||||
|
export default function Header({
|
||||||
|
onSearch,
|
||||||
|
onMessagePress,
|
||||||
|
onUserPress,
|
||||||
|
unreadCount = 0,
|
||||||
|
}: HeaderProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const colors = Colors[colorScheme];
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = useCallback(() => {
|
||||||
|
if (searchText.trim()) {
|
||||||
|
onSearch?.(searchText);
|
||||||
|
}
|
||||||
|
}, [searchText, onSearch]);
|
||||||
|
|
||||||
|
const handleClearSearch = useCallback(() => {
|
||||||
|
setSearchText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
{/* 顶部栏 */}
|
||||||
|
<View style={s.header}>
|
||||||
|
{/* Logo */}
|
||||||
|
<Text style={s.logo}>🎮 游戏大厅</Text>
|
||||||
|
|
||||||
|
{/* 搜索框 */}
|
||||||
|
<View style={s.searchContainer}>
|
||||||
|
<Text style={{ color: colors.text + '60', fontSize: 16 }}>🔍</Text>
|
||||||
|
<TextInput
|
||||||
|
style={[s.searchInput, s.searchPlaceholder]}
|
||||||
|
placeholder="搜索游戏..."
|
||||||
|
placeholderTextColor={colors.text + '60'}
|
||||||
|
value={searchText}
|
||||||
|
onChangeText={setSearchText}
|
||||||
|
onSubmitEditing={handleSearch}
|
||||||
|
returnKeyType="search"
|
||||||
|
/>
|
||||||
|
{searchText ? (
|
||||||
|
<TouchableOpacity onPress={handleClearSearch}>
|
||||||
|
<Text style={{ fontSize: 16 }}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 消息按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={s.iconButton}
|
||||||
|
onPress={onMessagePress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 18 }}>💬</Text>
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<View style={s.badge}>
|
||||||
|
<Text style={s.badgeText}>
|
||||||
|
{unreadCount > 99 ? '99+' : unreadCount}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
{/* 用户按钮 */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={s.iconButton}
|
||||||
|
onPress={onUserPress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={{ fontSize: 18 }}>👤</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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 { useColorScheme } from '@/hooks';
|
||||||
|
import { mockHighPrizeGames } from '@/services/mockHomeService';
|
||||||
|
import type { HighPrizeGame as HighPrizeGameType } from '@/types/home';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
scrollView: {
|
||||||
|
paddingHorizontal: 0,
|
||||||
|
},
|
||||||
|
gameItem: {
|
||||||
|
width: 110,
|
||||||
|
height: 110,
|
||||||
|
marginRight: 10,
|
||||||
|
borderRadius: 12,
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
overflow: 'hidden',
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 4,
|
||||||
|
},
|
||||||
|
gameIcon: {
|
||||||
|
fontSize: 44,
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
gameName: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.text,
|
||||||
|
textAlign: 'center',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
prizeTag: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
backgroundColor: colors.error,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 3,
|
||||||
|
borderRadius: 4,
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.2,
|
||||||
|
shadowRadius: 2,
|
||||||
|
},
|
||||||
|
prizeText: {
|
||||||
|
fontSize: 10,
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface HighPrizeGameProps {
|
||||||
|
games?: HighPrizeGameType[];
|
||||||
|
onGamePress?: (game: HighPrizeGameType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 高奖金游戏组件
|
||||||
|
*/
|
||||||
|
export default function HighPrizeGame({ games: propGames, onGamePress }: HighPrizeGameProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const [games, setGames] = useState<HighPrizeGameType[]>(propGames || []);
|
||||||
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 加载高奖金游戏数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (propGames && propGames.length > 0) {
|
||||||
|
setGames(propGames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGames = async () => {
|
||||||
|
try {
|
||||||
|
// const data = await getHighPrizeGames();
|
||||||
|
// setGames(data.length > 0 ? data : mockHighPrizeGames);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载高奖金游戏失败:', error);
|
||||||
|
setGames(mockHighPrizeGames);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadGames();
|
||||||
|
}, [propGames]);
|
||||||
|
|
||||||
|
if (games.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<Text style={s.title}>🏆 实时爆奖</Text>
|
||||||
|
<ScrollView
|
||||||
|
style={s.scrollView}
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
>
|
||||||
|
{games.map((game) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={game.id}
|
||||||
|
style={s.gameItem}
|
||||||
|
onPress={() => {
|
||||||
|
setSelectedId(game.id);
|
||||||
|
onGamePress?.(game);
|
||||||
|
}}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
{game.icon ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: game.icon }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={s.gameIcon}>🎰</Text>
|
||||||
|
)}
|
||||||
|
<Text style={s.gameName} numberOfLines={2}>
|
||||||
|
{game.play_up_name}
|
||||||
|
</Text>
|
||||||
|
<View style={s.prizeTag}>
|
||||||
|
<Text style={s.prizeText}>¥{Math.floor(game.payout_amount / 1000)}k</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
208
pages/HomeScreen/components/Lobby.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* 游戏大厅组件
|
||||||
|
*
|
||||||
|
* 展示游戏列表,使用真实数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
FlatList,
|
||||||
|
TouchableOpacity,
|
||||||
|
Image,
|
||||||
|
ActivityIndicator,
|
||||||
|
Dimensions,
|
||||||
|
} from 'react-native';
|
||||||
|
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme';
|
||||||
|
import { useSelectedCategory } from '@/hooks/useGameMenus';
|
||||||
|
import { getMockGamesByCategory } from '@/services/mockHomeService';
|
||||||
|
import type { Game } from '@/types/home';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 12,
|
||||||
|
},
|
||||||
|
gameGrid: {
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
gameCard: {
|
||||||
|
flex: 1,
|
||||||
|
margin: 6,
|
||||||
|
borderRadius: 12,
|
||||||
|
overflow: 'hidden',
|
||||||
|
backgroundColor: colors.card,
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 3 },
|
||||||
|
shadowOpacity: 0.15,
|
||||||
|
shadowRadius: 6,
|
||||||
|
},
|
||||||
|
gameCardPressed: {
|
||||||
|
opacity: 0.8,
|
||||||
|
},
|
||||||
|
gameImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: 140,
|
||||||
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
gameIcon: {
|
||||||
|
fontSize: 48,
|
||||||
|
},
|
||||||
|
gameInfo: {
|
||||||
|
padding: 10,
|
||||||
|
},
|
||||||
|
gameName: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.text,
|
||||||
|
marginBottom: 6,
|
||||||
|
},
|
||||||
|
gameButton: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 6,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 6,
|
||||||
|
},
|
||||||
|
gameButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingVertical: 40,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface LobbyProps {
|
||||||
|
games?: Game[];
|
||||||
|
onGamePress?: (game: Game) => void;
|
||||||
|
topHeight?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 游戏大厅组件
|
||||||
|
*/
|
||||||
|
export default function Lobby({
|
||||||
|
games: propGames,
|
||||||
|
onGamePress,
|
||||||
|
topHeight = 0,
|
||||||
|
}: LobbyProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const { colors } = useThemeInfo();
|
||||||
|
const { selectedCategory } = useSelectedCategory();
|
||||||
|
const [games, setGames] = useState<Game[]>(propGames || []);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// 加载游戏数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (propGames && propGames.length > 0) {
|
||||||
|
setGames(propGames);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGames = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
// const response = await getGames(selectedCategory);
|
||||||
|
// setGames(response.games.length > 0 ? response.games : getMockGamesByCategory(selectedCategory));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载游戏失败:', error);
|
||||||
|
setGames(getMockGamesByCategory(selectedCategory));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadGames();
|
||||||
|
}, [propGames, selectedCategory]);
|
||||||
|
|
||||||
|
const renderGameCard = ({ item }: { item: Game }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={s.gameCard}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
onPress={() => onGamePress?.(item)}
|
||||||
|
>
|
||||||
|
<View style={s.gameImage}>
|
||||||
|
{item.icon ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: item.icon }}
|
||||||
|
style={{ width: '100%', height: '100%' }}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Text style={s.gameIcon}>🎮</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={s.gameInfo}>
|
||||||
|
<Text style={s.gameName} numberOfLines={2}>
|
||||||
|
{item.play_up_name}
|
||||||
|
{item.play_cname ? ` - ${item.play_cname}` : ''}
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={s.gameButton}
|
||||||
|
onPress={() => onGamePress?.(item)}
|
||||||
|
>
|
||||||
|
<Text style={s.gameButtonText}>进入游戏</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={s.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={colors.primary} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (games.length === 0) {
|
||||||
|
return (
|
||||||
|
<View style={s.emptyContainer}>
|
||||||
|
<Text style={{ fontSize: 40 }}>🎮</Text>
|
||||||
|
<Text style={s.emptyText}>暂无游戏</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={s.container}>
|
||||||
|
<FlatList
|
||||||
|
data={games}
|
||||||
|
renderItem={renderGameCard}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
numColumns={2}
|
||||||
|
scrollEnabled={false}
|
||||||
|
contentContainerStyle={s.gameGrid}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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 { useColorScheme } from '@/hooks';
|
||||||
|
import { mockNotices } from '@/services/mockHomeService';
|
||||||
|
import type { Notice } from '@/types/home';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
backgroundColor: colors.backgroundSecondary,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
marginHorizontal: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: colors.cardShadow,
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 3,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#FFFFFF',
|
||||||
|
marginRight: 8,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 12,
|
||||||
|
color: colors.text,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
closeButton: {
|
||||||
|
marginLeft: 8,
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
closeText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.textSecondary,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface NoticeBarProps {
|
||||||
|
notices?: Notice[];
|
||||||
|
onNoticePress?: (notice: Notice) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公告栏组件
|
||||||
|
*/
|
||||||
|
export default function NoticeBar({ notices: propNotices, onNoticePress }: NoticeBarProps) {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const [notices, setNotices] = useState<Notice[]>(propNotices || []);
|
||||||
|
const [currentNotice, setCurrentNotice] = useState(0);
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
const animatedValue = useRef(new Animated.Value(1)).current;
|
||||||
|
|
||||||
|
// 加载公告数据
|
||||||
|
useEffect(() => {
|
||||||
|
if (propNotices && propNotices.length > 0) {
|
||||||
|
setNotices(propNotices);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadNotices = async () => {
|
||||||
|
try {
|
||||||
|
// const data = await getNotices();
|
||||||
|
// setNotices(data.length > 0 ? data : mockNotices);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载公告失败:', error);
|
||||||
|
setNotices(mockNotices);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadNotices();
|
||||||
|
}, [propNotices]);
|
||||||
|
|
||||||
|
// 自动切换公告
|
||||||
|
useEffect(() => {
|
||||||
|
if (notices.length === 0) return;
|
||||||
|
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentNotice((prev) => (prev + 1) % notices.length);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [notices.length]);
|
||||||
|
|
||||||
|
// 处理关闭公告
|
||||||
|
const handleClose = () => {
|
||||||
|
Animated.timing(animatedValue, {
|
||||||
|
toValue: 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start(() => {
|
||||||
|
setVisible(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理公告点击
|
||||||
|
const handleNoticePress = () => {
|
||||||
|
if (notices.length > 0) {
|
||||||
|
onNoticePress?.(notices[currentNotice]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!visible || notices.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNoticeData = notices[currentNotice];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
s.container,
|
||||||
|
{
|
||||||
|
opacity: animatedValue,
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
scaleY: animatedValue,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={s.label}>📢</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
onPress={handleNoticePress}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
>
|
||||||
|
<Text style={s.content} numberOfLines={1}>
|
||||||
|
{currentNoticeData.title || currentNoticeData.content}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={s.closeButton}
|
||||||
|
onPress={handleClose}
|
||||||
|
>
|
||||||
|
<Text style={s.closeText}>✕</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
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 GameMainMenusComponent from './GameMainMenus';
|
||||||
|
import LobbyComponent from './Lobby';
|
||||||
|
import HighPrizeGameComponent from './HighPrizeGame';
|
||||||
|
import FastFootNavComponent from './FastFootNav';
|
||||||
|
import HeaderComponent from './Header';
|
||||||
|
import BottomTabsComponent from './BottomTabs';
|
||||||
|
|
||||||
|
// 使用 React.memo 优化组件性能,避免不必要的重新渲染
|
||||||
|
export const BannerSwiper = React.memo(BannerSwiperComponent);
|
||||||
|
export const NoticeBar = React.memo(NoticeBarComponent);
|
||||||
|
export const GameMainMenus = React.memo(GameMainMenusComponent);
|
||||||
|
export const Lobby = React.memo(LobbyComponent);
|
||||||
|
export const HighPrizeGame = React.memo(HighPrizeGameComponent);
|
||||||
|
export const FastFootNav = React.memo(FastFootNavComponent);
|
||||||
|
export const Header = React.memo(HeaderComponent);
|
||||||
|
export const BottomTabs = React.memo(BottomTabsComponent);
|
||||||
|
|
||||||
157
pages/HomeScreen/index.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
/**
|
||||||
|
* 完整首页容器
|
||||||
|
* 包含 Header、内容区域、BottomTabs
|
||||||
|
* 支持主题切换和真实数据
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { View, ScrollView, RefreshControl, Alert } from 'react-native';
|
||||||
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
|
import { createThemeStyles, useColorScheme, useThemeInfo } from '@/theme';
|
||||||
|
import {
|
||||||
|
Header,
|
||||||
|
BannerSwiper,
|
||||||
|
NoticeBar,
|
||||||
|
GameMainMenus,
|
||||||
|
Lobby,
|
||||||
|
HighPrizeGame,
|
||||||
|
FastFootNav,
|
||||||
|
} from './components';
|
||||||
|
import { requestHomePageData } from '@/stores/gameStore';
|
||||||
|
import { useTenantLoad } from '@/stores/tenantStore';
|
||||||
|
import type {
|
||||||
|
Banner,
|
||||||
|
Notice,
|
||||||
|
GameCategory,
|
||||||
|
Game,
|
||||||
|
HighPrizeGame as HighPrizeGameType,
|
||||||
|
} from '@/types/home';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主题样式
|
||||||
|
*/
|
||||||
|
const styles = createThemeStyles((colors) => ({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.background,
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
paddingBottom: 20,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 完整首页容器
|
||||||
|
*/
|
||||||
|
export default function HomePage() {
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const s = styles[colorScheme];
|
||||||
|
const { isDark: isDarkTheme, colors } = useThemeInfo();
|
||||||
|
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
const tenantLoad = useTenantLoad();
|
||||||
|
|
||||||
|
// 加载首页数据
|
||||||
|
const loadHomePageData = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await requestHomePageData();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载首页数据失败:', error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('租户数据加载完成:', tenantLoad);
|
||||||
|
if (tenantLoad) {
|
||||||
|
loadHomePageData();
|
||||||
|
}
|
||||||
|
}, [loadHomePageData, tenantLoad]);
|
||||||
|
|
||||||
|
// 下拉刷新
|
||||||
|
const handleRefresh = useCallback(async () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
try {
|
||||||
|
await loadHomePageData();
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}, [loadHomePageData]);
|
||||||
|
|
||||||
|
// 处理游戏点击
|
||||||
|
const handleGamePress = useCallback((game: Game) => {
|
||||||
|
Alert.alert('游戏', `点击了: ${game.play_up_name}`);
|
||||||
|
// 这里可以添加打开游戏的逻辑
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理底部 Tab 点击
|
||||||
|
const handleTabPress = useCallback((tabId: string, action: string) => {
|
||||||
|
Alert.alert('导航', `点击了: ${tabId}`);
|
||||||
|
// 这里可以添加导航逻辑
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 处理搜索
|
||||||
|
const handleSearch = useCallback((keyword: string) => {
|
||||||
|
Alert.alert('搜索', `搜索关键词: ${keyword}`);
|
||||||
|
// 这里可以添加搜索逻辑
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 根据主题选择要显示的组件
|
||||||
|
const renderContent = () => {
|
||||||
|
if (isDarkTheme) {
|
||||||
|
// 深色主题布局
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GameMainMenus />
|
||||||
|
<BannerSwiper />
|
||||||
|
<NoticeBar />
|
||||||
|
<HighPrizeGame onGamePress={handleGamePress} />
|
||||||
|
<Lobby onGamePress={handleGamePress} />
|
||||||
|
<FastFootNav onTabPress={handleTabPress} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 浅色主题布局
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<BannerSwiper />
|
||||||
|
<NoticeBar />
|
||||||
|
<GameMainMenus />
|
||||||
|
<Lobby onGamePress={handleGamePress} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={s.container}>
|
||||||
|
{/* Header */}
|
||||||
|
<Header
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onMessagePress={() => Alert.alert('消息', '消息功能')}
|
||||||
|
onUserPress={() => Alert.alert('用户', '用户中心')}
|
||||||
|
unreadCount={3}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 内容区域 */}
|
||||||
|
<View style={s.contentContainer}>
|
||||||
|
<ScrollView
|
||||||
|
style={s.contentContainer}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.primary}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={s.scrollContent}>{renderContent()}</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,4 +37,5 @@
|
|||||||
|
|
||||||
// 导出业务页面组件
|
// 导出业务页面组件
|
||||||
export { default as TestPage } from './TestPage';
|
export { default as TestPage } from './TestPage';
|
||||||
|
export { default as HomeScreen } from './HomeScreen';
|
||||||
|
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
|||||||
react-native:
|
react-native:
|
||||||
specifier: 0.81.5
|
specifier: 0.81.5
|
||||||
version: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
|
version: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
|
||||||
|
react-native-linear-gradient:
|
||||||
|
specifier: ^2.8.3
|
||||||
|
version: 2.8.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||||
react-native-paper:
|
react-native-paper:
|
||||||
specifier: ^5.14.5
|
specifier: ^5.14.5
|
||||||
version: 5.14.5(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
version: 5.14.5(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
|
||||||
@@ -2985,6 +2988,12 @@ packages:
|
|||||||
react: '*'
|
react: '*'
|
||||||
react-native: '*'
|
react-native: '*'
|
||||||
|
|
||||||
|
react-native-linear-gradient@2.8.3:
|
||||||
|
resolution: {integrity: sha512-KflAXZcEg54PXkLyflaSZQ3PJp4uC4whM7nT/Uot9m0e/qxFV3p6uor1983D1YOBJbJN7rrWdqIjq0T42jOJyA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '*'
|
||||||
|
react-native: '*'
|
||||||
|
|
||||||
react-native-paper@5.14.5:
|
react-native-paper@5.14.5:
|
||||||
resolution: {integrity: sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==}
|
resolution: {integrity: sha512-eaIH5bUQjJ/mYm4AkI6caaiyc7BcHDwX6CqNDi6RIxfxfWxROsHpll1oBuwn/cFvknvA8uEAkqLk/vzVihI3AQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -7208,6 +7217,11 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
|
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
|
||||||
|
|
||||||
|
react-native-linear-gradient@2.8.3(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.1.0
|
||||||
|
react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0)
|
||||||
|
|
||||||
react-native-paper@5.14.5(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
react-native-paper@5.14.5(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@callstack/react-theme-provider': 3.0.9(react@19.1.0)
|
'@callstack/react-theme-provider': 3.0.9(react@19.1.0)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ app.use(cors({
|
|||||||
const API_TARGET = process.env.API_TARGET || 'https://51zhh5.notbug.org';
|
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) => {
|
PROXY_PATHS.forEach((path) => {
|
||||||
|
|||||||
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 authService } from './authService';
|
||||||
export { default as userService } from './userService';
|
export { default as userService } from './userService';
|
||||||
export { default as tenantService } from './tenantService';
|
export { default as tenantService } from './tenantService';
|
||||||
|
export { default as gameService } from './gameService';
|
||||||
|
|
||||||
|
|||||||
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: string | 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 { request } from '@/utils/network/api';
|
||||||
// import type { User, UpdateProfileFormData } from '@/schemas/user';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 响应接口
|
* API 响应接口
|
||||||
*/
|
*/
|
||||||
interface ApiResponse<T = any> {
|
interface ApiResponse<T = any> {
|
||||||
code: number;
|
success: string;
|
||||||
message: string;
|
message: string;
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
@@ -32,7 +31,6 @@ class TenantService {
|
|||||||
cmdId: 371130,
|
cmdId: 371130,
|
||||||
headerType: 1,
|
headerType: 1,
|
||||||
apiName: 'getPlatformData',
|
apiName: 'getPlatformData',
|
||||||
tid: '',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
267
stores/gameStore.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* 游戏状态管理
|
||||||
|
* 使用 Zustand + AsyncStorage 持久化
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { create } from 'zustand';
|
||||||
|
// import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||||
|
import gameService from '@/services/gameService';
|
||||||
|
import appConfig from '@/utils/config';
|
||||||
|
import useTenantStore from '@/stores/tenantStore';
|
||||||
|
import useMsgStore from '@/stores/msgStore';
|
||||||
|
import { filter, map, concat, cloneDeep, groupBy } from 'lodash-es';
|
||||||
|
import { GameMainKeysEnum, defaultHomeGameTabMenus } from '@/constants/game';
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
interface State {
|
||||||
|
appLoginPopType: number;
|
||||||
|
receiveAwardPopType: number;
|
||||||
|
appLoginPop: boolean;
|
||||||
|
menuSort: Record<string, any>[];
|
||||||
|
rebateGameSort: Record<string, any>[];
|
||||||
|
originalGames: Record<string, any>[];
|
||||||
|
blockchainGames: Record<string, any>[];
|
||||||
|
homeHotGames: Record<string, any>[];
|
||||||
|
gamesTry: Record<string, any>[];
|
||||||
|
gamesTryPlayIds: number[];
|
||||||
|
smallClassGames: Record<string, any>;
|
||||||
|
gameBigClass: Record<string, any>;
|
||||||
|
selectedCategory: string; // 当前选中的游戏分类
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
interface Actions {
|
||||||
|
setHomePageData: (data: Record<string, any>) => void;
|
||||||
|
setOriginalGames: (data: Record<string, any>[]) => void;
|
||||||
|
setBlockchainGames: (data: Record<string, any>[]) => void;
|
||||||
|
setGamesTry: (data: Record<string, any>[]) => void;
|
||||||
|
setHomeHotGames: (data: Record<string, any>[]) => void;
|
||||||
|
setSmallClassGame: (data: Record<string, any>) => void;
|
||||||
|
setGameBigClass: (data: Record<string, any>) => void;
|
||||||
|
setSelectedCategory: (categoryId: string) => void; // 设置选中的游戏分类
|
||||||
|
// requestHomePageData: (data?: Record<string, any>) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户状态 Store
|
||||||
|
*/
|
||||||
|
const useGameStore = create<State & Actions>()((set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
appLoginPopType: 0,
|
||||||
|
receiveAwardPopType: 0,
|
||||||
|
appLoginPop: false,
|
||||||
|
menuSort: [], // 游戏主菜单
|
||||||
|
rebateGameSort: [],
|
||||||
|
originalGames: [], // 原创游戏
|
||||||
|
blockchainGames: [], // 区块链游戏
|
||||||
|
homeHotGames: [], // 热门游戏
|
||||||
|
gamesTry: [], // 试玩游戏
|
||||||
|
gamesTryPlayIds: [], // 试玩游戏id列表
|
||||||
|
smallClassGames: {},
|
||||||
|
gameBigClass: {},
|
||||||
|
selectedCategory: '103', // 默认选中推荐分类
|
||||||
|
|
||||||
|
|
||||||
|
// 保存首页数据
|
||||||
|
setHomePageData: (data: any) => {
|
||||||
|
if (data) {
|
||||||
|
const { setNotices, setBanners } = useMsgStore.getState();
|
||||||
|
|
||||||
|
// 设置注册弹窗
|
||||||
|
const appLoginPopType = data.appLoginPopType || 0;
|
||||||
|
const receiveAwardPopType = data.receiveAwardPopType || 0;
|
||||||
|
const appLoginPop = data.appLoginPop || false;
|
||||||
|
|
||||||
|
// 菜单排序
|
||||||
|
const menuSort = filter(data.version_type, item => item.sort_v !== 0 && item.state == 1).sort((a, b) => {
|
||||||
|
return b.sort_v - a.sort_v;
|
||||||
|
});
|
||||||
|
console.log(menuSort, 'menuSort 1');
|
||||||
|
// version_shuffle
|
||||||
|
const rebateGameSort = data.version_shuffle || [];
|
||||||
|
|
||||||
|
// 所有游戏销售状态
|
||||||
|
// gameStore.setGameAllStates(res.data.gameState);
|
||||||
|
|
||||||
|
// 公告
|
||||||
|
setNotices(data.news?.data || []);
|
||||||
|
|
||||||
|
// 轮播图
|
||||||
|
setBanners(data.banners?.data || []);
|
||||||
|
|
||||||
|
set({ appLoginPopType, receiveAwardPopType, appLoginPop, menuSort, rebateGameSort });
|
||||||
|
|
||||||
|
// 原创游戏
|
||||||
|
get().setOriginalGames(data.originalGames);
|
||||||
|
|
||||||
|
// 区块链游戏
|
||||||
|
get().setBlockchainGames(data.hsGames);
|
||||||
|
|
||||||
|
// 试玩游戏
|
||||||
|
get().setGamesTry(data.gamesTry);
|
||||||
|
|
||||||
|
// 首页热门游戏
|
||||||
|
get().setHomeHotGames(data.homeHotGames?.[1] || []);
|
||||||
|
get().setSmallClassGame(data.homeHotGames);
|
||||||
|
|
||||||
|
// 三方游戏
|
||||||
|
get().setGameBigClass(data.thirdGames);
|
||||||
|
|
||||||
|
// 手动持久化
|
||||||
|
// storageManager.setItem(STORAGE_KEYS.TENANT_STORE, JSON.stringify({ tenantInfo: data }));
|
||||||
|
}
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log('💾 Tenant info saved:', data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setOriginalGames: (list: Record<string, any>[]) => {
|
||||||
|
const originalGames = map(list, (item: any) => ({
|
||||||
|
...item,
|
||||||
|
isOriginal: true,
|
||||||
|
})).sort((a: any, b: any) => a.hotVal - b.hotVal);
|
||||||
|
set({ originalGames });
|
||||||
|
},
|
||||||
|
|
||||||
|
setBlockchainGames: (list: Record<string, any>[]) => {
|
||||||
|
set({ blockchainGames: list || [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
setGamesTry: (list: Record<string, any>[]) => {
|
||||||
|
set({ gamesTry: list || [] });
|
||||||
|
storageManager.session.setItem(STORAGE_KEYS.GAME_TRY, list);
|
||||||
|
const gamesTryPlayIds = concat(...map(list, item => map(item.subList, subItem => Number(subItem.play_id))));
|
||||||
|
set({ gamesTryPlayIds });
|
||||||
|
},
|
||||||
|
|
||||||
|
setHomeHotGames: (list: Record<string, any>[]) => {
|
||||||
|
set({ homeHotGames: list || [] });
|
||||||
|
},
|
||||||
|
|
||||||
|
setSmallClassGame: (data: Record<string, any>) => {
|
||||||
|
set({ smallClassGames: {
|
||||||
|
[GameMainKeysEnum.HOT_ELECTRONIC]: data?.[2] || [],
|
||||||
|
[GameMainKeysEnum.HOT_FISHING]: data?.[3] || [],
|
||||||
|
[GameMainKeysEnum.HOT_CHESS]: data?.[5] || [],
|
||||||
|
[GameMainKeysEnum.HOT_BLOCK_THIRD]: data?.[8] || [],
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
|
||||||
|
setGameBigClass: (data: Record<string, any>) => {
|
||||||
|
const groupByType = cloneDeep(groupBy(data, item => item.big_type));
|
||||||
|
set({ gameBigClass: {
|
||||||
|
[GameMainKeysEnum.CHESS]: groupByType?.[1] || [],
|
||||||
|
[GameMainKeysEnum.ELECTRONIC]: groupByType?.[2] || [],
|
||||||
|
[GameMainKeysEnum.FISHING]: groupByType?.[3] || [],
|
||||||
|
[GameMainKeysEnum.LIVE]: groupByType?.[4] || [],
|
||||||
|
[GameMainKeysEnum.SPORTS]: groupByType?.[5] || [],
|
||||||
|
[GameMainKeysEnum.LOTTERY]: groupByType?.[7] || [],
|
||||||
|
[GameMainKeysEnum.BLOCK_THIRD]: groupByType?.[10] || [],
|
||||||
|
} });
|
||||||
|
},
|
||||||
|
|
||||||
|
setSelectedCategory: (categoryId: string) => {
|
||||||
|
set({ selectedCategory: categoryId });
|
||||||
|
// 保存到 session storage,页面刷新后仍然保留
|
||||||
|
storageManager.session.setItem(STORAGE_KEYS.APP_ACTIVE_MAIN_MENU_TAB, categoryId);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 从 AsyncStorage 恢复状态的函数
|
||||||
|
export const restoreGameState = async () => {
|
||||||
|
try {
|
||||||
|
const stored = await storageManager.session.getItem(STORAGE_KEYS.TENANT_STORE);
|
||||||
|
if (stored) {
|
||||||
|
const state = JSON.parse(stored);
|
||||||
|
useGameStore.setState(state);
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log('✅ Tenant state restored from storage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to restore tenant state:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取首页数据
|
||||||
|
export const requestHomePageData = async () => {
|
||||||
|
try {
|
||||||
|
const { tenantInfo } = useTenantStore.getState();
|
||||||
|
const params = {
|
||||||
|
tid: tenantInfo?.tid,
|
||||||
|
aseq: {
|
||||||
|
aseq: appConfig.app.aseqId,
|
||||||
|
},
|
||||||
|
version_type: {
|
||||||
|
version_type: 3, // 1 是棋牌, 2 是网赚 3, 是综合彩票
|
||||||
|
},
|
||||||
|
news: {
|
||||||
|
page_start: 1,
|
||||||
|
num_per_page: 999,
|
||||||
|
chan_con: 8,
|
||||||
|
state: 1,
|
||||||
|
message_id: '10,13', //10公告 13 跑马灯
|
||||||
|
sort_flag: 1,
|
||||||
|
vip_level: 0, //userInfo.value.cust_level || 0,
|
||||||
|
proxy: tenantInfo?.proxy,
|
||||||
|
apply: 8, // 1 棋牌;2 网赚;3 综合彩票;3 老版彩票; (apply后端设置默认为1,为可选参数)
|
||||||
|
language: 0, //Number(window.localStorage.getItem('languageNum') || '0'),
|
||||||
|
},
|
||||||
|
game_news: {
|
||||||
|
page_start: 1,
|
||||||
|
num_per_page: 9999,
|
||||||
|
state: 1,
|
||||||
|
message_id: 17,
|
||||||
|
sort_flag: 1,
|
||||||
|
tid: tenantInfo?.tid,
|
||||||
|
proxy: tenantInfo?.proxy,
|
||||||
|
chan_con: 0,
|
||||||
|
apply: 8,
|
||||||
|
ma_id: 10,
|
||||||
|
// vip_level: userInfo.value.cust_level || 0,
|
||||||
|
vip_level: 0,
|
||||||
|
type: 'MWEB',
|
||||||
|
language: 0,
|
||||||
|
},
|
||||||
|
banners: {
|
||||||
|
page_start: 1,
|
||||||
|
num_per_page: 999,
|
||||||
|
chan_con: 8,
|
||||||
|
state: 1,
|
||||||
|
message_id: 12,
|
||||||
|
sort_flag: 1,
|
||||||
|
tid: tenantInfo?.tid,
|
||||||
|
proxy: tenantInfo?.proxy,
|
||||||
|
apply: 8,
|
||||||
|
location: '1',
|
||||||
|
type: 'MWEB',
|
||||||
|
language: Number(window.localStorage.getItem('languageNum') || '0'),
|
||||||
|
},
|
||||||
|
homeHotGames: {
|
||||||
|
// cust_id: userInfo.value.cust_id || '0',
|
||||||
|
cust_id: '0',
|
||||||
|
},
|
||||||
|
proxyConfig: {
|
||||||
|
proxy: tenantInfo?.proxy,
|
||||||
|
},
|
||||||
|
hotGames: {
|
||||||
|
size: 12,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const { data } = await gameService.getHomePageData(params);
|
||||||
|
|
||||||
|
// ✅ 直接调用 setHomePageData action
|
||||||
|
useGameStore.getState().setHomePageData(data);
|
||||||
|
|
||||||
|
if (__DEV__) {
|
||||||
|
console.log('✅ Home-data info loaded:', data);
|
||||||
|
}
|
||||||
|
return Promise.resolve(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to request home-data info:', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useGameStore;
|
||||||
@@ -8,7 +8,6 @@ export {
|
|||||||
useUser,
|
useUser,
|
||||||
useIsLoggedIn,
|
useIsLoggedIn,
|
||||||
useToken,
|
useToken,
|
||||||
useUserActions,
|
|
||||||
restoreUserState,
|
restoreUserState,
|
||||||
} from './userStore';
|
} from './userStore';
|
||||||
export type { User } from './userStore';
|
export type { User } from './userStore';
|
||||||
@@ -21,7 +20,6 @@ export {
|
|||||||
useNotificationsEnabled,
|
useNotificationsEnabled,
|
||||||
useSoundEnabled,
|
useSoundEnabled,
|
||||||
useHapticsEnabled,
|
useHapticsEnabled,
|
||||||
useSettingsActions,
|
|
||||||
restoreSettingsState,
|
restoreSettingsState,
|
||||||
} from './settingsStore';
|
} from './settingsStore';
|
||||||
export type { Theme, Language } from './settingsStore';
|
export type { Theme, Language } from './settingsStore';
|
||||||
@@ -29,8 +27,12 @@ export type { Theme, Language } from './settingsStore';
|
|||||||
// Tenant Store
|
// Tenant Store
|
||||||
export {
|
export {
|
||||||
default as useTenantStore,
|
default as useTenantStore,
|
||||||
useTenantInfo,
|
|
||||||
useTenantStates,
|
|
||||||
useTenantActions,
|
|
||||||
restoreTenantState,
|
restoreTenantState,
|
||||||
} from './tenantStore';
|
} from './tenantStore';
|
||||||
|
|
||||||
|
// Game Store
|
||||||
|
export {
|
||||||
|
default as useGameStore,
|
||||||
|
restoreGameState,
|
||||||
|
} from './gameStore';
|
||||||
|
|
||||||
|
|||||||
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,12 +5,12 @@
|
|||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题类型
|
* 主题类型
|
||||||
*/
|
*/
|
||||||
export type Theme = 'light' | 'dark' | 'auto';
|
export type Theme = 'light' | 'dark' | 'orange' | 'auto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言类型
|
* 语言类型
|
||||||
@@ -59,7 +59,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
setTheme: (theme) => {
|
setTheme: (theme) => {
|
||||||
set({ theme });
|
set({ theme });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('🎨 Theme changed:', theme);
|
console.log('🎨 Theme changed:', theme);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
setLanguage: (language) => {
|
setLanguage: (language) => {
|
||||||
set({ language });
|
set({ language });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('🌐 Language changed:', language);
|
console.log('🌐 Language changed:', language);
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
setNotificationsEnabled: (enabled) => {
|
setNotificationsEnabled: (enabled) => {
|
||||||
set({ notificationsEnabled: enabled });
|
set({ notificationsEnabled: enabled });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled');
|
console.log('🔔 Notifications:', enabled ? 'enabled' : 'disabled');
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
setSoundEnabled: (enabled) => {
|
setSoundEnabled: (enabled) => {
|
||||||
set({ soundEnabled: enabled });
|
set({ soundEnabled: enabled });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled');
|
console.log('🔊 Sound:', enabled ? 'enabled' : 'disabled');
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
setHapticsEnabled: (enabled) => {
|
setHapticsEnabled: (enabled) => {
|
||||||
set({ hapticsEnabled: enabled });
|
set({ hapticsEnabled: enabled });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(get()));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled');
|
console.log('📳 Haptics:', enabled ? 'enabled' : 'disabled');
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
resetSettings: () => {
|
resetSettings: () => {
|
||||||
set(DEFAULT_SETTINGS);
|
set(DEFAULT_SETTINGS);
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('settings-storage', JSON.stringify(DEFAULT_SETTINGS));
|
storageManager.local.setItem(STORAGE_KEYS.SETTINGS_STORE, get());
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('🔄 Settings reset to default');
|
console.log('🔄 Settings reset to default');
|
||||||
}
|
}
|
||||||
@@ -119,10 +119,9 @@ export const useSettingsStore = create<SettingsState>()((set, get) => ({
|
|||||||
// 从 AsyncStorage 恢复状态的函数
|
// 从 AsyncStorage 恢复状态的函数
|
||||||
export const restoreSettingsState = async () => {
|
export const restoreSettingsState = async () => {
|
||||||
try {
|
try {
|
||||||
const stored = await AsyncStorage.getItem('settings-storage');
|
const stored = await storageManager.local.getItem(STORAGE_KEYS.SETTINGS_STORE);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const state = JSON.parse(stored);
|
useSettingsStore.setState(stored);
|
||||||
useSettingsStore.setState(state);
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('✅ Settings state restored from storage');
|
console.log('✅ Settings state restored from storage');
|
||||||
}
|
}
|
||||||
@@ -152,16 +151,3 @@ export const useSoundEnabled = () => useSettingsStore((state) => state.soundEnab
|
|||||||
// 获取触觉反馈状态
|
// 获取触觉反馈状态
|
||||||
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled);
|
export const useHapticsEnabled = () => useSettingsStore((state) => state.hapticsEnabled);
|
||||||
|
|
||||||
// 获取设置操作方法
|
|
||||||
// 使用 useShallow 避免每次渲染都返回新对象
|
|
||||||
export const useSettingsActions = () =>
|
|
||||||
useSettingsStore(
|
|
||||||
useShallow((state) => ({
|
|
||||||
setTheme: state.setTheme,
|
|
||||||
setLanguage: state.setLanguage,
|
|
||||||
setNotificationsEnabled: state.setNotificationsEnabled,
|
|
||||||
setSoundEnabled: state.setSoundEnabled,
|
|
||||||
setHapticsEnabled: state.setHapticsEnabled,
|
|
||||||
resetSettings: state.resetSettings,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -4,41 +4,36 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
// import { useShallow } from 'zustand/react/shallow';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import storageManager, { STORAGE_KEYS } from '@/utils/storageManager';
|
||||||
import { STORAGE_KEYS } from '@/utils/storage';
|
import { tenantService } from '@/services';
|
||||||
import { tenantService } from '@/services/tenantService';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户信息接口
|
* 租户信息接口
|
||||||
*/
|
*/
|
||||||
// export interface Tenant {
|
export interface Tenant {
|
||||||
// id: string;
|
tid: number;
|
||||||
// username: string;
|
proxy: number;
|
||||||
// email: string;
|
create_time: string;
|
||||||
// avatar?: string;
|
domain_addr: 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>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
interface State {
|
||||||
|
tenantInfo: Tenant | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
interface Actions {
|
||||||
|
setTenantInfo: (data: Record<string, any>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 租户状态 Store
|
* 租户状态 Store
|
||||||
*/
|
*/
|
||||||
const useTenantStore = create<TenantState>()((set, get) => ({
|
const useTenantStore = create<State & Actions>()((set, get) => ({
|
||||||
// 初始状态
|
// 初始状态
|
||||||
tenantInfo: null,
|
tenantInfo: null,
|
||||||
|
|
||||||
@@ -47,39 +42,19 @@ const useTenantStore = create<TenantState>()((set, get) => ({
|
|||||||
setTenantInfo: (data: any) => {
|
setTenantInfo: (data: any) => {
|
||||||
set({ tenantInfo: data });
|
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__) {
|
if (__DEV__) {
|
||||||
console.log('💾 Tenant info saved:', data);
|
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 恢复状态的函数
|
// 从 AsyncStorage 恢复状态的函数
|
||||||
export const restoreTenantState = async () => {
|
export const restoreTenantState = async () => {
|
||||||
try {
|
try {
|
||||||
const stored = await AsyncStorage.getItem(STORAGE_KEYS.TENANT_STORE);
|
const stored = storageManager.session.getItem(STORAGE_KEYS.TENANT_STORE);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const state = JSON.parse(stored);
|
const state = JSON.parse(stored);
|
||||||
useTenantStore.setState(state);
|
useTenantStore.setState(state);
|
||||||
@@ -99,24 +74,29 @@ export const restoreTenantState = async () => {
|
|||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
export const useTenantInfo = () => useTenantStore((state) => state.tenantInfo);
|
export const useTenantInfo = () => useTenantStore((state) => state.tenantInfo);
|
||||||
|
|
||||||
|
// 租户数据是否加载完成
|
||||||
|
export const useTenantLoad = () => useTenantStore((state) => !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time);
|
||||||
|
|
||||||
// 获取租户状态
|
// 获取租户信息
|
||||||
export const useTenantStates = () =>
|
export const requestTenantInfo = async (): Promise<Tenant> => {
|
||||||
useTenantStore(
|
try {
|
||||||
useShallow((state) => ({
|
// 使用 getState() 而不是 hook
|
||||||
tenantInfo: state.tenantInfo,
|
const { setTenantInfo } = useTenantStore.getState();
|
||||||
tenantLoad: !!state.tenantInfo?.tid || !!state.tenantInfo?.create_time,
|
const params = {
|
||||||
}))
|
domain_addr: 'https://51zhh5.notbug.org',
|
||||||
);
|
};
|
||||||
|
const { data } = await tenantService.getPlatformData(params);
|
||||||
|
|
||||||
// 获取租户操作方法
|
setTenantInfo(data);
|
||||||
// 使用 useShallow 避免每次渲染都返回新对象
|
|
||||||
export const useTenantActions = () =>
|
if (__DEV__) {
|
||||||
useTenantStore(
|
console.log('✅ Tenant info loaded:', data);
|
||||||
useShallow((state) => ({
|
}
|
||||||
setTenantInfo: state.setTenantInfo,
|
return Promise.resolve(data);
|
||||||
requestTenantInfo: state.requestTenantInfo,
|
} catch (error) {
|
||||||
}))
|
console.error('Failed to request tenant info:', error);
|
||||||
);
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default useTenantStore;
|
export default useTenantStore;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
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;
|
createdAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 状态
|
||||||
* 用户状态接口
|
interface State {
|
||||||
*/
|
|
||||||
interface UserState {
|
|
||||||
// 状态
|
|
||||||
user: User | null;
|
user: User | null;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
// 操作
|
// 操作
|
||||||
|
interface Actions {
|
||||||
setUser: (user: User) => void;
|
setUser: (user: User) => void;
|
||||||
setToken: (token: string) => void;
|
setToken: (token: string) => void;
|
||||||
login: (user: User, token: string) => void;
|
login: (user: User, token: string) => void;
|
||||||
@@ -40,7 +39,7 @@ interface UserState {
|
|||||||
/**
|
/**
|
||||||
* 用户状态 Store
|
* 用户状态 Store
|
||||||
*/
|
*/
|
||||||
export const useUserStore = create<UserState>()((set, get) => ({
|
export const useUserStore = create<State & Actions>()((set, get) => ({
|
||||||
// 初始状态
|
// 初始状态
|
||||||
user: null,
|
user: null,
|
||||||
isLoggedIn: false,
|
isLoggedIn: false,
|
||||||
@@ -51,7 +50,7 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
const newState = { user, isLoggedIn: true };
|
const newState = { user, isLoggedIn: true };
|
||||||
set(newState);
|
set(newState);
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('user-storage', JSON.stringify(newState));
|
storageManager.session.setItem(STORAGE_KEYS.USER_STORE, newState);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 设置 token
|
// 设置 token
|
||||||
@@ -59,8 +58,7 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
set({ token });
|
set({ token });
|
||||||
// 手动持久化 - 延迟执行以确保状态已更新
|
// 手动持久化 - 延迟执行以确保状态已更新
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const state = get();
|
storageManager.session.setItem(STORAGE_KEYS.USER_STORE, get());
|
||||||
AsyncStorage.setItem('user-storage', JSON.stringify(state));
|
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -74,9 +72,9 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
set(newState);
|
set(newState);
|
||||||
|
|
||||||
// 同时保存 token 到 AsyncStorage(用于 API 请求)
|
// 同时保存 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__) {
|
if (__DEV__) {
|
||||||
console.log('✅ User logged in:', user.username);
|
console.log('✅ User logged in:', user.username);
|
||||||
@@ -93,9 +91,9 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
set(newState);
|
set(newState);
|
||||||
|
|
||||||
// 清除 AsyncStorage 中的 token
|
// 清除 AsyncStorage 中的 token
|
||||||
AsyncStorage.removeItem('auth_token');
|
storageManager.session.removeItem('auth_token');
|
||||||
// 清除持久化状态
|
// 清除持久化状态
|
||||||
AsyncStorage.removeItem('user-storage');
|
storageManager.session.removeItem('user-storage');
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('👋 User logged out');
|
console.log('👋 User logged out');
|
||||||
@@ -109,7 +107,7 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
const newUser = { ...currentUser, ...updates };
|
const newUser = { ...currentUser, ...updates };
|
||||||
set({ user: newUser });
|
set({ user: newUser });
|
||||||
// 手动持久化
|
// 手动持久化
|
||||||
AsyncStorage.setItem('user-storage', JSON.stringify({ ...get(), user: newUser }));
|
storageManager.session.setItem(STORAGE_KEYS.USER_STORE, { ...get(), user: newUser });
|
||||||
|
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
console.log('📝 User updated:', updates);
|
console.log('📝 User updated:', updates);
|
||||||
@@ -121,7 +119,7 @@ export const useUserStore = create<UserState>()((set, get) => ({
|
|||||||
// 从 AsyncStorage 恢复状态的函数
|
// 从 AsyncStorage 恢复状态的函数
|
||||||
export const restoreUserState = async () => {
|
export const restoreUserState = async () => {
|
||||||
try {
|
try {
|
||||||
const stored = await AsyncStorage.getItem('user-storage');
|
const stored = await storageManager.session.getItem(STORAGE_KEYS.USER_STORE);
|
||||||
if (stored) {
|
if (stored) {
|
||||||
const state = JSON.parse(stored);
|
const state = JSON.parse(stored);
|
||||||
useUserStore.setState(state);
|
useUserStore.setState(state);
|
||||||
@@ -149,13 +147,13 @@ export const useToken = () => useUserStore((state) => state.token);
|
|||||||
|
|
||||||
// 获取用户操作方法
|
// 获取用户操作方法
|
||||||
// 使用 useShallow 避免每次渲染都返回新对象
|
// 使用 useShallow 避免每次渲染都返回新对象
|
||||||
export const useUserActions = () =>
|
// export const useUserActions = () =>
|
||||||
useUserStore(
|
// useUserStore(
|
||||||
useShallow((state) => ({
|
// useShallow((state) => ({
|
||||||
setUser: state.setUser,
|
// setUser: state.setUser,
|
||||||
setToken: state.setToken,
|
// setToken: state.setToken,
|
||||||
login: state.login,
|
// login: state.login,
|
||||||
logout: state.logout,
|
// logout: state.logout,
|
||||||
updateUser: state.updateUser,
|
// updateUser: state.updateUser,
|
||||||
}))
|
// }))
|
||||||
);
|
// );
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* 主题颜色配置
|
* 主题颜色配置
|
||||||
*
|
*
|
||||||
* 支持 light 和 dark 两种主题
|
|
||||||
* 可以通过 settingsStore 切换主题
|
* 可以通过 settingsStore 切换主题
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const tintColorLight = '#007AFF';
|
const tintColorLight = '#10c8e3';
|
||||||
const tintColorDark = '#0A84FF';
|
const tintColorDark = '#ffd69f';
|
||||||
|
const tintColorOrange = '#bd9534';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
light: {
|
light: {
|
||||||
@@ -59,6 +59,56 @@ export default {
|
|||||||
// 覆盖层
|
// 覆盖层
|
||||||
overlay: 'rgba(0, 0, 0, 0.5)',
|
overlay: 'rgba(0, 0, 0, 0.5)',
|
||||||
},
|
},
|
||||||
|
orange: {
|
||||||
|
// 文本颜色
|
||||||
|
text: '#000000',
|
||||||
|
textSecondary: '#666666',
|
||||||
|
textTertiary: '#999999',
|
||||||
|
textInverse: '#FFFFFF',
|
||||||
|
|
||||||
|
// 背景颜色
|
||||||
|
background: '#FFFFFF',
|
||||||
|
backgroundSecondary: '#F5F5F5',
|
||||||
|
backgroundTertiary: '#E5E5E5',
|
||||||
|
|
||||||
|
// 主题色
|
||||||
|
tint: tintColorOrange,
|
||||||
|
primary: '#007AFF',
|
||||||
|
secondary: '#5856D6',
|
||||||
|
success: '#34C759',
|
||||||
|
warning: '#FF9500',
|
||||||
|
error: '#FF3B30',
|
||||||
|
info: '#5AC8FA',
|
||||||
|
|
||||||
|
// 边框颜色
|
||||||
|
border: '#E5E5E5',
|
||||||
|
borderSecondary: '#D1D1D6',
|
||||||
|
|
||||||
|
// Tab 图标
|
||||||
|
tabIconDefault: '#8E8E93',
|
||||||
|
tabIconSelected: tintColorOrange,
|
||||||
|
|
||||||
|
// 卡片
|
||||||
|
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: {
|
dark: {
|
||||||
// 文本颜色
|
// 文本颜色
|
||||||
text: '#FFFFFF',
|
text: '#FFFFFF',
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 导出颜色配置
|
// 导出颜色配置
|
||||||
export { default as Colors } from '@/constants/Colors';
|
export { default as Colors } from './Colors';
|
||||||
|
|
||||||
// 导出主题 Hooks
|
// 导出主题 Hooks
|
||||||
export {
|
export {
|
||||||
@@ -23,6 +23,9 @@ export {
|
|||||||
View as ThemeView,
|
View as ThemeView,
|
||||||
} from '@/components/Themed';
|
} from '@/components/Themed';
|
||||||
|
|
||||||
|
// 导出主题类型
|
||||||
|
export { ThemeEnum } from '@/constants/theme';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ThemedTextProps,
|
ThemedTextProps,
|
||||||
ThemedViewProps,
|
ThemedViewProps,
|
||||||
|
|||||||
@@ -7,14 +7,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { StyleSheet, TextStyle, ViewStyle } from 'react-native';
|
import { StyleSheet, TextStyle, ViewStyle } from 'react-native';
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
|
import { ThemeEnum } from '@/constants/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题样式类型
|
* 主题样式类型
|
||||||
*/
|
*/
|
||||||
export type ThemeStyles = {
|
export type ThemeStyles = {
|
||||||
light: any;
|
[ThemeEnum.LIGHT]: any;
|
||||||
dark: any;
|
[ThemeEnum.DARK]: any;
|
||||||
|
[ThemeEnum.ORANGE]: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,11 +48,12 @@ export type ThemeStyles = {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
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 {
|
): ThemeStyles {
|
||||||
return {
|
return {
|
||||||
light: StyleSheet.create(createStyles(Colors.light)),
|
light: StyleSheet.create(createStyles(Colors.light)),
|
||||||
dark: StyleSheet.create(createStyles(Colors.dark)),
|
dark: StyleSheet.create(createStyles(Colors.dark)),
|
||||||
|
orange: StyleSheet.create(createStyles(Colors.orange)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
|||||||
*
|
*
|
||||||
* @param lightStyles - 浅色主题样式
|
* @param lightStyles - 浅色主题样式
|
||||||
* @param darkStyles - 深色主题样式
|
* @param darkStyles - 深色主题样式
|
||||||
|
* @param orangeStyles - 橙色主题样式
|
||||||
* @returns 主题样式对象
|
* @returns 主题样式对象
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -79,11 +83,13 @@ export function createThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
|||||||
*/
|
*/
|
||||||
export function createResponsiveThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
export function createResponsiveThemeStyles<T extends StyleSheet.NamedStyles<T>>(
|
||||||
lightStyles: T,
|
lightStyles: T,
|
||||||
darkStyles: T
|
darkStyles: T,
|
||||||
|
orangeStyles: T
|
||||||
): ThemeStyles {
|
): ThemeStyles {
|
||||||
return {
|
return {
|
||||||
light: StyleSheet.create(lightStyles),
|
light: StyleSheet.create(lightStyles),
|
||||||
dark: StyleSheet.create(darkStyles),
|
dark: StyleSheet.create(darkStyles),
|
||||||
|
orange: StyleSheet.create(orangeStyles),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +261,7 @@ export const commonStyles = createThemeStyles((colors) => ({
|
|||||||
* <View style={style.container} />
|
* <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];
|
return styles[theme];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,19 @@
|
|||||||
* 提供主题相关的辅助函数
|
* 提供主题相关的辅助函数
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Colors from '@/constants/Colors';
|
import { Colors } from '@/theme';
|
||||||
|
import { ThemeEnum } from '@/constants/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据主题获取颜色
|
* 根据主题获取颜色
|
||||||
*
|
*
|
||||||
* @param theme - 主题类型 'light' | 'dark'
|
* @param theme - 主题类型 ThemeEnum
|
||||||
* @param colorName - 颜色名称
|
* @param colorName - 颜色名称
|
||||||
* @returns 颜色值
|
* @returns 颜色值
|
||||||
*/
|
*/
|
||||||
export function getThemeColor(
|
export function getThemeColor(
|
||||||
theme: 'light' | 'dark',
|
theme: ThemeEnum,
|
||||||
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
|
colorName: keyof typeof Colors.light & keyof typeof Colors.dark & keyof typeof Colors.orange
|
||||||
): string {
|
): string {
|
||||||
return Colors[theme][colorName];
|
return Colors[theme][colorName];
|
||||||
}
|
}
|
||||||
@@ -23,10 +24,10 @@ export function getThemeColor(
|
|||||||
/**
|
/**
|
||||||
* 根据主题获取所有颜色
|
* 根据主题获取所有颜色
|
||||||
*
|
*
|
||||||
* @param theme - 主题类型 'light' | 'dark'
|
* @param theme - 主题类型 ThemeEnum
|
||||||
* @returns 颜色对象
|
* @returns 颜色对象
|
||||||
*/
|
*/
|
||||||
export function getThemeColors(theme: 'light' | 'dark') {
|
export function getThemeColors(theme: ThemeEnum) {
|
||||||
return Colors[theme];
|
return Colors[theme];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ export function getThemeColors(theme: 'light' | 'dark') {
|
|||||||
*
|
*
|
||||||
* @param lightStyle - 浅色主题样式
|
* @param lightStyle - 浅色主题样式
|
||||||
* @param darkStyle - 深色主题样式
|
* @param darkStyle - 深色主题样式
|
||||||
|
* @param orangeStyle - 橙色主题样式
|
||||||
* @param theme - 当前主题
|
* @param theme - 当前主题
|
||||||
* @returns 合并后的样式
|
* @returns 合并后的样式
|
||||||
*
|
*
|
||||||
@@ -50,9 +52,19 @@ export function getThemeColors(theme: 'light' | 'dark') {
|
|||||||
export function createThemedStyle<T>(
|
export function createThemedStyle<T>(
|
||||||
lightStyle: T,
|
lightStyle: T,
|
||||||
darkStyle: T,
|
darkStyle: T,
|
||||||
theme: 'light' | 'dark'
|
orangeStyle: T,
|
||||||
|
theme: ThemeEnum
|
||||||
): T {
|
): 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,6 +72,7 @@ export function createThemedStyle<T>(
|
|||||||
*
|
*
|
||||||
* @param lightValue - 浅色主题值
|
* @param lightValue - 浅色主题值
|
||||||
* @param darkValue - 深色主题值
|
* @param darkValue - 深色主题值
|
||||||
|
* @param orangeValue - 深色主题值
|
||||||
* @param theme - 当前主题
|
* @param theme - 当前主题
|
||||||
* @returns 选中的值
|
* @returns 选中的值
|
||||||
*
|
*
|
||||||
@@ -71,9 +84,19 @@ export function createThemedStyle<T>(
|
|||||||
export function selectByTheme<T>(
|
export function selectByTheme<T>(
|
||||||
lightValue: T,
|
lightValue: T,
|
||||||
darkValue: T,
|
darkValue: T,
|
||||||
theme: 'light' | 'dark'
|
orangeValue: T,
|
||||||
|
theme: ThemeEnum
|
||||||
): T {
|
): 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,8 +129,8 @@ export function withOpacity(color: string, opacity: number): string {
|
|||||||
* @param theme - 主题类型
|
* @param theme - 主题类型
|
||||||
* @returns 是否为深色主题
|
* @returns 是否为深色主题
|
||||||
*/
|
*/
|
||||||
export function isDarkTheme(theme: 'light' | 'dark'): boolean {
|
export function isDarkTheme(theme: ThemeEnum): boolean {
|
||||||
return theme === 'dark';
|
return theme === ThemeEnum.DARK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,7 +139,17 @@ export function isDarkTheme(theme: 'light' | 'dark'): boolean {
|
|||||||
* @param theme - 主题类型
|
* @param theme - 主题类型
|
||||||
* @returns 是否为浅色主题
|
* @returns 是否为浅色主题
|
||||||
*/
|
*/
|
||||||
export function isLightTheme(theme: 'light' | 'dark'): boolean {
|
export function isLightTheme(theme: ThemeEnum): boolean {
|
||||||
return theme === 'light';
|
return theme === ThemeEnum.LIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否为橙色主题
|
||||||
|
*
|
||||||
|
* @param theme - 主题类型
|
||||||
|
* @returns 是否为橙色主题
|
||||||
|
*/
|
||||||
|
export function isOrangeTheme(theme: ThemeEnum): boolean {
|
||||||
|
return theme === ThemeEnum.ORANGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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';
|
export type { ApiResponse, ApiError, RequestConfig } from './network/api';
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
export { default as Storage, STORAGE_KEYS } from './storage';
|
export { default as storageManager, STORAGE_KEYS } from './storageManager';
|
||||||
export { default as SessionStorage, SESSION_KEYS } from './sessionStorage';
|
|
||||||
export { default as StorageManager } from './storageManager';
|
|
||||||
export type { StorageType, StorageOptions } from './storageManager';
|
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
export { default as config, printConfig } from './config';
|
export { default as config, printConfig } from './config';
|
||||||
@@ -26,13 +23,7 @@ export {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatRelativeTime,
|
formatRelativeTime,
|
||||||
formatChatTime,
|
formatChatTime,
|
||||||
parseDate,
|
|
||||||
isToday,
|
isToday,
|
||||||
isYesterday,
|
isYesterday,
|
||||||
isSameDay,
|
|
||||||
addDays,
|
|
||||||
subtractDays,
|
|
||||||
startOfDay,
|
|
||||||
endOfDay,
|
|
||||||
} from './date';
|
} from './date';
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { transformRequest, parseResponse } from './helper';
|
import { transformRequest, parseResponse } from './helper';
|
||||||
import { cloneDeep, pick } from 'lodash-es';
|
import { cloneDeep, pick, includes } from 'lodash-es';
|
||||||
import md5 from 'md5';
|
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) {
|
if (error.response) {
|
||||||
const { status, data } = error.response;
|
const { status, data } = error.response;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HmacMD5 } from 'crypto-js';
|
import { HmacMD5 } from 'crypto-js';
|
||||||
import Base64 from 'crypto-js/enc-base64';
|
import Base64 from 'crypto-js/enc-base64';
|
||||||
import Latin1 from 'crypto-js/enc-latin1';
|
import Latin1 from 'crypto-js/enc-latin1';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import md5 from 'md5';
|
import md5 from 'md5';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import * as des from './des';
|
import * as des from './des';
|
||||||
@@ -8,11 +9,11 @@ import NetworkError from './error';
|
|||||||
import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es';
|
import { toNumber, toString, startsWith, isString, isNumber } from 'lodash-es';
|
||||||
import { NetworkTypeEnum } from '@/constants/network';
|
import { NetworkTypeEnum } from '@/constants/network';
|
||||||
import appConfig from '../config';
|
import appConfig from '../config';
|
||||||
|
// import storageManager, { STORAGE_KEYS } from '../storageManager';
|
||||||
|
|
||||||
// import NetworkError from './error'
|
// import NetworkError from './error'
|
||||||
// import { storeToRefs, useTenantStore, useUserStore, useAppStore, start } from '../index';
|
|
||||||
// import { isMobile, getBetPlatform } from '@star/utils';
|
type PlatformType = 'IOS' | 'ANDROID' | 'H5_IOS';
|
||||||
// import { langToNum } from '@star/languages';
|
|
||||||
|
|
||||||
// 请求到的数据返回
|
// 请求到的数据返回
|
||||||
export type NetworkResponse<T> = {
|
export type NetworkResponse<T> = {
|
||||||
@@ -20,38 +21,16 @@ export type NetworkResponse<T> = {
|
|||||||
data: 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=马甲包
|
// 5=PC; 7=HOMESCREEN_IOS; 6=HOMESCREEN_ANDROID; 4=H5_IOS 3=IOS 2=H5_ANDROID; 1=ANDROID 8=马甲包
|
||||||
|
switch (Platform.OS) {
|
||||||
|
case 'ios':
|
||||||
|
return 'IOS';
|
||||||
|
case 'android':
|
||||||
|
return 'ANDROID';
|
||||||
|
default:
|
||||||
return 'H5_IOS';
|
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';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const uuid = (len: number, radix: number) => {
|
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
|
// url code
|
||||||
if (type === 0) {
|
if (type === 0) {
|
||||||
const arr: any[] = [];
|
const arr: any[] = [];
|
||||||
@@ -107,44 +86,44 @@ export const formatSendData = (data: any, type: number = 0) => {
|
|||||||
return JSON.stringify(data);
|
return JSON.stringify(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getP = (p: any) => {
|
const getP = (p: any) => {
|
||||||
return HmacMD5(p, '7NEkojNzfkk=').toString();
|
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);
|
const enc = des.des(rk, str, 1, 0, null, 1);
|
||||||
return Base64.stringify(Latin1.parse(enc));
|
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 s = Latin1.stringify(Base64.parse(str));
|
||||||
const d = des.des(rk, s, 0, 0, null, 1);
|
const d = des.des(rk, s, 0, 0, null, 1);
|
||||||
return d;
|
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);
|
const enc = des.des(vk, rk + t, 1, 0, null, 1);
|
||||||
return Base64.stringify(Latin1.parse(enc));
|
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 s = Latin1.stringify(Base64.parse(str));
|
||||||
const p = des.des(vk, s, 0, 0, null, 1);
|
const p = des.des(vk, s, 0, 0, null, 1);
|
||||||
return p;
|
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);
|
const enc = HmacMD5(m + rk, vk);
|
||||||
return Base64.stringify(enc);
|
return Base64.stringify(enc);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRequestKey = (cmdId: number, data: any) => {
|
const getRequestKey = (cmdId: number, data: any) => {
|
||||||
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`;
|
return `${cmdId}&${data ? md5(JSON.stringify(data)) : ''}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加工请求数据
|
// 加工请求数据
|
||||||
export const transformRequest = (config: any) => {
|
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 headers: Record<string, any> = {};
|
||||||
// const { tenantInfo } = storeToRefs(useTenantStore());
|
// const { tenantInfo } = storeToRefs(useTenantStore());
|
||||||
// const { userInfo } = storeToRefs(useUserStore());
|
// const { userInfo } = storeToRefs(useUserStore());
|
||||||
@@ -153,10 +132,8 @@ export const transformRequest = (config: any) => {
|
|||||||
const rk = md5(toString(Math.random() + t)).substring(0, 8);
|
const rk = md5(toString(Math.random() + t)).substring(0, 8);
|
||||||
const vk = appConfig.app.vk as string;
|
const vk = appConfig.app.vk as string;
|
||||||
const pwds = enP(rk, vk, t);
|
const pwds = enP(rk, vk, t);
|
||||||
|
// const tid = cmdId !== '371130' ? storageManager.session.getItem(STORAGE_KEYS.TENANT_TID) : '';
|
||||||
|
|
||||||
const tenantInfo = {
|
|
||||||
tid: 3,
|
|
||||||
};
|
|
||||||
let userInfo = {
|
let userInfo = {
|
||||||
cust_id: '',
|
cust_id: '',
|
||||||
cust_name: '',
|
cust_name: '',
|
||||||
@@ -221,7 +198,7 @@ export const transformRequest = (config: any) => {
|
|||||||
headers.cmdId = cmdId;
|
headers.cmdId = cmdId;
|
||||||
headers.aseqId = appConfig.app.aseqId;
|
headers.aseqId = appConfig.app.aseqId;
|
||||||
headers.nc = appConfig.app.nc;
|
headers.nc = appConfig.app.nc;
|
||||||
headers.tid = tid ?? tenantInfo.tid ?? '';
|
headers.tid = 3;
|
||||||
// 试玩游戏cust_id=0 header需要保持一致
|
// 试玩游戏cust_id=0 header需要保持一致
|
||||||
headers.custId = (config.data?.cust_id === 0 ? '0' : '') || userInfo?.cust_id || '';
|
headers.custId = (config.data?.cust_id === 0 ? '0' : '') || userInfo?.cust_id || '';
|
||||||
headers.reqId = uuid(32, 16);
|
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
@@ -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;
|
|
||||||
@@ -4,239 +4,304 @@
|
|||||||
* 提供统一的接口来使用 localStorage (AsyncStorage) 或 sessionStorage
|
* 提供统一的接口来使用 localStorage (AsyncStorage) 或 sessionStorage
|
||||||
*
|
*
|
||||||
* 使用场景:
|
* 使用场景:
|
||||||
* - localStorage: 持久化数据,应用重启后仍然存在
|
* - local: 持久化数据,应用重启后仍然存在
|
||||||
* - sessionStorage: 临时数据,应用重启后丢失
|
* - session: 临时数据,应用重启后丢失
|
||||||
*
|
*
|
||||||
* 示例:
|
* 示例:
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // 使用 localStorage(默认)
|
* // 使用 localStorage
|
||||||
* await StorageManager.set('user', userData);
|
* await storageManager.local.setItem('user', userData);
|
||||||
|
* const user = await storageManager.local.getItem('user');
|
||||||
*
|
*
|
||||||
* // 使用 sessionStorage
|
* // 使用 sessionStorage
|
||||||
* await StorageManager.set('temp', tempData, { type: 'session' });
|
* storageManager.session.setItem('temp', tempData);
|
||||||
*
|
* const temp = storageManager.session.getItem('temp');
|
||||||
* // 获取数据(自动从正确的存储中读取)
|
|
||||||
* const user = await StorageManager.get('user');
|
|
||||||
* const temp = await StorageManager.get('temp', { type: 'session' });
|
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Storage from './storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import SessionStorage from './sessionStorage';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 存储类型
|
* 存储键名常量
|
||||||
*/
|
*/
|
||||||
export type StorageType = 'local' | 'session';
|
export enum STORAGE_KEYS {
|
||||||
|
AUTH_TOKEN = 'auth_token',
|
||||||
|
THEME = 'theme',
|
||||||
|
LANGUAGE = 'language',
|
||||||
|
USER_PREFERENCES = 'user_preferences',
|
||||||
|
TENANT_STORE = 'tenant_storage',
|
||||||
|
|
||||||
/**
|
USER_STORE = 'user_storage',
|
||||||
* 存储选项
|
USER_INFO = 'user_info',
|
||||||
*/
|
|
||||||
export interface StorageOptions {
|
// 游戏相关
|
||||||
/**
|
GAME_STORE = 'game_storage',
|
||||||
* 存储类型
|
GAME_TRY = 'game_try',
|
||||||
* - 'local': 持久化存储(AsyncStorage)
|
|
||||||
* - 'session': 会话存储(内存)
|
SETTINGS_STORE = 'settings_storage',
|
||||||
*/
|
MSG_STORE = 'msg_storage',
|
||||||
type?: StorageType;
|
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 {
|
enum DataType {
|
||||||
/**
|
STRING = 'string',
|
||||||
* 存储字符串
|
NUMBER = 'number',
|
||||||
*/
|
BOOLEAN = 'boolean',
|
||||||
static async setString(
|
OBJECT = 'object',
|
||||||
key: string,
|
ARRAY = 'array',
|
||||||
value: string,
|
NULL = 'null',
|
||||||
options: StorageOptions = {}
|
}
|
||||||
): Promise<void> {
|
|
||||||
const { type = 'local' } = options;
|
|
||||||
|
|
||||||
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(
|
static async getItem(key: string): Promise<any> {
|
||||||
key: string,
|
try {
|
||||||
options: StorageOptions = {}
|
const value = await AsyncStorage.getItem(key);
|
||||||
): Promise<string | null> {
|
if (value === null) {
|
||||||
const { type = 'local' } = options;
|
if (__DEV__) {
|
||||||
|
console.log(`📖 LocalStorage get: ${key} ✗`);
|
||||||
if (type === 'session') {
|
|
||||||
return SessionStorage.getString(key);
|
|
||||||
} else {
|
|
||||||
return await Storage.getString(key);
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
const result = this.deserializeValue(value);
|
||||||
/**
|
if (__DEV__) {
|
||||||
* 存储对象
|
console.log(`📖 LocalStorage get: ${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);
|
|
||||||
}
|
}
|
||||||
}
|
return result;
|
||||||
|
} catch (error) {
|
||||||
/**
|
console.error(`LocalStorage getItem error for key "${key}":`, error);
|
||||||
* 获取对象
|
return null;
|
||||||
*/
|
|
||||||
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 remove(key: string, options: StorageOptions = {}): Promise<void> {
|
static async removeItem(key: string): Promise<void> {
|
||||||
const { type = 'local' } = options;
|
try {
|
||||||
|
await AsyncStorage.removeItem(key);
|
||||||
if (type === 'session') {
|
|
||||||
SessionStorage.remove(key);
|
|
||||||
} else {
|
|
||||||
await Storage.remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空指定类型的所有存储
|
|
||||||
*/
|
|
||||||
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__) {
|
if (__DEV__) {
|
||||||
console.log('🗑️ All storage cleared (local + session)');
|
console.log(`🗑️ LocalStorage remove: ${key}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`LocalStorage removeItem error for key "${key}":`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有存储
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
|
||||||
|
|||||||