323 lines
9.2 KiB
Vue
323 lines
9.2 KiB
Vue
|
|
<template>
|
|||
|
|
<div>
|
|||
|
|
<!-- 页面标题 -->
|
|||
|
|
<Banner
|
|||
|
|
:titleKey="'news.hero.title'"
|
|||
|
|
:subtitleKey="'news.hero.subtitle'"
|
|||
|
|
bgImage="/images/bg/cases-bg.webp"
|
|||
|
|
:descriptionKey="'news.hero.description'"
|
|||
|
|
>
|
|||
|
|
<!-- 按钮或其他内容 -->
|
|||
|
|
</Banner>
|
|||
|
|
<Process/>
|
|||
|
|
<div class="news-header bg-gray-50 py-10 border-b">
|
|||
|
|
<div class="container">
|
|||
|
|
<h1 class="text-3xl font-bold mb-2">{{ $t('news.title') }}</h1>
|
|||
|
|
<p class="text-gray-600">{{ $t('news.description') }}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="container py-8">
|
|||
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|||
|
|
<!-- 左侧边栏:分类过滤和热门文章 -->
|
|||
|
|
<div class="col-span-1">
|
|||
|
|
<!-- 分类过滤器 -->
|
|||
|
|
<CategoryFilter
|
|||
|
|
:categories="categoryList"
|
|||
|
|
v-model:selectedCategory="selectedCategory"
|
|||
|
|
:totalArticles="articles?.length || 0"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 热门文章列表 -->
|
|||
|
|
<TrendingNewsList :trendingNews="trendingArticles" />
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 右侧:文章列表和特色文章 -->
|
|||
|
|
<div class="col-span-1 lg:col-span-3">
|
|||
|
|
<!-- 特色文章轮播 (仅在显示全部类别时显示) -->
|
|||
|
|
<div v-if="selectedCategory === 'all' && featuredArticles.length > 0" class="mb-8">
|
|||
|
|
<h2 class="text-xl font-bold mb-4">
|
|||
|
|
<i class="fas fa-star text-yellow-500 mr-2"></i>
|
|||
|
|
{{ $t('news.featuredArticles') }}
|
|||
|
|
</h2>
|
|||
|
|
|
|||
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|||
|
|
<NewsCard
|
|||
|
|
v-for="article in featuredArticles.slice(0, 2)"
|
|||
|
|
:key="article._path"
|
|||
|
|
:article="article"
|
|||
|
|
:getPath="getArticlePath"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 分类标题 -->
|
|||
|
|
<div class="mb-6">
|
|||
|
|
<h2 class="text-xl font-bold">
|
|||
|
|
<template v-if="selectedCategory === 'all'">
|
|||
|
|
{{ $t('news.latestArticles') }}
|
|||
|
|
</template>
|
|||
|
|
<template v-else>
|
|||
|
|
{{ $t(`news.categories.${selectedCategory}`) }}
|
|||
|
|
</template>
|
|||
|
|
</h2>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 筛选后的文章列表 -->
|
|||
|
|
<div v-if="filteredArticles.length > 0">
|
|||
|
|
<div v-if="isDataReady" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|||
|
|
<NewsCard
|
|||
|
|
v-for="article in optimizedArticles"
|
|||
|
|
:key="article.id"
|
|||
|
|
:article="article"
|
|||
|
|
:getPath="getArticlePath"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="text-center py-8 bg-gray-50 rounded-lg">
|
|||
|
|
<div class="text-gray-500">
|
|||
|
|
<i class="fas fa-search text-4xl mb-4"></i>
|
|||
|
|
<p>{{ $t('news.noArticlesFound') }}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
import CategoryFilter from '~/components/news/CategoryFilter.vue';
|
|||
|
|
import NewsCard from '~/components/news/NewsCard.vue';
|
|||
|
|
import TrendingNewsList from '~/components/news/TrendingNewsList.vue';
|
|||
|
|
import Process from '~/components/Process.vue';
|
|||
|
|
import { useAsyncData, useHead} from 'nuxt/app';
|
|||
|
|
|
|||
|
|
|
|||
|
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
|||
|
|
import { useI18n } from 'vue-i18n';
|
|||
|
|
import { useRoute } from 'vue-router';
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
// Nuxt 自动导入
|
|||
|
|
// 文章类型接口
|
|||
|
|
interface Article {
|
|||
|
|
_path?: string;
|
|||
|
|
path?: string;
|
|||
|
|
title: string;
|
|||
|
|
description: string;
|
|||
|
|
category: string;
|
|||
|
|
date: string;
|
|||
|
|
views: number;
|
|||
|
|
featured?: boolean;
|
|||
|
|
trending?: boolean;
|
|||
|
|
author?: string;
|
|||
|
|
image?: string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const { t, locale } = useI18n();
|
|||
|
|
const route = useRoute();
|
|||
|
|
|
|||
|
|
// 设置页面元数据
|
|||
|
|
useHead({
|
|||
|
|
title: 'AWS资讯中心',
|
|||
|
|
meta: [
|
|||
|
|
{ name: 'description', content: 'AWS云计算最新资讯、技术更新和最佳实践' }
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 当前选中的分类
|
|||
|
|
const selectedCategory = ref('all');
|
|||
|
|
|
|||
|
|
// 从内容模块获取文章
|
|||
|
|
const { data: articles } = await useAsyncData('aws-articles', async () => {
|
|||
|
|
try {
|
|||
|
|
// 先尝试直接查询全部内容
|
|||
|
|
const allContent = await queryContent('awsnews').find();
|
|||
|
|
// console.log('查询到的所有内容:', allContent);
|
|||
|
|
|
|||
|
|
// 过滤出有效的文章(含必要字段的内容)
|
|||
|
|
return allContent.filter((item: any) => {
|
|||
|
|
// console.log('item:', item);
|
|||
|
|
return item && item.title
|
|||
|
|
});
|
|||
|
|
// item &&
|
|||
|
|
// item.title &&
|
|||
|
|
// (item._path?.includes('awsnews') || item._path?.includes('cloud-computing') || item.category));
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('文章加载错误:', e);
|
|||
|
|
return [];
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// // 调试输出
|
|||
|
|
// console.log('文章列表数量:', articles.value?.length);
|
|||
|
|
// if (articles.value?.length > 0) {
|
|||
|
|
// console.log('第一篇文章示例:', articles.value[0]);
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
// 热门文章(按浏览量排序)
|
|||
|
|
const trendingArticles = computed(() => {
|
|||
|
|
if (!articles.value || !Array.isArray(articles.value)) return [];
|
|||
|
|
|
|||
|
|
return [...articles.value]
|
|||
|
|
.filter((article: any) => article && typeof article === 'object')
|
|||
|
|
.sort((a: Article, b: Article) => (b.views || 0) - (a.views || 0))
|
|||
|
|
.slice(0, 5);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 特色文章
|
|||
|
|
const featuredArticles = computed(() => {
|
|||
|
|
if (!articles.value || !Array.isArray(articles.value)) return [];
|
|||
|
|
|
|||
|
|
return articles.value
|
|||
|
|
.filter((article: any) => article && typeof article === 'object' && article.featured);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 根据选择的分类筛选文章
|
|||
|
|
const filteredArticles = computed(() => {
|
|||
|
|
if (!articles.value || !Array.isArray(articles.value)) return [];
|
|||
|
|
|
|||
|
|
const validArticles = articles.value.filter((article: any) => article && typeof article === 'object');
|
|||
|
|
|
|||
|
|
// 如果选择"全部",则返回所有文章
|
|||
|
|
if (selectedCategory.value === 'all') {
|
|||
|
|
return validArticles;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 否则按分类筛选
|
|||
|
|
return validArticles.filter(
|
|||
|
|
(article: Article) => article.category === selectedCategory.value
|
|||
|
|
);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 计算分类列表及每个分类的文章数量
|
|||
|
|
const categoryList = computed(() => {
|
|||
|
|
if (!articles.value || !Array.isArray(articles.value)) return [];
|
|||
|
|
|
|||
|
|
const validArticles = articles.value.filter((article: any) =>
|
|||
|
|
article && typeof article === 'object'
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 计算每个分类的文章数量
|
|||
|
|
const categoryCounts: Record<string, number> = {};
|
|||
|
|
|
|||
|
|
for (const article of validArticles) {
|
|||
|
|
const category = article.category;
|
|||
|
|
if (category) {
|
|||
|
|
if (categoryCounts[category]) {
|
|||
|
|
categoryCounts[category]++;
|
|||
|
|
} else {
|
|||
|
|
categoryCounts[category] = 1;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 确保所有已知分类都在列表中,即使没有文章
|
|||
|
|
const knownCategories = ['cloud-computing', 'security', 'serverless', 'ai', 'database', 'other'];
|
|||
|
|
for (const category of knownCategories) {
|
|||
|
|
if (!categoryCounts[category]) {
|
|||
|
|
categoryCounts[category] = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 转换为组件所需格式
|
|||
|
|
return Object.entries(categoryCounts)
|
|||
|
|
.map(([value, count]) => ({
|
|||
|
|
value,
|
|||
|
|
count
|
|||
|
|
}))
|
|||
|
|
.sort((a, b) => b.count - a.count); // 按文章数量降序排序
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 添加延迟加载
|
|||
|
|
const mounted = ref(false);
|
|||
|
|
onMounted(() => {
|
|||
|
|
mounted.value = true;
|
|||
|
|
// 首先渲染必要内容
|
|||
|
|
nextTick(() => {
|
|||
|
|
// 延迟加载非关键内容
|
|||
|
|
setTimeout(() => {
|
|||
|
|
// 可以在这里加载更多内容或执行昂贵操作
|
|||
|
|
}, 100);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
mounted.value = false;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 限制列表渲染数量
|
|||
|
|
const optimizedArticles = computed(() => {
|
|||
|
|
const result = filteredArticles.value.slice(0, 12); // 限制初始渲染数量
|
|||
|
|
return result;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
const formatDate = (date: string | Date) => {
|
|||
|
|
// 处理空值和无效值
|
|||
|
|
if (!date) return '日期未知';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
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: 'short',
|
|||
|
|
day: 'numeric'
|
|||
|
|
}).format(dateObj);
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('日期格式化错误:', e);
|
|||
|
|
return '日期未知';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 安全获取文章路径
|
|||
|
|
const getArticlePath = (article: any) => {
|
|||
|
|
if (!article) return '#';
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 检查是否存在有效路径
|
|||
|
|
const path = article._path || article.path || '';
|
|||
|
|
// 提取最后一部分作为 slug
|
|||
|
|
const slug = path.split('/').pop() || article.id?.split('/').pop() || '';
|
|||
|
|
return `/awsnews/${slug}`;
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('路径生成错误:', e);
|
|||
|
|
return '#'; // 发生错误时返回空链接
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 在NewsCard组件内部的任何DOM操作前检查元素是否存在
|
|||
|
|
const safelyAccessDOM = (callback: () => void) => {
|
|||
|
|
try {
|
|||
|
|
// 仅在组件挂载后执行DOM操作
|
|||
|
|
if (mounted.value) {
|
|||
|
|
callback();
|
|||
|
|
}
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('DOM操作错误:', error);
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 添加加载状态控制
|
|||
|
|
const isDataReady = ref(false);
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
// ...加载文章数据
|
|||
|
|
await nextTick();
|
|||
|
|
isDataReady.value = true;
|
|||
|
|
});
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.news-header {
|
|||
|
|
background-image: linear-gradient(to right, rgba(35, 47, 62, 0.05), rgba(255, 153, 0, 0.05));
|
|||
|
|
}
|
|||
|
|
</style>
|