管理端前后端实现,但还没测试;提供/user/info/getuserinfo接口获取用户信息(靠userid),数据dto对应在user/dto/userinfoDTO里面,有需要自己更改

main
哆哆咯哆哆咯 2 months ago
parent 5fc904198b
commit 0efc812afb

@ -18,6 +18,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
"/user/register", "/user/register",
"/user/captcha", "/user/captcha",
"/user/verify-captcha", "/user/verify-captcha",
"/user/info/getuserinfo",
"/post/list", "/post/list",
"/post/detail", "/post/detail",
"/comment/list", "/comment/list",

@ -56,6 +56,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- OpenAPI 3.0 (Swagger) 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

@ -0,0 +1,127 @@
package com.luojia_channel.modules.admin.controller;
import com.luojia_channel.common.domain.Result;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.admin.dto.*;
import com.luojia_channel.modules.admin.service.AdminService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
@Tag(name = "管理员模块", description = "管理员相关接口")
public class AdminController {
private final AdminService adminService;
@GetMapping("/overview")
@Operation(summary = "获取系统总览数据", description = "获取系统各项统计数据,包括用户总数、帖子总数、评论总数、活跃用户等")
public Result<AdminOverviewDTO> getOverview() {
return Result.success(adminService.getOverview());
}
@GetMapping("/users")
@Operation(summary = "获取用户列表", description = "分页获取用户列表,支持按用户名、手机号、邮箱等搜索筛选")
public Result<PageResponse<AdminUserDTO>> getUsers(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "用户角色") @RequestParam(required = false) Integer role,
@Parameter(description = "用户状态") @RequestParam(required = false) Integer status
) {
return Result.success(adminService.getUserList(page, size, keyword, role, status));
}
@PutMapping("/users/{id}/status")
@Operation(summary = "更改用户状态", description = "冻结或解冻用户")
public Result<Void> changeUserStatus(
@Parameter(description = "用户ID") @PathVariable Long id,
@Parameter(description = "状态1正常 2冻结") @RequestParam Integer status
) {
adminService.changeUserStatus(id, status);
return Result.success();
}
@PutMapping("/users/{id}/role")
@Operation(summary = "更改用户角色", description = "升级或降级用户角色")
public Result<Void> changeUserRole(
@Parameter(description = "用户ID") @PathVariable Long id,
@Parameter(description = "角色1普通用户 2管理员 3超级管理员") @RequestParam Integer role
) {
adminService.changeUserRole(id, role);
return Result.success();
}
@GetMapping("/posts")
@Operation(summary = "获取帖子列表", description = "分页获取帖子列表,支持按标题、内容、分类等搜索筛选")
public Result<PageResponse<AdminPostDTO>> getPosts(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "分类ID") @RequestParam(required = false) Long categoryId,
@Parameter(description = "状态") @RequestParam(required = false) Integer status
) {
return Result.success(adminService.getPostList(page, size, keyword, categoryId, status));
}
@DeleteMapping("/posts/{id}")
@Operation(summary = "删除帖子", description = "管理员删除帖子")
public Result<Void> deletePost(@Parameter(description = "帖子ID") @PathVariable Long id) {
adminService.deletePost(id);
return Result.success();
}
@PutMapping("/posts/{id}/status")
@Operation(summary = "更改帖子状态", description = "置顶、取消置顶、隐藏或显示帖子")
public Result<Void> changePostStatus(
@Parameter(description = "帖子ID") @PathVariable Long id,
@Parameter(description = "操作类型(1置顶 2取消置顶 3隐藏 4显示)") @RequestParam Integer action
) {
adminService.changePostStatus(id, action);
return Result.success();
}
@GetMapping("/comments")
@Operation(summary = "获取评论列表", description = "分页获取评论列表支持按内容、帖子ID等搜索筛选")
public Result<PageResponse<AdminCommentDTO>> getComments(
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer page,
@Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Integer size,
@Parameter(description = "搜索关键词") @RequestParam(required = false) String keyword,
@Parameter(description = "帖子ID") @RequestParam(required = false) Long postId
) {
return Result.success(adminService.getCommentList(page, size, keyword, postId));
}
@DeleteMapping("/comments/{id}")
@Operation(summary = "删除评论", description = "管理员删除评论")
public Result<Void> deleteComment(@Parameter(description = "评论ID") @PathVariable Long id) {
adminService.deleteComment(id);
return Result.success();
}
@GetMapping("/statistics/user")
@Operation(summary = "获取用户统计数据", description = "获取用户注册增长、活跃度等统计数据")
public Result<Map<String, Object>> getUserStatistics(
@Parameter(description = "统计类型(daily,weekly,monthly)") @RequestParam(defaultValue = "daily") String type,
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate
) {
return Result.success(adminService.getUserStatistics(type, startDate, endDate));
}
@GetMapping("/statistics/post")
@Operation(summary = "获取帖子统计数据", description = "获取帖子发布、点赞、收藏等统计数据")
public Result<Map<String, Object>> getPostStatistics(
@Parameter(description = "统计类型(daily,weekly,monthly)") @RequestParam(defaultValue = "daily") String type,
@Parameter(description = "开始日期") @RequestParam(required = false) String startDate,
@Parameter(description = "结束日期") @RequestParam(required = false) String endDate
) {
return Result.success(adminService.getPostStatistics(type, startDate, endDate));
}
}

@ -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,82 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* DTO
*/
@Data
public class AdminOverviewDTO {
/**
*
*/
private Long totalUsers;
/**
*
*/
private Long newUsers;
/**
*
*/
private Long totalPosts;
/**
*
*/
private Long newPosts;
/**
*
*/
private Long totalComments;
/**
*
*/
private Long newComments;
/**
*
*/
private Long totalViews;
/**
*
*/
private Long todayViews;
/**
*
*/
private List<AdminUserDTO> activeUsers;
/**
*
*/
private List<AdminPostDTO> hotPosts;
/**
* 7
*/
private Map<String, Long> userGrowth;
/**
* 7
*/
private Map<String, Long> postGrowth;
/**
*
*/
private Map<String, Long> userRoleDistribution;
/**
*
*/
private Map<String, Long> postCategoryDistribution;
}

@ -0,0 +1,91 @@
package com.luojia_channel.modules.admin.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DTO
*/
@Data
public class AdminPostDTO {
/**
* ID
*/
private Long id;
/**
*
*/
private String title;
/**
*
*/
private String content;
/**
*
*/
private String image;
/**
*
*/
private LocalDateTime createTime;
/**
*
*/
private LocalDateTime updateTime;
/**
* 0 1 2
*/
private Integer status;
/**
* ID
*/
private Long userId;
/**
*
*/
private String username;
/**
*
*/
private String userAvatar;
/**
* ID
*/
private Long categoryId;
/**
*
*/
private String categoryName;
/**
*
*/
private Integer likeCount;
/**
*
*/
private Integer commentCount;
/**
*
*/
private Integer favoriteCount;
/**
*
*/
private Integer viewCount;
}

@ -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);
}

