You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
unilife/Front/vue-unilife/src/views/PostManagement.vue

636 lines
15 KiB

<template>
<div class="post-management">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span class="title">我的帖子管理</span>
<el-button type="primary" @click="refreshData">
<el-icon><Refresh /></el-icon>
刷新
</el-button>
</div>
</template>
<!-- 搜索筛选区域 -->
<div class="filter-section">
<el-form :model="filterForm" inline>
<el-form-item label="帖子标题">
<el-input
v-model="filterForm.title"
placeholder="请输入帖子标题"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="分区">
<el-select
v-model="filterForm.category"
placeholder="请选择分区"
clearable
@change="handleSearch"
>
<el-option label="全部" value="" />
<el-option label="技术讨论" value="tech" />
<el-option label="生活随笔" value="life" />
<el-option label="学习分享" value="study" />
<el-option label="问答求助" value="qa" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 数据统计 -->
<div class="stats-section">
<el-row :gutter="20">
<el-col :span="6">
<el-statistic title="总帖子数" :value="stats.totalPosts" />
</el-col>
<el-col :span="6">
<el-statistic title="总浏览量" :value="stats.totalViews" />
</el-col>
<el-col :span="6">
<el-statistic title="总点赞数" :value="stats.totalLikes" />
</el-col>
<el-col :span="6">
<el-statistic title="平均浏览量" :value="stats.avgViews" :precision="1" />
</el-col>
</el-row>
</div>
<!-- 帖子列表表格 -->
<el-table
v-loading="loading"
:data="posts"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="title" label="帖子标题" min-width="200">
<template #default="{ row }">
<el-link type="primary" @click="viewPost(row.id)">
{{ row.title }}
</el-link>
</template>
</el-table-column>
<el-table-column prop="category" label="分区" width="120">
<template #default="{ row }">
<el-tag :type="getCategoryTagType(row.category)">
{{ getCategoryName(row.category) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="views" label="浏览量" width="100" sortable>
<template #default="{ row }">
<span class="stat-number">{{ row.views }}</span>
</template>
</el-table-column>
<el-table-column prop="likes" label="点赞量" width="100" sortable>
<template #default="{ row }">
<span class="stat-number">{{ row.likes }}</span>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="发布时间" width="180">
<template #default="{ row }">
{{ formatDate(row.createdAt) }}
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'published' ? 'success' : 'warning'">
{{ row.status === 'published' ? '已发布' : '草稿' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button
type="primary"
size="small"
@click="editPost(row.id)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="confirmDelete(row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 批量操作 -->
<div v-if="selectedPosts.length > 0" class="batch-actions">
<el-alert
:title="`已选择 ${selectedPosts.length} 个帖子`"
type="info"
show-icon
:closable="false"
/>
<div class="batch-buttons">
<el-button type="danger" @click="confirmBatchDelete">
批量删除
</el-button>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 删除确认对话框 -->
<el-dialog
v-model="deleteDialog.visible"
title="确认删除"
width="400px"
:before-close="handleCloseDialog"
>
<div class="delete-content">
<el-icon class="warning-icon"><WarningFilled /></el-icon>
<div class="delete-message">
<p v-if="deleteDialog.type === 'single'">
确定要删除帖子 <strong>"{{ deleteDialog.post?.title }}"</strong> 吗?
</p>
<p v-else>
确定要删除选中的 <strong>{{ selectedPosts.length }}</strong> 个帖子吗?
</p>
<p class="warning-text">此操作不可恢复,请谨慎操作!</p>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="deleteDialog.visible = false">取消</el-button>
<el-button
type="danger"
:loading="deleteDialog.loading"
@click="handleDelete"
>
确认删除
</el-button>
</span>
</template>
</el-dialog>
<router-link
to="/postEdit"
class="btn btn-primary btn-circle"
>
<span class="btn-icon">+</span>
</router-link>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh, WarningFilled } from '@element-plus/icons-vue'
import axios from 'axios'
// 类型定义
interface Post {
id: number
title: string
category: string
views: number
likes: number
createdAt: string
status: 'published' | 'draft'
}
interface FilterForm {
title: string
category: string
}
interface Pagination {
currentPage: number
pageSize: number
total: number
}
interface Stats {
totalPosts: number
totalViews: number
totalLikes: number
avgViews: number
}
interface DeleteDialog {
visible: boolean
loading: boolean
type: 'single' | 'batch'
post?: Post
}
// 响应式数据
const loading = ref(false)
const posts = ref<Post[]>([])
const selectedPosts = ref<Post[]>([])
const filterForm = reactive<FilterForm>({
title: '',
category: ''
})
const pagination = reactive<Pagination>({
currentPage: 1,
pageSize: 20,
total: 0
})
const deleteDialog = reactive<DeleteDialog>({
visible: false,
loading: false,
type: 'single'
})
// 计算属性
const stats = computed<Stats>(() => {
const totalPosts = posts.value.length
const totalViews = posts.value.reduce((sum, post) => sum + post.views, 0)
const totalLikes = posts.value.reduce((sum, post) => sum + post.likes, 0)
const avgViews = totalPosts > 0 ? totalViews / totalPosts : 0
return {
totalPosts,
totalViews,
totalLikes,
avgViews
}
})
// API 配置
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
(config) => {
// 添加 token 等认证信息
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
api.interceptors.response.use(
(response) => response,
(error) => {
ElMessage.error(error.response?.data?.message || '请求失败')
return Promise.reject(error)
}
)
// 方法
const fetchPosts = async () => {
loading.value = true
try {
const params = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
title: filterForm.title,
category: filterForm.category
}
const response = await api.get('/posts/my-posts', { params })
posts.value = response.data.data
pagination.total = response.data.total
} catch (error) {
console.error('获取帖子列表失败:', error)
// 模拟数据,实际开发中删除
posts.value = generateMockData()
pagination.total = 100
} finally {
loading.value = false
}
}
const generateMockData = (): Post[] => {
const categories = ['tech', 'life', 'study', 'qa']
const titles = [
'Vue3 Composition API 最佳实践',
'我的编程学习之路',
'TypeScript 进阶技巧分享',
'如何优化前端性能?',
'生活中的小确幸',
'Element Plus 组件库使用心得'
]
return Array.from({ length: 20 }, (_, index) => ({
id: index + 1,
title: titles[index % titles.length] + ` #${index + 1}`,
category: categories[index % categories.length],
views: Math.floor(Math.random() * 1000) + 100,
likes: Math.floor(Math.random() * 100) + 10,
createdAt: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
status: Math.random() > 0.2 ? 'published' : 'draft'
}))
}
const handleSearch = () => {
pagination.currentPage = 1
fetchPosts()
}
const handleReset = () => {
filterForm.title = ''
filterForm.category = ''
handleSearch()
}
const handleSelectionChange = (selection: Post[]) => {
selectedPosts.value = selection
}
const handleSizeChange = (newSize: number) => {
pagination.pageSize = newSize
fetchPosts()
}
const handleCurrentChange = (newPage: number) => {
pagination.currentPage = newPage
fetchPosts()
}
const refreshData = () => {
fetchPosts()
}
const viewPost = (postId: number) => {
// 跳转到帖子详情页
window.open(`/posts/${postId}`, '_blank')
}
const editPost = (postId: number) => {
// 跳转到编辑页面
window.open(`/posts/${postId}/edit`, '_blank')
}
const confirmDelete = (post: Post) => {
deleteDialog.type = 'single'
deleteDialog.post = post
deleteDialog.visible = true
}
const confirmBatchDelete = () => {
deleteDialog.type = 'batch'
deleteDialog.visible = true
}
const handleDelete = async () => {
deleteDialog.loading = true
try {
if (deleteDialog.type === 'single' && deleteDialog.post) {
await api.delete(`/posts/${deleteDialog.post.id}`)
ElMessage.success('删除成功')
} else if (deleteDialog.type === 'batch') {
const ids = selectedPosts.value.map(post => post.id)
await api.delete('/posts/batch', { data: { ids } })
ElMessage.success(`成功删除 ${ids.length} 个帖子`)
}
deleteDialog.visible = false
selectedPosts.value = []
await fetchPosts()
} catch (error) {
console.error('删除失败:', error)
} finally {
deleteDialog.loading = false
}
}
const handleCloseDialog = () => {
if (!deleteDialog.loading) {
deleteDialog.visible = false
}
}
const getCategoryName = (category: string): string => {
const categoryMap: Record<string, string> = {
tech: '技术讨论',
life: '生活随笔',
study: '学习分享',
qa: '问答求助'
}
return categoryMap[category] || category
}
const getCategoryTagType = (category: string): string => {
const typeMap: Record<string, string> = {
tech: 'primary',
life: 'success',
study: 'warning',
qa: 'info'
}
return typeMap[category] || 'info'
}
const formatDate = (dateString: string): string => {
const date = new Date(dateString)
return date.toLocaleString('zh-CN')
}
// 生命周期
onMounted(() => {
fetchPosts()
})
</script>
<style scoped lang="scss">
.post-management {
padding: 20px;
font-size: 20px; //
//
:deep(.el-icon) {
font-size: 16px !important;
}
//
:deep(.el-table) {
font-size: 20px;
.el-table__header {
font-size: 20px;
font-weight: 600;
}
}
//
:deep(.el-button) {
font-size: 20px;
}
//
:deep(.el-form-item__label) {
font-size: 20px;
}
:deep(.el-input__inner) {
font-size: 20px;
}
}
.box-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
.title {
font-size: 25px; // 18px20px
font-weight: 600;
}
}
}
.filter-section {
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
}
.stats-section {
margin-bottom: 20px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
color: white;
:deep(.el-statistic__content) {
color: white;
}
:deep(.el-statistic__head) {
color: rgba(255, 255, 255, 0.8);
font-size: 24px !important; //
}
//
:deep(.el-statistic__number) {
font-size: 36px !important;
}
}
.stat-number {
font-weight: 600;
color: #409EFF;
font-size: 25px; //
}
.batch-actions {
margin: 40px 0;
display: flex;
justify-content: space-between;
align-items: center;
.batch-buttons {
margin-left: 20px;
}
}
.pagination-wrapper {
margin-top: 40px;
display: flex;
justify-content: center;
}
.delete-content {
display: flex;
align-items: flex-start;
.warning-icon {
font-size: 48px; // 24px48px
color: #E6A23C;
margin-right: 12px;
margin-top: 2px;
}
.delete-message {
flex: 1;
p {
margin: 0 0 8px 0;
line-height: 1.5;
font-size: 15px; //
}
.warning-text {
color: #909399;
font-size: 13px; // 12px13px
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
//
@media (max-width: 768px) {
.post-management {
padding: 10px;
}
.filter-section {
:deep(.el-form--inline) {
.el-form-item {
display: block;
margin-right: 0;
margin-bottom: 12px;
}
}
}
.stats-section {
:deep(.el-row) {
.el-col {
margin-bottom: 12px;
}
}
}
:deep(.el-table) {
.el-table-column--selection,
.el-table__column:last-child {
display: none;
}
}
}
</style>