feat: new project

This commit is contained in:
2025-11-04 13:27:19 +08:00
commit 7526a9b827
49 changed files with 12520 additions and 0 deletions

66
app/(tabs)/_layout.tsx Normal file
View File

@@ -0,0 +1,66 @@
import React from 'react';
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { Link, Tabs } from 'expo-router';
import { Pressable } from 'react-native';
import Colors from '@/constants/Colors';
import { useColorScheme } from '@/components/useColorScheme';
import { useClientOnlyValue } from '@/components/useClientOnlyValue';
// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/
function TabBarIcon(props: {
name: React.ComponentProps<typeof FontAwesome>['name'];
color: string;
}) {
return <FontAwesome size={28} style={{ marginBottom: -3 }} {...props} />;
}
export default function TabLayout() {
const colorScheme = useColorScheme();
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
// Disable the static render of the header on web
// to prevent a hydration error in React Navigation v6.
headerShown: useClientOnlyValue(false, true),
}}>
<Tabs.Screen
name="index"
options={{
title: 'Tab One',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
headerRight: () => (
<Link href="/modal" asChild>
<Pressable>
{({ pressed }) => (
<FontAwesome
name="info-circle"
size={25}
color={Colors[colorScheme ?? 'light'].text}
style={{ marginRight: 15, opacity: pressed ? 0.5 : 1 }}
/>
)}
</Pressable>
</Link>
),
}}
/>
<Tabs.Screen
name="two"
options={{
title: 'Tab Two',
tabBarIcon: ({ color }) => <TabBarIcon name="code" color={color} />,
}}
/>
<Tabs.Screen
name="demo"
options={{
title: '完整示例',
tabBarIcon: ({ color }) => <TabBarIcon name="rocket" color={color} />,
}}
/>
</Tabs>
);
}

686
app/(tabs)/demo.tsx Normal file
View File

