// ============================================================ // ForgotPasswordModal — 忘记密码弹窗 // // 场景:未登录用户通过手机号 + 短信验证码重置密码 // 复用:可在登录页"忘记密码?"入口使用,也可在其他位置独立调用 // // API: // - AuthAPI.sendSms({ phone }) 发送短信验证码 // - AuthAPI.resetPassword({ phone, sms_code, new_password }) 重置密码 // ============================================================ import { useState, useCallback } from 'react'; import { logger } from '@/shared/utils/bus'; import { Modal, Input, Button, Typography, App } from 'antd'; import { PhoneOutlined, SafetyCertificateOutlined, LockOutlined, KeyOutlined, } from '@ant-design/icons'; import { useTheme } from '@/app/providers/ThemeProvider'; import { AuthAPI } from '@/services/modules/auth/auth'; import { validatePhone, validateSmsCode, validatePassword, validateConfirmPassword, runFieldChecks, } from '../../settings/validators'; import { useSmsCooldown } from '../../hooks/use-sms-cooldown'; const { Text, Title } = Typography; interface ForgotPasswordModalProps { open: boolean; onClose: () => void; } export function ForgotPasswordModal({ open, onClose }: ForgotPasswordModalProps) { const { isDark } = useTheme(); const { message } = App.useApp(); // ---- 表单字段 ---- const [phone, setPhone] = useState(''); const [smsCode, setSmsCode] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); // ---- 错误信息 ---- const [errors, setErrors] = useState>({}); // ---- 提交状态 ---- const [submitting, setSubmitting] = useState(false); // ---- 短信冷却 ---- const { cooldown, secondsLeft, startCooldown } = useSmsCooldown(60); // ---- 清除单个字段错误 ---- const clearError = useCallback((name: string) => { setErrors((p) => { if (!(name in p)) return p; const n = { ...p }; delete n[name]; return n; }); }, []); // ---- 发送验证码 ---- const handleSendSms = useCallback(() => { const err = validatePhone(phone); if (err) { setErrors((p) => ({ ...p, phone: err })); return; } clearError('phone'); startCooldown(); AuthAPI.sendSms({ phone }) .then(() => message.success('验证码已发送')) .catch((e) => { logger.warn('auth', '忘记密码-验证码发送失败', e instanceof Error ? e : undefined); message.error((e as Error).message || '验证码发送失败'); }); }, [phone, startCooldown, clearError, message]); // ---- 提交重置 ---- const handleSubmit = useCallback(async () => { if (runFieldChecks(setErrors, { field: 'phone', error: validatePhone(phone) }, { field: 'smsCode', error: validateSmsCode(smsCode) }, { field: 'newPassword', error: validatePassword(newPassword) }, { field: 'confirmPassword', error: validateConfirmPassword(confirmPassword, newPassword) }, )) return; setSubmitting(true); try { await AuthAPI.resetPassword({ phone, sms_code: smsCode, new_password: newPassword, }); message.success('密码重置成功,请使用新密码登录'); // 重置表单状态 setPhone(''); setSmsCode(''); setNewPassword(''); setConfirmPassword(''); onClose(); } catch (err) { logger.warn('auth', '密码重置失败', err instanceof Error ? err : undefined); message.error((err as Error).message || '密码重置失败'); } finally { setSubmitting(false); } }, [phone, smsCode, newPassword, confirmPassword, onClose, message]); // ---- 关闭时重置 ---- const handleClose = useCallback(() => { setErrors({}); onClose(); }, [onClose]); return ( {/* 标题 */}
重置密码 通过手机短信验证码重置您的密码
{/* 表单 */}
{/* 手机号 */}
} placeholder="请输入手机号" value={phone} onChange={(e) => { setPhone(e.target.value); clearError('phone'); }} status={errors.phone ? 'error' : undefined} allowClear /> {errors.phone && ( {errors.phone} )}
{/* 短信验证码 */}
} placeholder="请输入验证码" value={smsCode} onChange={(e) => { setSmsCode(e.target.value); clearError('smsCode'); }} status={errors.smsCode ? 'error' : undefined} className="flex-1" allowClear />
{errors.smsCode && ( {errors.smsCode} )}
{/* 新密码 */}
} placeholder="新密码(6~128位)" value={newPassword} onChange={(e) => { setNewPassword(e.target.value); clearError('newPassword'); }} status={errors.newPassword ? 'error' : undefined} allowClear /> {errors.newPassword && ( {errors.newPassword} )}
{/* 确认新密码 */}
} placeholder="请再次输入新密码" value={confirmPassword} onChange={(e) => { setConfirmPassword(e.target.value); clearError('confirmPassword'); }} status={errors.confirmPassword ? 'error' : undefined} allowClear /> {errors.confirmPassword && ( {errors.confirmPassword} )}
{/* 提交按钮 */}
); }