使用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做一个留言评论模块的更多相关文章

  1. 使用Ant Design写一个仿微软ToDo

    实习期的第一份活,自己看Ant Design的官网学习,然后用Ant Design写一个仿微软ToDo. 不做教学目的,只是记录一下. 1.学习 Ant Design 是个组件库,想要会用,至少要知道 ...

  2. 前端自动分环境打包(vue和ant design)

    现实中的问题:有时候版本上线的时候,打包时忘记切换环境,将测试包推上正式服务器,那你就会被批了. 期望:在写打包的命令行的时候就觉得自己在打包正式版本,避免推包时候的,不确信自己的包是否正确. 既然有 ...

  3. React学习及实例开发(二)——用Ant Design写一个简单页面

    本文基于React v16.4.1 初学react,有理解不对的地方,欢迎批评指正^_^ 一.引入Ant Design 1.安装antd yarn add antd 2.引入 react-app-re ...

  4. vue结合Ant Design实现后台系统的权限分配(支持无限子级嵌套)

    最近公司的业务需要,要做一个后台管理系统的管理系统类似于这样子 功能需求如下: 左边是权限菜单,右边对应的是具体权限. 1.父级权限菜单选中,父级权限菜单的权限包括其中所有子级权限菜单的权限也要选中, ...

  5. 用Django加PIL做一个证件照模板生成器网页

    最近在整理自己的简历,发现简历上面的ID照有些太老了,所以就准备重新准备一些证件照,刚好最近在弄自己的博客网站,想着直接做一个网页工具出来,直接生成证件照模板,这样还可以省去PS的麻烦.而且照片涉及到 ...

  6. Ant Design 的一个练习小Demo

    Ant Design 由蚂蚁金服团队出品, 基于 React 的组件化开发模式,封装了一套丰富而实用的 UI 组件库. 在这个练习Demo 中,按照 Ant Design 官网的教程示例,尝试使用 A ...

  7. Vue.js高效前端开发 • 【Ant Design of Vue框架基础】

    全部章节 >>>> 文章目录 一.Ant Design of Vue框架 1.Ant Design介绍 2.Ant Design of Vue安装 3.Ant Design o ...

  8. Vue3: 如何以 Vite 创建,以 Vue Router, Vuex, Ant Design 开始应用

    本文代码: https://github.com/ikuokuo/start-vue3 在线演示: https://ikuokuo.github.io/start-vue3/ Vite 创建 Vue ...

  9. Ant Design(ui框架)

    官方文档:https://ant.design/docs/react/introduce-cn 说明:Ant Design 是一个 ui框架,和 bootstrap 一样是ui框架.里面的组件很完善, ...

随机推荐

  1. Blazor一个简单的示例让我们来起飞

    Blazor Blazor他是一个开源的Web框架,不,这不是重点,重点是它可以使c#开发在浏览器上运行Web应用程序.它其实也简化了SPA的开发过程. Blazor = Browser + Razo ...

  2. Java——IO流超详细总结

    该系列博文会告诉你如何从入门到进阶,一步步地学习Java基础知识,并上手进行实战,接着了解每个Java知识点背后的实现原理,更完整地了解整个Java技术体系,形成自己的知识框架. 在初学Java时,I ...

  3. Jmeter系列(10)- 阶梯加压线程组Stepping Thread Group详解

    如果你想从头学习Jmeter,可以看看这个系列的文章哦 https://www.cnblogs.com/poloyy/category/1746599.html 前言 Stepping Thread ...

  4. Dreamoon Likes Coloring(模拟+构造)

    \(这题刚好撞到我的思路了,但是因为模拟......我看了几十遍测试数据....\) $首先当\sum_^m$小于n时一定无解 大于呢?那我们就要浪费一些区间(覆盖一些点,也就是多出来的点) 但是又不 ...

  5. 区间dp 例题

    D - 石子合并问题--直线版 HRBUST - 1818 这个题目是一个区间dp的入门,写完这个题目对于区间dp有那么一点点的感觉,不过还是不太会. 注意这个区间dp的定义 dp[i][j] 表示的 ...

  6. 记录关于Android多线程的一个坑

    最近在写项目的时候由于联网用得比较频繁,就简单地封装了一个工具类,省得每次联网得时候都要把联网配置写一遍,代码如下: public class okhttp_plus { public static ...

  7. 流媒体与实时计算,Netflix公司Druid应用实践

    Netflix(Nasdaq NFLX),也就是网飞公司,成立于1997年,是一家在线影片[租赁]提供商,主要提供Netflix超大数量的[DVD]并免费递送,总部位于美国加利福尼亚州洛斯盖图.199 ...

  8. Js 事件基础

    一:js中常见得事件 (1) : 鼠标事件         click :点击事件         dblclick :双击事件         contextmenu : 右键单击事件        ...

  9. Netty入门一:何为Netty

    先了解java的网络编程 Netty为何支持高并发 netty是基于java的nio非阻塞通信,而原始的阻塞通信无法满足高并发.下面我们通过两幅图来简要说明 BIO: 这种模式下一个线程处理一个连接, ...

  10. Mysql 常用函数(13)- right 函数

    Mysql常用函数的汇总,可看下面系列文章 https://www.cnblogs.com/poloyy/category/1765164.html right 的作用 返回字符串 str 中最右边的 ...