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:
1 parent
1346fbbd4b
commit
5d7f32db0a
138 files changed
+12846
-84248
No files matched your search
+1047
-1137
File diff suppressed because it is too large.
Load diff
+1080
-1163
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
@@ -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>© 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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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">✓</span><span>一站式AI自动化SOP</span></div>
|
||||
|
||||
+1606
-1637
File diff suppressed because it is too large.
Load diff
+1578
-1609
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user