2025-08-08 19:05:43 +08:00

389 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Fonts } from "@/constants/Fonts";
import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from 'react';
import { useTranslation } from "react-i18next";
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { useAuth } from "../../contexts/auth-context";
import { fetchApi } from "../../lib/server-api-util";
import { User } from "../../types/user";
import { ThemedText } from "../ThemedText";
import PrivacyModal from "../owner/qualification/privacy";
import Button from "./ui/Button";
import TextInput from "./ui/TextInput";
interface LoginProps {
updateUrlParam: (status: string, value: string) => void;
setError: (error: string) => void;
setShowPassword: (showPassword: boolean) => void;
showPassword: boolean;
setShowSecondPassword: (showSecondPassword: boolean) => void;
showSecondPassword: boolean;
}
const SignUp = ({ updateUrlParam, setError, setShowPassword, showPassword, setShowSecondPassword, showSecondPassword }: LoginProps) => {
const { t } = useTranslation();
const { login } = useAuth();
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordsMatch, setPasswordsMatch] = useState(true);
const [loading, setLoading] = useState(false);
const [checked, setChecked] = useState(false);
const [modalType, setModalType] = useState<'ai' | 'terms' | 'privacy' | 'user'>('ai');
// 协议弹窗
const [privacyModalVisible, setPrivacyModalVisible] = useState(false);
// 从 URL 参数中获取 task_id 和 steps
const params = useLocalSearchParams<{ task_id?: string; steps?: string }>();
const taskId = params.task_id;
const handlePasswordChange = (value: string) => {
setPassword(value);
// 当密码或确认密码变化时,检查是否匹配
if (confirmPassword && value !== confirmPassword) {
setPasswordsMatch(false);
} else {
setPasswordsMatch(true);
}
};
const handleConfirmPasswordChange = (value: string) => {
setConfirmPassword(value);
// 当密码或确认密码变化时,检查是否匹配
if (password && value !== password) {
setPasswordsMatch(false);
} else {
setPasswordsMatch(true);
}
};
const handleSubmit = async () => {
if (!email) {
setError(t('auth.signup.emailPlaceholder', { ns: 'login' }));
return;
}
if (!password) {
setError(t('auth.signup.passwordPlaceholder', { ns: 'login' }));
return;
}
// 验证两次密码是否一致
if (password !== confirmPassword) {
setPasswordsMatch(false);
setError(t('auth.signup.passwordNotMatch', { ns: 'login' }));
return;
}
if (!checked) {
setError(t('auth.signup.checkedRequired', { ns: 'login' }));
return;
}
if (email) {
// 校验是否符合邮箱规范
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
setError(t('auth.signup.emailAuth', { ns: 'login' }));
return;
}
}
if (password) {
// 校验密码是否符合规范
// const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
// if (!passwordRegex.test(password)) {
// setError(t('auth.signup.passwordAuth', { ns: 'login' }));
// return;
// }
if (password.length < 6) {
setError(t('auth.signup.pwdLengthError', { ns: 'login' }));
return;
}
}
setLoading(true);
try {
const body = {
email: email,
password: password
};
// 这里调用实际的注册API
const response = await fetchApi<User>('/iam/register/email', {
method: 'POST',
body: JSON.stringify(body),
headers: {
'Content-Type': 'application/json'
}
}, true, false);
// 存储token
login(response as User, response.access_token || '');
// 如果有任务ID跳转到上传页面
if (taskId) {
// 使用字符串路径格式传递参数
// router.push(`/upload?steps=${encodeURIComponent(steps || '')}&task_id=${encodeURIComponent(taskId)}`);
} else {
// 注册成功后跳转到首页
router.replace('/user-message');
}
setLoading(false);
} catch (error) {
// console.error('Registration failed:', error);
// 这里可以添加错误处理逻辑
setLoading(false);
}
};
useEffect(() => {
if (!passwordsMatch) {
setError(t('auth.login.passwordNotMatch', { ns: 'login' }));
}
}, [passwordsMatch])
// 初始化
useEffect(() => {
setShowPassword(false)
setShowSecondPassword(false)
}, [])
return (
<View style={styles.container}>
{/* 邮箱输入 */}
<TextInput
label={t('auth.login.email', { ns: 'login' })}
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
onChangeText={(text) => {
setEmail(text);
setError('123');
}}
autoCapitalize="none"
keyboardType="email-address"
value={email}
/>
{/* 密码输入 */}
<TextInput
label={t('auth.login.password', { ns: 'login' })}
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
autoCapitalize="none"
value={password}
onChangeText={(value) => {
handlePasswordChange(value)
setError('123')
}}
secureTextEntry={!showPassword}
type="password"
setShowPassword={setShowPassword}
showPassword={showPassword}
/>
{/* 确认密码 */}
<TextInput
label={t('auth.signup.confirmPassword', { ns: 'login' })}
placeholder={t('auth.signup.confirmPasswordPlaceholder', { ns: 'login' })}
autoCapitalize="none"
value={confirmPassword}
onChangeText={(value) => {
handleConfirmPasswordChange(value)
setError('123')
}}
secureTextEntry={!showSecondPassword}
type="password"
setShowPassword={setShowSecondPassword}
showPassword={showSecondPassword}
/>
{/* 注册按钮 */}
<Button isLoading={loading} handleLogin={handleSubmit} text={t("auth.signup.signupButton", { ns: 'login' })} />
<View style={styles.termsContainer}>
<TouchableOpacity
onPress={() => {
const newValue = !checked;
setChecked(newValue);
if (!newValue) {
setError(t('auth.signup.checkedRequired', { ns: 'login' }));
return
} else {
setError("123")
}
}}
style={[
styles.checkbox,
checked && styles.checkboxChecked
]}
>
{checked && (
<Ionicons name="checkmark" size={14} color={Fonts['textSecondary']} />
)}
</TouchableOpacity>
<View style={styles.termsTextContainer}>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agree", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('terms');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.terms", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('privacy');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.privacyPolicy", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('user');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.userAgreement", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.and", { ns: 'login' })}
</ThemedText>
<TouchableOpacity onPress={() => {
setModalType('ai');
setPrivacyModalVisible(true);
}}>
<ThemedText style={styles.termsLink} type="sfPro">
{t("auth.telLogin.aiAgreement", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.agreement", { ns: 'login' })}
</ThemedText>
<ThemedText style={styles.termsLink} type="sfPro">
{t("common.name")}
</ThemedText>
<ThemedText style={styles.termsText} type="sfPro">
{t("auth.telLogin.getPhone", { ns: 'login' })}
</ThemedText>
</View>
</View>
{/* 已有账号 */}
<View style={styles.loginContainer} >
<ThemedText type="sfPro" color="textPrimary" size="sm">
{t("auth.signup.haveAccount", { ns: 'login' })}
</ThemedText>
<TouchableOpacity
onPress={() => {
updateUrlParam("status", "login");
}}
>
<ThemedText type="sfPro" color="bgSecondary" weight="bold" size="sm">
{t("auth.signup.login", { ns: 'login' })}
</ThemedText>
</TouchableOpacity>
</View>
{/* 协议弹窗 */}
<PrivacyModal modalVisible={privacyModalVisible} setModalVisible={setPrivacyModalVisible} type={modalType} />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
inputContainer: {
marginBottom: 16,
},
inputWrapper: {
borderRadius: 12,
backgroundColor: '#FFF8DE',
overflow: 'hidden',
},
inputLabel: {
fontSize: 14,
color: '#AC7E35',
fontWeight: '600',
marginBottom: 8,
marginLeft: 8,
},
textInput: {
padding: 12,
fontSize: 16,
color: '#1F2937',
},
passwordInputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
backgroundColor: '#FFF8DE',
overflow: 'hidden',
},
eyeIcon: {
paddingHorizontal: 12,
paddingVertical: 8,
},
signupButton: {
width: '100%',
backgroundColor: '#E2793F',
borderRadius: 28,
padding: 16,
alignItems: 'center',
marginBottom: 16,
},
signupButtonText: {
color: '#FFFFFF',
fontWeight: '600',
},
termsContainer: {
flexDirection: 'row',
alignItems: 'flex-start',
marginVertical: 10,
},
checkbox: {
width: 20,
height: 20,
borderRadius: 6,
borderWidth: 1,
borderColor: Fonts['textPrimary'],
justifyContent: 'center',
alignItems: 'center',
marginRight: 8,
marginTop: 2,
},
checkboxChecked: {
backgroundColor: Fonts["bgCheck"],
borderColor: Fonts['bgCheck'],
},
termsTextContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
flex: 1,
},
termsText: {
fontSize: 14,
color: Fonts["textPrimary"],
lineHeight: 20,
},
termsLink: {
fontSize: 14,
color: Fonts['bgSecondary'],
lineHeight: 20
},
loginContainer: {
flexDirection: 'row',
justifyContent: 'center',
marginTop: 24,
gap: 4,
}
});
export default SignUp;