【前后端的那些事】评论功能实现

news2024/11/28 0:53:29

文章目录

    • 聊天模块
      • 1. 数据库表
      • 2. 后端初始化
        • 2.1 controller
        • 2.2 service
        • 2.3 dao
        • 2.4 mapper
      • 3. 前端初始化
        • 3.1 路由创建
        • 3.2 目录创建
        • 3.3 tailwindCSS安装
      • 4. tailwindUI
      • 5. 前端代码编写

前言:最近写项目,发现了一些很有意思的功能,想写文章,录视频把这些内容记录下。但这些功能太零碎,如果为每个功能都单独搭建一个项目,这明显不合适。于是我想,就搭建一个项目,把那些我想将的小功能全部整合到一起。实现 搭一次环境,处处使用。

本文主要实现以下功能

  1. 评论功能

环境搭建
文章链接

已录制视频
视频链接

仓库地址
https://github.com/xuhuafeifei/fgbg-font-and-back.git

聊天模块

效果展示

在这里插入图片描述

1. 数据库表

CREATE TABLE `communicate` (
  `id` int NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_croatian_ci DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `pid` int DEFAULT NULL,
  `user_id` int DEFAULT NULL,
  `reply_user_id` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_croatian_ci;

2. 后端初始化

2.1 controller

CommunicateController

package com.fgbg.demo.controller;

import com.fgbg.common.utils.R;
import com.fgbg.demo.entity.Communicate;
import com.fgbg.demo.service.CommunicateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequestMapping("comm")
@RestController
public class CommunicateController {
    @Autowired
    private CommunicateService service;

    /**
     * 返回树形结构评论数据
     */
    @RequestMapping("/list")
    public R list() {
        List<Communicate> list = service.listTree();
        return R.ok().put("data", list);
    }

    /**
     * 保存评论
     */
    @RequestMapping("/save")
    public R save(@RequestBody Communicate entity) {
        service.save(entity);
        return R.ok();
    }
}

2.2 service

CommunicateServiceImpl

package com.fgbg.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fgbg.demo.dao.CommunicateDao;
import com.fgbg.demo.entity.Communicate;
import com.fgbg.demo.service.CommunicateService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

/**
 *
 */
@Service
public class CommunicateServiceImpl extends ServiceImpl<CommunicateDao, Communicate>
    implements CommunicateService{

    /**
     * 返回树形评论数据
     *
     * @return
     */
    @Override
    public List<Communicate> listTree() {
        List<Communicate> list = this.list();
        // 映射id->index
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int index = 0; index < list.size(); index++) {
            map.put(list.get(index).getId(), index);
        }
        // 遍历寻找父节点
        for (Communicate communicate : list) {
            Integer pid = communicate.getPid();
            // 有父节点
            if (pid != null) {
                // 获取父节点id
                Integer indexFather = map.get(pid);
                Communicate father = list.get(indexFather);
                if (father.getChildren() == null) {
                    father.setChildren(new ArrayList<>());
                }
                // 在父节点上添加当前节点
                father.getChildren().add(communicate);
            }
        }
        // 过滤出一级节点
        List<Communicate> ans = list.stream().filter(child -> child.getPid() == null).collect(Collectors.toList());
        return ans;
    }
}

2.3 dao

CommunicateDao

package com.fgbg.demo.dao;

import com.fgbg.demo.entity.Communicate;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

/**
 * @Entity com.fgbg.demo.entity.Communicate
 */
@Mapper
public interface CommunicateDao extends BaseMapper<Communicate> {

}
2.4 mapper

CommunicateMapper.xml

<?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.fgbg.demo.dao.CommunicateDao">

    <resultMap id="BaseResultMap" type="com.fgbg.demo.entity.Communicate">
            <id property="id" column="id" jdbcType="INTEGER"/>
            <result property="content" column="content" jdbcType="VARCHAR"/>
            <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
            <result property="pid" column="pid" jdbcType="INTEGER"/>
            <result property="userId" column="user_id" jdbcType="INTEGER"/>
            <result property="replyUserId" column="reply_user_id" jdbcType="INTEGER"/>
    </resultMap>

    <sql id="Base_Column_List">
        id,content,create_time,
        pid,user_id
    </sql>
</mapper>

3. 前端初始化

3.1 路由创建

/src/router/modules/communicate.ts

const { VITE_HIDE_HOME } = import.meta.env;
const Layout = () => import("@/layout/index.vue");

