Files
MyfronyProject/src/views/home.vue
qingfeng1121 0cbb91077d feat: 添加页脚组件并更新多个功能
- 新增Footer组件显示版权信息和备案号
- 替换favicon为blogicon.jpg
- 更新API基础URL为生产环境
- 重命名nonsenseService方法为createNonsense
- 在文章编辑页添加返回列表按钮
- 优化分类和标签创建后的页面跳转逻辑
- 移除home.vue中不必要的height样式
2025-12-11 12:42:54 +08:00

466 lines
12 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.

<!-- 文章列表组件 -->
<template>
<div class="article-list-container">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :count="5" />
</div>
<!-- 文章列表 -->
<transition-group name="article-item" tag="div" class="article-list-content" v-else>
<div class="article-card" v-for="article in displayedArticles" :key="article.articleId"
@click="handleArticleClick(article)">
<h6 class="article-title">{{ article.title }}</h6>
<div v-if="article.marked" class="article-special-tag">标记文章</div>
<p class="article-content-preview">{{ formatContentPreview(article.content, 150) }}</p>
<div class="article-meta-info">
<span class="article-publish-date">{{ formatRelativeTime(article.createdAt || article.createTime) }}</span>
<span v-if="article.categoryName" class="article-category-badge">{{ article.categoryName }} </span>
<span v-if="article.viewCount" class="article-views-count">{{ article.viewCount }} 阅读</span>
<span v-if="article.likes > 0" class="article-likes-count">{{ article.likes }} 点赞</span>
<span v-if="article.commentCount > 0" class="article-comments-count">{{ article.commentCount }} 评论</span>
<div class="article-status-badge-container" v-if="globalStore.Login">
<span v-if="article.status === 0" class="article-status-badge badge badge-warning">草稿</span>
<span v-if="article.status === 1" class="article-status-badge badge badge-success">已发表</span>
<span v-if="article.status === 2" class="article-status-badge badge badge-danger">已删除</span>
</div>
</div>
</div>
<!-- 分页区域 -->
<PaginationComponent class="pagination-container" :list="articleList" :pageSize="10" @changePage="handleCurrentDataUpdate" />
</transition-group>
<!-- 空状态 -->
<div v-if="!loading && articleList.length === 0" class="empty-state-container">
<el-empty description="暂无文章" />
</div>
</div>
</template>
<script setup>
import { useRouter, useRoute } from 'vue-router'
import { ref, onMounted, watch } from 'vue'
import { formatRelativeTime } from '@/utils/dateUtils'
import { formatContentPreview } from '@/utils/stringUtils'
import { ElMessage } from 'element-plus'
import { articleService, messageService, categoryAttributeService } from '@/services'
import PaginationComponent from '@/views/page.vue'
import { useGlobalStore } from '@/store/globalStore'
// ========== 组件初始化 ==========
// 全局状态管理
const globalStore = useGlobalStore()
// 路由相关
const router = useRouter()
const route = useRoute()
// 响应式状态
const articleList = ref([])
const displayedArticles = ref([])
const loading = ref(false)
// ========== 分页数据处理 ==========
/**
* 处理分页组件的数据更新
* @param {Array} data - 分页组件传递的当前页数据
*/
const handleCurrentDataUpdate = (data) => {
displayedArticles.value = data
console.log('更新后的当前页数据:', data)
}
// ========== 文章数据获取模块 ==========
/**
* 根据路由路径获取对应的文章列表
* @returns {Promise<Object>} 文章列表响应数据
*/
const getArticlesByRoute = async () => {
// 检查URL参数确定获取文章的方式
const pathSegment = route.path.split('/')[2]
console.log('当前路由分段:', pathSegment)
switch (pathSegment) {
case 'aericletype':
// 按属性类型获取文章
const attributeData = globalStore.getValue('attribute')
return await articleService.getArticlesByAttributeId(attributeData?.id)
case 'aericletitle':
// 按标题搜索文章
const titleData = globalStore.getValue('articleserarch')
return await articleService.getArticlesByTitle(titleData?.name)
case 'aericlestatus':
// 按状态获取文章
const statusData = globalStore.getValue('articlestatus')
return await articleService.getArticlesByStatus(statusData?.status)
default:
// 默认获取所有文章
console.log('获取所有文章列表')
return await articleService.getAllArticles()
}
}
/**
* 为单篇文章补充额外信息(留言数量、分类名称等)
* @param {Object} article - 文章对象
* @returns {Promise<Object>} 补充信息后的文章对象
*/
const enrichArticleWithExtraInfo = async (article) => {
try {
// 获取留言数量
const messageResponse = await messageService.getMessagesByArticleId(article.articleid)
// 获取分类名称
const categoryResponse = await categoryAttributeService.getAttributeById(article.attributeid)
// 设置分类名称
article.categoryName = categoryResponse?.data?.attributename || '未分类'
// 设置评论数量
article.commentCount = messageResponse?.data?.length || 0
// 标准化标记字段名
article.marked = article.mg
return article
} catch (err) {
console.error(`获取文章${article.articleid}额外信息失败:`, err)
// 错误情况下设置默认值
article.commentCount = 0
article.categoryName = '未分类'
return article
}
}
/**
* 批量为文章列表补充额外信息
* @param {Array} articles - 原始文章列表
* @returns {Promise<Array>} 补充信息后的文章列表
*/
const enrichArticlesWithExtraInfo = async (articles) => {
const enrichedArticles = []
for (const article of articles) {
const enrichedArticle = await enrichArticleWithExtraInfo(article)
enrichedArticles.push(enrichedArticle)
}
return enrichedArticles
}
/**
* 初始化显示文章列表
* @param {Array} articles - 完整文章列表
*/
const initializeDisplayedArticles = (articles) => {
// 初始显示前3条数据
displayedArticles.value = articles.slice(0, 3)
}
/**
* 获取文章列表主函数
*/
const fetchArticles = async () => {
let response = {}
try {
loading.value = true
// 1. 根据路由获取文章列表
response = await getArticlesByRoute()
// 2. 确保数据存在
if (!response.data || !Array.isArray(response.data)) {
articleList.value = []
displayedArticles.value = []
return
}
// 3. 为文章列表补充额外信息
const enrichedArticles = await enrichArticlesWithExtraInfo(response.data)
// 4. 更新文章列表
articleList.value = enrichedArticles
// 5. 初始化显示的文章
initializeDisplayedArticles(enrichedArticles)
} catch (error) {
console.error('获取文章列表失败:', error)
ElMessage.error('获取文章列表失败,请稍后重试')
} finally {
console.log('最终文章列表数据:', articleList.value)
loading.value = false
}
}
// ========== 文章交互模块 ==========
/**
* 处理文章点击事件
* @param {Object} article - 文章对象
*/
const handleArticleClick = (article) => {
try {
// 增加文章浏览量(异步操作,不阻塞后续流程)
articleService.incrementArticleViews(article.articleId).catch(err => {
console.error('增加文章浏览量失败:', err)
})
// 清除之前的文章信息
globalStore.removeValue('articleInfo')
// 存储文章信息到全局状态
globalStore.setValue('articleInfo', article)
// 跳转到文章详情页
router.push({
path: '/article',
})
} catch (error) {
console.error('处理文章点击事件失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
// ========== 生命周期和监听器 ==========
/**
* 处理路由变化的回调函数
*/
const handleRouteChange = () => {
fetchArticles()
console.log('路由变化,重新获取文章列表')
}
/**
* 处理文章列表变化的回调函数
* @param {Array} newList - 新的文章列表
*/
const handleArticleListChange = (newList) => {
if (newList && newList.length > 0) {
displayedArticles.value = newList.slice(0, 3)
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchArticles()
})
// 监听路由变化,确保刷新时也能重新获取数据
watch(
// 监听路由路径和查询参数变化
() => [route.path, route.query],
// 路由变化时触发获取文章列表
handleRouteChange,
{ deep: true }
)
// 监听原始文章列表变化,确保初始数据正确显示
watch(
() => articleList.value,
handleArticleListChange,
{ deep: true }
)
</script>
<style scoped>
/* 文章列表容器样式 */
.article-list-container {
max-width: 100%;
}
/* 加载状态容器 */
.loading-container {
padding: 20px;
display: flex;
justify-content: center;
}
/* 文章卡片样式 */
.article-card {
text-align: left;
border-radius: 12px;
display: flex;
flex-direction: column;
gap: 12px;
background-color: rgba(255, 255, 255, 0.85);
padding: 24px;
margin-bottom: 24px;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
cursor: pointer;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 0, 0, 0.05);
}
/* 文章卡片悬停渐变效果 */
.article-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg,
rgba(255, 255, 255, 0.6) 0%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0.05) 100%);
pointer-events: none;
opacity: 0;
transition: opacity 0.4s ease;
}
/* 文章卡片悬停效果 */
.article-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15),
0 0 30px rgba(255, 255, 255, 0.2);
border-color: rgba(52, 152, 219, 0.3);
}
.article-card:hover::before {
opacity: 1;
}
/* 文章过渡动画 */
.article-item {
transition: all 0.4s ease;
}
/* 文章标题样式 */
.article-title {
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
margin: 0;
transition: color 0.3s ease;
line-height: 1.4;
}
.article-card:hover .article-title {
color: #3498db;
}
/* 特殊标记标签样式 */
.article-special-tag {
display: inline-block;
padding: 3px 10px;
background-color: rgba(52, 152, 219, 0.15);
color: #3498db;
border-radius: 6px;
font-size: 0.75rem;
font-weight: 500;
align-self: flex-start;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.article-list-content {
position: relative;
padding: 0 0 30px 0;
}
.pagination-container {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
justify-content: center;
}
/* 文章内容预览样式 */
.article-content-preview {
color: #555;
line-height: 1.6;
margin: 0;
font-size: 0.95rem;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
/* 文章元信息容器 */
.article-meta-info {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 0.875rem;
color: #7f8c8d;
margin-top: auto;
padding-top: 10px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
/* 空状态容器样式 */
.empty-state-container {
text-align: center;
padding: 60px 20px;
color: #7f8c8d;
}
/* 过渡动画配置 */
.article-item-enter-active,
.article-item-leave-active {
transition: all 0.5s ease;
}
.article-item-enter-from,
.article-item-leave-to {
opacity: 0;
transform: translateY(20px);
}
/* 响应式设计 - 平板和手机 */
@media (max-width: 768px) {
.article-list-container {
padding: 0 10px;
}
.article-card {
padding: 18px;
margin-bottom: 18px;
gap: 10px;
}
.article-title {
font-size: 1.125rem;
}
.article-meta-info {
font-size: 0.8rem;
gap: 8px;
}
.article-content-preview {
font-size: 0.9rem;
}
/* 响应式元信息分隔符 */
.article-views-count::before,
.article-category-badge::before,
.article-likes-count::before,
.article-comments-count::before {
margin-right: 8px;
}
}
/* 响应式设计 - 小屏幕手机 */
@media (max-width: 480px) {
.article-card {
padding: 16px;
margin-bottom: 16px;
}
.article-meta-info {
font-size: 0.75rem;
gap: 6px;
}
.article-special-tag {
font-size: 0.7rem;
padding: 2px 8px;
}
}
</style>