初始提交: Qihu-AIFlow v2.0.1 完整项目

- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
This commit is contained in:
qyyfw committed 2026-07-02 14:40:46 +08:00
commit 78ff261b5a
199 files changed
+69126

No files matched your search

+267
View File
@@ -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">&#x2713;</span><span>一站式AI自动化SOP</span></div>
<div className="brand-feature"><span className="bf-icon">&#x2713;</span><span>多模型自由切换</span></div>
<div className="brand-feature"><span className="bf-icon">&#x2713;</span><span>知识库 + 应用市场</span></div>
<div className="brand-feature"><span className="bf-icon">&#x2713;</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;