export default {
  path: "/communicate",
  name: "communicate",
  component: Layout,
  redirect: "/communicate",
  meta: {
    icon: "homeFilled",
    title: "沟通",
    rank: 0
  },
  children: [
    {
      path: "/communicate",
      name: "communicate",
      component: () => import("@/views/communicate/communicate.vue"),
      meta: {
        title: "评论",
        showLink: VITE_HIDE_HOME === "true" ? false : true
      }
    }
  ]
} as RouteConfigsTable;
3.2 目录创建

/src/views/communicate/communicate.vue

3.3 tailwindCSS安装
  • 安装

    pnpm install -D tailwindcss postcss autoprefixer
    
  • 输入命令初始化tailwind和postcss配置文件

    npx tailwindcss init -p
    
  • 打开vue项目,在src目录下新建一个css文件:index.css,在文件中写入

    @tailwind base;
     
    @tailwind components;
     
    @tailwind utilities;
    
  • main.ts中引入

    import './index.css'
    

检查tailwind.config.js。我的项目中,文件代码为

/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: "class",
  corePlugins: {
    preflight: false
  },
  content: ["./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}"],
  theme: {
    extend: {
      colors: {
        bg_color: "var(--el-bg-color)",
        primary: "var(--el-color-primary)",
        text_color_primary: "var(--el-text-color-primary)",
        text_color_regular: "var(--el-text-color-regular)"
      }
    }
  }
};

stylelint.config.js

module.exports = {
  root: true,
  extends: [
    "stylelint-config-standard",
    "stylelint-config-html/vue",
    "stylelint-config-recess-order"
  ],
  plugins: ["stylelint-order", "stylelint-prettier", "stylelint-scss"],
  overrides: [
    {
      files: ["**/*.(css|html|vue)"],
      customSyntax: "postcss-html"
    },
    {
      files: ["*.scss", "**/*.scss"],
      customSyntax: "postcss-scss",
      extends: [
        "stylelint-config-standard-scss",
        "stylelint-config-recommended-vue/scss"
      ]
    }
  ],
  rules: {
    "selector-class-pattern": null,
    "no-descending-specificity": null,
    "scss/dollar-variable-pattern": null,
    "selector-pseudo-class-no-unknown": [
      true,
      {
        ignorePseudoClasses: ["deep", "global"]
      }
    ],
    "selector-pseudo-element-no-unknown": [
      true,
      {
        ignorePseudoElements: ["v-deep", "v-global", "v-slotted"]
      }
    ],
    "at-rule-no-unknown": [
      true,
      {
        ignoreAtRules: [
          "tailwind",
          "apply",
          "variants",
          "responsive",
          "screen",
          "function",
          "if",
          "each",
          "include",
          "mixin",
          "use"
        ]
      }
    ],
    "rule-empty-line-before": [
      "always",
      {
        ignore: ["after-comment", "first-nested"]
      }
    ],
    "unit-no-unknown": [true, { ignoreUnits: ["rpx"] }],
    "order/order": [
      [
        "dollar-variables",
        "custom-properties",
        "at-rules",
        "declarations",
        {
          type: "at-rule",
          name: "supports"
        },
        {
          type: "at-rule",
          name: "media"
        },
        "rules"
      ],
      { severity: "warning" }
    ]
  },
  ignoreFiles: ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"]
};
  • 测试是否引入成功

    <p class=" text-2xl font-bold">Hello Tailwind!</p>
    

    如果出现css样式,则表明引入成功

4. tailwindUI

聊天框样式涉及参考了这个大神的UI设计

网址:[comment聊天](comment | Cards (tailwindcomponents.com))

源代码

<div class="flex justify-center relative top-1/3">




<!-- This is an example component -->
<div class="relative grid grid-cols-1 gap-4 p-4 mb-8 border rounded-lg bg-white shadow-lg">
    <div class="relative flex gap-4">
        <img src="https://icons.iconarchive.com/icons/diversity-avatars/avatars/256/charlie-chaplin-icon.png" class="relative rounded-lg -top-8 -mb-4 bg-white border h-20 w-20" alt="" loading="lazy">
        <div class="flex flex-col w-full">
            <div class="flex flex-row justify-between">
                <p class="relative text-xl whitespace-nowrap truncate overflow-hidden">COMMENTOR</p>
                <a class="text-gray-500 text-xl" href="#"><i class="fa-solid fa-trash"></i></a>
            </div>
            <p class="text-gray-400 text-sm">20 April 2022, at 14:88 PM</p>
        </div>
    </div>
    <p class="-mt-4 text-gray-500">Lorem ipsum dolor sit amet consectetur adipisicing elit. <br>Maxime quisquam vero adipisci beatae voluptas dolor ame.</p>
