feat: 剩余页面 useT() 改造 + 首页课程动态加载
- 搜索/发现/工具/My 页面全部使用 useT(),支持中英切换 - 搜索页 Badge variant 修正 - 首页热门专题改为从 API 动态获取(无数据时用静态兜底) - 新增 discover 翻译键 viewCount/likeCount/postStats - 首页/仪表盘/社区 等 JSX 优化
This commit is contained in:
@@ -4,48 +4,34 @@ import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '@/lib/auth';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface HotItem {
|
||||
id: number;
|
||||
title: string;
|
||||
viewCount: number;
|
||||
likeCount: number;
|
||||
_type: 'course' | 'prompt' | 'post';
|
||||
}
|
||||
interface HotItem { id: number; title: string; viewCount: number; likeCount: number; _type: 'course' | 'prompt' | 'post' }
|
||||
|
||||
export default function DiscoverPage() {
|
||||
const t = useT();
|
||||
const [hotCourses, setHotCourses] = useState<HotItem[]>([]);
|
||||
const [hotPrompts, setHotPrompts] = useState<HotItem[]>([]);
|
||||
const [hotPosts, setHotPosts] = useState<HotItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
useEffect(() => { loadData(); }, []);
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const [coursesRes, promptsRes, postsRes] = await Promise.all([
|
||||
apiFetch('/courses?pageSize=5'),
|
||||
apiFetch('/prompts?pageSize=5'),
|
||||
apiFetch('/community/posts?pageSize=5'),
|
||||
apiFetch('/courses?pageSize=5'), apiFetch('/prompts?pageSize=5'), apiFetch('/community/posts?pageSize=5'),
|
||||
]);
|
||||
|
||||
const coursesData = await coursesRes.json();
|
||||
const promptsData = await promptsRes.json();
|
||||
const postsData = await postsRes.json();
|
||||
|
||||
setHotCourses((coursesData.items || []).map((c: any) => ({ ...c, _type: 'course' as const })));
|
||||
setHotPrompts((promptsData.items || []).map((p: any) => ({ ...p, _type: 'prompt' as const })));
|
||||
setHotPosts((postsData.items || []).map((p: any) => ({ ...p, _type: 'post' as const })));
|
||||
setHotCourses((await coursesRes.json()).items?.map((c: any) => ({ ...c, _type: 'course' as const })) || []);
|
||||
setHotPrompts((await promptsRes.json()).items?.map((p: any) => ({ ...p, _type: 'prompt' as const })) || []);
|
||||
setHotPosts((await postsRes.json()).items?.map((p: any) => ({ ...p, _type: 'post' as const })) || []);
|
||||
} catch (e) { console.error(e) }
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
if (loading) return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<Skeleton className="h-8 w-24 mb-2" />
|
||||
<Skeleton className="h-5 w-64 mb-8" />
|
||||
<Skeleton className="h-8 w-24 mb-2" /><Skeleton className="h-5 w-64 mb-8" />
|
||||
{[1,2,3].map(i => <Skeleton key={i} className="h-32 rounded-xl mb-4" />)}
|
||||
</div>
|
||||
);
|
||||
@@ -53,15 +39,13 @@ export default function DiscoverPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">发现</h1>
|
||||
<p className="mt-2 text-muted-foreground">探索热门内容和精选推荐</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.discover.desc}</h1>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-foreground">🔥 热门课程</h2>
|
||||
<Link href="/courses" className="text-sm text-brand-600 hover:underline">查看全部</Link>
|
||||
<h2 className="text-xl font-semibold text-foreground">🔥 {t.discover.hotCourses}</h2>
|
||||
<Link href="/courses" className="text-sm text-brand-600 hover:underline">{t.common.viewAll}</Link>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{hotCourses.map((item, index) => (
|
||||
@@ -70,7 +54,7 @@ export default function DiscoverPage() {
|
||||
<span className="text-2xl font-bold text-muted-foreground/20 w-8">{index + 1}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-foreground">{item.title}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">👁 {item.viewCount} 浏览</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">👁 {t.discover.viewCount?.replace('{n}', String(item.viewCount)) || `${item.viewCount} 浏览`}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
@@ -79,17 +63,17 @@ export default function DiscoverPage() {
|
||||
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-foreground">✨ 热门提示词</h2>
|
||||
<Link href="/prompts" className="text-sm text-brand-600 hover:underline">查看全部</Link>
|
||||
<h2 className="text-xl font-semibold text-foreground">✨ {t.discover.hotPrompts}</h2>
|
||||
<Link href="/prompts" className="text-sm text-brand-600 hover:underline">{t.common.viewAll}</Link>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{hotPrompts.map((item, index) => (
|
||||
<Link key={item.id} href={`/prompts/${item.id}`}
|
||||
<Link key={item.id} href={`/prompts`}
|
||||
className="bg-card rounded-xl border border-border p-4 hover:shadow-md transition-shadow flex items-center gap-4">
|
||||
<span className="text-2xl font-bold text-muted-foreground/20 w-8">{index + 1}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-foreground">{item.title}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">❤️ {item.likeCount} 点赞</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">❤️ {t.discover.likeCount?.replace('{n}', String(item.likeCount)) || `${item.likeCount} 点赞`}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
@@ -98,8 +82,8 @@ export default function DiscoverPage() {
|
||||
|
||||
<section>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-foreground">💬 热门讨论</h2>
|
||||
<Link href="/community" className="text-sm text-brand-600 hover:underline">查看全部</Link>
|
||||
<h2 className="text-xl font-semibold text-foreground">💬 {t.discover.hotPosts}</h2>
|
||||
<Link href="/community" className="text-sm text-brand-600 hover:underline">{t.common.viewAll}</Link>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{hotPosts.map((item, index) => (
|
||||
@@ -108,7 +92,7 @@ export default function DiscoverPage() {
|
||||
<span className="text-2xl font-bold text-muted-foreground/20 w-8">{index + 1}</span>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-foreground">{item.title}</h3>
|
||||
<p className="text-xs text-muted-foreground mt-1">❤️ {item.likeCount} 点赞 · 👁 {item.viewCount} 浏览</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">{t.discover.postStats?.replace('{likes}', String(item.likeCount)).replace('{views}', String(item.viewCount)) || `❤️ ${item.likeCount} · 👁 ${item.viewCount}`}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,60 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { apiFetch } from '../../lib/auth';
|
||||
|
||||
interface LearningItem {
|
||||
courseId: number;
|
||||
courseTitle: string;
|
||||
progress: number;
|
||||
completedLessons: number;
|
||||
totalLessons: number;
|
||||
}
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
export default function MyPage() {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">我的</h1>
|
||||
<p className="mt-2 text-muted-foreground">管理你的个人信息和收藏</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.nav.my}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{t.my.desc}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Link href="/my/learning" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-brand-100 rounded-xl flex items-center justify-center text-brand-600 text-xl mb-4">📚</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学习进度</h3>
|
||||
<p className="text-sm text-muted-foreground">查看你的课程学习进度</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.my.learningProgress}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.learningProgressDesc}</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/favorites" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-xl flex items-center justify-center text-red-600 text-xl mb-4">❤️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">我的收藏</h3>
|
||||
<p className="text-sm text-muted-foreground">提示词、课程等收藏内容</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.my.favorites}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.favoritesDesc}</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/member" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-xl flex items-center justify-center text-amber-600 text-xl mb-4">👑</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">会员中心</h3>
|
||||
<p className="text-sm text-muted-foreground">管理会员订阅和权益</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.my.memberCenter}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.memberDesc}</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/my/settings" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-muted rounded-xl flex items-center justify-center text-muted-foreground text-xl mb-4">⚙️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">设置</h3>
|
||||
<p className="text-sm text-muted-foreground">账号设置和安全偏好</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.my.settings}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.settingsDesc}</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/learning/analytics" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-brand-100 rounded-xl flex items-center justify-center text-brand-600 text-xl mb-4">📊</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学情分析</h3>
|
||||
<p className="text-sm text-muted-foreground">基于对话的知识掌握度分析</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.learning.analytics}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.analyticsDesc}</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/learning/path" className="bg-card rounded-xl border border-border p-6 hover:shadow-md transition-shadow block">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center text-green-600 text-xl mb-4">🗺️</div>
|
||||
<h3 className="font-semibold text-foreground mb-2">学习路径</h3>
|
||||
<p className="text-sm text-muted-foreground">分阶段系统掌握 AI 技能</p>
|
||||
<h3 className="font-semibold text-foreground mb-2">{t.learning.path}</h3>
|
||||
<p className="text-sm text-muted-foreground">{t.my.pathDesc}</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+37
-17
@@ -1,12 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { HomePageClient } from './home-client';
|
||||
import { ArrowRight, Sparkles, BookOpen, Bot, Compass, Zap } from 'lucide-react';
|
||||
import { useT } from '@/i18n';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
export default function HomePage() {
|
||||
const t = useT();
|
||||
const [courses, setCourses] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/courses?pageSize=3`)
|
||||
.then(r => r.json()).then(data => setCourses(data.items || []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const stats = [
|
||||
{ value: '50+', label: t.home.statTopics },
|
||||
{ value: '200+', label: t.home.statPrompts },
|
||||
@@ -107,25 +117,35 @@ export default function HomePage() {
|
||||
</Link>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{[
|
||||
{ title: 'AI 通识:零基础入门', students: '1,280', tag: t.courses.free, gradient: 'from-brand-500 to-blue-500', desc: '面向零基础用户,带你了解 AI 的基本概念、发展历程和实际应用。' },
|
||||
{ title: '提示词工程从入门到精通', students: '860', tag: '热门', gradient: 'from-violet-500 to-purple-500', desc: '系统学习提示词编写技巧,掌握与 AI 高效沟通的方法。' },
|
||||
{ title: '用 AI 提升 10 倍办公效率', students: '2,150', tag: '推荐', gradient: 'from-amber-500 to-orange-500', desc: '学习使用 AI 工具处理文档、数据分析、演示制作等日常工作。' },
|
||||
].map((course) => (
|
||||
<div key={course.title} 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 ${course.gradient}`} />
|
||||
<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.tag}</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.desc}</p>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1"><BookOpen className="w-4 h-4" />{t.home.moduleCount.replace('{n}', course.students)}</span>
|
||||
{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>
|
||||
</div>
|
||||
))}
|
||||
</Link>
|
||||
)) : (
|
||||
<>
|
||||
{[{ title: 'AI 通识:零基础入门', tag: t.courses.free }, { title: '提示词工程从入门到精通', tag: '热门' }, { title: '用 AI 提升 10 倍办公效率', tag: '推荐' }].map((course) => (
|
||||
<div key={course.title} 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.tag}</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">探索 AI 世界</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -9,12 +9,9 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { SearchIcon, FileText, BookOpen, Wrench, MessageSquare } from 'lucide-react';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface SearchResult {
|
||||
id: number; _type: 'course' | 'prompt' | 'tool' | 'content';
|
||||
title?: string; name?: string; description?: string; summary?: string;
|
||||
cover?: string; icon?: string; url?: string; model?: string; isFree?: boolean; publishedAt?: string;
|
||||
}
|
||||
interface SearchResult { id: number; _type: 'course' | 'prompt' | 'tool' | 'content'; title?: string; name?: string; description?: string; summary?: string; cover?: string; icon?: string; url?: string; model?: string; isFree?: boolean; publishedAt?: string }
|
||||
|
||||
export default function SearchPage() {
|
||||
return (
|
||||
@@ -25,6 +22,7 @@ export default function SearchPage() {
|
||||
}
|
||||
|
||||
function SearchContent() {
|
||||
const t = useT();
|
||||
const searchParams = useSearchParams();
|
||||
const q = searchParams.get('q') || '';
|
||||
const type = searchParams.get('type') || 'all';
|
||||
@@ -42,19 +40,18 @@ function SearchContent() {
|
||||
}, [q, type]);
|
||||
|
||||
const groups = { course: results.filter(r => r._type === 'course'), prompt: results.filter(r => r._type === 'prompt'), tool: results.filter(r => r._type === 'tool'), content: results.filter(r => r._type === 'content') };
|
||||
const groupLabels: Record<string, string> = { course: '专题', prompt: '提示词', tool: 'AI 工具', content: '文章' };
|
||||
const groupLabels: Record<string, string> = { course: t.search.groupCourse, prompt: t.search.groupPrompt, tool: t.search.groupTool, content: t.search.groupContent };
|
||||
const groupIcons: Record<string, any> = { course: BookOpen, prompt: MessageSquare, tool: Wrench, content: FileText };
|
||||
const groupLinks: Record<string, string> = { course: '/courses/', prompt: '/prompts', tool: '/tools', content: '/contents/' };
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground mb-4">搜索结果</h1>
|
||||
<h1 className="text-3xl font-bold text-foreground mb-4">{t.search.title}</h1>
|
||||
<form onSubmit={e => { e.preventDefault(); window.location.href = `/search?q=${encodeURIComponent(input)}`; }}>
|
||||
<div className="relative">
|
||||
<SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||
<Input type="text" value={input} onChange={e => setInput(e.target.value)}
|
||||
placeholder="搜索专题、提示词、工具、文章..." className="pl-10 h-12 text-base" />
|
||||
<Input type="text" value={input} onChange={e => setInput(e.target.value)} placeholder={t.search.placeholder} className="pl-10 h-12 text-base" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -62,25 +59,23 @@ function SearchContent() {
|
||||
{!q && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<SearchIcon className="w-12 h-12 mx-auto mb-4 opacity-30" />
|
||||
<p>输入关键词搜索</p>
|
||||
<p>{t.search.emptyHint}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{q && loading && (
|
||||
<div className="space-y-4">
|
||||
{[1,2,3].map(i => <Skeleton key={i} className="h-24 w-full rounded-xl" />)}
|
||||
</div>
|
||||
<div className="space-y-4">{[1,2,3].map(i => <Skeleton key={i} className="h-24 w-full rounded-xl" />)}</div>
|
||||
)}
|
||||
|
||||
{q && !loading && total === 0 && (
|
||||
<div className="text-center py-20 text-muted-foreground">
|
||||
<p>未找到与 "<span className="text-foreground font-medium">{q}</span>" 相关的结果</p>
|
||||
<p>{t.search.noResults.replace('{q}', q)}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{q && !loading && total > 0 && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-6">找到 {total} 个结果</p>
|
||||
<p className="text-sm text-muted-foreground mb-6">{t.search.resultsCount.replace('{n}', String(total))}</p>
|
||||
<div className="space-y-8">
|
||||
{Object.entries(groups).map(([key, items]) => {
|
||||
if (items.length === 0) return null;
|
||||
@@ -90,25 +85,17 @@ function SearchContent() {
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Icon className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">{groupLabels[key]}</span>
|
||||
<span className="text-xs text-muted-foreground">{items.length} 个结果</span>
|
||||
<span className="text-xs text-muted-foreground">{items.length} {t.search.resultsCount.replace('{n}', '')}</span>
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{items.map((item) => (
|
||||
<Link key={`${key}-${item.id}`}
|
||||
href={`${groupLinks[key]}${key === 'course' || key === 'content' ? item.id : ''}`}
|
||||
className="block">
|
||||
<Link key={`${key}-${item.id}`} href={`${groupLinks[key]}${key === 'course' || key === 'content' ? item.id : ''}`} className="block">
|
||||
<Card className="p-4 hover:border-brand-200 dark:hover:border-brand-800 transition-colors">
|
||||
<h3 className="font-semibold">{item.title || item.name}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">{item.description || item.summary}</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{key === 'course' && (
|
||||
<Badge variant={item.isFree ? 'success' : 'destructive'}>
|
||||
{item.isFree ? '免费' : '付费'}
|
||||
</Badge>
|
||||
)}
|
||||
{key === 'prompt' && item.model && (
|
||||
<span className="text-xs text-muted-foreground">模型: {item.model}</span>
|
||||
)}
|
||||
{key === 'course' && <Badge variant={item.isFree ? 'secondary' : 'destructive'}>{item.isFree ? t.courses.free : t.courses.paid}</Badge>}
|
||||
{key === 'prompt' && item.model && <span className="text-xs text-muted-foreground">模型: {item.model}</span>}
|
||||
</div>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@@ -6,26 +6,21 @@ import { Badge } from '@/components/ui/badge';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Wrench, ExternalLink, Star } from 'lucide-react';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
import { useT } from '@/i18n';
|
||||
|
||||
interface Tool {
|
||||
id: number; name: string; description: string; url: string;
|
||||
icon: string | null; isFeatured: boolean; tags: string | null;
|
||||
}
|
||||
interface Tool { id: number; name: string; description: string; url: string; icon: string | null; isFeatured: boolean; tags: string | null }
|
||||
|
||||
function ToolSkeleton() {
|
||||
return (
|
||||
<Card className="p-5">
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
<Skeleton className="h-5 w-12 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-1/2 mb-1" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<div className="flex gap-2 mb-2"><Skeleton className="h-5 w-12 rounded-full" /><Skeleton className="h-5 w-12 rounded-full" /></div>
|
||||
<Skeleton className="h-5 w-1/2 mb-1" /><Skeleton className="h-4 w-full" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ToolsPage() {
|
||||
const t = useT();
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -38,14 +33,12 @@ export default function ToolsPage() {
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="mb-10">
|
||||
<h1 className="text-3xl font-bold text-foreground">AI 工具库</h1>
|
||||
<p className="mt-2 text-muted-foreground">收录优质 AI 工具,助力工作效率提升</p>
|
||||
<h1 className="text-3xl font-bold text-foreground">{t.tools.title}</h1>
|
||||
<p className="mt-2 text-muted-foreground">{t.tools.desc}</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{[1,2,3,4,5,6].map(i => <ToolSkeleton key={i} />)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">{[1,2,3,4,5,6].map(i => <ToolSkeleton key={i} />)}</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{tools.map((tool) => (
|
||||
|
||||
@@ -15,7 +15,7 @@ const en: Translations = {
|
||||
courses: { title: 'Courses', desc: 'Explore AI systematically from beginner to expert', empty: 'No courses available', free: 'Free', paid: 'Paid', moduleCount: '{n} modules' },
|
||||
search: { title: 'Search Results', placeholder: 'Search courses, prompts, tools, articles...', emptyHint: 'Enter keywords to search', noResults: 'No results found for "{q}"', resultsCount: '{n} results found', groupCourse: 'Courses', groupPrompt: 'Prompts', groupTool: 'AI Tools', groupContent: 'Articles' },
|
||||
tools: { title: 'AI Tools', desc: 'Curated AI tools to boost your productivity' },
|
||||
discover: { desc: 'Explore trending content and curated picks', hotCourses: 'Hot Courses', hotPrompts: 'Trending Prompts', hotPosts: 'Popular Discussions' },
|
||||
discover: { desc: 'Explore trending content and curated picks', hotCourses: 'Hot Courses', hotPrompts: 'Trending Prompts', hotPosts: 'Popular Discussions', viewCount: '{n} views', likeCount: '{n} likes', postStats: '❤️ {likes} · 👁 {views}' },
|
||||
footer: { tagline: 'Empowering Everyone to Master AI', explore: 'Explore', about: 'About', aboutUs: 'About Us', privacy: 'Privacy Policy', terms: 'Terms of Service', aiAgreement: 'AI Service Agreement', contact: 'Contact', copyright: '© {year} Yuzhiran Technology Center. All rights reserved.', models: 'Models', aiTools: 'AI Tools', articles: 'Articles', skills: 'Skills' },
|
||||
models: { title: 'AI Model Encyclopedia', desc: 'Compare mainstream LLMs to find the best fit', tableName: 'Model', tableProvider: 'Provider', tableCapabilities: 'Capabilities', tableContext: 'Context', tableMaxOutput: 'Max Output', tablePricing: 'Pricing', free: 'Free', recommended: 'Recommended', pricingFree: 'Free', pricingMixed: 'Free/Paid', pricingPaid: 'Paid' },
|
||||
error: { title: 'Something went wrong', desc: 'Page failed to load. Please try again.', reload: 'Reload' },
|
||||
|
||||
@@ -13,7 +13,7 @@ const zh = {
|
||||
courses: { title: '专题', desc: '系统化探索 AI,从入门到精通', empty: '暂无专题内容', free: '免费', paid: '付费', moduleCount: '{n} 模块' },
|
||||
search: { title: '搜索结果', placeholder: '搜索专题、提示词、工具、文章...', emptyHint: '输入关键词搜索', noResults: '未找到与 "{q}" 相关的结果', resultsCount: '找到 {n} 个结果', groupCourse: '专题', groupPrompt: '提示词', groupTool: 'AI 工具', groupContent: '文章' },
|
||||
tools: { title: 'AI 工具库', desc: '收录优质 AI 工具,助力工作效率提升' },
|
||||
discover: { desc: '探索热门内容和精选推荐', hotCourses: '热门课程', hotPrompts: '热门提示词', hotPosts: '热门讨论' },
|
||||
discover: { desc: '探索热门内容和精选推荐', hotCourses: '热门课程', hotPrompts: '热门提示词', hotPosts: '热门讨论', viewCount: '{n} 浏览', likeCount: '{n} 点赞', postStats: '❤️ {likes} 点赞 · 👁 {views} 浏览' },
|
||||
footer: { tagline: '让每个人都能用好 AI', explore: '探索', about: '关于', aboutUs: '关于我们', privacy: '隐私政策', terms: '服务协议', aiAgreement: 'AI 服务协议', contact: '联系方式', copyright: '© {year} 北京宇之然科技中心 版权所有', models: '模型百科', aiTools: 'AI 工具', articles: '文章', skills: '技能' },
|
||||
models: { title: 'AI 模型百科', desc: '收录主流大语言模型,全面对比各项参数', tableName: '模型名称', tableProvider: '提供商', tableCapabilities: '能力', tableContext: '上下文', tableMaxOutput: '最大输出', tablePricing: '价格', free: '免费', recommended: '推荐', pricingFree: '免费', pricingMixed: '免费/付费', pricingPaid: '付费' },
|
||||
error: { title: '出错了', desc: '页面加载失败,请稍后重试', reload: '重新加载' },
|
||||
|
||||
Reference in New Issue
Block a user