jiachenlong/frontend/src.bak/pages/Login.jsx

672 lines
21 KiB
React
Raw Normal View History

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({ agreeTerms: false,
2026-03-20 15:03:26 +08:00
username: '',
password: '',
confirmPassword: '',
email: '',
phone: '',
verifyCode: ''
})
const [registerLoading, setRegisterLoading] = useState(false)
const [registerError, setRegisterError] = useState('')
2026-03-20 15:03:26 +08:00
// 短信获取相关
const [sendingCode, setSendingCode] = useState(false)
const [codeCountdown, setCodeCountdown] = useState(0)
const [codeSent, setCodeSent] = useState(false)
2026-03-20 15:03:26 +08:00
// 生成获取
useEffect(() => {
generateCaptcha()
}, [])
2026-03-20 15:03:26 +08:00
// 获取倒计时
useEffect(() => {
if (codeCountdown > 0) {
const timer = setTimeout(() => setCodeCountdown(codeCountdown - 1), 1000)
return () => clearTimeout(timer)
}
}, [codeCountdown])
const generateCaptcha = () => {
const num1 = Math.floor(Math.random() * 10)
const num2 = Math.floor(Math.random() * 10)
setCaptcha({ num1, num2, answer: '' })
}
2026-03-20 15:03:26 +08:00
// 发送短信获取
const handleSendCode = async () => {
if (!registerData.phone) {
setRegisterError('请先输入手机号')
return
}
// 验证手机号格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(registerData.phone)) {
setRegisterError('请输入正确的手机号')
return
}
setSendingCode(true)
setRegisterError('')
try {
const res = await fetch('/api/auth/send-verification-code', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ phone: registerData.phone })
})
const data = await res.json()
if (data.success) {
setCodeSent(true)
setCodeCountdown(60)
setRegisterError('')
} else {
setRegisterError(data.message || '发送失败')
}
} catch (err) {
setRegisterError('发送失败,请稍后重试')
} finally {
setSendingCode(false)
}
}
// 处理注册
const handleRegister = async () => {
// 验证输入
if (!registerData.username || !registerData.password) {
setRegisterError('用户名和密码为必填项')
return
}
if (!registerData.agreeTerms) {
setRegisterError('请先同意用户协议')
return
}
2026-03-20 15:03:26 +08:00
if (registerData.username.length < 2) {
setRegisterError('用户名至少 2 个字符')
return
}
2026-03-20 15:03:26 +08:00
if (registerData.password.length < 8 || !/[A-Z]/.test(registerData.password) || !/[a-z]/.test(registerData.password) || !/[0-9]/.test(registerData.password)) {
setRegisterError('密码至少8位需包含大写、小写、数字')
return
}
if (registerData.password !== registerData.confirmPassword) {
setRegisterError('两次输入的密码不一致')
return
}
2026-03-20 15:03:26 +08:00
// 验证手机号
if (!registerData.phone) {
setRegisterError('手机号为必填项')
return
}
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(registerData.phone)) {
setRegisterError('请输入正确的手机号')
return
}
// 验证短信获取
if (!registerData.verifyCode) {
setRegisterError('请输入短信获取')
return
}
setRegisterLoading(true)
setRegisterError('')
try {
const payload = {
username: registerData.username,
2026-03-20 15:03:26 +08:00
password: registerData.password,
phone: registerData.phone,
verifyCode: registerData.verifyCode
}
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) {
2026-03-20 15:03:26 +08:00
throw new Error(data.detail || data.error?.message || '注册失败')
}
alert('注册成功!请登录')
setShowRegister(false)
2026-03-20 15:03:26 +08:00
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
setCodeSent(false)
setCodeCountdown(0)
generateCaptcha()
} catch (err) {
console.error('注册错误:', err)
setRegisterError(err.message || '注册失败')
} finally {
setRegisterLoading(false)
}
}
const handleLogin = async () => {
// 验证输入
if (!username || !password) {
setError(ErrorCodes.E00020.message)
return
}
2026-03-20 15:03:26 +08:00
if (username.length < 2) {
setError(ErrorCodes.E00021.message)
return
}
2026-03-20 15:03:26 +08:00
// 验证获取
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 = '/'
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'
}}
/>
<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>
2026-03-20 15:03:26 +08:00
{/* 获取 */}
<div style={{ marginBottom: '24px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<div onClick={generateCaptcha} style={{
2026-03-20 15:03:26 +08:00
flex: 1, minWidth: "90px", width: "auto",
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',
2026-03-20 15:03:26 +08:00
border: '1px solid rgba(255,255,255,0.1)',
maxHeight: '90vh',
overflowY: 'auto'
}}>
<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 }))}
2026-03-20 15:03:26 +08:00
placeholder="用户名(至少 2 个字符)"
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="tel"
value={registerData.phone}
onChange={(e) => setRegisterData(prev => ({ ...prev, phone: e.target.value }))}
placeholder="手机号11位"
maxLength={11}
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>
2026-03-20 15:03:26 +08:00
{/* 短信获取 */}
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<input
type="text"
value={registerData.verifyCode}
onChange={(e) => setRegisterData(prev => ({ ...prev, verifyCode: e.target.value }))}
placeholder="短信验证码"
maxLength={6}
style={{
flex: 1, minWidth: "90px", width: "auto",
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'
}}
/>
<button
onClick={handleSendCode}
disabled={sendingCode || codeCountdown > 0}
style={{
minWidth: "90px", width: "auto",
padding: '14px',
borderRadius: '8px',
border: 'none',
background: codeCountdown > 0 ? 'rgba(148, 163, 184, 0.3)' : 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)',
color: '#fff',
fontSize: '14px',
fontWeight: '600',
cursor: codeCountdown > 0 ? 'not-allowed' : 'pointer',
whiteSpace: 'nowrap'
}}
>
{codeCountdown > 0 ? `${codeCountdown}` : sendingCode ? '发送中...' : '获取验证码'}
</button>
</div>
</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 }))}
2026-03-20 15:03:26 +08:00
placeholder="密码至少8位需大写+小写+数字)"
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={{ marginBottom: '20px', display: 'flex', alignItems: 'flex-start', gap: '8px' }}>
<input
type="checkbox"
checked={registerData.agreeTerms}
onChange={(e) => setRegisterData(prev => ({ ...prev, agreeTerms: e.target.checked }))}
style={{ width: '18px', height: '18px', marginTop: '2px', cursor: 'pointer' }}
/>
<div style={{ fontSize: '12px', color: 'rgba(255,255,255,0.7)', lineHeight: '1.5' }}>
我已阅读并同意
<span
onClick={() => window.open('/user_agreement.html', '_blank')}
style={{ color: '#fbbf24', cursor: 'pointer', textDecoration: 'underline' }}
>用户协议</span>
</div>
</div>
{/* 按钮 */}
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => {
setShowRegister(false)
2026-03-20 15:03:26 +08:00
setRegisterData({ username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' })
setRegisterError('')
2026-03-20 15:03:26 +08:00
setCodeSent(false)
setCodeCountdown(0)
}}
style={{
2026-03-20 15:03:26 +08:00
flex: 1, minWidth: "90px", width: "auto",
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={{
2026-03-20 15:03:26 +08:00
flex: 1, minWidth: "90px", width: "auto",
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>
)
}