460 lines
15 KiB
Vue
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> |