轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

265 lines
8.4 KiB

import { Ionicons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import { useEffect, useRef, useState } from 'react';
import {
Alert,
Pressable,
ScrollView,
StyleSheet,
Text,
TextInput,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { colors, headerGradientShort } from '../src/theme';
import { useAuth } from '../src/context/AuthContext';
import { useLogin } from '../src/hooks';
const PHONE_RE = /^1\d{10}$/;
export default function LoginScreen() {
const router = useRouter();
const { login: authLogin } = useAuth();
const { login, loading, error } = useLogin();
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [agreed, setAgreed] = useState(false);
const [countdown, setCountdown] = useState(0);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
return () => {
if (timerRef.current) clearInterval(timerRef.current);
};
}, []);
const phoneValid = PHONE_RE.test(phone);
const canSubmit = phoneValid && code.length >= 4 && agreed && !loading;
const startCountdown = () => {
if (!phoneValid || countdown > 0) return;
setCountdown(60);
timerRef.current = setInterval(() => {
setCountdown((c) => {
if (c <= 1) {
if (timerRef.current) clearInterval(timerRef.current);
return 0;
}
return c - 1;
});
}, 1000);
};
const handleLogin = async () => {
if (!canSubmit) return;
try {
const res = await login(phone, code);
authLogin(res.token);
router.replace('/identity-select');
} catch {
// 错误已由 error 向上展示
}
};
const handleGuest = () => {
router.replace('/identity-select');
};
return (
<SafeAreaView style={styles.container} edges={['top']}>
<LinearGradient colors={[...headerGradientShort]} style={styles.header}>
<View style={styles.logoCircle}>
<Text style={styles.logoText}>职</Text>
</View>
<Text style={styles.headerTitle}>登录职来</Text>
<Text style={styles.headerSub}>智能求职 · 企业招聘一站式服务平台</Text>
</LinearGradient>
<ScrollView contentContainerStyle={styles.body} showsVerticalScrollIndicator={false}>
<View style={styles.field}>
<Text style={styles.label}>手机号</Text>
<TextInput
style={styles.input}
value={phone}
onChangeText={setPhone}
placeholder="请输入手机号"
placeholderTextColor={colors.textHint}
keyboardType="phone-pad"
maxLength={11}
/>
</View>
<View style={styles.field}>
<Text style={styles.label}>验证码</Text>
<View style={styles.codeRow}>
<TextInput
style={[styles.input, styles.codeInput]}
value={code}
onChangeText={setCode}
placeholder="请输入验证码"
placeholderTextColor={colors.textHint}
keyboardType="number-pad"
maxLength={6}
/>
<Pressable
style={[styles.codeBtn, (countdown > 0 || !phoneValid) && styles.codeBtnDisabled]}
onPress={startCountdown}
disabled={countdown > 0 || !phoneValid}
>
<Text style={styles.codeBtnText}>
{countdown > 0 ? `${countdown}s 后重发` : '获取验证码'}
</Text>
</Pressable>
</View>
</View>
{error && <Text style={styles.errorText}>{error.message}</Text>}
<Pressable
style={[styles.submitBtn, !canSubmit && styles.submitDisabled]}
onPress={handleLogin}
disabled={!canSubmit}
>
<Text style={styles.submitText}>{loading ? '登录中…' : '登录 / 注册'}</Text>
</Pressable>
<View style={styles.agreement}>
<Pressable
style={[styles.checkbox, agreed && styles.checkboxChecked]}
onPress={() => setAgreed((v) => !v)}
hitSlop={6}
>
{agreed && <Ionicons name="checkmark" size={14} color={colors.textInverse} />}
</Pressable>
<Text style={styles.agreementText}>
已阅读并同意
<Text style={styles.link} onPress={() => router.push('/settings/privacy-policy')}>
《隐私政策》
</Text>
和
<Text style={styles.link} onPress={() => router.push('/settings/user-agreement')}>
《用户协议》
</Text>
</Text>
</View>
<View style={styles.others}>
<Text style={styles.othersTitle}>其他登录方式</Text>
<View style={styles.othersRow}>
<Pressable style={styles.otherItem} onPress={() => Alert.alert('提示', '该功能开发中,敬请期待')} hitSlop={8}>
<Ionicons name="logo-wechat" size={26} color={colors.success} />
</Pressable>
<Pressable style={styles.otherItem} onPress={() => Alert.alert('提示', '该功能开发中,敬请期待')} hitSlop={8}>
<Ionicons name="logo-apple" size={26} color={colors.textPrimary} />
</Pressable>
</View>
</View>
</ScrollView>
<Pressable style={styles.guest} onPress={handleGuest} hitSlop={6}>
<Text style={styles.guestText}>暂不登录,先逛逛</Text>
</Pressable>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.pageBg },
header: {
alignItems: 'center',
paddingTop: 44,
paddingBottom: 36,
},
logoCircle: {
width: 64,
height: 64,
borderRadius: 20,
backgroundColor: colors.white,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 14,
},
logoText: { fontSize: 30, fontWeight: '800', color: colors.primary },
headerTitle: { fontSize: 22, fontWeight: '800', color: colors.textInverse },
headerSub: { fontSize: 13, color: colors.textInverse, opacity: 0.9, marginTop: 6 },
body: { flexGrow: 1, padding: 20, paddingTop: 24 },
field: { marginBottom: 18 },
label: { fontSize: 14, fontWeight: '600', color: colors.textPrimary, marginBottom: 8 },
input: {
height: 50,
borderRadius: 12,
borderWidth: 1,
borderColor: colors.border,
backgroundColor: colors.white,
paddingHorizontal: 14,
fontSize: 16,
color: colors.textPrimary,
},
codeRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
codeInput: { flex: 1 },
codeBtn: {
height: 50,
paddingHorizontal: 16,
borderRadius: 12,
backgroundColor: colors.primaryLight,
alignItems: 'center',
justifyContent: 'center',
},
codeBtnDisabled: { backgroundColor: colors.borderLight, opacity: 0.7 },
codeBtnText: { fontSize: 14, fontWeight: '600', color: colors.primaryText },
errorText: {
fontSize: 13,
color: colors.danger,
marginBottom: 12,
},
submitBtn: {
height: 50,
borderRadius: 25,
backgroundColor: colors.primaryBtn,
alignItems: 'center',
justifyContent: 'center',
marginTop: 8,
},
submitDisabled: { backgroundColor: colors.border, opacity: 0.7 },
submitText: { fontSize: 16, fontWeight: '700', color: colors.textInverse },
agreement: {
flexDirection: 'row',
alignItems: 'flex-start',
marginTop: 16,
},
checkbox: {
width: 18,
height: 18,
borderRadius: 5,
borderWidth: 1.5,
borderColor: colors.borderStrong,
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
marginRight: 8,
},
checkboxChecked: { backgroundColor: colors.primary, borderColor: colors.primary },
agreementText: { flex: 1, fontSize: 12, color: colors.textSecondary, lineHeight: 18 },
link: { color: colors.primaryText, fontWeight: '600' },
others: { alignItems: 'center', marginTop: 32 },
othersTitle: { fontSize: 12, color: colors.textHint, marginBottom: 16 },
othersRow: { flexDirection: 'row', gap: 36 },
otherItem: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: colors.white,
borderWidth: 1,
borderColor: colors.borderLight,
alignItems: 'center',
justifyContent: 'center',
},
guest: {
alignItems: 'center',
paddingVertical: 18,
backgroundColor: colors.pageBg,
},
guestText: { fontSize: 14, color: colors.textHint },
});