</div>



</div>

在这里插入图片描述

tip: 本项目编写时,对其代码进行一定程度的调整

5. 前端代码编写

笔者考虑篇幅问题,没有封装组件。读者可以尝试着将聊天代码封装为组件,一级评论一个样式,回复评论一个样式。通过这样的方式实现代码复用。

<script setup lang="ts">
import { CommunicateEntity, list, save } from "/src/api/communicate.ts";
import { ElMessage } from "element-plus";
import { ref, onMounted } from "vue";

const input = ref("");

const replyInput = ref("");

const chatList = ref<Array<CommunicateEntity>>();

const submit = (replyUserId?: Number, pid?: Number) => {
  const entity = new CommunicateEntity();
  entity.replyUserId = replyUserId;
  entity.content = input.value;
  entity.pid = pid;
  console.log(entity);

  save(entity).then(res => {
    if (res.code === 0) {
      ElMessage.success("提交成功");
      getData();
    } else {
      ElMessage.error("提交失败: " + res.msg);
    }
  });
};

onMounted(() => {
  getData();
});

const getData = () => {
  list().then(res => {
    console.log(res);
    chatList.value = res.data;
  });
};

// 模拟获取用户信息(一般用户信息会在登陆时, 存储在浏览器本地)
const getUser = (userId: Number) => {
  return "测试人员";
};
</script>

<template>
  <div style="border: 1px solid #ccc; margin-top: 10px">
    <el-input v-model="input" textarea style="height: 200px" />
    <el-button @click="submit()">提交</el-button>
    <el-divider />
    <div v-for="item in chatList" :key="item.id">
      <!-- This is an example component -->
      <div class="relative gap-4 p-6 rounded-lg mb-8 bg-white border">
        <div class="relative flex gap-4">
          <img
            src="https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png"
            class="relative rounded-lg -top-8 -mb-4 bg-white border h-20 w-20"
            alt=""
            loading="lazy"
          />
          <div class="flex flex-col w-full">
            <div class="flex flex-row justify-between">
              <p
                class="relative text-xl whitespace-nowrap truncate overflow-hidden"
              >
                {{ getUser(item.id) }}
              </p>
              <a class="text-gray-500 text-xl" href="#"
                ><i class="fa-solid fa-trash"
              /></a>
            </div>
            <p class="text-gray-400 text-sm">{{ item.createTime }}</p>
          </div>
        </div>
        <p class="-mt-4 text-gray-500">
          {{ item.content }}
        </p>
        <!-- 回复按钮 -->
        <div>
          <el-popover placement="bottom-start" trigger="click" :width="200">
            <template #reference>
              <el-button style="float: right" link type="primary"
                >回复</el-button
              >
            </template>
            <el-input v-model="input" />
            <el-button @click="submit(item.userId, item.id)" style="width: 100%"
              >确定</el-button
            >
          </el-popover>
        </div>
      </div>
      <!-- 回复 -->
      <div v-for="subItem in item.children" :key="subItem.id">
        <div
          class="relative gap-4 p-6 rounded-lg mb-8 bg-white border"
          style="margin-left: 50px"
        >
          <div class="relative flex gap-4">
            <img
              src="https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png"
              class="relative rounded-lg -top-8 -mb-4 bg-white border h-20 w-20"
              alt=""
              loading="lazy"
            />

            <div class="flex flex-col w-full">
              <div class="flex flex-row justify-between">
                <p
                  class="relative text-xl whitespace-nowrap truncate overflow-hidden"
                >
                  {{ getUser(subItem.userId) }} 回复
                  <span style="color: cornflowerblue"
                    >@{{ getUser(subItem.replyUserId) }}</span
                  >
                </p>
                <a class="text-gray-500 text-xl" href="#"
                  ><i class="fa-solid fa-trash"
                /></a>
              </div>
              <p class="text-gray-400 text-sm">{{ item.createTime }}</p>
            </div>
          </div>
          <p class="-mt-4 text-gray-500">
            {{ subItem.content }}
          </p>
          <!-- 回复按钮 -->
          <div>
            <el-popover placement="bottom-start" trigger="click" :width="200">
              <template #reference>
                <el-button style="float: right" link type="primary"
                  >回复</el-button
                >
              </template>
              <el-input v-model="input" />
              <el-button
                @click="submit(item.userId, item.id)"
                style="width: 100%"
                >确定</el-button
              >
            </el-popover>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped></style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1392862.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Power Designer 连接 PostgreSQL 逆向工程生成pd表结构操作步骤以及过程中出现的问题解决