@@ -0,0 +1,686 @@
/**
* 完整示例页面
* 展示所有工具的使用方法
*/
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
View,
Text,
TextInput,
TouchableOpacity,
ScrollView,
Switch,
Alert,
ActivityIndicator,
} from 'react-native';
import { useForm, Controller } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Image } from 'expo-image';
// 导入所有工具
import {
// 工具函数
Storage,
STORAGE_KEYS,
formatDate,
formatRelativeTime,
formatChatTime,
// 状态管理
useUserStore,
useUser,
useIsLoggedIn,
useSettingsStore,
useTheme,
useLanguage,
useHapticsEnabled,
// 验证规则
loginSchema,
type LoginFormData,
// API 服务
authService,
// 自定义 Hooks
useDebounce,
useThrottle,
useHaptics,
} from '@/src';
export default function DemoScreen() {
const haptics = useHaptics();
// 状态管理示例
const user = useUser();
const isLoggedIn = useIsLoggedIn();
const login = useUserStore((state) => state.login);
const logout = useUserStore((state) => state.logout);
// 设置状态
const theme = useTheme();
const language = useLanguage();
const hapticsEnabled = useHapticsEnabled();
const setTheme = useSettingsStore((state) => state.setTheme);
const setLanguage = useSettingsStore((state) => state.setLanguage);
const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
// 本地状态
const [searchText, setSearchText] = useState('');
const [searchResults, setSearchResults] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [counter, setCounter] = useState(0);
const [storageValue, setStorageValue] = useState('');
// 表单配置
const {
control,
handleSubmit,
formState: { errors },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
},
});
// 防抖搜索示例
const debouncedSearch = useDebounce(async (text: string) => {
if (!text.trim()) {
setSearchResults([]);
return;
}
console.log('执行搜索:', text);
// 模拟 API 调用
await new Promise((resolve) => setTimeout(resolve, 500));
setSearchResults([
`结果 1: ${text}`,
`结果 2: ${text}`,
`结果 3: ${text}`,
]);
}, 500);
// 监听搜索文本变化
useEffect(() => {
debouncedSearch(searchText);
}, [searchText]);
// 节流点击示例
const throttledClick = useThrottle(() => {
haptics.light();
setCounter((prev) => prev + 1);
console.log('节流点击:', counter + 1);
}, 1000);
// 登录处理
const onLogin = async (data: LoginFormData) => {
try {
setLoading(true);
haptics.light();
// 模拟登录 API 调用
console.log('登录数据:', data);
// 实际项目中使用:
// const { user, token } = await authService.login(data);
// login(user, token);
// 模拟登录成功
const mockUser = {
id: '1',
username: data.email.split('@')[0],
email: data.email,
avatar: 'https://i.pravatar.cc/150?img=1',
nickname: '演示用户',
createdAt: new Date().toISOString(),
};
login(mockUser, 'mock-token-123456');
haptics.success();
Alert.alert('成功', '登录成功!');
} catch (error: any) {
haptics.error();
Alert.alert('失败', error.message || '登录失败');
} finally {
setLoading(false);
}
};
// 登出处理
const handleLogout = () => {
haptics.warning();
Alert.alert(
'确认',
'确定要退出登录吗?',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: () => {
logout();
haptics.success();
},
},
]
);
};
// 存储示例
const handleSaveToStorage = async () => {
try {
haptics.light();
const testData = {
message: 'Hello from AsyncStorage!',
timestamp: new Date().toISOString(),
counter,
};
await Storage.setObject(STORAGE_KEYS.USER_PREFERENCES, testData);
haptics.success();
Alert.alert('成功', '数据已保存到本地存储');
} catch (error) {
haptics.error();
Alert.alert('失败', '保存失败');
}
};
const handleLoadFromStorage = async () => {
try {
haptics.light();
const data = await Storage.getObject<any>(STORAGE_KEYS.USER_PREFERENCES);
if (data) {
setStorageValue(JSON.stringify(data, null, 2));
haptics.success();
} else {
setStorageValue('暂无数据');
haptics.warning();
}
} catch (error) {
haptics.error();
Alert.alert('失败', '读取失败');
}
};
// 主题切换
const handleThemeChange = () => {
haptics.selection();
const themes: Array<'light' | 'dark' | 'auto'> = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(theme);
const nextTheme = themes[(currentIndex + 1) % themes.length];
setTheme(nextTheme);
};
// 语言切换
const handleLanguageChange = () => {
haptics.selection();
setLanguage(language === 'zh-CN' ? 'en-US' : 'zh-CN');
};
return (
<ScrollView style={styles.container}>
<View style={styles.content}>
{/* 标题 */}
<Text style={styles.title}>🎯 </Text>
<Text style={styles.subtitle}>使</Text>
{/* 用户状态显示 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>👤 (Zustand)</Text>
{isLoggedIn ? (
<View style={styles.userInfo}>
{user?.avatar && (
<Image
source={{ uri: user.avatar }}
style={styles.avatar}
contentFit="cover"
/>
)}
<View style={styles.userDetails}>
<Text style={styles.userName}>{user?.nickname}</Text>
<Text style={styles.userEmail}>{user?.email}</Text>
<Text style={styles.userDate}>
: {formatRelativeTime(user?.createdAt || '')}
</Text>
</View>
<TouchableOpacity
style={[styles.button, styles.logoutButton]}
onPress={handleLogout}
>
<Text style={styles.buttonText}>退</Text>
</TouchableOpacity>
</View>
) : (
<Text style={styles.infoText}></Text>
)}
</View>
{/* 登录表单 */}
{!isLoggedIn && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>🔐 (React Hook Form + Zod)</Text>
<Controller
control={control}
name="email"
render={({ field: { onChange, value } }) => (
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<TextInput
value={value}
onChangeText={onChange}
placeholder="请输入邮箱"
keyboardType="email-address"
autoCapitalize="none"
style={[
styles.input,
errors.email && styles.inputError,
]}
/>
{errors.email && (
<Text style={styles.errorText}>{errors.email.message}</Text>
)}
</View>
)}
/>
<Controller
control={control}
name="password"
render={({ field: { onChange, value } }) => (
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<TextInput
value={value}
onChangeText={onChange}
placeholder="请输入密码"
secureTextEntry
style={[
styles.input,
errors.password && styles.inputError,
]}
/>
{errors.password && (
<Text style={styles.errorText}>{errors.password.message}</Text>
)}
</View>
)}
/>
<TouchableOpacity
style={[styles.button, styles.loginButton]}
onPress={handleSubmit(onLogin)}
disabled={loading}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}></Text>
)}
</TouchableOpacity>
</View>
)}
{/* 搜索示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>🔍 (useDebounce)</Text>
<TextInput
value={searchText}
onChangeText={setSearchText}
placeholder="输入搜索内容..."
style={styles.input}
/>
{searchResults.length > 0 && (
<View style={styles.searchResults}>
{searchResults.map((result, index) => (
<Text key={index} style={styles.searchResult}>
{result}
</Text>
))}
</View>
)}
</View>
{/* 节流点击示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}> (useThrottle)</Text>
<Text style={styles.infoText}>: {counter}</Text>
<TouchableOpacity
style={[styles.button, styles.primaryButton]}
onPress={throttledClick}
>
<Text style={styles.buttonText}>1</Text>
</TouchableOpacity>
</View>
{/* 本地存储示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>💾 (AsyncStorage)</Text>
<View style={styles.buttonRow}>
<TouchableOpacity
style={[styles.button, styles.primaryButton, styles.halfButton]}
onPress={handleSaveToStorage}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.secondaryButton, styles.halfButton]}
onPress={handleLoadFromStorage}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
{storageValue && (
<View style={styles.codeBlock}>
<Text style={styles.codeText}>{storageValue}</Text>
</View>
)}
</View>
{/* 日期格式化示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>📅 (Day.js)</Text>
<Text style={styles.infoText}>
: {formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss')}
</Text>
<Text style={styles.infoText}>
: {formatRelativeTime(new Date())}
</Text>
<Text style={styles.infoText}>
: {formatChatTime(Date.now())}
</Text>
</View>
{/* 设置示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}> </Text>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>: {theme}</Text>
<TouchableOpacity
style={[styles.button, styles.smallButton]}
onPress={handleThemeChange}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}>: {language}</Text>
<TouchableOpacity
style={[styles.button, styles.smallButton]}
onPress={handleLanguageChange}
>
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
</View>
<View style={styles.settingRow}>
<Text style={styles.settingLabel}></Text>
<Switch
value={hapticsEnabled}
onValueChange={(value) => {
haptics.selection();
setHapticsEnabled(value);
}}
/>
</View>
</View>
{/* 触觉反馈示例 */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>📳 (Expo Haptics)</Text>
<View style={styles.buttonGrid}>
<TouchableOpacity
style={[styles.button, styles.hapticsButton]}
onPress={() => haptics.light()}
>
<Text style={styles.buttonText}>Light</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.hapticsButton]}
onPress={() => haptics.medium()}
>
<Text style={styles.buttonText}>Medium</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.hapticsButton]}
onPress={() => haptics.heavy()}
>
<Text style={styles.buttonText}>Heavy</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.successButton]}
onPress={() => haptics.success()}
>
<Text style={styles.buttonText}>Success</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.warningButton]}
onPress={() => haptics.warning()}
>
<Text style={styles.buttonText}>Warning</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.errorButton]}
onPress={() => haptics.error()}
>
<Text style={styles.buttonText}>Error</Text>
</TouchableOpacity>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>
使 📖
</Text>
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
content: {
padding: 16,
},
title: {
fontSize: 28,
fontWeight: 'bold',
marginBottom: 8,
color: '#333',
},
subtitle: {
fontSize: 16,
color: '#666',
marginBottom: 24,
},
section: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
marginBottom: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 12,
color: '#333',
},
userInfo: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
width: 60,
height: 60,
borderRadius: 30,
marginRight: 12,
},
userDetails: {
flex: 1,
},
userName: {
fontSize: 16,
fontWeight: '600',
color: '#333',
},
userEmail: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
userDate: {
fontSize: 12,
color: '#999',
marginTop: 4,
},
inputGroup: {
marginBottom: 16,
},
label: {
fontSize: 14,
fontWeight: '500',
marginBottom: 6,
color: '#333',
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
fontSize: 16,
backgroundColor: '#fff',
},
inputError: {
borderColor: '#ff3b30',
},
errorText: {
color: '#ff3b30',
fontSize: 12,
marginTop: 4,
},
button: {
paddingVertical: 12,
paddingHorizontal: 20,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
loginButton: {
backgroundColor: '#007AFF',
marginTop: 8,
},
logoutButton: {
backgroundColor: '#ff3b30',
paddingVertical: 8,
paddingHorizontal: 16,
},
primaryButton: {
backgroundColor: '#007AFF',
},
secondaryButton: {
backgroundColor: '#5856D6',
},
smallButton: {
backgroundColor: '#007AFF',
paddingVertical: 8,
paddingHorizontal: 16,
},
buttonRow: {
flexDirection: 'row',
gap: 12,
},
halfButton: {
flex: 1,
},
searchResults: {
marginTop: 12,
},
searchResult: {
padding: 12,
backgroundColor: '#f9f9f9',
borderRadius: 8,
marginBottom: 8,
color: '#333',
},
infoText: {
fontSize: 14,
color: '#666',
marginBottom: 8,
},
codeBlock: {
backgroundColor: '#f9f9f9',
borderRadius: 8,
padding: 12,
marginTop: 12,
},
codeText: {
fontFamily: 'monospace',
fontSize: 12,
color: '#333',
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
settingLabel: {
fontSize: 16,
color: '#333',
},
buttonGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
hapticsButton: {
backgroundColor: '#5856D6',
flex: 1,
minWidth: '30%',
},
successButton: {
backgroundColor: '#34C759',
flex: 1,
minWidth: '30%',
},
warningButton: {
backgroundColor: '#FF9500',
flex: 1,
minWidth: '30%',
},
errorButton: {
backgroundColor: '#FF3B30',
flex: 1,
minWidth: '30%',
},
footer: {
marginTop: 24,
marginBottom: 40,
alignItems: 'center',
},
footerText: {
fontSize: 14,
color: '#999',
},
});

