207 lines
6.5 KiB
Vue
Raw Permalink Normal View History

2025-09-11 10:55:59 +08:00
<!-- 新闻文章卡片组件 -->
<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>