Files
MyfronyProject/src/views/home.vue
qingfeng1121 0151afcde7 feat: 添加分类树接口和分页组件,优化留言板功能
refactor: 重构文章服务和留言服务API调用方式
fix: 修复token验证逻辑和全局状态管理问题
style: 更新页脚样式和关于页面内容
docs: 添加类型定义和接口注释
2025-12-23 13:57:35 +08:00

417 lines
11 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.createtime || article.createtime) }}</span>
<span v-if="article.attributename" class="article-category-badge">{{ article.attributename }} </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>
<!-- 分页区域 -->
<CustomPagination
v-model:pageNum="pageNum"
:pageSize="pageSize"
:totalPages="totalPages"
@pageChange="changePage"
/>
</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 { useGlobalStore } from '@/store/globalStore'
import CustomPagination from '@/components/CustomPagination.vue'
// ========== 组件初始化 ==========
// 全局状态管理
const globalStore = useGlobalStore()
// 路由相关
const router = useRouter()
const route = useRoute()
// 分页属性
const pageNum = ref(1) // 当前页码
const pageSize = ref(10) // 每页数量
const totalPages = ref(0) // 总页数
// 响应式状态
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')
// console.log('按状态获取文章:', statusData.status)
return await articleService.getPagedArticles({status: statusData.status}, pageNum.value, pageSize.value)
default:
// 默认获取所有文章
// console.log('获取所有文章列表')
return await articleService.getPagedArticles({status: 1}, pageNum.value, pageSize.value)
}
}
/**
* 获取文章列表主函数
*/
const fetchArticles = async () => {
let response = {}
try {
loading.value = true
// 1. 根据路由获取文章列表
response = await getArticlesByRoute()
// console.log('更新后的文章列表:', response)
// 2. 确保数据存在
if (!response.data || !Array.isArray(response.data)) {
articleList.value = []
totalPages.value = 0
return
}
// 3. 为文章列表补充额外信息
console.log('补充额外信息后的文章列表:', response.data)
// 4. 更新文章列表
articleList.value = response.data
// 5. 更新总页数
totalPages.value = response.totalPages
} 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.setValue('articleInfo', article)
// 跳转到文章详情页
router.push({
path: '/article',
})
} catch (error) {
console.error('处理文章点击事件失败:', error)
ElMessage.error('操作失败,请稍后重试')
}
}
/**
* 处理分页变化事件
* @param {number} newPage - 新的页码
*/
const changePage = (newPage) => {
pageNum.value = newPage
fetchArticles()
}
// ========== 生命周期和监听器 ==========
/**
* 处理路由变化的回调函数
*/
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>