192
app/(tabs)/index.tsx Normal file
View File

@@ -0,0 +1,192 @@
import { useState } from 'react';
import { StyleSheet, TouchableOpacity, Alert, ActivityIndicator } from 'react-native';
import * as Updates from 'expo-updates';
import { Text, View } from '@/components/Themed';
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();
};
return (
<View style={styles.container}>
<Text style={styles.title}>🚀 </Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<View style={styles.infoContainer}>
<Text style={styles.infoTitle}></Text>
<Text style={styles.infoText}>{getUpdateInfo()}</Text>
</View>
<TouchableOpacity
style={[styles.button, isChecking && styles.buttonDisabled]}
onPress={checkForUpdates}
disabled={isChecking}
>
{isChecking ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={styles.buttonText}></Text>
)}
</TouchableOpacity>
{updateInfo ? (
<View style={styles.updateInfoContainer}>
<Text style={styles.updateInfoText}>{updateInfo}</Text>
</View>
) : null}
<View style={styles.instructionsContainer}>
<Text style={styles.instructionsTitle}>📝 使</Text>
<Text style={styles.instructionsText}>
1. 使 EAS Build {'\n'}
2. eas update {'\n'}
3. "检查更新"{'\n'}
4.
</Text>
</View>
</View>
);
}
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,
},
});

