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, usePasswordReset, useSmsCode } from '../../../src/hooks'; import { isValidPhone, isValidSmsCode, SMS_CODE_MAX_LENGTH } from '../../../src/utils'; import { useAuth } from '../../../src/context/AuthContext'; import { useRole } from '../../../src/context/RoleContext'; import { PasswordField } from '../../../src/components/PasswordField'; import { ScreenShell } from '../ui/ScreenShell'; import { CountdownButton } from '../ui/CountdownButton'; import { PrimaryButton } from '../ui/PrimaryButton'; import { AgreementModal } from '../ui/AgreementModal'; /** 重置阶段:验证手机号 → 设置新密码 */ type Stage = 'verify' | 'password'; /** * P2R 设置 / 重置登录密码(同页两阶段)。 * * 一个页面同时承担两件事: * 1. 忘记密码 —— 老用户忘记原密码,验证手机号后重设; * 2. 首次设置密码 —— 「登录即注册」的用户天生没有密码,走同一入口首次获得密码。 * 因此文案对两者都成立,不区分来源,避免出现「有密码登录入口但没人拿得到密码」的死结。 */ export function P2PasswordReset() { const { data, setData, navigate, back } = useOnboarding(); const { gate, modalProps } = useAgreementGate(); const { reset, loading, error, checkPassword } = usePasswordReset(); const { send: sendCode, error: smsError } = useSmsCode(); /** 重置成功即写入登录态,用户不必再用新密码登录一次 */ const { login: writeAuth } = useAuth(); const { role } = useRole(); const router = useRouter(); const [stage, setStage] = useState('verify'); const [code, setCode] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const phone = data.phone; const phoneValid = isValidPhone(phone); const codeValid = isValidSmsCode(code); /** 密码规则提示(规则在 API 层计算,这里只渲染文案) */ const hint = password.length > 0 ? checkPassword(password, confirm) : null; const canSubmit = codeValid && password.length > 0 && confirm.length > 0 && hint === null && !loading; /** * 发送验证码(只发送,不切阶段)。 * 验证码是否正确由最终提交时的后端校验统一判定,本步不做本地校验, * 避免「前端说对了、后端说错了」的不一致。 */ const sendCodeNow = async () => { if (!phoneValid) return; try { await sendCode(phone, 'reset-password'); } catch { // 失败原因由 useSmsCode 记录,页面下方展示 } }; const handleSubmit = async () => { if (!canSubmit) return; let result; try { result = await reset({ phone, code, password }); writeAuth(result.token); } catch { // 失败原因由 usePasswordReset 记录,页面下方展示 return; } // 已有身份的老用户直接进对应主界面,新用户才去选身份 if (role) { router.replace(role === 'enterprise' ? '/(enterprise)/job-management' : '/(tabs)/jobs'); return; } navigate('P3'); }; return ( setStage('verify') : back}> {stage === 'verify' ? ( /* ---------- 阶段1:验证手机号 ---------- */ 验证手机号 验证通过后即可设置新的登录密码 +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} setCode(t.replace(/[^\d]/g, '').slice(0, SMS_CODE_MAX_LENGTH))} placeholder="请输入验证码" placeholderTextColor={colors.textHint} keyboardType="number-pad" maxLength={SMS_CODE_MAX_LENGTH} /> void sendCodeNow()} /> gate(() => setStage('password'))} /> {smsError ? {smsError.message} : null} ) : ( /* ---------- 阶段2:设置新密码 ---------- */ 设置登录密码 设置后可用手机号 + 密码登录 {hint ? ( {hint} ) : ( 8–20 位,需同时包含字母和数字 )} gate(handleSubmit)} /> {error ? {error.message} : null} )} ); } 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: 26, 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 }, hint: { marginTop: spacing.block, fontSize: 12, color: colors.textHint }, hintDanger: { marginTop: spacing.block, fontSize: 12, color: colors.danger }, btnArea: { marginTop: 26 }, formError: { marginTop: spacing.sm, textAlign: 'center', fontSize: 12, color: colors.danger }, });