refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir

This commit is contained in:
qyyfw committed 2026-07-11 06:29:32 +08:00
1 parent 1346fbbd4b
commit 5d7f32db0a
138 files changed
+12846 -84248

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -407,7 +407,7 @@ const TimelinePanel: React.FC<{
pdf.text('我的AI进化时间线', 10, 15);
pdf.setFontSize(10);
pdf.text(`生成日期: ${dateStr}`, 10, 22);
pdf.text('七虎AI', pageWidth - 10, 22, { align: 'right' });
pdf.text('Qihu-AIFlow', pageWidth - 10, 22, { align: 'right' });
// 单页添加图片,自动缩放适应
const maxHeight = pageHeight - 35;
@@ -417,7 +417,7 @@ const TimelinePanel: React.FC<{
// 底部水印
pdf.setFontSize(8);
pdf.setTextColor(200, 200, 200);
pdf.text('七虎AI', pageWidth / 2, pageHeight - 5, { align: 'center' });
pdf.text('Qihu-AIFlow', pageWidth / 2, pageHeight - 5, { align: 'center' });
pdf.save(`${filename}.pdf`);
setExportMsg(`时间线已导出为 PDF: ${filename}.pdf`);
@@ -746,7 +746,7 @@ const ProfilePanel: React.FC<{ profile: EvolutionProfile; skills: EvolutionSkill
{/* Footer */}
<div className="evo-share-card-footer">
<span style={{ opacity: 0.5, fontSize: '11px' }}>{new Date().toLocaleDateString()}</span>
<span style={{ opacity: 0.5, fontSize: '11px' }}>七虎AI</span>
<span style={{ opacity: 0.5, fontSize: '11px' }}>Qihu-AIFlow</span>
</div>
</div>
</div>
File diff suppressed because it is too large. Load diff
+370 -486
View File
@@ -1,486 +1,370 @@
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { mockAdBanners, mockApps } from '@/mock/data';
import type { AdBanner, MarketApp } from '@/types';
import './LandingPage.css';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{children}
</svg>
);
const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' };
const typeColors: Record<string, string> = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' };
const LandingPage: React.FC = () => {
const navigate = useNavigate();
const [currentSlide, setCurrentSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const banners = mockAdBanners.filter(b => b.status === 'active');
const publishedApps = mockApps.filter(a => a.status === 'published');
const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>('');
const [skillData, setSkillData] = useState<{skills: any[], agents: any[], mcps: any[]}>({skills: [], agents: [], mcps: []});
useEffect(() => {
(async () => {
try {
const apiModule = await import('@/api');
const res = await apiModule.adminAPI.getAdminSkills({page: 1, page_size: 50, status: 'published'});
const allApps: any[] = (res as any)?.data?.items || (res as any)?.data || [];
const sortFn = (a: any, b: any) => (b.usage_count || b.install_count || 0) - (a.usage_count || a.install_count || 0);
const skills = allApps.filter(a => a.type === 'skill').sort(sortFn).slice(0, 5);
const agents = allApps.filter(a => a.type === 'agent').sort(sortFn).slice(0, 5);
const mcps = allApps.filter(a => a.type === 'mcp').sort(sortFn).slice(0, 5);
if (skills.length + agents.length + mcps.length === 0) {
setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)});
} else { setSkillData({skills, agents, mcps}); }
} catch (e) {
setSkillData({skills: publishedApps.filter(a => a.type === 'skill').slice(0, 5), agents: publishedApps.filter(a => a.type === 'agent').slice(0, 5), mcps: publishedApps.filter(a => a.type === 'mcp').slice(0, 5)});
}
})();
}, []);
const [toastMsg, setToastMsg] = useState('');
const [scrolled, setScrolled] = useState(false);
const filteredApps = appTypeFilter
? publishedApps.filter(a => a.type === appTypeFilter)
: publishedApps;
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 60);
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const showToast = (msg: string) => {
setToastMsg(msg);
setTimeout(() => setToastMsg(''), 2000);
};
const handleAppClick = (appName: string) => {
showToast(`"${appName}" 需登录后使用,即将跳转登录页`);
setTimeout(() => navigate('/login'), 800);
};
const nextSlide = useCallback(() => {
if (banners.length === 0) return;
setCurrentSlide(prev => (prev + 1) % banners.length);
}, [banners.length]);
useEffect(() => {
if (banners.length <= 1 || isPaused) return;
const timer = setInterval(nextSlide, 4000);
return () => clearInterval(timer);
}, [banners.length, isPaused, nextSlide]);
const goToSlide = (idx: number) => setCurrentSlide(idx);
const getGradient = (app: MarketApp): string => {
const gradients: Record<string, string> = {
a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)',
a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
};
return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)';
};
// Platform Architecture - layers from bottom to top
const archLayers = [
{
label: '应用层',
color: '#6C63FF',
items: ['AI 对话工作台', '知识库问答', '自动化流程', 'IM Bot 接入', 'API 集成'],
},
{
label: '能力层',
color: '#10B981',
items: ['技能市场 (Skill)', '智能体 (Agent)', 'MCP 协议', 'RAG 检索增强', '工具链编排'],
},
{
label: '模型层',
color: '#F59E0B',
items: ['DeepSeek', 'GPT-4o', 'Claude', 'Qwen', '多模态模型'],
},
{
label: '基础设施',
color: '#3B82F6',
items: ['向量数据库', '对象存储', '任务调度', 'API 网关', '监控告警'],
},
];
// Capability groups
const capabilities = [
{
icon: <SVGIcon size={28}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>,
title: 'AI 对话',
desc: '接入 50+ 主流大模型,支持流式输出、多轮对话、Function Calling,你的专属 AI 助手',
},
{
icon: <SVGIcon size={28}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon>,
title: '知识库',
desc: '上传文档、网页、代码构建个人知识库,RAG 增强检索让 AI 回答更精准可靠',
},
{
icon: <SVGIcon size={28}><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon>,
title: '技能市场',
desc: '100+ 预制 AI 技能,一键安装即用。支持 Skill、Agent、MCP 三种形态,持续扩展',
},
{
icon: <SVGIcon size={28}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon>,
title: 'IM Bot',
desc: '将 AI 能力接入飞书、微信、钉钉,在聊天中调用 AI,无缝融入日常工作流',
},
{
icon: <SVGIcon size={28}><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></SVGIcon>,
title: 'AI 自进化',
desc: '从每次对话中持续学习,自动沉淀知识图谱,你的 AI 越用越懂你',
},
{
icon: <SVGIcon size={28}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>,
title: 'Token 驾驶舱',
desc: '实时监控调用量、Token 消耗与成本,精细化管理你的 AI 使用预算',
},
];
// Scenarios - individual practitioner focused
const scenarios = [
{ icon: '📚', title: '学习助手', desc: '构建课程知识库,AI 辅助理解复杂概念、整理笔记、解答疑问' },
{ icon: '💼', title: '工作效率', desc: '自动化重复任务、智能撰写文档邮件、快速检索信息,效率翻倍' },
{ icon: '🤖', title: 'AI Agent 开发', desc: '用 Skill + Agent + MCP 搭建专属 AI 助手,从原型到生产一站完成' },
{ icon: '📊', title: '数据分析', desc: '自然语言查询数据库,自动生成可视化报告,让数据说话更简单' },
{ icon: '✍️', title: '内容创作', desc: 'AI 辅助写文章、做翻译、润色文案,让创作更有灵感' },
{ icon: '🔬', title: '技术探索', desc: '接入最新模型做实验,对比不同 LLM 效果,探索 AI 的前沿能力' },
];
return (
<div className="landing-page-v2">
{/* Toast */}
{toastMsg && <div className="landing-toast-v2">{toastMsg}</div>}
{/* Header */}
<header className={`landing-header-v2 ${scrolled ? 'scrolled' : ''}`}>
<div className="landing-header-inner-v2">
<div className="landing-brand-v2">
<svg width="36" height="36" viewBox="0 0 32 32" fill="none">
<defs>
<linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#5B5FED"/>
<stop offset="100%" stopColor="#A78BFA"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="8" fill="url(#logoGrad)"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
<span className="landing-brand-text">七虎AI</span>
</div>
<nav className="landing-nav-v2" style={{ cursor: 'pointer' }}>
<a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a>
<a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a>
<a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a>
<a onClick={() => document.getElementById('scenarios')?.scrollIntoView({ behavior: 'smooth' })}>应用场景</a>
<a onClick={() => document.getElementById('market')?.scrollIntoView({ behavior: 'smooth' })}>技能生态</a>
</nav>
<div className="landing-actions-v2">
<button className="btn-ghost-v2" onClick={() => navigate('/login')}>登录</button>
<button className="btn-primary-v2" onClick={() => navigate('/login')}>免费注册</button>
</div>
</div>
</header>
{/* Hero Section — What is 七虎AI */}
<section className="hero-section-v2">
<div className="hero-bg-orb orb-1" />
<div className="hero-bg-orb orb-2" />
<div className="hero-bg-orb orb-3" />
<div className="hero-content-v2">
<div className="hero-badge-v2">
<span className="hero-badge-dot" />
AI 个人工作台 · 为创造者而生
</div>
<h1 className="hero-title-v2">
每一个 AI 创造者
<span className="hero-gradient-text"> 的超级工作台</span>
</h1>
<p className="hero-subtitle-v2">
七虎AI 是一站式 AI 能力集成平台,让你在一个地方完成对话、知识库、技能编排、模型切换、自动化搭建<br/>
无需拼接多个工具,从想法到落地,一个平台就够了
</p>
<div className="hero-cta-v2">
<button className="btn-hero-primary" onClick={() => navigate('/login')}>
免费开始使用
<SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon>
</button>
<button className="btn-hero-secondary" onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>
了解平台
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M6 9l6 6 6-6"/></svg>
</button>
</div>
{/* Stats */}
<div className="hero-stats-v2">
<div className="hero-stat-item">
<span className="hero-stat-num">100+</span>
<span className="hero-stat-label">AI 技能</span>
</div>
<div className="hero-stat-divider" />
<div className="hero-stat-item">
<span className="hero-stat-num">50+</span>
<span className="hero-stat-label">大模型接入</span>
</div>
<div className="hero-stat-divider" />
<div className="hero-stat-item">
<span className="hero-stat-num">6</span>
<span className="hero-stat-label">IM 平台</span>
</div>
<div className="hero-stat-divider" />
<div className="hero-stat-item">
<span className="hero-stat-num">99.9%</span>
<span className="hero-stat-label">可用性</span>
</div>
</div>
</div>
</section>
{/* What is 七虎AI — Platform Introduction */}
<section id="what-is" className="whatis-section-v2">
<div className="section-header-v2">
<span className="section-badge-v2">平台介绍</span>
<h2>七虎AI 是什么?</h2>
<p>一个面向 AI 爱好者和从业者的个人 AI 能力集成平台</p>
</div>
<div className="whatis-cards-v2">
<div className="whatis-card">
<div className="whatis-card-icon">🎯</div>
<h3>能力聚合</h3>
<p>将大模型对话、知识库、技能编排、自动化、IM Bot 等能力整合在一个平台,告别工具碎片化</p>
</div>
<div className="whatis-card">
<div className="whatis-card-icon">⚡</div>
<h3>开箱即用</h3>
<p>预设 100+ AI 技能模板,50+ 大模型即配即用,无需从零搭建,注册即可上手</p>
</div>
<div className="whatis-card">
<div className="whatis-card-icon">🔌</div>
<h3>开放扩展</h3>
<p>支持 MCP 协议、Skill 插件、Agent 编排、API 开放,你可以自由扩展和定制</p>
</div>
<div className="whatis-card">
<div className="whatis-card-icon">🚀</div>
<h3>持续进化</h3>
<p>AI 自进化引擎从使用中持续学习,知识库自动更新,模型热切换,你的工作台越来越强</p>
</div>
</div>
</section>
{/* Platform Architecture */}
<section id="architecture" className="architecture-section-v2">
<div className="section-header-v2">
<span className="section-badge-v2">平台架构</span>
<h2>四层架构 · 层层赋能</h2>
<p>从基础设施到上层应用,理解 七虎AI 的技术全貌</p>
</div>
<div className="arch-layers-v2">
{archLayers.map((layer, idx) => (
<div key={idx} className="arch-layer" style={{ '--layer-color': layer.color } as React.CSSProperties}>
<div className="arch-layer-header">
<div className="arch-layer-dot" style={{ background: layer.color }} />
<span className="arch-layer-label">{layer.label}</span>
<div className="arch-layer-line" />
</div>
<div className="arch-layer-items">
{layer.items.map((item, i) => (
<span key={i} className="arch-layer-chip">{item}</span>
))}
</div>
</div>
))}
{/* Downward arrows between layers */}
</div>
</section>
{/* Banner Carousel */}
{banners.length > 0 && (
<section className="banner-section-v2">
<div
className="banner-carousel-v2"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div className="banner-track-v2" style={{ transform: `translateX(-${currentSlide * 100}%)` }}>
{banners.map((banner, idx) => (
<div key={idx} className="banner-slide-v2">
<div className="banner-content-v2">
<span className="banner-tag">AI 精选</span>
<h2>{banner.title}</h2>
<p>{banner.title}</p>
</div>
<div className="banner-visual-v2">
<div className="banner-shape" />
</div>
</div>
))}
</div>
<div className="banner-dots-v2">
{banners.map((_, idx) => (
<button
key={idx}
className={`banner-dot ${idx === currentSlide ? 'active' : ''}`}
onClick={() => goToSlide(idx)}
/>
))}
</div>
</div>
</section>
)}
{/* Capabilities Section */}
<section id="capabilities" className="capabilities-section-v2">
<div className="section-header-v2">
<span className="section-badge-v2">能力体系</span>
<h2>六大核心能力</h2>
<p>从对话到自动化,覆盖 AI 从业者的全部工作流</p>
</div>
<div className="capabilities-grid-v2">
{capabilities.map((c, idx) => (
<div key={idx} className="capability-card-v2">
<div className="capability-icon-v2">{c.icon}</div>
<h3>{c.title}</h3>
<p>{c.desc}</p>
</div>
))}
</div>
</section>
{/* Scenarios Section */}
<section id="scenarios" className="scenarios-section-v2">
<div className="section-header-v2">
<span className="section-badge-v2">应用场景</span>
<h2>每个 AI 创作者都用得上</h2>
<p>无论你是学习者、开发者、还是内容创作者,都能找到属于你的打开方式</p>
</div>
<div className="scenarios-grid-v2">
{scenarios.map((s, idx) => (
<div key={idx} className="scenario-card-v2">
<div className="scenario-emoji">{s.icon}</div>
<h3>{s.title}</h3>
<p>{s.desc}</p>
</div>
))}
</div>
</section>
{/* Market Preview */}
<section id="market" className="market-section-v2">
<div className="section-header-v2">
<span className="section-badge-v2">技能生态</span>
<h2>丰富的 AI 技能生态</h2>
<p>Skills、Agent、MCP 工具一键安装,持续扩展你的 AI 能力边界</p>
<div className="market-filter-v2">
{(['', 'skill', 'agent', 'mcp'] as const).map(t => (
<button
key={t || 'all'}
className={`filter-chip ${appTypeFilter === t ? 'active' : ''}`}
onClick={() => setAppTypeFilter(t)}
>
{t === '' ? '全部' : typeLabels[t]}
</button>
))}
</div>
</div>
<div className="market-grid-v2">
{(() => {
const hasRealData = skillData.skills.length + skillData.agents.length + skillData.mcps.length > 0;
let displayApps: any[];
if (appTypeFilter) {
const key = appTypeFilter === 'skill' ? 'skills' : appTypeFilter === 'agent' ? 'agents' : 'mcps';
displayApps = hasRealData ? skillData[key] : filteredApps.slice(0, 5);
} else {
displayApps = hasRealData ? [...skillData.skills, ...skillData.agents, ...skillData.mcps] : filteredApps.slice(0, 6);
}
return displayApps.map((app: any) => {
const appType = app.type || 'skill';
const appColor = typeColors[appType] || '#6C63FF';
const appLabel = typeLabels[appType] || 'Skill';
const appName = app.name || app.title || '未命名';
const appDesc = app.description || app.desc || '';
const appUsage = app.usage_count || app.install_count || 0;
const appPrice = app.price || 0;
return (
<div key={app.id || appName} className="market-card-v2" onClick={() => handleAppClick(appName)}>
<div className="market-card-icon-v2" style={{ background: hasRealData ? `linear-gradient(135deg, ${appColor}44, ${appColor}11)` : getGradient(app) }}>
<span style={{ color: appColor, fontWeight: 700, fontSize: 16 }}>{appName.charAt(0)}</span>
</div>
<div className="market-card-body-v2">
<div className="market-card-header-v2">
<h4>{appName}</h4>
<span className="type-badge" style={{ background: appColor, color: '#fff' }}>{appLabel}</span>
</div>
<p className="market-card-desc-v2">{appDesc}</p>
<div className="market-card-meta-v2">
{appUsage > 0 && <span>📥 {appUsage >= 1000 ? `${(appUsage/1000).toFixed(1)}k` : appUsage}次</span>}
<span>{appPrice === 0 ? '免费' : `¥${appPrice}`}</span>
</div>
</div>
</div>
);
});
})()}
</div>
</section>
{/* CTA */}
<section className="cta-section-v2">
<div className="cta-bg-glow" />
<h2>准备好开始你的 AI 之旅了吗?</h2>
<p>免费注册,即刻拥有属于你自己的 AI 超级工作台</p>
<button className="btn-hero-primary" onClick={() => navigate('/login')}>
免费注册
<SVGIcon size={18}><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></SVGIcon>
</button>
</section>
{/* Footer */}
<footer className="landing-footer-v2">
<div className="footer-inner-v2">
<div className="footer-brand-v2">
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
<span>七虎AI</span>
</div>
<div className="footer-links-v2">
<div className="footer-col-v2">
<h4>平台</h4>
<a onClick={() => document.getElementById('what-is')?.scrollIntoView({ behavior: 'smooth' })}>平台介绍</a>
<a onClick={() => document.getElementById('architecture')?.scrollIntoView({ behavior: 'smooth' })}>平台架构</a>
<a onClick={() => document.getElementById('capabilities')?.scrollIntoView({ behavior: 'smooth' })}>能力体系</a>
</div>
<div className="footer-col-v2">
<h4>资源</h4>
<a href="#">API 文档</a>
<a href="#">开发者指南</a>
<a href="#">更新日志</a>
</div>
<div className="footer-col-v2">
<h4>支持</h4>
<a href="#">帮助中心</a>
<a href="#">联系我们</a>
<a href="#">社区论坛</a>
</div>
</div>
</div>
<div className="footer-bottom-v2">
<p>&copy; 2026 七虎AI. All rights reserved.</p>
</div>
</footer>
</div>
);
};
export default LandingPage;
import React, { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { mockAdBanners, mockApps } from '@/mock/data';
import type { AdBanner, MarketApp } from '@/types';
import './LandingPage.css';
const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
{children}
</svg>
);
const typeLabels: Record<string, string> = { skill: 'Skill', agent: 'Agent', mcp: 'MCP' };
const typeColors: Record<string, string> = { skill: '#10B981', agent: '#5B5FED', mcp: '#F59E0B' };
const LandingPage: React.FC = () => {
const navigate = useNavigate();
const [currentSlide, setCurrentSlide] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const banners = mockAdBanners.filter(b => b.status === 'active');
const publishedApps = mockApps.filter(a => a.status === 'published');
const [appTypeFilter, setAppTypeFilter] = useState<'' | 'skill' | 'agent' | 'mcp'>('');
const [toastMsg, setToastMsg] = useState('');
const filteredApps = appTypeFilter
? publishedApps.filter(a => a.type === appTypeFilter)
: publishedApps;
const showToast = (msg: string) => {
setToastMsg(msg);
setTimeout(() => setToastMsg(''), 2000);
};
const handleAppClick = (appName: string) => {
showToast(`"${appName}" 需登录后使用,即将跳转登录页`);
setTimeout(() => navigate('/login'), 800);
};
// Auto rotate carousel
const nextSlide = useCallback(() => {
if (banners.length === 0) return;
setCurrentSlide(prev => (prev + 1) % banners.length);
}, [banners.length]);
useEffect(() => {
if (banners.length <= 1 || isPaused) return;
const timer = setInterval(nextSlide, 4000);
return () => clearInterval(timer);
}, [banners.length, isPaused, nextSlide]);
const goToSlide = (idx: number) => setCurrentSlide(idx);
const getGradient = (app: MarketApp): string => {
const gradients: Record<string, string> = {
a1: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
a2: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
a3: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
a4: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
a5: 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
a6: 'linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%)',
a8: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
};
return gradients[app.id] || 'linear-gradient(135deg, #5B5FED 0%, #A78BFA 100%)';
};
return (
<div className="landing-page">
{/* Toast notification */}
{toastMsg && (
<div className="landing-toast">{toastMsg}</div>
)}
{/* Hero / Nav bar */}
<header className="landing-header">
<div className="landing-header-inner">
<div className="landing-brand">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M6 22h20" stroke="white" strokeWidth="1.5" strokeLinecap="round" opacity="0.5"/>
</svg>
<span className="brand-name">Qihu-AIFlow</span>
</div>
<nav className="landing-nav-links">
<a href="#market">应用市场</a>
<a href="#features">平台能力</a>
</nav>
<div className="landing-header-actions">
<button className="landing-btn landing-btn-outline" onClick={() => navigate('/login')}>登录</button>
<button className="landing-btn landing-btn-primary" onClick={() => navigate('/login')}>免费注册</button>
</div>
</div>
</header>
{/* Hero Section */}
<section className="landing-hero">
<div className="landing-hero-content">
<h1 className="hero-title">
用 <span className="hero-accent">AI</span> 构建你的<br/>智能自动化工作流
</h1>
<p className="hero-desc">
Qihu-AIFlow 是一站式AI智能自动化PaaS平台,集成多模态大模型、知识库管理、应用市场,
让每个人都能轻松搭建属于自己的AI自动化SOP。
</p>
<div className="hero-actions">
<button className="landing-btn landing-btn-primary landing-btn-lg" onClick={() => navigate('/login')}>
免费开始使用
</button>
<button className="landing-btn landing-btn-outline landing-btn-lg" onClick={() => { document.getElementById('market')?.scrollIntoView({ behavior: 'smooth' }); }}>
浏览应用市场
</button>
</div>
<div className="hero-stats">
<div className="hero-stat">
<span className="stat-number">3,500+</span>
<span className="stat-label">活跃用户</span>
</div>
<div className="hero-stat">
<span className="stat-number">120+</span>
<span className="stat-label">应用/Skills</span>
</div>
<div className="hero-stat">
<span className="stat-number">99.8%</span>
<span className="stat-label">服务可用率</span>
</div>
</div>
</div>
</section>
{/* Carousel Section */}
{banners.length > 0 && (
<section className="landing-banners">
<div className="landing-section-inner">
<h2 className="section-title">推荐应用</h2>
<div
className="carousel-container"
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<div className="carousel-track" style={{ transform: `translateX(-${currentSlide * 100}%)` }}>
{banners.map(banner => (
<div key={banner.id} className="carousel-slide">
<div className="carousel-card" style={{ background: banner.linkAppId ? getGradient({ id: banner.linkAppId } as MarketApp) : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }}>
<div className="carousel-card-content">
<div className="carousel-badge">推荐</div>
<h3 className="carousel-title">{banner.title}</h3>
{banner.linkAppName && (
<p className="carousel-subtitle">立即体验 {banner.linkAppName}</p>
)}
<button
className="carousel-cta"
onClick={() => navigate('/login')}
>
查看详情
<SVGIcon size={14}><polyline points="9 18 15 12 9 6"/></SVGIcon>
</button>
</div>
<div className="carousel-card-visual">
<div className="visual-circle c1"/>
<div className="visual-circle c2"/>
<div className="visual-circle c3"/>
</div>
</div>
</div>
))}
</div>
{/* Dots */}
{banners.length > 1 && (
<div className="carousel-dots">
{banners.map((_, idx) => (
<button
key={idx}
className={`carousel-dot ${idx === currentSlide ? 'active' : ''}`}
onClick={() => goToSlide(idx)}
/>
))}
</div>
)}
{/* Arrows */}
{banners.length > 1 && (
<>
<button
className="carousel-arrow carousel-arrow-left"
onClick={() => setCurrentSlide(prev => (prev - 1 + banners.length) % banners.length)}
>
<SVGIcon size={20}><polyline points="15 18 9 12 15 6"/></SVGIcon>
</button>
<button
className="carousel-arrow carousel-arrow-right"
onClick={() => nextSlide()}
>
<SVGIcon size={20}><polyline points="9 18 15 12 9 6"/></SVGIcon>
</button>
</>
)}
</div>
</div>
</section>
)}
{/* Market Preview Section */}
<section className="landing-market" id="market">
<div className="landing-section-inner">
<div className="section-header">
<h2 className="section-title">应用市场</h2>
<p className="section-desc">丰富的 Skills、Agent、MCP 应用,一键接入你的工作流</p>
</div>
{/* Type Filter */}
<div className="market-filter-tabs">
<button
className={`filter-tab ${appTypeFilter === '' ? 'active' : ''}`}
onClick={() => setAppTypeFilter('')}
>全部</button>
<button
className={`filter-tab ${appTypeFilter === 'skill' ? 'active' : ''}`}
onClick={() => setAppTypeFilter('skill')}
>Skills</button>
<button
className={`filter-tab ${appTypeFilter === 'agent' ? 'active' : ''}`}
onClick={() => setAppTypeFilter('agent')}
>Agent</button>
<button
className={`filter-tab ${appTypeFilter === 'mcp' ? 'active' : ''}`}
onClick={() => setAppTypeFilter('mcp')}
>MCP</button>
</div>
{/* App Grid */}
<div className="market-app-grid">
{filteredApps.map(app => (
<div key={app.id} className="market-app-card" onClick={() => handleAppClick(app.name)}>
<div className="app-card-cover" style={{ background: getGradient(app) }}>
<span className="app-card-type-badge" style={{ background: typeColors[app.type] }}>
{typeLabels[app.type]}
</span>
{app.price > 0 && (
<span className="app-card-price">{app.price} 元</span>
)}
</div>
<div className="app-card-body">
<h4 className="app-card-name">{app.name}</h4>
<p className="app-card-desc">{app.description}</p>
<div className="app-card-meta">
<span className="app-card-rating">
<SVGIcon size={12}><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></SVGIcon>
{app.ratingAvg > 0 ? app.ratingAvg : '-'}
</span>
<span className="app-card-installs">{app.installCount.toLocaleString()} 次安装</span>
</div>
</div>
</div>
))}
</div>
<div className="market-more">
<button className="landing-btn landing-btn-outline" onClick={() => navigate('/login')}>
登录查看更多应用
<SVGIcon size={14}><polyline points="9 18 15 12 9 6"/></SVGIcon>
</button>
</div>
</div>
</section>
{/* Features Section */}
<section className="landing-features" id="features">
<div className="landing-section-inner">
<div className="section-header">
<h2 className="section-title">平台核心能力</h2>
<p className="section-desc">从对话到自动化,Qihu-AIFlow 为你提供完整的 AI 工作流解决方案</p>
</div>
<div className="features-grid">
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></SVGIcon>
</div>
<h3 className="feature-title">多模型AI对话</h3>
<p className="feature-desc">支持GPT-4o、DeepSeek-R1、Qwen-Max等多种大模型自由切换,按需使用最优模型</p>
</div>
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></SVGIcon>
</div>
<h3 className="feature-title">私有知识库</h3>
<p className="feature-desc">上传文档构建专属知识库,AI回答基于你的内容,更精准可靠</p>
</div>
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><rect x="3" y="3" width="7" height="7" rx="1.2"/><rect x="14" y="3" width="7" height="7" rx="1.2"/><rect x="3" y="14" width="7" height="7" rx="1.2"/><rect x="14" y="14" width="7" height="7" rx="1.2"/></SVGIcon>
</div>
<h3 className="feature-title">应用市场</h3>
<p className="feature-desc">丰富的Skills、Agent、MCP应用,一站式接入工作流,即装即用</p>
</div>
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></SVGIcon>
</div>
<h3 className="feature-title">Token驾驶舱</h3>
<p className="feature-desc">实时监控Token消耗,费用透明可控,每一笔都有据可查</p>
</div>
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></SVGIcon>
</div>
<h3 className="feature-title">IM频道绑定</h3>
<p className="feature-desc">支持飞书、钉钉、微信等多平台绑定,AI助手融入日常工作流</p>
</div>
<div className="feature-card">
<div className="feature-icon">
<SVGIcon size={28}><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/><line x1="4" y1="4" x2="9" y2="9"/></SVGIcon>
</div>
<h3 className="feature-title">自动化任务编排</h3>
<p className="feature-desc">创建关键词触发、定时执行等自动化任务,让AI按你的节奏工作</p>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="landing-cta">
<div className="landing-section-inner">
<h2 className="cta-title">准备好开始了吗?</h2>
<p className="cta-desc">免费注册即可体验全部功能,无需任何技术背景</p>
<button className="landing-btn landing-btn-primary landing-btn-lg" onClick={() => navigate('/login')}>
免费注册,立即体验
</button>
</div>
</section>
{/* Footer */}
<footer className="landing-footer">
<div className="landing-section-inner">
<div className="footer-content">
<div className="footer-brand">
<div className="landing-brand">
<svg width="28" height="28" viewBox="0 0 32 32" fill="none">
<rect width="32" height="32" rx="8" fill="#5B5FED"/>
<path d="M8 20V12l4 4 4-4v8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
</svg>
<span className="brand-name" style={{ color: 'var(--color-text-primary)' }}>Qihu-AIFlow</span>
</div>
<p style={{ color: 'var(--color-text-tertiary)', fontSize: 'var(--text-sm)', marginTop: 'var(--space-md)' }}>
AI智能自动化PaaS平台
</p>
</div>
<div className="footer-links">
<div className="footer-col">
<h4>产品</h4>
<a>AI聊天</a>
<a>知识库</a>
<a>应用市场</a>
</div>
<div className="footer-col">
<h4>支持</h4>
<a>帮助文档</a>
<a>API参考</a>
<a>联系我们</a>
</div>
</div>
</div>
<div className="footer-bottom">
<span>Qihu-AIFlow 2026. All rights reserved.</span>
</div>
</div>
</footer>
</div>
);
};
export default LandingPage;
+153 -158
View File
@@ -1,15 +1,14 @@
/* ===== Login Page V3 - Dark Theme ===== */
/* ===== Login Page ===== */
.login-page {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: #F5F7FC;
background: var(--color-bg-primary);
position: relative;
overflow: hidden;
}
/* Background decorations */
.login-bg-decor {
position: absolute;
inset: 0;
@@ -20,37 +19,35 @@
.bg-circle {
position: absolute;
border-radius: 50%;
filter: blur(80px);
opacity: 0.08;
}
.bg-circle.c1 {
width: 600px; height: 600px;
background: #6C63FF;
background: var(--color-accent);
top: -200px; right: -100px;
opacity: 0.12;
}
.bg-circle.c2 {
width: 400px; height: 400px;
background: #06B6D4;
background: #8B8DFF;
bottom: -100px; left: -50px;
opacity: 0.1;
}
.bg-circle.c3 {
width: 300px; height: 300px;
background: #A78BFA;
background: var(--color-accent);
top: 50%; left: 50%;
transform: translate(-50%, -50%);
opacity: 0.06;
opacity: 0.04;
}
.bg-grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(rgba(108, 99, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(108, 99, 255, 0.03) 1px, transparent 1px);
linear-gradient(rgba(0,0,0,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 60px 60px;
}
@@ -60,211 +57,202 @@
width: 900px;
min-height: 560px;
background: #FFFFFF;
backdrop-filter: blur(24px);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
border-radius: var(--radius-2xl);
box-shadow: var(--shadow-xl);
overflow: hidden;
z-index: var(--z-base);
position: relative;
z-index: 1;
animation: loginSlideUp 0.5s ease;
}
@keyframes loginSlideUp {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
/* Left brand panel */
/* Brand Panel */
.login-brand-panel {
width: 400px;
background: linear-gradient(135deg, rgba(108, 99, 255, 0.08) 0%, rgba(167, 139, 250, 0.05) 100%);
padding: 48px 40px;
background: linear-gradient(135deg, #2D2B55 0%, #1A1A2E 50%, #2D2B55 100%);
display: flex;
align-items: center;
border-right: 1px solid rgba(0, 0, 0, 0.06);
justify-content: center;
position: relative;
overflow: hidden;
}
.login-brand-panel::before {
content: '';
position: absolute;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(91,95,237,0.3) 0%, transparent 70%);
top: -50px; right: -50px;
}
.login-brand-panel::after {
content: '';
position: absolute;
width: 200px; height: 200px;
border-radius: 50%;
background: radial-gradient(circle, rgba(139,141,255,0.2) 0%, transparent 70%);
bottom: -30px; left: -30px;
}
.brand-content {
width: 100%;
text-align: center;
color: white;
z-index: 1;
}
.brand-icon {
margin-bottom: 20px;
margin-bottom: var(--space-xl);
}
.brand-title {
font-size: 32px;
font-size: 36px;
font-weight: 800;
color: #1A1F36;
margin-bottom: 8px;
letter-spacing: -0.02em;
letter-spacing: -0.03em;
margin-bottom: var(--space-sm);
}
.brand-desc {
font-size: 15px;
color: #5A6480;
margin-bottom: 32px;
font-size: var(--text-base);
color: rgba(255,255,255,0.6);
margin-bottom: var(--space-3xl);
}
.brand-features {
display: flex;
flex-direction: column;
gap: 14px;
gap: var(--space-md);
text-align: left;
}
.brand-feature {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: #5A6480;
gap: var(--space-md);
font-size: var(--text-sm);
color: rgba(255,255,255,0.75);
}
.bf-icon {
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(16, 185, 129, 0.15);
color: #10B981;
color: #6C63FF;
font-weight: 700;
font-size: var(--text-base);
}
/* Form Panel */
.login-form-panel {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
}
/* Right form panel */
.login-form-panel {
flex: 1;
padding: 48px 40px;
display: flex;
align-items: center;
padding: var(--space-4xl);
}
.login-form-inner {
width: 100%;
max-width: 340px;
}
/* Tabs */
.login-tabs {
display: flex;
background: rgba(0, 0, 0, 0.03);
border-radius: 10px;
padding: 4px;
margin-bottom: 24px;
gap: var(--space-2xl);
margin-bottom: var(--space-3xl);
}
.login-tab {
flex: 1;
padding: 10px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
color: #5A6480;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s;
font-size: var(--text-lg);
font-weight: 700;
color: var(--color-text-tertiary);
padding-bottom: var(--space-sm);
border-bottom: 2px solid transparent;
transition: all var(--transition-fast);
}
.login-tab.active {
background: rgba(108, 99, 255, 0.15);
color: #8B85FF;
color: var(--color-text-primary);
border-bottom-color: var(--color-accent);
}
.login-tab:hover:not(.active) {
color: #5A6480;
.login-tab:hover {
color: var(--color-text-primary);
}
/* Error message */
.form-error {
padding: 10px 14px;
border-radius: 8px;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.2);
color: #F87171;
font-size: 13px;
text-align: center;
margin-bottom: 16px;
}
/* Form */
.login-form {
display: flex;
flex-direction: column;
gap: 16px;
gap: var(--space-xl);
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
gap: var(--space-sm);
margin-bottom: 16px;
}
.form-label {
font-size: 13px;
font-weight: 500;
color: #5A6480;
}
.required {
color: #EF4444;
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 6px;
}
.form-input {
padding: 12px 14px;
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: rgba(0, 0, 0, 0.02);
color: #1A1F36;
font-size: 14px;
outline: none;
transition: all 0.2s;
width: 100%;
padding: 12px 16px;
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
font-size: var(--text-base);
color: var(--color-text-primary);
background: var(--color-bg-primary);
transition: all var(--transition-fast);
outline: none;
}
.form-input:focus {
border-color: rgba(108, 99, 255, 0.4);
background: rgba(108, 99, 255, 0.05);
box-shadow: 0 0 0 3px rgba(108, 99, 255, 0.1);
border-color: var(--color-accent);
box-shadow: 0 0 0 3px var(--color-accent-light);
background: #FFFFFF;
}
.form-input::placeholder {
color: #3A4460;
color: var(--color-text-disabled);
}
/* Submit button */
.login-submit-btn {
width: 100%;
padding: 13px;
border-radius: 10px;
border: none;
background: linear-gradient(135deg, #6C63FF, #A78BFA);
height: 46px;
background: linear-gradient(135deg, #5B5FED, #6C63FF);
color: white;
font-size: 15px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-top: 8px;
border-radius: var(--radius-lg);
font-size: var(--text-base);
font-weight: 700;
letter-spacing: 0.15em;
transition: all var(--transition-fast);
}
.login-submit-btn:hover:not(:disabled) {
box-shadow: 0 4px 20px rgba(108, 99, 255, 0.4);
.login-submit-btn:hover {
background: linear-gradient(135deg, #4A4ECC, #5B5FED);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.login-submit-btn:active {
transform: translateY(0);
}
.login-submit-btn:disabled {
opacity: 0.6;
opacity: 0.5;
cursor: not-allowed;
}
/* Register method tabs */
.login-submit-btn:disabled:hover {
transform: none;
box-shadow: none;
}
/* Register Method Tabs */
.register-method-tabs {
display: flex;
gap: 12px;
margin-bottom: 4px;
gap: var(--space-sm);
margin-bottom: var(--space-lg);
}
.register-method-tab {
@@ -273,32 +261,32 @@
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.10);
background: transparent;
color: #5A6480;
font-size: 13px;
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-md);
border: 1px solid var(--color-border);
background: var(--color-bg-primary);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
transition: all var(--transition-fast);
}
.register-method-tab:hover {
border-color: var(--color-accent-light);
}
.register-method-tab.active {
border-color: rgba(108, 99, 255, 0.3);
background: rgba(108, 99, 255, 0.1);
color: #8B85FF;
border-color: var(--color-accent);
background: var(--color-accent-subtle);
color: var(--color-accent);
font-weight: 600;
}
.register-method-tab:hover:not(.active) {
border-color: rgba(255, 255, 255, 0.12);
color: #5A6480;
}
/* SMS row */
/* SMS Row */
.sms-row {
display: flex;
gap: 10px;
gap: var(--space-sm);
}
.sms-input {
@@ -306,42 +294,49 @@
}
.sms-btn {
padding: 10px 16px;
border-radius: 10px;
border: 1px solid rgba(108, 99, 255, 0.2);
background: rgba(108, 99, 255, 0.08);
color: #8B85FF;
font-size: 13px;
padding: var(--space-sm) var(--space-lg);
border-radius: var(--radius-md);
border: 1px solid var(--color-accent);
background: transparent;
color: var(--color-accent);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
transition: all var(--transition-fast);
}
.sms-btn:hover:not(:disabled) {
background: rgba(108, 99, 255, 0.15);
background: var(--color-accent);
color: white;
}
.sms-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
border-color: var(--color-border);
color: var(--color-text-tertiary);
}
/* Required Marker */
.required {
color: var(--color-error);
}
/* Responsive */
@media (max-width: 768px) {
.login-container {
flex-direction: column;
width: calc(100vw - 32px);
width: 90vw;
min-height: auto;
margin: 16px;
}
.login-brand-panel {
width: 100%;
padding: 32px 24px;
border-right: none;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding: var(--space-3xl) var(--space-2xl);
}
.login-form-panel {
padding: 32px 24px;
padding: var(--space-2xl);
}
.brand-features {
display: none;
}
}
+40 -26
View File
@@ -65,22 +65,38 @@ const LoginPage: React.FC = () => {
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 { 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 || 'user',
status: 'active', realNameStatus: 'unverified',
createdAt: new Date().toISOString(),
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(userObj.role === 'admin' ? '/admin' : '/chat');
navigate(userData.role === 'admin' ? '/admin' : '/chat');
} catch (e: any) {
const msg = (e as any).detail || (e as any).message || '';
if (msg.includes('Network') || msg.includes('fetch')) {
// 处理后端返回的精确错误信息,区分用户不存在和密码错误
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 || '登录失败,请稍后重试');
@@ -105,10 +121,14 @@ const LoginPage: React.FC = () => {
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',
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);
@@ -138,18 +158,12 @@ const LoginPage: React.FC = () => {
<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"/>
<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">七虎AI</h1>
<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>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -91,7 +91,7 @@
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.06);
background: rgba(255, 255, 255, 0.08);
top: -30px;
right: -30px;
}
@@ -102,7 +102,7 @@
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.04);
background: rgba(255, 255, 255, 0.06);
bottom: -20px;
left: -20px;
}
-228
View File
@@ -1,228 +0,0 @@
// @ts-nocheck
import React, { useState, useEffect, useCallback } from 'react';
import { imUserAPI } from '@/api';
const WechatBot: React.FC = () => {
const [loading, setLoading] = useState(false);
const [status, setStatus] = useState<any>({ connected: false, configured: false });
const [qrcode, setQrcode] = useState<any>(null);
const [polling, setPolling] = useState(false);
const [reconnecting, setReconnecting] = useState(false);
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
};
// 加载当前用户的连接状态
const loadStatus = useCallback(async () => {
try {
const res = await imUserAPI.getStatus();
if ((res as any)?.success) setStatus(res.data || { connected: false, configured: false });
} catch (e) {
// 静默失败
}
}, []);
useEffect(() => {
loadStatus();
}, [loadStatus]);
// 获取扫码二维码(iLink 协议免 AppID)
const handleQrcode = async () => {
setLoading(true);
try {
const res = await imUserAPI.qrcode();
if ((res as any)?.success && (res as any).data) {
setQrcode((res as any).data);
setPolling(true);
showToast('二维码已生成,请用手机微信扫码');
} else {
showToast((res as any).error || '获取二维码失败', 'error');
}
} catch (e) {
showToast('获取二维码失败', 'error');
} finally {
setLoading(false);
}
};
// 轮询扫码状态
const handlePoll = useCallback(async () => {
if (!qrcode?.qrcode) return;
try {
const res = await imUserAPI.qrcodeStatus(qrcode.qrcode);
if ((res as any)?.success && (res as any).data) {
const d = (res as any).data;
if (d.status === 'confirmed' || d.connected) {
setPolling(false);
setReconnecting(false);
setStatus({ ...status, connected: true, configured: true });
showToast('个人微信已连接!');
loadStatus();
} else if (d.status === 'expired') {
setPolling(false);
showToast('二维码已过期,请重新获取', 'error');
}
}
} catch (e) {
// 忽略轮询错误
}
}, [qrcode, status, loadStatus]);
useEffect(() => {
if (polling && qrcode?.qrcode) {
const t = setInterval(() => handlePoll(), 3000);
return () => clearInterval(t);
}
}, [polling, qrcode, handlePoll]);
// 断开连接
const handleDisconnect = async () => {
try {
await imUserAPI.disconnect();
setPolling(false);
setQrcode(null);
setReconnecting(false);
setStatus({ connected: false, configured: false });
showToast('已断开个人微信连接');
loadStatus();
} catch (e) {
showToast('断开失败', 'error');
}
};
const formatTime = (t: string) => (t ? new Date(t).toLocaleString('zh-CN') : '-');
return (
<div style={{ minHeight: '100%', background: '#FFFFFF', color: '#1A1F36', padding: '32px' }}>
<div style={{ maxWidth: '860px', margin: '0 auto' }}>
{/* Header */}
<div style={{ marginBottom: '24px' }}>
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0, color: '#1A1F36' }}>我的微信机器人</h1>
<p style={{ color: '#6B7280', fontSize: '14px', margin: '6px 0 0 0' }}>
绑定你自己的个人微信,由七虎AI 大脑自动应答。基于腾讯官方 iLink 协议,扫码授权即可,无需任何开放平台凭证。
</p>
</div>
{/* Toast */}
{toast && (
<div style={{
position: 'fixed', top: '20px', right: '20px', zIndex: 9999,
padding: '12px 24px', borderRadius: '8px',
background: toast.type === 'success' ? '#10b981' : '#ef4444',
color: '#fff', fontSize: '14px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}>{toast.msg}</div>
)}
{/* 状态条 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: '#F8F9FC', borderRadius: '12px', padding: '16px 24px', marginBottom: '16px' }}>
<div>
<h3 style={{ margin: 0, fontSize: '16px', color: '#1A1F36' }}>个人微信(iLink 协议)</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '13px', color: '#6B7280' }}>
直接对接腾讯官方 iLink Bot HTTP API,扫码授权后由七虎AI 自动回复。
</p>
</div>
<span style={{
padding: '4px 12px', borderRadius: '4px', fontSize: '12px',
background: status.connected ? '#10b98120' : status.status === 'expired' ? '#ef444420' : '#6b728020',
color: status.connected ? '#10b981' : status.status === 'expired' ? '#ef4444' : '#6b7280',
}}>
{status.connected ? '● 已连接' : status.status === 'expired' ? '⚠ 会话已过期' : status.configured ? '○ 已配置' : '○ 未绑定'}
</span>
</div>
{/* 扫码绑定 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}>
{status.status === 'expired' && (
<div style={{ background: '#fef2f2', border: '1px solid #fecaca', color: '#b91c1c', borderRadius: '8px', padding: '12px 14px', marginBottom: '16px', fontSize: '13px', lineHeight: '1.7' }}>
⚠ <b>微信会话已失效</b>:检测到与微信的连接已过期(通常是服务更新/重启后导致),消息无法再送达。请点击下方「获取扫码二维码」<b>重新扫码授权</b>即可恢复,历史对话记录仍保留。
</div>
)}
<h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>扫码授权绑定</h4>
{status.connected && !reconnecting ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#10b981' }}>已绑定,你的个人微信正在自动收发消息并由七虎AI 应答。</div>
<div>
<button onClick={() => { setReconnecting(true); handleQrcode(); }} style={secondaryBtnStyle}>重新连接(重扫二维码)</button>
</div>
<div style={{ fontSize: '12px', color: '#9CA3AF' }}>若发现消息收不到,可点此重新扫码以重建与微信的会话。</div>
</div>
) : !qrcode ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '12px' }}>
<div style={{ fontSize: '13px', color: '#6B7280' }}>
点击「获取扫码二维码」,使用手机微信扫描二维码并确认授权,即可将你的个人微信接入七虎AI。整个过程无需申请任何开放平台凭证。
</div>
<button onClick={handleQrcode} disabled={loading} style={primaryBtnStyle}>
{loading ? '生成中…' : '获取扫码二维码'}
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
{qrcode.qrcode_img ? (
<img src={qrcode.qrcode_img} alt="微信扫码" style={{ width: '220px', height: '220px', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB' }} />
) : (
<div style={{ width: '220px', height: '220px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB', fontSize: '12px', color: '#9CA3AF', textAlign: 'center', padding: '12px' }}>
二维码图片生成失败,<a href={qrcode.qrcode_img_content} target="_blank" rel="noreferrer" style={{color:'#6C63FF'}}>请点击此处在新标签页打开</a>。
</div>
)}
<div style={{ fontSize: '13px', color: polling ? '#f59e0b' : '#6B7280' }}>
{polling ? '等待扫码中…(每 3 秒自动检测一次)' : '已停止轮询'}
</div>
<div style={{ display: 'flex', gap: '12px' }}>
{polling && (
<button onClick={() => setPolling(false)} style={secondaryBtnStyle}>取消轮询</button>
)}
<button onClick={handleDisconnect} style={{ ...secondaryBtnStyle, color: '#ef4444', borderColor: '#ef4444' }}>断开连接</button>
</div>
</div>
)}
</div>
{/* 已连接信息 */}
{status.connected && (
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginBottom: '16px' }}>
<h4 style={{ marginTop: 0, fontSize: '15px', color: '#1A1F36', marginBottom: '12px' }}>连接信息</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px' }}>
<DetailItem label="绑定账号" value={status.username || '-'} />
<DetailItem label="连接时间" value={formatTime(status.connected_at)} />
<DetailItem label="iLink Bot ID" value={status.ilink_bot_id || '-'} />
<DetailItem label="Base URL" value={status.baseurl || '-'} />
</div>
</div>
)}
{/* 说明 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '24px', marginTop: '16px' }}>
<h4 style={{ marginTop: 0, fontSize: '14px', color: '#888' }}>接入说明</h4>
<ul style={{ fontSize: '13px', color: '#6B7280', lineHeight: '1.8', paddingLeft: '20px', margin: 0 }}>
<li>每个账号独立绑定自己的个人微信,数据严格按账号隔离,互不影响</li>
<li>个人微信通过腾讯官方 iLink Bot HTTP API(微信 ClawBot 插件)接入,消息经七虎AI 大脑生成回复</li>
<li>采用官方合法协议,无需 Hook / 逆向,封号风险远低于历史方案,但仍请遵守微信平台规范</li>
<li>消息收发基于长轮询(getupdates)+ 官方 sendmessage,回复自动关联对话窗口(context_token)</li>
</ul>
</div>
</div>
</div>
);
};
const DetailItem: React.FC<{ label: string; value: any }> = ({ label, value }) => (
<div style={{ padding: '8px 12px', background: '#FFFFFF', borderRadius: '6px' }}>
<div style={{ fontSize: '12px', color: '#666' }}>{label}</div>
<div style={{ fontSize: '14px', marginTop: '2px', wordBreak: 'break-all' }}>{value || '-'}</div>
</div>
);
const primaryBtnStyle: React.CSSProperties = {
padding: '8px 20px', background: '#6C63FF', border: 'none', borderRadius: '6px',
color: '#fff', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
};
const secondaryBtnStyle: React.CSSProperties = {
padding: '8px 20px', background: '#F0F2F8', border: '1px solid #F0F2F8', borderRadius: '6px',
color: '#1A1F36', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
};
export default WechatBot;
@@ -1,97 +0,0 @@
// @ts-nocheck
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { imUserAPI } from '@/api';
const WechatHistory: React.FC = () => {
const [loading, setLoading] = useState(true);
const [messages, setMessages] = useState<any[]>([]);
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 3000);
};
const load = useCallback(async () => {
setLoading(true);
try {
const res = await imUserAPI.history();
if ((res as any)?.success && (res as any).data) {
setMessages((res as any).data.messages || []);
} else {
showToast('加载失败', 'error');
}
} catch (e) {
showToast('加载失败', 'error');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
const formatTime = (t: string) => (t ? new Date(t).toLocaleString('zh-CN') : '-');
return (
<div style={{ minHeight: '100%', background: '#FFFFFF', color: '#1A1F36', padding: '32px' }}>
<div style={{ maxWidth: '860px', margin: '0 auto' }}>
{/* Header */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h1 style={{ fontSize: '24px', fontWeight: 700, margin: 0, color: '#1A1F36' }}>微信对话记录</h1>
<p style={{ color: '#6B7280', fontSize: '14px', margin: '6px 0 0 0' }}>
你通过个人微信与七虎AI 的对话历史,按时间顺序记录,可随时回看(仅本人可见,按账号隔离)。
</p>
</div>
<button onClick={load} disabled={loading} style={btnStyle}>{loading ? '刷新中…' : '刷新'}</button>
</div>
{/* Toast */}
{toast && (
<div style={{
position: 'fixed', top: '20px', right: '20px', zIndex: 9999,
padding: '12px 24px', borderRadius: '8px',
background: toast.type === 'success' ? '#10b981' : '#ef4444',
color: '#fff', fontSize: '14px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
}}>{toast.msg}</div>
)}
{/* 记录列表 */}
<div style={{ background: '#F8F9FC', borderRadius: '12px', padding: '16px', minHeight: '420px' }}>
{messages.length === 0 ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '380px', color: '#9CA3AF', fontSize: '14px', textAlign: 'center', padding: '0 24px' }}>
{loading ? '加载中…' : '暂无对话记录。请先在「微信机器人」中绑定并发送消息,对话记录会显示在这里。'}
</div>
) : (
messages.map((m, i) => (
<div key={i} style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start', marginBottom: '14px' }}>
<div style={{ maxWidth: '78%' }}>
<div style={{ fontSize: '11px', color: '#9CA3AF', marginBottom: '4px', textAlign: m.role === 'user' ? 'right' : 'left' }}>
{m.role === 'user' ? '我(微信)' : '七虎AI'} · {formatTime(m.created_at)}
</div>
<div style={{
padding: '10px 14px', borderRadius: '12px', fontSize: '14px', lineHeight: '1.6',
whiteSpace: 'pre-wrap', wordBreak: 'break-word',
background: m.role === 'user' ? '#6C63FF' : '#FFFFFF',
color: m.role === 'user' ? '#fff' : '#1A1F36',
border: m.role === 'user' ? 'none' : '1px solid #E5E7EB',
}}>{m.content}</div>
</div>
</div>
))
)}
<div ref={bottomRef} />
</div>
</div>
</div>
);
};
const btnStyle: React.CSSProperties = {
padding: '8px 20px', background: '#6C63FF', border: 'none', borderRadius: '6px',
color: '#fff', fontSize: '14px', fontWeight: 500, cursor: 'pointer',
};
export default WechatHistory;