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, username: '', password: '', confirmPassword: '', email: '', phone: '', verifyCode: '' }) const [registerLoading, setRegisterLoading] = useState(false) const [registerError, setRegisterError] = useState('') // 短信获取相关 const [sendingCode, setSendingCode] = useState(false) const [codeCountdown, setCodeCountdown] = useState(0) const [codeSent, setCodeSent] = useState(false) // 生成获取 useEffect(() => { generateCaptcha() }, []) // 获取倒计时 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: '' }) } // 发送短信获取 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 } if (registerData.username.length < 2) { setRegisterError('用户名至少 2 个字符') return } 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 } // 验证手机号 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, 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) { throw new Error(data.detail || data.error?.message || '注册失败') } alert('注册成功!请登录') setShowRegister(false) 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 } if (username.length < 2) { setError(ErrorCodes.E00021.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 = '/' console.log('跳转首页') } catch (err) { console.error('登录错误:', err) const errorCode = err.code || 'E00000' const errorMsg = err.message || '登录失败' setError(`${errorCode}: ${errorMsg}`) } finally { setLoading(false) } } return (
{/* Logo - 龙型 + 甲辰收藏文字 (橙色圆形设计) */}
甲辰收藏

生肖纪念钞管理系统

{/* 登录表单 */}

欢迎回来 👋

{error && (
⚠️ {error}
)} {/* 用户名 */}
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' }} />
{/* 密码 */}
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' }} />
{/* 获取 */}
{captcha.num1} + {captcha.num2} = ?
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' }} />
{/* 登录按钮 */} {/* 注册链接 */}
setShowRegister(true)} style={{ color: '#fbbf24', fontSize: '14px', cursor: 'pointer', textDecoration: 'underline' }} > 注册新账号 | 需要帮助?联系管理员
{/* 版本号 */}
v{APP_VERSION}
{/* 注册弹窗 */} {showRegister && (

注册新账号 🎉

{registerError && (
⚠️ {registerError}
)} {/* 用户名 */}
setRegisterData(prev => ({ ...prev, username: e.target.value }))} 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' }} />
{/* 手机号 */}
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' }} />
{/* 短信获取 */}
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' }} />
{/* 邮箱(可选) */}
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' }} />
{/* 密码 */}
setRegisterData(prev => ({ ...prev, password: e.target.value }))} 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' }} />
{/* 确认密码 */}
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' }} />
{/* 用户协议勾选 */}
setRegisterData(prev => ({ ...prev, agreeTerms: e.target.checked }))} style={{ width: '18px', height: '18px', marginTop: '2px', cursor: 'pointer' }} />
我已阅读并同意 window.open('/user_agreement.html', '_blank')} style={{ color: '#fbbf24', cursor: 'pointer', textDecoration: 'underline' }} >《用户协议》
{/* 按钮 */}
)}
) }