forked from pizvue73f/unilife
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.
636 lines
15 KiB
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; // 从18px调整为20px
|
|
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; // 从24px调整为48px
|
|
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; // 从12px调整为13px
|
|
}
|
|
}
|
|
}
|
|
|
|
.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> |