Files
MyfronyProject/src/views/home.vue
qingfeng1121 f4263af343 refactor(前端): 重构前端代码结构并优化功能
重构路由配置和API调用逻辑,统一分页处理方式
优化分类和标签模块的交互,提取蒙版组件到主布局
调整样式和布局,增强响应式设计
更新接口字段名以保持前后端一致性
添加网站运行时间显示功能
2025-12-18 15:20:14 +08:00

470 lines
13 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 articleList" :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>
<!-- 分页区域 -->
<el-pagination size="medium" background :layout="pageLayout" v-model:current-page="pageNum" hide-on-single-page="true" @current-change="changePage" :page-size="pageSize" :page-count="totalPages" class="mt-4" />
</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 pageNum = ref(1) // 当前页码
const pageSize = ref(10) // 每页数量
const totalPages = ref(0) // 总页数
const pageLayout = ref('pager, next')// 分页布局
// 响应式状态
const articleList = ref([])
const loading = ref(false)
// ========== 分页数据处理 ==========
/**
* 处理分页组件的数据更新
* @param {Array} data - 分页组件传递的当前页数据
*/
// ========== 文章数据获取模块 ==========
/**
* 根据路由路径获取对应的文章列表
* @returns {Promise<Object>} 文章列表响应数据
*/
const getArticlesByRoute = async () => {
// 检查URL参数确定获取文章的方式
const pathSegment = route.path.split('/')[2]
// console.log('当前路由分段:', pathSegment)
switch (pathSegment) {
case 'aericleattribute':
// 按属性类型获取文章
const attributeData = globalStore.getValue('attribute')
return await articleService.getPagedArticles({attributeid: attributeData?.id}, pageNum.value, pageSize.value)
case 'aericlecategory':
// 按分类类型获取文章
const categoryData = globalStore.getValue('category')
return await articleService.getPagedArticles({categoryid: categoryData?.id}, pageNum.value, pageSize.value)
case 'aericletitle':
// 按标题搜索文章
const titleData = globalStore.getValue('articleserarch')
console.log('按标题搜索文章:', titleData.name)
return await articleService.getPagedArticles({title: titleData?.name}, pageNum.value, pageSize.value)
case 'aericlestatus':
// 按状态获取文章
const statusData = globalStore.getValue('articlestatus')
return await articleService.getPagedArticles({status: statusData?.status}, pageNum.value, pageSize.value)
default:
// 默认获取所有文章
// console.log('获取所有文章列表')
return await articleService.getPagedArticles({status: 1}, pageNum.value, pageSize.value)
}
}
/**
* 为单篇文章补充额外信息(留言数量、分类名称等)
* @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
}
/**
* 获取文章列表主函数
*/
const fetchArticles = async () => {
let response = {}
try {
loading.value = true
// 1. 根据路由获取文章列表
response = await getArticlesByRoute()
// console.log('更新后的文章列表:', response)
// 2. 确保数据存在
if (!response.data.content || !Array.isArray(response.data.content)) {
articleList.value = []
return
}
// 3. 为文章列表补充额外信息
const enrichedArticles = await enrichArticlesWithExtraInfo(response.data.content)
// 4. 更新文章列表
articleList.value = enrichedArticles
} catch (error) {
console.error('获取文章列表失败:', error)
ElMessage.error('获取文章列表失败,请稍后重试')
} finally {
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('操作失败,请稍后重试')
}
}
/**
* 处理分页变化事件
* @param {number} newPage - 新的页码
*/
const changePage = (newPage) => {
fetchArticles()
// 根据当前页码优化分页布局
if (page === 1) {
// 第一页只显示页码和下一页按钮
pageLayout.value = 'pager, next'
} else if (page === totalPages.value) {
// 最后一页只显示上一页按钮和页码
pageLayout.value = 'prev, pager'
} else {
// 中间页显示完整的上一页、页码、下一页
pageLayout.value = 'prev, pager, next'
}
}
// ========== 生命周期和监听器 ==========
/**
* 处理路由变化的回调函数
*/
const handleRouteChange = () => {
fetchArticles()
// console.log('路由变化,重新获取文章列表')
}
/**
* 处理文章列表变化的回调函数
* @param {Array} newList - 新的文章列表
*/
// 组件挂载时获取数据
onMounted(() => {
fetchArticles()
})
// 监听路由变化,确保刷新时也能重新获取数据
watch(
// 监听路由路径和查询参数变化
() => [route.path, route.query],
// 路由变化时触发获取文章列表
handleRouteChange,
{ deep: true }
)
// 监听原始文章列表变化,确保初始数据正确显示
watch(
() => articleList.value,
{ 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 {
}
.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>