vue实现留言,回复留言

首先进行分析:

        

        一级评论就是父评论,二级评论三级评论四级评论是子评论,通过@name的方式进行回复父级评论

响应的格式是:(下面是一个有二级评论的响应格式)

{
    "code": 200,
    "rows": [
        {
            "createBy": null,
            "createTime": null,
            "updateBy": null,
            "updateTime": null,
            "remark": null,
            "id": 172,
            "content": "<p>aaa</p>",
            "username": "admin",
            "time": "2025-01-06 23:16:14",
            "parentId": null,
            "foreignId": 0,
            "children": [
                {
                    "createBy": null,
                    "createTime": null,
                    "updateBy": null,
                    "updateTime": null,
                    "remark": null,
                    "id": 173,
                    "content": "@admin zzz",
                    "username": "admin",
                    "time": "2025-01-07 17:34:47",
                    "parentId": 172,
                    "foreignId": 0,
                    "children": null,
              
                }
            ]
        },
       

一、后端实现

1.创建springboot工程

2.创建留言实体类

package com.ruoyi.comment.domain;

import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;

import javax.validation.constraints.Past;

/**
 * 求职论坛对象 comment
 * 
 * @author ruoyi
 * @date 2024-12-11
 */
public class Comment extends BaseEntity {
    private static final long serialVersionUID = 1L;

    /**
     * id
     */
    private Long id;

    /**
     * 内容
     */
    @Excel(name = "内容")
    private String content;

    /**
     * 用户id
     */
    @Excel(name = "用户id")
    private String username;

    /**
     * 时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd")
    @Excel(name = "时间", width = 30, dateFormat = "yyyy-MM-dd")
    private String time;

    /**
     * 父id
     */
    @Excel(name = "父id")
    private Long parentId;

    /**
     * 关联id
     */
    @Excel(name = "关联id")
    private Long foreignId;

    private List<Comment> children;


    public List<Comment> getChildren() {
        return children;
    }

    public void setChildren(List<Comment> children) {
        this.children = children;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public void setTime(String time) {
        this.time = time;
    }

    public String getTime() {
        return time;
    }

    public void setParentId(Long parentId) {
        this.parentId = parentId;
    }

    public Long getParentId() {
        return parentId;
    }

    public void setForeignId(Long foreignId) {
        this.foreignId = foreignId;
    }

    public Long getForeignId() {
        return foreignId;
    }

    public String getUsername() {
        return username;
    }

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

    @Override
    public String toString() {
        return "Comment{" +
                "id=" + id +
                ", content='" + content + '\'' +
                ", username='" + username + '\'' +
                ", time='" + time + '\'' +
                ", parentId=" + parentId +
                ", foreignId=" + foreignId +
                ", children=" + children +
                '}';
    }
}


3.操作留言表的mapper接口

package com.ruoyi.comment.mapper;

import java.util.List;
import com.ruoyi.comment.domain.Comment;
import org.apache.ibatis.annotations.Param;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;

/**
 * 求职论坛Mapper接口
 * 
 * @author ruoyi
 * @date 2024-12-11
 */
public interface CommentMapper 
{
    /**
     * 查询求职论坛
     * 
     * @param id 求职论坛主键
     * @return 求职论坛
     */
    public Comment selectCommentById(Long id);

    /**
     * 查询求职论坛列表
     * 
     * @param comment 求职论坛
     * @return 求职论坛集合
     */
    public List<Comment> selectCommentList(Comment comment);
    public List<Comment> selectCommentListByid(Long id);
    public List<Comment> selectCommentByParentId(@Param("parentId")Long parentId);

    /**
     * 新增求职论坛
     * 
     * @param comment 求职论坛
     * @return 结果
     */
    public int insertComment(Comment comment);

   

    /**
     * 删除求职论坛
     * 
     * @param id 求职论坛主键
     * @return 结果
     */
    public int deleteCommentById(Long id);

   
}

4.服务类

package com.ruoyi.comment.service.impl;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.comment.mapper.CommentMapper;
import com.ruoyi.comment.domain.Comment;
import com.ruoyi.comment.service.ICommentService;

/**
 * 求职论坛Service业务层处理
 * 
 * @author ruoyi
 * @date 2024-12-11
 */
@Service
public class CommentServiceImpl implements ICommentService 
{
    @Autowired
    private CommentMapper commentMapper;

    /**
     * 查询求职论坛
     * 
     * @param id 求职论坛主键
     * @return 求职论坛
     */
    @Override
    public Comment selectCommentById(Long id)
    {
        return commentMapper.selectCommentById(id);
    }

    /**
     * 查询求职论坛列表
     * 
     * @param comment 求职论坛
     * @return 求职论坛
     */
    @Override
    public List<Comment> selectCommentList(Comment comment)
    {
        return commentMapper.selectCommentList(comment);
    }

    @Override
    public List<Comment> selectCommentByParentId(Long parentId) {
        return commentMapper.selectCommentByParentId(parentId);
    }

    /**
     * 新增求职论坛
     * 
     * @param comment 求职论坛
     * @return 结果
     */
    @Override
    public int insertComment(Comment comment)
    {
        return commentMapper.insertComment(comment);
    }

    @Override
    public List<Comment> selectCommentListByid(Long id) {
        return commentMapper.selectCommentListByid(id);
    }

   
    /**
     * 删除求职论坛信息
     * 
     * @param id 求职论坛主键
     * @return 结果
     */
    @Override
    public int deleteCommentById(Long id)
    {
        return commentMapper.deleteCommentById(id);
    }
}

 5.编写Controller层

package com.ruoyi.comment.controller;

import java.lang.reflect.Array;
import java.util.*;
import javax.servlet.http.HttpServletResponse;

import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.system.service.ISysUserService;
import org.apache.poi.ss.usermodel.DateUtil;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.comment.domain.Comment;
import com.ruoyi.comment.service.ICommentService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;

/**
 * 求职论坛Controller
 * 
 * @author ruoyi
 * @date 2024-12-11
 */
@RestController
@RequestMapping("/comment/comment")
public class CommentController extends BaseController
{
    @Autowired
    private ICommentService commentService;


    /**
     * 查询求职论坛列表
     */
    @PreAuthorize("@ss.hasPermi('comment:comment:list')")
    @GetMapping("/list")
    public TableDataInfo list(Comment comment) {
        List<Comment> list = commentService.selectCommentList(comment);
        List<Comment> rootComments = new ArrayList<>();
        Map<Long, List<Comment>> childCommentsMap = new HashMap<>();

        for (Comment comment1 : list) {
            Long parentId = comment1.getParentId();
            if (parentId == null) {
                rootComments.add(comment1);
            } else {
                childCommentsMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(comment1);
            }
        }

        for (Comment rootComment : rootComments) {
            Long rootCommentId = rootComment.getId();
            List<Comment> childComments = childCommentsMap.get(rootCommentId);
            if (childComments != null) {
                rootComment.setChildren(childComments);
            }
        }

        return pagination(rootComments);
    }


    /**
         * 新增求职论坛
         */
    @PreAuthorize("@ss.hasPermi('comment:comment:add')")
    @Log(title = "求职论坛", businessType = BusinessType.INSERT)
    @PostMapping("/add")
    public AjaxResult add(@RequestBody Comment comment)
    {

        comment.setUsername(SecurityUtils.getUsername());
        comment.setTime(DateUtils.getTime());
        return toAjax(commentService.insertComment(comment));
    }


    /**
     * 删除求职论坛
     */
    @PreAuthorize("@ss.hasPermi('comment:comment:remove')")
    @Log(title = "求职论坛", businessType = BusinessType.DELETE)
	@DeleteMapping("/{ids}")
    public AjaxResult remove(@PathVariable Long[] ids)
    {
        return toAjax(commentService.deleteCommentByIds(ids));
    }
}

6.编写sql语句

<?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.ruoyi.comment.mapper.CommentMapper">

    <resultMap type="com.ruoyi.comment.domain.Comment" id="CommentResult">
        <result property="id"    column="id"    />
        <result property="content"    column="content"    />
        <result property="username"    column="username"    />
        <result property="time"    column="time"    />
        <result property="parentId"    column="parent_id"    />
        <result property="foreignId"    column="foreign_id"    />
        <collection property="children" ofType="com.ruoyi.comment.domain.Comment">
            <id property="id" column="parent_id"/>
            <result property="content" column="parent_content"/>
            <result property="username" column="parent_username"/>
            <result property="time" column="parent_time"/>
            <result property="parentId" column="parent_parent_id"/>
            <result property="foreignId" column="parent_foreign_id"/>
        </collection>
    </resultMap>

    <select id="selectCommentList" resultMap="CommentResult">
        SELECT c.*, p.*
        FROM comment c
        LEFT JOIN comment p ON c.parent_id = p.id
        <where>
            <if test="content != null and content != ''">
                AND c.content = #{content}
            </if>
            <if test="username != null">
                AND c.username = #{username}
            </if>
            <if test="parentId != null">
                AND c.parent_id = #{parentId}
            </if>
            <if test="foreignId != null">
                AND c.foreign_id = #{foreignId}
            </if>
        </where>
        ORDER BY
        CASE WHEN c.parent_id IS NULL THEN c.time END DESC,  -- 按时间降序排序父评论
        CASE WHEN c.parent_id IS NOT NULL THEN c.id END ASC  -- 按ID升序排序子评论
    </select>


    <select id="selectCommentById" parameterType="Long" resultType="com.ruoyi.comment.domain.Comment">
   select * from comment where id = #{id}
    </select>

    <insert id="insertComment" parameterType="Comment" useGeneratedKeys="true" keyProperty="id">
        insert into comment
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="content != null">content,</if>
            <if test="username != null">username,</if>
            <if test="time != null">time,</if>
            <if test="parentId != null">parent_id,</if>
            <if test="foreignId != null">foreign_id,</if>
         </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="content != null">#{content},</if>
            <if test="username != null">#{username},</if>
            <if test="time != null">#{time},</if>
            <if test="parentId != null">#{parentId},</if>
            <if test="foreignId != null">#{foreignId},</if>
         </trim>
    </insert>

    <update id="updateComment" parameterType="Comment">
        update comment
        <trim prefix="SET" suffixOverrides=",">
            <if test="content != null">content = #{content},</if>
            <if test="username != null">username = #{username},</if>
            <if test="time != null">time = #{time},</if>
            <if test="parentId != null">parent_id = #{parentId},</if>
            <if test="foreignId != null">foreign_id = #{foreignId},</if>
        </trim>
        where id = #{id}
    </update>

    <delete id="deleteCommentById" parameterType="Long">
        delete from comment where id = #{id}
    </delete>

    <delete id="deleteCommentByIds" parameterType="String">
        delete from comment where id in
        <foreach item="id" collection="array" open="(" separator="," close=")">
            #{id}
        </foreach>
    </delete>
</mapper>

二、搭建前端

1.编写api 

import request from '@/utils/request'

// 查询求职论坛列表
export function listComment(query) {
  return request({
    url: '/comment/comment/list',
    method: 'get',
    params: query
  })
}

// 查询求职论坛详细
export function getComment(id) {
  return request({
    url: '/comment/comment/' + id,
    method: 'get'
  })
}

// 新增求职论坛
export function addComment(data) {
  return request({
    url: '/comment/comment/add',
    method: 'post',
    data: data
  })
}



// 删除求职论坛
export function delComment(id) {
  return request({
    url: '/comment/comment/' + id,
    method: 'delete'
  })
}

2. 写前端的模块

首先是写前端的留言的发布以及回复和删除评论

<template>
  <div id="building" style="margin-top: 10px; margin-bottom: 80px">
    <el-card>
      <div class="text-container">
        <div style="padding: 20px; color: #888;">
          <quill-editor
            v-model="entity.content"
            :options="editorOptions"
            placeholder="添加评论..."
            @blur="onEditorBlur"
          ></quill-editor>
          <div style="text-align: right; padding: 10px;">
            <el-button type="primary" @click="save">发送</el-button>
          </div>
        </div>

        <!-- 渲染评论列表 -->
        <div class="comment-list">
          <comment-item
            v-for="item in commentsList"
            :key="item.id"
            :item="item"
            :depth="0"
            @reply="reply"
            @delete="del"
          ></comment-item>

        </div>

        <!-- 分页控件 -->
        <el-pagination
          v-if="commentsList.length > 0"
          @current-change="handleCurrentChange"
          :current-page="queryParam.pageNum"
          :page-size="queryParam.pageSize"
          :total="totalComments"
          layout="total, prev, pager, next, jumper"
        ></el-pagination>
      </div>
      <img src="/static/img/img.png" alt="背景图片" class="background-image">
    </el-card>
  </div>
</template>

<script>
import { quillEditor } from 'vue-quill-editor';
import request from "@/utils/request";
import { listComment } from "@/api/comment/comment";
import CommentItem from "@/views/comment/comment/CommentItem";
import 'quill/dist/quill.core.css'; // Quill 样式
import 'quill/dist/quill.snow.css'; // Quill 雪花主题样式
import 'quill/dist/quill.bubble.css'; // Quill 气泡主题样式
import 'vue-quill-editor/dist/ssr'; // SSR 支持

export default {
  name: "Comment",
  components: {
    CommentItem,
    quillEditor
  },
  data() {
    return {
      loading: true,
      commentsList: [],
      totalComments: 0, // 用于保存总评论数
      entity: {
        content: '',
        parentId: null // 回复关联的父评论ID
      },
      queryParam: {
        pageNum: 1,
        pageSize: 10,
        id: '',
      },
      editorOptions: {
        modules: {
          toolbar: [
            [{ 'header': [1, 2, false] }],
            ['bold', 'italic', 'underline'],
            ['link', 'image'],
            ['clean'] // 清除格式
          ]
        },
        placeholder: '添加评论...',
      }
    };
  },
  created() {
    this.getlist(); // 组件创建时获取评论列表
  },
  methods: {
    onEditorBlur() {
      // 可以处理编辑器失去焦点的逻辑
    },
    getlist() {
      this.loading = true;
      listComment(this.queryParam).then(res => {
        this.commentsList = res.rows; // 评论数据
        this.totalComments = res.total; // 假设返回数据包含总评论数
        this.loading = false;
      });
    },
    handleCurrentChange(pageNum) {
      this.queryParam.pageNum = pageNum; // 更新当前页码
      this.getlist(); // 获取新页码的评论列表
    },
    reply(replyData) {
      const dataToSend = {
        content: replyData.content,
        parentId: replyData.parentId,
      };

      request.post("/comment/comment/add", dataToSend).then(() => {
        this.$message({
          message: "回复成功",
          type: "success"
        });
        this.getlist(); // 刷新评论列表
      });
    },
    del(id) {
      request.delete("/comment/comment/" + id).then(() => {
        this.$message({
          message: "删除成功",
          type: "success"
        });
        this.getlist(); // 刷新评论列表
      });
    },
    save() {
      const dataToSend = {
        content: this.entity.content,
        parentId: null // 顶级评论的parentId为null
      };

      request.post("/comment/comment/add", dataToSend).then(() => {
        this.$message({
          message: "评论成功",
          type: "success"
        });
        this.entity.content = ''; // 重置输入
        this.getlist(); // 刷新评论列表
      });
    }
  }
};
</script>

<style scoped>
#building {
  position: relative;
  width: 100%;
  height: 100%;
  padding: 20px;
  border-radius: 10px;
}

.text-container {
  position: relative;
  z-index: 2;
}

.comment-list {

  padding: 20px;                     /* 内边距 */
  background: transparent;           /* 透明背景 */
  margin-top: 20px;                  /* 上部间距 */
}

.background-image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 1;
  opacity: 0.5; /* 背景透明度 */
  pointer-events: none;
}
</style>

3.展示子评论的模块

<template>
  <div :style="{ paddingLeft: `${depth * 20}px`, marginBottom: '10px' }">
    <div class="comment" v-if="item.content">
      <strong>{{ item.username }}:</strong>
      <div class="comment-content"><span v-html="item.content"></span></div>
      <span class="comment-time">{{ item.time }}</span>
      <el-button size="mini" @click="toggleReply">回复</el-button>
      <el-button size="mini" @click="$emit('delete', item.id)">删除</el-button>
    </div>

    <!-- 回复框 -->
    <div v-if="isReplying" class="reply-container">
      <el-input
        type="textarea"
        v-model="replyContent"
        :placeholder="getReplyPlaceholder"
        rows="2"
        style="width: 100%;"
      ></el-input>
      <el-button type="primary" @click="submitReply">提交回复</el-button>
    </div>

    <!-- 渲染子评论 -->
    <div v-if="item.children && item.children.length > 0" class="children-container">
      <comment-item
        v-for="child in item.children"
        :key="child.id"
        :item="child"
        :depth="depth + 1"
        @reply="$emit('reply', child)"
        @delete="$emit('delete', child.id)"
      ></comment-item>
    </div>

  </div>
</template>

<script>
export default {
  name: 'CommentItem',
  props: {
    item: {
      type: Object,
      required: true
    },
    depth: {
      type: Number,
      default: 0
    }

  },
  data() {
    return {
      isReplying: false,
      replyContent: ''
    };
  },
  computed: {
    getReplyPlaceholder() {
      return this.depth === 0
        ? "请输入评论..."
        : `@${this.item.username} 请输入回复...`; // 动态生成占位符
    }
  },
  methods: {
    toggleReply() {
      this.isReplying = !this.isReplying;
      if (this.isReplying) {
        this.replyContent = `@${this.item.username} `; // 添加 @username
      }
    },
    submitReply() {
      if (!this.replyContent.trim()) {
        this.$message({
          message: "回复内容不能为空!",
          type: "warning"
        });
        return;
      }

      this.$emit('reply', {
        content: this.replyContent,
        parentId: this.item.id // 传递当前评论的ID作为父ID
      });

      this.replyContent = '';
      this.isReplying = false; // 隐藏回复输入框
    }
  }
};
</script>

<style scoped>
.comment {
  background: #f5f5f5;
  border: 1px solid #eee;
  border-radius: 5px;
  padding: 10px;
  margin-bottom: 5px;
}


.comment-content {
  padding: 15px;                     /* 内边距 */
  margin-bottom: 10px;               /* 下边距 */
  background: transparent;           /* 透明背景 */
  border: none;
  /* 背景模糊效果 */

}
.reply-container {
  margin-top: 10px;
}

.comment-time {
  color: #888;
  font-size: 12px;
  margin-left: 10px;
}

.children-container {
  margin-left: 20px; /* 子评论的缩进 */
}
</style>

四、最后展示

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值