| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 1 | // src/pages/LoginPage/LoginPage.jsx |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 2 | |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 3 | import React, { useState, useEffect } from 'react' |
| 4 | import { useNavigate, Link } from 'react-router-dom' |
| 5 | import { Input, Checkbox, Modal, Alert } from 'antd' |
| 6 | import { |
| 7 | MailOutlined, |
| 8 | LockOutlined, |
| 9 | ExclamationCircleOutlined, |
| 10 | CheckCircleOutlined |
| 11 | } from '@ant-design/icons' |
| 12 | import { |
| 13 | getRememberedLoginInfo, |
| 14 | saveRememberedLoginInfo, |
| 15 | saveAuthInfo, |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 16 | isLoggedIn, |
| 17 | clearAuthInfo // ← 新增 |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 18 | } from '../../utils/auth' |
| 19 | import { hashPassword } from '../../utils/crypto' |
| 20 | import './LoginPage.css' |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 21 | |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 22 | const baseURL = 'http://10.126.59.25:8082' |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 23 | |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 24 | export default function LoginPage() { |
| 25 | const navigate = useNavigate() |
| 26 | |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 27 | // —— 登录页加载时先清除旧的认证信息 —— |
| 28 | useEffect(() => { |
| 29 | clearAuthInfo(/* clearRemembered= */ false) |
| 30 | }, []) |
| 31 | |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 32 | const [formData, setFormData] = useState({ |
| 33 | email: '', |
| 34 | password: '' |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 35 | }) |
| 36 | const [rememberMe, setRememberMe] = useState(false) |
| 37 | const [isLoading, setIsLoading] = useState(false) |
| 38 | const [errors, setErrors] = useState({ email: '', password: '' }) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 39 | const [errorModal, setErrorModal] = useState({ |
| 40 | visible: false, |
| 41 | title: '', |
| 42 | content: '' |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 43 | }) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 44 | const [successAlert, setSuccessAlert] = useState({ |
| 45 | visible: false, |
| 46 | message: '' |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 47 | }) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 48 | |
| 49 | // 显示错误弹窗 |
| 50 | const showErrorModal = (title, content) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 51 | setErrorModal({ visible: true, title, content }) |
| 52 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 53 | // 关闭错误弹窗 |
| 54 | const closeErrorModal = () => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 55 | setErrorModal({ visible: false, title: '', content: '' }) |
| 56 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 57 | // 显示成功提示 |
| 58 | const showSuccessAlert = (message) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 59 | setSuccessAlert({ visible: true, message }) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 60 | setTimeout(() => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 61 | setSuccessAlert({ visible: false, message: '' }) |
| 62 | }, 3000) |
| 63 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 64 | |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 65 | // 初始化:检查登录 & 填充“记住我” |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 66 | useEffect(() => { |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 67 | if (isLoggedIn()) { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 68 | console.log('用户已登录') |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 69 | // 如果想自动跳转: navigate('/home', { replace: true }) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 70 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 71 | const { email, password, rememberMe } = getRememberedLoginInfo() |
| 72 | if (rememberMe && email) { |
| 73 | setFormData({ email, password }) |
| 74 | setRememberMe(true) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 75 | } |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 76 | }, []) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 77 | |
| 78 | const handleEmailChange = (e) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 79 | setFormData(f => ({ ...f, email: e.target.value })) |
| 80 | if (errors.email) setErrors(e => ({ ...e, email: '' })) |
| 81 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 82 | const handlePasswordChange = (e) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 83 | setFormData(f => ({ ...f, password: e.target.value })) |
| 84 | if (errors.password) setErrors(e => ({ ...e, password: '' })) |
| 85 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 86 | const handleRememberMeChange = (e) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 87 | const checked = e.target.checked |
| 88 | setRememberMe(checked) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 89 | if (!checked) { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 90 | saveRememberedLoginInfo('', '', false) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 91 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 92 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 93 | |
| 94 | const validateForm = () => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 95 | const newErr = { email: '', password: '' } |
| 96 | let hasError = false |
| 97 | if (!formData.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { |
| 98 | newErr.email = '请输入有效的邮箱地址' |
| 99 | hasError = true |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 100 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 101 | if (!formData.password.trim() || formData.password.length < 6) { |
| 102 | newErr.password = '密码长度至少6位' |
| 103 | hasError = true |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 104 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 105 | setErrors(newErr) |
| 106 | return !hasError |
| 107 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 108 | |
| 109 | const handleSubmit = async (e) => { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 110 | e.preventDefault() |
| 111 | if (!validateForm()) return |
| 112 | |
| 113 | setIsLoading(true) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 114 | try { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 115 | const res = await fetch(baseURL + '/login', { |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 116 | method: 'POST', |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 117 | headers: { 'Content-Type': 'application/json' }, |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 118 | body: JSON.stringify({ |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 119 | email: formData.email, |
| 120 | password: hashPassword(formData.password) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 121 | }) |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 122 | }) |
| 123 | const result = await res.json() |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 124 | if (result.success) { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 125 | showSuccessAlert('登录成功!正在跳转...') |
| 126 | saveAuthInfo(result.token, result.user, rememberMe) |
| 127 | saveRememberedLoginInfo(formData.email, formData.password, rememberMe) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 128 | setTimeout(() => { |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 129 | const uid = result.user.id |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 130 | switch (result.user.role) { |
| 131 | case 'admin': |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 132 | navigate(`/admin/${uid}`, { replace: true }) |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 133 | break |
| 134 | case 'superadmin': |
| wu | 70fc8c5 | 2025-06-19 15:55:03 +0800 | [diff] [blame^] | 135 | navigate(`/superadmin/${uid}/users`, { replace: true }) |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 136 | break |
| 137 | default: |
| 138 | navigate('/home', { replace: true }) |
| 139 | } |
| 140 | }, 1500) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 141 | } else { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 142 | let title = '登录失败' |
| 143 | let content = result.message || '登录失败,请检查您的邮箱和密码' |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 144 | if (result.message) { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 145 | if (/邮箱|email/.test(result.message)) { |
| 146 | title = '邮箱验证失败' |
| 147 | content = '请输入正确的邮箱地址' |
| 148 | } else if (/密码|password/.test(result.message)) { |
| 149 | title = '密码验证失败' |
| 150 | content = '密码不正确,请重试' |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 151 | } |
| 152 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 153 | showErrorModal(title, content) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 154 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 155 | } catch (err) { |
| 156 | console.error(err) |
| 157 | showErrorModal('网络异常', '无法连接到服务器,请稍后重试') |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 158 | } finally { |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 159 | setIsLoading(false) |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 160 | } |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 161 | } |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 162 | |
| 163 | return ( |
| 164 | <div className="login-container"> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 165 | <div className="login-background" /> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 166 | {isLoading && ( |
| 167 | <div className="loading-overlay"> |
| 168 | <div className="loading-content"> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 169 | <div className="loading-spinner-large" /> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 170 | <p className="loading-text">正在登录...</p> |
| 171 | </div> |
| 172 | </div> |
| 173 | )} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 174 | <div className="login-content"> |
| 175 | <div className="login-card"> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 176 | {successAlert.visible && ( |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 177 | <Alert |
| 178 | message={successAlert.message} |
| 179 | type="success" |
| 180 | icon={<CheckCircleOutlined />} |
| 181 | closable |
| 182 | style={{ marginBottom: 16, borderRadius: 8 }} |
| 183 | /> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 184 | )} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 185 | <div className="login-header"> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 186 | <h1>欢迎来到小红书</h1> |
| 187 | <p>标记我的生活</p> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 188 | </div> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 189 | <form className="login-form" onSubmit={handleSubmit}> |
| 190 | <div className="form-group"> |
| 191 | <Input |
| 192 | type="email" |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 193 | placeholder="邮箱" |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 194 | value={formData.email} |
| 195 | onChange={handleEmailChange} |
| 196 | prefix={<MailOutlined />} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 197 | status={errors.email ? 'error' : ''} |
| 198 | /> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 199 | {errors.email && <div className="error-message">{errors.email}</div>} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 200 | </div> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 201 | <div className="form-group"> |
| 202 | <Input.Password |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 203 | placeholder="密码" |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 204 | value={formData.password} |
| 205 | onChange={handlePasswordChange} |
| 206 | prefix={<LockOutlined />} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 207 | status={errors.password ? 'error' : ''} |
| 208 | /> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 209 | {errors.password && <div className="error-message">{errors.password}</div>} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 210 | </div> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 211 | <div className="form-options"> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 212 | <Checkbox checked={rememberMe} onChange={handleRememberMeChange}> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 213 | 记住我 |
| 214 | </Checkbox> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 215 | <Link to="/forgot-password">忘记密码?</Link> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 216 | </div> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 217 | <button |
| 218 | type="submit" |
| 219 | className={`login-button ${isLoading ? 'loading' : ''}`} |
| 220 | disabled={isLoading} |
| 221 | > |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 222 | {isLoading ? '登录中...' : '登录'} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 223 | </button> |
| 224 | </form> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 225 | <div className="signup-link"> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 226 | <p>还没有账户?<Link to="/register">立即注册</Link></p> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 227 | </div> |
| 228 | </div> |
| 229 | </div> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 230 | <Modal |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 231 | title={<><ExclamationCircleOutlined style={{ color: '#ff4d4f' }} /> {errorModal.title}</>} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 232 | open={errorModal.visible} |
| 233 | onOk={closeErrorModal} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 234 | cancelButtonProps={{ style: { display: 'none' } }} |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 235 | > |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 236 | <p>{errorModal.content}</p> |
| TRM-coding | c4b4f3d | 2025-06-18 19:02:46 +0800 | [diff] [blame] | 237 | </Modal> |
| 238 | </div> |
| wu | 2f28f67 | 2025-06-19 14:29:30 +0800 | [diff] [blame] | 239 | ) |
| 240 | } |