@ -0,0 +1,547 @@
package com.luojia_channel.modules.admin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.luojia_channel.common.domain.page.PageResponse;
import com.luojia_channel.modules.admin.dto.*;
import com.luojia_channel.modules.admin.service.AdminService;
import com.luojia_channel.modules.post.entity.Post;
import com.luojia_channel.modules.post.entity.Comment;
import com.luojia_channel.modules.post.mapper.PostMapper;
import com.luojia_channel.modules.post.mapper.CommentMapper;
import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
/**
*
*/
@Service
@RequiredArgsConstructor
public class AdminServiceImpl implements AdminService {
private final UserMapper userMapper;
private final PostMapper postMapper;
private final CommentMapper commentMapper;
@Override
public AdminOverviewDTO getOverview() {
AdminOverviewDTO overview = new AdminOverviewDTO();
// 获取用户总数
overview.setTotalUsers(userMapper.selectCount(null));
// 获取今日新增用户数
LocalDateTime today = LocalDate.now().atStartOfDay();
overview.setNewUsers(userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, today)
));
// 获取帖子总数
overview.setTotalPosts(postMapper.selectCount(null));
// 获取今日新增帖子数
overview.setNewPosts(postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, today)
));
// 获取评论总数
overview.setTotalComments(commentMapper.selectCount(null));
// 获取今日新增评论数
overview.setNewComments(commentMapper.selectCount(
new LambdaQueryWrapper<Comment>()
.ge(Comment::getCreateTime, today)
));
// 获取总浏览量和今日浏览量(假设有这样的字段或方法)
overview.setTotalViews(0L); // 实际项目中应该从数据库获取
overview.setTodayViews(0L); // 实际项目中应该从数据库获取
// 获取活跃用户排行(假设通过发帖和评论数量判断)
List<AdminUserDTO> activeUsers = new ArrayList<>(); // 实际项目中应该从数据库联表查询
overview.setActiveUsers(activeUsers);
// 获取热门帖子排行
List<AdminPostDTO> hotPosts = new ArrayList<>(); // 实际项目中应该从数据库联表查询
overview.setHotPosts(hotPosts);
// 用户增长趋势最近7天
Map<String, Long> userGrowth = new LinkedHashMap<>();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(6);
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, dayStart)
.lt(User::getCreateTime, dayEnd)
);
userGrowth.put(dateStr, count);
}
overview.setUserGrowth(userGrowth);
// 帖子增长趋势最近7天
Map<String, Long> postGrowth = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, dayStart)
.lt(Post::getCreateTime, dayEnd)
);
postGrowth.put(dateStr, count);
}
overview.setPostGrowth(postGrowth);
// 用户类型分布(按角色)
Map<String, Long> userRoleDistribution = new HashMap<>();
userRoleDistribution.put("普通用户", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 1)
));
userRoleDistribution.put("管理员", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 2)
));
userRoleDistribution.put("超级管理员", userMapper.selectCount(
new LambdaQueryWrapper<User>().eq(User::getRole, 3)
));
overview.setUserRoleDistribution(userRoleDistribution);
// 帖子分类分布(实际项目中应该从分类表联查)
Map<String, Long> postCategoryDistribution = new HashMap<>();
overview.setPostCategoryDistribution(postCategoryDistribution);
return overview;
}
@Override
public PageResponse<AdminUserDTO> getUserList(Integer page, Integer size, String keyword, Integer role, Integer status) {
Page<User> pageParam = new Page<>(page, size);
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(User::getUsername, keyword);
}
if (role != null) {
queryWrapper.eq(User::getRole, role);
}
if (status != null) {
queryWrapper.eq(User::getStatus, status);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(User::getCreateTime);
IPage<User> userPage = userMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminUserDTO> userDTOs = userPage.getRecords().stream().map(user -> {
AdminUserDTO dto = new AdminUserDTO();
BeanUtils.copyProperties(user, dto);
// 获取用户的发帖数、评论数、粉丝数、关注数(实际项目中应该通过关联查询获取)
dto.setPostCount(0);
dto.setCommentCount(0);
dto.setFollowerCount(0);
dto.setFollowingCount(0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminUserDTO>builder()
.current((long) page)
.size((long) size)
.total(userPage.getTotal())
.records(userDTOs)
.build();
}
@Override
@Transactional
public void changeUserStatus(Long id, Integer status) {
if (id == null || status == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查状态值是否合法
if (status != 1 && status != 2) {
throw new IllegalArgumentException("用户状态值不合法");
}
// 检查用户是否存在
User user = userMapper.selectById(id);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
// 修改状态
user.setStatus(status);
userMapper.updateById(user);
}
@Override
@Transactional
public void changeUserRole(Long id, Integer role) {
if (id == null || role == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查角色值是否合法
if (role < 1 || role > 3) {
throw new IllegalArgumentException("用户角色值不合法");
}
// 检查用户是否存在
User user = userMapper.selectById(id);
if (user == null) {
throw new IllegalArgumentException("用户不存在");
}
// 修改角色
user.setRole(role);
userMapper.updateById(user);
}
@Override
public PageResponse<AdminPostDTO> getPostList(Integer page, Integer size, String keyword, Long categoryId, Integer status) {
Page<Post> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Post> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Post::getTitle, keyword)
.or().like(Post::getContent, keyword);
}
if (categoryId != null) {
queryWrapper.eq(Post::getCategoryId, categoryId);
}
if (status != null) {
queryWrapper.eq(Post::getStatus, status);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(Post::getCreateTime);
IPage<Post> postPage = postMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminPostDTO> postDTOs = postPage.getRecords().stream().map(post -> {
AdminPostDTO dto = new AdminPostDTO();
BeanUtils.copyProperties(post, dto);
// 获取发帖用户信息(实际项目中应该通过关联查询获取)
User user = userMapper.selectById(post.getUserId());
if (user != null) {
dto.setUsername(user.getUsername());
dto.setUserAvatar(user.getAvatar());
}
// 获取分类信息(实际项目中应该通过关联查询获取)
dto.setCategoryName("默认分类"); // 示例值,实际应从数据库获取
// 获取帖子的点赞数、评论数、收藏数、浏览量(实际项目中应该通过关联查询获取)
dto.setLikeCount(0);
dto.setCommentCount(0);
dto.setFavoriteCount(0);
dto.setViewCount(0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminPostDTO>builder()
.current((long) page)
.size((long) size)
.total(postPage.getTotal())
.records(postDTOs)
.build();
}
@Override
@Transactional
public void deletePost(Long id) {
if (id == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查帖子是否存在
Post post = postMapper.selectById(id);
if (post == null) {
throw new IllegalArgumentException("帖子不存在或已被删除");
}
// 删除帖子
postMapper.deleteById(id);
// 删除关联的评论(实际项目中应该通过关联删除)
commentMapper.delete(new LambdaQueryWrapper<Comment>()
.eq(Comment::getPostId, id));
}
@Override
@Transactional
public void changePostStatus(Long id, Integer action) {
if (id == null || action == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查帖子是否存在
Post post = postMapper.selectById(id);
if (post == null) {
throw new IllegalArgumentException("帖子不存在或已被删除");
}
// 根据操作类型修改状态
switch (action) {
case 1: // 置顶
post.setStatus(1);
break;
case 2: // 取消置顶
post.setStatus(0);
break;
case 3: // 隐藏
post.setStatus(2);
break;
case 4: // 显示
post.setStatus(0);
break;
default:
throw new IllegalArgumentException("操作类型不合法");
}
postMapper.updateById(post);
}
@Override
public PageResponse<AdminCommentDTO> getCommentList(Integer page, Integer size, String keyword, Long postId) {
Page<Comment> pageParam = new Page<>(page, size);
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
// 按条件筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Comment::getContent, keyword);
}
if (postId != null) {
queryWrapper.eq(Comment::getPostId, postId);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(Comment::getCreateTime);
IPage<Comment> commentPage = commentMapper.selectPage(pageParam, queryWrapper);
// 转换为DTO
List<AdminCommentDTO> commentDTOs = commentPage.getRecords().stream().map(comment -> {
AdminCommentDTO dto = new AdminCommentDTO();
BeanUtils.copyProperties(comment, dto);
// 获取评论用户信息(实际项目中应该通过关联查询获取)
User user = userMapper.selectById(comment.getUserId());
if (user != null) {
dto.setUsername(user.getUsername());
dto.setUserAvatar(user.getAvatar());
}
// 获取回复用户信息(实际项目中应该通过关联查询获取)
if (comment.getParentCommentId() != null) {
User replyUser = userMapper.selectById(comment.getParentCommentId());
if (replyUser != null) {
dto.setReplyUsername(replyUser.getUsername());
}
}
// 获取帖子信息(实际项目中应该通过关联查询获取)
Post post = postMapper.selectById(comment.getPostId());
if (post != null) {
dto.setPostTitle(post.getTitle());
}
// 获取评论的点赞数(实际项目中应该通过关联查询获取)
dto.setLikeCount(comment.getLikeCount() != null ? comment.getLikeCount().intValue() : 0);
return dto;
}).collect(Collectors.toList());
return PageResponse.<AdminCommentDTO>builder()
.current((long) page)
.size((long) size)
.total(commentPage.getTotal())
.records(commentDTOs)
.build();
}
@Override
@Transactional
public void deleteComment(Long id) {
if (id == null) {
throw new IllegalArgumentException("参数不能为空");
}
// 检查评论是否存在
Comment comment = commentMapper.selectById(id);
if (comment == null) {
throw new IllegalArgumentException("评论不存在或已被删除");
}
// 删除评论
commentMapper.deleteById(id);
// 删除子评论(实际项目中应该根据具体业务逻辑决定是否删除)
commentMapper.delete(new LambdaQueryWrapper<Comment>()
.eq(Comment::getParentCommentId, id));
}
@Override
public Map<String, Object> getUserStatistics(String type, String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 日期格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 默认查询最近7天
LocalDate end = endDate != null ? LocalDate.parse(endDate, formatter) : LocalDate.now();
LocalDate start;
if (startDate != null) {
start = LocalDate.parse(startDate, formatter);
} else {
switch (type) {
case "weekly":
start = end.minusDays(7);
break;
case "monthly":
start = end.minusDays(30);
break;
case "daily":
default:
start = end.minusDays(7);
break;
}
}
// 用户注册统计
Map<String, Long> registrations = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = userMapper.selectCount(
new LambdaQueryWrapper<User>()
.ge(User::getCreateTime, dayStart)
.lt(User::getCreateTime, dayEnd)
);
registrations.put(dateStr, count);
}
result.put("registrations", registrations);
// 用户活跃度统计(实际项目中应该根据登录记录或操作记录统计)
Map<String, Long> activeUsers = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
activeUsers.put(dateStr, 0L); // 示例值,实际应从数据库获取
}
result.put("activeUsers", activeUsers);
return result;
}
@Override
public Map<String, Object> getPostStatistics(String type, String startDate, String endDate) {
Map<String, Object> result = new HashMap<>();
// 日期格式化器
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 默认查询最近7天
LocalDate end = endDate != null ? LocalDate.parse(endDate, formatter) : LocalDate.now();
LocalDate start;
if (startDate != null) {
start = LocalDate.parse(startDate, formatter);
} else {
switch (type) {
case "weekly":
start = end.minusDays(7);
break;
case "monthly":
start = end.minusDays(30);
break;
case "daily":
default:
start = end.minusDays(7);
break;
}
}
// 帖子发布统计
Map<String, Long> publications = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = postMapper.selectCount(
new LambdaQueryWrapper<Post>()
.ge(Post::getCreateTime, dayStart)
.lt(Post::getCreateTime, dayEnd)
);
publications.put(dateStr, count);
}
result.put("publications", publications);
// 评论统计
Map<String, Long> comments = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
Long count = commentMapper.selectCount(
new LambdaQueryWrapper<Comment>()
.ge(Comment::getCreateTime, dayStart)
.lt(Comment::getCreateTime, dayEnd)
);
comments.put(dateStr, count);
}
result.put("comments", comments);
// 点赞统计(实际项目中应该从点赞表查询)
Map<String, Long> likes = new LinkedHashMap<>();
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
String dateStr = date.format(formatter);
likes.put(dateStr, 0L); // 示例值,实际应从数据库获取
}
result.put("likes", likes);
return result;
}
}

@ -3,6 +3,7 @@ package com.luojia_channel.modules.user.controller;
import com.luojia_channel.common.domain.Result; import com.luojia_channel.common.domain.Result;
import com.luojia_channel.modules.file.dto.UploadFileDTO; import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.dto.UserInfoDTO;
import com.luojia_channel.modules.user.service.UserInfoService; import com.luojia_channel.modules.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
@ -18,6 +19,21 @@ import org.springframework.web.multipart.MultipartFile;
@Tag(name = "用户信息管理", description = "用户修改个人信息相关接口") @Tag(name = "用户信息管理", description = "用户修改个人信息相关接口")
public class UserInfoController { public class UserInfoController {
private final UserInfoService userInfoService; private final UserInfoService userInfoService;
@GetMapping("/getuserinfo")
@Operation(
summary="获取用户信息",
description="获取指定用户的信息如果不指定userId则获取当前登录用户信息",
tags = {"用户信息管理"}
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "获取成功"),
@ApiResponse(responseCode = "500", description = "获取失败,用户不存在或未登录")
})
public Result<UserInfoDTO> getUserInfo(@RequestParam(required = false) Long userId) {
return Result.success(userInfoService.getUserInfo(userId));
}
@PostMapping("/update") @PostMapping("/update")
@Operation( @Operation(
summary="修改用户信息", summary="修改用户信息",

@ -0,0 +1,59 @@
package com.luojia_channel.modules.user.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户信息DTO")
public class UserInfoDTO {
@Schema(
description = "用户ID"
)
private Long id;
@Schema(
description = "用户名"
)
private String username;
@Schema(
description = "手机号"
)
private String phone;
@Schema(
description = "邮箱"
)
private String email;
@Schema(
description = "头像url"
)
private String avatar;
@Schema(
description = "性别(0未知1男2女)"
)
private Integer gender;
@Schema(
description = "学院"
)
private String college;
@Schema(
description = "角色(1普通用户2管理员3超级管理员)"
)
private Integer role;
@Schema(
description = "状态(1正常2冻结)"
)
private Integer status;
}

@ -3,6 +3,7 @@ package com.luojia_channel.modules.user.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.luojia_channel.modules.file.dto.UploadFileDTO; import com.luojia_channel.modules.file.dto.UploadFileDTO;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.dto.UserInfoDTO;
import com.luojia_channel.modules.user.entity.User; import com.luojia_channel.modules.user.entity.User;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -13,4 +14,11 @@ public interface UserInfoService extends IService<User> {
void updatePassword(String password); void updatePassword(String password);
String uploadAvatar(MultipartFile file); String uploadAvatar(MultipartFile file);
/**
*
* @param userId IDnull
* @return DTO
*/
UserInfoDTO getUserInfo(Long userId);
} }

@ -10,6 +10,7 @@ import com.luojia_channel.modules.file.service.impl.FileServiceImpl;
import com.luojia_channel.modules.file.utils.GeneratePathUtil; import com.luojia_channel.modules.file.utils.GeneratePathUtil;
import com.luojia_channel.modules.file.utils.ValidateFileUtil; import com.luojia_channel.modules.file.utils.ValidateFileUtil;
import com.luojia_channel.modules.user.dto.UserChangeInfoDTO; import com.luojia_channel.modules.user.dto.UserChangeInfoDTO;
import com.luojia_channel.modules.user.dto.UserInfoDTO;
import com.luojia_channel.modules.user.entity.User; import com.luojia_channel.modules.user.entity.User;
import com.luojia_channel.modules.user.mapper.UserMapper; import com.luojia_channel.modules.user.mapper.UserMapper;
import com.luojia_channel.modules.user.service.UserInfoService; import com.luojia_channel.modules.user.service.UserInfoService;
@ -65,4 +66,36 @@ public class UserInfoServiceImpl extends ServiceImpl<UserMapper, User> implement
public String uploadAvatar(MultipartFile file) { public String uploadAvatar(MultipartFile file) {
return fileService.uploadFile(file); return fileService.uploadFile(file);
} }
@Override
public UserInfoDTO getUserInfo(Long userId) {
// 如果userId为null则获取当前登录用户的ID
if (userId == null) {
userId = UserContext.getUserId();
if (userId == null) {
throw new UserException("用户未登录");
}
}
// 从数据库获取用户信息
User user = userMapper.selectById(userId);
if (user == null) {
throw new UserException("用户不存在");
}
// 转换为DTO对象
UserInfoDTO userInfoDTO = UserInfoDTO.builder()
.id(user.getId())
.username(user.getUsername())
.phone(user.getPhone())
.email(user.getEmail())
.avatar(user.getAvatar())
.gender(user.getGender())
.college(user.getCollege())
.role(user.getRole())
.status(user.getStatus())
.build();
return userInfoDTO;
}
} }

@ -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

@ -8,8 +8,11 @@ springdoc:
disable-swagger-default-url: true disable-swagger-default-url: true
tags-sorter: alpha tags-sorter: alpha
operations-sorter: alpha operations-sorter: alpha
packages-to-scan: com.luojia_channel.modules enabled: true
paths-to-match: /user/**, /post/**, /comment/**, /message/**, /follow/**, /file/** default-produces-media-type: application/json
default-consumes-media-type: application/json
packagesToScan: com.luojia_channel.modules
paths-to-match: /api/**,/admin/**
spring: spring:
application: application:
name: service name: service
@ -62,8 +65,5 @@ mybatis-plus:
mapper-locations: classpath*:mapper/**/*.xml mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.luojia.luojia_channel.modules.*.entity type-aliases-package: com.luojia.luojia_channel.modules.*.entity
management:
health:
elasticsearch:
enabled: false

@ -31,23 +31,23 @@
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<!-- 登录/注册按钮 -->
<div class="nav-section"> <div class="nav-section">
<button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button> <button v-if="!isLoggedIn" @click="showModal" class="login-btn">/</button>
<div v-else class="user-menu"> <div v-else class="user-menu">
<router-link to="/notificationlist" class="nav-btn">通知</router-link> <router-link to="/notificationlist" class="nav-btn">通知</router-link>
<router-link v-if="showAdmin" to="/admin" class="nav-btn admin-btn"></router-link>
<router-link to="/feedback" class="nav-btn">反馈站</router-link> <router-link to="/feedback" class="nav-btn">反馈站</router-link>
<div class="user-avatar" @mouseenter="showDropdown" @mouseleave="hideDropdown"> <div class="user-avatar" @mouseenter="showDropdown" @mouseleave="hideDropdown">
<img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" /> <img :src="userInfo.avatar || defaultAvatar" alt="用户头像" class="avatar-img" />
<!-- 悬浮板块 --> <!-- 悬浮板块 -->
<div class="user-dropdown-menu" v-show="isDropdownVisible"> <div class="user-dropdown-menu" v-show="isDropdownVisible">
<p class="user-name">{{ userInfo.userName }}</p> <p class="user-name">{{ userInfo.username }}</p>
<div class="button-container"> <div class="button-container">
<button @click="goToProfile" class="dropdown-btn">个人中心</button> <button @click="goToProfile" class="dropdown-btn">个人中心</button>
<router-link to="/ChangeInformation" class="nav-btn">修改个人信息</router-link> <router-link to="/ChangeInformation" class="dropdown-btn">修改个人信息</router-link>
<router-link to="/postpublish" class="dropdown-btn">发布帖子</router-link>
<button @click="logout" class="dropdown-btn">退出登录</button> <button @click="logout" class="dropdown-btn">退出登录</button>
</div> </div>
</div> </div>
@ -62,10 +62,11 @@
</template> </template>
<script lang="js" setup name="Header"> <script lang="js" setup name="Header">
import { ref, computed } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { useUserStore } from '@/stores/user.js'; import { useUserStore } from '@/stores/user.js';
import LoginRegisterModal from './LoginRegisterModal.vue'; import LoginRegisterModal from './LoginRegisterModal.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import request from '@/utils/request';
// //
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
@ -96,7 +97,14 @@ const defaultAvatar = require("@/assets/default-avatar/boy_4.png");
// //
const isLoggedIn = computed(() => userStore.isLoggedIn); const isLoggedIn = computed(() => userStore.isLoggedIn);
const userInfo = computed(() => userStore.userInfo); const userInfo = computed(() => {
return userStore.userInfo;
});
const showAdmin = computed(() => {
const isAdmin = userStore.isLoggedIn && (userInfo.value.role >= 2);
console.log('检查管理员权限:', userStore.isLoggedIn, userInfo.value.role, isAdmin);
return isAdmin;
});
// //
const showModal = () => { const showModal = () => {
@ -113,9 +121,20 @@ const goToProfile = () => {
}; };
const logout = () => { const logout = () => {
userStore.logout(); //
router.push({ name: 'MainPage' }); // request.post('/user/logout')
isDropdownVisible.value = false; // .then(() => {
console.log('成功调用后端登出接口');
})
.catch(error => {
console.error('调用后端登出接口失败:', error);
})
.finally(() => {
//
userStore.logout();
router.push({ name: 'MainPage' }); //
isDropdownVisible.value = false;
});
}; };
let showTimer = null; // let showTimer = null; //
@ -134,6 +153,18 @@ const hideDropdown = () => {
isDropdownVisible.value = false; // 1 isDropdownVisible.value = false; // 1
}, 400); }, 400);
}; };
//
onMounted(() => {
if (userStore.isLoggedIn) {
console.log('Header组件挂载: 检测到用户已登录:', userStore.userInfo.username, '角色:', userStore.userInfo.role);
}
});
//
watch(() => userStore.userInfo, (newInfo) => {
console.log('用户信息更新:', newInfo.username, '角色:', newInfo.role);
}, { deep: true });
</script> </script>
<style scoped> <style scoped>
@ -364,12 +395,14 @@ const hideDropdown = () => {
.button-container { .button-container {
display: flex; display: flex;
justify-content: space-between; /* 按钮左右排列 */ flex-direction: column; /* 改为垂直排列 */
gap: 8px; /* 按钮间间距 */
width: 100%; /* 按钮容器占满父容器宽度 */ width: 100%; /* 按钮容器占满父容器宽度 */
} }
/* 悬浮板块按钮样式 */ /* 悬浮板块按钮样式 */
.dropdown-btn { .dropdown-btn {
width: 45%; /* 按钮占据父容器的 45% 宽度 */ width: 100%; /* 按钮占据父容器的100%宽度 */
padding: 8px 10px; padding: 8px 10px;
background: none; background: none;
border: 0; border: 0;
@ -379,10 +412,20 @@ const hideDropdown = () => {
color: #333; color: #333;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s, color 0.3s; transition: background-color 0.3s, color 0.3s;
text-decoration: none; /* 去除router-link的下划线 */
} }
.dropdown-btn:hover { .dropdown-btn:hover {
background-color: #f0f0f0; background-color: #f0f0f0;
color: #6fbd87; color: #6fbd87;
} }
.admin-btn {
color: #ff6b6b !important;
font-weight: bold;
}
.admin-btn:hover {
background-color: #ffeeee;
}
</style> </style>

@ -216,8 +216,7 @@ async function login() {
userStore.login({ userStore.login({
avatar:require ('@/assets/default-avatar/boy_1.png'), avatar:require ('@/assets/default-avatar/boy_1.png'),
username: loginForm.value.userFlag, // 使 userName: '珈人一号',
password: loginForm.value.password,
userid:1 userid:1
}); });

@ -7,6 +7,7 @@ import UserPage from '@/views/UserPage.vue';
import NotificationList from '@/views/NotificationList.vue'; import NotificationList from '@/views/NotificationList.vue';
import ChangeInformation from '@/views/ChangeInformation.vue'; import ChangeInformation from '@/views/ChangeInformation.vue';
import FeedBack from '@/views/FeedBack.vue'; import FeedBack from '@/views/FeedBack.vue';
import PostPublish from '@/views/PostPublish.vue';
const routes = [ const routes = [
{ {
@ -45,7 +46,7 @@ const routes = [
component: () => import('@/views/NotificationDetail.vue'), component: () => import('@/views/NotificationDetail.vue'),
props: true props: true
}, },
{//通知页面 {//反馈页面
path: '/feedback', path: '/feedback',
name: 'FeedBack', name: 'FeedBack',
component: FeedBack component: FeedBack
@ -55,6 +56,46 @@ const routes = [
path:'/changeinformation', path:'/changeinformation',
name:'ChangeInformation', name:'ChangeInformation',
component:ChangeInformation component:ChangeInformation
},
{
// 发布帖子页面
path: '/postpublish',
name: 'PostPublish',
component: PostPublish
},
{
// 管理员页面
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/AdminDashboard.vue'),
children: [
{
path: '',
name: 'AdminOverview',
component: () => import('@/views/admin/AdminOverview.vue')
},
{
path: 'posts',
name: 'AdminPosts',
component: () => import('@/views/admin/AdminPosts.vue')
},
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/AdminUsers.vue')
},
{
path: 'comments',
name: 'AdminComments',
component: () => import('@/views/admin/AdminComments.vue')
},
{
path: 'categories',
name: 'AdminCategories',
component: () => import('@/views/admin/AdminCategories.vue')
}
],
meta: { requiresAdmin: true }
} }
]; ];

@ -1,5 +1,4 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
//import axios from 'axios';
import request from '@/utils/request'; import request from '@/utils/request';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
@ -47,17 +46,17 @@ export const usePostListStore = defineStore('postList', {
const res = await request.post('/post/list', requestData); const res = await request.post('/post/list', requestData);
if (res.code === 200) { if (res.code === 200) {
const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data; const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data;
if (records.length > 0) { if (records && records.length > 0) {
// 字段映射 // 字段映射
const mappedRecords = records.map(post => ({ const mappedRecords = records.map(post => ({
id: post.id, id: post.id,
image: post.image, image: post.image,
avatar: post.userAvatar , avatar: post.userAvatar || require('@/assets/default-avatar/boy_1.png'),
title: post.title, title: post.title,
summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '', summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '',
likes: post.likeCount, likes: post.likeCount || 0,
comments: post.commentCount, comments: post.commentCount || 0,
favorites: post.favoriteCount, favorites: post.favoriteCount || 0,
category: post.category || '全部', category: post.category || '全部',
createTime: post.createTime, createTime: post.createTime,
userName: post.userName, userName: post.userName,
@ -67,23 +66,25 @@ export const usePostListStore = defineStore('postList', {
this.offset = newOffset; this.offset = newOffset;
this.pageSize = newSize; this.pageSize = newSize;
} }
if (records.length < this.pageSize) { if (!records || records.length < this.pageSize) {
this.finished = true; // 没有更多数据 this.finished = true; // 没有更多数据
} }
} } else {
else { console.error('获取帖子列表返回错误:', res.msg);
// 登录失败
ElMessage({ ElMessage({
message: '获取帖子列表失败,请稍后重试', message: res.msg || '获取帖子列表失败,请稍后重试',
type: 'error', type: 'error',
duration: 500 duration: 1500
}); });
} }
}catch (error) { } catch (error) {
console.error("获取帖子列表失败:", error); console.error("获取帖子列表失败:", error);
console.error('获取失败', error); ElMessage({
alert(error.response?.message || '获取失败,请稍后重试'); message: error.response?.data?.msg || '获取帖子列表失败,请稍后重试',
}finally { type: 'error',
duration: 1500
});
} finally {
this.loading = false; this.loading = false;
} }
}, },

@ -4,26 +4,36 @@ export const useUserStore = defineStore('user', {
state: () => ({ state: () => ({
isLoggedIn: false, // 登录状态 isLoggedIn: false, // 登录状态
userInfo: { userInfo: {
avatar: '', // 用户头像 URL
username: '', // 用户名
userid: 0, // 用户 ID userid: 0, // 用户 ID
moto:'',// 用户简介 username: '', // 用户名
phone: '', // 用户手机号 avatar: '', // 用户头像
email: '', // 用户邮箱 email: '', // 用户邮箱
studentID: '', // 学号 phone: '', // 用户手机号
studentId: '', // 学号
college: '', // 学院 college: '', // 学院
gender: 0, // 性别 0: 未知, 1: 男, 2: 女 gender: 0, // 性别,0-未知,1-男,2-女
password: ''
}, },
}), }),
actions: { actions: {
login(userData) { login(userData) {
this.isLoggedIn = true; this.isLoggedIn = true;
this.userInfo = userData; this.userInfo = {
...userData,
};
}, },
logout() { logout() {
this.isLoggedIn = false; this.isLoggedIn = false;
this.userInfo = { avatar: '', username: '' , userid: 0 }; this.userInfo = {
userid: 0,
username: '',
avatar: '',
email: '',
phone: '',
studentId: '',
college: '',
gender: 0,
isPublic: true
};
}, },
}, },
}); });

@ -1,8 +1,8 @@
import {defineStore} from 'pinia'; import {defineStore} from 'pinia';
//import axios from 'axios';
import request from '@/utils/request'; import request from '@/utils/request';
import { ElMessage } from 'element-plus';
export const usePostListStore = defineStore('postList', { export const useUserPostStore = defineStore('userPost', {
state: () => ({ state: () => ({
posts: [], // 帖子列表 posts: [], // 帖子列表
total: 0, // 帖子总数 total: 0, // 帖子总数
@ -41,16 +41,16 @@ export const usePostListStore = defineStore('postList', {
const res = await request.post('/post/user', { lastVal, offset, size }); const res = await request.post('/post/user', { lastVal, offset, size });
if (res.code === 200) { if (res.code === 200) {
const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data; const { records, lastVal: newLastVal, offset: newOffset, size: newSize } = res.data;
if (records.length > 0) { if (records && records.length > 0) {
// 字段映射 // 字段映射
const mappedRecords = records.map(post => ({ const mappedRecords = records.map(post => ({
id: post.id, id: post.id,
avatar: post.userAvatar || post.image || require('@/assets/default-avatar/boy_1.png'), avatar: post.userAvatar || post.image || require('@/assets/default-avatar/boy_1.png'),
title: post.title, title: post.title,
summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '', summary: post.content ? post.content.slice(0, 40) + (post.content.length > 40 ? '...' : '') : '',
likes: post.likeCount, likes: post.likeCount || 0,
comments: post.commentCount, comments: post.commentCount || 0,
favorites: post.favoriteCount, favorites: post.favoriteCount || 0,
category: post.category || '全部', category: post.category || '全部',
createTime: post.createTime, createTime: post.createTime,
userName: post.userName, userName: post.userName,
@ -60,10 +60,24 @@ export const usePostListStore = defineStore('postList', {
this.offset = newOffset; this.offset = newOffset;
this.pageSize = newSize; this.pageSize = newSize;
} }
if (records.length < size) { if (!records || records.length < size) {
this.finished = true; // 没有更多数据 this.finished = true; // 没有更多数据
} }
} else {
console.error('获取用户帖子列表返回错误:', res.msg);
ElMessage({
message: res.msg || '获取用户帖子列表失败,请稍后重试',
type: 'error',
duration: 1500
});
} }
} catch (error) {
console.error("获取用户帖子列表失败:", error);
ElMessage({
message: error.response?.data?.msg || '获取用户帖子列表失败,请稍后重试',
type: 'error',
duration: 1500
});
} finally { } finally {
this.loading = false; this.loading = false;
} }

@ -4,7 +4,7 @@
<div class="editor-wrapper"> <div class="editor-wrapper">
<el-form <el-form
ref="postForm" ref="postFormRef"
:model="form" :model="form"
:rules="rules" :rules="rules"
label-width="80px" label-width="80px"
@ -46,7 +46,7 @@
:on-remove="handleRemove" :on-remove="handleRemove"
:limit="3" :limit="3"
> >
<i class="el-icon-plus"></i> <el-icon><Plus /></el-icon>
</el-upload> </el-upload>
</el-form-item> </el-form-item>
@ -91,116 +91,138 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { import { ref, reactive, onMounted } from 'vue';
data() { import { useRouter } from 'vue-router';
return { import axios from 'axios';
submitting: false, import { Plus } from '@element-plus/icons-vue';
categories: [], // API import { ElMessage } from 'element-plus';
fileList: [],
form: { const router = useRouter();
title: '', const postFormRef = ref(null);
content: '',
categoryId: null, //
image: '', const submitting = ref(false);
status: 0 // 0 const categories = ref([]);
}, const fileList = ref([]);
rules: { const form = reactive({
title: [ title: '',
{ required: true, message: '标题不能为空', trigger: 'blur' }, content: '',
{ min: 3, max: 50, message: '长度在3到50个字符', trigger: 'blur' } categoryId: null,
], image: '',
content: [ status: 0 // 0
{ required: true, message: '内容不能为空', trigger: 'blur' }, });
{ min: 10, message: '内容至少10个字符', trigger: 'blur' }
], const rules = {
categoryId: [ title: [
{ required: true, message: '请选择分类', trigger: 'change' } { required: true, message: '标题不能为空', trigger: 'blur' },
] { min: 3, max: 50, message: '长度在3到50个字符', trigger: 'blur' }
}, ],
submitResult: { content: [
visible: false, { required: true, message: '内容不能为空', trigger: 'blur' },
type: 'success', { min: 10, message: '内容至少10个字符', trigger: 'blur' }
title: '' ],
} categoryId: [
} { required: true, message: '请选择分类', trigger: 'change' }
}, ]
async created() { };
//
try { const submitResult = reactive({
const response = await this.$axios.get('/categories') visible: false,
this.categories = response.data.data type: 'success',
} catch (error) { title: ''
console.error('加载分类失败:', error) });
//
const loadCategories = async () => {
try {
const response = await axios.get('/api/categories');
if (response.data.code === 0) {
categories.value = response.data.data || [];
} }
}, } catch (error) {
methods: { console.error('加载分类失败:', error);
// ElMessage.error('加载分类失败,请刷新页面重试');
handleUploadSuccess(response) { }
this.form.image = this.form.image };
? `${this.form.image},${response.data.url}`
: response.data.url //
}, const handleUploadSuccess = (response) => {
handleRemove(file) { form.image = form.image
const urls = this.form.image.split(',') ? `${form.image},${response.data.url}`
this.form.image = urls.filter(url => url !== file.url).join(',') : response.data.url;
}, };
// const handleRemove = (file) => {
async submitForm() { if (!form.image) return;
try { const urls = form.image.split(',');
this.submitting = true form.image = urls.filter(url => url !== file.url).join(',');
await this.$refs.postForm.validate() };
const response = await this.$axios.post('/post', this.form) //
const submitForm = async () => {
if (response.data.code === 0) { if (!postFormRef.value) return;
this.showResult('success', '帖子发布成功')
setTimeout(() => { try {
this.$router.push(`/post/${response.data.data}`) submitting.value = true;
}, 1500) await postFormRef.value.validate();
} else {
this.showResult('error', response.data.msg || '发布失败') const response = await axios.post('/api/post', form);
}
} catch (error) { if (response.data.code === 0) {
this.showResult('error', error.response?.data?.msg || '网络错误') showResult('success', '帖子发布成功');
} finally {
this.submitting = false
}
},
// 稿
async saveDraft() {
try {
await this.$axios.post('/drafts', {
...this.form,
status: 1 // 1稿
})
this.showResult('success', '草稿保存成功')
} catch (error) {
this.showResult('error', '保存失败')
}
},
//
showResult(type, message) {
this.submitResult = {
visible: true,
type,
title: message
}
setTimeout(() => { setTimeout(() => {
this.submitResult.visible = false router.push(`/post/${response.data.data}`);
}, 3000) }, 1500);
} else {
showResult('error', response.data.msg || '发布失败');
} }
} catch (error) {
showResult('error', error.response?.data?.msg || '网络错误');
} finally {
submitting.value = false;
} }
} };
// 稿
const saveDraft = async () => {
try {
const response = await axios.post('/api/drafts', {
...form,
status: 1 // 1稿
});
if (response.data.code === 0) {
showResult('success', '草稿保存成功');
} else {
showResult('error', response.data.msg || '保存失败');
}
} catch (error) {
showResult('error', error.response?.data?.msg || '网络错误');
}
};
//
const showResult = (type, message) => {
submitResult.visible = true;
submitResult.type = type;
submitResult.title = message;
setTimeout(() => {
submitResult.visible = false;
}, 3000);
};
//
onMounted(() => {
loadCategories();
});
</script> </script>
<style scoped> <style scoped>
.post-container { .post-container {
max-width: 800px; max-width: 800px;
margin: 20px auto; margin: 80px auto 20px; /* 增加顶部边距,避免被导航栏遮挡 */
padding: 30px; padding: 30px;
background-color: #f9f9f9; background-color: #f9f9f9;
border-radius: 8px; border-radius: 8px;
@ -242,14 +264,14 @@ export default {
} }
/* 保持与通知页面一致的输入框样式 */ /* 保持与通知页面一致的输入框样式 */
.el-textarea__inner, :deep(.el-textarea__inner),
.el-input__inner { :deep(.el-input__inner) {
border-radius: 4px; border-radius: 4px;
border: 1px solid #e4e4e4; border: 1px solid #e4e4e4;
transition: border-color 0.2s; transition: border-color 0.2s;
} }
.el-input__inner:focus { :deep(.el-input__inner):focus {
border-color: #409eff; border-color: #409eff;
} }
</style> </style>

@ -6,161 +6,402 @@
<img :src="userInfo.avatar" alt="用户头像" /> <img :src="userInfo.avatar" alt="用户头像" />
</div> </div>
<div class="user-details"> <div class="user-details">
<h2 class="username">{{ userInfo.username }}</h2> <h2 class="username">{{ userInfo.username }}</h2>
<p class="user-status">ONLINE</p>
<div class="user-info-list"> <div class="user-info-list">
<div class="user-info-row"> <div class="user-info-item" v-if="userInfo.college && (isCurrentUser || privacySettings.showCollege)">
<span class="user-info-label">学院</span> <span class="info-icon"><i class="el-icon-school"></i></span>
<span class="user-info-value">{{ userInfo.college || '未填写' }}</span> <span class="info-label">学院</span>
</div> <span class="info-value">{{ userInfo.college || '未设置' }}</span>
<div class="user-info-row"> </div>
<span class="user-info-label">性别</span> <div class="user-info-item">
<span class="user-info-value"> <span class="info-icon"><i class="el-icon-user"></i></span>
<span v-if="userInfo.gender === 1"></span> <span class="info-label">性别</span>
<span v-else-if="userInfo.gender === 2"></span> <span class="info-value">
<span v-else></span> <span v-if="userInfo.gender === 1"></span>
</span> <span v-else-if="userInfo.gender === 2"></span>
</div> <span v-else></span>
</div> </span>
</div> </div>
<div class="user-info-item" v-if="userInfo.phone && (isCurrentUser || privacySettings.showPhone)">
<span class="info-icon"><i class="el-icon-phone"></i></span>
<span class="info-label">电话</span>
<span class="info-value">{{ userInfo.phone || '未设置' }}</span>
</div>
<div class="user-info-item" v-if="userInfo.email && (isCurrentUser || privacySettings.showEmail)">
<span class="info-icon"><i class="el-icon-message"></i></span>
<span class="info-label">邮箱</span>
<span class="info-value">{{ userInfo.email || '未设置' }}</span>
</div>
<div class="user-info-item" v-if="isCurrentUser">
<span class="info-icon"><i class="el-icon-lock"></i></span>
<span class="info-label">隐私设置</span>
<div class="privacy-toggles">
<el-switch
v-model="privacySettings.showPhone"
active-text="电话"
inactive-text="电话"
@change="updatePrivacy"
class="privacy-switch"
/>
<el-switch
v-model="privacySettings.showEmail"
active-text="邮箱"
inactive-text="邮箱"
@change="updatePrivacy"
class="privacy-switch"
/>
<el-switch
v-model="privacySettings.showCollege"
active-text="学院"
inactive-text="学院"
@change="updatePrivacy"
class="privacy-switch"
/>
</div>
</div>
</div>
<div class="social-actions" v-if="!isCurrentUser">
<el-button
type="primary"
:plain="isFollowing"
size="small"
@click="toggleFollow"
>
{{ isFollowing ? '已关注' : '关注' }}
</el-button>
<el-button type="primary" plain size="small" @click="openChat"></el-button>
</div>
</div>
</div> </div>
<div class="user-posts"> <div class="user-posts">
<h3>你的帖子</h3> <h3>{{ isCurrentUser ? '我的帖子' : `${userInfo.username}的帖子` }}</h3>
<div v-if="userPosts.length === 0" class="no-posts">
暂无帖子
</div>
<div <div
class="post-item" class="post-item"
v-for="post in userPosts" v-for="post in userPosts"
:key="post.id" :key="post.id"
@click="goToPostDetail(post.id)" @click="goToPostDetail(post.id)"
> >
<h4 class="post-title">{{ post.title }}</h4> <div class="post-header">
<h4 class="post-title">{{ post.title }}</h4>
<span class="post-time">{{ formatDate(post.createTime) }}</span>
</div>
<p class="post-summary">{{ post.summary }}</p> <p class="post-summary">{{ post.summary }}</p>
<div class="post-stats"> <div class="post-stats">
<span>热度 {{ post.likes }}</span> <span><i class="icon-view"></i> {{ post.viewCount || 0 }}</span>
<span>评论 {{ post.comments }}</span> <span><i class="icon-like"></i> {{ post.likeCount || 0 }}</span>
<span> {{ post.favorites }}</span> <span><i class="icon-comment"></i> {{ post.commentCount || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- 加载更多 -->
<div v-if="loading" class="loading-more">...</div>
<div v-if="!loading && !finished" class="load-more" @click="loadMorePosts"></div>
<div v-if="finished" class="no-more"></div>
</div> </div>
<div class="right-container"> <div class="right-container">
<div class="user-stats-card"> <div class="user-stats-card">
<h3>{{ userInfo.userName }} 个人信息</h3> <h3>{{ userInfo.username }} 的统计数据</h3>
<div class="stats"> <div class="stats">
<div class="stat-item"> <div class="stat-item">
<span>发帖数</span> <div class="stat-value">{{ userStats.postCount || 0 }}</div>
<span>{{ userStats.postCount }}</span> <div class="stat-label">发帖数</div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span>粉丝</span> <div class="stat-value">{{ userStats.followers || 0 }}</div>
<span>{{ userStats.followers }}</span> <div class="stat-label">粉丝</div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span>你关注的人数</span> <div class="stat-value">{{ userStats.following || 0 }}</div>
<span>{{ userStats.following }}</span> <div class="stat-label">关注</div>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span>获赞数</span> <div class="stat-value">{{ userStats.likes || 0 }}</div>
<span>{{ userStats.likes }}</span> <div class="stat-label">获赞</div>
</div>
</div>
</div>
<!-- 可能感兴趣的用户或帖子推荐 -->
<div class="recommendations" v-if="recommendations.length > 0">
<h3>推荐关注</h3>
<div class="recommendation-list">
<div
v-for="user in recommendations"
:key="user.id"
class="recommendation-item"
@click="viewUser(user.id)"
>
<img :src="user.avatar" alt="用户头像" class="recommendation-avatar" />
<div class="recommendation-info">
<div class="recommendation-name">{{ user.username }}</div>
<div class="recommendation-followers">{{ user.followers }}位粉丝</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- 添加加载和错误状态显示 -->
<div v-if="loading" class="loading">...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else>
<!-- 你的页面内容 -->
<div v-for="post in userPosts" :key="post.id">
{{ post.title }}
</div> </div>
</div> </div>
</template> </template>
<script setup lang="js" name="UserPage"> <script setup lang="js" name="UserPage">
import { ref, computed, onMounted, onUnmounted } from 'vue'; import { ref, reactive, computed, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useUserStore } from '@/stores/user.js'; import { useUserStore } from '@/stores/user.js';
import { usePostListStore } from '@/stores/userpost'; // import axios from 'axios';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const userStore = useUserStore(); const userStore = useUserStore();
const postListStore = usePostListStore(); // postListStore
// //
const isComponentMounted = ref(false); const userInfo = ref({});
const userId = computed(() => route.params.userId || userStore.userInfo.userid);
const isCurrentUser = computed(() => String(userId.value) === String(userStore.userInfo.userid));
//
const userPosts = ref([]);
const loading = ref(false);
const finished = ref(false);
const error = ref(null);
const pageParams = reactive({
lastVal: 0,
offset: 0,
size: 10
});
// //
const userInfo = computed(() => userStore.userInfo); const userStats = ref({
const userPosts = computed(() => postListStore.posts); postCount: 0,
const userStats = computed(() => ({
postCount: postListStore.total,
followers: 0, followers: 0,
following: 0, following: 0,
likes: userPosts.value.reduce((sum, post) => sum + (post.likes || 0), 0) likes: 0
})); });
const handleScroll = async () => { //
if (!isComponentMounted.value) return; const isFollowing = ref(false);
//
const privacySettings = ref({
showPhone: true,
showEmail: true,
showCollege: true
});
//
const recommendations = ref([]);
//
const loadUserInfo = async () => {
try {
const response = await axios.get(`/api/user/detail?id=${userId.value}`);
if (response.data.code === 0) {
userInfo.value = response.data.data;
//
if (isCurrentUser.value) {
loadPrivacySettings();
}
}
} catch (error) {
console.error('加载用户信息失败:', error);
}
};
//
const loadUserPosts = async () => {
if (loading.value || finished.value) return;
const scrollContainer = document.documentElement; loading.value = true;
if ( try {
scrollContainer.scrollTop + window.innerHeight >= scrollContainer.scrollHeight - 10 && const response = await axios.get('/api/post/user', {
!postListStore.loading && params: {
!postListStore.finished userId: userId.value,
) { ...pageParams
}
});
if (response.data.code === 0) {
const newPosts = response.data.data.list || [];
userPosts.value = [...userPosts.value, ...newPosts];
if (newPosts.length < pageParams.size) {
finished.value = true;
} else {
pageParams.offset += newPosts.length;
pageParams.lastVal = newPosts[newPosts.length - 1].createTime;
}
}
} catch (err) {
error.value = '加载帖子失败,请重试';
console.error(err);
} finally {
loading.value = false;
}
};
//
const loadMorePosts = () => {
loadUserPosts();
};
//
const loadUserStats = async () => {
try {
const response = await axios.get(`/api/user/stats?id=${userId.value}`);
if (response.data.code === 0) {
userStats.value = response.data.data;
}
} catch (error) {
console.error('加载用户统计数据失败:', error);
}
};
//
const checkFollowStatus = async () => {
if (!isCurrentUser.value && userStore.isLoggedIn) {
try { try {
// store const response = await axios.get(`/api/follow/check?followUserId=${userId.value}`);
await postListStore.getList({ if (response.data.code === 0) {
lastVal: postListStore.lastVal, isFollowing.value = response.data.data;
offset: postListStore.offset, }
size: postListStore.pageSize
});
} catch (error) { } catch (error) {
console.error("加载帖子失败:", error); console.error('检查关注状态失败:', error);
} }
} }
}; };
onMounted(async () => { // /
isComponentMounted.value = true; const toggleFollow = async () => {
if (!userStore.isLoggedIn) {
router.push('/');
return;
}
try { try {
// 使 store const url = isFollowing.value
await postListStore.getList({ ? `/api/follow/cancel?followUserId=${userId.value}`
lastVal: postListStore.lastVal, : `/api/follow/add?followUserId=${userId.value}`;
offset: postListStore.offset,
size: postListStore.pageSize const response = await axios.post(url);
});
if (response.data.code === 0) {
isFollowing.value = !isFollowing.value;
//
loadUserStats();
}
} catch (error) { } catch (error) {
console.error("初始化加载失败:", error); console.error('操作关注失败:', error);
} }
};
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => { //
isComponentMounted.value = false; const openChat = () => {
window.removeEventListener('scroll', handleScroll); //
console.log('打开与用户', userId.value, '的聊天');
};
//
const loadPrivacySettings = async () => {
try {
const response = await axios.get('/api/user/privacy');
if (response.data.code === 0) {
privacySettings.value = response.data.data;
}
} catch (error) {
console.error('加载隐私设置失败:', error);
}
};
//
const updatePrivacy = async () => {
try {
await axios.post('/api/user/privacy', privacySettings.value);
} catch (error) {
console.error('更新隐私设置失败:', error);
}
};
//
const loadRecommendations = async () => {
if (!userStore.isLoggedIn) return;
// try {
// postListStore.resetList(); const response = await axios.get('/api/user/recommend', {
}); params: { limit: 5 }
});
if (response.data.code === 0) {
recommendations.value = response.data.data || [];
}
} catch (error) {
console.error('加载推荐失败:', error);
}
};
// //
const viewUser = (id) => {
router.push({ name: 'UserPage', params: { userId: id } });
};
//
const goToPostDetail = (postId) => { const goToPostDetail = (postId) => {
router.push({ router.push({
name: 'PostDetail', name: 'PostDetail',
params: { id: postId }, params: { id: postId }
state: { from: 'user' }
}); });
}; };
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
const now = new Date();
const diff = now - date;
// 1
if (diff < 24 * 60 * 60 * 1000) {
const hours = Math.floor(diff / (60 * 60 * 1000));
if (hours < 1) {
const minutes = Math.floor(diff / (60 * 1000));
return minutes <= 0 ? '刚刚' : `${minutes}分钟前`;
}
return `${hours}小时前`;
}
// 7
if (diff < 7 * 24 * 60 * 60 * 1000) {
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
return `${days}天前`;
}
//
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
};
onMounted(async () => {
await loadUserInfo();
loadUserPosts();
loadUserStats();
checkFollowStatus();
loadRecommendations();
});
onUnmounted(() => {
//
// postListStore.resetList();
});
</script> </script>
<style scoped> <style scoped>
.user-page-container { .user-page-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
@ -168,7 +409,7 @@ const goToPostDetail = (postId) => {
padding: 20px; padding: 20px;
background-color: #f5f5f5; background-color: #f5f5f5;
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 80px auto 0; /* 顶部边距,避免被导航栏遮挡 */
box-sizing: border-box; box-sizing: border-box;
} }
@ -184,121 +425,280 @@ const goToPostDetail = (postId) => {
} }
.user-info-card { .user-info-card {
background-color: #fff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
padding: 24px;
display: flex; display: flex;
align-items: center; margin-bottom: 20px;
gap: 20px; }
padding: 20px;
background-color: white; .user-avatar {
border-radius: 8px; margin-right: 24px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.user-avatar img { .user-avatar img {
width: 80px; width: 120px;
height: 80px; height: 120px;
border-radius: 50%; border-radius: 60px;
object-fit: cover;
border: 4px solid #f0f5ff;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
} }
.user-details { .user-details {
display: flex; flex: 1;
flex-direction: column; }
align-items: flex-start;
.username {
font-size: 24px;
margin: 0 0 5px 0;
color: #333;
font-weight: 600;
} }
.user-info-list { .user-info-list {
margin-top: 8px; margin-top: 15px;
width: 100%;
} }
.user-info-row { .user-info-item {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 4px; margin-bottom: 12px;
padding: 8px 10px;
background-color: #f8f9fa;
border-radius: 6px;
transition: all 0.3s ease;
} }
.user-info-label { .user-info-item:hover {
font-weight: bold; background-color: #eef5fe;
}
.info-icon {
margin-right: 10px;
color: #409eff;
width: 20px;
text-align: center;
}
.info-label {
width: 60px;
color: #606266;
font-weight: 500;
}
.info-value {
flex: 1;
color: #303133;
}
.privacy-toggles {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 5px;
}
.privacy-switch {
margin-right: 15px;
}
.social-actions {
margin-top: 20px;
display: flex;
gap: 10px;
}
/* 用户帖子区域 */
.user-posts {
background-color: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-posts h3 {
margin-top: 0;
margin-bottom: 16px;
color: #333; color: #333;
min-width: 48px; font-size: 18px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
} }
.user-info-value { .no-posts {
color: #444; color: #999;
text-align: center;
padding: 20px 0;
} }
.username { .post-item {
font-size: 24px; padding: 15px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.post-item:last-child {
border-bottom: none;
}
.post-item:hover {
background-color: #f9f9f9;
}
.post-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.post-title {
margin: 0; margin: 0;
font-size: 16px;
color: #333;
} }
.user-status { .post-time {
color: #54ac52; font-size: 12px;
font-weight: bold; color: #999;
margin: 8px 0;
} }
.user-motto { .post-summary {
color: #666; color: #666;
font-size: 14px; font-size: 14px;
margin: 8px 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.post-stats {
display: flex;
gap: 15px;
font-size: 12px;
color: #999;
}
/* 加载更多 */
.loading-more, .load-more, .no-more {
text-align: center;
padding: 15px 0;
color: #999;
font-size: 14px;
} }
.load-more {
cursor: pointer;
color: #54ac52;
}
.load-more:hover {
text-decoration: underline;
}
/* 用户统计卡片 */
.user-stats-card { .user-stats-card {
padding: 20px; padding: 20px;
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
}
.user-stats-card h3 {
margin-top: 0;
margin-bottom: 16px;
color: #333;
font-size: 18px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
} }
.stats { .stats {
display: flex; display: grid;
flex-direction: column; grid-template-columns: repeat(2, 1fr);
gap: 10px; gap: 15px;
margin-top: 10px;
} }
.stat-item { .stat-item {
display: flex; text-align: center;
justify-content: space-between; padding: 10px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #54ac52;
}
.stat-label {
font-size: 14px; font-size: 14px;
color: #333; color: #666;
margin-top: 5px;
} }
.user-posts { /* 推荐用户 */
.recommendations {
padding: 20px; padding: 20px;
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.post-item { .recommendations h3 {
padding: 10px 0; margin-top: 0;
margin-bottom: 16px;
color: #333;
font-size: 18px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.recommendation-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.recommendation-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 4px;
cursor: pointer; cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s; transition: background-color 0.2s;
} }
.post-item:hover { .recommendation-item:hover {
transform: translateY(-2px); background-color: #f5f5f5;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1);
} }
.post-title { .recommendation-avatar {
font-size: 18px; width: 40px;
font-weight: bold; height: 40px;
margin: 0 0 8px 0; border-radius: 50%;
color: #2c3e50; object-fit: cover;
} }
.post-summary { .recommendation-info {
flex: 1;
}
.recommendation-name {
font-size: 14px; font-size: 14px;
color: #666; font-weight: bold;
margin-bottom: 8px; color: #333;
} }
.post-stats { .recommendation-followers {
display: flex;
gap: 15px;
font-size: 12px; font-size: 12px;
color: #999; color: #999;
margin-top: 4px;
} }
</style> </style>

@ -0,0 +1,373 @@
<template>
<div class="admin-categories">
<div class="category-header">
<h3>分类管理</h3>
<el-button type="primary" @click="showAddDialog">
<el-icon><Plus /></el-icon>
</el-button>
</div>
<!-- 分类表格 -->
<el-table
v-loading="loading"
:data="categoryList"
style="width: 100%"
border
stripe
>
<el-table-column type="index" width="60" align="center" label="序号" />
<el-table-column prop="name" label="分类名称" min-width="180" />
<el-table-column prop="description" label="分类描述" min-width="250" />
<el-table-column label="图标" width="100" align="center">
<template #default="{ row }">
<el-avatar
v-if="row.icon"
:size="40"
:src="row.icon"
fit="cover"
/>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="sort" label="排序" width="100" align="center" sortable />
<el-table-column prop="postCount" label="帖子数量" width="100" align="center" />
<el-table-column prop="createTime" label="创建时间" width="180" sortable />
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button size="small" type="primary" @click="handleEdit(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]"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 添加/编辑分类对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑分类' : '添加分类'"
width="500px"
>
<el-form
ref="categoryFormRef"
:model="categoryForm"
:rules="rules"
label-width="100px"
>
<el-form-item label="分类名称" prop="name">
<el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
</el-form-item>
<el-form-item label="分类描述" prop="description">
<el-input
v-model="categoryForm.description"
type="textarea"
:rows="3"
placeholder="请输入分类描述"
/>
</el-form-item>
<el-form-item label="分类图标">
<el-upload
class="avatar-uploader"
action="/api/upload"
:show-file-list="false"
:on-success="handleUploadSuccess"
>
<img
v-if="categoryForm.icon"
:src="categoryForm.icon"
class="avatar"
/>
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="icon-tip">支持jpgpng格式建议尺寸100x100px</div>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="categoryForm.sort"
:min="1"
:max="999"
:step="1"
/>
<div class="sort-tip">数字越小排序越靠前</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm"></el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { Plus } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';
//
const categoryList = ref([]);
//
const loading = ref(false);
//
const pagination = reactive({
page: 1,
size: 10,
total: 0
});
//
const dialogVisible = ref(false);
//
const isEdit = ref(false);
//
const categoryFormRef = ref(null);
//
const categoryForm = reactive({
id: null,
name: '',
description: '',
icon: '',
sort: 1
});
//
const rules = {
name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
],
description: [
{ max: 200, message: '长度不能超过 200 个字符', trigger: 'blur' }
],
sort: [
{ required: true, message: '请输入排序值', trigger: 'blur' }
]
};
//
const loadCategories = async () => {
try {
loading.value = true;
const response = await axios.get('/api/admin/categories', {
params: {
page: pagination.page,
size: pagination.size
}
});
if (response.data.code === 0) {
const data = response.data.data;
categoryList.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 handleSizeChange = (size) => {
pagination.size = size;
loadCategories();
};
//
const handleCurrentChange = (page) => {
pagination.page = page;
loadCategories();
};
//
const showAddDialog = () => {
isEdit.value = false;
resetForm();
dialogVisible.value = true;
};
//
const handleEdit = (row) => {
isEdit.value = true;
resetForm();
Object.assign(categoryForm, row);
dialogVisible.value = true;
};
//
const handleDelete = async (row) => {
try {
const response = await axios.delete(`/api/admin/categories/${row.id}`);
if (response.data.code === 0) {
ElMessage.success('分类删除成功');
loadCategories();
} else {
ElMessage.error(response.data.msg || '删除失败');
}
} catch (error) {
console.error('删除分类失败:', error);
ElMessage.error('网络错误,请稍后重试');
}
};
//
const handleUploadSuccess = (response) => {
if (response.code === 0) {
categoryForm.icon = response.data.url;
} else {
ElMessage.error('上传图片失败');
}
};
//
const resetForm = () => {
categoryForm.id = null;
categoryForm.name = '';
categoryForm.description = '';
categoryForm.icon = '';
categoryForm.sort = 1;
//
if (categoryFormRef.value) {
categoryFormRef.value.resetFields();
}
};
//
const submitForm = async () => {
if (!categoryFormRef.value) return;
try {
await categoryFormRef.value.validate();
const url = isEdit.value
? `/api/admin/categories/${categoryForm.id}`
: '/api/admin/categories';
const method = isEdit.value ? 'put' : 'post';
const response = await axios[method](url, categoryForm);
if (response.data.code === 0) {
ElMessage.success(isEdit.value ? '分类更新成功' : '分类添加成功');
dialogVisible.value = false;
loadCategories();
} else {
ElMessage.error(response.data.msg || '操作失败');
}
} catch (error) {
console.error('保存分类失败:', error);
ElMessage.error('网络错误,请稍后重试');
}
};
//
onMounted(() => {
loadCategories();
});
</script>
<style scoped>
.admin-categories {
width: 100%;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.category-header h3 {
margin: 0;
font-size: 18px;
font-weight: 500;
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
.avatar-uploader {
width: 100px;
height: 100px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
text-align: center;
}
.avatar-uploader:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
}
.avatar {
width: 100px;
height: 100px;
display: block;
}
.icon-tip,
.sort-tip {
font-size: 12px;
color: #999;
margin-top: 5px;
}
</style>

@ -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,577 @@
<template>
<div class="admin-overview">
<div class="stat-cards">
<div class="stat-card">
<div class="stat-icon users-icon">👥</div>
<div class="stat-content">
<h3>在线用户</h3>
<div class="stat-value">{{ stats.onlineUsers }}</div>
<div class="stat-description">当前在线用户数</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon posts-icon">📝</div>
<div class="stat-content">
<h3>帖子总数</h3>
<div class="stat-value">{{ stats.totalPosts }}</div>
<div class="stat-description">
今日新增:
<span class="text-success">+{{ stats.todayPosts }}</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon comments-icon">💬</div>
<div class="stat-content">
<h3>评论总数</h3>
<div class="stat-value">{{ stats.totalComments }}</div>
<div class="stat-description">
今日新增:
<span class="text-success">+{{ stats.todayComments }}</span>
</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon users-total-icon">👤</div>
<div class="stat-content">
<h3>用户总数</h3>
<div class="stat-value">{{ stats.totalUsers }}</div>
<div class="stat-description">
今日注册:
<span class="text-success">+{{ stats.todayUsers }}</span>
</div>
</div>
</div>
</div>
<div class="admin-charts">
<div class="chart-card">
<div class="card-header">
<h3>过去7天活跃度</h3>
</div>
<div class="chart-container">
<!-- 简单图表展示 - 实际中可以使用ECharts等库 -->
<div class="simple-chart">
<div
v-for="(value, day) in activityData"
:key="day"
class="chart-bar"
:style="{ height: `${value * 2}px` }"
:title="`${day}: ${value}人`"
></div>
</div>
<div class="chart-labels">
<span v-for="day in Object.keys(activityData)" :key="day">{{ day }}</span>
</div>
</div>
</div>
<div class="chart-card">
<div class="card-header">
<h3>分类数据统计</h3>
</div>
<div class="category-stats">
<table class="stats-table">
<thead>
<tr>
<th>分类名称</th>
<th>帖子数量</th>
<th>占比</th>
</tr>
</thead>
<tbody>
<tr v-for="(category, index) in categoryStats" :key="index">
<td>{{ category.name }}</td>
<td>{{ category.count }}</td>
<td>
<div class="progress-bar">
<div
class="progress-fill"
:style="{ width: `${category.percentage}%` }"
></div>
<span>{{ category.percentage }}%</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="admin-tables">
<div class="table-card">
<div class="card-header">
<h3>最新帖子</h3>
<router-link to="/admin/posts" class="view-all">查看全部</router-link>
</div>
<table class="data-table">
<thead>
<tr>
<th>标题</th>
<th>作者</th>
<th>分类</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(post, index) in latestPosts" :key="index">
<td class="title-cell">{{ post.title }}</td>
<td>{{ post.author }}</td>
<td>{{ post.category }}</td>
<td>{{ formatDate(post.createTime) }}</td>
<td>
<button class="action-btn view-btn" @click="viewPost(post.id)"></button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="table-card">
<div class="card-header">
<h3>最新用户</h3>
<router-link to="/admin/users" class="view-all">查看全部</router-link>
</div>
<table class="data-table">
<thead>
<tr>
<th>用户名</th>
<th>注册时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in latestUsers" :key="index">
<td>{{ user.username }}</td>
<td>{{ formatDate(user.createTime) }}</td>
<td>
<span
class="status-badge"
:class="user.status === 1 ? 'status-active' : 'status-frozen'"
>
{{ user.status === 1 ? '正常' : '冻结' }}
</span>
</td>
<td>
<button class="action-btn view-btn" @click="viewUser(user.id)"></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
const router = useRouter();
//
const stats = ref({
onlineUsers: 0,
totalPosts: 0,
todayPosts: 0,
totalComments: 0,
todayComments: 0,
totalUsers: 0,
todayUsers: 0
});
//
const activityData = ref({
'周一': 45,
'周二': 32,
'周三': 38,
'周四': 51,
'周五': 68,
'周六': 82,
'周日': 56
});
//
const categoryStats = ref([
{ name: '学习', count: 156, percentage: 45 },
{ name: '娱乐', count: 89, percentage: 25 },
{ name: '二手交易', count: 67, percentage: 20 },
{ name: '求助', count: 34, percentage: 10 }
]);
//
const latestPosts = ref([]);
//
const latestUsers = ref([]);
//
const loadDashboardData = async () => {
try {
//
const statsResponse = await axios.get('/api/admin/stats');
if (statsResponse.data.code === 0) {
stats.value = statsResponse.data.data;
}
//
const postsResponse = await axios.get('/api/admin/posts/latest');
if (postsResponse.data.code === 0) {
latestPosts.value = postsResponse.data.data;
}
//
const usersResponse = await axios.get('/api/admin/users/latest');
if (usersResponse.data.code === 0) {
latestUsers.value = usersResponse.data.data;
}
//
simulateData();
} catch (error) {
console.error('加载数据失败', error);
}
};
// 使API
const simulateData = () => {
stats.value = {
onlineUsers: 238,
totalPosts: 1356,
todayPosts: 24,
totalComments: 8754,
todayComments: 132,
totalUsers: 4568,
todayUsers: 15
};
latestPosts.value = [
{ id: 1, title: '武汉大学2023年度招生简章', author: '大学新闻', category: '学习', createTime: '2023-06-15T08:30:00' },
{ id: 2, title: '求推荐计算机专业课程的参考书', author: 'CS爱好者', category: '学习', createTime: '2023-06-14T16:45:00' },
{ id: 3, title: '樱花节摄影作品分享', author: '摄影达人', category: '娱乐', createTime: '2023-06-14T14:20:00' },
{ id: 4, title: '出售全新未拆封的iPhone 14', author: '数码控', category: '二手交易', createTime: '2023-06-14T10:15:00' },
{ id: 5, title: '武大食堂美食推荐', author: '吃货代表', category: '生活', createTime: '2023-06-13T19:40:00' }
];
latestUsers.value = [
{ id: 101, username: '新同学2023', createTime: '2023-06-15T09:20:00', status: 1 },
{ id: 102, username: '学习达人', createTime: '2023-06-14T18:30:00', status: 1 },
{ id: 103, username: '校园生活家', createTime: '2023-06-14T15:45:00', status: 1 },
{ id: 104, username: '毕业季节', createTime: '2023-06-14T12:10:00', status: 2 },
{ id: 105, username: '樱花摄影', createTime: '2023-06-13T20:25:00', status: 1 }
];
};
//
const viewPost = (id) => {
router.push(`/post/${id}`);
};
//
const viewUser = (id) => {
router.push(`/admin/users?id=${id}`);
};
//
const formatDate = (dateString) => {
if (!dateString) return '';
const date = new Date(dateString);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
};
onMounted(() => {
loadDashboardData();
});
</script>
<style scoped>
.admin-overview {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 统计卡片样式 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
gap: 15px;
}
.stat-icon {
width: 50px;
height: 50px;
border-radius: 10px;
background-color: #e8f5e9;
color: #54ac52;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.users-icon {
background-color: #e3f2fd;
color: #1976d2;
}
.posts-icon {
background-color: #f9fbe7;
color: #afb42b;
}
.comments-icon {
background-color: #fff8e1;
color: #ffb300;
}
.users-total-icon {
background-color: #e8eaf6;
color: #3f51b5;
}
.stat-content {
flex: 1;
}
.stat-content h3 {
margin: 0 0 5px 0;
font-size: 14px;
color: #666;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 5px;
}
.stat-description {
font-size: 12px;
color: #888;
}
.text-success {
color: #4caf50;
}
/* 图表卡片样式 */
.admin-charts {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.chart-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 16px;
color: #333;
}
.chart-container {
height: 200px;
display: flex;
flex-direction: column;
}
/* 简易图表样式 */
.simple-chart {
flex: 1;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 8px;
padding-bottom: 10px;
}
.chart-bar {
flex: 1;
background-color: #54ac52;
border-radius: 3px 3px 0 0;
transition: height 0.5s;
}
.chart-labels {
display: flex;
justify-content: space-between;
}
.chart-labels span {
flex: 1;
text-align: center;
font-size: 12px;
color: #666;
}
/* 分类统计表格 */
.category-stats {
max-height: 200px;
overflow-y: auto;
}
.stats-table {
width: 100%;
border-collapse: collapse;
}
.stats-table th, .stats-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
.stats-table th {
font-weight: bold;
color: #333;
background-color: #f9f9f9;
}
.progress-bar {
height: 10px;
background-color: #f1f1f1;
border-radius: 5px;
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.progress-fill {
height: 100%;
background-color: #54ac52;
border-radius: 5px;
position: absolute;
left: 0;
top: 0;
}
.progress-bar span {
position: relative;
font-size: 12px;
margin-left: 10px;
color: #666;
}
/* 数据表格样式 */
.admin-tables {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.table-card {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.view-all {
color: #54ac52;
font-size: 14px;
text-decoration: none;
}
.view-all:hover {
text-decoration: underline;
}
.data-table {
width: 100%;
border-collapse: collapse;
}
.data-table th, .data-table td {
padding: 10px;
text-align: left;
border-bottom: 1px solid #eee;
}
.data-table th {
font-weight: bold;
color: #333;
background-color: #f9f9f9;
}
.title-cell {
max-width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.status-badge {
padding: 4px 8px;
border-radius: 10px;
font-size: 12px;
font-weight: bold;
}
.status-active {
background-color: #e8f5e9;
color: #2e7d32;
}
.status-frozen {
background-color: #ffebee;
color: #c62828;
}
.action-btn {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s;
}
.view-btn {
background-color: #e3f2fd;
color: #1976d2;
}
.view-btn:hover {
background-color: #bbdefb;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.stat-cards, .admin-charts, .admin-tables {
grid-template-columns: 1fr;
}
}
</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…
Cancel
Save