<!-- views/Home.vue - 首页视图组件 -->
<template>
<div class="home">
<el-row :gutter="20">
<!-- 左侧边栏 -->
<el-col :span="6">
<div class="sidebar">
<!-- 分类列表 -->
<el-card class="category-card">
<template #header>
<div class="card-header">
<span>分类</span>
</div>
</template>
<div class="category-list">
<el-menu
:default-active="activeCategory"
@select="handleCategorySelect"
>
<el-menu-item index="all">
<span>全部</span>
</el-menu-item>
<el-menu-item
v-for="category in categories"
:key="category.id"
:index="category.id.toString()"
>
<span>{{ category.name }}</span>
</el-menu-item>
</el-menu>
</div>
</el-card>
</div>
</el-col>
<!-- 右侧内容区 -->
<el-col :span="18">
<div class="content-area">
<!-- 顶部操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="handleCreatePost">
<el-icon><Plus /></el-icon> 发布新帖
</el-button>
<el-input
v-model="searchKeyword"
placeholder="搜索帖子"
class="search-input"
clearable
@keyup.enter="handleSearch"
>
<template #suffix>
<el-icon class="el-input__icon" @click="handleSearch">
<Search />
</el-icon>
</template>
</el-input>
</div>
<!-- 帖子列表 -->
<div class="post-list">
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-skeleton :rows="10" animated />
</div>
<!-- 帖子列表内容 -->
<template v-else>
<el-empty v-if="posts.length === 0" description="暂无帖子" />
<el-card v-for="post in posts" :key="post.id" class="post-item" shadow="hover">
<div class="post-item-content" @click="viewPostDetail(post.id)">
<div class="post-item-header">
<h3 class="post-item-title">{{ post.title }}</h3>
<el-tag size="small" type="info">{{ getCategoryName(post.categoryId) }}</el-tag>
</div>
<div class="post-item-body">
<p class="post-item-summary">{{ truncateContent(post.content) }}</p>
</div>
<div class="post-item-footer">
<div class="post-item-author">
<el-avatar :size="24" :src="post.author.avatar || ''">
{{ post.author.username.charAt(0).toUpperCase() }}
</el-avatar>
<span class="author-name">{{ post.author.username }}</span>
</div>
<div class="post-item-meta">
<span class="post-item-time">{{ formatDate(post.createTime) }}</span>
<span class="post-item-stats">
<el-icon><View /></el-icon> {{ post.viewCount || 0 }}
<el-icon><ChatDotRound /></el-icon> {{ post.commentCount || 0 }}
<el-icon><Star /></el-icon> {{ post.likeCount || 0 }}
</span>
</div>
</div>
</div>
</el-card>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="totalPosts"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { Plus, Search, View, ChatDotRound, Star } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
export default {
name: 'Home',
components: {
Plus,
Search,
View,
ChatDotRound,
Star
},
setup() {
const store = useStore()
const router = useRouter()
// 状态
const loading = ref(false)
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const totalPosts = ref(0)
// 从store获取数据
const posts = computed(() => store.getters.allPosts)
const categories = computed(() => store.getters.getCategories)
const activeCategory = computed(() => store.getters.getActiveCategory)
// 获取帖子列表
const fetchPosts = async () => {
loading.value = true
try {
const response = await store.dispatch('fetchPosts', {
categoryId: activeCategory.value,
page: currentPage.value - 1, // 后端分页从0开始
size: pageSize.value
})
totalPosts.value = response.totalElements || 0
} catch (error) {
ElMessage.error('获取帖子列表失败')
console.error('获取帖子列表失败:', error)
} finally {
loading.value = false
}
}
// 监听分类变化,重新获取帖子
watch(activeCategory, () => {
currentPage.value = 1 // 切换分类时重置页码
fetchPosts()
})
// 处理分类选择
const handleCategorySelect = (categoryId) => {
store.dispatch('setActiveCategory', categoryId)
}
// 处理页码变化
const handleCurrentChange = (page) => {
currentPage.value = page
fetchPosts()
}
// 处理每页条数变化
const handleSizeChange = (size) => {
pageSize.value = size
fetchPosts()
}
// 处理搜索
const handleSearch = () => {
if (searchKeyword.value.trim()) {
// 这里应该调用搜索API
ElMessage.info(`搜索: ${searchKeyword.value}`)
// 实际项目中需要实现后端搜索API
}
}
// 处理创建帖子
const handleCreatePost = () => {
// 检查用户是否已登录
if (!store.getters.isAuthenticated) {
ElMessage.warning('请先登录')
router.push('/login')
return
}
router.push('/create-post')
}
// 查看帖子详情
const viewPostDetail = (postId) => {
router.push(`/post/${postId}`)
}
// 获取分类名称
const getCategoryName = (categoryId) => {
const category = categories.value.find(c => c.id === categoryId)
return category ? category.name : '未分类'
}
// 截断内容
const truncateContent = (content) => {
if (!content) return ''
return content.length > 100 ? content.substring(0, 100) + '...' : content
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
const now = new Date()
const diff = now - date
// 一分钟内
if (diff < 60 * 1000) {
return '刚刚'
}
// 一小时内
if (diff < 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 1000))}分钟前`
}
// 一天内
if (diff < 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (60 * 60 * 1000))}小时前`
}
// 一周内
if (diff < 7 * 24 * 60 * 60 * 1000) {
return `${Math.floor(diff / (24 * 60 * 60 * 1000))}天前`
}
// 其他
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
}
// 组件挂载时获取帖子列表
onMounted(() => {
fetchPosts()
})
return {
loading,
posts,
categories,
activeCategory,
searchKeyword,
currentPage,
pageSize,
totalPosts,
handleCategorySelect,
handleCurrentChange,
handleSizeChange,
handleSearch,
handleCreatePost,
viewPostDetail,
getCategoryName,
truncateContent,
formatDate
}
}
}
</script>
<style scoped>
.home {
width: 100%;
}
.sidebar {
position: sticky;
top: 80px;
}
.category-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.action-bar {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.search-input {
width: 300px;
}
.post-item {
margin-bottom: 15px;
cursor: pointer;
transition: all 0.3s;
}
.post-item:hover {
transform: translateY(-3px);
}
.post-item-content {
display: flex;
flex-direction: column;
}
.post-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.post-item-title {
margin: 0;
font-size: 18px;
color: #303133;
}
.post-item-summary {
color: #606266;
margin: 0 0 10px 0;
line-height: 1.5;
}
.post-item-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
font-size: 14px;
color: #909399;
}
.post-item-author {
display: flex;
align-items: center;
}
.author-name {
margin-left: 8px;
}
.post-item-meta {
display: flex;
align-items: center;
}
.post-item-time {
margin-right: 15px;
}
.post-item-stats {
display: flex;
align-items: center;
}
.post-item-stats .el-icon {
margin: 0 5px 0 15px;
}
.pagination-container {
margin-top: 30px;
display: flex;
justify-content: center;
}
.loading-container {
padding: 20px;
}
</style>