feat: mobile forgot-password redesign, navbar/search updates, vite proxy and request tweaks
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user