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