package com.example.springboot.controller;


import com.example.springboot.entity.Forum.*;
import com.example.springboot.service.ForumService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/forum")
public class ForumController {

    @Resource
    private ForumService forumService;

    // 发布帖子
    @PostMapping("/post")
    public String createPost(@RequestBody Post post) {
        int result = forumService.createPost(post);
        return result > 0 ? "Post created successfully" : "Post creation failed";
    }

    // 删除帖子（普通用户需要在业务层验证是否本人）
    @DeleteMapping("/post/{id}")
    public String deletePost(@PathVariable Integer id) {
        int result = forumService.deletePost(id);
        return result > 0 ? "Post deleted successfully" : "Post deletion failed";
    }

    // 修改帖子
    @PutMapping("/post")
    public String updatePost(@RequestBody Post post) {
        int result = forumService.updatePost(post);
        return result > 0 ? "Post updated successfully" : "Post update failed";
    }

    // 根据帖子ID获取帖子详情
    @GetMapping("/post/{id}")
    public Post getPostById(@PathVariable Integer id) {
        return forumService.getPostById(id);
    }

    // 模糊查询帖子标题
    @GetMapping("/posts")
    public List<Post> searchPostsByTitle(@RequestParam String title) {
        return forumService.searchPostsByTitle(title);
    }

    // 新增评论
    @PostMapping("/comment")
    public String createComment(@RequestBody Comment comment) {
        int result = forumService.createComment(comment);
        return result > 0 ? "Comment added successfully" : "Comment adding failed";
    }

    // 删除评论
    @DeleteMapping("/comment/{id}")
    public String deleteComment(@PathVariable Integer id) {
        int result = forumService.deleteComment(id);
        return result > 0 ? "Comment deleted successfully" : "Comment deletion failed";
    }

    // 修改评论
    @PutMapping("/comment")
    public String updateComment(@RequestBody Comment comment) {
        int result = forumService.updateComment(comment);
        return result > 0 ? "Comment updated successfully" : "Comment update failed";
    }

    // 获取某帖子的评论列表（包含回复）
    @GetMapping("/comments/{postId}")
    public List<Comment> getCommentsByPostId(@PathVariable Integer postId) {
        return forumService.getCommentsByPostId(postId);
    }

    // 点赞帖子
    @PostMapping("/post/like")
    public String likePost(@RequestBody LikePost likePost) {
        int result = forumService.createLikePost(likePost);
        return result > 0 ? "点赞成功" : "已经点赞过或点赞失败";
    }

    // 取消点赞
    @DeleteMapping("/post/like")
    public String unlikePost(@RequestParam Integer userId, @RequestParam Integer postId) {
        int result = forumService.deleteLikePost(userId, postId);
        return result > 0 ? "取消点赞成功" : "取消点赞失败";
    }

    // 收藏帖子
    @PostMapping("/post/favorite")
    public String favoritePost(@RequestBody FavoritePost favoritePost) {
        int result = forumService.createFavoritePost(favoritePost);
        return result > 0 ? "收藏成功" : "收藏失败或已收藏";
    }

    // 取消收藏
    @DeleteMapping("/post/favorite")
    public String unfavoritePost(@RequestParam Integer userId, @RequestParam Integer postId) {
        int result = forumService.deleteFavoritePost(userId, postId);
        return result > 0 ? "取消收藏成功" : "取消收藏失败";
    }

    // 举报帖子
    @PostMapping("/post/report")
    public String reportPost(@RequestBody ReportPost reportPost) {
        int result = forumService.createReportPost(reportPost);
        return result > 0 ? "举报成功" : "举报失败";
    }

    // 管理员查看举报
    @GetMapping("/post/reports/{postId}")
    public List<ReportPost> getReportsByPostId(@PathVariable Integer postId) {
        return forumService.getReportPostsByPostId(postId);
    }

    @GetMapping("/user/{userId}/posts")
    public List<Post> getPostsByUserId(@PathVariable Integer userId) {
        List<Post> posts = forumService.getPostsByUserId(userId);

        // 这里可以查询并设置每个帖子的点赞数，点赞和收藏状态，比如（简单示例，效率不错的话可DAO一次搞定）：
        posts.forEach(post -> {
            int likeCount = forumService.getLikeCountByPostId(post.getId());


            // 查询当前用户是否点赞/收藏，改成传参可改
            // 这里暂时userId是查询者，请改成当前登录用户id
            LikePost likePost = forumService.getLikePost(userId, post.getId());
            post.setLiked(likePost != null);

            FavoritePost favPost = forumService.getFavoritePost(userId, post.getId());
            post.setFavorited(favPost != null);
        });
        return posts;
    }
    /**
     * 获取用户点赞过的帖子列表
     */
    @GetMapping("/user/{userId}/liked-posts")
    public List<Post> getLikedPostsByUserId(@PathVariable Integer userId) {
        return forumService.getLikedPostsByUserId(userId);
    }

    /**
     * 获取用户收藏过的帖子列表
     */
    @GetMapping("/user/{userId}/favorited-posts")
    public List<Post> getFavoritedPostsByUserId(@PathVariable Integer userId) {
        return forumService.getFavoritedPostsByUserId(userId);
    }

    @GetMapping("/post/like/status")
    public Map<String, Boolean> getLikeStatus(@RequestParam Integer userId, @RequestParam Integer postId) {
        boolean liked = forumService.getLikePost(userId, postId) != null;
        return Collections.singletonMap("liked", liked);
    }

    @GetMapping("/post/favorite/status")
    public Map<String, Boolean> getFavoriteStatus(@RequestParam Integer userId, @RequestParam Integer postId) {
        boolean favorited = forumService.getFavoritePost(userId, postId) != null;
        return Collections.singletonMap("favorited", favorited);
    }
    // 获取所有举报列表，支持分页
    @GetMapping("/post/reports")
    public List<ReportPost> getAllReports(
            @RequestParam(required = false, defaultValue = "0") Integer page,
            @RequestParam(required = false, defaultValue = "20") Integer size) {
        return forumService.getAllReportPosts(page, size);
    }
}
package com.example.springboot.entity.Forum;


import java.time.LocalDateTime;

// 评论实体
public class Comment {
    private Integer id;
    private Integer postId;
    private Integer userId;
    private Integer parentCommentId;  // null表示一级评论
    private String content;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String username;

    // getter和setter


    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Integer getPostId() {
        return postId;
    }
    public void setPostId(Integer postId) {
        this.postId = postId;
    }
    public Integer getUserId() {
        return userId;
    }
    public void setUserId(Integer userId) {
        this.userId = userId;
    }
    public Integer getParentCommentId() {
        return parentCommentId;
    }
    public void setParentCommentId(Integer parentCommentId) {
        this.parentCommentId = parentCommentId;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }
    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
}
package com.example.springboot.entity.Forum;

import java.util.Date;

public class FavoritePost {
    private Integer id;
    private Integer userId;
    private Integer postId;
    private Date createdAt;

    // getter/setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Integer getUserId() { return userId; }
    public void setUserId(Integer userId) { this.userId = userId; }
    public Integer getPostId() { return postId; }
    public void setPostId(Integer postId) { this.postId = postId; }
    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
}
package com.example.springboot.entity.Forum;

import java.util.Date;

public class LikePost {
    private Integer id;
    private Integer userId;
    private Integer postId;
    private Date createdAt;

    // getter/setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Integer getUserId() { return userId; }
    public void setUserId(Integer userId) { this.userId = userId; }
    public Integer getPostId() { return postId; }
    public void setPostId(Integer postId) { this.postId = postId; }
    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
}
package com.example.springboot.entity.Forum;


import java.time.LocalDateTime;

// 帖子实体
public class Post {
    private Integer id;
    private Integer userId;
    private String title;
    private String content;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
    private String username;
    private Boolean liked;       // 是否被当前用户点赞
    private Boolean favorited;   // 是否被当前用户收藏

    // getter和setter


    private Integer likeCount;

    public Integer getLikeCount() {
        return likeCount;
    }
    public void setLikeCount(Integer likeCount) {
        this.likeCount = likeCount;
    }

    public Boolean getLiked() {
        return liked;
    }

    public void setLiked(Boolean liked) {
        this.liked = liked;
    }

    public Boolean getFavorited() {
        return favorited;
    }

    public void setFavorited(Boolean favorited) {
        this.favorited = favorited;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Integer getUserId() {
        return userId;
    }
    public void setUserId(Integer userId) {
        this.userId = userId;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public LocalDateTime getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
    }
    public LocalDateTime getUpdatedAt() {
        return updatedAt;
    }
    public void setUpdatedAt(LocalDateTime updatedAt) {
        this.updatedAt = updatedAt;
    }
}
package com.example.springboot.entity.Forum;

import java.util.Date;

public class ReportPost {
    private Integer id;
    private Integer reporterId;
    private Integer postId;
    private String reason;
    private Date createdAt;


