210 lines
12 KiB
TypeScript
210 lines
12 KiB
TypeScript
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { Article } from '@/lib/types';
|
|
import { formatDate } from '@/lib/utils';
|
|
import { Card, CardContent } from '@/app/components/ui/card';
|
|
import { Badge } from '@/app/components/ui/badge';
|
|
import { Share2, ArrowLeft, Calendar, User, Tag, Clock, Eye } from 'lucide-react';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
import rehypeRaw from 'rehype-raw';
|
|
import rehypeSanitize from 'rehype-sanitize';
|
|
import ServerHeader from '@/app/components/ServerHeader';
|
|
import Footer from '@/app/components/Footer';
|
|
import { newsDetailTranslations } from '@/translations/news-detail';
|
|
import ShareButton from './ShareButton';
|
|
import BackButton from './BackButton';
|
|
|
|
interface NewsArticleServerComponentProps {
|
|
article: Article;
|
|
relatedArticles: Article[];
|
|
locale: string;
|
|
}
|
|
|
|
export default function NewsArticleServerComponent({
|
|
article,
|
|
relatedArticles,
|
|
locale
|
|
}: NewsArticleServerComponentProps) {
|
|
// 获取当前语言的翻译
|
|
const currentTranslations = newsDetailTranslations[locale as keyof typeof newsDetailTranslations] || newsDetailTranslations['zh-CN'];
|
|
|
|
// 估算阅读时间
|
|
const estimateReadingTime = (content: string) => {
|
|
const wordsPerMinute = 200;
|
|
const words = content.split(/\s+/).length;
|
|
return Math.ceil(words / wordsPerMinute);
|
|
};
|
|
|
|
const readingTime = estimateReadingTime(article.content);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
<ServerHeader
|
|
language={locale}
|
|
translations={currentTranslations}
|
|
locale={locale}
|
|
/>
|
|
|
|
<main className="container mx-auto px-4 py-8">
|
|
<div className="max-w-4xl mx-auto">
|
|
{/* 返回按钮 */}
|
|
<BackButton
|
|
text={currentTranslations.backToNews}
|
|
href={`/${locale}/news`}
|
|
/>
|
|
|
|
{/* 文章主体 */}
|
|
<article className="bg-white rounded-lg shadow-sm overflow-hidden mb-8">
|
|
{/* 文章标题区域 */}
|
|
<div className="p-4 sm:p-6 lg:p-8 border-b border-gray-200">
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
<Badge variant="secondary" className="bg-blue-100 text-blue-800">
|
|
{article.metadata.category}
|
|
</Badge>
|
|
{article.metadata.tags.map((tag, index) => (
|
|
<Badge key={index} variant="outline" className="text-gray-600">
|
|
<Tag size={12} className="mr-1" />
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 mb-6 leading-tight">
|
|
{article.metadata.title}
|
|
</h1>
|
|
|
|
<div className="flex flex-wrap items-center gap-4 sm:gap-6 text-gray-600 mb-6 text-sm sm:text-base">
|
|
<div className="flex items-center gap-2">
|
|
<User size={16} />
|
|
<span>{article.metadata.author}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Calendar size={16} />
|
|
<span>{formatDate(article.metadata.date, locale)}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Clock size={16} />
|
|
<span>{readingTime} 分钟阅读</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<p className="text-lg text-gray-700 leading-relaxed flex-1">
|
|
{article.metadata.description}
|
|
</p>
|
|
<div className="flex-shrink-0">
|
|
<ShareButton article={article} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 文章封面图 */}
|
|
{article.metadata.image && (
|
|
<div className="relative h-96 bg-gray-200">
|
|
<Image
|
|
src={article.metadata.image}
|
|
alt={article.metadata.title}
|
|
fill
|
|
className="object-cover"
|
|
priority
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 文章内容 */}
|
|
<div className="p-4 sm:p-6 lg:p-8">
|
|
<div className="prose prose-base sm:prose-lg prose-gray max-w-none prose-headings:text-gray-900 prose-a:text-blue-600 prose-strong:text-gray-900 prose-code:text-blue-800 prose-code:bg-blue-50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded">
|
|
<ReactMarkdown
|
|
remarkPlugins={[remarkGfm]}
|
|
rehypePlugins={[rehypeRaw, rehypeSanitize]}
|
|
components={{
|
|
img: ({ src, alt, ...props }) => (
|
|
<div className="relative my-8">
|
|
<Image
|
|
src={src || ''}
|
|
alt={alt || ''}
|
|
width={800}
|
|
height={400}
|
|
className="rounded-lg shadow-md"
|
|
/>
|
|
</div>
|
|
),
|
|
a: ({ href, children, ...props }) => (
|
|
<Link
|
|
href={href || ''}
|
|
className="text-blue-600 hover:text-blue-800 underline"
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Link>
|
|
),
|
|
}}
|
|
>
|
|
{article.content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
{/* 相关文章推荐 */}
|
|
{relatedArticles.length > 0 && (
|
|
<section className="mt-12 pt-8 border-t border-gray-200">
|
|
<div className="flex items-center mb-8">
|
|
<div className="flex-1">
|
|
<h2 className="text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900 mb-2">
|
|
{currentTranslations.relatedArticles}
|
|
</h2>
|
|
<p className="text-gray-600 text-sm sm:text-base">
|
|
{currentTranslations.relatedArticlesSubtitle}
|
|
</p>
|
|
</div>
|
|
<div className="hidden sm:block w-16 h-1 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full"></div>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
{relatedArticles.map((relatedArticle) => (
|
|
<Card
|
|
key={relatedArticle.id}
|
|
className="group hover:shadow-xl transition-all duration-300 hover:-translate-y-2 border-0 shadow-md bg-white"
|
|
>
|
|
<CardContent className="p-0">
|
|
<Link href={`/${locale}/news/${relatedArticle.metadata.slug}`}>
|
|
<div className="relative h-48 sm:h-56 rounded-t-lg overflow-hidden bg-gray-100">
|
|
<Image
|
|
src={relatedArticle.metadata.image || '/api/placeholder/400/250'}
|
|
alt={relatedArticle.metadata.title}
|
|
fill
|
|
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
|
/>
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-300"></div>
|
|
</div>
|
|
<div className="p-4 sm:p-5">
|
|
<h3 className="font-bold text-gray-900 mb-3 line-clamp-2 text-base sm:text-lg leading-tight group-hover:text-blue-600 transition-colors duration-200">
|
|
{relatedArticle.metadata.title}
|
|
</h3>
|
|
<p className="text-gray-600 text-sm mb-4 line-clamp-2 leading-relaxed">
|
|
{relatedArticle.metadata.excerpt}
|
|
</p>
|
|
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
|
|
<span className="text-xs text-gray-500 font-medium">{formatDate(relatedArticle.metadata.date, locale)}</span>
|
|
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200 hover:bg-blue-100 transition-colors">
|
|
{relatedArticle.metadata.category}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</main>
|
|
|
|
<Footer translations={currentTranslations} />
|
|
</div>
|
|
);
|
|
}
|