feat: mobile forgot-password redesign, navbar/search updates, vite proxy and request tweaks

This commit is contained in:
chunyu
2026-09-04 12:53:05 +08:00
parent a14badf8f7
commit 5bb503ff2c
12 changed files with 1224 additions and 866 deletions
+362 -147
View File
@@ -1,198 +1,413 @@
.forgot-password-page {
/* ===== Apple 极简白 - 忘记密码 ===== */
.fp {
min-height: 100vh;
background: var(--primary-gradient);
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
min-height: 100dvh;
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
padding: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
-webkit-font-smoothing: antialiased;
}
.forgot-password-page::before {
content: "";
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.08) 0%, transparent 60%);
animation: forgotPulse 8s ease-in-out infinite;
/* ===== 日间模式 ===== */
.fp.fp-light {
background: #f5f5f7;
color: #1d1d1f;
}
@keyframes forgotPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
/* ===== 夜间模式 ===== */
.fp.fp-dark {
background: #000;
color: #f5f5f7;
}
.forgot-password-card {
background: var(--card-background, #ffffff);
border-radius: 16px;
padding: 40px;
/* ===== Content ===== */
.fp-content {
width: 100%;
max-width: 460px;
box-sizing: border-box;
box-shadow: var(--shadow-xl);
padding: 48px 32px 0;
position: relative;
z-index: 1;
}
.forgot-password-header {
display: flex;
/* Logo */
.fp-logo {
width: 56px;
height: 56px;
border-radius: 16px;
display: inline-flex;
align-items: center;
margin-bottom: 24px;
justify-content: center;
font-size: 28px;
}
.fp-light .fp-logo { background: #1d1d1f; }
.fp-dark .fp-logo { background: rgba(255,255,255,0.1); }
/* Header */
.fp-header { text-align: center; margin-bottom: 32px; }
.fp-title {
font-size: 28px; font-weight: 800; margin-top: 16px;
letter-spacing: -0.5px;
}
.fp-subtitle { font-size: 15px; margin-top: 6px; }
.fp-light .fp-title { color: #1d1d1f; }
.fp-light .fp-subtitle { color: #86868b; }
.fp-dark .fp-title { color: #fff; }
.fp-dark .fp-subtitle { color: rgba(255,255,255,0.4); }
/* ===== Steps - Segmented Control ===== */
.fp-steps-track {
display: flex;
position: relative;
}
.forgot-password-header .back-button {
position: absolute;
left: -8px;
color: var(--text-secondary, #666);
font-size: 16px;
}
.forgot-password-header h2 {
flex: 1;
text-align: center;
margin: 0;
font-size: 22px;
font-weight: 600;
color: var(--text-primary, #1a1a2e);
}
.forgot-password-steps {
height: 56px;
border-radius: 14px;
padding: 6px;
overflow: hidden;
margin-bottom: 32px;
}
.fp-light .fp-steps-track { background: #e8e8ed; }
.fp-dark .fp-steps-track { background: rgba(255,255,255,0.06); }
.step-content {
.fp-steps-slide {
position: absolute;
top: 6px;
bottom: 6px;
left: 6px;
width: calc(50% - 6px);
border-radius: 10px;
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
.fp-light .fp-steps-slide { background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.fp-dark .fp-steps-slide { background: #2c2c2e; box-shadow: 0 1px 4px rgba(0,0,0,0.3); }
.fp-steps-slide[data-pos="1"] { transform: translateX(calc(100% + 0px)); }
.fp-step-btn {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
align-items: center;
justify-content: center;
gap: 4px;
border: none;
background: none;
cursor: default;
position: relative;
z-index: 2;
padding: 0;
font-family: inherit;
}
.fp-step-num {
width: 22px; height: 22px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700;
transition: all 0.3s;
}
.fp-step-label {
font-size: 12px; font-weight: 600;
transition: color 0.3s;
}
.input-group {
display: flex;
/* Step colors - Light */
.fp-light .fp-step-num { background: #d1d1d6; color: #aeaeb2; }
.fp-light .fp-step-label { color: #aeaeb2; }
.fp-light .fp-step-btn.active .fp-step-num { background: #1d1d1f; color: #fff; }
.fp-light .fp-step-btn.active .fp-step-label { color: #1d1d1f; }
.fp-light .fp-step-btn.done .fp-step-num { background: #34d399; color: #fff; }
.fp-light .fp-step-btn.done .fp-step-label { color: #34d399; }
/* Step colors - Dark */
.fp-dark .fp-step-num { background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.25); }
.fp-dark .fp-step-label { color: rgba(255,255,255,0.25); }
.fp-dark .fp-step-btn.active .fp-step-num { background: #667eea; color: #fff; }
.fp-dark .fp-step-btn.active .fp-step-label { color: #fff; }
.fp-dark .fp-step-btn.done .fp-step-num { background: #34d399; color: #fff; }
.fp-dark .fp-step-btn.done .fp-step-label { color: #34d399; }
/* ===== Form Step ===== */
.fp-step-content {
display: none;
width: 100%;
flex-direction: column;
}
.fp-step-content.active {
display: flex;
animation: fpFadeSlide 0.35s ease;
}
@keyframes fpFadeSlide {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
/* ===== Input group ===== */
.fp-field {
margin-bottom: 16px;
}
.fp-field label {
display: block;
font-size: 13px;
font-weight: 600;
margin-bottom: 8px;
}
.fp-light .fp-field label { color: #333; }
.fp-dark .fp-field label { color: rgba(255,255,255,0.6); }
.fp-input {
width: 100%;
height: 52px;
border-radius: 12px;
padding: 0 16px;
font-size: 16px;
outline: none;
transition: all 0.25s ease;
font-family: inherit;
box-sizing: border-box;
}
.fp-light .fp-input {
border: 1.5px solid #e5e5ea;
background: #fff;
color: #1d1d1f;
}
.fp-light .fp-input::placeholder { color: #c7c7cc; }
.fp-light .fp-input:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102,126,234,0.12);
}
.fp-dark .fp-input {
border: 1.5px solid rgba(255,255,255,0.1);
background: #1c1c1e;
color: #fff;
}
.fp-dark .fp-input::placeholder { color: rgba(255,255,255,0.25); }
.fp-dark .fp-input:focus {
border-color: rgba(102,126,234,0.6);
box-shadow: 0 0 0 3px rgba(102,126,234,0.12);
}
/* Code row */
.fp-code-row {
display: flex;
gap: 10px;
margin-bottom: 16px;
align-items: stretch;
}
.fp-code-row .fp-input {
flex: 1;
margin-bottom: 0;
}
/* Code button */
.fp-code-btn {
flex-shrink: 0;
height: 52px;
padding: 0 18px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
white-space: nowrap;
}
.fp-light .fp-code-btn {
border: 1.5px solid #e5e5ea;
background: #fafafa;
color: #667eea;
}
.fp-light .fp-code-btn:hover:not(:disabled) {
border-color: #667eea;
background: #f0f0ff;
}
.fp-dark .fp-code-btn {
border: 1.5px solid rgba(255,255,255,0.1);
background: #1c1c1e;
color: #667eea;
}
.fp-dark .fp-code-btn:hover:not(:disabled) {
background: rgba(102,126,234,0.12);
color: #8b9cf7;
}
.fp-code-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Primary button - Apple black */
.fp-btn-primary {
width: 100%;
height: 54px;
border-radius: 14px;
border: none;
font-size: 17px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 8px;
letter-spacing: 0.2px;
font-family: inherit;
}
.fp-btn-primary:active { transform: scale(0.97); opacity: 0.9; }
.fp-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.fp-light .fp-btn-primary { background: #1d1d1f; color: #fff; }
.fp-dark .fp-btn-primary { background: #fff; color: #000; }
/* Ghost button */
.fp-btn-ghost {
width: 100%;
height: 54px;
border-radius: 14px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
margin-top: 12px;
transition: all 0.3s ease;
font-family: inherit;
}
.fp-light .fp-btn-ghost {
border: 1.5px solid #e5e5ea;
background: #fff;
color: #1d1d1f;
}
.fp-light .fp-btn-ghost:hover { border-color: #d2d2d7; background: #fafafa; }
.fp-dark .fp-btn-ghost {
border: 1.5px solid rgba(255,255,255,0.12);
background: transparent;
color: rgba(255,255,255,0.8);
}
.fp-dark .fp-btn-ghost:hover { border-color: rgba(255,255,255,0.2); }
/* Back link */
.fp-back-link {
display: inline-flex;
align-items: center;
gap: 6px;
}
.input-group label {
font-size: 14px;
cursor: pointer;
margin-bottom: 20px;
font-weight: 500;
color: var(--text-primary, #333);
font-family: inherit;
border: none;
background: none;
padding: 0;
transition: color 0.2s;
}
.fp-light .fp-back-link { color: #667eea; }
.fp-dark .fp-back-link { color: #667eea; }
.fp-back-link:hover { text-decoration: underline; }
.email-display {
background: var(--background-color, #f5f7fa);
padding: 12px 16px;
border-radius: 8px;
color: var(--primary-color);
font-size: 14px;
display: flex;
/* Email badge */
.fp-email-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 14px;
font-weight: 500;
}
.fp-light .fp-email-badge { background: #f0f0f2; color: #667eea; }
.fp-dark .fp-email-badge {
background: rgba(102,126,234,0.1);
color: #8b9cf7;
border: 1px solid rgba(255,255,255,0.08);
}
.next-button,
.reset-button {
margin-top: 8px;
height: 44px !important;
font-size: 16px !important;
border-radius: 8px !important;
background: var(--primary-gradient) !important;
border: none !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
/* Password strength */
.fp-strength { display: flex; gap: 4px; margin-top: -8px; margin-bottom: 12px; }
.fp-strength-bar {
flex: 1; height: 3px; border-radius: 2px;
transition: all 0.3s;
}
.next-button:hover,
.reset-button:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important;
.fp-light .fp-strength-bar { background: #e5e5ea; }
.fp-dark .fp-strength-bar { background: rgba(255,255,255,0.08); }
.fp-strength-bar.s-weak { background: #ff3b30; }
.fp-strength-bar.s-medium { background: #ff9500; }
.fp-strength-bar.s-strong { background: #34d399; }
.fp-strength-text {
font-size: 12px; margin-top: 4px; margin-bottom: 12px; font-weight: 500;
}
.fp-light .fp-strength-text { color: #86868b; }
.fp-dark .fp-strength-text { color: rgba(255,255,255,0.35); }
/* 暗色模式覆盖 */
[data-theme="dark"] .forgot-password-page {
background: linear-gradient(135deg, #3730a3 0%, #581c87 50%, #831843 100%);
/* Password rules */
.fp-rules {
padding: 14px 16px;
border-radius: 12px;
margin-top: 10px;
margin-bottom: 8px;
}
.fp-light .fp-rules { background: #f8f8fa; border: 1px solid #e5e5ea; }
.fp-dark .fp-rules { background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); }
.fp-rules p { font-size: 13px; line-height: 2; }
.fp-light .fp-rules p { color: #86868b; }
.fp-dark .fp-rules p { color: rgba(255,255,255,0.35); }
.fp-rules strong { font-weight: 600; }
.fp-light .fp-rules strong { color: #1d1d1f; }
.fp-dark .fp-rules strong { color: rgba(255,255,255,0.6); }
[data-theme="dark"] .forgot-password-page::before {
background: radial-gradient(circle, rgba(255, 255, 255, 0.06) 0%, transparent 60%);
/* ===== Success ===== */
.fp-success {
display: none;
text-align: center;
padding: 48px 0;
}
[data-theme="dark"] .forgot-password-card {
background: var(--card-background);
border: 1px solid var(--border-color);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
.fp-success.active {
display: block;
animation: fpFadeSlide 0.4s ease;
}
[data-theme="dark"] .forgot-password-header .back-button {
color: var(--text-secondary);
.fp-success-check {
width: 72px; height: 72px; border-radius: 50%;
display: inline-flex; align-items: center; justify-content: center;
font-size: 36px; margin-bottom: 20px;
}
.fp-light .fp-success-check { background: #f0fff4; }
.fp-dark .fp-success-check { background: rgba(52,211,153,0.1); }
.fp-success h2 { font-size: 22px; font-weight: 800; margin-bottom: 8px; }
.fp-light .fp-success h2 { color: #1d1d1f; }
.fp-dark .fp-success h2 { color: #fff; }
.fp-success p { font-size: 15px; margin-bottom: 32px; }
.fp-light .fp-success p { color: #86868b; }
.fp-dark .fp-success p { color: rgba(255,255,255,0.4); }
[data-theme="dark"] .forgot-password-header h2 {
color: var(--text-primary);
/* ===== Divider ===== */
.fp-divider {
display: flex; align-items: center; gap: 16px;
margin: 24px 0 20px; width: 100%; padding: 0 4px;
}
[data-theme="dark"] .input-group label {
color: var(--text-secondary);
.fp-divider::before,
.fp-divider::after {
content: ''; flex: 1; height: 0.5px;
}
[data-theme="dark"] .email-display {
background: rgba(129, 140, 248, 0.1);
color: var(--primary-color);
border: 1px solid var(--border-color);
.fp-light .fp-divider::before,
.fp-light .fp-divider::after { background: #d2d2d7; }
.fp-dark .fp-divider::before,
.fp-dark .fp-divider::after { background: rgba(255,255,255,0.08); }
.fp-divider span {
font-size: 13px; color: #86868b; white-space: nowrap; font-weight: 500;
}
.fp-dark .fp-divider span { color: rgba(255,255,255,0.3); }
[data-theme="dark"] .next-button,
[data-theme="dark"] .reset-button {
background: var(--primary-gradient) !important;
box-shadow: 0 4px 15px rgba(129, 140, 248, 0.3) !important;
/* ===== Agreement ===== */
.fp-agreement {
text-align: center; font-size: 12px; line-height: 1.7;
padding: 16px 20px 32px; margin-top: 12px;
}
[data-theme="dark"] .next-button:hover,
[data-theme="dark"] .reset-button:hover {
box-shadow: 0 6px 20px rgba(129, 140, 248, 0.45) !important;
.fp-light .fp-agreement { color: #aeaeb2; }
.fp-dark .fp-agreement { color: rgba(255,255,255,0.2); }
.fp-agree-link {
background: none; border: none; font-size: 12px;
cursor: pointer; padding: 0; font-weight: 500; font-family: inherit;
}
.fp-light .fp-agree-link { color: #8e8e93; }
.fp-dark .fp-agree-link { color: rgba(255,255,255,0.35); }
/* ===== Responsive ===== */
@media (max-width: 480px) {
.forgot-password-card {
padding: 24px 20px;
max-width: 100%;
.fp-content {
padding-left: 24px !important;
padding-right: 24px !important;
}
.forgot-password-header h2 {
font-size: 18px;
}
.forgot-password-steps {
font-size: 13px;
margin-bottom: 24px;
}
.step-content {
gap: 16px;
}
.input-group label {
font-size: 13px;
}
.email-display {
font-size: 13px;
padding: 10px 14px;
}
.next-button,
.reset-button {
min-height: 44px;
font-size: 14px !important;
white-space: normal;
line-height: 1.3;
}
}
@media (max-width: 768px) {
.forgot-password-card {
max-width: 100%;
}
}
}
+169 -131
View File
@@ -1,16 +1,19 @@
import React, { useState } from "react";
import { Input, Button, Space, Steps, App as AntdApp } from "antd";
import { MailOutlined, LockOutlined, SafetyOutlined, ArrowLeftOutlined } from "@ant-design/icons";
import React, { useState } from "react";
import { App as AntdApp } from "antd";
import { useNavigate } from "react-router-dom";
import { api_request } from "@/utils/request";
import CaptchaInput from "@/components/CaptchaInput/CaptchaInput";
import { useTranslation } from 'react-i18next';
import { useTheme } from "@/contexts/ThemeContext";
import "./ForgotPassword.css";
const ForgotPassword: React.FC = () => {
const { t } = useTranslation();
const { message } = AntdApp.useApp();
const navigate = useNavigate();
const { isDark } = useTheme();
const themeClass = isDark ? "fp-dark" : "fp-light";
const [step, setStep] = useState(0);
const [email, setEmail] = useState("");
const [code, setCode] = useState("");
@@ -21,19 +24,21 @@ const ForgotPassword: React.FC = () => {
const [loading, setLoading] = useState(false);
const [captcha, setCaptcha] = useState({ captcha_key: "", captcha_code: "" });
const [showCaptcha, setShowCaptcha] = useState(false);
const [passwordStrength, setPasswordStrength] = useState<{ level: number; text: string; color: string }>({
level: 0, text: "", color: "",
});
const [resetSuccess, setResetSuccess] = useState(false);
const handleSendCode = async () => {
if (!email) {
message.warning(t('forgotPassword.warnEmailRequired'));
return;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
message.warning(t('forgotPassword.warnEmailFormat'));
return;
}
if (showCaptcha && !captcha.captcha_code) {
message.warning(t('forgotPassword.warnImageCaptchaRequired'));
return;
@@ -54,10 +59,7 @@ const ForgotPassword: React.FC = () => {
setShowCaptcha(false);
const intervalId = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(intervalId);
return 0;
}
if (prev <= 1) { clearInterval(intervalId); return 0; }
return prev - 1;
});
}, 1000);
@@ -86,33 +88,17 @@ const ForgotPassword: React.FC = () => {
};
const handleResetPassword = async () => {
if (!code) {
message.warning(t('forgotPassword.warnCodeRequired'));
return;
}
if (!newPassword) {
message.warning(t('forgotPassword.warnPasswordRequired'));
return;
}
if (newPassword !== confirmPassword) {
message.error(t('forgotPassword.errorPasswordMismatch'));
return;
}
if (newPassword.length < 8) {
message.error(t('forgotPassword.errorPasswordTooShort'));
return;
}
if (!code) { message.warning(t('forgotPassword.warnCodeRequired')); return; }
if (!newPassword) { message.warning(t('forgotPassword.warnPasswordRequired')); return; }
if (newPassword !== confirmPassword) { message.error(t('forgotPassword.errorPasswordMismatch')); return; }
if (newPassword.length < 8) { message.error(t('forgotPassword.errorPasswordTooShort')); return; }
const hasLetter = /[a-zA-Z]/.test(newPassword);
const hasDigit = /\d/.test(newPassword);
if (!hasLetter || !hasDigit) {
message.error(t('forgotPassword.errorPasswordFormat'));
return;
}
if (!hasLetter || !hasDigit) { message.error(t('forgotPassword.errorPasswordFormat')); return; }
setLoading(true);
try {
const params: any = {
email,
code,
email, code,
new_password: newPassword,
confirm_password: confirmPassword,
};
@@ -122,9 +108,8 @@ const ForgotPassword: React.FC = () => {
}
const res: any = await api_request.login.auth.forgot_password_reset(params);
if (res?.code === 10010) {
message.success(t('forgotPassword.successReset'));
setShowCaptcha(false);
navigate("/");
setResetSuccess(true);
setStep(2);
} else {
message.error(res?.message || t('forgotPassword.errorReset'));
}
@@ -144,147 +129,200 @@ const ForgotPassword: React.FC = () => {
const handleKeyEnter = () => {
if (loading) return;
if (step === 0) {
// 未发送验证码时回车=发送验证码;已发送时回车=下一步
if (codeSent) {
handleNextStep();
} else {
handleSendCode();
}
} else {
codeSent ? handleNextStep() : handleSendCode();
} else if (step === 1) {
handleResetPassword();
}
};
const checkPasswordStrength = (pwd: string) => {
if (!pwd) { setPasswordStrength({ level: 0, text: "", color: "" }); return; }
let score = 0;
if (pwd.length >= 8) score++;
if (/[a-zA-Z]/.test(pwd) && /\d/.test(pwd)) score++;
if (pwd.length >= 12 && /[A-Z]/.test(pwd)) score++;
if (score <= 1) setPasswordStrength({ level: 1, text: t('forgotPassword.strengthWeak') || '弱 - 建议增加字符', color: '#ff3b30' });
else if (score === 2) setPasswordStrength({ level: 2, text: t('forgotPassword.strengthMedium') || '中等 - 不错', color: '#ff9500' });
else setPasswordStrength({ level: 3, text: t('forgotPassword.strengthStrong') || '强 - 安全', color: '#34d399' });
};
const getStrengthBarClass = (index: number) => {
if (passwordStrength.level === 0) return "";
if (index >= passwordStrength.level) return "";
if (passwordStrength.level === 1) return "s-weak";
if (passwordStrength.level === 2) return "s-medium";
return "s-strong";
};
return (
<div className="forgot-password-page">
<div className="forgot-password-card">
<div className="forgot-password-header">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => step === 0 ? navigate("/") : setStep(0)}
className="back-button"
/>
<h2>{t('forgotPassword.title')}</h2>
<div className={`fp ${themeClass}`}>
<div className="fp-content">
{/* Header */}
<div className="fp-header">
<div className="fp-logo">🥤</div>
<h1 className="fp-title">{t('forgotPassword.title')}</h1>
<p className="fp-subtitle">{t('forgotPassword.subtitle') || '别担心,几步就能重置密码'}</p>
</div>
<Steps
current={step}
size="small"
className="forgot-password-steps"
items={[
{ title: t('forgotPassword.step1'), icon: <MailOutlined /> },
{ title: t('forgotPassword.step2'), icon: <LockOutlined /> },
]}
/>
{/* Segmented Steps */}
{!resetSuccess && (
<div className="fp-steps-track">
<div className="fp-steps-slide" data-pos={step} />
<div className={`fp-step-btn ${step === 0 ? 'active' : step > 0 ? 'done' : ''}`}>
<div className="fp-step-num">{step > 0 ? '✓' : '1'}</div>
<div className="fp-step-label">{t('forgotPassword.step1')}</div>
</div>
<div className={`fp-step-btn ${step === 1 ? 'active' : step > 1 ? 'done' : ''}`}>
<div className="fp-step-num">{step > 1 ? '✓' : '2'}</div>
<div className="fp-step-label">{t('forgotPassword.step2')}</div>
</div>
</div>
)}
{step === 0 ? (
<div className="step-content">
<div className="input-group">
{/* Step 0: Verify identity */}
{step === 0 && (
<div className="fp-step-content active">
<div className="fp-field">
<label>{t('forgotPassword.emailLabel')}</label>
<Input
size="large"
prefix={<MailOutlined />}
<input
className="fp-input"
type="email"
placeholder={t('forgotPassword.emailPlaceholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
onPressEnter={handleKeyEnter}
placeholder={t('forgotPassword.emailPlaceholder')}
onKeyDown={(e) => e.key === 'Enter' && handleKeyEnter()}
/>
</div>
<div className="input-group">
<label>{t('forgotPassword.codeLabel')}</label>
<Space.Compact style={{ width: "100%" }}>
<Input
size="large"
prefix={<SafetyOutlined />}
value={code}
onChange={(e) => setCode(e.target.value)}
onPressEnter={handleKeyEnter}
placeholder={t('forgotPassword.codePlaceholder')}
maxLength={8}
/>
<Button
size="large"
disabled={countdown > 0}
loading={loading && !codeSent}
onClick={handleSendCode}
style={{ whiteSpace: "nowrap" }}
>
{countdown > 0 ? `${countdown}s` : codeSent ? t('forgotPassword.resend') : t('forgotPassword.sendCode')}
</Button>
</Space.Compact>
<div className="fp-code-row">
<input
className="fp-input"
type="text"
placeholder={t('forgotPassword.codePlaceholder')}
value={code}
onChange={(e) => setCode(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleKeyEnter()}
maxLength={8}
/>
<button
className="fp-code-btn"
onClick={handleSendCode}
disabled={countdown > 0 || loading}
>
{countdown > 0 ? `${countdown}s` : codeSent ? t('forgotPassword.resend') : t('forgotPassword.sendCode')}
</button>
</div>
{showCaptcha && (
<div className="input-group">
<div className="fp-field">
<label>{t('forgotPassword.imageCaptchaLabel')}</label>
<CaptchaInput value={captcha} onChange={setCaptcha} />
</div>
)}
<Button
type="primary"
size="large"
block
disabled={!codeSent}
<button
className="fp-btn-primary"
onClick={handleNextStep}
className="next-button"
disabled={!codeSent}
>
{t('forgotPassword.nextStep')}
</Button>
</button>
<div className="fp-divider"><span>或</span></div>
<button className="fp-btn-ghost" onClick={() => navigate("/")}>
{t('forgotPassword.backToLogin') || '返回登录'}
</button>
</div>
) : (
<div className="step-content">
<div className="email-display">
<MailOutlined /> {email}
</div>
<div className="input-group">
)}
{/* Step 1: Reset password */}
{step === 1 && (
<div className="fp-step-content active">
<button className="fp-back-link" onClick={() => setStep(0)}>
← {t('forgotPassword.backToVerify') || '返回验证'}
</button>
<div className="fp-email-badge">✉️ {email}</div>
<div className="fp-field">
<label>{t('forgotPassword.codeLabel')}</label>
<Input
size="large"
prefix={<SafetyOutlined />}
<input
className="fp-input"
type="text"
placeholder={t('forgotPassword.codePlaceholder')}
value={code}
onChange={(e) => setCode(e.target.value)}
onPressEnter={handleKeyEnter}
placeholder={t('forgotPassword.codePlaceholder')}
onKeyDown={(e) => e.key === 'Enter' && handleKeyEnter()}
maxLength={8}
/>
</div>
<div className="input-group">
<div className="fp-field">
<label>{t('forgotPassword.newPasswordLabel')}</label>
<Input.Password
size="large"
prefix={<LockOutlined />}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
onPressEnter={handleKeyEnter}
<input
className="fp-input"
type="password"
placeholder={t('forgotPassword.newPasswordPlaceholder')}
value={newPassword}
onChange={(e) => { setNewPassword(e.target.value); checkPasswordStrength(e.target.value); }}
onKeyDown={(e) => e.key === 'Enter' && handleKeyEnter()}
/>
<div className="fp-strength">
{[0, 1, 2].map((i) => (
<div key={i} className={`fp-strength-bar ${getStrengthBarClass(i)}`} />
))}
</div>
{passwordStrength.text && (
<div className="fp-strength-text" style={{ color: passwordStrength.color }}>
{passwordStrength.text}
</div>
)}
</div>
<div className="input-group">
<div className="fp-field">
<label>{t('forgotPassword.confirmPasswordLabel')}</label>
<Input.Password
size="large"
prefix={<LockOutlined />}
<input
className="fp-input"
type="password"
placeholder={t('forgotPassword.confirmPasswordPlaceholder')}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onPressEnter={handleKeyEnter}
placeholder={t('forgotPassword.confirmPasswordPlaceholder')}
onKeyDown={(e) => e.key === 'Enter' && handleResetPassword()}
/>
</div>
<Button
type="primary"
size="large"
block
loading={loading}
<div className="fp-rules">
<p><strong>•</strong> {t('forgotPassword.ruleMin8') || '至少8个字符'}</p>
<p><strong>•</strong> {t('forgotPassword.ruleLetterNumber') || '包含字母和数字'}</p>
<p><strong>•</strong> {t('forgotPassword.ruleSuggest12') || '建议12位以上更安全'}</p>
</div>
<button
className="fp-btn-primary"
onClick={handleResetPassword}
className="reset-button"
disabled={loading}
>
{t('forgotPassword.resetPassword')}
</Button>
{loading ? '...' : t('forgotPassword.resetPassword')}
</button>
</div>
)}
{/* Step 2: Success */}
{step === 2 && resetSuccess && (
<div className="fp-success active">
<div className="fp-success-check">✅</div>
<h2>{t('forgotPassword.successTitle') || '密码重置成功!'}</h2>
<p>{t('forgotPassword.successDesc') || '你的密码已更新,现在可以使用新密码登录了'}</p>
<button className="fp-btn-primary" onClick={() => navigate("/")}>
{t('forgotPassword.backToLogin') || '返回登录'}
</button>
</div>
)}
{/* Agreement */}
<div className="fp-agreement">
{t('forgotPassword.agreementPrefix') || '继续即表示你同意'}
<button className="fp-agree-link" onClick={() => navigate("/agreement")}>
{t('forgotPassword.userAgreement') || '用户协议'}
</button>
{t('forgotPassword.agreementAnd') || '和'}
<button className="fp-agree-link" onClick={() => navigate("/privacy")}>
{t('forgotPassword.privacyPolicy') || '隐私政策'}
</button>
</div>
</div>
</div>
);
};
export default ForgotPassword;
export default ForgotPassword;