    // 额外字段（非数据库表主字段，供查询用）
    private String reporterUsername;  // 举报人用户名
    private String postTitle;         // 被举报帖子标题

    public String getReporterUsername() {
        return reporterUsername;
    }

    public void setReporterUsername(String reporterUsername) {
        this.reporterUsername = reporterUsername;
    }

    public String getPostTitle() {
        return postTitle;
    }

    public void setPostTitle(String postTitle) {
        this.postTitle = postTitle;
    }

    // getter/setter
    public Integer getId() { return id; }
    public void setId(Integer id) { this.id = id; }
    public Integer getReporterId() { return reporterId; }
    public void setReporterId(Integer reporterId) { this.reporterId = reporterId; }
    public Integer getPostId() { return postId; }
    public void setPostId(Integer postId) { this.postId = postId; }
    public String getReason() { return reason; }
    public void setReason(String reason) { this.reason = reason; }
    public Date getCreatedAt() { return createdAt; }
    public void setCreatedAt(Date createdAt) { this.createdAt = createdAt; }
}
package com.example.springboot.mapper;

import com.example.springboot.entity.Forum.*;


import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface ForumMapper {
    // --- 帖子相关 ---

    // 新增帖子
    int insertPost(Post post);

    // 删除帖子（管理员可删任意，用户删自己的可通过业务层判断）
    int deletePostById(@Param("id") Integer id);

    // 更新帖子（只改标题和内容）
    int updatePost(Post post);

    // 根据id查询帖子
    Post selectPostById(@Param("id") Integer id);

    // 模糊搜索帖子标题，支持分页
    List<Post> selectPostsByTitleLike(@Param("title") String title);


    // --- 评论相关 ---

    // 新增评论
    int insertComment(Comment comment);

    // 删除评论（只能删除自己评论，管理员权限由业务层判断）
    int deleteCommentById(@Param("id") Integer id);

    // 修改评论内容
    int updateComment(Comment comment);

    // 查询某帖子下的所有评论（含回复）
    List<Comment> selectCommentsByPostId(@Param("postId") Integer postId);


    // --- 评论相关 ---

    // 点赞相关
    int insertLikePost(LikePost likePost);

    // 删除点赞
    int deleteLikePost(@Param("userId") Integer userId, @Param("postId") Integer postId);

    // 查询某用户是否已点赞该帖子
    LikePost selectLikePost(@Param("userId") Integer userId, @Param("postId") Integer postId);

    // 收藏相关
    int insertFavoritePost(FavoritePost favoritePost);

    int deleteFavoritePost(@Param("userId") Integer userId, @Param("postId") Integer postId);

    FavoritePost selectFavoritePost(@Param("userId") Integer userId, @Param("postId") Integer postId);

    // 举报相关
    int insertReportPost(ReportPost reportPost);

    // 查询某帖子的举报记录（供管理员查看）
    List<ReportPost> selectReportPostsByPostId(@Param("postId") Integer postId);

    List<Post> selectPostsByUserId(@Param("userId") Integer userId);

    int selectLikeCountByPostId(@Param("postId") Integer postId);

    /**
     * 查询用户点赞过的帖子列表（关联post和like_post表）
     */
    List<Post> selectLikedPostsByUserId(@Param("userId") Integer userId);

    /**
     * 查询用户收藏过的帖子列表（关联post和favorite_post表）
     */
    List<Post> selectFavoritedPostsByUserId(@Param("userId") Integer userId);

    List<ReportPost> selectAllReportPosts(@Param("offset") int offset, @Param("limit") int limit);
}
package com.example.springboot.service;

import com.example.springboot.entity.Forum.*;
import com.example.springboot.mapper.ForumMapper;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class ForumService {

    @Resource
    private ForumMapper forumMapper;

    // ---------------- 帖子相关 ---------------- //

    public int createPost(Post post) {
        return forumMapper.insertPost(post);
    }

    public int deletePost(Integer postId) {
        return forumMapper.deletePostById(postId);
    }

    public int updatePost(Post post) {
        return forumMapper.updatePost(post);
    }

    /**
     * 根据帖子ID查询帖子详情，注意likeCount是从 like_post 表动态统计的，需要你的Mapper支持
     * 建议对应 Mapper 查询中联表统计点赞数
     */
    public Post getPostById(Integer postId) {
        return forumMapper.selectPostById(postId);
    }

    public List<Post> searchPostsByTitle(String title) {
        return forumMapper.selectPostsByTitleLike(title);
    }

    /**
     * 查询某用户的帖子列表
     * 建议 SQL 联表统计点赞数
     */
    public List<Post> getPostsByUserId(Integer userId) {
        List<Post> posts = forumMapper.selectPostsByUserId(userId);
        // 补充点赞状态和收藏状态等
        enrichPostsWithLikeFavorite(userId, posts);
        return posts;
    }


    // ---------------- 评论相关 ---------------- //

    public int createComment(Comment comment) {
        return forumMapper.insertComment(comment);
    }

    public int deleteComment(Integer commentId) {
        return forumMapper.deleteCommentById(commentId);
    }

    public int updateComment(Comment comment) {
        return forumMapper.updateComment(comment);
    }

    public List<Comment> getCommentsByPostId(Integer postId) {
        return forumMapper.selectCommentsByPostId(postId);
    }


    // ---------------- 点赞相关 ---------------- //

    /**
     * 点赞某帖子，重复点赞会返回0表示已点赞
     * 只往 like_post 插入记录，不修改 post 表
     */
    public int createLikePost(LikePost likePost) {
        LikePost exist = forumMapper.selectLikePost(likePost.getUserId(), likePost.getPostId());
        if (exist != null) return 0;
        return forumMapper.insertLikePost(likePost);
    }

    /**
     * 取消点赞，删除 like_post 记录，不修改 post 表
     */
    public int deleteLikePost(Integer userId, Integer postId) {
        return forumMapper.deleteLikePost(userId, postId);
    }

    public LikePost getLikePost(Integer userId, Integer postId) {
        return forumMapper.selectLikePost(userId, postId);
    }

    /**
     * 查询某帖子点赞数量，动态查询 like_post 表
     */
    public int getLikeCountByPostId(Integer postId) {
        return forumMapper.selectLikeCountByPostId(postId);
    }


    // ---------------- 收藏相关 ---------------- //

    public int createFavoritePost(FavoritePost favoritePost) {
        FavoritePost exist = forumMapper.selectFavoritePost(favoritePost.getUserId(), favoritePost.getPostId());
        if (exist != null) return 0;
        return forumMapper.insertFavoritePost(favoritePost);
    }

    public int deleteFavoritePost(Integer userId, Integer postId) {
        return forumMapper.deleteFavoritePost(userId, postId);
    }

    public FavoritePost getFavoritePost(Integer userId, Integer postId) {
        return forumMapper.selectFavoritePost(userId, postId);
    }


    // ---------------- 举报相关 ---------------- //

    public int createReportPost(ReportPost reportPost) {
        // 可加重复举报校验，暂略
        return forumMapper.insertReportPost(reportPost);
    }

    public List<ReportPost> getReportPostsByPostId(Integer postId) {
        return forumMapper.selectReportPostsByPostId(postId);
    }


    // ---------------- 用户点赞和收藏的帖子列表 ---------------- //

    /**
     * 查询用户点赞过的帖子列表
     */
    public List<Post> getLikedPostsByUserId(Integer userId) {
        List<Post> posts = forumMapper.selectLikedPostsByUserId(userId);
        enrichPostsWithLikeFavorite(userId, posts);
        return posts;
    }

    /**
     * 查询用户收藏过的帖子列表
     */
    public List<Post> getFavoritedPostsByUserId(Integer userId) {
        List<Post> posts = forumMapper.selectFavoritedPostsByUserId(userId);
        enrichPostsWithLikeFavorite(userId, posts);
        return posts;
    }


    /**
     * 给帖子列表补全点赞数，点赞状态，收藏状态
     */
    private void enrichPostsWithLikeFavorite(Integer userId, List<Post> posts) {
        if (posts == null || posts.isEmpty()) return;

        boolean isLikedList = true;

        for (Post post : posts) {
            int likeCount = forumMapper.selectLikeCountByPostId(post.getId());
            post.setLikeCount(likeCount);

            if(isLikedList) {
                post.setLiked(true); // 对于我点赞的帖子的列表，这里直接赋true
            } else {
                LikePost likePost = forumMapper.selectLikePost(userId, post.getId());
                post.setLiked(likePost != null);
            }

            FavoritePost favPost = forumMapper.selectFavoritePost(userId, post.getId());
            post.setFavorited(favPost != null);

            // 如果某个帖子没有点赞，说明不是点赞过帖子列表，退出标记
            if(isLikedList && !post.getLiked()){
                isLikedList = false;
            }
        }
    }

    public List<ReportPost> getAllReportPosts(int page, int size) {
        int offset = page * size;
        return forumMapper.selectAllReportPosts(offset, size);
    }

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.springboot.mapper.ForumMapper">

    <!-- 插入帖子 -->
    <insert id="insertPost" parameterType="com.example.springboot.entity.Forum.Post" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO post(user_id,title,content,created_at,updated_at)
        VALUES(#{userId}, #{title}, #{content}, NOW(), NOW())
    </insert>

    <!-- 删除帖子 -->
    <delete id="deletePostById" parameterType="int">
        DELETE FROM post WHERE id = #{id}
    </delete>

    <!-- 更新帖子 -->
    <update id="updatePost" parameterType="com.example.springboot.entity.Forum.Post">
        UPDATE post
        SET title = #{title}, content = #{content}, updated_at = NOW()
        WHERE id = #{id}
    </update>

    <!-- 根据id查询帖子（带用户名） -->
    <select id="selectPostById" parameterType="int" resultMap="postResultMap">
        SELECT
            p.id,
            p.user_id AS userId,
            u.username,
            p.title,
            p.content,
            p.created_at AS createdAt,
            p.updated_at AS updatedAt,
            IFNULL(like_counts.count, 0) AS likeCount
        FROM post p
                 LEFT JOIN user u ON p.user_id = u.id
                 LEFT JOIN (
            SELECT post_id, COUNT(*) AS count FROM like_post GROUP BY post_id
        ) like_counts ON like_counts.post_id = p.id
        WHERE p.id = #{id}
    </select>

    <resultMap id="postResultMap" type="com.example.springboot.entity.Forum.Post">
        <id property="id" column="id"/>
        <result property="userId" column="userId"/>
        <result property="username" column="username"/>
        <result property="title" column="title"/>
        <result property="content" column="content"/>
        <result property="createdAt" column="createdAt"/>
        <result property="updatedAt" column="updatedAt"/>
        <result property="likeCount" column="likeCount"/>
        <result property="liked" column="liked"/>
        <result property="favorited" column="favorited"/>
    </resultMap>

    <!-- 配置resultMap对应属性映射到Post实体 -->

    <!-- 模糊搜索帖子的标题 -->
    <select id="selectPostsByTitleLike" parameterType="string" resultType="com.example.springboot.entity.Forum.Post">
        SELECT
            p.id,
            p.user_id AS userId,
            u.username,
            p.title,
            p.content,
            p.created_at AS createdAt,
            p.updated_at AS updatedAt
        FROM post p
                 LEFT JOIN user u ON p.user_id = u.id
        WHERE p.title LIKE CONCAT('%', #{title}, '%')
        ORDER BY p.created_at DESC
    </select>

    <!-- 新增评论 -->
    <insert id="insertComment" parameterType="com.example.springboot.entity.Forum.Comment" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO comment(post_id, user_id, parent_comment_id, content, created_at, updated_at)
        VALUES(#{postId}, #{userId}, #{parentCommentId}, #{content}, NOW(), NOW())
    </insert>

    <!-- 删除评论 -->
    <delete id="deleteCommentById" parameterType="int">
        DELETE FROM comment WHERE id = #{id}
    </delete>

    <!-- 修改评论内容 -->
    <update id="updateComment" parameterType="com.example.springboot.entity.Forum.Comment">
        UPDATE comment
        SET content = #{content}, updated_at = NOW()
        WHERE id = #{id}
    </update>

    <!-- 查询某帖子下所有评论（带用户名） -->
    <select id="selectCommentsByPostId" parameterType="int" resultType="com.example.springboot.entity.Forum.Comment">
        SELECT
            c.id,
            c.post_id AS postId,
            c.user_id AS userId,
            u.username,
            c.parent_comment_id AS parentCommentId,
            c.content,
            c.created_at AS createdAt,
            c.updated_at AS updatedAt
        FROM comment c
                 LEFT JOIN user u ON c.user_id = u.id
        WHERE c.post_id = #{postId}
        ORDER BY c.created_at ASC
    </select>

    <!-- 点赞 -->
    <insert id="insertLikePost" parameterType="com.example.springboot.entity.Forum.LikePost" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO like_post(user_id, post_id, created_at) VALUES(#{userId}, #{postId}, NOW())
    </insert>

    <delete id="deleteLikePost" parameterType="map">
        DELETE FROM like_post WHERE user_id = #{userId} AND post_id = #{postId}
    </delete>

    <select id="selectLikePost" parameterType="map" resultType="com.example.springboot.entity.Forum.LikePost">
        SELECT * FROM like_post WHERE user_id = #{userId} AND post_id = #{postId} LIMIT 1
    </select>

    <!-- 收藏 -->
    <insert id="insertFavoritePost" parameterType="com.example.springboot.entity.Forum.FavoritePost" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO favorite_post(user_id, post_id, created_at) VALUES(#{userId}, #{postId}, NOW())
    </insert>

    <delete id="deleteFavoritePost" parameterType="map">
        DELETE FROM favorite_post WHERE user_id = #{userId} AND post_id = #{postId}
    </delete>

    <select id="selectFavoritePost" parameterType="map" resultType="com.example.springboot.entity.Forum.FavoritePost">
        SELECT * FROM favorite_post WHERE user_id = #{userId} AND post_id = #{postId} LIMIT 1
    </select>

    <!-- 举报 -->
    <insert id="insertReportPost" parameterType="com.example.springboot.entity.Forum.ReportPost" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO report_post(reporter_id, post_id, reason, created_at) VALUES(#{reporterId}, #{postId}, #{reason}, NOW())
    </insert>

    <select id="selectReportPostsByPostId" parameterType="int" resultType="com.example.springboot.entity.Forum.ReportPost">
        SELECT * FROM report_post WHERE post_id = #{postId} ORDER BY created_at DESC
    </select>

    <!-- 查询某用户的所有帖子 -->
    <select id="selectPostsByUserId" parameterType="int" resultType="com.example.springboot.entity.Forum.Post">
        SELECT
            p.id,
            p.user_id AS userId,
            u.username,
            p.title,
            p.content,
            p.created_at AS createdAt,
            p.updated_at AS updatedAt,
            IFNULL(like_counts.count, 0) AS likeCount
        FROM post p
                 LEFT JOIN user u ON p.user_id = u.id
                 LEFT JOIN (
            SELECT post_id, COUNT(*) AS count
            FROM like_post
            GROUP BY post_id
        ) like_counts ON like_counts.post_id = p.id
        WHERE p.user_id = #{userId}
        ORDER BY p.created_at DESC
    </select>

    <!-- 查询某帖子的点赞数量 -->
    <select id="selectLikeCountByPostId" parameterType="int" resultType="int">
        SELECT COUNT(*) FROM like_post WHERE post_id = #{postId}
    </select>

    <!-- 查询用户点赞过的帖子 -->
    <select id="selectLikedPostsByUserId" parameterType="int" resultType="com.example.springboot.entity.Forum.Post">
        SELECT
            p.id,
            p.user_id AS userId,
            u.username,
            p.title,
            p.content,
            p.created_at AS createdAt,
            p.updated_at AS updatedAt
        FROM post p
                 INNER JOIN like_post lp ON p.id = lp.post_id
                 LEFT JOIN user u ON p.user_id = u.id
        WHERE lp.user_id = #{userId}
        ORDER BY lp.created_at DESC
    </select>

    <!-- 查询用户收藏过的帖子 -->
    <select id="selectFavoritedPostsByUserId" parameterType="int" resultType="com.example.springboot.entity.Forum.Post">
        SELECT
            p.id,
            p.user_id AS userId,
            u.username,
            p.title,
            p.content,
            p.created_at AS createdAt,
            p.updated_at AS updatedAt
        FROM post p
                 INNER JOIN favorite_post fp ON p.id = fp.post_id
                 LEFT JOIN user u ON p.user_id = u.id
        WHERE fp.user_id = #{userId}
        ORDER BY fp.created_at DESC
    </select>

    <select id="selectAllReportPosts" parameterType="map" resultType="com.example.springboot.entity.Forum.ReportPost">
        SELECT
            r.id, r.reporter_id AS reporterId, r.post_id AS postId, r.reason, r.created_at AS createdAt,
            u.username AS reporterUsername,
            p.title AS postTitle
        FROM report_post r
                 LEFT JOIN user u ON r.reporter_id = u.id
                 LEFT JOIN post p ON r.post_id = p.id
        ORDER BY r.created_at DESC
        LIMIT #{limit} OFFSET #{offset}
    </select>

</mapper>
<template>
  <div class="forum-home">
    <h1>论坛管理</h1>

    <div class="controls">
      <input
          v-model="searchTitle"
          placeholder="搜索帖子标题"
          @keyup.enter="searchPosts"
          class="search-input"
      />
      <button @click="searchPosts" class="btn">搜索</button>
      <button @click="refreshPosts" class="btn btn-refresh">刷新</button>

      <!-- 查看全部举报按钮 -->
      <button @click="openAllReports" class="btn btn-report-all">查看全部举报</button>
    </div>

    <div v-if="loading" class="loading">加载中...</div>
    <div v-else>
      <ul class="posts-list">
        <li
            v-for="post in posts"
            :key="post.id"
            @click="goPostDetail(post.id)"
            class="post-card"
        >
          <!-- 红点 -->
          <span
              v-if="reportedPostIds.has(post.id)"
              class="red-dot"
              title="该帖子有举报"
          ></span>

          <h3 class="post-title">{{ post.title }}</h3>
          <p class="post-info">
            作者ID: {{ post.userId }} |
            用户名: {{ post.username }} |
            发布时间: {{ formatDateTime(post.createdAt) }}
          </p>

          <button class="btn btn-delete" @click.stop="deletePost(post.id)">
            删除
          </button>

          <button class="btn btn-report" @click.stop="openReportList(post)">
            查看举报
          </button>
        </li>
      </ul>

      <div v-if="posts.length === 0" class="no-data">无数据</div>
    </div>

    <!-- 单个帖子举报弹窗 -->
    <div
        v-if="showPostReportsModal"
        class="modal-overlay"
        @click.self="closePostReportList"
    >
      <div class="modal-content">
        <h3>举报列表 - 帖子：《{{ currentPost?.title }}》</h3>

        <div v-if="postReportLoading" class="loading">加载举报中...</div>
        <ul v-else-if="reportsForPost.length" class="report-list">
          <li v-for="report in reportsForPost" :key="report.id" class="report-item">
            <p><strong>举报人ID：</strong> {{ report.reporterId }}</p>
            <p><strong>举报内容：</strong> {{ report.reason }}</p>
            <p><strong>举报时间：</strong> {{ formatDateTime(report.createdAt) }}</p>
          </li>
        </ul>

        <div v-else class="no-data">暂无举报</div>

        <button class="btn btn-cancel" @click="closePostReportList">关闭</button>
      </div>
    </div>

    <!-- 查看所有举报弹窗 -->
    <div
        v-if="showAllReportsModal"
        class="modal-overlay"
        @click.self="closeAllReportList"
    >
      <div class="modal-content">
        <h3>全部举报列表</h3>

        <div v-if="allReportLoading" class="loading">加载举报中...</div>
        <ul v-else-if="allReports.length" class="report-list">
          <li v-for="report in allReports" :key="report.id" class="report-item">
            <p>
              <strong>被举报帖子：</strong>
              <span
                  class="report-post-title"
                  @click="goPostDetail(report.postId)"
                  title="点击查看帖子详情"
              >《{{ report.postTitle }}》</span
              >
            </p>
            <p>
              <strong>举报人：</strong> {{ report.reporterUsername }} (ID:
              {{ report.reporterId }})
            </p>
            <p><strong>举报内容：</strong> {{ report.reason }}</p>
            <p><strong>举报时间：</strong> {{ formatDateTime(report.createdAt) }}</p>
          </li>
        </ul>

        <div v-else class="no-data">暂无举报</div>

        <button class="btn btn-cancel" @click="closeAllReportList">关闭</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import { useRouter } from "vue-router";
import dayjs from "dayjs";

const formatDateTime = (isoString) => {
  if (!isoString) return "";
  return dayjs(isoString).format("YYYY年MM月DD日 HH:mm:ss");
};

// 帖子列表及搜索
const searchTitle = ref("");
const posts = ref([]);
const loading = ref(false);

const router = useRouter();

// 新增：被举报帖子ID集合
const reportedPostIds = ref(new Set());

// 获取带举报帖子ID的函数
const fetchReportedPostIds = async () => {
  try {
    const res = await axios.get("http://localhost:8080/forum/post/reports", {
      params: { size: 1000 }, // 根据需要调整条数限制
    });
    // 假设接口返回格式如 [{id, postId, reporterId, reason, ...}, ...]
    const ids = res.data.map((report) => report.postId);
    reportedPostIds.value = new Set(ids);
  } catch (err) {
    console.error("获取带举报帖子ID失败:", err);
    reportedPostIds.value = new Set();
  }
};

const fetchPosts = async (title = "") => {
  loading.value = true;
  try {
    const res = await axios.get("http://localhost:8080/forum/posts", {
      params: { title },
    });
    posts.value = res.data;
    // 先拿帖子数据，再请求举报帖子ID
    await fetchReportedPostIds();
  } catch (err) {
    console.error(err);
    posts.value = [];
    reportedPostIds.value = new Set();
  } finally {
    loading.value = false;
  }
};

const searchPosts = () => {
  fetchPosts(searchTitle.value);
};

const refreshPosts = () => {
  fetchPosts(searchTitle.value);
};

const goPostDetail = (postId) => {
  router.push(`/post/${postId}`);
};

const deletePost = async (postId) => {
  if (!confirm("确定要删除该帖子吗？")) {
    return;
  }
  try {
    await axios.delete(`http://localhost:8080/forum/post/${postId}`);
    alert("删除成功");
    fetchPosts(searchTitle.value);
  } catch (error) {
    console.error(error);
    alert("删除失败");
  }
};

// **************** 单个帖子举报弹窗 ****************
const showPostReportsModal = ref(false);
const currentPost = ref(null);
const reportsForPost = ref([]);
const postReportLoading = ref(false);

const openReportList = async (post) => {
  currentPost.value = post;
  showPostReportsModal.value = true;
  postReportLoading.value = true;
  reportsForPost.value = [];
  try {
    const res = await axios.get(
        `http://localhost:8080/forum/post/reports/${post.id}`
    );
    reportsForPost.value = res.data || [];
  } catch (error) {
    console.error("获取帖子举报列表失败:", error);
    reportsForPost.value = [];
  } finally {
    postReportLoading.value = false;
  }
};

const closePostReportList = () => {
  showPostReportsModal.value = false;
  currentPost.value = null;
  reportsForPost.value = [];
};

// **************** 查看全部举报弹窗 ****************
const showAllReportsModal = ref(false);
const allReports = ref([]);
const allReportLoading = ref(false);

const openAllReports = async () => {
  showAllReportsModal.value = true;
  allReportLoading.value = true;
  allReports.value = [];
  try {
    const res = await axios.get("http://localhost:8080/forum/post/reports", {
      params: { size: 100 },
    });
    allReports.value = res.data || [];
  } catch (error) {
    console.error("获取全部举报失败:", error);
    allReports.value = [];
  } finally {
    allReportLoading.value = false;
  }
};

const closeAllReportList = () => {
  showAllReportsModal.value = false;
  allReports.value = [];
};

onMounted(() => {
  fetchPosts();
});
</script>

<style scoped>
.forum-home {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: #333;
}

.controls {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 20px;
}

.search-input {
  flex: 1;
  padding: 8px 12px;
  font-size: 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  transition: border-color 0.3s;
}
.search-input:focus {
  outline: none;
  border-color: #409eff;
  box-shadow: 0 0 5px rgba(64, 158, 255, 0.5);
}

.btn {
  padding: 8px 16px;
  border: none;
  background-color: #409eff;
  color: white;
  font-weight: 600;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}
.btn:hover {
  background-color: #66b1ff;
}
.btn-refresh {
  background-color: #67c23a;
}
.btn-refresh:hover {
  background-color: #85ce61;
}

.loading,
.no-data {
  font-size: 18px;
  text-align: center;
  color: #888;
  margin-top: 40px;
}

.posts-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.post-card {
  background-color: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 16px 20px;
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.05);
  cursor: pointer;
  transition: box-shadow 0.3s, border-color 0.3s;
  position: relative;
}
.post-card:hover {
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.12);
  border-color: #409eff;
}

.post-title {
  margin: 0 0 8px;
  color: #409eff;
  font-weight: 700;
  font-size: 20px;
}

.post-info {
  margin: 0;
  font-size: 14px;
  color: #666;
}

.btn-delete {
  position: absolute;
  top: 16px;
  right: 20px;
  background-color: #f56c6c;
  color: white;
  border: none;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: background-color 0.3s;
  z-index: 10;
}
.btn-delete:hover {
  background-color: #f78989;
}

/* 新增举报按钮样式 */
.btn-report {
  position: absolute;
  top: 16px;
  right: 80px;
  background-color: #e6a23c;
  color: white;
  border: none;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: background-color 0.3s;
  z-index: 10;
}
.btn-report:hover {
  background-color: #ebb563;
}

/* 弹窗样式 */
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.modal-content {
  background: white;
  padding: 20px;
  border-radius: 8px;
  width: 500px;
  max-width: 95%;
  max-height: 80vh;
  overflow-y: auto;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

.report-list {
  list-style: none;
  padding: 0;
  margin: 16px 0 0 0;
  max-height: 400px;
  overflow-y: auto;
  border-top: 1px solid #ebeef5;
}

.report-item {
  border-bottom: 1px solid #ebeef5;
  padding: 12px 0;
  font-size: 14px;
  color: #444;
}

.report-item p {
  margin: 4px 0;
}

.btn-cancel {
  background-color: #909399;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
  margin-top: 15px;
}
.btn-cancel:hover {
  background-color: #a6a9ad;
}

.btn-report-all {
  background-color: #f90;
  color: white;
  border: none;
  padding: 4px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  transition: background-color 0.3s;
  margin-left: 10px;
}
.btn-report-all:hover {
  background-color: #fb3;
}

.red-dot {
  position: absolute;
  top: 14px;
  left: 14px;
  width: 10px;
  height: 10px;
  background-color: #f56c6c;
  border-radius: 50%;
  box-shadow: 0 0 3px #f56c6c;
  z-index: 20;
}

.report-post-title {
  color: #409eff;
  cursor: pointer;
  text-decoration: underline;
}

.report-post-title:hover {
  color: #66b1ff;
}
</style>
<template>
  <div class="forum-home">
    <h1>英语经验论坛首页</h1>

    <div class="controls">
      <input
          v-model="searchTitle"
          placeholder="搜索帖子标题"
          @keyup.enter="searchPosts"
          class="search-input"
      />
      <button @click="searchPosts" class="btn">搜索</button>
      <button @click="refreshPosts" class="btn btn-refresh">刷新</button>
    </div>

    <div v-if="loading" class="loading">加载中...</div>
    <div v-else>
      <ul class="posts-list">
        <li
            v-for="post in posts"
            :key="post.id"
            @click="goPostDetail(post.id)"
            class="post-card"
        >
          <h3 class="post-title">{{ post.title }}</h3>
          <p class="post-info">
            作者ID: {{ post.userId }} |
            用户名: {{ post.username }} |
            发布时间: {{ formatDateTime(post.createdAt) }}
          </p>
        </li>
      </ul>

      <div v-if="posts.length === 0" class="no-data">无数据</div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import dayjs from 'dayjs'

const formatDateTime = (isoString) => {
  if (!isoString) return ''
  return dayjs(isoString).format('YYYY年MM月DD日 HH:mm:ss')
}

const searchTitle = ref('')
const posts = ref([])
const loading = ref(false)

const router = useRouter()
const userId = localStorage.getItem('userId')

const fetchPosts = async (title = '') => {
  loading.value = true
  try {
    const res = await axios.get('http://localhost:8080/forum/posts', {
      params: { title }
    })
    posts.value = res.data
  } catch (err) {
    console.error(err)
    posts.value = []
  } finally {
    loading.value = false
  }
}

const searchPosts = () => {
  fetchPosts(searchTitle.value)
}

const refreshPosts = () => {
  fetchPosts(searchTitle.value)
}

const goPostDetail = (postId) => {
  router.push(`/post/${postId}`)
}

onMounted(() => {
  fetchPosts()
})
</script>

<style scoped>
.forum-home {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  color: #333;
}

.controls {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 20px;
}

.search-input {
  flex: 1;
  padding: 8px 12px;
  font-size: 16px;
  border: 1px solid #ccc;
  border-radius: 4px;
  transition: border-color 0.3s;
}
.search-input:focus {
  outline: none;
  border-color: #409eff;
  box-shadow: 0 0 5px rgba(64, 158, 255, 0.5);
}

.btn {
  padding: 8px 16px;
  border: none;
  background-color: #409eff;
  color: white;
  font-weight: 600;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}
.btn:hover {
  background-color: #66b1ff;
}
.btn-refresh {
  background-color: #67c23a;
}
.btn-refresh:hover {
  background-color: #85ce61;
}

.loading,
.no-data {
  font-size: 18px;
  text-align: center;
  color: #888;
  margin-top: 40px;
}

.posts-list {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.post-card {
  background-color: #fff;
  border: 1px solid #ebeef5;
  border-radius: 8px;
  padding: 16px 20px;
  box-shadow: 0 2px 6px rgb(0 0 0 / 0.05);
  cursor: pointer;
  transition: box-shadow 0.3s, border-color 0.3s;
}
.post-card:hover {
  box-shadow: 0 8px 20px rgb(0 0 0 / 0.12);
  border-color: #409eff;
}

.post-title {
  margin: 0 0 8px;
  color: #409eff;
  font-weight: 700;
  font-size: 20px;
}

.post-info {
  margin: 0;
  font-size: 14px;
  color: #666;
}
</style>
<template>
  <div class="my-posts">
    <h2>帖子中心</h2>

    <!-- 分类切换Tabs -->
    <div class="tabs">
      <button
          v-for="tab in tabs"
          :key="tab.key"
          :class="['tab-item', { active: currentTab === tab.key }]"
          @click="switchTab(tab.key)"
      >
        {{ tab.label }}
      </button>
    </div>

    <button @click="fetchPosts" class="btn-refresh">刷新</button>

    <div v-if="loading" class="loading">加载中...</div>

    <div v-else>
      <div v-if="posts.length === 0" class="no-posts">
        {{ noPostsText }}
      </div>
      <ul class="posts-list">
        <li v-for="post in posts" :key="post.id" class="post-item">
          <div class="post-info">
            <router-link :to="`/post/${post.id}`" class="post-title">{{ post.title }}</router-link>
            <span class="post-date">发布时间: {{ formatDateTime(post.createdAt) }}</span>
          </div>
          <div class="post-actions">
            <button
                @click="toggleLike(post)"
                :class="['btn-like', { active: post.liked }]"
                title="点赞 / 取消点赞"
            >
              {{ post.liked ? "取消点赞" : "点赞" }} ({{ post.likeCount }})
            </button>
            <button
                @click="toggleFavorite(post)"
                :class="['btn-favorite', { active: post.favorited }]"
                title="收藏 / 取消收藏"
            >
              {{ post.favorited ? "取消收藏" : "收藏" }}
            </button>
            <!-- 只有我的帖子才显示删除按钮 -->
            <button
                v-if="currentTab === 'my-posts'"
                @click="deletePost(post)"
                class="btn-delete"
                title="删除帖子"
            >
              删除
            </button>
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";
import axios from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs";
import { useRouter } from "vue-router";

const userId = localStorage.getItem("userId");
const router = useRouter();

if (!userId) {
  ElMessage.warning("请先登录");
  router.push("/login");
}

const tabs = [
  { key: "my-posts", label: "我的帖子" },
  { key: "liked-posts", label: "我点赞过的" },
  { key: "favorited-posts", label: "我收藏的" },
];

const currentTab = ref("my-posts");
const posts = ref([]);
const loading = ref(false);

const noPostsTextMap = {
  "my-posts": "还没有发表任何帖子",
  "liked-posts": "还没有点赞过任何帖子",
  "favorited-posts": "还没有收藏过任何帖子",
};

const noPostsText = ref(noPostsTextMap[currentTab.value]);

const BASE_URL = "http://localhost:8080/forum";

const formatDateTime = (isoString) => {
  if (!isoString) return "";
  return dayjs(isoString).format("YYYY年MM月DD日 HH:mm:ss");
};

const fetchPosts = async () => {
  if (!userId) return;
  loading.value = true;
  try {
    let res;
    if (currentTab.value === "my-posts") {
      res = await axios.get(`${BASE_URL}/user/${userId}/posts`);
    } else if (currentTab.value === "liked-posts") {
      res = await axios.get(`${BASE_URL}/user/${userId}/liked-posts`);
    } else if (currentTab.value === "favorited-posts") {
      res = await axios.get(`${BASE_URL}/user/${userId}/favorited-posts`);
    }
    posts.value = res?.data || [];

    posts.value.forEach(post => {
      if (post.liked === undefined) post.liked = false;
      if (post.favorited === undefined) post.favorited = false;
      if (post.likeCount === undefined) post.likeCount = 0;
    });

    noPostsText.value = noPostsTextMap[currentTab.value];
  } catch (error) {
    ElMessage.error("加载帖子失败");
    console.error(error);
  } finally {
    loading.value = false;
  }
};

const switchTab = (tabKey) => {
  if (currentTab.value === tabKey) return;
  currentTab.value = tabKey;
  fetchPosts();
};

const toggleLike = async (post) => {
  if (!userId) {
    ElMessage.warning("请先登录");
    return;
  }
  try {
    if (!post.liked) {
      await axios.post(`${BASE_URL}/post/like`, { userId: +userId, postId: post.id });
      post.liked = true;
      post.likeCount++;
      ElMessage.success("点赞成功");
    } else {
      await axios.delete(`${BASE_URL}/post/like`, { params: { userId: +userId, postId: post.id } });
      post.liked = false;
      post.likeCount--;
      ElMessage.success("取消点赞成功");
    }
  } catch (error) {
    ElMessage.error("点赞操作失败");
  }
};

const toggleFavorite = async (post) => {
  if (!userId) {
    ElMessage.warning("请先登录");
    return;
  }
  try {
    if (!post.favorited) {
      await axios.post(`${BASE_URL}/post/favorite`, { userId: +userId, postId: post.id });
      post.favorited = true;
      ElMessage.success("收藏成功");
    } else {
      await axios.delete(`${BASE_URL}/post/favorite`, { params: { userId: +userId, postId: post.id } });
      post.favorited = false;
      ElMessage.success("取消收藏成功");
    }
  } catch (error) {
    ElMessage.error("收藏操作失败");
  }
};

const deletePost = async (post) => {
  try {
    await ElMessageBox.confirm("确定删除此帖子吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    });
    await axios.delete(`${BASE_URL}/post/${post.id}`);
    ElMessage.success("删除成功");
    fetchPosts();
  } catch (error) {
    if (error !== "cancel") {
      ElMessage.error("删除失败");
    }
  }
};

onMounted(() => {
  fetchPosts();
});
</script>

<style scoped>
.my-posts {
  max-width: 700px;
  margin: auto;
  padding: 20px;
  background: #fff;
  border-radius: 10px;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
}

h2 {
  color: #111;
  margin-bottom: 15px;
  font-weight: 600;
  font-size: 24px;
  text-align: center;
}

/* Tabs */
.tabs {
  display: flex;
  justify-content: center;
  margin-bottom: 12px;
  gap: 12px;
}

.tab-item {
  padding: 8px 20px;
  border-radius: 20px;
  border: 1px solid #d0d9ff;
  background-color: #f0f5ff;
  cursor: pointer;
  font-weight: 600;
  color: #409eff;
  transition: all 0.3s ease;
  user-select: none;
  outline: none;
}

.tab-item.active {
  background-color: #409eff;
  color: white;
  border-color: #409eff;
  box-shadow: 0 0 8px rgba(64, 158, 255, 0.6);
}

.tab-item:hover:not(.active) {
  background-color: #ebf2ff;
}

/* 刷新按钮 */
.btn-refresh {
  display: block;
  margin: 0 auto 20px auto;
  background-color: #409eff;
  color: white;
  border: none;
  border-radius: 20px;
  padding: 8px 30px;
  cursor: pointer;
  font-weight: 700;
  font-size: 14px;
  transition: background-color 0.3s ease;
  user-select: none;
}

.btn-refresh:hover {
  background-color: #66b1ff;
}

/* 帖子列表 */
.posts-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.post-item {
  border-radius: 12px;
  background-color: #f5faff;
  margin-bottom: 14px;
  padding: 16px 20px;
  box-shadow: 0 4px 14px rgb(64 158 255 / 0.15);
  transition: box-shadow 0.3s ease;
  cursor: pointer;
}

.post-item:hover {
  box-shadow: 0 8px 20px rgb(64 158 255 / 0.3);
}

.post-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 10px;
}

.post-title {
  font-weight: 700;
  font-size: 18px;
  color: #409eff;
  text-decoration: none;
}

.post-title:hover {
  text-decoration: underline;
}

.post-date {
  font-size: 13px;
  color: #708090; /* 稍深点的灰蓝 */
  white-space: nowrap;
}

/* 按钮 */
.post-actions {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
}

.post-actions button {
  padding: 6px 16px;
  border-radius: 20px;
  border: 1px solid transparent;
  cursor: pointer;
  font-weight: 600;
  font-size: 14px;
  user-select: none;
  transition: all 0.3s ease;
  background-color: white;
  color: #409eff;
  box-shadow: 0 0 6px rgb(64 158 255 / 0.15);
}

.post-actions button:hover {
  box-shadow: 0 0 10px rgb(64 158 255 / 0.3);
  filter: brightness(1.1);
}

.btn-like.active {
  background-color: #409eff;
  color: white;
  box-shadow: 0 0 8px rgb(64 158 255 / 0.9);
}

.btn-favorite.active {
  background-color: #66b1ff;
  color: white;
  box-shadow: 0 0 8px rgb(102 177 255 / 0.8);
}

.btn-delete {
  background-color: #91caff;
  color: white;
  box-shadow: none;
  border-color: #91caff;
}

.btn-delete:hover {
  background-color: #409eff;
  box-shadow: 0 0 8px rgb(64 158 255 / 0.9);
}

/* 无帖子文字 */
.no-posts {
  text-align: center;
  font-size: 16px;
  color: #708090;
  margin: 40px 0;
}

/* 加载中 */
.loading {
  text-align: center;
  font-size: 16px;
  color: #708090;
}
</style>
<template>
  <div class="post-detail">
    <button @click="goBack" class="back-btn">返回主页</button>

    <h2 class="post-title">{{ post?.title }}</h2>
    <p class="post-info">
      作者: {{ post?.username || post?.userId }} |
      发布时间: {{ formatDateTime(post?.createdAt) }}
    </p>
    <p class="post-content monospace-content">{{ post?.content }}</p>

    <!-- 点赞 收藏 举报 按钮 -->
    <div class="post-actions">
      <button @click="toggleLike" :class="{ active: liked }" class="btn-like">
        {{ liked ? "已点赞" : "点赞" }} ({{ likeCount }})
      </button>
      <button @click="toggleFavorite" :class="{ active: favorited }" class="btn-favorite">
        {{ favorited ? "已收藏" : "收藏" }}
      </button>
      <button @click="openReportDialog" class="btn-report">
        举报
      </button>
    </div>

    <hr />

    <h3>评论区</h3>

    <div v-if="loadingComments" class="loading">加载评论中...</div>
    <div v-else>
      <ul class="comments-list">
        <li v-for="comment in rootComments" :key="comment.id" class="comment-item">
          <div class="comment-main">
            <div class="comment-header">
              <b>{{ comment.username || ("用户ID: " + comment.userId) }}</b>
              <span v-if="comment.createdAt" class="comment-time"
              >评论时间 {{ formatDateTime(comment.createdAt) }}</span
              >
              <button
                  v-if="comment.userId == userId"
                  @click="deleteComment(comment.id)"
                  class="btn-delete"
                  title="删除评论"
              >
                删除
              </button>

              <button @click="toggleReplyBox(comment.id)" class="btn-reply">
                {{ replyingTo === comment.id ? "取消回复" : "回复" }}
              </button>

              <button @click="toggleRepliesCollapse(comment.id)" class="btn-collapse">
                {{ repliesCollapsed[comment.id] ? "展开回复" : "收起回复" }}
              </button>
            </div>

            <p class="comment-content monospace-content">{{ comment.content }}</p>

            <div v-if="replyingTo === comment.id" class="reply-box">
              <el-input
                  v-model="replyContent"
                  type="textarea"
                  :autosize="{ minRows: 2, maxRows: 12 }"
                  placeholder="回复内容"
                  style="width: 100%;"
                  class="autosize-textarea monospace-textarea"
              />
              <button class="btn-submit" @click="submitReply(comment.id)">提交回复</button>
            </div>
          </div>

          <ul
              v-if="!repliesCollapsed[comment.id] && flattenReplies[comment.id]?.length"
              class="replies-list"
          >
            <li
                v-for="reply in flattenReplies[comment.id]"
                :key="reply.id"
                class="comment-reply"
            >
              <div class="comment-header">
                <b>{{ reply.username || ("用户ID: " + reply.userId) }}</b>
                <span class="comment-time"
                >回复时间 {{ formatDateTime(reply.createdAt) }}</span
                >

                <span class="reply-to" v-if="reply.parentCommentId">
                  回复 <b>{{ rootCommentUsernames[comment.id] }}</b>
                </span>

                <button
                    v-if="reply.userId == userId"
                    @click="deleteComment(reply.id)"
                    class="btn-delete"
                    title="删除回复"
                >
                  删除
                </button>

                <button @click="toggleReplyBox(reply.id)" class="btn-reply">
                  {{ replyingTo === reply.id ? "取消回复" : "回复" }}
                </button>
              </div>

              <p class="comment-content monospace-content">
                {{ reply.content }}
              </p>

              <div v-if="replyingTo === reply.id" class="reply-box">
                <el-input
                    v-model="replyContent"
                    type="textarea"
                    :autosize="{ minRows: 2, maxRows: 12 }"
                    placeholder="回复内容"
                    style="width: 100%;"
                    class="autosize-textarea monospace-textarea"
                />
                <button class="btn-submit" @click="submitReply(reply.id)">
                  提交回复
                </button>
              </div>
            </li>
          </ul>
        </li>
      </ul>

      <hr />

      <h4>发表评论</h4>
      <el-input
          v-model="newComment"
          type="textarea"
          :autosize="{ minRows: 3, maxRows: 12 }"
          placeholder="请输入评论内容"
          class="new-comment-textarea autosize-textarea monospace-textarea"
          style="width: 100%;"
      />
      <button class="btn-submit" @click="submitComment">提交评论</button>
    </div>

    <el-dialog v-model="reportDialogVisible" title="举报帖子" width="400px">
      <el-input
          v-model="reportReason"
          type="textarea"
          :autosize="{ minRows: 4, maxRows: 12 }"
          placeholder="请输入举报原因"
          style="width: 100%;"
          class="autosize-textarea monospace-textarea"
      />
      <span slot="footer" class="dialog-footer">
        <el-button @click="reportDialogVisible = false">取消</el-button>
        <el-button type="primary" @click="submitReport">提交举报</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from "vue";
import axios from "axios";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import dayjs from "dayjs";

const formatDateTime = (isoString) => {
  if (!isoString) return "";
  return dayjs(isoString).format("YYYY年MM月DD日 HH:mm:ss");
};

const route = useRoute();
const router = useRouter();

const postId = Number(route.params.id);
const post = ref(null);
const comments = ref([]);
const loadingComments = ref(false);
const userId = localStorage.getItem("userId");

const newComment = ref("");
const replyingTo = ref(null);
const replyContent = ref("");
const repliesCollapsed = ref({});

// 点赞收藏举报相关
const liked = ref(false);
const likeCount = ref(0);
const favorited = ref(false);
const reportDialogVisible = ref(false);
const reportReason = ref("");

const fetchPost = async () => {
  try {
    const res = await axios.get(`http://localhost:8080/forum/post/${postId}`);
    post.value = res.data;
    likeCount.value = res.data.likeCount || 0;
  } catch (error) {
    ElMessage.error("加载帖子详情失败");
    router.back();
  }
};

const fetchComments = async () => {
  loadingComments.value = true;
  try {
    const res = await axios.get(`http://localhost:8080/forum/comments/${postId}`);
    comments.value = res.data;
  } catch (error) {
    ElMessage.error("加载评论失败");
  } finally {
    loadingComments.value = false;
  }
};

const fetchLikeStatus = async () => {
  if (!userId) return;
  try {
    const res = await axios.get(`http://localhost:8080/forum/post/like/status`, {
      params: { userId, postId }
    });
    liked.value = res.data.liked;
  } catch (error) {
    console.error("查询点赞状态失败", error);
  }
};

const fetchFavoriteStatus = async () => {
  if (!userId) return;
  try {
    const res = await axios.get(`http://localhost:8080/forum/post/favorite/status`, {
      params: { userId, postId }
    });
    favorited.value = res.data.favorited;
  } catch (error) {
    console.error("查询收藏状态失败", error);
  }
};

const toggleRepliesCollapse = (commentId) => {
  repliesCollapsed.value[commentId] = !repliesCollapsed.value[commentId];
};

const goBack = () => {
  router.back();
};

const submitComment = async () => {
  const content = newComment.value.trim();
  if (!content) {
    ElMessage.warning("请输入评论内容");
    return;
  }
  try {
    await axios.post("http://localhost:8080/forum/comment", {
      postId,
      userId,
      content,
      parentCommentId: null,
    });
    ElMessage.success("评论成功");
    newComment.value = "";
    fetchComments();
  } catch (error) {
    ElMessage.error("发表评论失败");
  }
};

const toggleReplyBox = (commentId) => {
  if (replyingTo.value === commentId) {
    replyingTo.value = null;
    replyContent.value = "";
  } else {
    replyingTo.value = commentId;
    replyContent.value = "";
  }
};

const submitReply = async (parentCommentId) => {
  const content = replyContent.value.trim();
  if (!content) {
    ElMessage.warning("请输入回复内容");
    return;
  }
  try {
    await axios.post("http://localhost:8080/forum/comment", {
      postId,
      userId,
      content,
      parentCommentId,
    });
    ElMessage.success("回复成功");
    replyingTo.value = null;
    replyContent.value = "";
    fetchComments();
  } catch (error) {
    ElMessage.error("回复失败");
  }
};

const deleteComment = async (commentId) => {
  try {
    await ElMessageBox.confirm("确定删除此评论吗?", "提示", {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    });
    await axios.delete(`http://localhost:8080/forum/comment/${commentId}`);
    ElMessage.success("删除成功");
    fetchComments();
  } catch (error) {
    if (error !== "cancel") {
      ElMessage.error("删除失败");
    }
  }
};

const toggleLike = async () => {
  if (!userId) {
    ElMessage.warning("请先登录");
    return;
  }
  try {
    if (!liked.value) {
      await axios.post("http://localhost:8080/forum/post/like", { userId, postId });
      liked.value = true;
      likeCount.value++;
      ElMessage.success("点赞成功");
    } else {
      await axios.delete("http://localhost:8080/forum/post/like", { params: { userId, postId } });
      liked.value = false;
      likeCount.value--;
      ElMessage.success("取消点赞成功");
    }
  } catch {
    ElMessage.error("点赞操作失败");
  }
};

const toggleFavorite = async () => {
  if (!userId) {
    ElMessage.warning("请先登录");
    return;
  }
  try {
    if (!favorited.value) {
      await axios.post("http://localhost:8080/forum/post/favorite", { userId, postId });
      favorited.value = true;
      ElMessage.success("收藏成功");
    } else {
      await axios.delete("http://localhost:8080/forum/post/favorite", { params: { userId, postId } });
      favorited.value = false;
      ElMessage.success("取消收藏成功");
    }
  } catch {
    ElMessage.error("收藏操作失败");
  }
};

const openReportDialog = () => {
  if (!userId) {
    ElMessage.warning("请先登录");
    return;
  }
  reportReason.value = "";
  reportDialogVisible.value = true;
};

const submitReport = async () => {
  if (!reportReason.value.trim()) {
    ElMessage.warning("请输入举报原因");
    return;
  }
  try {
    await axios.post("http://localhost:8080/forum/post/report", {
      reporterId: userId,
      postId,
      reason: reportReason.value,
    });
    ElMessage.success("举报成功，管理员会尽快处理");
    reportDialogVisible.value = false;
  } catch {
    ElMessage.error("举报失败");
  }
};

// 计算一级评论（parentCommentId === null）
const rootComments = computed(() =>
    comments.value
        .filter((c) => !c.parentCommentId)
        .sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
);

// flattenReplies: 二级及以上回复折叠到一级评论
const flattenReplies = computed(() => {
  const map = {};
  rootComments.value.forEach((root) => {
    map[root.id] = [];
  });
  comments.value.forEach((c) => {
    if (c.parentCommentId) {
      let parentId = c.parentCommentId;
      let current = comments.value.find((cc) => cc.id === parentId);
      while (current && current.parentCommentId) {
        parentId = current.parentCommentId;
        current = comments.value.find((cc) => cc.id === parentId);
      }
      if (map[parentId]) {
        map[parentId].push(c);
      }
    }
  });
  Object.values(map).forEach((list) =>
      list.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt))
  );
  return map;
});

const rootCommentUsernames = computed(() => {
  const map = {};
  rootComments.value.forEach((root) => {
    map[root.id] = root.username || `用户ID: ${root.userId}`;
  });
  return map;
});

onMounted(() => {
  fetchPost();
  fetchComments();
  fetchLikeStatus();
  fetchFavoriteStatus();
});
</script>

<style scoped>
.post-detail {
  max-width: 800px;
  margin: auto;
  padding: 20px;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  color: #222;
  background: #f9f9f9;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
}

.back-btn {
  background: none;
  border: none;
  color: #409eff;
  font-size: 16px;
  cursor: pointer;
  margin-bottom: 10px;
  padding: 0;
  transition: color 0.3s;
}
.back-btn:hover {
  color: #66b1ff;
}

.post-title {
  margin-bottom: 8px;
  color: #303133;
  font-weight: 700;
}

.post-info {
  margin-bottom: 20px;
  color: #909399;
  font-size: 14px;
}

/* 内容显示用等宽字体，保留换行和空格 */
.post-content.monospace-content,
.comment-content.monospace-content {
  font-family: monospace, "Courier New", Courier, monospace !important;
  white-space: pre-wrap !important;
  word-break: break-word !important;
  overflow-wrap: break-word !important;
}

.post-content {
  background: white;
  padding: 16px;
  border-radius: 6px;
  margin-bottom: 30px;
  line-height: 1.5;
  box-shadow: inset 0 0 8px rgb(0 0 0 / 0.03);
}

.post-actions {
  margin: 10px 0 20px;
  display: flex;
  gap: 12px;
}
.post-actions button {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  background-color: #f0f2f5;
  border: 1px solid #dcdfe6;
  border-radius: 18px;
  font-size: 14px;
  font-weight: 600;
  color: #606266;
  cursor: pointer;
  transition: all 0.3s ease;
  user-select: none;
}

.btn-like.active,
.btn-favorite.active {
  color: #f56c6c;
}

.comments-list,
.replies-list {
  list-style-type: none;
  padding-left: 0;
  margin: 0;
}

.comment-item {
  background: white;
  border-radius: 6px;
  padding: 14px 20px;
  margin-bottom: 16px;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.1);
}

.comment-main {
  position: relative;
}

.comment-header {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  font-size: 14px;
  margin-bottom: 8px;
}

.comment-header b {
  color: #409eff;
  font-weight: 600;
}

.comment-time {
  color: #909399;
  font-size: 12px;
}

.reply-to {
  color: #606266;
  font-size: 12px;
  margin-left: 4px;
}

/* 评论文字等宽 */
.comment-content {
  font-size: 15px;
  color: #606266;
  line-height: 1.5;
  margin: 0;
  padding-left: 4px;
}

/* 回复框及发表评论输入框等宽字体 */
.autosize-textarea.monospace-textarea .el-textarea__inner {
  font-family: monospace, "Courier New", Courier, monospace !important;
  white-space: pre-wrap !important;
  word-break: break-word !important;
  overflow-wrap: break-word !important;
  resize: vertical;
}

.reply-box {
  margin-top: 8px;
  width: 100%;
}

/* 让 el-input 宽度撑满容器 */
.reply-box .autosize-textarea,
.new-comment-textarea.autosize-textarea {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
}

.btn-delete,
.btn-reply,
.btn-submit,
.btn-collapse {
  background: none;
  border: none;
  color: #409eff;
  cursor: pointer;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 13px;
  transition: background-color 0.3s;
  user-select: none;
}

.btn-delete {
  color: #f56c6c;
}
.btn-delete:hover {
  background-color: #fde2e2;
}

.btn-reply:hover,
.btn-submit:hover,
.btn-collapse:hover {
  background-color: #ecf5ff;
  color: #66b1ff;
}

.reply-box .btn-submit {
  margin-top: 6px;
  font-weight: 600;
}

.replies-list {
  margin-top: 10px;
  padding-left: 30px;
  border-left: 2px solid #ebeef5;
}

.comment-reply {
  background: #f5f7fa;
  border-radius: 6px;
  padding: 10px 16px;
  margin-bottom: 12px;
  box-shadow: inset 0 1px 3px rgb(0 0 0 / 0.03);
}

.new-comment-textarea {
  margin-bottom: 10px;
  border-radius: 6px;
  border: 1px solid #dcdfe6;
  padding: 8px 10px;
  font-size: 15px;
  resize: vertical;
}

.btn-submit {
  background-color: #409eff;
  color: white;
  border-radius: 4px;
  padding: 8px 18px;
  font-weight: 600;
  transition: background-color 0.3s;
}
.btn-submit:hover {
  background-color: #66b1ff;
}

.loading {
  font-size: 16px;
  color: #909399;
  text-align: center;
  padding: 20px 0;
}

.post-actions button:hover {
  background-color: #e6f7ff;
  border-color: #91d5ff;
  color: #1890ff;
}

.post-actions button.active {
  background-color: #bae7ff;
  border-color: #40a9ff;
  color: #096dd9;
  box-shadow: 0 0 6px rgb(24 144 255 / 0.6);
}

/* 点赞按钮单独颜色 */
.btn-like {
  color: #fa541c;
  border-color: #ffa39e;
}
.btn-like.active {
  background-color: #ffccc7;
  border-color: #ff4d4f;
  color: #a8071a;
  box-shadow: 0 0 6px rgb(255 77 79 / 0.8);
}

/* 收藏按钮单独颜色 */
.btn-favorite {
  color: #faad14;
  border-color: #ffe58f;
}
.btn-favorite.active {
  background-color: #fff7e6;
  border-color: #ffc53d;
  color: #ad6800;
  box-shadow: 0 0 6px rgb(255 197 0 / 0.8);
}

/* 举报按钮采用警告色 */
.btn-report {
  color: #f5222d;
  border-color: #ffccc7;
}
.btn-report:hover {
  background-color: #fff1f0;
  border-color: #ff4d4f;
  color: #a8071a;
  box-shadow: 0 0 6px rgb(255 77 79 / 0.8);
}
</style>
<template>
  <el-card class="post-edit-card" shadow="hover">
    <el-button type="text" icon="el-icon-arrow-left" @click="goBack" class="back-button">
      返回
    </el-button>

    <h2 class="title">{{ isEdit ? '编辑帖子' : '发布帖子' }}</h2>

    <el-form :model="form" ref="postForm" label-width="60px" @submit.prevent="submitPost" status-icon>
      <el-form-item
          label="标题"
          prop="title"
          :rules="[
          { required: true, message: '请输入标题', trigger: 'blur' },
          { max: 12, message: '标题不能超过12个字符', trigger: 'blur' }
        ]"
      >
        <el-input
            v-model="form.title"
            maxlength="12"
            show-word-limit
            placeholder="请输入帖子标题"
        />
      </el-form-item>

      <el-form-item
          label="内容"
          prop="content"
          :rules="[
          { required: true, message: '请输入内容', trigger: 'blur' },
          { max: 1200, message: '正文不能超过1200个字符', trigger: 'blur' }
        ]"
      >
        <el-input
            type="textarea"
            v-model="form.content"
            rows="10"
            maxlength="1200"
            show-word-limit
            placeholder="请输入帖子内容"
            style="white-space: pre-wrap;"
        />
      </el-form-item>

      <el-form-item>
        <el-button type="primary" :loading="loading" native-type="submit">
          {{ isEdit ? '更新' : '发布' }}
        </el-button>
        <el-button @click="goBack" style="margin-left: 10px;">取消</el-button>
      </el-form-item>
    </el-form>
  </el-card>
</template>

<script setup>
import { reactive, ref, onMounted } from 'vue'
import axios from 'axios'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'

const route = useRoute()
const router = useRouter()

const postId = route.params.id ? Number(route.params.id) : null
const isEdit = ref(!!postId)

const form = reactive({
  title: '',
  content: ''
})

const postForm = ref(null)
const loading = ref(false)

const userId = localStorage.getItem('userId')  // 假设发帖时需要用户ID

const fetchPost = async () => {
  if (!postId) return
  loading.value = true
  try {
    const res = await axios.get(`http://localhost:8080/forum/post/${postId}`)
    form.title = res.data.title
    form.content = res.data.content
  } catch (err) {
    ElMessage.error('加载帖子失败')
    router.back()
  } finally {
    loading.value = false
  }
}

const submitPost = () => {
  postForm.value.validate(async (valid) => {
    if (!valid) return
    loading.value = true
    try {
      if (isEdit.value) {
        // 修改帖子
        await axios.put('http://localhost:8080/forum/post', {
          id: postId,
          userId,
          title: form.title,
          content: form.content
        })
        ElMessage.success('更新成功')
      } else {
        // 新增帖子
        await axios.post('http://localhost:8080/forum/post', {
          userId,
          title: form.title,
          content: form.content
        })
        ElMessage.success('发布成功')
      }
      router.back()
    } catch (err) {
      ElMessage.error(isEdit.value ? '更新失败' : '发布失败')
    } finally {
      loading.value = false
    }
  })
}

const goBack = () => {
  router.back()
}

onMounted(() => {
  fetchPost()
})
</script>

<style scoped>
.post-edit-card {
  max-width: 700px;
  margin: 40px auto;
  padding: 30px 40px;
}

.back-button {
  margin-bottom: 20px;
  font-size: 14px;
  color: #409EFF;
  cursor: pointer;
}

.title {
  text-align: center;
  margin-bottom: 30px;
  font-weight: 600;
  color: #303133;
}
</style>

管理员和用户的运行流程是什么