207 lines
6.5 KiB
Vue
207 lines
6.5 KiB
Vue
|
|
<!-- 新闻文章卡片组件 -->
|
|||
|
|
<template>
|
|||
|
|
<div class="news-card card-hover">
|
|||
|
|
<NuxtLink :to="getArticleUrl(article)">
|
|||
|
|
<div class="news-card-image">
|
|||
|
|
<img v-if="article.image || (article.meta && article.meta.image)" :src="article.image || article.meta.image" :alt="article.title" class="w-full h-48 object-cover rounded-t-lg">
|
|||
|
|
<div v-else class="w-full h-48 bg-gray-200 flex items-center justify-center rounded-t-lg">
|
|||
|
|
<i class="fas fa-cloud text-4xl text-gray-400"></i>
|
|||
|
|
</div>
|
|||
|
|
<span v-if="article.trending" class="trending-badge">
|
|||
|
|
<i class="fas fa-fire-alt mr-1"></i> {{ $t('news.meta.trending') }}
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
<div class="news-card-content">
|
|||
|
|
<div class="news-card-category">
|
|||
|
|
<span :class="getCategoryClass(article.category || (article.meta && article.meta.category) || 'other')">
|
|||
|
|
{{ $t(`news.categories.${article.category || (article.meta && article.meta.category) || 'other'}`) }}
|
|||
|
|
</span>
|
|||
|
|
<span class="news-card-date">{{ formatDate(article.date || (article.meta && article.meta.date)) }}</span>
|
|||
|
|
</div>
|
|||
|
|
<h3 class="news-card-title">{{ article.title || $t('news.noTitle') }}</h3>
|
|||
|
|
<p class="news-card-description">{{ article.description || '' }}</p>
|
|||
|
|
<div class="news-card-meta">
|
|||
|
|
<span>
|
|||
|
|
<i class="fas fa-eye mr-1"></i> {{ article.views || (article.meta && article.meta.views) || 0 }}
|
|||
|
|
</span>
|
|||
|
|
<span>
|
|||
|
|
<!-- <i class="fas fa-user mr-1"></i> {{ article.author || (article.meta && article.meta.author) || $t('news.meta.unknownAuthor') }} -->
|
|||
|
|
<i class="fas fa-user mr-1"></i>
|
|||
|
|
</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</NuxtLink>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup lang="ts">
|
|||
|
|
/// <reference types="../../node_modules/.vue-global-types/vue_3.5_0_0_0.d.ts" />
|
|||
|
|
import type { PropType } from 'vue';
|
|||
|
|
import { useI18n } from 'vue-i18n';
|
|||
|
|
|
|||
|
|
const { t, locale } = useI18n();
|
|||
|
|
|
|||
|
|
// 文章类型定义
|
|||
|
|
const props = defineProps({
|
|||
|
|
article: {
|
|||
|
|
type: Object as PropType<any>,
|
|||
|
|
required: true
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 生成文章URL
|
|||
|
|
const getArticleUrl = (article: any) => {
|
|||
|
|
if (!article) return `/awsnews`;
|
|||
|
|
|
|||
|
|
// 如果有_path属性,使用它来构建链接
|
|||
|
|
if (article._path) {
|
|||
|
|
// 如果路径以content开头,移除content前缀
|
|||
|
|
if (article._path.startsWith('content/')) {
|
|||
|
|
return `/${article._path.replace('content/', '')}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果路径以awsnews开头但不是/awsnews开头,添加前导斜杠
|
|||
|
|
if (article._path.startsWith('awsnews/') && !article._path.startsWith('/awsnews/')) {
|
|||
|
|
return `/${article._path}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果路径已经是/awsnews开头,直接使用
|
|||
|
|
if (article._path.startsWith('/awsnews/')) {
|
|||
|
|
return article._path;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他情况,添加/awsnews/前缀
|
|||
|
|
return `/awsnews/${article._path}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有路径属性
|
|||
|
|
if (article.path) {
|
|||
|
|
// 如果路径以content开头,移除content前缀
|
|||
|
|
if (article.path.startsWith('content/')) {
|
|||
|
|
return `/${article.path.replace('content/', '')}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果路径以awsnews开头但不是/awsnews开头,添加前导斜杠
|
|||
|
|
if (article.path.startsWith('awsnews/') && !article.path.startsWith('/awsnews/')) {
|
|||
|
|
return `/${article.path}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果路径已经是/awsnews开头,直接使用
|
|||
|
|
if (article.path.startsWith('/awsnews/')) {
|
|||
|
|
return article.path;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 其他情况,添加/awsnews/前缀
|
|||
|
|
return `/awsnews/${article.path}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有_dir属性,可能表示内容目录
|
|||
|
|
if (article._dir && article._file) {
|
|||
|
|
const fileName = article._file.split('/').pop() || '';
|
|||
|
|
const fileNameWithoutExt = fileName.replace(/\.md$/, '');
|
|||
|
|
return `/awsnews/${fileNameWithoutExt}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 基于标题和可能的类别构建URL
|
|||
|
|
if (article.title) {
|
|||
|
|
// 尝试从meta.category或直接的category获取
|
|||
|
|
const category = article.meta?.category || article.category || 'uncategorized';
|
|||
|
|
const slug = article.title
|
|||
|
|
.toLowerCase()
|
|||
|
|
.replace(/[^\w\s-]/g, '') // 移除特殊字符
|
|||
|
|
.replace(/\s+/g, '-'); // 空格替换为连字符
|
|||
|
|
|
|||
|
|
return `/awsnews/${slug}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 后备方案
|
|||
|
|
return `/awsnews`;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
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: 'short',
|
|||
|
|
day: 'numeric'
|
|||
|
|
}).format(dateObj);
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('日期格式化错误:', error);
|
|||
|
|
return '-';
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 获取分类标签样式
|
|||
|
|
const getCategoryClass = (category: string) => {
|
|||
|
|
const baseClass = 'px-2 py-1 rounded text-xs font-medium';
|
|||
|
|
const categoryClasses: Record<string, string> = {
|
|||
|
|
'cloud-computing': `${baseClass} bg-blue-100 text-blue-700`,
|
|||
|
|
'security': `${baseClass} bg-red-100 text-red-700`,
|
|||
|
|
'serverless': `${baseClass} bg-purple-100 text-purple-700`,
|
|||
|
|
'ai': `${baseClass} bg-green-100 text-green-700`,
|
|||
|
|
'database': `${baseClass} bg-yellow-100 text-yellow-700`,
|
|||
|
|
'other': `${baseClass} bg-gray-100 text-gray-700`
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return categoryClasses[category] || categoryClasses.other;
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.news-card {
|
|||
|
|
@apply bg-white dark:bg-gray-800 rounded-lg shadow transition-all duration-300 overflow-hidden;
|
|||
|
|
max-width: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-image {
|
|||
|
|
@apply relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.trending-badge {
|
|||
|
|
@apply absolute top-2 right-2 bg-red-500 text-white text-xs px-2 py-1 rounded;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-content {
|
|||
|
|
@apply p-4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-category {
|
|||
|
|
@apply flex justify-between items-center mb-2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-date {
|
|||
|
|
@apply text-xs text-gray-500 dark:text-gray-400;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-title {
|
|||
|
|
@apply text-lg font-bold mb-2 line-clamp-2 dark:text-white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-description {
|
|||
|
|
@apply text-sm text-gray-600 dark:text-gray-300 mb-4 line-clamp-3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.news-card-meta {
|
|||
|
|
@apply flex justify-between text-xs text-gray-500 dark:text-gray-400 pt-2 border-t border-gray-100 dark:border-gray-700;
|
|||
|
|
}
|
|||
|
|
</style>
|