617 lines
39 KiB
TypeScript
617 lines
39 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import Link from 'next/link';
|
|
import SEOHead from '../../../../components/SEOHead';
|
|
|
|
interface SlugPageProps {
|
|
params: { locale: string; slug: string };
|
|
}
|
|
|
|
export default function NewsSlugPage({ params }: SlugPageProps) {
|
|
const [article, setArticle] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
const [readingProgress, setReadingProgress] = useState(0);
|
|
const [relatedArticles, setRelatedArticles] = useState<any[]>([]);
|
|
const [showBackToTop, setShowBackToTop] = useState(false);
|
|
|
|
// 计算阅读时间(基于平均阅读速度)
|
|
const calculateReadingTime = (content: string) => {
|
|
const wordsPerMinute = 200; // 中文阅读速度约200字/分钟
|
|
const wordCount = content.length;
|
|
const readingTime = Math.ceil(wordCount / wordsPerMinute);
|
|
return readingTime;
|
|
};
|
|
|
|
// 返回顶部功能
|
|
const scrollToTop = () => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
};
|
|
|
|
// 阅读进度监听
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
const scrollTop = window.scrollY;
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
const progress = (scrollTop / docHeight) * 100;
|
|
setReadingProgress(Math.min(100, Math.max(0, progress)));
|
|
|
|
// 显示返回顶部按钮
|
|
setShowBackToTop(scrollTop > 300);
|
|
};
|
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const loadArticle = async () => {
|
|
try {
|
|
|
|
|
|
// 直接使用 params.locale 而不是 currentLang 状态
|
|
const markdownRes = await fetch(`/docs/${params.locale}/news/${params.slug}.md`);
|
|
if (markdownRes.ok) {
|
|
const markdownContent = await markdownRes.text();
|
|
const { frontmatter, content } = parseMarkdown(markdownContent);
|
|
setArticle({ ...frontmatter, content });
|
|
|
|
// 加载相关文章
|
|
await loadRelatedArticles(frontmatter.category, frontmatter.tags);
|
|
} else {
|
|
|
|
// 如果当前语言的文章不存在,尝试检查是否有其他语言版本
|
|
const fallbackLanguages = ['zh-CN', 'en', 'zh-TW'].filter(lang => lang !== params.locale);
|
|
let found = false;
|
|
|
|
for (const fallbackLang of fallbackLanguages) {
|
|
const fallbackRes = await fetch(`/docs/${fallbackLang}/news/${params.slug}.md`);
|
|
if (fallbackRes.ok) {
|
|
|
|
setArticle({
|
|
error: 'language_not_available',
|
|
availableLanguage: fallbackLang,
|
|
slug: params.slug
|
|
});
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
setArticle(null);
|
|
}
|
|
}
|
|
setLoading(false);
|
|
} catch (error) {
|
|
console.error('Failed to load article:', error);
|
|
setArticle(null);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
loadArticle();
|
|
}, [params.locale, params.slug]);
|
|
|
|
// 加载相关文章
|
|
const loadRelatedArticles = async (category: string, tags: string[]) => {
|
|
try {
|
|
const articlesApiRes = await fetch(`/api/articles/${params.locale}`);
|
|
const articlesApiData = await articlesApiRes.json();
|
|
const articleSlugs = articlesApiData.articles || [];
|
|
|
|
const related: any[] = [];
|
|
for (const slug of articleSlugs) {
|
|
if (slug === params.slug) continue; // 排除当前文章
|
|
|
|
try {
|
|
const markdownRes = await fetch(`/docs/${params.locale}/news/${slug}.md`);
|
|
if (markdownRes.ok) {
|
|
const markdownContent = await markdownRes.text();
|
|
const { frontmatter } = parseMarkdown(markdownContent);
|
|
|
|
// 简单的相关性匹配:相同分类或有共同标签
|
|
if (frontmatter.category === category ||
|
|
(tags && frontmatter.tags && frontmatter.tags.some((tag: string) => tags.includes(tag)))) {
|
|
related.push({ ...frontmatter, slug });
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// 忽略错误
|
|
}
|
|
|
|
if (related.length >= 3) break; // 最多3篇相关文章
|
|
}
|
|
|
|
setRelatedArticles(related);
|
|
} catch (error) {
|
|
console.error('Failed to load related articles:', error);
|
|
}
|
|
};
|
|
|
|
// 分享功能
|
|
const shareArticle = (platform: string) => {
|
|
const url = encodeURIComponent(window.location.href);
|
|
const title = encodeURIComponent(article?.title || '');
|
|
|
|
const shareUrls = {
|
|
twitter: `https://twitter.com/intent/tweet?url=${url}&text=${title}`,
|
|
facebook: `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
|
linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${url}`,
|
|
wechat: `javascript:void(0)`, // 微信需要特殊处理
|
|
};
|
|
|
|
if (platform === 'wechat') {
|
|
// 复制链接到剪贴板
|
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
|
alert('链接已复制到剪贴板,可以分享到微信了!');
|
|
});
|
|
} else {
|
|
window.open(shareUrls[platform as keyof typeof shareUrls], '_blank', 'width=600,height=400');
|
|
}
|
|
};
|
|
|
|
// Helper function to parse markdown frontmatter
|
|
const parseMarkdown = (markdown: string) => {
|
|
// 使用与列表页相同的强健正则表达式
|
|
const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
|
|
const match = markdown.match(frontmatterRegex);
|
|
|
|
if (!match) {
|
|
return {
|
|
frontmatter: {},
|
|
content: markdown,
|
|
};
|
|
}
|
|
|
|
const frontmatterText = match[1];
|
|
const content = match[2];
|
|
|
|
const frontmatter: any = {};
|
|
frontmatterText.split('\n').forEach((line) => {
|
|
const colonIndex = line.indexOf(':');
|
|
if (colonIndex === -1) return;
|
|
|
|
const key = line.substring(0, colonIndex).trim();
|
|
const value = line.substring(colonIndex + 1).trim();
|
|
|
|
if (key && value) {
|
|
if (key === 'tags') {
|
|
if (value.startsWith('[') && value.endsWith(']')) {
|
|
frontmatter[key] = value
|
|
.slice(1, -1)
|
|
.split(',')
|
|
.map((tag) => tag.trim().replace(/['"]/g, ''))
|
|
.filter((tag) => tag.length > 0);
|
|
} else {
|
|
frontmatter[key] = [value.replace(/['"]/g, '')];
|
|
}
|
|
} else if (key === 'featured') {
|
|
frontmatter[key] = value.toLowerCase() === 'true';
|
|
} else {
|
|
frontmatter[key] = value.replace(/['"]/g, '');
|
|
}
|
|
}
|
|
});
|
|
|
|
return { frontmatter, content };
|
|
};
|
|
|
|
// 简单的Markdown渲染器
|
|
const renderMarkdown = (markdown: string) => {
|
|
if (!markdown) return '';
|
|
|
|
return markdown
|
|
// 标题处理
|
|
.replace(/^### (.*$)/gim, '<h3 class="text-xl font-bold mt-6 mb-4 text-gray-800">$1</h3>')
|
|
.replace(/^## (.*$)/gim, '<h2 class="text-2xl font-bold mt-8 mb-6 text-gray-800">$1</h2>')
|
|
.replace(/^# (.*$)/gim, '<h1 class="text-3xl font-bold mt-8 mb-6 text-gray-800">$1</h1>')
|
|
// 粗体和斜体
|
|
.replace(/\*\*(.*?)\*\*/g, '<strong class="font-bold">$1</strong>')
|
|
.replace(/\*(.*?)\*/g, '<em class="italic">$1</em>')
|
|
// 列表处理
|
|
.replace(/^\s*\* (.*$)/gim, '<li class="ml-4 mb-2">• $1</li>')
|
|
.replace(/^\s*- (.*$)/gim, '<li class="ml-4 mb-2">• $1</li>')
|
|
.replace(/^\s*\d+\. (.*$)/gim, '<li class="ml-4 mb-2 list-decimal">$1</li>')
|
|
// 链接处理
|
|
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="text-blue-600 hover:text-blue-800 underline">$1</a>')
|
|
// 代码块处理
|
|
.replace(/```([^`]+)```/g, '<pre class="bg-gray-100 p-4 rounded-lg my-4 overflow-x-auto"><code>$1</code></pre>')
|
|
.replace(/`([^`]+)`/g, '<code class="bg-gray-100 px-2 py-1 rounded text-sm">$1</code>')
|
|
// 段落处理
|
|
.replace(/\n\n/g, '</p><p class="mb-4">')
|
|
// 换行处理
|
|
.replace(/\n/g, '<br />');
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
|
|
<div className="text-center">
|
|
<div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
|
<p className="text-gray-600">加载中...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!article) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
|
|
<div className="text-center">
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
|
{params.locale === 'en' ? 'Article Not Found' :
|
|
params.locale === 'zh-TW' ? '文章未找到' : '文章未找到'}
|
|
</h1>
|
|
<p className="text-gray-600 mb-8">
|
|
{params.locale === 'en' ? 'Sorry, the article you are looking for does not exist.' :
|
|
params.locale === 'zh-TW' ? '抱歉,您訪問的文章不存在。' : '抱歉,您访问的文章不存在。'}
|
|
</p>
|
|
<Link
|
|
href={`/${params.locale}/news`}
|
|
className="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
{params.locale === 'en' ? 'Back to News' :
|
|
params.locale === 'zh-TW' ? '返回新聞列表' : '返回新闻列表'}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 处理语言不可用的情况
|
|
if (article.error === 'language_not_available') {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50">
|
|
<div className="text-center max-w-md">
|
|
<div className="bg-amber-50 border border-amber-200 rounded-lg p-6 mb-6">
|
|
<svg className="w-12 h-12 text-amber-500 mx-auto mb-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
</svg>
|
|
<h1 className="text-xl font-bold text-gray-900 mb-2">
|
|
{params.locale === 'en' ? 'Article Not Available in English' :
|
|
params.locale === 'zh-TW' ? '文章暫無繁體中文版本' : '文章暂无简体中文版本'}
|
|
</h1>
|
|
<p className="text-gray-600 mb-4">
|
|
{params.locale === 'en' ? 'This article is available in other languages.' :
|
|
params.locale === 'zh-TW' ? '此文章有其他語言版本可供閱讀。' : '此文章有其他语言版本可供阅读。'}
|
|
</p>
|
|
<div className="space-y-3">
|
|
<Link
|
|
href={`/${article.availableLanguage}/news/${article.slug}`}
|
|
className="block px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
>
|
|
{params.locale === 'en' ? `Read in ${article.availableLanguage === 'zh-CN' ? 'Chinese (Simplified)' : article.availableLanguage === 'zh-TW' ? 'Chinese (Traditional)' : 'English'}` :
|
|
params.locale === 'zh-TW' ? `閱讀${article.availableLanguage === 'zh-CN' ? '簡體中文' : article.availableLanguage === 'en' ? '英文' : '繁體中文'}版本` :
|
|
`阅读${article.availableLanguage === 'zh-TW' ? '繁体中文' : article.availableLanguage === 'en' ? '英文' : '简体中文'}版本`}
|
|
</Link>
|
|
<Link
|
|
href={`/${params.locale}/news`}
|
|
className="block px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
|
>
|
|
{params.locale === 'en' ? 'Back to News' :
|
|
params.locale === 'zh-TW' ? '返回新聞列表' : '返回新闻列表'}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<SEOHead
|
|
page="news"
|
|
locale={params.locale as 'zh-CN' | 'zh-TW' | 'en'}
|
|
canonicalUrl={`/news/${params.slug}`}
|
|
/>
|
|
|
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
|
|
{/* 优雅的阅读进度条 */}
|
|
<div className="fixed top-0 left-0 w-full h-1 bg-transparent z-[60]">
|
|
<div
|
|
className="h-full bg-gradient-to-r from-blue-400/80 via-purple-500/80 to-pink-500/80 transition-all duration-500 ease-out"
|
|
style={{
|
|
width: `${readingProgress}%`,
|
|
boxShadow: readingProgress > 0 ? '0 0 10px rgba(139, 92, 246, 0.3)' : 'none'
|
|
}}
|
|
></div>
|
|
</div>
|
|
|
|
{/* 悬浮分享按钮 */}
|
|
<div className="fixed right-6 top-1/2 transform -translate-y-1/2 z-40 space-y-3">
|
|
<div className="bg-white/95 backdrop-blur-sm rounded-2xl shadow-xl p-3 flex flex-col space-y-3 border border-gray-100">
|
|
<button
|
|
onClick={() => shareArticle('wechat')}
|
|
className="w-10 h-10 bg-green-500 text-white rounded-full flex items-center justify-center hover:bg-green-600 transition-colors"
|
|
title="分享到微信"
|
|
>
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18 0 .659-.52 1.188-1.162 1.188-.642 0-1.162-.529-1.162-1.188 0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18 0 .659-.52 1.188-1.162 1.188-.642 0-1.162-.529-1.162-1.188 0-.651.52-1.18 1.162-1.18z"/>
|
|
<path d="M24 14.388c0-3.14-2.956-5.689-6.594-5.689-3.638 0-6.594 2.549-6.594 5.689 0 3.14 2.956 5.689 6.594 5.689a7.84 7.84 0 0 0 2.276-.336c.237-.063.473-.018.675.078l1.485.87a.24.24 0 0 0 .13.04c.127 0 .23-.103.23-.23 0-.056-.022-.107-.037-.158l-.305-1.115a.463.463 0 0 1 .167-.513C23.05 17.898 24 16.213 24 14.388z"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => shareArticle('facebook')}
|
|
className="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center hover:bg-blue-700 transition-colors"
|
|
title="分享到Facebook"
|
|
>
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => shareArticle('twitter')}
|
|
className="w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center hover:bg-sky-600 transition-colors"
|
|
title="分享到Twitter"
|
|
>
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
|
</svg>
|
|
</button>
|
|
<button
|
|
onClick={() => shareArticle('linkedin')}
|
|
className="w-10 h-10 bg-blue-700 text-white rounded-full flex items-center justify-center hover:bg-blue-800 transition-colors"
|
|
title="分享到LinkedIn"
|
|
>
|
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 返回顶部按钮 */}
|
|
{showBackToTop && (
|
|
<button
|
|
onClick={scrollToTop}
|
|
className="fixed bottom-8 right-6 w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-full shadow-lg hover:shadow-xl transition-all duration-200 z-40 flex items-center justify-center"
|
|
title="返回顶部"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
</svg>
|
|
</button>
|
|
)}
|
|
|
|
{/* Navigation */}
|
|
<nav className="bg-white/80 backdrop-blur-md border-b border-white/20 sticky top-0 z-50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center h-16">
|
|
<Link href={`/${params.locale}`} className="flex items-center space-x-2">
|
|
<div className="w-8 h-8 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
|
<svg className="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M3 4a1 1 0 011-1h12a1 1 0 011 1v2a1 1 0 01-1 1H4a1 1 0 01-1-1V4zM3 10a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H4a1 1 0 01-1-1v-6zM14 9a1 1 0 00-1 1v6a1 1 0 001 1h2a1 1 0 001-1v-6a1 1 0 00-1-1h-2z" />
|
|
</svg>
|
|
</div>
|
|
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
|
CloudTech
|
|
</span>
|
|
</Link>
|
|
|
|
<div className="hidden md:flex items-center space-x-8">
|
|
<Link href={`/${params.locale}`} className="text-gray-700 hover:text-blue-600 font-medium transition-colors">
|
|
{params.locale === 'en' ? 'Home' : params.locale === 'zh-TW' ? '首頁' : '首页'}
|
|
</Link>
|
|
<Link href={`/${params.locale}/products`} className="text-gray-700 hover:text-blue-600 font-medium transition-colors">
|
|
{params.locale === 'en' ? 'Products' : params.locale === 'zh-TW' ? '產品與服務' : '产品与服务'}
|
|
</Link>
|
|
<Link href={`/${params.locale}/news`} className="text-blue-600 font-medium">
|
|
{params.locale === 'en' ? 'News' : params.locale === 'zh-TW' ? '新聞資訊' : '新闻资讯'}
|
|
</Link>
|
|
<Link href={`/${params.locale}/support`} className="text-gray-700 hover:text-blue-600 font-medium transition-colors">
|
|
{params.locale === 'en' ? 'Support' : params.locale === 'zh-TW' ? '客戶支持' : '客户支持'}
|
|
</Link>
|
|
<Link href={`/${params.locale}/about`} className="text-gray-700 hover:text-blue-600 font-medium transition-colors">
|
|
{params.locale === 'en' ? 'About' : params.locale === 'zh-TW' ? '關於我們' : '关于我们'}
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-4">
|
|
<select
|
|
value={params.locale}
|
|
onChange={(e) => {
|
|
const newLocale = e.target.value;
|
|
const currentPath = window.location.pathname;
|
|
const pathWithoutLocale = currentPath.replace(/^\/[^\/]+/, '');
|
|
window.location.href = `/${newLocale}${pathWithoutLocale}`;
|
|
}}
|
|
className="bg-white/50 border border-white/30 rounded-lg px-3 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
title={params.locale === 'en' ? 'Select Language' : params.locale === 'zh-TW' ? '選擇語言' : '选择语言'}
|
|
>
|
|
<option value="zh-CN">简体中文</option>
|
|
<option value="zh-TW">繁體中文</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
{/* Article Content */}
|
|
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
<div className="bg-white rounded-3xl shadow-lg overflow-hidden">
|
|
{/* Article Header */}
|
|
<div className="bg-gradient-to-r from-blue-500 to-purple-600 text-white p-8">
|
|
<div className="flex items-center mb-4">
|
|
<Link
|
|
href={`/${params.locale}/news`}
|
|
className="text-white/80 hover:text-white font-medium flex items-center"
|
|
>
|
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
{params.locale === 'en' ? 'Back to News' : params.locale === 'zh-TW' ? '返回新聞列表' : '返回新闻列表'}
|
|
</Link>
|
|
</div>
|
|
<h1 className="text-3xl md:text-4xl font-bold mb-4">{article.title}</h1>
|
|
<div className="flex flex-wrap items-center space-x-6 text-white/80">
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
|
</svg>
|
|
{article.date}
|
|
</div>
|
|
{article.author && (
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clipRule="evenodd" />
|
|
</svg>
|
|
{article.author}
|
|
</div>
|
|
)}
|
|
{article.category && (
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z" clipRule="evenodd" />
|
|
</svg>
|
|
{article.category}
|
|
</div>
|
|
)}
|
|
<div className="flex items-center">
|
|
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clipRule="evenodd" />
|
|
</svg>
|
|
{params.locale === 'en' ? `About ${calculateReadingTime(article.content || '')} min read` :
|
|
params.locale === 'zh-TW' ? `約 ${calculateReadingTime(article.content || '')} 分鐘閱讀` :
|
|
`约 ${calculateReadingTime(article.content || '')} 分钟阅读`}
|
|
</div>
|
|
</div>
|
|
{article.tags && article.tags.length > 0 && (
|
|
<div className="flex flex-wrap gap-2 mt-4">
|
|
{article.tags.map((tag: string, index: number) => (
|
|
<span
|
|
key={index}
|
|
className="px-3 py-1 bg-white/20 rounded-full text-sm font-medium"
|
|
>
|
|
#{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Article Body */}
|
|
<div className="p-8">
|
|
{article.summary && (
|
|
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-8">
|
|
<p className="text-blue-700 font-medium">{article.summary}</p>
|
|
</div>
|
|
)}
|
|
<div className="prose prose-lg max-w-none">
|
|
<div
|
|
dangerouslySetInnerHTML={{
|
|
__html: `<p class="mb-4">${renderMarkdown(article.content)}</p>`
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 相关文章推荐 */}
|
|
{relatedArticles.length > 0 && (
|
|
<div className="mt-12">
|
|
<div className="bg-white rounded-3xl shadow-lg p-8">
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-6 flex items-center">
|
|
<svg className="w-6 h-6 mr-2 text-blue-600" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
{params.locale === 'en' ? 'Related Articles' : params.locale === 'zh-TW' ? '相關推薦' : '相关推荐'}
|
|
</h3>
|
|
<div className="grid md:grid-cols-3 gap-6">
|
|
{relatedArticles.map((relatedArticle, index) => (
|
|
<Link
|
|
key={index}
|
|
href={`/${params.locale}/news/${relatedArticle.slug}`}
|
|
className="group block"
|
|
>
|
|
<div className="bg-gradient-to-br from-slate-50 to-blue-50 rounded-xl p-6 h-full hover:shadow-md transition-all duration-200 border border-gray-100 group-hover:border-blue-200">
|
|
<div className="flex items-center mb-3">
|
|
<div className="w-2 h-2 bg-blue-500 rounded-full mr-2"></div>
|
|
<span className="text-sm text-gray-500">
|
|
{relatedArticle.category || '技术'}
|
|
</span>
|
|
</div>
|
|
<h4 className="font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors line-clamp-2">
|
|
{relatedArticle.title}
|
|
</h4>
|
|
<p className="text-gray-600 text-sm mb-3 line-clamp-3">
|
|
{relatedArticle.summary}
|
|
</p>
|
|
<div className="flex items-center text-xs text-gray-500">
|
|
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
|
</svg>
|
|
{relatedArticle.date}
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 文章操作区域 */}
|
|
<div className="mt-12">
|
|
<div className="bg-white rounded-3xl shadow-lg p-8">
|
|
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="flex items-center space-x-2">
|
|
<svg className="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M15 8a3 3 0 10-2.977-2.63l-4.94 2.47a3 3 0 100 4.319l4.94 2.47a3 3 0 10.895-1.789l-4.94-2.47a3.027 3.027 0 000-.74l4.94-2.47C13.456 7.68 14.19 8 15 8z" />
|
|
</svg>
|
|
<span className="text-gray-600 font-medium">
|
|
{params.locale === 'en' ? 'Share Article' : params.locale === 'zh-TW' ? '分享文章' : '分享文章'}
|
|
</span>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => shareArticle('wechat')}
|
|
className="px-3 py-1 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors text-sm flex items-center space-x-1"
|
|
>
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348z"/>
|
|
<path d="M24 14.388c0-3.14-2.956-5.689-6.594-5.689-3.638 0-6.594 2.549-6.594 5.689 0 3.14 2.956 5.689 6.594 5.689a7.84 7.84 0 0 0 2.276-.336c.237-.063.473-.018.675.078l1.485.87a.24.24 0 0 0 .13.04c.127 0 .23-.103.23-.23 0-.056-.022-.107-.037-.158l-.305-1.115a.463.463 0 0 1 .167-.513C23.05 17.898 24 16.213 24 14.388z"/>
|
|
</svg>
|
|
<span>微信</span>
|
|
</button>
|
|
<button
|
|
onClick={() => shareArticle('facebook')}
|
|
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors text-sm flex items-center space-x-1"
|
|
>
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
|
</svg>
|
|
<span>Facebook</span>
|
|
</button>
|
|
<button
|
|
onClick={() => shareArticle('twitter')}
|
|
className="px-3 py-1 bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors text-sm flex items-center space-x-1"
|
|
>
|
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z"/>
|
|
</svg>
|
|
<span>Twitter</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<Link
|
|
href={`/${params.locale}/news`}
|
|
className="inline-flex items-center px-6 py-3 bg-gradient-to-r from-blue-500 to-purple-600 text-white rounded-lg hover:shadow-lg transition-all duration-200"
|
|
>
|
|
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
</svg>
|
|
{params.locale === 'en' ? 'View More News' : params.locale === 'zh-TW' ? '查看更多新聞' : '查看更多新闻'}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|