添加帖子管理界面,添加个人主页界面

main
parent 2f52a66c74
commit c8063fa38f

@ -19,7 +19,7 @@
"marked-highlight": "^2.2.1",
"vee-validate": "^4.15.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-router": "4",
"yup": "^1.6.1"
},
"devDependencies": {

@ -39,7 +39,7 @@ importers:
specifier: ^3.5.13
version: 3.5.13(typescript@5.7.3)
vue-router:
specifier: ^4.5.0
specifier: '4'
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
yup:
specifier: ^1.6.1

@ -70,6 +70,53 @@ button {
transform: translateY(-2px);
}
/* ======================
(.btn)
====================== */
.btn-circle {
/* 复用现有按钮基础样式 */
@apply btn btn-primary; /* 如果使用Tailwind这类工具 */
/* 新增圆形特性 */
--size: 56px;
width: var(--size);
height: var(--size);
padding: 0;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
/* 定位系统(新增) */
position: fixed;
right: 30px;
bottom: 30px;
margin: 0 !important; /* 覆盖原有margin */
/* 层级管理 */
z-index: 1000;
/* 复用现有悬停动画 */
/* 原有.btn-primary:hover已包含效果 */
}
/* 图标微调(新增) */
.btn-circle .btn-icon {
font-size: 1.8rem;
line-height: 1;
margin-top: -3px; /* 视觉居中补偿 */
}
/* 响应式调整(新增) */
@media (max-width: 768px) {
.btn-circle {
--size: 50px;
right: 15px;
bottom: 15px;
}
}
/*信息展示在card上*/
.card {
background-color: #fff;
@ -130,3 +177,19 @@ button {
background-color: #f9f9f9;
}
}
/* 帖子管理容器样式 */
.post-management {
padding: 30px; /* 比原来的20px更大 */
width: 100%;
max-width: 1800px; /* 比建议的1200px更宽 */
margin: 0 auto;
min-height: 80vh;
box-sizing: border-box;
/* 添加一些额外的样式让内容更突出 */
background-color: var(--light-purple);
border-radius: 20px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

@ -59,14 +59,14 @@ export default defineComponent({
<div class="text">个人课表</div>
</router-link>
</li>
<li :class="{active:route.name === 'AiManager'}" @click="setActive(3)">
<router-link :to="{name:'Manager'}">
<li :class="{active:route.name === 'PostManager'}" @click="setActive(3)">
<router-link :to="{name:'PostManager'}">
<div class="icon">
<div class="imageBox">
<img src="@/assets/images/个人.png">
</div>
</div>
<div class="text">测试样例3</div>
<div class="text">帖子管理</div>
</router-link>
</li>
<li :class="{active:route.name === 'AIManager'}" @click="setActive(4)">

@ -1,12 +1,14 @@
import type { RouteRecordRaw } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecord, RouteRecordRaw } from 'vue-router';
import { createWebHashHistory, createRouter,createWebHistory } from 'vue-router';
import LogPage from '../views/LogPage.vue';
import Personal from '@/components/Personal.vue';
import Manager from '@/views/AcountManager.vue';
import PersonalHome from '@/views/Home.vue';
import ForumHome from '@/views/ForumHome.vue';
import PostManager from '@/views/PostManagement.vue';
import Curriculum from '@/views/Curriculum.vue';
import DirectMessage from '@/views/DirectMessage.vue';
import DirectMessage from '@/views/DirectMessage.vue';
import AIManager from '@/views/AiManager.vue'
const routes: Array<RouteRecordRaw> = [
{
@ -39,13 +41,19 @@ const routes: Array<RouteRecordRaw> = [
component: Manager,
},
{
path: 'ai',
redirect: '/personal',
path:'ai',
name:'AIManager',
component:AIManager,
},
{
path: 'curriculum',
name: 'Curriculum',
component: Curriculum,
},
{
path:'postManager',
name:'PostManager',
component:PostManager,
}
]
},
@ -55,7 +63,7 @@ const routes: Array<RouteRecordRaw> = [
component: DirectMessage,
},
{
path: '/uniLifeHome',
path:'/uniLifeHome',
name: 'ForumHome',
component: ForumHome,
},
@ -64,6 +72,8 @@ const routes: Array<RouteRecordRaw> = [
name: 'PostDetail',
component: () => import('@/views/PostDetailPage.vue'),
},
];
const router = createRouter({

@ -0,0 +1,149 @@
// api/post.ts
import request from './request'
import type { Post, PostListParams, PostListResponse } from '@/views/post'
export const postApi = {
// 获取我的帖子列表
getMyPosts(params: PostListParams): Promise<PostListResponse> {
return request({
url: '/posts/my-posts',
method: 'GET',
params
})
},
// 获取帖子详情
getPostDetail(id: number): Promise<Post> {
return request({
url: `/posts/${id}`,
method: 'GET'
})
},
// 创建帖子
createPost(data: Partial<Post>): Promise<Post> {
return request({
url: '/posts',
method: 'POST',
data
})
},
// 更新帖子
updatePost(id: number, data: Partial<Post>): Promise<Post> {
return request({
url: `/posts/${id}`,
method: 'PUT',
data
})
},
// 删除单个帖子
deletePost(id: number): Promise<void> {
return request({
url: `/posts/${id}`,
method: 'DELETE'
})
},
// 批量删除帖子
batchDeletePosts(ids: number[]): Promise<void> {
return request({
url: '/posts/batch-delete',
method: 'DELETE',
data: { ids }
})
},
// 发布帖子(从草稿状态发布)
publishPost(id: number): Promise<Post> {
return request({
url: `/posts/${id}/publish`,
method: 'POST'
})
},
// 将帖子设为草稿
draftPost(id: number): Promise<Post> {
return request({
url: `/posts/${id}/draft`,
method: 'POST'
})
},
// 获取帖子统计信息
getPostStats(): Promise<{
totalPosts: number
totalViews: number
totalLikes: number
totalComments: number
todayPosts: number
weekPosts: number
monthPosts: number
}> {
return request({
url: '/posts/stats',
method: 'GET'
})
},
// 点赞帖子
likePost(id: number): Promise<{ liked: boolean; likesCount: number }> {
return request({
url: `/posts/${id}/like`,
method: 'POST'
})
},
// 取消点赞
unlikePost(id: number): Promise<{ liked: boolean; likesCount: number }> {
return request({
url: `/posts/${id}/unlike`,
method: 'POST'
})
},
// 增加浏览量
increaseViews(id: number): Promise<{ views: number }> {
return request({
url: `/posts/${id}/view`,
method: 'POST'
})
},
// 搜索帖子
searchPosts(params: {
keyword: string
category?: string
page?: number
pageSize?: number
}): Promise<PostListResponse> {
return request({
url: '/posts/search',
method: 'GET',
params
})
},
// 获取热门帖子
getHotPosts(params: {
period?: 'day' | 'week' | 'month'
limit?: number
} = {}): Promise<Post[]> {
return request({
url: '/posts/hot',
method: 'GET',
params
})
},
// 获取推荐帖子
getRecommendedPosts(limit = 10): Promise<Post[]> {
return request({
url: '/posts/recommended',
method: 'GET',
params: { limit }
})
}
}
//

@ -1,21 +1,299 @@
<script set lang="ts">
import { defineComponent } from 'vue';
import { computed } from 'vue';
interface UserInfo {
avatar: string;
nickname: string;
gender: 'male' | 'female';
followers: number;
following: number;
postsCount: number;
}
interface Post {
id: number;
title: string;
category: string;
views: number;
}
interface Course {
id: number;
time: string;
name: string;
location: string;
}
interface Assignment {
id: number;
title: string;
course: string;
dueDate: string;
}
export default defineComponent({
name:'Home',
setup() {
//
const userInfo: UserInfo = {
avatar: '@/assets/images/默认头像.jpg',
nickname: '学习小能手',
gender: 'male',
followers: 128,
following: 56,
postsCount: 32
};
const posts: Post[] = [
{ id: 1, title: '高数复习笔记', category: '学习资料', views: 356 },
{ id: 2, title: '英语作文模板分享', category: '学习交流', views: 234 }
];
const schedule: Course[] = [
{ id: 1, time: '周一 8:00', name: '高等数学', location: '教201' },
{ id: 2, time: '周三 10:00', name: '大学英语', location: '教305' },
{ id: 3, time: '周四 10:00', name: '大学物理', location: '教305' }
];
const assignments: Assignment[] = [
{ id: 1, title: '线性代数作业', course: '数学', dueDate: '2024-03-20' },
{ id: 2, title: '实验报告', course: '物理', dueDate: '2024-03-22' }
];
function getTodayWeekday(): string {
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const today = new Date();
return days[today.getDay()];
}
const todayWeekday = getTodayWeekday();
const todaySchedule = computed(() => {
return schedule.filter(course => course.time.startsWith(todayWeekday));
});
return {
userInfo,
posts,
schedule,
assignments,
todaySchedule
};
}
});
</script>
<template>
<h1>个人主页</h1>
<p>还没做
期望完成
查看已发帖
查看已发帖的情况
编辑帖子
</p>
<div class="profile-container">
<!-- 用户信息头部 -->
<div class="user-header">
<img :src="userInfo.avatar" class="user-avatar" alt="头像">
<div class="user-info">
<h2 class="username">
{{ userInfo.nickname }}
<span class="gender-icon" :class="userInfo.gender"></span>
</h2>
<div class="stats">
<div class="stat-item">
<span class="stat-number">{{ userInfo.followers }}</span>
<span class="stat-label">粉丝</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ userInfo.following }}</span>
<span class="stat-label">关注</span>
</div>
<div class="stat-item">
<span class="stat-number">{{ userInfo.postsCount }}</span>
<span class="stat-label">帖子</span>
</div>
</div>
</div>
</div>
<!-- 内容区域 -->
<div class="content-wrapper">
<!-- 帖子区域 -->
<div class="posts-section scrollable">
<h3 class="section-title">发布的帖子</h3>
<div v-for="post in posts" :key="post.id" class="post-item">
<h4 class="post-title">{{ post.title }}</h4>
<div class="post-meta">
<span class="post-category">{{ post.category }}</span>
<span class="post-views">浏览: {{ post.views }}</span>
</div>
</div>
</div>
<!-- 右侧侧边栏 -->
<div class="sidebar">
<!-- 课表区域 -->
<div class="schedule-section scrollable">
<h3 class="section-title">本周课表</h3>
<div v-for="course in todaySchedule" :key="course.id" class="schedule-item">
<div class="course-time">{{ course.time }}</div>
<div class="course-info">
<div class="course-name">{{ course.name }}</div>
<div class="course-location">{{ course.location }}</div>
</div>
</div>
</div>
<!-- 学习计划区域 -->
<div class="assignments-section scrollable">
<h3 class="section-title">待做作业</h3>
<div v-for="assignment in assignments" :key="assignment.id" class="assignment-item">
<div class="assignment-title">{{ assignment.title }}</div>
<div class="assignment-course">{{ assignment.course }}</div>
<div class="assignment-due">截止: {{ assignment.dueDate }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.profile-container {
width: 90vw; /* 宽度自适应,保证在大屏幕拉伸 */
max-width: 2000px; /* 最大宽度限制宽屏幕下显示 */
margin: 20px 0;
padding: 20px;
background: #f6f6f6; /* 白底 */
border-radius: 8px; /* 圆角,增强视觉 */
box-shadow: 0 0 10px rgba(0,0,0,0.1);
font-size: 20px;
}
.user-header {
display: flex;
align-items: center;
margin-bottom: 20px;
margin-top:40px;
}
.user-avatar {
width: 100px;
height: 60px;
border-radius: 50%;
margin-right: 30px;
}
.user-info h2 {
margin: 0 0 8px 0;
}
.gender-icon {
display: inline-block;
width: 20px;
height: 20px;
margin-left: 8px;
background-size: contain;
}
/*
.gender-icon.male {
background-image: url('male-icon.svg');
}
.gender-icon.female {
background-image: url('female-icon.svg');
}*/
.stats {
display: flex;
gap: 30px;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 30px;
font-weight: bold;
display: block;
}
.stat-label {
color: #666;
}
.content-wrapper {
display: grid;
grid-template-columns: 40% 25% 25%;
gap: 4%;
height: 800px;
width: 120%; /* 宽度撑满父容器 */
}
.scrollable {
overflow-y: auto;
height: 100%;
padding-right: 10px;
}
/* 帖子区域样式 */
.posts-section {
background: #f0eaf2;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.post-item {
padding: 12px;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
}
.post-title {
margin: 0 0 6px 0;
font-size: 24px;
}
.post-meta {
display: flex;
justify-content: space-between;
color: #666;
font-size: 18px;
}
/* 侧边栏公共样式 */
.sidebar {
display: flex;
flex-direction: column;
gap: 60px;
}
/* 课表样式 */
.schedule-item {
display: flex;
padding: 20px;
background: #f0eaf2;
margin-bottom: 8px;
border-radius: 6px;
}
.course-time {
width: 70px;
color: #666;
}
.course-name {
font-weight: 500;
}
/* 作业样式 */
.assignment-item {
padding: 10px;
background: #f0eaf2;
margin-bottom: 8px;
border-radius: 6px;
}
.assignment-due {
color: #e67e22;
font-size: 18px;
}
.section-title {
margin: 0 0 15px 0;
padding-bottom: 8px;
border-bottom: 2px solid #eee;
font-size: 30px;
}
</style>

@ -0,0 +1,636 @@
<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="/create-post"
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>

@ -0,0 +1,111 @@
// types/post.ts
export interface Post {
id: number
title: string
content?: string
category: string
views: number
likes: number
comments?: number
createdAt: string
updatedAt?: string
status: 'published' | 'draft'
author?: {
id: number
username: string
avatar?: string
}
}
export interface PostListParams {
page: number
pageSize: number
title?: string
category?: string
status?: string
sortBy?: 'createdAt' | 'views' | 'likes'
sortOrder?: 'asc' | 'desc'
}
export interface PostListResponse {
data: Post[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface FilterForm {
title: string
category: string
status?: string
}
export interface Pagination {
currentPage: number
pageSize: number
total: number
}
export interface Stats {
totalPosts: number
totalViews: number
totalLikes: number
totalComments: number
avgViews: number
avgLikes: number
}
export interface DeleteDialog {
visible: boolean
loading: boolean
type: 'single' | 'batch'
post?: Post
}
// types/api.ts
export interface ApiResponse<T = any> {
code: number
message: string
data: T
timestamp: number
}
export interface ApiError {
code: number
message: string
details?: any
}
// types/category.ts
export interface Category {
id: string
name: string
description?: string
color?: string
icon?: string
}
export const CATEGORIES: Category[] = [
{ id: 'tech', name: '技术讨论', color: 'primary', icon: 'Monitor' },
{ id: 'life', name: '生活随笔', color: 'success', icon: 'Coffee' },
{ id: 'study', name: '学习分享', color: 'warning', icon: 'Reading' },
{ id: 'qa', name: '问答求助', color: 'info', icon: 'QuestionFilled' }
]
// types/user.ts
export interface User {
id: number
username: string
email: string
avatar?: string
role: 'admin' | 'user'
createdAt: string
profile?: {
nickname?: string
bio?: string
location?: string
website?: string
}
}

@ -0,0 +1,138 @@
// hooks/usePost.ts
import { ref, reactive, computed } from 'vue'
import type { Post, PostListParams, FilterForm, Pagination, Stats } from '@/views/post'
import { postApi } from '@/utils/post'
import { ElMessage } from 'element-plus'
export function usePostManagement() {
const loading = ref(false)
const posts = ref<Post[]>([])
const selectedPosts = ref<Post[]>([])
const filterForm = reactive<FilterForm>({
title: '',
category: '',
status: ''
})
const pagination = reactive<Pagination>({
currentPage: 1,
pageSize: 20,
total: 0
})
// 计算统计数据
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 totalComments = posts.value.reduce((sum, post) => sum + (post.comments || 0), 0)
const avgViews = totalPosts > 0 ? totalViews / totalPosts : 0
const avgLikes = totalPosts > 0 ? totalLikes / totalPosts : 0
return {
totalPosts,
totalViews,
totalLikes,
totalComments,
avgViews,
avgLikes
}
})
// 获取帖子列表
const fetchPosts = async () => {
loading.value = true
try {
const params: PostListParams = {
page: pagination.currentPage,
pageSize: pagination.pageSize,
title: filterForm.title || undefined,
category: filterForm.category || undefined,
status: filterForm.status || undefined
}
const response = await postApi.getMyPosts(params)
posts.value = response.data
pagination.total = response.total
} catch (error) {
ElMessage.error('获取帖子列表失败')
console.error('Fetch posts error:', error)
} finally {
loading.value = false
}
}
// 删除帖子
const deletePost = async (postId: number): Promise<boolean> => {
try {
await postApi.deletePost(postId)
ElMessage.success('删除成功')
return true
} catch (error) {
ElMessage.error('删除失败')
console.error('Delete post error:', error)
return false
}
}
// 批量删除
const batchDeletePosts = async (postIds: number[]): Promise<boolean> => {
try {
await postApi.batchDeletePosts(postIds)
ElMessage.success(`成功删除 ${postIds.length} 个帖子`)
return true
} catch (error) {
ElMessage.error('批量删除失败')
console.error('Batch delete posts error:', error)
return false
}
}
// 搜索
const handleSearch = () => {
pagination.currentPage = 1
fetchPosts()
}
// 重置筛选
const handleReset = () => {
Object.keys(filterForm).forEach(key => {
filterForm[key as keyof FilterForm] = ''
})
handleSearch()
}
// 选择变化
const handleSelectionChange = (selection: Post[]) => {
selectedPosts.value = selection
}
// 分页变化
const handleSizeChange = (newSize: number) => {
pagination.pageSize = newSize
fetchPosts()
}
const handleCurrentChange = (newPage: number) => {
pagination.currentPage = newPage
fetchPosts()
}
return {
loading,
posts,
selectedPosts,
filterForm,
pagination,
stats,
fetchPosts,
deletePost,
batchDeletePosts,
handleSearch,
handleReset,
handleSelectionChange,
handleSizeChange,
handleCurrentChange
}
}
Loading…
Cancel
Save