feat: 添加关键词词云页面国际化支持

This commit is contained in:
2026-01-27 20:39:38 +08:00
parent 9164086d6e
commit e1f250d397
5 changed files with 82 additions and 15 deletions
@@ -15,12 +15,15 @@ export interface KeywordData {
word: string; word: string;
trendScore: number; trendScore: number;
description: string; description: string;
descriptionEn?: string | null;
detailPoints: string[]; detailPoints: string[];
detailPointsEn?: string[] | null;
visualConfig: VisualConfig; visualConfig: VisualConfig;
} }
interface CloudWordProps { interface CloudWordProps {
data: KeywordData; data: KeywordData;
locale: string;
} }
// 颜色映射 // 颜色映射
@@ -31,11 +34,15 @@ const colorMap: Record<string, string> = {
gray: 'bg-gray-100 dark:bg-gray-700', gray: 'bg-gray-100 dark:bg-gray-700',
}; };
export function CloudWord({ data }: CloudWordProps) { export function CloudWord({ data, locale }: CloudWordProps) {
const { word, visualConfig, description, detailPoints } = data; const { word, visualConfig, description, descriptionEn, detailPoints, detailPointsEn } = data;
const { color, size, border, rotation } = visualConfig; const { color, size, border, rotation } = visualConfig;
const colorClass = colorMap[color]; const colorClass = colorMap[color];
// 根据语言选择内容
const displayDescription = locale === 'en' && descriptionEn ? descriptionEn : description;
const displayPoints = locale === 'en' && detailPointsEn ? detailPointsEn : detailPoints;
return ( return (
<span <span
className={cn( className={cn(
@@ -63,8 +70,8 @@ export function CloudWord({ data }: CloudWordProps) {
{word} {word}
<WordPopover <WordPopover
title={word} title={word}
description={description} description={displayDescription}
points={detailPoints} points={displayPoints}
titleColor={color} titleColor={color}
/> />
</span> </span>
@@ -6,7 +6,18 @@ import { QuarterNavigator } from './QuarterNavigator';
import { ProgressIndicator } from './ProgressIndicator'; import { ProgressIndicator } from './ProgressIndicator';
import { useKeywordCloud } from '@/hooks/useKeywordCloudClient'; import { useKeywordCloud } from '@/hooks/useKeywordCloudClient';
export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) { interface KeywordCloudProps {
initialQuarter: string;
locale: string;
texts: {
loading: string;
loadFailed: string;
retry: string;
hotKeyword: string;
};
}
export function KeywordCloud({ initialQuarter, locale, texts }: KeywordCloudProps) {
const [currentQuarter, setCurrentQuarter] = useState(initialQuarter); const [currentQuarter, setCurrentQuarter] = useState(initialQuarter);
const [quarters, setQuarters] = useState<string[]>([]); const [quarters, setQuarters] = useState<string[]>([]);
const { data, isLoading, error } = useKeywordCloud(currentQuarter); const { data, isLoading, error } = useKeywordCloud(currentQuarter);
@@ -41,7 +52,7 @@ export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) {
<div className="flex items-center justify-center min-h-[400px]"> <div className="flex items-center justify-center min-h-[400px]">
<div className="text-center"> <div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-black border-t-primary mb-4" /> <div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-black border-t-primary mb-4" />
<p className="font-display font-bold text-lg">...</p> <p className="font-display font-bold text-lg">{texts.loading}</p>
</div> </div>
</div> </div>
); );
@@ -52,13 +63,13 @@ export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) {
<div className="flex items-center justify-center min-h-[400px]"> <div className="flex items-center justify-center min-h-[400px]">
<div className="text-center"> <div className="text-center">
<p className="font-display font-bold text-lg text-red-500 mb-4"> <p className="font-display font-bold text-lg text-red-500 mb-4">
{texts.loadFailed}
</p> </p>
<button <button
onClick={() => window.location.reload()} onClick={() => window.location.reload()}
className="bg-primary text-black border-2 border-black px-4 py-2 font-bold hover:bg-yellow-500 transition-colors" className="bg-primary text-black border-2 border-black px-4 py-2 font-bold hover:bg-yellow-500 transition-colors"
> >
{texts.retry}
</button> </button>
</div> </div>
</div> </div>
@@ -87,7 +98,7 @@ export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) {
<div className="relative py-12 px-4"> <div className="relative py-12 px-4">
<div className="word-cluster flex flex-wrap gap-3 items-center justify-center max-w-full"> <div className="word-cluster flex flex-wrap gap-3 items-center justify-center max-w-full">
{data.keywords.map((keyword) => ( {data.keywords.map((keyword) => (
<CloudWord key={keyword.id} data={keyword} /> <CloudWord key={keyword.id} data={keyword} locale={locale} />
))} ))}
</div> </div>
</div> </div>
@@ -95,7 +106,7 @@ export function KeywordCloud({ initialQuarter }: { initialQuarter: string }) {
{/* 装饰元素 */} {/* 装饰元素 */}
{data.keywords.length > 0 && firstKeyword && ( {data.keywords.length > 0 && firstKeyword && (
<div className="absolute -top-16 -right-8 md:right-0 bg-black text-white p-5 rounded-xl text-sm md:text-base font-display w-64 shadow-hard rotate-6 hidden lg:block"> <div className="absolute -top-16 -right-8 md:right-0 bg-black text-white p-5 rounded-xl text-sm md:text-base font-display w-64 shadow-hard rotate-6 hidden lg:block">
&ldquo;{firstKeyword.word}&rdquo; ! {texts.hotKeyword.replace('{word}', firstKeyword.word)}
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-black transform rotate-45" /> <div className="absolute -bottom-2 left-1/2 -translate-x-1/2 w-4 h-4 bg-black transform rotate-45" />
</div> </div>
)} )}
+30 -5
View File
@@ -1,4 +1,5 @@
import { KeywordCloud } from './components/KeywordCloud'; import { KeywordCloud } from './components/KeywordCloud';
import { getTranslations } from 'next-intl/server';
interface PageProps { interface PageProps {
params: { params: {
@@ -9,27 +10,51 @@ interface PageProps {
}; };
} }
export default function KeywordCloudPage({ searchParams }: PageProps) { export async function generateMetadata({
params
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params;
const t = await getTranslations('keywordCloud');
return {
title: t('metaTitle'),
description: t('metaDescription'),
};
}
export default async function KeywordCloudPage({ searchParams, params }: PageProps) {
const { locale } = await params;
const t = await getTranslations('keywordCloud');
// 如果 URL 中有 quarter 参数,使用它;否则使用默认季度 // 如果 URL 中有 quarter 参数,使用它;否则使用默认季度
const quarter = searchParams.quarter || '2024-Q1'; const quarter = searchParams.quarter || '2024-Q1';
const texts = {
loading: t('loading'),
loadFailed: t('loadFailed'),
retry: t('retry'),
hotKeyword: t('hotKeyword'),
};
return ( return (
<main className="relative z-10 max-w-6xl mx-auto px-4 pb-32 overflow-visible"> <main className="relative z-10 max-w-6xl mx-auto px-4 pb-32 overflow-visible">
{/* 页面标题 */} {/* 页面标题 */}
<header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4"> <header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4">
<div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]"> <div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]">
AI {t('badge')}
</div> </div>
<h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight"> <h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight">
AI <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500" style={{ WebkitTextStroke: '1.5px black' }}></span> {t('title')} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500" style={{ WebkitTextStroke: '1.5px black' }}>{t('titleHighlight')}</span>
</h1> </h1>
<p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8"> <p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8">
&ldquo;&rdquo; &ldquo;Agent &rdquo; AI {t('subtitle')}
</p> </p>
</header> </header>
{/* 词云组件 */} {/* 词云组件 */}
<KeywordCloud initialQuarter={quarter} /> <KeywordCloud initialQuarter={quarter} locale={locale} texts={texts} />
</main> </main>
); );
} }
+12
View File
@@ -95,5 +95,17 @@
"followUs": "Follow Us", "followUs": "Follow Us",
"copyright": "© 2025 Agent Park. All rights reserved.", "copyright": "© 2025 Agent Park. All rights reserved.",
"designedFor": "DESIGNED FOR AI BUILDERS" "designedFor": "DESIGNED FOR AI BUILDERS"
},
"keywordCloud": {
"metaTitle": "AI Hotspot Word Cloud - Agent Park",
"metaDescription": "Explore the evolution of quarterly AI hotspots, from Large Language Models to Agent Workflows",
"badge": "AI Trend Tracker",
"title": "Quarterly AI",
"titleHighlight": "Hotspot Word Cloud",
"subtitle": "From &ldquo;Large Language Models&rdquo; to &ldquo;Agent Workflows&rdquo;. Explore the evolution of AI discourse.",
"loading": "Loading...",
"loadFailed": "Failed to load",
"retry": "Retry",
"hotKeyword": "&ldquo;{word}&rdquo; is the hottest keyword this quarter!"
} }
} }
+12
View File
@@ -95,5 +95,17 @@
"followUs": "关注我们", "followUs": "关注我们",
"copyright": "© 2025 Agent Park. 保留所有权利。", "copyright": "© 2025 Agent Park. 保留所有权利。",
"designedFor": "专为 AI 构建者设计" "designedFor": "专为 AI 构建者设计"
},
"keywordCloud": {
"metaTitle": "AI 热点词云 - Agent Park",
"metaDescription": "探索季度 AI 热点词汇的演变历程,从大型语言模型到 Agent 工作流",
"badge": "AI 热点追踪",
"title": "季度 AI",
"titleHighlight": "热点词云",
"subtitle": "从 &ldquo;大型语言模型&rdquo; 到 &ldquo;Agent 工作流&rdquo;。探索 AI 话语的演变历程。",
"loading": "加载中...",
"loadFailed": "加载失败",
"retry": "重试",
"hotKeyword": "&ldquo;{word}&rdquo; 是本季度最热门词汇!"
} }
} }