Files
ai-learning-platform/frontend/src/app/page.tsx
T
TradeMate Dev 8f13604e0e chore: 全站改为免费+联盟模式,原支付保留备用
- 课程详情页下架付费购买与扫码支付弹窗
- 首页/能力导览文案 购买→免费使用
- 付费面(会员/技能广场/用量包/课程)均已免费化,原 orders/payment 模块保留后端备用
2026-07-11 17:08:32 +08:00

305 lines
19 KiB
TypeScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import { HomePageClient } from './home-client';
import { ArrowRight, Sparkles, BookOpen, Bot, Wrench, GraduationCap, FileText, Puzzle, MessageSquare, Code, BarChart3 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Card } from '@/components/ui/card';
import { useT } from '@/i18n';
import { API_BASE } from '@/lib/config';
export default function HomePage() {
const t = useT();
const [courses, setCourses] = useState<any[]>([]);
const [featuredTools, setFeaturedTools] = useState<any[]>([]);
const [stats, setStats] = useState<{ tools: number; courses: number; prompts: number; practices: number } | null>(null);
useEffect(() => {
Promise.all([
fetch(`${API_BASE}/tools?isFeatured=true&pageSize=6&categoryId=7`).then(r => r.json()),
fetch(`${API_BASE}/courses?pageSize=3`).then(r => r.json()),
fetch(`${API_BASE}/tools?pageSize=1`).then(r => r.json()),
fetch(`${API_BASE}/prompts?pageSize=1`).then(r => r.json()),
fetch(`${API_BASE}/practices?pageSize=1`).then(r => r.json()),
]).then(([toolsData, coursesData, toolsAll, promptsData, practicesData]) => {
setFeaturedTools(toolsData.items || []);
setCourses(coursesData.items || []);
setStats({
tools: toolsAll.total || toolsAll.items?.length || 0,
courses: coursesData.total || coursesData.items?.length || 0,
prompts: promptsData.total || promptsData.items?.length || 0,
practices: practicesData.total || practicesData.items?.length || 0,
});
}).catch(() => {
setStats({ tools: 0, courses: 0, prompts: 0, practices: 0 });
});
}, []);
const statItems = [
{ value: stats ? `${stats.tools}+` : '--', label: '收录工具', href: '/tools', icon: Wrench },
{ value: stats ? `${stats.courses}+` : '--', label: '课程资源', href: '/courses', icon: BookOpen },
{ value: stats ? `${stats.prompts}+` : '--', label: '提示词库', href: '/prompts', icon: FileText },
{ value: stats ? `${stats.practices}+` : '--', label: '练习场景', href: '/practices', icon: Puzzle },
];
return (
<HomePageClient>
{/* Hero */}
<section className="relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-brand-50 via-white to-blue-50 dark:from-brand-950/30 dark:via-background dark:to-blue-950/20" />
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjAiIGhlaWdodD0iNjAiIHZpZXdCb3g9IjAgMCA2MCA2MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxnIGZpbGw9IiM2NjdlZWEiIGZpbGwtb3BhY2l0eT0iMC4wNCI+PHBhdGggZD0iTTM2IDM0djItSDI0di0xaDEyek0zNiAyNHYySDI0di0yaDEyeiIvPjwvZz48L2c+PC9zdmc+')] opacity-50 dark:opacity-20" />
<div className="absolute top-20 right-0 w-96 h-96 bg-brand-400/10 dark:bg-brand-400/5 rounded-full blur-3xl" />
<div className="absolute bottom-0 left-20 w-72 h-72 bg-blue-400/10 dark:bg-blue-400/5 rounded-full blur-3xl" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20 md:py-28 relative">
<div className="text-center max-w-3xl mx-auto animate-fade-in-up">
<span className="inline-flex items-center gap-1.5 px-4 py-1.5 text-sm font-medium text-brand-700 dark:text-brand-300 bg-brand-100 dark:bg-brand-900/50 rounded-full mb-8 border border-brand-200 dark:border-brand-800">
<Sparkles className="w-3.5 h-3.5" />
AI工具 ·
</span>
<h1 className="text-4xl md:text-6xl font-bold tracking-tight leading-tight">
<span className="bg-gradient-to-r from-brand-600 via-brand-500 to-blue-500 bg-clip-text text-transparent">
AI工具
</span>
</h1>
<p className="mt-6 text-lg md:text-xl text-muted-foreground leading-relaxed max-w-2xl mx-auto">
AI工具使AI应用能力
</p>
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
<Link href="/tools" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-all shadow-lg shadow-brand-200/50 dark:shadow-brand-900/30 hover:shadow-xl hover:-translate-y-0.5 active:scale-[0.98]">
<Wrench className="w-5 h-5" />
<ArrowRight className="w-4 h-4" />
</Link>
<Link href="/sandbox" className="inline-flex items-center justify-center gap-2 px-8 py-3 text-base font-medium text-foreground bg-card border border-border rounded-xl hover:bg-accent transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-sm">
<Bot className="w-5 h-5" />
</Link>
</div>
</div>
</div>
</section>
{/* Stats */}
<section className="py-12 md:py-16 border-y border-border">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8">
{statItems.map((s) => (
<Link key={s.label} href={s.href} className="text-center group">
<div className="text-3xl md:text-4xl font-bold bg-gradient-to-b from-brand-600 to-brand-400 bg-clip-text text-transparent group-hover:scale-110 transition-transform">
{s.value}
</div>
<div className="mt-1.5 flex items-center justify-center gap-1.5 text-sm text-muted-foreground group-hover:text-brand-600 transition-colors">
<s.icon className="w-3.5 h-3.5" />
{s.label}
<ArrowRight className="w-3 h-3 opacity-0 group-hover:opacity-100 transition-opacity" />
</div>
</Link>
))}
</div>
</div>
</section>
{/* Featured Tools */}
{featuredTools.length > 0 && (
<section className="py-16 bg-muted/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-3xl font-bold">AI工具推荐</h2>
<p className="mt-2 text-muted-foreground"></p>
</div>
<Link href="/tools" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{featuredTools.map((tool: any) => {
const highlightMatch = tool.description?.match(/【推荐理由】\s*(.+?)(?:\n|$)/);
const baseDesc = tool.description?.split('\n\n')[0] || '';
return (
<Link key={tool.id} href={`/tools/${tool.slug}`} className="block group">
<Card className="p-4 hover:shadow-md hover:border-brand-200 dark:hover:border-brand-800 transition-all group h-full">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-brand-100 dark:bg-brand-900/30 rounded-xl flex items-center justify-center shrink-0 group-hover:scale-110 transition-transform">
<Wrench className="w-5 h-5 text-brand-600 dark:text-brand-400" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold group-hover:text-brand-600 transition-colors truncate">{tool.name}</h3>
<p className="text-xs text-muted-foreground line-clamp-1">{baseDesc}</p>
</div>
<ArrowRight className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity shrink-0" />
</div>
{highlightMatch && (
<div className="mt-2 px-2 py-1 bg-brand-50 dark:bg-brand-950/40 rounded-md">
<p className="text-xs text-brand-600 dark:text-brand-400 font-medium">💡 {highlightMatch[1].trim().slice(0, 35)}</p>
</div>
)}
{tool.tags && (
<div className="flex gap-1.5 mt-2 flex-wrap">
{tool.tags.split(',').slice(0, 3).map((tag: string) => (
<span key={tag} className="text-xs px-2 py-0.5 bg-muted rounded-full text-muted-foreground">{tag.trim()}</span>
))}
</div>
)}
</Card>
</Link>
);
})}
</div>
</div>
</section>
)}
{/* Marketplace Skills Section */}
<section className="py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-3xl font-bold">💼 AI 广</h2>
<p className="mt-2 text-muted-foreground"> AI ¥9.9 </p>
</div>
<Link href="/marketplace" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
<ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Link href="/marketplace" className="group">
<Card className="p-5 hover:shadow-md hover:border-brand-200 dark:hover:border-brand-800 transition-all h-full cursor-pointer">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-xl bg-emerald-100 dark:bg-emerald-900/30 flex items-center justify-center shrink-0">
<span className="text-xl"></span>
</div>
<div>
<h3 className="font-semibold text-foreground"></h3>
<p className="text-xs text-muted-foreground">AI </p>
</div>
</div>
<div className="flex items-center justify-between">
<Badge variant="secondary" className="bg-emerald-50 dark:bg-emerald-950/40 text-emerald-700 dark:text-emerald-400">¥9.9</Badge>
<span className="text-xs text-muted-foreground">1000+</span>
</div>
</Card>
</Link>
<Link href="/marketplace" className="group">
<Card className="p-5 hover:shadow-md hover:border-brand-200 dark:hover:border-brand-800 transition-all h-full cursor-pointer">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center shrink-0">
<span className="text-xl">📋</span>
</div>
<div>
<h3 className="font-semibold text-foreground"></h3>
<p className="text-xs text-muted-foreground">5</p>
</div>
</div>
<div className="flex items-center justify-between">
<Badge variant="secondary" className="bg-blue-50 dark:bg-blue-950/40 text-blue-700 dark:text-blue-400">¥19.9</Badge>
<span className="text-xs text-muted-foreground">6</span>
</div>
</Card>
</Link>
<Link href="/marketplace" className="group">
<Card className="p-5 hover:shadow-md hover:border-brand-200 dark:hover:border-brand-800 transition-all h-full cursor-pointer">
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center shrink-0">
<span className="text-xl"></span>
</div>
<div>
<h3 className="font-semibold text-foreground"></h3>
<p className="text-xs text-muted-foreground">AI </p>
</div>
</div>
<div className="flex items-center justify-between">
<Badge variant="secondary" className="bg-purple-50 dark:bg-purple-950/40 text-purple-700 dark:text-purple-400">¥29.9</Badge>
<span className="text-xs text-muted-foreground"></span>
</div>
</Card>
</Link>
</div>
</div>
</section>
{/* AI 能力导览 */}
<section className="py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold">AI </h2>
<p className="mt-4 text-lg text-muted-foreground"> AI </p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[
{ icon: MessageSquare, title: '对话写作', desc: '与 AI 进行自然对话,获取灵感、润色文案、翻译语言', href: '/sandbox', cta: '打开沙盒' },
{ icon: Code, title: '编程辅助', desc: '生成代码、调试错误、解释算法,你的随身编程搭档', href: '/sandbox', cta: '打开沙盒' },
{ icon: BarChart3, title: '数据分析', desc: '分析数据、生成图表、洞察趋势,让数据说话', href: '/sandbox', cta: '打开沙盒' },
{ icon: FileText, title: '提示词工程', desc: '学习编写高效提示词,掌握与 AI 沟通的最佳实践', href: '/prompts', cta: '浏览提示词库' },
{ icon: GraduationCap, title: '技能实战', desc: '在真实场景中练习 AI 技能,获得即时评分反馈', href: '/practices', cta: '开始练习' },
{ icon: Puzzle, title: '工作流技能', desc: '免费使用的 AI 工作流技能,解决具体业务问题', href: '/marketplace', cta: '前往技能广场' },
].map((item) => {
const Icon = item.icon;
return (
<Link key={item.title} href={item.href} className="group bg-card rounded-xl border border-border p-6 hover:border-brand-200 hover:shadow-sm transition-all">
<div className="w-10 h-10 rounded-lg bg-brand-50 dark:bg-brand-950 flex items-center justify-center mb-4 group-hover:bg-brand-100 dark:group-hover:bg-brand-900 transition-colors">
<Icon className="h-5 w-5 text-brand-600 dark:text-brand-400" />
</div>
<h3 className="text-lg font-semibold text-foreground mb-2">{item.title}</h3>
<p className="text-sm text-muted-foreground mb-4 leading-relaxed">{item.desc}</p>
<span className="text-sm font-medium text-brand-600 dark:text-brand-400 group-hover:underline">
{item.cta}
</span>
</Link>
);
})}
</div>
</div>
</section>
{/* Popular Courses */}
<section className="py-20 bg-muted/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between mb-10">
<div>
<h2 className="text-3xl font-bold">{t.home.popularTopics}</h2>
<p className="mt-2 text-muted-foreground">{t.home.popularDesc}</p>
</div>
<Link href="/courses" className="hidden sm:inline-flex items-center gap-1 text-brand-600 hover:text-brand-700 font-medium text-sm group">
{t.common.viewAll} <ArrowRight className="w-4 h-4 group-hover:translate-x-0.5 transition-transform" />
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{courses.length > 0 ? courses.map((course: any) => (
<Link key={course.id} href={`/courses/${course.id}`} className="block group">
<div className="group bg-card rounded-xl border border-border overflow-hidden hover:shadow-xl transition-all hover:-translate-y-1">
<div className="h-2 bg-gradient-to-r from-brand-500 to-blue-500" />
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-brand-700 dark:text-brand-300 bg-brand-50 dark:bg-brand-900/30 px-2 py-1 rounded-full">{course.isFree ? t.courses.free : t.courses.paid}</span>
</div>
<h3 className="text-lg font-semibold mb-2 group-hover:text-brand-600 transition-colors">{course.title}</h3>
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">{course.description || ''}</p>
</div>
</div>
</Link>
)) : (
<div className="col-span-3 text-center py-12 text-muted-foreground">{t.common.loading}</div>
)}
</div>
</div>
</section>
{/* CTA */}
<section className="py-20 bg-gradient-to-r from-brand-600 to-brand-800 dark:from-brand-900 dark:to-brand-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMjAgMjB2MTBoLTEwVjIwaDEwek0yMCAwaDEwdjEwSDIwVjB6IiBmaWxsPSIjZmZmIiBmaWxsLW9wYWNpdHk9IjAuMDMiLz48L3N2Zz4=')] opacity-50" />
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative">
<h2 className="text-3xl font-bold text-white mb-4">{t.home.ctaTitle}</h2>
<p className="text-brand-100/80 dark:text-brand-200/80 mb-8 text-lg">{t.home.ctaDesc}</p>
<Link href="/auth?tab=register" className="inline-flex items-center gap-2 px-8 py-3 text-base font-medium text-brand-600 bg-white rounded-xl hover:bg-brand-50 transition-all hover:-translate-y-0.5 active:scale-[0.98] shadow-xl">
{t.home.freeRegister}
<ArrowRight className="w-4 h-4" />
</Link>
</div>
</section>
</HomePageClient>
);
}