使用Vue+Django+Ant Design做一个留言评论模块的示例代码

1.总览

留言的展示参考网络上参见的格式,如掘金社区:

一共分为两层,子孙留言都在第二层中

最终效果如下:

接下是数据库的表结构,如下所示:

有一张user表和留言表,关系为一对多,留言表有父留言字段的id,和自身有一个一对多的关系,建表语句如下:

CREATE TABLE `message` (
 `id` int NOT NULL AUTO_INCREMENT,
 `date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
 `content` text NOT NULL,
 `parent_msg_id` int DEFAULT NULL,
 `user_id` int NOT NULL,
 PRIMARY KEY (`id`),
 KEY `user_id` (`user_id`),
 KEY `message_ibfk_1` (`parent_msg_id`),
 CONSTRAINT `message_ibfk_1` FOREIGN KEY (`parent_msg_id`) REFERENCES `message` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
 CONSTRAINT `message_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8

CREATE TABLE `user` (
 `id` int NOT NULL AUTO_INCREMENT,
 `username` varchar(255) NOT NULL,
 `password` varchar(255) NOT NULL,
 `identity` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8

2.后台接口

2.1获取留言接口

在Django的views.py中定义两个接口,一个负责提供留言内容,一个负责插入留言,如下:

# 获取留言信息
@require_http_methods(['GET'])
def findAllMsg(request):
  response = {}
  try:
    sql = '''
    SELECT
    msg1.*,
    user.username,
    msg2.username AS parent_msg_username
    FROM message msg1
    LEFT JOIN
    (SELECT
    m.id,
    user.username
    FROM message m
    LEFT JOIN USER
    ON m.user_id = user.id
    )AS msg2
    ON msg1.parent_msg_id = msg2.id
    LEFT JOIN USER
    ON msg1.user_id = user.id
    ORDER BY msg1.date DESC;
    '''
    with connection.cursor() as cursor:
      cursor.execute(sql)
      response['messages'] = sortMsg(cursor)
    response['status_code'] = 200
  except Exception as e:
    response['status_code'] = 500
    response['error'] = e

  return JsonResponse(response)

先来看看这个sql能查出些什么东西:

上面接口中的sorMsg()函数用于整理留言信息,使子留言和父留言能对应起来,算法实现如下:

# 整理留言信息返回格式
def sortMsg(cursor):
  list = []
  allMsg = dictfetchall(cursor)
  for i in range(len(allMsg)):
    tmpParent = allMsg[i]
    tmpChild = []
    # 如果没有属于根评论,则搜索该评论下的所有子评论
    if tmpParent.get('parent_msg_id') == None:
      tmpChild = bfs(tmpParent, allMsg)
    # 如果是子评论则跳过,子评论最终会出现在根评论的子节点中
    else:
      continue
    tmpParent['children'] = tmpChild
    # 格式化时间
    tmpParent['date'] = datetime.datetime.strftime(tmpParent['date'], '%Y-%m-%d %H:%M:%S')
    list.append(tmpParent)
  return list

# 搜索一条留言的所有子留言,广度优先
import queue
def bfs(parent, allMsg):
  childrenList = []
  q = queue.Queue()
  q.put(parent)
  while(not q.empty()):
    tmpChild = q.get()
    for i in range(len(allMsg)):
      if allMsg[i]['parent_msg_id'] is not None and allMsg[i]['parent_msg_id'] == tmpChild['id']:
        childrenList.append(allMsg[i])
        q.put(allMsg[i])
  # 子留言列表按时间降序排序
  childrenList = sorted(childrenList, key = lambda d: d['date'], reverse = True)
  # 格式化日期格式
  for item in childrenList:
    item['date'] = datetime.datetime.strftime(item['date'], '%Y-%m-%d %H:%M:%S')
  return childrenList

用postman测试接口,得到的json格式如下:

{
  "messages": [
    {
      "id": 12,
      "date": "2020-05-31 12:19:43",
      "content": "你好啊,太棒了",
      "parent_msg_id": null,
      "user_id": 5,
      "username": "wangwu",
      "parent_msg_username": null,
      "children": []
    },
    {
      "id": 11,
      "date": "2020-05-31 12:18:55",
      "content": "的时刻层6666666632\n2面的思考名称看到什么材料是isdafjoisdjiojildsc",
      "parent_msg_id": null,
      "user_id": 3,
      "username": "zhangsan",
      "parent_msg_username": null,
      "children": []
    },
    {
      "id": 5,
      "date": "2020-05-29 19:09:33",
      "content": "发的发射点发吖方吖是发是呵等方5爱的非4阿瑟东方 发",
      "parent_msg_id": null,
      "user_id": 4,
      "username": "lisi",
      "parent_msg_username": null,
      "children": [
        {
          "id": 13,
          "date": "2020-05-31 12:20:12",
          "content": "号好好好矮好矮好矮好好",
          "parent_msg_id": 5,
          "user_id": 6,
          "username": "zhaoliu",
          "parent_msg_username": "lisi"
        }
      ]
    },
    {
      "id": 1,
      "date": "2020-05-29 19:06:21",
      "content": "fasfdsafas法阿萨德方吖65阿瑟东方5是的发",
      "parent_msg_id": null,
      "user_id": 1,
      "username": "student",
      "parent_msg_username": null,
      "children": [
        {
          "id": 7,
          "date": "2020-05-29 19:29:29",
          "content": "hfhf2h22h222223232",
          "parent_msg_id": 6,
          "user_id": 1,
          "username": "student",
          "parent_msg_username": "zhaoliu"
        },
        {
          "id": 6,
          "date": "2020-05-29 19:09:56",
          "content": "而离开离开邻居哦i据哦i报价哦v保健品45465",
          "parent_msg_id": 4,
          "user_id": 6,
          "username": "zhaoliu",
          "parent_msg_username": "mike"
        },
        {
          "id": 4,
          "date": "2020-05-29 19:09:14",
          "content": "发送端非场地萨擦手d5asd32 1dads\r\ndsac十多次ds出错",
          "parent_msg_id": 2,
          "user_id": 8,
          "username": "mike",
          "parent_msg_username": "lisi"
        },
        {
          "id": 3,
          "date": "2020-05-29 19:08:56",
          "content": "奋发恶法撒打发士大夫士大夫是大 大师傅撒",
          "parent_msg_id": 2,
          "user_id": 2,
          "username": "teacher",
          "parent_msg_username": "lisi"
        },
        {
          "id": 2,
          "date": "2020-05-29 19:08:41",
          "content": "fasdfasdf发生的法撒旦飞洒多发点房地产",
          "parent_msg_id": 1,
          "user_id": 4,
          "username": "lisi",
          "parent_msg_username": "student"
        }
      ]
    }
  ],
  "status_code": 200
}

这个就是前台所要的内容了。

其实一开始我是很直观地认为是用深度优先来取出层层嵌套的留言的,如下:

# 递归搜索一条留言的所有子留言,深度优先
def dfs(parent, allMsg):
  childrenList = []
  for i in range(len(allMsg)):
    if allMsg[i]['parent_msg_id'] is not None and allMsg[i]['parent_msg_id'] == parent['id']:
      allMsg[i]['children'] = dfs(allMsg[i], allMsg)
      childrenList.append(allMsg[i])
  return childrenList

这样取出的json格式是这样的:

{
  "messages": [
    {
      "id": 5,
      "date": "2020-05-29 19:09:33",
      "content": "发的发射点发吖方吖是发是呵等方5爱的非4阿瑟东方 发",
      "parent_msg_id": null,
      "user_id": 4,
      "username": "lisi",
      "children": [
        {
          "id": 8,
          "date": "2020-05-29T17:23:37",
          "content": "哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈呵呵呵呵呵呵",
          "parent_msg_id": 5,
          "user_id": 3,
          "username": "zhangsan",
          "children": []
        }
      ]
    },
    {
      "id": 1,
      "date": "2020-05-29 19:06:21",
      "content": "fasfdsafas法阿萨德方吖65阿瑟东方5是的发",
      "parent_msg_id": null,
      "user_id": 1,
      "username": "student",
      "children": [
        {
          "id": 2,
          "date": "2020-05-29T19:08:41",
          "content": "fasdfasdf发生的法撒旦飞洒多发点房地产",
          "parent_msg_id": 1,
          "user_id": 4,
          "username": "lisi",
          "children": [
            {
              "id": 4,
              "date": "2020-05-29T19:09:14",
              "content": "发送端非场地萨擦手d5asd32 1dads\r\ndsac十多次ds出错",
              "parent_msg_id": 2,
              "user_id": 8,
              "username": "mike",
              "children": [
                {
                  "id": 6,
                  "date": "2020-05-29T19:09:56",
                  "content": "而离开离开邻居哦i据哦i报价哦v保健品45465",
                  "parent_msg_id": 4,
                  "user_id": 6,
                  "username": "zhaoliu",
                  "children": [
                    {
                      "id": 7,
                      "date": "2020-05-29T19:29:29",
                      "content": "hfhf2h22h222223232",
                      "parent_msg_id": 6,
                      "user_id": 1,
                      "username": "student",
                      "children": []
                    }
                  ]
                }
              ]
            },
            {
              "id": 3,
              "date": "2020-05-29T19:08:56",
              "content": "奋发恶法撒打发士大夫士大夫是大 大师傅撒",
              "parent_msg_id": 2,
              "user_id": 2,
              "username": "teacher",
              "children": []
            },
            {
              "id": 9,
              "date": "2020-05-29T17:27:13",
              "content": "alalla啦啦啦啦啦啦来的队列李大水泛滥的萨拉发 的  第三方哈l",
              "parent_msg_id": 2,
              "user_id": 7,
              "username": "joke",
              "children": []
            }
          ]
        }
      ]
    }
  ],
  "status_code": 200
}

但仔细一想,实际页面展示的时候肯定不能这样一层层无限地嵌套下去,否则留言多了页面就装不下了,于是还是改成了两层留言的格式,第二层使用广度优先搜索将树转为列表存储。

2.2 新增留言接口

前台提供留言内容、留言者id以及父留言的id(如果不是回复信息的话就是空)

import datetime

@require_http_methods(['POST'])
def insertMsg(request):
  response = {}
  try:
    request.POST = request.POST.copy()
    request.POST['date'] = datetime.datetime.now()
    msg = Message()
    msg.date = request.POST.get('date')
    msg.content = request.POST.get('content')
    msg.parent_msg_id = request.POST.get('parent_msg_id')
    msg.user_id = request.POST.get('user_id')
    msg.save()
    response['msg'] = 'success'
    response['status_code'] = 200
  except Exception as e:
    response['error'] = str(e)
    response['status_code'] = 500

  return JsonResponse(response)

3.前台设计

有了后台提供的数据,前台展示就比较简单了。

留言板块的设计我使用了Ant Design的留言组件。

留言界面主要由两个组件所构成——留言区组件以及评论表单的组件

3.1主视图Messeage.vue

<template>
 <div>
  <comment-message @handleReply="handleReply" :commentList="comments"></comment-message>
  <comment-area @reload="reload" :parentMsgId="replyMsgId" :replyMsgUsername="replyMsgUsername"></comment-area>
 </div>
</template>

<script>
import CommentMessage from "components/common/comment/CommentMessage";
import CommentArea from "components/common/comment/CommentArea";

import { findAllMsg } from "network/ajax";

export default {
 name: "Message",
 components: {
  CommentMessage,
  CommentArea
 },
 data() {
  return {
   comments: [],
   replyMsgId: "",
   replyMsgUsername: ""
  };
 },
 mounted() {
  findAllMsg()
   .then(res => {
    this.comments = res.data.messages;
   })
   .catch(err => {
    console.log(err);
    this.$router.push("/500");
   });
 },
 methods: {
  handleReply(data) {
   this.replyMsgId = data.msgId;
   this.replyMsgUsername = data.msgUsername;
  },
  reload() {
   this.$emit("reload")
  }
 }
};
</script>

<style>
</style>

3.2 留言区域组件CommentMessage.vue:

<template>
 <div id="commentMsg">
  <div v-if="isEmpty(commentList)" class="head-message">暂无留言内容</div>
  <div v-else class="head-message">留言内容</div>
  <comment
   @handleReply="handleReply"
   v-for="(item1, index) in commentList"
   :key="'parent-' + index"
   :comment="item1"
  >
   <!-- 二层留言 -->
   <template #childComment v-if="!isEmpty(item1.children)">
    <comment
     v-for="(item2, index) in item1.children"
     :key="'children-' + index"
     :comment="item2"
     @handleReply="handleReply"
    ></comment>
   </template>
  </comment>
 </div>
</template>

<script>
import Comment from "./Comment";
import Vue from "vue";

export default {
 name: "CommentMessage",
 components: {
  Comment
 },
 props: {
  commentList: {
   type: Array,
   default: []
  }
 },
 methods: {
  isEmpty(ls) {
   return ls.length === 0;
  },
  handleReply(data) {
   this.$emit("handleReply", {
    msgId: data.msgId,
    msgUsername: data.msgUsername
   });
  }
 }
};
</script>

<style scoped>
.head-message {
 font-size: 20px;
 text-align: center;
}
</style>

3.3 留言区域由多个Comment留言组件所构成,留言组件定义如下

<template>
 <a-comment>
  <span
   slot="actions"
   key="comment-basic-reply-to"
   @click="handlReply(comment.id, comment.username)"
  >
   <a href="#my-textarea">回复</a>
  </span>
  <a slot="author" style="font-size: 15px">{{comment.username}}</a>
  <a
   v-if="comment.parent_msg_username"
   slot="author"
   class="reply-to"
  >@{{comment.parent_msg_username}}</a>
  <a-avatar slot="avatar" :src="require('assets/images/login_logo.png')" alt />
  <p slot="content">{{comment.content}}</p>
  <a-tooltip slot="datetime">
   <span>{{comment.date}}</span>
  </a-tooltip>
  <slot name="childComment"></slot>
 </a-comment>
</template>

<script>
export default {
 name: "Comment",
 props: {
  comment: ""
 },
 methods: {
  handlReply(msgId, msgUsername) {
   this.$emit("handleReply", { msgId, msgUsername });
  }
 }
};
</script>

<style scoped>
.reply-to {
 padding-left: 5px;
 color: #409eff;
 font-weight: 500;
 font-size: 15px;
}
</style>

3.4 添加留言或回复的表单组件CommentArea.vue

<template>
 <div>
  <a-comment id="comment-area">
   <a-avatar slot="avatar" :src="require('assets/images/login_logo.png')" alt="Han Solo" />
   <div slot="content">
    <a-form-item>
     <a-textarea id="my-textarea" :rows="4" v-model="content" />
    </a-form-item>
    <a-form-item>
     <a-button
      html-type="submit"
      :loading="submitting"
      type="primary"
      @click="handleSubmit"
     >添加留言</a-button>
    </a-form-item>
   </div>
  </a-comment>
 </div>
</template>
<script>
import {insertMsg} from 'network/ajax.js'

export default {
 data() {
  return {
   content: "",
   submitting: false
  };
 },
 props: {
  parentMsgId: "",
  replyMsgUsername: ""
 },
 watch: {
  replyMsgUsername() {
   document
    .querySelector("#my-textarea")
    .setAttribute("placeholder", "回复: " + "@" + this.replyMsgUsername);
  }
 },
 methods: {
  handleSubmit() {
   if (!this.content) {
    return;
   }
   this.submitting = true;
   insertMsg(this.content, this.parentMsgId, this.$store.state.userId).then(res => {
    this.submitting = false;
    this.content = "";
    document
    .querySelector("#my-textarea")
    .setAttribute("placeholder", '');
    this.$emit('reload')
   }).catch(err => {
    console.log(err);
    this.$router.push('/500')
   })
  },
  handleChange(e) {
   this.value = e.target.value;
  }
 }
};
</script>

组装完成后实现的功能有:

留言界面的展示

点击回复按钮跳到留言表单(这里我直接用了a标签来锚定位,试过用scrollToView来平滑滚动过去,但不知道为什么只有第一次点击回复按钮时才能平滑滚动到,之后再点击他就不滚动了。。。),并把被回复者的用户名显示在placeholder中

点击添加留言按钮,清空placeholder,并自动实现router-view的局部刷新(不是整页刷新)显示出新增的留言

局部刷新的实现就是通过代码中的自定义事件 reload ,具体就是从表单组件开始发送 reload 事件,其父组件 Message.vue 收到后,再继续发送 reload 事件给外层的视图Home.vue,Home的再外层就是App.vue了,Home.vue的定义如下:

<template>
 <el-container class="main-el-container">
  <!-- 侧边栏 -->
  <el-aside width="15%" class="main-el-aside">
   <side-bar></side-bar>
  </el-aside>
  <!-- 主体部分 -->
  <el-main>
   <el-main>
    <router-view @reload="reload" v-if="isRouterAlive"></router-view>
   </el-main>
  </el-main>
 </el-container>
</template>

<script>
import SideBar from "components/common/sidebar/SideBar";

export default {
 name: "Home",
 components: { SideBar },
 data() {
  return {
   isRouterAlive: true
  };
 },
 props: {
  isReload: ""
 },
 watch: {
  isReload() {
   this.reload();
  }
 },
 methods: {
  reload() {
   this.isRouterAlive = false;
   this.$nextTick(() => {
    this.isRouterAlive = true;
   });
  }
 }
};
</script>

<style scoped>
.main-el-container {
 height: 750px;
 border: 1px solid #eee;
}
.main-el-aside {
 background-color: rgb(238, 241, 246);
}
</style>

里面有一个reload方法,通过改变isRouterAlive来让router-view先隐藏,再显示,实现重新挂载。

到此这篇关于使用Vue+Django+Ant Design做一个留言评论模块的示例代码的文章就介绍到这了,更多相关Vue+Django+Ant Design留言评论内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • django和vue实现数据交互的方法

    我使用的是jQuery的ajax与django进行数据交互,遇到的问题是django的csrf 传输数据的方法如下: $(function() { $.ajax({ url: 'account/register', type: 'post', dataType:'json', data: $('#form1').serialize(), success: function (result) { console.log(result); if (result) { alert("result&qu

  • Django与Vue语法的冲突问题完美解决方法

    当我们在django web框架中,使用vue的时候,会遇到语法冲突. 因为vue使用{{}},而django也使用{{}},因此会冲突. 解决办法1: 在django1.5以后,加入了标签: {% verbatim myblock %} {% endverbatim myblock %} 被此标签包裹的代码将不会被Django的模板引擎渲染. 因此,我们可以把带有{{ }} 的Vue代码放在 {% verbatim myblock %}标签中间,例如: <div id="app1&quo

  • vue与django集成打包的实现方法

    1.首先创建 django项目 并在 django项目文件下创建vue项目 2.修改django项目文件下的setting.py 修改django模板路径 指向 vue项目的dist TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR+"/vue-resources/dist"], 'APP_DIRS': True, 'OPTIONS': { 'c

  • Django+Vue跨域环境配置详解

    概述 在使用Django+Vue开发过程中,遇到了很多开发环境相关的问题,比如跨域,比如ajax请求参数等,本篇文章主要记录解决在开发过程中,遇到的一些问题. 跨域不带Cookie 在使用Vue脚手架开发的过程中,会使用Vue脚手架自带的Server进行项目调试,Vue自带的Server支持 hot reloading ,这个特性是非常好用的.但是在开发过程中,因为要与后端交互,所以在请求后端接口的时候,会遇到跨域问题,这个问题在一些职责划分清楚的团队并不存在,因为前端开发人员会才用Mock数据

  • django中使用vue.js的要点总结

    有接口如下: http://127.0.0.1:8000/info/schemes/ 返回json数据: [ { "name": "(山上双人标准间)黄山经典二日游(魅力黄山,日出云海,人间仙境,春暖花开)", "day": 2, "night": 1, "favorites": 0, "score_avg": 4, "photo_url": "/media

  • Django+Vue实现WebSocket连接的示例代码

    近期有一需求:前端页面点击执行任务,实时显示后端执行情况,思考一波:发现 WebSocket 最适合做这件事. 效果 测试 ping www.baidu.com 效果 点击连接建立ws连接 后端实现 所需软件包 后端主要借助Django Channels 实现socket连接,官网文档链接 这里想实现每个连接进来加入组进行广播,所以还需要引入 channels-redis . pip channels==2.2.0 channels-redis==2.4.0 引入 settings.py INS

  • 使用django和vue进行数据交互的方法步骤

    一.前端请求的封装 1.将请求地址封装起来,以便日后修改,在src/assets/js目录下创建getPath.js文件 export default function getUrl(str) { let url = 'http://localhost:8000/' + str; return url; } 2.在同一个目录下创建axios.js文件 我的前端数据交互使用的模块使用的是axios import axios from 'axios' import getUrl from './ge

  • Vue+Django项目部署详解

    本地项目配置 1 复制 luffy/settings/dev.py为prop.py 修改luffy/settings/prop.py中以下几项 (1) allow_hosts ALLOWED_HOSTS = [ 'api.youdomain.com', ] (2) 跨域白名单 CORS_ORIGIN_WHITELIST = ( # 前端域名 "www.youdomain.com", # 后端api接口域名 "api.youdomain.com" ) (3) 支付宝电

  • Django+vue跨域问题解决的详细步骤

    跨域 由于开发模式为前后端分离式开发,故而通常情况下,前端和后端可能运行不同的ip或者port下,导致出现跨域问题,故而单独说明 什么是跨域 跨域是指一个域下的文档或脚本试图去请求另一个域下的资源,这里跨域是广义的. 其实我们通常所说的跨域是狭义的,是由浏览器同源策略限制的一类请求场景. 什么是同源策略? 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS.

  • Django+Vue.js搭建前后端分离项目的示例

    在写这篇文章的时候,顺带学习了一下关于Markdown的使用方法. 笔者是个渣渣,一切都是自己在摸索的学着,所以也谈不上什么体系.系统学习.在这里主要是为了实现把项目前后端分离开. 这里假设你的电脑上所需的django.vue.js已经有了,如果没有,往下拉就是vue.js的安装流程.django前面写过了,就不赘述了. 一,正常搭建前后端分离项目流程 1.创建django项目 命令: django-admin startproject ulb_manager 结构: ├── manage.py

随机推荐