feat: 首页更新

This commit is contained in:
2025-11-13 16:47:10 +08:00
parent 9ef9233797
commit 54bf84b19b
1244 changed files with 3507 additions and 951 deletions

View File

@@ -0,0 +1,363 @@
/**
* 登录/注册弹窗组件
* 支持登录和注册两种模式
*/
import React, { useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
TextInput,
Modal,
ScrollView,
KeyboardAvoidingView,
Platform,
Alert,
} from 'react-native';
import { useColorScheme, useThemeColors } from '@/theme';
import { createThemeStyles } from '@/theme';
import { useUserStore } from '@/stores';
import Ionicons from '@expo/vector-icons/Ionicons';
/**
* 创建主题样式
*/
const styles = createThemeStyles((colors) => ({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'flex-end',
},
container: {
backgroundColor: colors.background,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 40,
maxHeight: '80%',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: '700',
color: colors.text,
},
closeButton: {
padding: 8,
},
tabContainer: {
flexDirection: 'row',
marginBottom: 20,
borderBottomWidth: 1,
borderBottomColor: colors.borderSecondary,
},
tab: {
flex: 1,
paddingVertical: 12,
alignItems: 'center',
borderBottomWidth: 2,
borderBottomColor: 'transparent',
},
activeTab: {
borderBottomColor: colors.primary,
},
tabText: {
fontSize: 14,
fontWeight: '600',
color: colors.text + '80',
},
activeTabText: {
color: colors.primary,
},
form: {
gap: 12,
},
inputContainer: {
marginBottom: 12,
},
label: {
fontSize: 12,
color: colors.text + '80',
marginBottom: 6,
},
input: {
borderWidth: 1,
borderColor: colors.borderSecondary,
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 14,
color: colors.text,
backgroundColor: colors.card,
},
inputFocused: {
borderColor: colors.primary,
},
errorText: {
fontSize: 12,
color: '#ff4444',
marginTop: 4,
},
submitButton: {
backgroundColor: colors.primary,
borderRadius: 8,
paddingVertical: 12,
alignItems: 'center',
marginTop: 20,
},
submitButtonText: {
color: '#fff',
fontSize: 16,
fontWeight: '700',
},
divider: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 16,
},
dividerLine: {
flex: 1,
height: 1,
backgroundColor: colors.borderSecondary,
},
dividerText: {
marginHorizontal: 12,
fontSize: 12,
color: colors.text + '80',
},
socialButtons: {
flexDirection: 'row',
gap: 12,
},
socialButton: {
flex: 1,
borderWidth: 1,
borderColor: colors.borderSecondary,
borderRadius: 8,
paddingVertical: 10,
alignItems: 'center',
},
agreementText: {
fontSize: 12,
color: colors.text + '80',
marginTop: 12,
lineHeight: 18,
},
agreementLink: {
color: colors.primary,
textDecorationLine: 'underline',
},
}));
interface LoginRegisterModalProps {
visible: boolean;
onClose: () => void;
}
/**
* 登录/注册弹窗组件
*/
export default function LoginRegisterModal({ visible, onClose }: LoginRegisterModalProps) {
const colorScheme = useColorScheme();
const s = styles[colorScheme];
const themeColors = useThemeColors();
const { login } = useUserStore();
const [mode, setMode] = useState<'login' | 'register'>('login');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [errors, setErrors] = useState<Record<string, string>>({});
const [loading, setLoading] = useState(false);
const validateEmail = useCallback((email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}, []);
const validateForm = useCallback(() => {
const newErrors: Record<string, string> = {};
if (!email.trim()) {
newErrors.email = '请输入邮箱';
} else if (!validateEmail(email)) {
newErrors.email = '邮箱格式不正确';
}
if (!password.trim()) {
newErrors.password = '请输入密码';
} else if (password.length < 6) {
newErrors.password = '密码至少6个字符';
}
if (mode === 'register') {
if (!confirmPassword.trim()) {
newErrors.confirmPassword = '请确认密码';
} else if (password !== confirmPassword) {
newErrors.confirmPassword = '两次输入的密码不一致';
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [email, password, confirmPassword, mode, validateEmail]);
const handleSubmit = useCallback(async () => {
if (!validateForm()) {
return;
}
try {
setLoading(true);
// 模拟 API 调用
await new Promise((resolve) => setTimeout(resolve, 1000));
// 模拟登录/注册成功
const mockUser = {
id: '1',
username: email.split('@')[0],
email,
avatar: 'https://i.pravatar.cc/150?img=1',
nickname: '用户',
createdAt: new Date().toISOString(),
};
login(mockUser, 'mock-token-' + Date.now());
Alert.alert('成功', mode === 'login' ? '登录成功!' : '注册成功!');
onClose();
} catch (error: any) {
Alert.alert('失败', error.message || '操作失败,请重试');
} finally {
setLoading(false);
}
}, [validateForm, email, mode, login, onClose]);
const handleSwitchMode = useCallback(() => {
setMode(mode === 'login' ? 'register' : 'login');
setErrors({});
}, [mode]);
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
<View style={s.overlay}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={{ flex: 1 }}
>
<TouchableOpacity style={{ flex: 1 }} activeOpacity={1} onPress={onClose} />
<View style={s.container}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* 头部 */}
<View style={s.header}>
<Text style={s.title}>{mode === 'login' ? '登录' : '注册'}</Text>
<TouchableOpacity style={s.closeButton} onPress={onClose} activeOpacity={0.7}>
<Ionicons name="close" size={24} color={themeColors.text} />
</TouchableOpacity>
</View>
{/* 标签页 */}
<View style={s.tabContainer}>
<TouchableOpacity
style={[s.tab, mode === 'login' && s.activeTab]}
onPress={() => setMode('login')}
activeOpacity={0.7}
>
<Text style={[s.tabText, mode === 'login' && s.activeTabText]}></Text>
</TouchableOpacity>
<TouchableOpacity
style={[s.tab, mode === 'register' && s.activeTab]}
onPress={() => setMode('register')}
activeOpacity={0.7}
>
<Text style={[s.tabText, mode === 'register' && s.activeTabText]}></Text>
</TouchableOpacity>
</View>
{/* 表单 */}
<View style={s.form}>
{/* 邮箱 */}
<View style={s.inputContainer}>
<Text style={s.label}></Text>
<TextInput
style={[s.input, errors.email && { borderColor: '#ff4444' }]}
placeholder="请输入邮箱"
placeholderTextColor={themeColors.text + '60'}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
editable={!loading}
/>
{errors.email && <Text style={s.errorText}>{errors.email}</Text>}
</View>
{/* 密码 */}
<View style={s.inputContainer}>
<Text style={s.label}></Text>
<TextInput
style={[s.input, errors.password && { borderColor: '#ff4444' }]}
placeholder="请输入密码"
placeholderTextColor={themeColors.text + '60'}
value={password}
onChangeText={setPassword}
secureTextEntry
editable={!loading}
/>
{errors.password && <Text style={s.errorText}>{errors.password}</Text>}
</View>
{/* 确认密码(注册模式) */}
{mode === 'register' && (
<View style={s.inputContainer}>
<Text style={s.label}></Text>
<TextInput
style={[s.input, errors.confirmPassword && { borderColor: '#ff4444' }]}
placeholder="请再次输入密码"
placeholderTextColor={themeColors.text + '60'}
value={confirmPassword}
onChangeText={setConfirmPassword}
secureTextEntry
editable={!loading}
/>
{errors.confirmPassword && (
<Text style={s.errorText}>{errors.confirmPassword}</Text>
)}
</View>
)}
</View>
{/* 提交按钮 */}
<TouchableOpacity
style={s.submitButton}
onPress={handleSubmit}
disabled={loading}
activeOpacity={0.7}
>
<Text style={s.submitButtonText}>
{loading ? '处理中...' : mode === 'login' ? '登录' : '注册'}
</Text>
</TouchableOpacity>
{/* 用户协议 */}
{mode === 'register' && (
<Text style={s.agreementText}>
<Text style={s.agreementLink}></Text>
<Text style={s.agreementLink}></Text>
</Text>
)}
</ScrollView>
</View>
</KeyboardAvoidingView>
</View>
</Modal>
);
}