main
parent
5fc904198b
commit
0efc812afb
@ -0,0 +1,81 @@
|
|||||||
|
package com.luojia_channel.modules.admin.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员评论DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdminCommentDTO {
|
||||||
|
/**
|
||||||
|
* 评论ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论内容
|
||||||
|
*/
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户头像
|
||||||
|
*/
|
||||||
|
private String userAvatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帖子ID
|
||||||
|
*/
|
||||||
|
private Long postId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 帖子标题
|
||||||
|
*/
|
||||||
|
private String postTitle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 父评论ID
|
||||||
|
*/
|
||||||
|
private Long parentCommentId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 顶级评论ID
|
||||||
|
*/
|
||||||
|
private Long topId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复用户名
|
||||||
|
*/
|
||||||
|
private String replyUsername;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 点赞数
|
||||||
|
*/
|
||||||
|
private Integer likeCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回复数
|
||||||
|
*/
|
||||||
|
private Integer replyCount;
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.luojia_channel.modules.admin.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员用户DTO - 仅包含管理员可见的非隐私信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdminUserDTO {
|
||||||
|
/**
|
||||||
|
* 用户ID
|
||||||
|
*/
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名
|
||||||
|
*/
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 头像URL
|
||||||
|
*/
|
||||||
|
private String avatar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户状态(1正常 2冻结)
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户角色(1普通用户 2管理员 3超级管理员)
|
||||||
|
*/
|
||||||
|
private Integer role;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户积分
|
||||||
|
*/
|
||||||
|
private Integer integral;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发帖数量
|
||||||
|
*/
|
||||||
|
private Integer postCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 评论数量
|
||||||
|
*/
|
||||||
|
private Integer commentCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 粉丝数量
|
||||||
|
*/
|
||||||
|
private Integer followerCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关注数量
|
||||||
|
*/
|
||||||
|
private Integer followingCount;
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
package com.luojia_channel.modules.admin.service;
|
||||||
|
|
||||||
|
import com.luojia_channel.common.domain.page.PageResponse;
|
||||||
|
import com.luojia_channel.modules.admin.dto.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员服务接口
|
||||||
|
*/
|
||||||
|
public interface AdminService {
|
||||||
|
/**
|
||||||
|
* 获取系统总览数据
|
||||||
|
* @return 总览数据
|
||||||
|
*/
|
||||||
|
AdminOverviewDTO getOverview();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表
|
||||||
|
* @param page 页码
|
||||||
|
* @param size 每页数量
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @param role 角色
|
||||||
|
* @param status 状态
|
||||||
|
* @return 用户列表分页结果
|
||||||
|
*/
|
||||||
|
PageResponse<AdminUserDTO> getUserList(Integer page, Integer size, String keyword, Integer role, Integer status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户状态
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param status 状态
|
||||||
|
*/
|
||||||
|
void changeUserStatus(Long id, Integer status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改用户角色
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param role 角色
|
||||||
|
*/
|
||||||
|
void changeUserRole(Long id, Integer role);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取帖子列表
|
||||||
|
* @param page 页码
|
||||||
|
* @param size 每页数量
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @param categoryId 分类ID
|
||||||
|
* @param status 状态
|
||||||
|
* @return 帖子列表分页结果
|
||||||
|
*/
|
||||||
|
PageResponse<AdminPostDTO> getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除帖子
|
||||||
|
* @param id 帖子ID
|
||||||
|
*/
|
||||||
|
void deletePost(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 修改帖子状态
|
||||||
|
* @param id 帖子ID
|
||||||
|
* @param action 操作类型
|
||||||
|
*/
|
||||||
|
void changePostStatus(Long id, Integer action);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取评论列表
|
||||||
|
* @param page 页码
|
||||||
|
* @param size 每页数量
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @param postId 帖子ID
|
||||||
|
* @return 评论列表分页结果
|
||||||
|
*/
|
||||||
|
PageResponse<AdminCommentDTO> getCommentList(Integer page, Integer size, String keyword, Long postId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除评论
|
||||||
|
* @param id 评论ID
|
||||||
|
*/
|
||||||
|
void deleteComment(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户统计数据
|
||||||
|
* @param type 统计类型
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return 统计数据
|
||||||
|
*/
|
||||||
|
Map<String, Object> getUserStatistics(String type, String startDate, String endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取帖子统计数据
|
||||||
|
* @param type 统计类型
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return 统计数据
|
||||||
|
*/
|
||||||
|
Map<String, Object> getPostStatistics(String type, String startDate, String endDate);
|
||||||
|
}
|
@ -1,36 +1,36 @@
|
|||||||
##本地开发环境
|
##本地开发环境
|
||||||
# lj:
|
lj:
|
||||||
# db:
|
db:
|
||||||
# host: localhost
|
host: localhost
|
||||||
# password: lzt&264610
|
password: 123456
|
||||||
# redis:
|
redis:
|
||||||
# host: localhost
|
host: localhost
|
||||||
# port: 6379
|
port: 6379
|
||||||
# password: 123456
|
password: 123456
|
||||||
# rabbitmq:
|
rabbitmq:
|
||||||
# host: localhost
|
host: localhost
|
||||||
# port: 5672
|
port: 5672
|
||||||
# username: guest
|
username: root
|
||||||
# password: guest
|
password: 123456
|
||||||
# minio:
|
minio:
|
||||||
# endpoint: http://localhost:9000
|
endpoint: http://localhost:9000
|
||||||
# accessKey: minioadmin
|
accessKey: minioadmin
|
||||||
# secretKey: minioadmin
|
secretKey: minioadmin
|
||||||
|
|
||||||
lj:
|
#lj:
|
||||||
db:
|
# db:
|
||||||
host: 192.168.125.128
|
# host: 192.168.125.128
|
||||||
password: MySQL@5678
|
# password: MySQL@5678
|
||||||
redis:
|
# redis:
|
||||||
host: 192.168.125.128
|
# host: 192.168.125.128
|
||||||
port: 6379
|
# port: 6379
|
||||||
password: Redis@9012
|
# password: Redis@9012
|
||||||
rabbitmq:
|
# rabbitmq:
|
||||||
host: 192.168.125.128
|
# host: 192.168.125.128
|
||||||
port: 5672
|
# port: 5672
|
||||||
username: rabbit_admin
|
# username: rabbit_admin
|
||||||
password: Rabbit@3456
|
# password: Rabbit@3456
|
||||||
minio:
|
# minio:
|
||||||
endpoint: http://192.168.125.128:9000
|
# endpoint: http://192.168.125.128:9000
|
||||||
accessKey: minio_admin
|
# accessKey: minio_admin
|
||||||
secretKey: Minio@1234
|
# secretKey: Minio@1234
|
||||||
|
@ -0,0 +1,285 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-comments">
|
||||||
|
<!-- 搜索和筛选区域 -->
|
||||||
|
<div class="filter-area">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="filter-form">
|
||||||
|
<el-form-item label="内容关键词">
|
||||||
|
<el-input v-model="searchForm.keyword" placeholder="评论内容" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="帖子ID">
|
||||||
|
<el-input v-model="searchForm.postId" placeholder="帖子ID" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon> 搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetForm">
|
||||||
|
<el-icon><RefreshLeft /></el-icon> 重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<div class="table-area">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="commentList"
|
||||||
|
style="width: 100%"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||||
|
|
||||||
|
<el-table-column label="评论内容" min-width="300">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="comment-content">
|
||||||
|
<el-tooltip :content="row.content" placement="top" :show-after="1000">
|
||||||
|
<p>{{ row.content }}</p>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<div v-if="row.replyUsername" class="reply-info">
|
||||||
|
<span>回复 @{{ row.replyUsername }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="评论者" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="30" :src="row.userAvatar" />
|
||||||
|
<span class="username">{{ row.username }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="所属帖子" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<router-link :to="`/post/${row.postId}`" class="post-link" target="_blank">
|
||||||
|
{{ row.postTitle }}
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" label="评论时间" width="180" sortable />
|
||||||
|
|
||||||
|
<el-table-column label="点赞数" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.likeCount }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="120" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-popconfirm
|
||||||
|
title="确定要删除该评论吗?此操作不可恢复!"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { Search, RefreshLeft } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
postId: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 评论列表
|
||||||
|
const commentList = ref([]);
|
||||||
|
|
||||||
|
// 加载评论列表
|
||||||
|
const loadComments = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response = await axios.get('/api/admin/comments', {
|
||||||
|
params: {
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
keyword: searchForm.keyword || null,
|
||||||
|
postId: searchForm.postId ? Number(searchForm.postId) : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const data = response.data.data;
|
||||||
|
commentList.value = data.records;
|
||||||
|
pagination.total = data.total;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '加载评论列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载评论列表失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1;
|
||||||
|
loadComments();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
searchForm.keyword = '';
|
||||||
|
searchForm.postId = '';
|
||||||
|
handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size;
|
||||||
|
loadComments();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page;
|
||||||
|
loadComments();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除评论
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/admin/comments/${row.id}`);
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('评论已删除');
|
||||||
|
loadComments();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除评论失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadComments();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-comments {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content p {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 60px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reply-info {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link {
|
||||||
|
color: #409EFF;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格响应式调整 */
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.el-table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-dashboard">
|
||||||
|
<!-- 使用全局Header组件保持一致的顶部导航 -->
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<div class="admin-container">
|
||||||
|
<!-- 左侧菜单 -->
|
||||||
|
<div class="admin-sidebar">
|
||||||
|
<h2 class="sidebar-title">管理中心</h2>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
class="admin-menu"
|
||||||
|
router
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/admin">
|
||||||
|
<el-icon><DataLine /></el-icon>
|
||||||
|
<span>系统概览</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/admin/users">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/admin/posts">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>帖子管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/admin/comments">
|
||||||
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
|
<span>评论管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-menu-item index="/admin/categories">
|
||||||
|
<el-icon><Collection /></el-icon>
|
||||||
|
<span>分类管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧内容区域 -->
|
||||||
|
<div class="admin-content">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
import {
|
||||||
|
DataLine,
|
||||||
|
User,
|
||||||
|
Document,
|
||||||
|
ChatDotRound,
|
||||||
|
Collection
|
||||||
|
} from '@element-plus/icons-vue';
|
||||||
|
import Header from '@/components/Header.vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
|
||||||
|
// 当前激活的菜单项
|
||||||
|
const activeMenu = ref('');
|
||||||
|
|
||||||
|
// 根据当前路由设置激活菜单
|
||||||
|
const setActiveMenu = () => {
|
||||||
|
activeMenu.value = route.path;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理菜单选择
|
||||||
|
const handleSelect = (key) => {
|
||||||
|
activeMenu.value = key;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查用户权限
|
||||||
|
const checkPermission = () => {
|
||||||
|
console.log('正在检查管理员权限:', userStore.isLoggedIn, userStore.userInfo.role);
|
||||||
|
|
||||||
|
// 检查用户是否登录以及是否有管理员权限
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.error('请先登录');
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userStore.userInfo.role < 2) {
|
||||||
|
ElMessage.error('您没有管理员权限');
|
||||||
|
router.push('/');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('权限检查通过,用户角色:', userStore.userInfo.role);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听路由变化
|
||||||
|
watch(() => route.path, () => {
|
||||||
|
setActiveMenu();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 组件挂载时
|
||||||
|
onMounted(() => {
|
||||||
|
setActiveMenu();
|
||||||
|
checkPermission();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-container {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 50px; /* 为Header预留空间 */
|
||||||
|
min-height: calc(100vh - 50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
background-color: #fff;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
position: fixed;
|
||||||
|
top: 50px; /* Header高度 */
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 100;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #409eff;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
margin-left: 240px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图标样式 */
|
||||||
|
.el-menu-item .el-icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.admin-sidebar {
|
||||||
|
width: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-content {
|
||||||
|
margin-left: 64px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,408 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-posts">
|
||||||
|
<!-- 搜索和筛选区域 -->
|
||||||
|
<div class="filter-area">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="filter-form">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="searchForm.keyword" placeholder="标题/内容" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="分类">
|
||||||
|
<el-select v-model="searchForm.categoryId" placeholder="全部分类" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
:label="category.name"
|
||||||
|
:value="category.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="全部状态" clearable>
|
||||||
|
<el-option label="正常" :value="0" />
|
||||||
|
<el-option label="置顶" :value="1" />
|
||||||
|
<el-option label="隐藏" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon> 搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetForm">
|
||||||
|
<el-icon><RefreshLeft /></el-icon> 重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<div class="table-area">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="postList"
|
||||||
|
style="width: 100%"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||||
|
|
||||||
|
<el-table-column prop="title" label="帖子标题" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="row.title" placement="top" :show-after="1000">
|
||||||
|
<router-link :to="`/post/${row.id}`" class="post-title" target="_blank">
|
||||||
|
{{ row.title }}
|
||||||
|
</router-link>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="作者" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="30" :src="row.userAvatar" />
|
||||||
|
<span class="username">{{ row.username }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="categoryName" label="分类" width="120" />
|
||||||
|
|
||||||
|
<el-table-column label="统计" width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="post-stats">
|
||||||
|
<span title="浏览"><el-icon><View /></el-icon> {{ row.viewCount }}</span>
|
||||||
|
<span title="点赞"><el-icon><Star /></el-icon> {{ row.likeCount }}</span>
|
||||||
|
<span title="评论"><el-icon><ChatDotRound /></el-icon> {{ row.commentCount }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" label="发布时间" width="180" sortable />
|
||||||
|
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.status === 0" type="success">正常</el-tag>
|
||||||
|
<el-tag v-else-if="row.status === 1" type="warning">置顶</el-tag>
|
||||||
|
<el-tag v-else-if="row.status === 2" type="info">隐藏</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="250" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-if="row.status !== 1" size="small" type="primary" @click="handleSetTop(row)">
|
||||||
|
置顶
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="row.status === 1" size="small" type="warning" @click="handleCancelTop(row)">
|
||||||
|
取消置顶
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button v-if="row.status !== 2" size="small" type="info" @click="handleHide(row)">
|
||||||
|
隐藏
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="row.status === 2" size="small" type="success" @click="handleShow(row)">
|
||||||
|
显示
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-popconfirm
|
||||||
|
title="确定要删除该帖子吗?此操作不可恢复!"
|
||||||
|
@confirm="handleDelete(row)"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button size="small" type="danger">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import { Search, RefreshLeft, View, Star, ChatDotRound } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
categoryId: null,
|
||||||
|
status: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 帖子列表
|
||||||
|
const postList = ref([]);
|
||||||
|
|
||||||
|
// 分类列表
|
||||||
|
const categories = ref([]);
|
||||||
|
|
||||||
|
// 加载分类数据
|
||||||
|
const loadCategories = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/categories');
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
categories.value = response.data.data || [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载分类失败:', error);
|
||||||
|
ElMessage.error('加载分类失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载帖子列表
|
||||||
|
const loadPosts = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response = await axios.get('/api/admin/posts', {
|
||||||
|
params: {
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
keyword: searchForm.keyword || null,
|
||||||
|
categoryId: searchForm.categoryId || null,
|
||||||
|
status: searchForm.status !== null ? searchForm.status : null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
const data = response.data.data;
|
||||||
|
postList.value = data.records;
|
||||||
|
pagination.total = data.total;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '加载帖子列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载帖子列表失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1;
|
||||||
|
loadPosts();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
searchForm.keyword = '';
|
||||||
|
searchForm.categoryId = null;
|
||||||
|
searchForm.status = null;
|
||||||
|
handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size;
|
||||||
|
loadPosts();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page;
|
||||||
|
loadPosts();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 置顶帖子
|
||||||
|
const handleSetTop = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/admin/posts/${row.id}/status`, null, {
|
||||||
|
params: { action: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('帖子已置顶');
|
||||||
|
loadPosts();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('置顶帖子失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消置顶
|
||||||
|
const handleCancelTop = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/admin/posts/${row.id}/status`, null, {
|
||||||
|
params: { action: 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('已取消置顶');
|
||||||
|
loadPosts();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('取消置顶失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 隐藏帖子
|
||||||
|
const handleHide = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/admin/posts/${row.id}/status`, null, {
|
||||||
|
params: { action: 3 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('帖子已隐藏');
|
||||||
|
loadPosts();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('隐藏帖子失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示帖子
|
||||||
|
const handleShow = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/admin/posts/${row.id}/status`, null, {
|
||||||
|
params: { action: 4 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('帖子已显示');
|
||||||
|
loadPosts();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('显示帖子失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除帖子
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/admin/posts/${row.id}`);
|
||||||
|
|
||||||
|
if (response.data.code === 0) {
|
||||||
|
ElMessage.success('帖子已删除');
|
||||||
|
loadPosts();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除帖子失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadCategories();
|
||||||
|
loadPosts();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-posts {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
color: #333;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title:hover {
|
||||||
|
color: #54ac52;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-stats span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-stats .el-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格响应式调整 */
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.el-table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -0,0 +1,373 @@
|
|||||||
|
<template>
|
||||||
|
<div class="admin-users">
|
||||||
|
<!-- 搜索和筛选区域 -->
|
||||||
|
<div class="filter-area">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="filter-form">
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input v-model="searchForm.keyword" placeholder="用户名" clearable @keyup.enter="handleSearch" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="角色">
|
||||||
|
<el-select v-model="searchForm.role" placeholder="全部角色" clearable>
|
||||||
|
<el-option label="普通用户" :value="1" />
|
||||||
|
<el-option label="管理员" :value="2" />
|
||||||
|
<el-option label="超级管理员" :value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="全部状态" clearable>
|
||||||
|
<el-option label="正常" :value="1" />
|
||||||
|
<el-option label="冻结" :value="2" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">
|
||||||
|
<el-icon><Search /></el-icon> 搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetForm">
|
||||||
|
<el-icon><RefreshLeft /></el-icon> 重置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<div class="table-area">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="userList"
|
||||||
|
style="width: 100%"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
row-key="id"
|
||||||
|
>
|
||||||
|
<el-table-column type="index" width="60" align="center" label="序号" />
|
||||||
|
|
||||||
|
<el-table-column label="用户信息" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-info">
|
||||||
|
<el-avatar :size="40" :src="row.avatar" />
|
||||||
|
<div class="user-meta">
|
||||||
|
<div class="username">{{ row.username }}</div>
|
||||||
|
<div class="user-id">ID: {{ row.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="统计数据" width="240">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="user-stats">
|
||||||
|
<span title="发帖"><el-icon><Document /></el-icon> {{ row.postCount || 0 }}</span>
|
||||||
|
<span title="评论"><el-icon><ChatDotRound /></el-icon> {{ row.commentCount || 0 }}</span>
|
||||||
|
<span title="粉丝"><el-icon><User /></el-icon> {{ row.followerCount || 0 }}</span>
|
||||||
|
<span title="积分"><el-icon><Medal /></el-icon> {{ row.integral || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="createTime" label="注册时间" width="180" sortable />
|
||||||
|
|
||||||
|
<el-table-column label="角色" width="120" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.role === 1" type="info">普通用户</el-tag>
|
||||||
|
<el-tag v-else-if="row.role === 2" type="warning">管理员</el-tag>
|
||||||
|
<el-tag v-else-if="row.role === 3" type="danger">超级管理员</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="状态" width="100" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.status === 1" type="success">正常</el-tag>
|
||||||
|
<el-tag v-else-if="row.status === 2" type="danger">冻结</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column label="操作" width="250" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button v-if="row.status === 1" size="small" type="danger" @click="handleFreezeUser(row)">
|
||||||
|
冻结账号
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="row.status === 2" size="small" type="success" @click="handleUnfreezeUser(row)">
|
||||||
|
解冻账号
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-dropdown trigger="click" @command="(cmd) => handleRoleChange(cmd, row)">
|
||||||
|
<el-button size="small" type="primary">
|
||||||
|
设置角色 <el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item :command="1" :disabled="row.role === 1">普通用户</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="2" :disabled="row.role === 2">管理员</el-dropdown-item>
|
||||||
|
<el-dropdown-item :command="3" :disabled="row.role === 3 || currentUserRole !== 3">超级管理员</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue';
|
||||||
|
import { Search, RefreshLeft, Document, ChatDotRound, User, Medal, ArrowDown } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { useUserStore } from '@/stores/user.js';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const currentUserRole = computed(() => userStore.userInfo?.role || 1);
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
keyword: '',
|
||||||
|
role: null,
|
||||||
|
status: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数据加载状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 用户列表
|
||||||
|
const userList = ref([]);
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
const loadUsers = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response = await axios.get('/api/admin/users', {
|
||||||
|
params: {
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size,
|
||||||
|
keyword: searchForm.keyword || null,
|
||||||
|
role: searchForm.role || null,
|
||||||
|
status: searchForm.status || null
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
const data = response.data.data;
|
||||||
|
userList.value = data.records;
|
||||||
|
pagination.total = data.total;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '加载用户列表失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.page = 1;
|
||||||
|
loadUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
searchForm.keyword = '';
|
||||||
|
searchForm.role = null;
|
||||||
|
searchForm.status = null;
|
||||||
|
handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size;
|
||||||
|
loadUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page;
|
||||||
|
loadUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 冻结用户
|
||||||
|
const handleFreezeUser = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'确定要冻结该用户吗?冻结后用户将无法登录系统',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await axios.put(`/api/admin/users/${row.id}/status`, null, {
|
||||||
|
params: { status: 2 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('用户已冻结');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('冻结用户失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解冻用户
|
||||||
|
const handleUnfreezeUser = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/admin/users/${row.id}/status`, null, {
|
||||||
|
params: { status: 1 }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('用户已解冻');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解冻用户失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改用户角色
|
||||||
|
const handleRoleChange = async (role, row) => {
|
||||||
|
if (role === row.role) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要将该用户角色修改为${role === 1 ? '普通用户' : role === 2 ? '管理员' : '超级管理员'}吗?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await axios.put(`/api/admin/users/${row.id}/role`, null, {
|
||||||
|
params: { role }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.code === 200) {
|
||||||
|
ElMessage.success('用户角色已修改');
|
||||||
|
loadUsers();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.data.msg || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('修改用户角色失败:', error);
|
||||||
|
ElMessage.error('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadUsers();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-users {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-form {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats .el-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格响应式调整 */
|
||||||
|
@media screen and (max-width: 1200px) {
|
||||||
|
.el-table {
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in new issue