使用Vue+Django+Ant Design做一個留言評論模組

PanzVor發表於2020-05-31

使用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先隱藏,再顯示,實現重新掛載。

相關文章