31
app/(tabs)/two.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function TabTwoScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Tab Two</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/(tabs)/two.tsx" />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});

38
app/+html.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { ScrollViewStyleReset } from 'expo-router/html';
// This file is web-only and used to configure the root HTML for every
// web page during static rendering.
// The contents of this function only run in Node.js environments and
// do not have access to the DOM or browser APIs.
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}
const responsiveBackground = `
body {
background-color: #fff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #000;
}
}`;

40
app/+not-found.tsx Normal file
View File

@@ -0,0 +1,40 @@
import { Link, Stack } from 'expo-router';
import { StyleSheet } from 'react-native';
import { Text, View } from '@/components/Themed';
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
padding: 20,
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
marginTop: 15,
paddingVertical: 15,
},
linkText: {
fontSize: 14,
color: '#2e78b7',
},
});

102
app/_layout.tsx Normal file
View File

@@ -0,0 +1,102 @@
import FontAwesome from '@expo/vector-icons/FontAwesome';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import * as Updates from 'expo-updates';
import { useEffect } from 'react';
import { Alert, Platform } from 'react-native';
import 'react-native-reanimated';
import { useColorScheme } from '@/components/useColorScheme';
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
...FontAwesome.font,
});
// Expo Router uses Error Boundaries to catch errors in the navigation tree.
useEffect(() => {
if (error) throw error;
}, [error]);
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
}
}, [loaded]);
// 检查热更新
useEffect(() => {
async function checkForUpdates() {
if (__DEV__) {
// 开发模式下不检查更新
return;
}
try {
const update = await Updates.checkForUpdateAsync();
if (update.isAvailable) {
await Updates.fetchUpdateAsync();
// 提示用户重启应用以应用更新
Alert.alert(
'更新可用',
'发现新版本,是否立即重启应用?',
[
{
text: '稍后',
style: 'cancel',
},
{
text: '立即重启',
onPress: async () => {
await Updates.reloadAsync();
},
},
]
);
}
} catch (error) {
// 处理更新检查错误
console.error('检查更新失败:', error);
}
}
checkForUpdates();
}, []);
if (!loaded) {
return null;
}
return <RootLayoutNav />;
}
function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
</ThemeProvider>
);
}

35
app/modal.tsx Normal file
View File

@@ -0,0 +1,35 @@
import { StatusBar } from 'expo-status-bar';
import { Platform, StyleSheet } from 'react-native';
import EditScreenInfo from '@/components/EditScreenInfo';
import { Text, View } from '@/components/Themed';
export default function ModalScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
separator: {
marginVertical: 30,
height: 1,
width: '80%',
},
});