初始提交: Qihu-AIFlow v2.0.1 完整项目
- 前端 React+TypeScript (源码+构建产物) - 后端 Python FastAPI - 数据库迁移 001-015 - Docker部署配置 - 微信ClawBot API频道绑定 - 项目文档 README.md
This commit is contained in:
commit
78ff261b5a
199 files changed
+69126
No files matched your search
@@ -0,0 +1,267 @@
|
||||
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 { 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: userData.role,
|
||||
status: userData.status,
|
||||
realNameStatus: userData.real_name_status || 'unverified',
|
||||
createdAt: userData.created_at || new Date().toISOString(),
|
||||
membership: userData.membership_level > 0 ? {
|
||||
planId: userData.membership_level >= 3 ? 'mp3' : (userData.membership_level >= 2 ? 'mp2' : 'mp1'),
|
||||
planName: userData.role === 'admin' ? '管理员' : (userData.membership_level >= 3 ? '企业版' : (userData.membership_level >= 2 ? '专业版' : '免费版')),
|
||||
level: userData.membership_level,
|
||||
expireAt: userData.membership_expire_at || '',
|
||||
autoRenew: userData.auto_renew || false,
|
||||
} : undefined,
|
||||
};
|
||||
login(userObj);
|
||||
navigate(userData.role === 'admin' ? '/admin' : '/chat');
|
||||
} catch (e: any) {
|
||||
// 处理后端返回的精确错误信息,区分用户不存在和密码错误
|
||||
const status = e.status || e.response?.status || 0;
|
||||
const msg = e.detail || e.message || '';
|
||||
if (status === 403) {
|
||||
setErrorMsg('账号已被禁用,请联系管理员');
|
||||
} else if (status === 401) {
|
||||
// 后端的detail精确指明了原因:"用户名不存在" 或 "密码错误"
|
||||
setErrorMsg(msg || '用户名或密码错误');
|
||||
} else if (status === 0 || msg.includes('NetworkError') || msg.includes('Failed to 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">
|
||||
<rect width="32" height="32" rx="8" fill="white"/>
|
||||
<path d="M8 20V12l4 4 4-4v8" stroke="#5B5FED" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
|
||||
<path d="M6 22h20" stroke="#5B5FED" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="brand-title">AIFlow</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;
|
||||
Reference in new issue
Block a user