feat: 登录+忘记密码
This commit is contained in:
parent
3d4ee1b210
commit
727ebcb483
4
app.json
4
app.json
@ -50,7 +50,9 @@
|
|||||||
"expo-font",
|
"expo-font",
|
||||||
{
|
{
|
||||||
"fonts": [
|
"fonts": [
|
||||||
"./assets/font/english.otf"
|
"./assets/fonts/Quicksand.otf",
|
||||||
|
"./assets/fonts/SF-Pro.otf",
|
||||||
|
"./assets/fonts/Inter-Regular.otf"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@ -60,7 +60,9 @@ export default function TabLayout() {
|
|||||||
|
|
||||||
// 加载字体
|
// 加载字体
|
||||||
const [loaded, error] = useFonts({
|
const [loaded, error] = useFonts({
|
||||||
english: require('@/assets/font/english.otf'),
|
quicksand: require('@/assets/fonts/Quicksand.otf'),
|
||||||
|
sfPro: require('@/assets/fonts/SF-Pro.otf'),
|
||||||
|
inter: require('@/assets/fonts/Inter-Regular.otf'),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -74,9 +74,9 @@ const LoginScreen = () => {
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
bounces={false}
|
bounces={false}
|
||||||
>
|
>
|
||||||
<ThemedView className="flex-1 bg-bgPrimary justify-end">
|
<ThemedView className="flex-1 justify-end" bgColor="bgPrimary">
|
||||||
<View style={{ width: "100%", alignItems: "center", marginTop: insets.top + 8, opacity: keyboardOffset === 0 ? 1 : 0 }}>
|
<View style={{ width: "100%", alignItems: "center", marginTop: insets.top + 8, opacity: keyboardOffset === 0 ? 1 : 0 }}>
|
||||||
<ThemedText style={{ fontSize: 20, fontWeight: 'bold', color: "#fff" }}>{t('login:auth.login.titleText')}</ThemedText>
|
<ThemedText size="xl" weight="bold" color="textWhite">{t('login:auth.login.titleText')}</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<View
|
<View
|
||||||
@ -105,7 +105,8 @@ const LoginScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<ThemedView
|
<ThemedView
|
||||||
className="w-full bg-white pt-4 px-6 relative z-20 shadow-lg pb-5"
|
className="w-full pt-4 px-6 relative z-20 shadow-lg pb-5"
|
||||||
|
bgColor="textWhite"
|
||||||
style={{
|
style={{
|
||||||
borderTopLeftRadius: 50,
|
borderTopLeftRadius: 50,
|
||||||
borderTopRightRadius: 50,
|
borderTopRightRadius: 50,
|
||||||
@ -120,7 +121,7 @@ const LoginScreen = () => {
|
|||||||
>
|
>
|
||||||
{/* 错误提示 */}
|
{/* 错误提示 */}
|
||||||
<View className={`${error !== "123" ? 'opacity-100' : 'opacity-0'} w-full flex justify-center items-center text-primary-500 text-sm`}>
|
<View className={`${error !== "123" ? 'opacity-100' : 'opacity-0'} w-full flex justify-center items-center text-primary-500 text-sm`}>
|
||||||
<ThemedText className="text-sm !text-textPrimary">
|
<ThemedText size='xxs' color='bgSecondary' type='inter'>
|
||||||
{error}
|
{error}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
@ -161,22 +162,22 @@ const LoginScreen = () => {
|
|||||||
return components[status as keyof typeof components] || components.login;
|
return components[status as keyof typeof components] || components.login;
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
<View style={{ width: "100%", alignItems: "center", marginTop: 16 }}>
|
<View style={{ width: "100%", alignItems: "center", }}>
|
||||||
{status == 'login' || !status &&
|
{status == 'login' || !status &&
|
||||||
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
<View className="flex-row justify-center mt-2 flex-wrap w-[85%] items-center">
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645" }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter'>
|
||||||
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
{status === 'login' || !status ? t('auth.agree.logintext', { ns: 'login' }) : t('auth.agree.singupText', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('terms') }}>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' style={{ textDecorationLine: 'underline' }}>
|
||||||
{t('auth.agree.terms', { ns: 'login' })}
|
{t('auth.agree.terms', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", flexWrap: 'wrap' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap'>
|
||||||
{t('auth.agree.join', { ns: 'login' })}
|
{t('auth.agree.join', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
<TouchableOpacity onPress={() => { setModalVisible(true); setModalType('privacy') }}>
|
||||||
<ThemedText style={{ fontSize: 11, color: "#FFB645", textDecorationLine: 'underline' }}>
|
<ThemedText color='bgPrimary' size='xxs' type='inter' className='flex-wrap' style={{ textDecorationLine: 'underline' }}>
|
||||||
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
{t('auth.agree.privacyPolicy', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
BIN
assets/fonts/Inter-Regular.otf
Normal file
BIN
assets/fonts/Inter-Regular.otf
Normal file
Binary file not shown.
BIN
assets/fonts/SF-Pro.otf
Normal file
BIN
assets/fonts/SF-Pro.otf
Normal file
Binary file not shown.
@ -1,16 +1,26 @@
|
|||||||
import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native';
|
import { StyleProp, StyleSheet, Text, TextStyle, type TextProps } from 'react-native';
|
||||||
|
|
||||||
import { Fonts } from '@/constants/Fonts';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { FontColor, Fonts } from '@/constants/Fonts';
|
||||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||||
|
|
||||||
|
export type ThemeColor = keyof typeof Colors.light & keyof typeof Colors.dark;
|
||||||
|
export type ColorValue = `#${string}` | `rgb(${string})` | `rgba(${string})` | string;
|
||||||
|
|
||||||
export type ThemedTextProps = TextProps & {
|
export type ThemedTextProps = TextProps & {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
darkColor?: string;
|
darkColor?: string;
|
||||||
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
|
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link' | 'sfPro' | 'inter';
|
||||||
weight?: 'regular' | 'medium' | 'semiBold' | 'bold';
|
weight?: 'regular' | 'medium' | 'semiBold' | 'bold';
|
||||||
size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
size?: 'xxs' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||||
|
radius?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||||
|
color?: ThemeColor | FontColor | ColorValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function isFontColorKey(key: string): key is FontColor {
|
||||||
|
return ['bgPrimary', 'bgSecondary', 'textPrimary', 'textSecondary', 'textThird', 'textWhite'].includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
export function ThemedText({
|
export function ThemedText({
|
||||||
style,
|
style,
|
||||||
lightColor,
|
lightColor,
|
||||||
@ -18,20 +28,38 @@ export function ThemedText({
|
|||||||
type = 'default',
|
type = 'default',
|
||||||
weight = 'regular',
|
weight = 'regular',
|
||||||
size,
|
size,
|
||||||
|
radius,
|
||||||
|
color,
|
||||||
...rest
|
...rest
|
||||||
}: ThemedTextProps) {
|
}: ThemedTextProps) {
|
||||||
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
|
||||||
|
const themeColor = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
|
||||||
|
|
||||||
|
const textColor = (() => {
|
||||||
|
if (!color) return themeColor;
|
||||||
|
|
||||||
|
// 检查是否是主题颜色
|
||||||
|
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||||
|
if (themeColors.includes(color as ThemeColor)) {
|
||||||
|
return useThemeColor({ light: lightColor, dark: darkColor }, color as ThemeColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是 Fonts 中定义的颜色
|
||||||
|
if (isFontColorKey(color)) {
|
||||||
|
return Fonts[color as FontColor];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回自定义颜色值
|
||||||
|
return color;
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
const baseStyle: StyleProp<TextStyle> = {
|
const baseStyle: StyleProp<TextStyle> = {
|
||||||
fontFamily: Fonts.primary,
|
fontFamily: Fonts.quicksand,
|
||||||
color,
|
color: textColor,
|
||||||
fontWeight: Fonts[weight as keyof typeof Fonts] as TextStyle['fontWeight'],
|
fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (size) {
|
|
||||||
baseStyle.fontSize = Fonts[size as keyof typeof Fonts];
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
@ -41,6 +69,11 @@ export function ThemedText({
|
|||||||
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
|
||||||
type === 'subtitle' ? styles.subtitle : undefined,
|
type === 'subtitle' ? styles.subtitle : undefined,
|
||||||
type === 'link' ? styles.link : undefined,
|
type === 'link' ? styles.link : undefined,
|
||||||
|
type === 'sfPro' ? styles.sfPro : undefined,
|
||||||
|
type === 'inter' ? styles.inter : undefined,
|
||||||
|
size && { fontSize: Number(Fonts[size as keyof typeof Fonts]) },
|
||||||
|
weight && { fontWeight: Number(Fonts[weight as keyof typeof Fonts]) as TextStyle['fontWeight'] },
|
||||||
|
color && { color: textColor },
|
||||||
style,
|
style,
|
||||||
]}
|
]}
|
||||||
{...rest}
|
{...rest}
|
||||||
@ -50,11 +83,12 @@ export function ThemedText({
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
default: {
|
default: {
|
||||||
fontSize: Fonts.base,
|
fontSize: Number(Fonts.base),
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
|
fontFamily: Fonts.quicksand,
|
||||||
},
|
},
|
||||||
defaultSemiBold: {
|
defaultSemiBold: {
|
||||||
fontSize: Fonts.base,
|
fontSize: Number(Fonts.base),
|
||||||
lineHeight: 24,
|
lineHeight: 24,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
@ -74,4 +108,16 @@ const styles = StyleSheet.create({
|
|||||||
color: '#0a7ea4',
|
color: '#0a7ea4',
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
},
|
},
|
||||||
|
sfPro: {
|
||||||
|
fontSize: Number(Fonts.base),
|
||||||
|
lineHeight: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontFamily: Fonts.sfPro,
|
||||||
|
},
|
||||||
|
inter: {
|
||||||
|
fontSize: Number(Fonts.base),
|
||||||
|
lineHeight: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
fontFamily: Fonts.inter,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,32 @@
|
|||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { FontColor, Fonts } from '@/constants/Fonts';
|
||||||
|
import { useThemeColor } from '@/hooks/useThemeColor';
|
||||||
import { View, type ViewProps } from 'react-native';
|
import { View, type ViewProps } from 'react-native';
|
||||||
|
import { ColorValue, isFontColorKey, ThemeColor } from './ThemedText';
|
||||||
|
|
||||||
type ThemedViewProps = ViewProps & {
|
type ThemedViewProps = ViewProps & {
|
||||||
className?: string;
|
className?: string;
|
||||||
|
bgColor?: FontColor | ColorValue | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ThemedView({ className, style, ...props }: ThemedViewProps) {
|
export function ThemedView({ className, style, bgColor, ...props }: ThemedViewProps) {
|
||||||
return <View className={className} style={style} {...props} />;
|
const themeColor = useThemeColor({ light: bgColor, dark: bgColor }, 'background');
|
||||||
|
|
||||||
|
const bgColorValue = (() => {
|
||||||
|
if (!bgColor) return themeColor;
|
||||||
|
|
||||||
|
// 检查是否是主题颜色
|
||||||
|
const themeColors = Object.keys(Colors.light) as ThemeColor[];
|
||||||
|
if (themeColors.includes(bgColor as ThemeColor)) {
|
||||||
|
return useThemeColor({ light: bgColor, dark: bgColor }, bgColor as ThemeColor);
|
||||||
|
}
|
||||||
|
// 检查是否是 Fonts 中定义的颜色
|
||||||
|
if (isFontColorKey(bgColor)) {
|
||||||
|
return Fonts[bgColor];
|
||||||
|
}
|
||||||
|
// 返回自定义颜色值
|
||||||
|
return bgColor;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return <View className={className} style={[{ backgroundColor: bgColorValue }, style]} {...props} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { fetchApi } from "@/lib/server-api-util";
|
import { fetchApi } from "@/lib/server-api-util";
|
||||||
import { User } from "@/types/user";
|
import { User } from "@/types/user";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
setIsSignUp?: (isSignUp: string) => void;
|
setIsSignUp?: (isSignUp: string) => void;
|
||||||
@ -69,45 +72,29 @@ const ForgetPwd = ({ setIsSignUp, updateUrlParam, setError }: LoginProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.inputContainer}>
|
{/* 邮箱 */}
|
||||||
<ThemedText style={styles.inputLabel}>
|
<TextInput
|
||||||
{t('auth.forgetPwd.title', { ns: 'login' })}
|
label={t('auth.forgetPwd.title', { ns: 'login' })}
|
||||||
</ThemedText>
|
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
||||||
<TextInput
|
onChangeText={setEmail}
|
||||||
style={styles.textInput}
|
autoCapitalize="none"
|
||||||
placeholder={t('auth.forgetPwd.emailPlaceholder', { ns: 'login' })}
|
value={email}
|
||||||
placeholderTextColor="#ccc"
|
/>
|
||||||
value={email}
|
{/* 发送邮箱 */}
|
||||||
onChangeText={setEmail}
|
<Button
|
||||||
keyboardType="email-address"
|
isLoading={isDisabled || loading}
|
||||||
autoCapitalize="none"
|
handleLogin={handleSubmit}
|
||||||
/>
|
text={isDisabled
|
||||||
</View>
|
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
||||||
|
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
||||||
<TouchableOpacity
|
/>
|
||||||
style={[
|
|
||||||
styles.submitButton,
|
|
||||||
(isDisabled || loading) && styles.disabledButton
|
|
||||||
]}
|
|
||||||
onPress={handleSubmit}
|
|
||||||
disabled={isDisabled || loading}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<ThemedText style={styles.buttonText}>
|
|
||||||
{isDisabled
|
|
||||||
? `${t("auth.forgetPwd.sendEmailBtnDisabled", { ns: "login" })} (${countdown}s)`
|
|
||||||
: t("auth.forgetPwd.sendEmailBtn", { ns: "login" })}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
|
{/* 返回登录 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={handleBackToLogin}
|
onPress={handleBackToLogin}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.backButtonText}>
|
<ThemedText type='inter' color="bgSecondary" size="sm">
|
||||||
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
{t('auth.forgetPwd.goback', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
@ -123,16 +110,24 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
fontSize: 16,
|
fontSize: Fonts['base'],
|
||||||
color: '#1F2937',
|
color: Fonts['textPrimary'],
|
||||||
|
fontWeight: Fonts['bold'],
|
||||||
|
fontFamily: Fonts['sfPro'],
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
borderRadius: 12,
|
borderRadius: Fonts['xs'],
|
||||||
padding: 12,
|
paddingHorizontal: Fonts['base'],
|
||||||
fontSize: 16,
|
paddingVertical: Fonts['xs'],
|
||||||
backgroundColor: '#FFF8DE',
|
fontSize: Fonts['sm'],
|
||||||
|
lineHeight: Fonts['base'],
|
||||||
|
textAlignVertical: 'center',
|
||||||
|
backgroundColor: Fonts['bgInput'],
|
||||||
|
color: Fonts['textSecondary'],
|
||||||
|
fontFamily: Fonts['inter'],
|
||||||
|
paddingRight: Fonts['5xl'],
|
||||||
},
|
},
|
||||||
submitButton: {
|
submitButton: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -151,11 +146,7 @@ const styles = StyleSheet.create({
|
|||||||
backButton: {
|
backButton: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
},
|
}
|
||||||
backButtonText: {
|
|
||||||
color: '#1F2937',
|
|
||||||
fontSize: 14,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ForgetPwd;
|
export default ForgetPwd;
|
||||||
@ -1,14 +1,15 @@
|
|||||||
import { Ionicons } from "@expo/vector-icons";
|
import { Fonts } from "@/constants/Fonts";
|
||||||
import { router } from "expo-router";
|
import { router } from "expo-router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ActivityIndicator, StyleSheet, TextInput, TouchableOpacity, View } from "react-native";
|
import { StyleSheet, TouchableOpacity, View } from "react-native";
|
||||||
import { useAuth } from "../../contexts/auth-context";
|
import { useAuth } from "../../contexts/auth-context";
|
||||||
import { fetchApi } from "../../lib/server-api-util";
|
import { fetchApi } from "../../lib/server-api-util";
|
||||||
import { User } from "../../types/user";
|
import { User } from "../../types/user";
|
||||||
import { ThemedText } from "../ThemedText";
|
import { ThemedText } from "../ThemedText";
|
||||||
|
import Button from "./ui/Button";
|
||||||
|
import TextInput from "./ui/TextInput";
|
||||||
|
|
||||||
const REMEMBER_ACCOUNT_KEY = 'fairclip_remembered_account';
|
|
||||||
interface LoginProps {
|
interface LoginProps {
|
||||||
updateUrlParam: (status: string, value: string) => void;
|
updateUrlParam: (status: string, value: string) => void;
|
||||||
setError: (error: string) => void;
|
setError: (error: string) => void;
|
||||||
@ -22,7 +23,6 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
|||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [rememberMe, setRememberMe] = useState(false);
|
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!email) {
|
if (!email) {
|
||||||
@ -69,85 +69,61 @@ const Login = ({ updateUrlParam, setError, setShowPassword, showPassword }: Logi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={[styles.inputContainer, { marginBottom: 20 }]}>
|
|
||||||
<ThemedText style={styles.inputLabel}>
|
|
||||||
{t('auth.login.email', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
<TextInput
|
|
||||||
style={styles.textInput}
|
|
||||||
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
|
||||||
placeholderTextColor="#ccc"
|
|
||||||
value={email}
|
|
||||||
onChangeText={(text) => {
|
|
||||||
setEmail(text);
|
|
||||||
setError('123');
|
|
||||||
}}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.inputContainer}>
|
{/* 邮箱 */}
|
||||||
<ThemedText style={styles.inputLabel}>
|
<TextInput
|
||||||
{t('auth.login.password', { ns: 'login' })}
|
label={t('auth.login.email', { ns: 'login' })}
|
||||||
</ThemedText>
|
placeholder={t('auth.login.accountPlaceholder', { ns: 'login' })}
|
||||||
<View style={styles.passwordInputContainer}>
|
onChangeText={(text) => {
|
||||||
<TextInput
|
setEmail(text);
|
||||||
style={[styles.textInput, { paddingRight: 48 }]}
|
setError('123');
|
||||||
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
}}
|
||||||
placeholderTextColor="#ccc"
|
autoCapitalize="none"
|
||||||
value={password}
|
value={email}
|
||||||
onChangeText={(text) => {
|
/>
|
||||||
setPassword(text);
|
|
||||||
setError('123');
|
|
||||||
}}
|
|
||||||
secureTextEntry={!showPassword}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.eyeIcon}
|
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={showPassword ? 'eye' : 'eye-off'}
|
|
||||||
size={20}
|
|
||||||
color="#666"
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
{/* 密码 */}
|
||||||
|
<TextInput
|
||||||
|
label={t('auth.login.password', { ns: 'login' })}
|
||||||
|
placeholder={t('auth.login.passwordPlaceholder', { ns: 'login' })}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setPassword(text);
|
||||||
|
setError('123');
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
value={password}
|
||||||
|
type="password"
|
||||||
|
setShowPassword={setShowPassword}
|
||||||
|
showPassword={showPassword}
|
||||||
|
containerStyle={{ marginBottom: 0 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 忘记密码 */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.forgotPassword}
|
style={styles.forgotPassword}
|
||||||
onPress={handleForgotPassword}
|
onPress={handleForgotPassword}
|
||||||
>
|
>
|
||||||
<ThemedText style={styles.forgotPasswordText}>
|
<ThemedText style={styles.forgotPasswordText} color="textPrimary" type="inter">
|
||||||
{t('auth.login.forgotPassword', { ns: 'login' })}
|
{t('auth.login.forgotPassword', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
<TouchableOpacity
|
{/* 登录按钮 */}
|
||||||
style={[styles.loginButton, isLoading && { opacity: 0.7 }]}
|
<Button isLoading={isLoading} handleLogin={handleLogin} text={t('auth.login.loginButton', { ns: 'login' })} />
|
||||||
onPress={handleLogin}
|
|
||||||
disabled={isLoading}
|
{/* 注册 */}
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<ActivityIndicator color="#fff" />
|
|
||||||
) : (
|
|
||||||
<ThemedText style={styles.loginButtonText}>
|
|
||||||
{t('auth.login.loginButton', { ns: 'login' })}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={styles.signupContainer}>
|
<View style={styles.signupContainer}>
|
||||||
<ThemedText style={styles.signupText}>
|
<ThemedText style={styles.signupText} type="sfPro">
|
||||||
{t('auth.login.signUpMessage', { ns: 'login' })}
|
{t('auth.login.signUpMessage', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<TouchableOpacity onPress={handleSignUp}>
|
<TouchableOpacity onPress={handleSignUp}>
|
||||||
<ThemedText style={styles.signupLink}>
|
<ThemedText style={styles.signupLink} type="sfPro">
|
||||||
{t('auth.login.signUp', { ns: 'login' })}
|
{t('auth.login.signUp', { ns: 'login' })}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 第三方登录 */}
|
||||||
<View style={{ width: "100%", alignItems: "center", opacity: 0 }}>
|
<View style={{ width: "100%", alignItems: "center", opacity: 0 }}>
|
||||||
<View style={styles.loginTypeContainer}>
|
<View style={styles.loginTypeContainer}>
|
||||||
<ThemedText>
|
<ThemedText>
|
||||||
@ -178,8 +154,8 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
loginType: {
|
loginType: {
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
width: 54,
|
width: 42,
|
||||||
height: 54,
|
height: 42,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: '#FADBA1'
|
backgroundColor: '#FADBA1'
|
||||||
},
|
},
|
||||||
@ -187,19 +163,24 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
},
|
},
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
fontSize: 16,
|
fontSize: Fonts['base'],
|
||||||
color: '#AC7E35',
|
color: Fonts['textPrimary'],
|
||||||
fontWeight: '600',
|
fontWeight: Fonts['bold'],
|
||||||
|
fontFamily: Fonts['sfPro'],
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
textInput: {
|
textInput: {
|
||||||
borderRadius: 12,
|
borderRadius: Fonts['xs'],
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: Fonts['base'],
|
||||||
paddingVertical: 12,
|
paddingVertical: Fonts['xs'],
|
||||||
fontSize: 14,
|
fontSize: Fonts['sm'],
|
||||||
|
lineHeight: Fonts['base'],
|
||||||
textAlignVertical: 'center',
|
textAlignVertical: 'center',
|
||||||
backgroundColor: '#FFF8DE'
|
backgroundColor: Fonts['bgInput'],
|
||||||
|
color: Fonts['textSecondary'],
|
||||||
|
fontFamily: Fonts['inter'],
|
||||||
|
paddingRight: Fonts['5xl'],
|
||||||
},
|
},
|
||||||
passwordInputContainer: {
|
passwordInputContainer: {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -237,11 +218,11 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
signupText: {
|
signupText: {
|
||||||
color: '#AC7E35',
|
color: '#AC7E35',
|
||||||
fontSize: 17,
|
fontSize: Fonts['sm'],
|
||||||
},
|
},
|
||||||
signupLink: {
|
signupLink: {
|
||||||
color: '#E2793F',
|
color: '#E2793F',
|
||||||
fontSize: 17,
|
fontSize: Fonts['sm'],
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
textDecorationLine: 'underline',
|
textDecorationLine: 'underline',
|
||||||
|
|||||||
37
components/login/ui/Button.tsx
Normal file
37
components/login/ui/Button.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { ActivityIndicator, StyleSheet, TouchableOpacity, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
handleLogin?: () => void;
|
||||||
|
text: string;
|
||||||
|
containerStyle?: ViewStyle;
|
||||||
|
}
|
||||||
|
const Button = ({ isLoading, handleLogin, text, containerStyle }: ButtonProps) => {
|
||||||
|
return <TouchableOpacity
|
||||||
|
style={[styles.loginButton, isLoading && { opacity: 0.7 }, containerStyle]}
|
||||||
|
onPress={handleLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color="#fff" />
|
||||||
|
) : (
|
||||||
|
<ThemedText type="sfPro" size="lg" weight="bold" color="textWhite">
|
||||||
|
{text}
|
||||||
|
</ThemedText>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
loginButton: {
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: '#E2793F',
|
||||||
|
borderRadius: 28,
|
||||||
|
padding: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Button
|
||||||
111
components/login/ui/TextInput.tsx
Normal file
111
components/login/ui/TextInput.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import { ThemedText } from "@/components/ThemedText";
|
||||||
|
import { Fonts } from "@/constants/Fonts";
|
||||||
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
|
import { TextInput as RNTextInput, TextInputProps as RNTextInputProps, StyleProp, StyleSheet, TextStyle, TouchableOpacity, View, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
interface CustomTextInputProps {
|
||||||
|
label: string;
|
||||||
|
placeholder: string;
|
||||||
|
value: string;
|
||||||
|
onChangeText: (text: string) => void;
|
||||||
|
showPassword?: boolean;
|
||||||
|
setShowPassword?: (showPassword: boolean) => void;
|
||||||
|
setError?: (error: string) => void;
|
||||||
|
type?: 'default' | 'password';
|
||||||
|
containerStyle?: StyleProp<ViewStyle>;
|
||||||
|
style?: StyleProp<TextStyle>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TextInputProps = RNTextInputProps & CustomTextInputProps;
|
||||||
|
|
||||||
|
const TextInput = ({
|
||||||
|
type = 'default',
|
||||||
|
label,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
onChangeText,
|
||||||
|
setError,
|
||||||
|
showPassword,
|
||||||
|
setShowPassword,
|
||||||
|
style,
|
||||||
|
containerStyle,
|
||||||
|
...props
|
||||||
|
}: TextInputProps) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.inputContainer, containerStyle]}>
|
||||||
|
<ThemedText style={styles.inputLabel}>
|
||||||
|
{label}
|
||||||
|
</ThemedText>
|
||||||
|
<View style={styles.inputTextContainer}>
|
||||||
|
<RNTextInput
|
||||||
|
style={[styles.textInput, style]}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={Fonts['placeholderTextColor']}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
secureTextEntry={type === 'password' ? !showPassword : undefined}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
type === 'password' &&
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.eyeIcon}
|
||||||
|
onPress={() => {
|
||||||
|
if (setShowPassword) {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={showPassword ? 'eye' : 'eye-off'}
|
||||||
|
size={20}
|
||||||
|
color="#666"
|
||||||
|
/>
|
||||||
|
</TouchableOpacity>
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
inputContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: Fonts['sm'],
|
||||||
|
color: Fonts['textPrimary'],
|
||||||
|
fontWeight: Fonts['bold'],
|
||||||
|
fontFamily: Fonts['sfPro'],
|
||||||
|
marginBottom: 8,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
borderRadius: Fonts['xs'],
|
||||||
|
paddingHorizontal: Fonts['base'],
|
||||||
|
paddingVertical: Fonts['sm'],
|
||||||
|
fontSize: Fonts['sm'],
|
||||||
|
lineHeight: Fonts['base'],
|
||||||
|
textAlignVertical: 'center',
|
||||||
|
backgroundColor: Fonts['bgInput'],
|
||||||
|
color: Fonts['textSecondary'],
|
||||||
|
fontFamily: Fonts['inter'],
|
||||||
|
paddingRight: 48, // Make space for the eye icon
|
||||||
|
},
|
||||||
|
inputTextContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
eyeIcon: {
|
||||||
|
position: 'absolute',
|
||||||
|
right: 12,
|
||||||
|
top: '50%',
|
||||||
|
transform: [{ translateY: -10 }], // Half of the icon's height (20/2 = 10)
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default TextInput;
|
||||||
@ -1,14 +1,18 @@
|
|||||||
export const Fonts = {
|
export const Fonts = {
|
||||||
// Font family
|
// Font family
|
||||||
primary: 'english',
|
quicksand: 'quicksand',
|
||||||
|
sfPro: 'sfPro',
|
||||||
|
inter: 'inter',
|
||||||
|
|
||||||
// Font weights
|
// Font weights
|
||||||
regular: '400',
|
regular: '400',
|
||||||
medium: '500',
|
medium: '500',
|
||||||
semiBold: '600',
|
semiBold: '600',
|
||||||
bold: '700',
|
bold: '700',
|
||||||
|
extraBold: '800',
|
||||||
|
|
||||||
// Font sizes
|
// Font sizes
|
||||||
|
xxs: 11,
|
||||||
xs: 12,
|
xs: 12,
|
||||||
sm: 14,
|
sm: 14,
|
||||||
base: 16,
|
base: 16,
|
||||||
@ -18,7 +22,20 @@ export const Fonts = {
|
|||||||
'3xl': 30,
|
'3xl': 30,
|
||||||
'4xl': 36,
|
'4xl': 36,
|
||||||
'5xl': 48,
|
'5xl': 48,
|
||||||
|
|
||||||
|
// color
|
||||||
|
bgPrimary: '#FFB645',
|
||||||
|
bgSecondary: '#E2793F',
|
||||||
|
bgInput: '#FFF8DE',
|
||||||
|
textPrimary: '#AC7E35',
|
||||||
|
textSecondary: '#4C320C',
|
||||||
|
textThird: '#7F786F',
|
||||||
|
textWhite: "#FFFFFF",
|
||||||
|
placeholderTextColor: "#ccc",
|
||||||
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FontWeight = keyof Omit<typeof Fonts, 'primary' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
|
export type FontWeight = keyof Omit<typeof Fonts, 'quicksand' | 'sfPro' | 'inter' | 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl'>;
|
||||||
export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
export type FontSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl';
|
||||||
|
export type FontColor = 'bgPrimary' | 'bgSecondary' | 'textPrimary' | 'textSecondary' | 'textThird' | 'textWhite';
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user