364 lines
10 KiB
TypeScript
364 lines
10 KiB
TypeScript
|
|
/**
|
||
|
|
* 登录/注册弹窗组件
|
||
|
|
* 支持登录和注册两种模式
|
||
|
|
*/
|
||
|
|
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|