302 lines
9.0 KiB
Vue
302 lines
9.0 KiB
Vue
<template>
|
||
<div>
|
||
<!-- 页面标题 -->
|
||
<HeroBanner
|
||
:titleKey="'news.hero.title'"
|
||
:subtitleKey="'news.hero.subtitle'"
|
||
bgImage="/images/bg/cases-bg.jpg"
|
||
:descriptionKey="'news.hero.description'"
|
||
>
|
||
<!-- 按钮或其他内容 -->
|
||
</HeroBanner>
|
||
<Process/>
|
||
<div class="news-header bg-gray-50 dark:bg-gray-800 py-10 border-b dark:border-gray-700">
|
||
<div class="container">
|
||
<h1 class="text-3xl font-bold mb-2 dark:text-white">{{ $t('news.title') }}</h1>
|
||
<p class="text-gray-600 dark:text-gray-300">{{ $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 dark:text-white">
|
||
<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 dark:text-white">
|
||
<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 dark:bg-gray-700 rounded-lg">
|
||
<div class="text-gray-500 dark:text-gray-400">
|
||
<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 { useAsyncData } from 'nuxt/app';
|
||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { useRoute } from 'vue-router';
|
||
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';
|
||
// Nuxt 自动导入
|
||
import type { Article } from '~/types/content';
|
||
|
||
const { t, locale } = useI18n();
|
||
const route = useRoute();
|
||
|
||
import { useHead } from "nuxt/app";
|
||
useHead({
|
||
title: () => t("meta.news.title"),
|
||
meta: [
|
||
{ name: "description", content: () => t("meta.news.description") },
|
||
{ name: "keywords", content: () => t("meta.news.keywords") },
|
||
],
|
||
});
|
||
|
||
// 当前选中的分类
|
||
const selectedCategory = ref('all');
|
||
|
||
// 从内容模块获取文章
|
||
const { data: articles } = await useAsyncData('aws-articles', async () => {
|
||
try {
|
||
// 直接查询awsnews目录下的内容
|
||
const allContent = await queryContent('awsnews').find();
|
||
// console.log('查询到的所有文章:', allContent);
|
||
|
||
// 过滤出有效的文章(含必要字段的内容)
|
||
return allContent.filter((item: any) =>
|
||
item && item.title
|
||
);
|
||
} 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> |