ct/app/components/RecommendedArticles.vue
2025-10-11 08:45:18 +08:00

314 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts" setup>
import { ref, onMounted, computed } from 'vue'
import { fetchRecommendedArticles, getArticleTypeName, type Article } from '~/composables/useArticleApi'
// 组件属性
interface Props {
showTypes?: string[] // 显示的文章类型
articlesPerType?: number // 每个类型显示的文章数量
showTitle?: boolean // 是否显示标题
title?: string // 自定义标题
}
const props = withDefaults(defineProps<Props>(), {
showTypes: () => ['solution', 'developKnowledge', 'industryTrend'],
articlesPerType: 5,
showTitle: true,
title: '推荐文章'
})
// 响应式数据
const articles = ref<Record<string, Article[]>>({})
const loading = ref(false)
const error = ref('')
// 计算属性
const hasArticles = computed(() => {
return Object.values(articles.value).some(typeArticles => typeArticles.length > 0)
})
// 格式化时间
const formatTime = (timeStr: string): string => {
if (!timeStr) return ''
const date = new Date(timeStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 获取文章链接
const getArticleLink = (article: Article): string => {
// 根据文章ID生成链接这里可以根据实际路由规则调整
return `/article/${article.id}`
}
// 加载推荐文章
const loadRecommendedArticles = async () => {
loading.value = true
error.value = ''
try {
const result = await fetchRecommendedArticles(props.showTypes, props.articlesPerType)
articles.value = result
} catch (err) {
console.error('加载推荐文章失败:', err)
error.value = '加载推荐文章失败,请稍后重试'
} finally {
loading.value = false
}
}
// 组件挂载时加载数据
onMounted(() => {
loadRecommendedArticles()
})
</script>
<template>
<div class="recommended-articles">
<!-- 标题 -->
<div v-if="showTitle" class="articles-title">
<h3>{{ title }}</h3>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<span>加载中...</span>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error">
<p>{{ error }}</p>
<button @click="loadRecommendedArticles" class="retry-btn">重试</button>
</div>
<!-- 文章列表 -->
<div v-else-if="hasArticles" class="articles-content">
<div
v-for="(typeArticles, typeCode) in articles"
:key="typeCode"
v-show="typeArticles.length > 0"
class="article-type-section"
>
<!-- 类型标题 -->
<div class="type-title">
<h4>{{ getArticleTypeName(typeCode) }}</h4>
</div>
<!-- 文章列表 -->
<div class="article-list">
<div
v-for="article in typeArticles"
:key="article.id"
class="article-item"
>
<a
:href="getArticleLink(article)"
:title="article.title"
class="article-link"
>
<div class="article-content">
<h5 class="article-title">{{ article.title }}</h5>
<div class="article-meta">
<span class="article-date">{{ formatTime(article.createTime) }}</span>
</div>
<p v-if="article.brief" class="article-brief">{{ article.brief }}</p>
</div>
</a>
</div>
</div>
</div>
</div>
<!-- 无数据状态 -->
<div v-else class="no-articles">
<p>暂无推荐文章</p>
</div>
</div>
</template>
<style scoped>
.recommended-articles {
width: 100%;
}
.articles-title {
margin-bottom: 20px;
}
.articles-title h3 {
font-size: 24px;
font-weight: bold;
color: #333;
margin: 0;
padding-bottom: 10px;
border-bottom: 2px solid #007bff;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #666;
}
.loading-spinner {
width: 20px;
height: 20px;
border: 2px solid #f3f3f3;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
text-align: center;
padding: 40px 0;
color: #dc3545;
}
.retry-btn {
background: #007bff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
.retry-btn:hover {
background: #0056b3;
}
.articles-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.article-type-section {
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.type-title {
margin-bottom: 15px;
}
.type-title h4 {
font-size: 18px;
font-weight: 600;
color: #007bff;
margin: 0;
padding-bottom: 8px;
border-bottom: 1px solid #e9ecef;
}
.article-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.article-item {
border-bottom: 1px solid #f8f9fa;
padding-bottom: 12px;
}
.article-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.article-link {
text-decoration: none;
color: inherit;
display: block;
transition: all 0.3s ease;
}
.article-link:hover {
color: #007bff;
}
.article-content {
padding: 8px 0;
}
.article-title {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0 0 8px 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.article-meta {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 6px;
}
.article-date {
font-size: 12px;
color: #6c757d;
background: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
}
.article-brief {
font-size: 14px;
color: #666;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.no-articles {
text-align: center;
padding: 40px 0;
color: #6c757d;
}
/* 响应式设计 */
@media (max-width: 768px) {
.articles-title h3 {
font-size: 20px;
}
.article-type-section {
padding: 15px;
}
.type-title h4 {
font-size: 16px;
}
.article-title {
font-size: 14px;
}
}
</style>