轻松聘
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.
 
 
 

153 lines
6.2 KiB

import { useState } from 'react';
import { Ionicons } from '@expo/vector-icons';
import { Alert, Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import { colors, spacing } from '../../../src/theme';
import { useAgreementGate, useLoginLanding, useOnboarding, usePasswordLogin } from '../../../src/hooks';
import { isValidPhone } from '../../../src/utils';
import { useAuth } from '../../../src/context/AuthContext';
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 { land, go } = useLoginLanding();
const { verifySession } = useAuth();
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;
let result;
try {
result = await login(phone, password);
} catch {
// 失败原因由 usePasswordLogin 记录,页面下方展示
return;
}
// 登录响应里就带角色:直接按它落地(消费一次,不额外打接口)
const target = land(result);
if (target !== 'identity') {
go(target);
return;
}
/*
* 登录响应要求或暗示还需选身份时,再用 /auth/me 复核一次:
* 早先这里在 /auth/me 失败后「退回本地已选身份」并继续跳转 —— 本地角色只是上次的
* 缓存,服务器此刻可能已经把它改了(例如选了企业、又被后台调整)。拿过期缓存把人
* 放进业务页面,比停下来更糟。现在失败即提示重试,不进业务页。
*/
try {
const verified = await verifySession();
if (verified) {
go(verified === 'enterprise' ? 'enterprise' : 'candidate');
return;
}
} catch (e) {
Alert.alert('无法确认账号身份', (e as Error).message || '请检查网络后重试');
return;
}
navigate('P3');
};
return (
<ScreenShell showBack onBack={back}>
<Text style={styles.h1}>密码登录</Text>
<Text style={styles.subtitle}>使用手机号与登录密码进入轻松聘</Text>
{/* 手机号 */}
<View style={styles.fieldRow}>
<Text style={styles.prefix}>+86</Text>
<Ionicons name="caret-down" size={14} color={colors.textSecondary} />
<View style={styles.divider} />
<TextInput
style={styles.input}
value={phone}
onChangeText={(t) => setData({ phone: t.replace(/[^\d]/g, '').slice(0, 11) })}
placeholder="请输入手机号"
placeholderTextColor={colors.textHint}
keyboardType="phone-pad"
maxLength={11}
/>
{phone.length > 0 ? (
<Pressable onPress={() => setData({ phone: '' })} hitSlop={8}>
<Ionicons name="close-circle" size={18} color={colors.textDisabled} />
</Pressable>
) : null}
</View>
<View style={styles.underline} />
{/* 密码 */}
<View style={styles.passwordArea}>
<PasswordField value={password} onChangeText={setPassword} placeholder="请输入登录密码" />
</View>
<Pressable onPress={() => gate(() => navigate('P2R'))} hitSlop={8}>
<Text style={styles.forgotLink}>忘记密码?</Text>
</Pressable>
<View style={styles.btnArea}>
<PrimaryButton label="登录" active={canSubmit} loading={loading} onPress={() => gate(handleLogin)} />
</View>
{error ? <Text style={styles.formError}>{error.message}</Text> : null}
<View style={styles.agree}>
<AgreementCheckbox
checked={agreed}
onToggle={() => setData({ agreed: !agreed })}
text={
<Text style={styles.agreeText}>
已阅读并同意
<Text style={styles.link} onPress={() => navigate('B2')}>《轻松聘用户协议》</Text>
和
<Text style={styles.link} onPress={() => navigate('B2')}>《隐私政策》</Text>
,允许轻松聘统一管理本人账号信息
</Text>
}
/>
</View>
<Pressable onPress={() => gate(() => navigate('P2'))} hitSlop={8}>
<Text style={styles.switchLink}>验证码登录</Text>
</Pressable>
<AgreementModal {...modalProps} />
</ScreenShell>
);
}
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 },
});