您好,登錄后才能下訂單哦!
小編這次要給大家分享的是如何使用Vue+Django+Ant Design做一個留言評論模塊,文章內(nèi)容豐富,感興趣的小伙伴可以來了解一下,希望大家閱讀完這篇文章之后能夠有所收獲。
1.總覽
留言的展示參考網(wǎng)絡(luò)上參見的格式,如掘金社區(qū):
一共分為兩層,子孫留言都在第二層中
最終效果如下:
接下是數(shù)據(jù)庫的表結(jié)構(gòu),如下所示:
有一張user表和留言表,關(guān)系為一對多,留言表有父留言字段的id,和自身有一個一對多的關(guān)系,建表語句如下:
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中定義兩個接口,一個負(fù)責(zé)提供留言內(nèi)容,一個負(fù)責(zé)插入留言,如下:
# 獲取留言信息 @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()函數(shù)用于整理留言信息,使子留言和父留言能對應(yīng)起來,算法實現(xiàn)如下:
# 整理留言信息返回格式 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) # 如果是子評論則跳過,子評論最終會出現(xiàn)在根評論的子節(jié)點中 else: continue tmpParent['children'] = tmpChild # 格式化時間 tmpParent['date'] = datetime.datetime.strftime(tmpParent['date'], '%Y-%m-%d %H:%M:%S') list.append(tmpParent) return list # 搜索一條留言的所有子留言,廣度優(yōu)先 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": "發(fā)的發(fā)射點發(fā)吖方吖是發(fā)是呵等方5愛的非4阿瑟東方 發(fā)", "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是的發(fā)", "parent_msg_id": null, "user_id": 1, "username": "student", "parent_msg_username": null, "children": [ { "id": 7, "date": "2020-05-29 19:29:29", "content": "hfhf2h32h322223232", "parent_msg_id": 6, "user_id": 1, "username": "student", "parent_msg_username": "zhaoliu" }, { "id": 6, "date": "2020-05-29 19:09:56", "content": "而離開離開鄰居哦i據(jù)哦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": "發(fā)送端非場地薩擦手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": "奮發(fā)惡法撒打發(fā)士大夫士大夫是大 大師傅撒", "parent_msg_id": 2, "user_id": 2, "username": "teacher", "parent_msg_username": "lisi" }, { "id": 2, "date": "2020-05-29 19:08:41", "content": "fasdfasdf發(fā)生的法撒旦飛灑多發(fā)點房地產(chǎn)", "parent_msg_id": 1, "user_id": 4, "username": "lisi", "parent_msg_username": "student" } ] } ], "status_code": 200 }
這個就是前臺所要的內(nèi)容了。
其實一開始我是很直觀地認(rèn)為是用深度優(yōu)先來取出層層嵌套的留言的,如下:
# 遞歸搜索一條留言的所有子留言,深度優(yōu)先 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": "發(fā)的發(fā)射點發(fā)吖方吖是發(fā)是呵等方5愛的非4阿瑟東方 發(fā)", "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是的發(fā)", "parent_msg_id": null, "user_id": 1, "username": "student", "children": [ { "id": 2, "date": "2020-05-29T19:08:41", "content": "fasdfasdf發(fā)生的法撒旦飛灑多發(fā)點房地產(chǎn)", "parent_msg_id": 1, "user_id": 4, "username": "lisi", "children": [ { "id": 4, "date": "2020-05-29T19:09:14", "content": "發(fā)送端非場地薩擦手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據(jù)哦i報價哦v保健品45465", "parent_msg_id": 4, "user_id": 6, "username": "zhaoliu", "children": [ { "id": 7, "date": "2020-05-29T19:29:29", "content": "hfhf2h32h322223232", "parent_msg_id": 6, "user_id": 1, "username": "student", "children": [] } ] } ] }, { "id": 3, "date": "2020-05-29T19:08:56", "content": "奮發(fā)惡法撒打發(fā)士大夫士大夫是大 大師傅撒", "parent_msg_id": 2, "user_id": 2, "username": "teacher", "children": [] }, { "id": 9, "date": "2020-05-29T17:27:13", "content": "alalla啦啦啦啦啦啦來的隊列李大水泛濫的薩拉發(fā) 的 第三方哈l", "parent_msg_id": 2, "user_id": 7, "username": "joke", "children": [] } ] } ] } ], "status_code": 200 }
但仔細(xì)一想,實際頁面展示的時候肯定不能這樣一層層無限地嵌套下去,否則留言多了頁面就裝不下了,于是還是改成了兩層留言的格式,第二層使用廣度優(yōu)先搜索將樹轉(zhuǎn)為列表存儲。
2.2 新增留言接口
前臺提供留言內(nèi)容、留言者id以及父留言的id(如果不是回復(fù)信息的話就是空)
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.前臺設(shè)計
有了后臺提供的數(shù)據(jù),前臺展示就比較簡單了。
留言板塊的設(shè)計我使用了Ant Design的留言組件。
留言界面主要由兩個組件所構(gòu)成——留言區(qū)組件以及評論表單的組件
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 留言區(qū)域組件CommentMessage.vue:
<template> <div id="commentMsg"> <div v-if="isEmpty(commentList)" class="head-message">暫無留言內(nèi)容</div> <div v-else class="head-message">留言內(nèi)容</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 留言區(qū)域由多個Comment留言組件所構(gòu)成,留言組件定義如下
<template> <a-comment> <span slot="actions" key="comment-basic-reply-to" @click="handlReply(comment.id, comment.username)" > <a href="#my-textarea">回復(fù)</a> </span> <a slot="author" >{{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 添加留言或回復(fù)的表單組件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", "回復(fù): " + "@" + 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>
組裝完成后實現(xiàn)的功能有:
留言界面的展示
點擊回復(fù)按鈕跳到留言表單(這里我直接用了a標(biāo)簽來錨定位,試過用scrollToView來平滑滾動過去,但不知道為什么只有第一次點擊回復(fù)按鈕時才能平滑滾動到,之后再點擊他就不滾動了。。。),并把被回復(fù)者的用戶名顯示在placeholder中
點擊添加留言按鈕,清空placeholder,并自動實現(xiàn)router-view的局部刷新(不是整頁刷新)顯示出新增的留言
局部刷新的實現(xiàn)就是通過代碼中的自定義事件 reload
,具體就是從表單組件開始發(fā)送 reload
事件,其父組件 Message.vue
收到后,再繼續(xù)發(fā)送 reload
事件給外層的視圖Home.vue,Home的再外層就是App.vue了,Home.vue的定義如下:
<template> <el-container class="main-el-container"> <!-- 側(cè)邊欄 --> <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先隱藏,再顯示,實現(xiàn)重新掛載。
看完這篇關(guān)于如何使用Vue+Django+Ant Design做一個留言評論模塊的文章,如果覺得文章內(nèi)容寫得不錯的話,可以把它分享出去給更多人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。