一、使用PowerDesigner16.5 链接pg数据库 1.1、启动PD.选择Create Model…。 1.2、选择Model types / Physical Data Model Physical Diagram&#xff1a;选择pgsql直接【ok】 1.3、选择connect 在工具栏选择Database-Connect… 快捷键&#xff1a;ctrlshiftN.如下图&#xff…

第八站:C++面向对象(继承和派生)

继承和派生 派生:由父类派生出子类 继承:子类继承父类(继承不会继承析构函数和构造函数:父类的所有成员函数&#xff0c;以及数据成员&#xff0c;都会被子类继承&#xff01;) "子类派生出的类"会指向"父类被继承的类",父类就是基类 实例1: 先创建一个父…

Flask框架小程序后端分离开发学习笔记《2》构建基础的HTTP服务器

Flask框架小程序后端分离开发学习笔记《2》构建基础的HTTP服务器 Flask是使用python的后端&#xff0c;由于小程序需要后端开发&#xff0c;遂学习一下后端开发。本节提供一个构建简单的本地服务器的代码&#xff0c;仔细看注释&#xff0c;学习每一步的流程&#xff0c;理解服…

react-app框架——使用monaco editor实现online编辑html代码编辑器

文章目录 ⭐前言&#x1f496;react系列文章 ⭐配置monaco-editor&#x1f496;引入react-monaco-editor&#x1f496;引入react-app-rewired&#x1f496;通过config-overrides.js添加monaco插件配置 ⭐编辑代码的react页面配置&#x1f496;扩展 可自定义配置语言 ⭐效果⭐总…

使用 mybatis-plus 的mybaits的一对多时, total和record的不匹配问题

应该是框架的问题&#xff0c;去官方仓库提了个issues&#xff0c;等回复 https://github.com/baomidou/mybatis-plus/issues/5923 背景 发现 record是两条&#xff0c;但是total显示3 使用resultMap一对多时&#xff0c;三条数据会变成两条&#xff0c;但是total确是3条 下…

新能源汽车智慧充电桩方案:基于视频监控的可视化智能监管平台

一、方案概述 TSINGSEE青犀&触角云新能源汽车智慧充电桩方案围绕互联网、物联网、车联网、人工智能、视频技术、大数据、4G/5G等技术&#xff0c;结合云计算、移动支付等&#xff0c;实现充电停车一体化、充电桩与站点管理等功能&#xff0c;达到充电设备与站点的有效监控…

[足式机器人]Part2 Dr. CAN学习笔记-Ch04 Advanced控制理论

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记 - Ch04 Advanced控制理论 1. 绪论2. 状态空间表达State-Space Representation3. Phase Portrait相图&#xff0c;相轨迹3 1. 1-D3 2. 2-D3 3. General Form3 4. Summary3.5. 爱情中的数学-Phase …

Luckysheet类似excel的在线表格(vue)

参考文档&#xff1a;快速上手 | Luckysheet文档 一、引入 在vue项目的public文件夹下的index.html的<head>标签里面引入 <link relstylesheet hrefhttps://cdn.jsdelivr.net/npm/luckysheetlatest/dist/plugins/css/pluginsCss.css /><link relstylesheet hre…

使用 Neo4j 和 LangChain 集成非结构化知识图增强 QA

目前基于大模型的信息检索有两种方法&#xff0c;一种是基于微调的方法&#xff0c;一种是基于 RAG 的方法。 信息检索和知识提取是一个不断发展的领域&#xff0c;随着大型语言模型&#xff08;LLM&#xff09;和知识图的出现&#xff0c;这一领域发生了显着的变化&#xff0…

河南选调生报名照片上传成功,不能大于50kb

河南选调生报名照片要求&#xff1a; 1、上传近期正面免冠证件照 2、照片背景&#xff1a;要求为蓝底 3、照片格式&#xff1a;jpg格式 4、照片宽高比例约为1.3:1.6&#xff0c;大小为130160像素 5、照片大小&#xff1a;50kb以下&#xff0c;最终效果以输出后的大小为准

pytest学习和使用-pytest如何进行分布式测试?(pytest-xdist)

1 什么是分布式测试&#xff1f; 在进行本文之前&#xff0c;先了解些基础知识&#xff0c;什么是分布式测试&#xff1f;分布式测试&#xff1a;是指通过局域网和Internet&#xff0c;把分布于不同地点、独立完成特定功能的测试计算机连接起来&#xff0c;以达到测试资源共享…

SpringMVC JSON数据处理见解6

6.JSON数据处理 6.1.添加json依赖 springmvc 默认使用jackson作为json类库,不需要修改applicationContext-servlet.xml任何配置&#xff0c;只需引入以下类库springmvc就可以处理json数据&#xff1a; <!--spring-json依赖--> <dependency><groupId>com.f…

群晖搭建LDAP服务器实现一个账号登录DSM、Gitea、jellyfin

文章目录 前言安装LDAP Server新建群组新增用户 DSM加入LDAPDSM使用LDAP登录 Gitea配置登录取消其登录权限 Jellyfin配置登录 总结 前言 LDAP&#xff08;轻量级目录访问协议&#xff09;是一种用于访问和管理分布式目录服务的协议&#xff0c;它具有以下好处&#xff1a; 集…

flutter开发windows桌面软件,使用Inno Setup打包成安装程序,支持中文

最近使用flutter开发windows桌面软件的时候&#xff0c;想要将软件打包成安装程序&#xff0c;使用了flutter官方推荐的msix打包&#xff0c;但是打包出来的软件生成的桌面快捷方式有蓝色背景&#xff1a; 这个蓝色背景应该是没有设置为动态导致的&#xff0c;windows系统的屏幕…

POE工业交换机:点亮灯光控制与建筑自动化的新时代

随着科技的不断发展&#xff0c;灯光控制和建筑自动化在现代建筑中扮演着重要角色。而POE工业交换机作为一种创新的网络设备&#xff0c;不仅能够为灯光控制和建筑自动化提供稳定可靠的网络通信&#xff0c;还具备便捷的供电功能。本文将探讨POE工业交换机对灯光控制和建筑自动…

Unity URP切换品质和Feature开关的性能问题

现在对我的项目进行安卓端发布&#xff0c;需要切换品质和一些Feature开关。 我是这样做的。 划分品质 首先Renerer分为2个Android和PC&#xff0c;图中其他不用参考。 每个副本的URP Asset分为pc和android&#xff0c;例如图中的 hall和hall_android。 我们可以看到hall用的…

【性能调优】local模式模式下flink处理离线任务能力分析

文章目录 一. flink的内存管理1.Jobmanager的内存模型2.TaskManager的内存模型2.1. 模型说明2.2. 通讯、数据传输方面2.3. 框架、任务堆外内存2.4. 托管内存 3.任务分析 二. 单个节点的带宽瓶颈1. 带宽相关理论2. 使用speedtest-cli 测试带宽3. 任务分析3. 其他工具使用介绍 本…

使用CSS计算高度铺满屏幕

前言 今天写项目时出现高度设置百分百却不占满屏幕&#xff0c;第一反应看自己设置的是块级元素还是行级元素。看了几篇博客&#xff0c;发现并不能解决问题。脱离文档流的做法都没考虑&#xff0c;前期模板搭建脱离文档流&#xff0c;后面开发会出现很多问题。 以上图片是我…

几何_直线方程 Ax + By + C = 0 的系数A,B,C几何含义是?

参考&#xff1a; 直线方程 Ax By C 0 的系数A&#xff0c;B&#xff0c;C有什么几何含义&#xff1f;_设直线 l 的方程为axbyc0 怎么理解-CSDN博客 1. A B的含义&#xff1a;组成一个与直线垂直的向量 我们先来看A和B有什么含义。 在直线上取任意两点 P1:&#xff08;x1…

无界面自动化测试(IDEA+Java+Selenium+testng)(PhantomJS)

自动化测试&#xff08;IDEAJavaSeleniumtestng&#xff09;(PhantomJS)_phantomjs怎么写js脚本idea-CSDN博客 上述连接是参考&#xff1a;现在如果按照如上链接进行操作大概率会失败&#xff0c;下面会针对如上链接的部分步骤做出修改 1、在pom.xml文件中需要使用低版本sele…