import { useState } from 'react'; import { Ionicons } from '@expo/vector-icons'; import { useRouter } from 'expo-router'; import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; import { colors, spacing } from '../../../src/theme'; import { useAgreementGate, useOnboarding, usePasswordLogin } from '../../../src/hooks'; import { isValidPhone } from '../../../src/utils'; import { useAuth } from '../../../src/context/AuthContext'; import { useRole } from '../../../src/context/RoleContext'; import { fetchCurrentUser } from '../../../src/api'; import { PasswordField } from '../../../src/components/PasswordField'; import { ScreenShell } from '../ui/ScreenShell'; import { AgreementCheckbox } from '../ui/AgreementCheckbox'; import { PrimaryButton } from '../ui/PrimaryButton'; import { AgreementModal } from '../ui/AgreementModal'; /** * P2P 手机号 + 密码登录。 * * 与 P2(验证码)、P2R(设置密码)互相可达:本页底部可切回验证码登录,忘记密码进 P2R。 * 登录成功后按是否已选过身份决定落地页,与 P1 微信分支的原逻辑一致。 */ export function P2PasswordLogin() { const { data, setData, navigate, back } = useOnboarding(); const { gate, modalProps } = useAgreementGate(); const { login, loading, error } = usePasswordLogin(); /** 双 Token 写入:换取令牌后必须落库,否则冷启动会被打回登录页 */ const { setSession } = useAuth(); const { role, setRole } = useRole(); const router = useRouter(); const [password, setPassword] = useState(''); const agreed = data.agreed; const phone = data.phone; const phoneValid = isValidPhone(phone); const canSubmit = phoneValid && password.length > 0; const handleLogin = async () => { if (!canSubmit || loading) return; try { const result = await login(phone, password); // 双 Token 落库(access + refresh),后续请求自动带 Bearer await setSession(result.accessToken, result.refreshToken, result.expiresAt); } catch { // 失败原因由 usePasswordLogin 记录,页面下方展示 return; } // 角色以 /auth/me 后端返回为准(规范 §5),不一致时纠正路由 let landingRole = role; try { const me = await fetchCurrentUser(); setRole(me.role); landingRole = me.role; } catch { // /auth/me 失败时退回到本地已选身份,不阻断登录 } // 已有身份的老用户直接进对应主界面,新用户才去选身份 if (landingRole) { router.replace(landingRole === 'enterprise' ? '/(enterprise)/job-management' : '/(tabs)/jobs'); return; } navigate('P3'); }; return ( 密码登录 使用手机号与登录密码进入轻松聘 {/* 手机号 */} +86 setData({ phone: t.replace(/[^\d]/g, '').slice(0, 11) })} placeholder="请输入手机号" placeholderTextColor={colors.textHint} keyboardType="phone-pad" maxLength={11} /> {phone.length > 0 ? ( setData({ phone: '' })} hitSlop={8}> ) : null} {/* 密码 */} gate(() => navigate('P2R'))} hitSlop={8}> 忘记密码? gate(handleLogin)} /> {error ? {error.message} : null} setData({ agreed: !agreed })} text={ 已阅读并同意 navigate('B2')}>《轻松聘用户协议》 和 navigate('B2')}>《隐私政策》 ,允许轻松聘统一管理本人账号信息 } /> gate(() => navigate('P2'))} hitSlop={8}> 验证码登录 ); } const styles = StyleSheet.create({ h1: { fontSize: 26, fontWeight: '800', color: colors.textPrimary, marginTop: spacing.block }, subtitle: { fontSize: 14, color: colors.textSecondary, marginTop: spacing.sm }, fieldRow: { flexDirection: 'row', alignItems: 'center', height: 56, marginTop: 28, gap: 6 }, prefix: { fontSize: 17, fontWeight: '700', color: colors.textPrimary }, divider: { width: 1, height: 20, backgroundColor: colors.border, marginHorizontal: 10 }, input: { flex: 1, fontSize: 17, color: colors.textPrimary, padding: 0 }, underline: { height: 1, backgroundColor: colors.borderLight }, passwordArea: { marginTop: spacing.section }, forgotLink: { marginTop: spacing.block, alignSelf: 'flex-end', fontSize: 13, color: colors.primaryText, fontWeight: '600' }, btnArea: { marginTop: 26 }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger }, agree: { marginTop: spacing.block }, agreeText: { fontSize: 12, color: colors.textSecondary, lineHeight: 18 }, link: { color: colors.primary, fontWeight: '600' }, switchLink: { marginTop: 26, textAlign: 'center', fontSize: 14, color: colors.textSecondary }, });