|
|
|
|
# 💡 使用示例
|
|
|
|
|
|
|
|
|
|
本文档提供项目中各个工具和模块的实际使用示例。
|
|
|
|
|
|
|
|
|
|
## 📋 目录
|
|
|
|
|
|
|
|
|
|
- [登录功能示例](#登录功能示例)
|
|
|
|
|
- [用户资料更新示例](#用户资料更新示例)
|
|
|
|
|
- [搜索功能示例](#搜索功能示例)
|
|
|
|
|
- [设置页面示例](#设置页面示例)
|
|
|
|
|
- [列表加载示例](#列表加载示例)
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 登录功能示例
|
|
|
|
|
|
|
|
|
|
完整的登录页面实现,包含表单验证、API 调用、状态管理。
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { View, TextInput, TouchableOpacity, Text, Alert } from 'react-native';
|
|
|
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
import { useRouter } from 'expo-router';
|
|
|
|
|
import {
|
|
|
|
|
loginSchema,
|
|
|
|
|
type LoginFormData,
|
|
|
|
|
authService,
|
|
|
|
|
useUserStore,
|
|
|
|
|
useHaptics,
|
|
|
|
|
} from '@/src';
|
|
|
|
|
|
|
|
|
|
export default function LoginScreen() {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const haptics = useHaptics();
|
|
|
|
|
const login = useUserStore((state) => state.login);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 表单配置
|
|
|
|
|
const {
|
|
|
|
|
control,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
formState: { errors },
|
|
|
|
|
} = useForm<LoginFormData>({
|
|
|
|
|
resolver: zodResolver(loginSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
email: '',
|
|
|
|
|
password: '',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 提交处理
|
|
|
|
|
const onSubmit = async (data: LoginFormData) => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
// 调用登录 API
|
|
|
|
|
const { user, token } = await authService.login(data);
|
|
|
|
|
|
|
|
|
|
// 更新状态
|
|
|
|
|
login(user, token);
|
|
|
|
|
|
|
|
|
|
// 触觉反馈
|
|
|
|
|
haptics.success();
|
|
|
|
|
|
|
|
|
|
// 跳转到首页
|
|
|
|
|
router.replace('/(tabs)');
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
haptics.error();
|
|
|
|
|
Alert.alert('登录失败', error.message || '请检查邮箱和密码');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<View style={{ padding: 20 }}>
|
|
|
|
|
{/* 邮箱输入 */}
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name="email"
|
|
|
|
|
render={({ field: { onChange, value } }) => (
|
|
|
|
|
<View>
|
|
|
|
|
<TextInput
|
|
|
|
|
value={value}
|
|
|
|
|
onChangeText={onChange}
|
|
|
|
|
placeholder="邮箱"
|
|
|
|
|
keyboardType="email-address"
|
|
|
|
|
autoCapitalize="none"
|
|
|
|
|
style={{
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderColor: errors.email ? 'red' : '#ccc',
|
|
|
|
|
padding: 10,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{errors.email && (
|
|
|
|
|
<Text style={{ color: 'red', marginTop: 5 }}>
|
|
|
|
|
{errors.email.message}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 密码输入 */}
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name="password"
|
|
|
|
|
render={({ field: { onChange, value } }) => (
|
|
|
|
|
<View style={{ marginTop: 15 }}>
|
|
|
|
|
<TextInput
|
|
|
|
|
value={value}
|
|
|
|
|
onChangeText={onChange}
|
|
|
|
|
placeholder="密码"
|
|
|
|
|
secureTextEntry
|
|
|
|
|
style={{
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderColor: errors.password ? 'red' : '#ccc',
|
|
|
|
|
padding: 10,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
{errors.password && (
|
|
|
|
|
<Text style={{ color: 'red', marginTop: 5 }}>
|
|
|
|
|
{errors.password.message}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* 登录按钮 */}
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
onPress={handleSubmit(onSubmit)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: loading ? '#ccc' : '#007AFF',
|
|
|
|
|
padding: 15,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
marginTop: 20,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text style={{ color: 'white', textAlign: 'center', fontWeight: 'bold' }}>
|
|
|
|
|
{loading ? '登录中...' : '登录'}
|
|
|
|
|
</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 用户资料更新示例
|
|
|
|
|
|
|
|
|
|
使用表单验证和 API 服务更新用户资料。
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { View, TextInput, TouchableOpacity, Text, Alert } from 'react-native';
|
|
|
|
|
import { useForm, Controller } from 'react-hook-form';
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
import {
|
|
|
|
|
updateProfileSchema,
|
|
|
|
|
type UpdateProfileFormData,
|
|
|
|
|
userService,
|
|
|
|
|
useUserStore,
|
|
|
|
|
useHaptics,
|
|
|
|
|
} from '@/src';
|
|
|
|
|
|
|
|
|
|
export default function EditProfileScreen() {
|
|
|
|
|
const haptics = useHaptics();
|
|
|
|
|
const user = useUserStore((state) => state.user);
|
|
|
|
|
const updateUser = useUserStore((state) => state.updateUser);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
control,
|
|
|
|
|
handleSubmit,
|
|
|
|
|
formState: { errors },
|
|
|
|
|
} = useForm<UpdateProfileFormData>({
|
|
|
|
|
resolver: zodResolver(updateProfileSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
nickname: user?.nickname || '',
|
|
|
|
|
phone: user?.phone || '',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: UpdateProfileFormData) => {
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
|
|
// 调用更新 API
|
|
|
|
|
const updatedUser = await userService.updateProfile(data);
|
|
|
|
|
|
|
|
|
|
// 更新本地状态
|
|
|
|
|
updateUser(updatedUser);
|
|
|
|
|
|
|
|
|
|
haptics.success();
|
|
|
|
|
Alert.alert('成功', '资料更新成功');
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
haptics.error();
|
|
|
|
|
Alert.alert('失败', error.message || '更新失败');
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<View style={{ padding: 20 }}>
|
|
|
|
|
<Controller
|
|
|
|
|
control={control}
|
|
|
|
|
name="nickname"
|
|
|
|
|
render={({ field: { onChange, value } }) => (
|
|
|
|
|
<View>
|
|
|
|
|
<Text>昵称</Text>
|
|
|
|
|
<TextInput
|
|
|
|
|
value={value}
|
|
|
|
|
onChangeText={onChange}
|
|
|
|
|
placeholder="请输入昵称"
|
|
|
|
|
style={{ borderWidth: 1, padding: 10, marginTop: 5 }}
|
|
|
|
|
/>
|
|
|
|
|
{errors.nickname && (
|
|
|
|
|
<Text style={{ color: 'red' }}>{errors.nickname.message}</Text>
|
|
|
|
|
)}
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
onPress={handleSubmit(onSubmit)}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: '#007AFF',
|
|
|
|
|
padding: 15,
|
|
|
|
|
marginTop: 20,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text style={{ color: 'white', textAlign: 'center' }}>
|
|
|
|
|
{loading ? '保存中...' : '保存'}
|
|
|
|
|
</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 搜索功能示例
|
|
|
|
|
|
|
|
|
|
使用防抖优化搜索性能。
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
|
|
|
import { View, TextInput, FlatList, Text } from 'react-native';
|
|
|
|
|
import { useDebounce } from '@/src';
|
|
|
|
|
|
|
|
|
|
export default function SearchScreen() {
|
|
|
|
|
const [searchText, setSearchText] = useState('');
|
|
|
|
|
const [results, setResults] = useState<any[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
|
|
|
|
|
// 防抖搜索函数
|
|
|
|
|
const debouncedSearch = useDebounce(async (text: string) => {
|
|
|
|
|
if (!text.trim()) {
|
|
|
|
|
setResults([]);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
// 调用搜索 API
|
|
|
|
|
const response = await fetch(`/api/search?q=${text}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
setResults(data.results);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Search error:', error);
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
|
|
|
// 监听搜索文本变化
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
debouncedSearch(searchText);
|
|
|
|
|
}, [searchText]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<View style={{ flex: 1, padding: 20 }}>
|
|
|
|
|
<TextInput
|
|
|
|
|
value={searchText}
|
|
|
|
|
onChangeText={setSearchText}
|
|
|
|
|
placeholder="搜索..."
|
|
|
|
|
style={{
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
borderColor: '#ccc',
|
|
|
|
|
padding: 10,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{loading && <Text style={{ marginTop: 10 }}>搜索中...</Text>}
|
|
|
|
|
|
|
|
|
|
<FlatList
|
|
|
|
|
data={results}
|
|
|
|
|
keyExtractor={(item) => item.id}
|
|
|
|
|
renderItem={({ item }) => (
|
|
|
|
|
<View style={{ padding: 10, borderBottomWidth: 1 }}>
|
|
|
|
|
<Text>{item.title}</Text>
|
|
|
|
|
</View>
|
|
|
|
|
)}
|
|
|
|
|
style={{ marginTop: 20 }}
|
|
|
|
|
/>
|
|
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 设置页面示例
|
|
|
|
|
|
|
|
|
|
使用状态管理和触觉反馈。
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
import React from 'react';
|
|
|
|
|
import { View, Text, Switch, TouchableOpacity } from 'react-native';
|
|
|
|
|
import {
|
|
|
|
|
useSettingsStore,
|
|
|
|
|
useHaptics,
|
|
|
|
|
type Theme,
|
|
|
|
|
} from '@/src';
|
|
|
|
|
|
|
|
|
|
export default function SettingsScreen() {
|
|
|
|
|
const haptics = useHaptics();
|
|
|
|
|
const theme = useSettingsStore((state) => state.theme);
|
|
|
|
|
const notificationsEnabled = useSettingsStore((state) => state.notificationsEnabled);
|
|
|
|
|
const hapticsEnabled = useSettingsStore((state) => state.hapticsEnabled);
|
|
|
|
|
|
|
|
|
|
const setTheme = useSettingsStore((state) => state.setTheme);
|
|
|
|
|
const setNotificationsEnabled = useSettingsStore((state) => state.setNotificationsEnabled);
|
|
|
|
|
const setHapticsEnabled = useSettingsStore((state) => state.setHapticsEnabled);
|
|
|
|
|
|
|
|
|
|
const handleThemeChange = (newTheme: Theme) => {
|
|
|
|
|
haptics.selection();
|
|
|
|
|
setTheme(newTheme);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleToggle = (setter: (value: boolean) => void, value: boolean) => {
|
|
|
|
|
haptics.light();
|
|
|
|
|
setter(value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<View style={{ flex: 1, padding: 20 }}>
|
|
|
|
|
{/* 主题选择 */}
|
|
|
|
|
<View style={{ marginBottom: 20 }}>
|
|
|
|
|
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 10 }}>
|
|
|
|
|
主题
|
|
|
|
|
</Text>
|
|
|
|
|
{(['light', 'dark', 'auto'] as Theme[]).map((t) => (
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
key={t}
|
|
|
|
|
onPress={() => handleThemeChange(t)}
|
|
|
|
|
style={{
|
|
|
|
|
padding: 15,
|
|
|
|
|
backgroundColor: theme === t ? '#007AFF' : '#f0f0f0',
|
|
|
|
|
marginBottom: 10,
|
|
|
|
|
borderRadius: 5,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text style={{ color: theme === t ? 'white' : 'black' }}>
|
|
|
|
|
{t === 'light' ? '亮色' : t === 'dark' ? '暗色' : '自动'}
|
|
|
|
|
</Text>
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
))}
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
{/* 通知开关 */}
|
|
|
|
|
<View
|
|
|
|
|
style={{
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
marginBottom: 15,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text>通知</Text>
|
|
|
|
|
<Switch
|
|
|
|
|
value={notificationsEnabled}
|
|
|
|
|
onValueChange={(value) => handleToggle(setNotificationsEnabled, value)}
|
|
|
|
|
/>
|
|
|
|
|
</View>
|
|
|
|
|
|
|
|
|
|
{/* 触觉反馈开关 */}
|
|
|
|
|
<View
|
|
|
|
|
style={{
|
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Text>触觉反馈</Text>
|
|
|
|
|
<Switch
|
|
|
|
|
value={hapticsEnabled}
|
|
|
|
|
onValueChange={(value) => handleToggle(setHapticsEnabled, value)}
|
|
|
|
|
/>
|
|
|
|
|
</View>
|
|
|
|
|
</View>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 📚 更多示例
|
|
|
|
|
|
|
|
|
|
查看以下文档了解更多:
|
|
|
|
|
|
|
|
|
|
- [工具库使用指南](./LIBRARIES.md) - 各个工具库的详细用法
|
|
|
|
|
- [项目结构说明](./PROJECT_STRUCTURE.md) - 项目结构和最佳实践
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
**提示**:这些示例都是可以直接使用的代码,复制到你的项目中即可!
|