254 lines
12 KiB
TypeScript
254 lines
12 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { authAPI } from '@/api';
|
|
import type { User } from '@/types';
|
|
import './LoginPage.css';
|
|
|
|
const LoginPage: React.FC = () => {
|
|
const { login } = useAuth();
|
|
const navigate = useNavigate();
|
|
const [activeTab, setActiveTab] = useState<'login' | 'register'>('login');
|
|
const [errorMsg, setErrorMsg] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
// Register state
|
|
const [registerMethod, setRegisterMethod] = useState<'phone' | 'email'>('phone');
|
|
const [regPhone, setRegPhone] = useState('');
|
|
const [regEmail, setRegEmail] = useState('');
|
|
const [regUsername, setRegUsername] = useState('');
|
|
const [regPassword, setRegPassword] = useState('');
|
|
const [smsCode, setSmsCode] = useState('');
|
|
const [smsSent, setSmsSent] = useState(false);
|
|
const [smsCountdown, setSmsCountdown] = useState(0);
|
|
const [emailCode, setEmailCode] = useState('');
|
|
const [emailSent, setEmailSent] = useState(false);
|
|
const [emailCountdown, setEmailCountdown] = useState(0);
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
// Login state
|
|
const [loginAccount, setLoginAccount] = useState('');
|
|
const [loginPassword, setLoginPassword] = useState('');
|
|
|
|
const startCountdown = (setter: (v: number) => void) => {
|
|
let count = 60;
|
|
setter(count);
|
|
if (timerRef.current) clearInterval(timerRef.current);
|
|
timerRef.current = setInterval(() => {
|
|
count--;
|
|
setter(count);
|
|
if (count <= 0 && timerRef.current) {
|
|
clearInterval(timerRef.current);
|
|
timerRef.current = null;
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
const handleSendSms = () => {
|
|
if (!regPhone.trim() || smsCountdown > 0) return;
|
|
setSmsSent(true);
|
|
startCountdown(setSmsCountdown);
|
|
};
|
|
|
|
const handleSendEmailCode = () => {
|
|
if (!regEmail.trim() || emailCountdown > 0) return;
|
|
setEmailSent(true);
|
|
startCountdown(setEmailCountdown);
|
|
};
|
|
|
|
const handleLogin = async () => {
|
|
setErrorMsg('');
|
|
if (!loginAccount.trim() || !loginPassword.trim()) {
|
|
setErrorMsg('请输入用户名和密码');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const res = await authAPI.login(loginAccount, loginPassword);
|
|
const data: any = (res as any).data || res;
|
|
const token = data.access_token || data.token;
|
|
const userData = data.user || data;
|
|
localStorage.setItem('aiflow_token', token);
|
|
const userObj: User = {
|
|
id: userData.id, username: userData.username,
|
|
email: userData.email || '', phone: userData.phone || '',
|
|
avatar: userData.avatar || '', role: userData.role || 'user',
|
|
status: 'active', realNameStatus: 'unverified',
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
login(userObj);
|
|
navigate(userObj.role === 'admin' ? '/admin' : '/chat');
|
|
} catch (e: any) {
|
|
const msg = (e as any).detail || (e as any).message || '';
|
|
if (msg.includes('Network') || msg.includes('fetch')) {
|
|
setErrorMsg('网络连接失败,请检查网络');
|
|
} else {
|
|
setErrorMsg(msg || '登录失败,请稍后重试');
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleRegister = async () => {
|
|
setErrorMsg('');
|
|
if (!regUsername.trim() || !regPassword.trim() || regPassword.length < 6) {
|
|
setErrorMsg('用户名和密码为必填(密码至少6位)');
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const data: any = { username: regUsername, password: regPassword };
|
|
if (registerMethod === 'phone') data.phone = regPhone;
|
|
else data.email = regEmail;
|
|
const res = await authAPI.register(data);
|
|
const { access_token, user: userData } = res.data;
|
|
localStorage.setItem('aiflow_token', access_token);
|
|
const userObj: User = {
|
|
id: userData.id, username: userData.username,
|
|
email: userData.email || '', phone: userData.phone || '',
|
|
avatar: userData.avatar || '', role: 'user',
|
|
status: 'active', realNameStatus: 'unverified',
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
login(userObj);
|
|
navigate('/chat');
|
|
} catch (e: any) {
|
|
setErrorMsg(e.message || '注册失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
if (e.key === 'Enter' && activeTab === 'login') handleLogin();
|
|
};
|
|
|
|
return (
|
|
<div className="login-page">
|
|
<div className="login-bg-decor">
|
|
<div className="bg-circle c1" />
|
|
<div className="bg-circle c2" />
|
|
<div className="bg-circle c3" />
|
|
<div className="bg-grid" />
|
|
</div>
|
|
|
|
<div className="login-container">
|
|
<div className="login-brand-panel">
|
|
<div className="brand-content">
|
|
<div className="brand-icon">
|
|
<svg width="48" height="48" viewBox="0 0 32 32" fill="none">
|
|
<defs>
|
|
<linearGradient id="lpBrandGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
<stop offset="0%" stopColor="#6C63FF"/>
|
|
<stop offset="100%" stopColor="#A78BFA"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<rect width="32" height="32" rx="8" fill="url(#lpBrandGrad)"/>
|
|
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
|
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
|
</svg>
|
|
</div>
|
|
<h1 className="brand-title">七虎AI</h1>
|
|
<p className="brand-desc">智能自动化PaaS平台</p>
|
|
<div className="brand-features">
|
|
<div className="brand-feature"><span className="bf-icon">✓</span><span>一站式AI自动化SOP</span></div>
|
|
<div className="brand-feature"><span className="bf-icon">✓</span><span>多模型自由切换</span></div>
|
|
<div className="brand-feature"><span className="bf-icon">✓</span><span>知识库 + 应用市场</span></div>
|
|
<div className="brand-feature"><span className="bf-icon">✓</span><span>Token消耗实时掌控</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="login-form-panel">
|
|
<div className="login-form-inner">
|
|
<div className="login-tabs">
|
|
<button className={`login-tab ${activeTab === 'login' ? 'active' : ''}`} onClick={() => setActiveTab('login')}>登录</button>
|
|
<button className={`login-tab ${activeTab === 'register' ? 'active' : ''}`} onClick={() => setActiveTab('register')}>注册</button>
|
|
</div>
|
|
|
|
{errorMsg && <div className="form-error">{errorMsg}</div>}
|
|
|
|
{activeTab === 'login' ? (
|
|
<div className="login-form" onKeyDown={handleKeyDown}>
|
|
<div className="form-group">
|
|
<label className="form-label">用户名</label>
|
|
<input type="text" className="form-input" placeholder="请输入用户名" value={loginAccount} onChange={e => setLoginAccount(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">密码</label>
|
|
<input type="password" className="form-input" placeholder="请输入密码" value={loginPassword} onChange={e => setLoginPassword(e.target.value)} />
|
|
</div>
|
|
<button className="login-submit-btn" onClick={handleLogin} disabled={loading}>
|
|
{loading ? '登录中...' : '登 录'}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="login-form">
|
|
<div className="register-method-tabs">
|
|
<button className={`register-method-tab ${registerMethod === 'phone' ? 'active' : ''}`} onClick={() => setRegisterMethod('phone')}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg>
|
|
手机号注册
|
|
</button>
|
|
<button className={`register-method-tab ${registerMethod === 'email' ? 'active' : ''}`} onClick={() => setRegisterMethod('email')}>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
|
邮箱注册
|
|
</button>
|
|
</div>
|
|
|
|
{registerMethod === 'phone' ? (
|
|
<>
|
|
<div className="form-group">
|
|
<label className="form-label">手机号</label>
|
|
<input type="tel" className="form-input" placeholder="请输入手机号码" value={regPhone} onChange={e => setRegPhone(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">短信验证码</label>
|
|
<div className="sms-row">
|
|
<input type="text" className="form-input sms-input" placeholder="请输入验证码" value={smsCode} onChange={e => setSmsCode(e.target.value)} />
|
|
<button className="sms-btn" onClick={handleSendSms} disabled={!regPhone.trim() || smsCountdown > 0}>
|
|
{smsCountdown > 0 ? `${smsCountdown}秒后重发` : smsSent ? '重新发送' : '获取验证码'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div className="form-group">
|
|
<label className="form-label">邮箱</label>
|
|
<input type="email" className="form-input" placeholder="请输入邮箱地址" value={regEmail} onChange={e => setRegEmail(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">邮箱验证码</label>
|
|
<div className="sms-row">
|
|
<input type="text" className="form-input sms-input" placeholder="请输入邮箱验证码" value={emailCode} onChange={e => setEmailCode(e.target.value)} />
|
|
<button className="sms-btn" onClick={handleSendEmailCode} disabled={!regEmail.trim() || emailCountdown > 0}>
|
|
{emailCountdown > 0 ? `${emailCountdown}秒后重发` : emailSent ? '重新发送' : '发送验证码'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="form-group">
|
|
<label className="form-label">用户名 <span className="required">*</span></label>
|
|
<input type="text" className="form-input" placeholder="请输入用户名" value={regUsername} onChange={e => setRegUsername(e.target.value)} />
|
|
</div>
|
|
<div className="form-group">
|
|
<label className="form-label">密码 <span className="required">*</span></label>
|
|
<input type="password" className="form-input" placeholder="请设置密码(至少6位)" value={regPassword} onChange={e => setRegPassword(e.target.value)} />
|
|
</div>
|
|
<button className="login-submit-btn" onClick={handleRegister} disabled={loading}>
|
|
{loading ? '注册中...' : '注 册'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LoginPage;
|