2025-09-11 10:55:59 +08:00

460 lines
15 KiB
Vue

<template>
<div>
<div v-if="article" class="article-container">
<!-- 文章头部信息 -->
<div class="article-header dark:bg-gray-900">
<div class="container py-8">
<div class="max-w-4xl mx-auto">
<!-- 返回按钮 -->
<NuxtLink to="/awsnews" class="inline-flex items-center text-blue-600 dark:text-blue-400 mb-4">
<i class="fas fa-arrow-left mr-2"></i> {{ $t('news.backToList') }}
</NuxtLink>
<!-- 标题和描述 -->
<h1 class="text-3xl md:text-4xl font-bold mb-4 dark:text-white">{{ article.title }}</h1>
<p class="text-xl text-gray-600 dark:text-gray-300 mb-6">{{ article.description }}</p>
<!-- 元数据 -->
<div class="flex flex-wrap items-center gap-4 text-sm text-gray-500 dark:text-gray-400 mb-6">
<div>
<i class="far fa-calendar-alt mr-1"></i> {{ formatDate(article.meta?.date || article.date) }}
</div>
<!-- //作者 -->
<!-- <div>
<i class="far fa-user mr-1"></i> {{ article.meta?.author || article.author || $t('news.meta.unknownAuthor') }}
</div> -->
<div>
<i class="far fa-eye mr-1"></i> {{ article.meta?.views || article.views || 0 }} {{ $t('news.views') }}
</div>
<div class="category-badge" :class="getCategoryClass(article.meta?.category || article.category || 'other')">
{{ $t(`news.categories.${article.meta?.category || article.category || 'other'}`) }}
</div>
</div>
<!-- 特色图片 -->
<div v-if="article.image || article.meta?.image" class="article-featured-image mb-8 h-1/2">
<img :src="article.image || article.meta?.image" :alt="article.title" class="w-full h-auto rounded-lg shadow">
</div>
</div>
</div>
</div>
<!-- 文章内容 -->
<div class="article-content pb-16 dark:bg-gray-900">
<div class="container">
<div class="max-w-4xl mx-auto bg-white dark:bg-gray-800 p-6 md:p-8 rounded-lg shadow dark:shadow-gray-700">
<!-- 标签列表 -->
<div v-if="article.tags && article.tags.length" class="mb-8 flex flex-wrap gap-2">
<span
v-for="tag in article.tags"
:key="tag"
class="inline-block px-3 py-1 bg-gray-100 dark:bg-gray-700 rounded-full text-sm text-gray-700 dark:text-gray-300"
>
#{{ tag }}
</span>
</div>
<!-- 文章主体内容 -->
<ContentRenderer :value="article" class="prose prose-lg max-w-none dark:prose-invert">
<template #empty>
<p class="dark:text-gray-300">{{ $t('news.contentNotAvailable') }}</p>
</template>
</ContentRenderer>
<!-- 分享按钮 -->
<div class="mt-12 pt-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center justify-between">
<div class="text-gray-600 dark:text-gray-400">{{ $t('news.shareThis') }}:</div>
<div class="flex gap-3">
<button @click="shareArticle('twitter')" class="social-share-btn bg-blue-600 dark:bg-blue-700">
<i class="fab fa-twitter"></i>
</button>
<button @click="shareArticle('facebook')" class="social-share-btn bg-blue-800 dark:bg-blue-900">
<i class="fab fa-facebook"></i>
</button>
<button @click="shareArticle('whatsapp')" class="social-share-btn bg-green-800 dark:bg-green-900">
<i class="fab fa-whatsapp"></i>
</button>
<button @click="shareArticle('telegram')" class="social-share-btn bg-blue-800 dark:bg-blue-900">
<i class="fab fa-telegram"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 相关文章 -->
<div class="related-articles py-10 bg-gray-50 dark:bg-gray-800">
<div class="container">
<div class="max-w-4xl mx-auto">
<h3 class="text-2xl font-bold mb-6 dark:text-white">{{ $t('news.relatedArticles') }}</h3>
<div v-if="relatedArticles.length" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<NewsCard
v-for="relatedArticle in relatedArticles"
:key="relatedArticle._path"
:article="relatedArticle"
/>
</div>
<div v-else class="text-center py-4 text-gray-500 dark:text-gray-400">
{{ $t('news.noRelatedArticles') }}
</div>
</div>
</div>
</div>
</div>
<!-- 文章不存在 -->
<div v-else class="container py-20 text-center dark:bg-gray-900">
<div class="max-w-lg mx-auto">
<i class="fas fa-newspaper text-5xl text-gray-300 dark:text-gray-600 mb-6"></i>
<h2 class="text-2xl font-bold mb-2 dark:text-white">{{ $t('news.articleNotFound') }}</h2>
<p class="text-gray-600 dark:text-gray-400 mb-8">{{ $t('news.articleNotFoundDesc') }}</p>
<NuxtLink to="/awsnews" class="btn-primary">
{{ $t('news.browseAllArticles') }}
</NuxtLink>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import NewsCard from '~/components/news/NewsCard.vue';
import { useAsyncData, useHead } from '#app';
// 使用类型声明替代导入
// Nuxt 自动导入
const route = useRoute();
const { t, locale } = useI18n();
// 文章接口定义
interface Article {
_path?: string;
title?: string;
description?: string;
category?: string;
date?: string;
views?: number;
tags?: string[];
author?: string;
image?: string;
}
const slugPath = Array.isArray(route.params.slug)
? route.params.slug.join('/')
: route.params.slug;
// 确定完整路径
const contentPath = `awsnews/${slugPath}`;
// console.log('完整内容路径:', contentPath);
// 获取文章内容
const { data: article } = await useAsyncData(
`article-${contentPath}`,
async () => {
// 使用try-catch处理可能的错误
try {
// 直接查询awsnews目录下的内容
let result = await queryContent('awsnews').where({
_path: { $contains: slugPath }
}).find();
// console.log('查询到的内容:', result);
// 如果找到匹配的内容,返回第一个
if (result && result.length > 0) {
return result[0];
}
// 如果没有找到,尝试直接通过路径获取
try {
// console.log('尝试直接通过路径获取:', `awsnews/${slugPath}`);
const directResult = await queryContent(`awsnews/${slugPath}`).find();
if (directResult && directResult.length > 0) {
return directResult[0];
}
} catch (e) {
console.log('直接路径查询失败:', e);
}
// 如果仍未找到,尝试查询所有内容并在内存中过滤
const allContent = await queryContent().find();
// console.log('获取到的所有内容数量:', allContent?.length);
// 尝试通过文件名匹配
result = allContent.find((item: any) =>
item &&
item._path &&
item._path.includes(`awsnews/${slugPath}`)
);
if (result) {
// console.log('通过文件路径匹配成功:', result._path);
return result;
}
// 尝试通过标题匹配
if (Array.isArray(route.params.slug) && route.params.slug.length > 0) {
const lastSlugPart = route.params.slug[route.params.slug.length - 1];
result = allContent.find((item: any) =>
item &&
item.title &&
item._path &&
item._path.includes('awsnews') &&
item.title.toLowerCase().includes(lastSlugPart.toLowerCase())
);
if (result) {
// console.log('通过标题匹配成功:', result.title);
return result;
}
}
console.log('未找到匹配的文章内容');
return null;
} catch (error) {
console.error('获取文章错误:', error);
return null;
}
}
);
// console.log("获取到的文章:", article.value);
// 获取相关文章
const { data: relatedArticles } = await useAsyncData(
`related-articles-${contentPath}`,
async () => {
try {
// 如果文章未找到,返回空数组
if (!article.value) return [];
// 从awsnews目录获取所有文章
const allArticles = await queryContent('awsnews').find();
// console.log('用于查找相关文章的所有内容数量:', allArticles?.length);
// 过滤掉当前文章,并按相关性排序
return allArticles
.filter((item: any) =>
item &&
item._path &&
item._path !== contentPath &&
item.title
)
.map((item: any) => {
// 计算相关性得分
let score = 0;
// 相同分类加分
const itemCategory = item.category || (item.meta && item.meta.category);
const articleCategory = article.value?.category || (article.value?.meta && article.value.meta.category);
if (itemCategory && articleCategory && itemCategory === articleCategory) {
score += 5;
}
// 相同标签加分
const itemTags = item.tags || (item.meta && item.meta.tags) || [];
const articleTags = article.value?.tags || (article.value?.meta && article.value.meta.tags) || [];
if (Array.isArray(itemTags) && Array.isArray(articleTags)) {
const matchingTags = itemTags.filter((tag: string) =>
articleTags.includes(tag)
);
score += matchingTags.length * 2;
}
return { ...item, score };
})
.sort((a: any, b: any) => b.score - a.score) // 按得分降序排序
.slice(0, 3); // 取前3篇
} catch (error) {
console.error('获取相关文章错误:', error);
return [];
}
}
);
// 设置页面标题和元数据
useHead({
title: article.value?.title || 'AWS新闻',
meta: [
{
name: 'description',
content: article.value?.description || 'AWS新闻和更新',
},
],
});
// 格式化日期
const formatDate = (date: string | Date) => {
if (!date) return '-'; // 如果日期为空,返回占位符
try {
// 检查是否在浏览器环境中
if (typeof window === 'undefined') {
// 服务器端或预渲染环境中
const dateObj = new Date(date);
if (isNaN(dateObj.getTime())) {
return '-';
}
return dateObj.toLocaleDateString();
}
const dateObj = new Date(date);
// 检查日期是否有效
if (isNaN(dateObj.getTime())) {
return '-'; // 如果是无效日期,返回占位符
}
return new Intl.DateTimeFormat(locale.value === 'zh-CN' ? 'zh-CN' : 'en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(dateObj);
} catch (error) {
console.error('日期格式化错误:', error);
return '-';
}
};
// 分享文章
const shareArticle = (platform: string) => {
// 获取当前URL
const currentUrl = window?.location?.href || '';
const articleTitle = article.value?.title || '';
// 不同平台的分享URL
const shareUrls: Record<string, string> = {
twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(currentUrl)}&text=${encodeURIComponent(articleTitle)}`,
facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(currentUrl)}`,
whatsapp: `https://api.whatsapp.com/send?text=${encodeURIComponent(articleTitle + ' ' + currentUrl)}`,
telegram: `https://t.me/share/url?url=${encodeURIComponent(currentUrl)}&text=${encodeURIComponent(articleTitle)}`
};
// 打开分享窗口
if (shareUrls[platform]) {
window.open(shareUrls[platform], '_blank', 'width=600,height=400');
}
};
// 获取分类样式
const getCategoryClass = (category: string) => {
const categoryClasses: Record<string, string> = {
'cloud-computing': 'bg-blue-100 text-blue-700',
'security': 'bg-red-100 text-red-700',
'serverless': 'bg-purple-100 text-purple-700',
'ai': 'bg-green-100 text-green-700',
'database': 'bg-yellow-100 text-yellow-700',
'other': 'bg-gray-100 text-gray-700'
};
return categoryClasses[category] || categoryClasses.other;
};
</script>
<style scoped>
.article-header {
background-image: linear-gradient(to right, rgba(35, 47, 62, 0.05), rgba(255, 153, 0, 0.05));
}
.dark .article-header {
background-image: linear-gradient(to right, rgba(35, 47, 62, 0.3), rgba(255, 153, 0, 0.2));
}
.category-badge {
@apply px-2 py-1 rounded text-xs font-medium;
}
.social-share-btn {
@apply w-8 h-8 rounded-full text-white flex items-center justify-center transition-transform hover:scale-110;
}
/* 自定义文章内容样式 */
:deep(.prose) {
@apply max-w-none;
}
:deep(.prose h2) {
@apply text-2xl font-bold mt-8 mb-4 pb-2 border-b border-gray-200 dark:border-gray-700 dark:text-white;
}
:deep(.prose h3) {
@apply text-xl font-bold mt-6 mb-3 dark:text-gray-200;
}
:deep(.prose h4) {
@apply text-lg font-semibold mt-4 mb-2 dark:text-gray-300;
}
:deep(.prose blockquote) {
@apply border-l-4 border-blue-500 bg-blue-50 dark:bg-gray-700 dark:border-blue-400 py-2 px-4 my-4 dark:text-gray-200;
}
:deep(.prose pre) {
@apply bg-gray-800 text-white p-4 rounded-lg overflow-x-auto;
}
:deep(.prose a) {
@apply text-blue-600 hover:underline dark:text-blue-400;
}
:deep(.prose ul) {
@apply list-disc list-inside space-y-2 dark:text-gray-300;
}
:deep(.prose ol) {
@apply list-decimal list-inside space-y-2 dark:text-gray-300;
}
:deep(.prose p) {
@apply dark:text-gray-300;
}
:deep(.prose img) {
@apply dark:border dark:border-gray-700 dark:rounded-lg;
}
:deep(.prose table) {
@apply dark:border-gray-700;
}
:deep(.prose th) {
@apply dark:border-gray-700 dark:text-gray-200 dark:bg-gray-800;
}
:deep(.prose td) {
@apply dark:border-gray-700 dark:text-gray-300;
}
:deep(.prose thead) {
@apply dark:bg-gray-700 dark:text-gray-200;
}
:deep(.prose tbody tr) {
@apply dark:border-gray-700 dark:hover:bg-gray-800/50;
}
:deep(.prose code) {
@apply dark:bg-gray-700 dark:text-gray-200;
}
:deep(.prose figcaption) {
@apply dark:text-gray-400;
}
:deep(.prose strong) {
@apply dark:text-white;
}
:deep(.prose hr) {
@apply dark:border-gray-700;
}
.dark .related-articles {
background-image: linear-gradient(to right, rgba(35, 47, 62, 0.2), rgba(35, 47, 62, 0.1));
}
</style>