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> |