504 lines
16 KiB
React
504 lines
16 KiB
React
|
|
import React, { useState, useEffect } from 'react'
|
|||
|
|
import { APP_VERSION } from '../config/version'
|
|||
|
|
import { api } from '../utils/api'
|
|||
|
|
import { ErrorCodes } from '../utils/errorCodes'
|
|||
|
|
|
|||
|
|
export default function Login() {
|
|||
|
|
const [username, setUsername] = useState('')
|
|||
|
|
const [password, setPassword] = useState('')
|
|||
|
|
const [loading, setLoading] = useState(false)
|
|||
|
|
const [error, setError] = useState('')
|
|||
|
|
const [captcha, setCaptcha] = useState({ num1: 0, num2: 0, answer: '' })
|
|||
|
|
|
|||
|
|
// 注册相关状态
|
|||
|
|
const [showRegister, setShowRegister] = useState(false)
|
|||
|
|
const [registerData, setRegisterData] = useState({ username: '', password: '', confirmPassword: '', email: '' })
|
|||
|
|
const [registerLoading, setRegisterLoading] = useState(false)
|
|||
|
|
const [registerError, setRegisterError] = useState('')
|
|||
|
|
|
|||
|
|
// 生成验证码
|
|||
|
|
useEffect(() => {
|
|||
|
|
generateCaptcha()
|
|||
|
|
}, [])
|
|||
|
|
|
|||
|
|
const generateCaptcha = () => {
|
|||
|
|
const num1 = Math.floor(Math.random() * 10)
|
|||
|
|
const num2 = Math.floor(Math.random() * 10)
|
|||
|
|
setCaptcha({ num1, num2, answer: '' })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理注册
|
|||
|
|
const handleRegister = async () => {
|
|||
|
|
// 验证输入
|
|||
|
|
if (!registerData.username || !registerData.password) {
|
|||
|
|
setRegisterError('用户名和密码为必填项')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (registerData.username.length < 3) {
|
|||
|
|
setRegisterError('用户名至少 3 个字符')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (registerData.password.length < 6) {
|
|||
|
|
setRegisterError('密码至少 6 个字符')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (registerData.password !== registerData.confirmPassword) {
|
|||
|
|
setRegisterError('两次输入的密码不一致')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setRegisterLoading(true)
|
|||
|
|
setRegisterError('')
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const payload = {
|
|||
|
|
username: registerData.username,
|
|||
|
|
password: registerData.password
|
|||
|
|
}
|
|||
|
|
if (registerData.email) {
|
|||
|
|
payload.email = registerData.email
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const res = await fetch('/api/auth/register', {
|
|||
|
|
method: 'POST',
|
|||
|
|
headers: {
|
|||
|
|
'Content-Type': 'application/json'
|
|||
|
|
},
|
|||
|
|
body: JSON.stringify(payload)
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const data = await res.json()
|
|||
|
|
|
|||
|
|
if (!res.ok) {
|
|||
|
|
throw new Error(data.error?.message || '注册失败')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
alert('注册成功!请登录')
|
|||
|
|
setShowRegister(false)
|
|||
|
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '' })
|
|||
|
|
generateCaptcha()
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('注册错误:', err)
|
|||
|
|
setRegisterError(err.message || '注册失败')
|
|||
|
|
} finally {
|
|||
|
|
setRegisterLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleLogin = async () => {
|
|||
|
|
// 验证输入
|
|||
|
|
if (!username || !password) {
|
|||
|
|
setError(ErrorCodes.E00020.message)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (username.length < 3) {
|
|||
|
|
setError(ErrorCodes.E00021.message)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (password.length < 6) {
|
|||
|
|
setError(ErrorCodes.E00022.message)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 验证验证码
|
|||
|
|
const expectedAnswer = captcha.num1 + captcha.num2
|
|||
|
|
if (!captcha.answer || Number(captcha.answer) !== expectedAnswer) {
|
|||
|
|
setError(ErrorCodes.E00012.message)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setLoading(true)
|
|||
|
|
setError('')
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
console.log('开始登录...')
|
|||
|
|
const loginData = await api.auth.login(username, password)
|
|||
|
|
console.log('登录响应:', loginData)
|
|||
|
|
|
|||
|
|
if (!loginData.access_token) {
|
|||
|
|
throw new Error('登录响应中没有 access_token')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
localStorage.setItem('token', loginData.access_token)
|
|||
|
|
console.log('Token 已保存')
|
|||
|
|
|
|||
|
|
const userData = await api.user.me()
|
|||
|
|
console.log('用户信息:', userData)
|
|||
|
|
|
|||
|
|
localStorage.setItem('user', JSON.stringify(userData))
|
|||
|
|
console.log('用户信息已保存')
|
|||
|
|
|
|||
|
|
// 强制刷新页面,确保 React 状态更新
|
|||
|
|
window.location.href = window.location.origin + window.location.pathname + '#/'
|
|||
|
|
console.log('跳转首页')
|
|||
|
|
} catch (err) {
|
|||
|
|
console.error('登录错误:', err)
|
|||
|
|
const errorCode = err.code || 'E00000'
|
|||
|
|
const errorMsg = err.message || '登录失败'
|
|||
|
|
setError(`${errorCode}: ${errorMsg}`)
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div style={{
|
|||
|
|
minHeight: '100vh',
|
|||
|
|
background: 'linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%)',
|
|||
|
|
display: 'flex',
|
|||
|
|
flexDirection: 'column',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
padding: '20px'
|
|||
|
|
}}>
|
|||
|
|
{/* Logo - 龙型 + 甲辰收藏文字 (橙色圆形设计) */}
|
|||
|
|
<div style={{ marginBottom: '40px', textAlign: 'center' }}>
|
|||
|
|
<img
|
|||
|
|
src="/static/images/jiachenlong-logo.png?v=1"
|
|||
|
|
alt="甲辰收藏"
|
|||
|
|
style={{
|
|||
|
|
width: '200px',
|
|||
|
|
height: '200px',
|
|||
|
|
marginBottom: '12px',
|
|||
|
|
borderRadius: '50%',
|
|||
|
|
boxShadow: '0 0 40px rgba(251, 191, 36, 0.4)',
|
|||
|
|
background: '#fff'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
<h1 style={{ color: '#fbbf24', fontSize: '28px', fontWeight: '700', margin: '16px 0 8px' }}>甲辰收藏</h1>
|
|||
|
|
<p style={{ color: 'rgba(255,255,255,0.6)', fontSize: '14px', margin: 0 }}>生肖纪念钞管理系统</p>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 登录表单 */}
|
|||
|
|
<div style={{
|
|||
|
|
width: '100%',
|
|||
|
|
maxWidth: '320px',
|
|||
|
|
background: 'rgba(30, 41, 59, 0.8)',
|
|||
|
|
borderRadius: '16px',
|
|||
|
|
padding: '24px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.1)'
|
|||
|
|
}}>
|
|||
|
|
<h2 style={{ color: '#fff', fontSize: '20px', fontWeight: '600', marginBottom: '24px', textAlign: 'center' }}>
|
|||
|
|
欢迎回来 👋
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
{error && (
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(239, 68, 68, 0.2)',
|
|||
|
|
border: '1px solid rgba(239, 68, 68, 0.5)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
padding: '12px',
|
|||
|
|
marginBottom: '16px',
|
|||
|
|
color: '#fca5a5',
|
|||
|
|
fontSize: '13px'
|
|||
|
|
}}>
|
|||
|
|
⚠️ {error}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 用户名 */}
|
|||
|
|
<div style={{ marginBottom: '16px' }}>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={username}
|
|||
|
|
onChange={(e) => setUsername(e.target.value)}
|
|||
|
|
placeholder="用户名"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 密码 */}
|
|||
|
|
<div style={{ marginBottom: '20px' }}>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={password}
|
|||
|
|
onChange={(e) => setPassword(e.target.value)}
|
|||
|
|
placeholder="密码"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 验证码 */}
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
|||
|
|
<div onClick={generateCaptcha} style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fbbf24',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
fontWeight: '700',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
userSelect: 'none'
|
|||
|
|
}}>
|
|||
|
|
{captcha.num1} + {captcha.num2} = ?
|
|||
|
|
</div>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={captcha.answer}
|
|||
|
|
onChange={(e) => setCaptcha(prev => ({ ...prev, answer: e.target.value }))}
|
|||
|
|
placeholder="?"
|
|||
|
|
style={{
|
|||
|
|
width: '80px',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
outline: 'none',
|
|||
|
|
textAlign: 'center',
|
|||
|
|
fontWeight: '700'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 登录按钮 */}
|
|||
|
|
<button
|
|||
|
|
onClick={handleLogin}
|
|||
|
|
disabled={loading}
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '16px',
|
|||
|
|
borderRadius: '12px',
|
|||
|
|
border: 'none',
|
|||
|
|
background: loading ? '#f59e0b' : 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)',
|
|||
|
|
color: '#0f172a',
|
|||
|
|
fontSize: '18px',
|
|||
|
|
fontWeight: '700',
|
|||
|
|
cursor: loading ? 'not-allowed' : 'pointer',
|
|||
|
|
boxShadow: '0 4px 15px rgba(251, 191, 36, 0.3)'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{loading ? '登录中...' : '登录'}
|
|||
|
|
</button>
|
|||
|
|
|
|||
|
|
{/* 注册链接 */}
|
|||
|
|
<div style={{ textAlign: 'center', marginTop: '16px', display: 'flex', justifyContent: 'center', gap: '16px' }}>
|
|||
|
|
<span
|
|||
|
|
onClick={() => setShowRegister(true)}
|
|||
|
|
style={{
|
|||
|
|
color: '#fbbf24',
|
|||
|
|
fontSize: '14px',
|
|||
|
|
cursor: 'pointer',
|
|||
|
|
textDecoration: 'underline'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
注册新账号
|
|||
|
|
</span>
|
|||
|
|
<span style={{ color: 'rgba(255,255,255,0.3)' }}>|</span>
|
|||
|
|
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: '14px' }}>
|
|||
|
|
需要帮助?联系管理员
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 版本号 */}
|
|||
|
|
<div style={{
|
|||
|
|
position: 'fixed',
|
|||
|
|
bottom: '20px',
|
|||
|
|
color: 'rgba(251, 191, 36, 0.5)',
|
|||
|
|
fontSize: '12px',
|
|||
|
|
textAlign: 'center',
|
|||
|
|
fontWeight: '600'
|
|||
|
|
}}>
|
|||
|
|
v{APP_VERSION}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 注册弹窗 */}
|
|||
|
|
{showRegister && (
|
|||
|
|
<div style={{
|
|||
|
|
position: 'fixed',
|
|||
|
|
top: 0,
|
|||
|
|
left: 0,
|
|||
|
|
right: 0,
|
|||
|
|
bottom: 0,
|
|||
|
|
background: 'rgba(0,0,0,0.8)',
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
justifyContent: 'center',
|
|||
|
|
padding: '20px',
|
|||
|
|
zIndex: 1000
|
|||
|
|
}}>
|
|||
|
|
<div style={{
|
|||
|
|
width: '100%',
|
|||
|
|
maxWidth: '320px',
|
|||
|
|
background: 'rgba(30, 41, 59, 0.95)',
|
|||
|
|
borderRadius: '16px',
|
|||
|
|
padding: '24px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.1)'
|
|||
|
|
}}>
|
|||
|
|
<h2 style={{ color: '#fbbf24', fontSize: '20px', fontWeight: '600', marginBottom: '20px', textAlign: 'center' }}>
|
|||
|
|
注册新账号 🎉
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
{registerError && (
|
|||
|
|
<div style={{
|
|||
|
|
background: 'rgba(239, 68, 68, 0.2)',
|
|||
|
|
border: '1px solid rgba(239, 68, 68, 0.5)',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
padding: '12px',
|
|||
|
|
marginBottom: '16px',
|
|||
|
|
color: '#fca5a5',
|
|||
|
|
fontSize: '13px'
|
|||
|
|
}}>
|
|||
|
|
⚠️ {registerError}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{/* 用户名 */}
|
|||
|
|
<div style={{ marginBottom: '16px' }}>
|
|||
|
|
<input
|
|||
|
|
type="text"
|
|||
|
|
value={registerData.username}
|
|||
|
|
onChange={(e) => setRegisterData(prev => ({ ...prev, username: e.target.value }))}
|
|||
|
|
placeholder="用户名(至少 3 个字符)"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 邮箱(可选) */}
|
|||
|
|
<div style={{ marginBottom: '16px' }}>
|
|||
|
|
<input
|
|||
|
|
type="email"
|
|||
|
|
value={registerData.email}
|
|||
|
|
onChange={(e) => setRegisterData(prev => ({ ...prev, email: e.target.value }))}
|
|||
|
|
placeholder="邮箱(可选)"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 密码 */}
|
|||
|
|
<div style={{ marginBottom: '16px' }}>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={registerData.password}
|
|||
|
|
onChange={(e) => setRegisterData(prev => ({ ...prev, password: e.target.value }))}
|
|||
|
|
placeholder="密码(至少 6 个字符)"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 确认密码 */}
|
|||
|
|
<div style={{ marginBottom: '24px' }}>
|
|||
|
|
<input
|
|||
|
|
type="password"
|
|||
|
|
value={registerData.confirmPassword}
|
|||
|
|
onChange={(e) => setRegisterData(prev => ({ ...prev, confirmPassword: e.target.value }))}
|
|||
|
|
placeholder="确认密码"
|
|||
|
|
style={{
|
|||
|
|
width: '100%',
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: '1px solid rgba(255,255,255,0.2)',
|
|||
|
|
background: 'rgba(255,255,255,0.05)',
|
|||
|
|
color: '#fff',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
outline: 'none'
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 按钮 */}
|
|||
|
|
<div style={{ display: 'flex', gap: '12px' }}>
|
|||
|
|
<button
|
|||
|
|
onClick={() => {
|
|||
|
|
setShowRegister(false)
|
|||
|
|
setRegisterData({ username: '', password: '', confirmPassword: '', email: '' })
|
|||
|
|
setRegisterError('')
|
|||
|
|
}}
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: 'none',
|
|||
|
|
background: 'rgba(148, 163, 184, 0.2)',
|
|||
|
|
color: '#94a3b8',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
取消
|
|||
|
|
</button>
|
|||
|
|
<button
|
|||
|
|
onClick={handleRegister}
|
|||
|
|
disabled={registerLoading}
|
|||
|
|
style={{
|
|||
|
|
flex: 1,
|
|||
|
|
padding: '14px',
|
|||
|
|
borderRadius: '8px',
|
|||
|
|
border: 'none',
|
|||
|
|
background: registerLoading ? '#f59e0b' : 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%)',
|
|||
|
|
color: '#0f172a',
|
|||
|
|
fontSize: '16px',
|
|||
|
|
fontWeight: '600',
|
|||
|
|
cursor: registerLoading ? 'not-allowed' : 'pointer'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{registerLoading ? '注册中...' : '注册'}
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|