django channels打造客服聊天系統(tǒng)

????最近公司業(yè)務需求, 打算開發(fā)一款在線客服的功能提供給app使用, 本人一開始打算是使用第三方平臺, 比如騰訊云的客服系統(tǒng)來打造的, 不過后來查找資料, 了解了下websocket之后, 發(fā)現實現起來并不困難. 在此之前, 我研究了微信網頁版的聊天方式, 發(fā)現微信網頁版聊天并不是基于websocket的, 還是使用了http請求發(fā)送post請求, 進行頁面長輪詢方式進行信息的交互的.?

? ? 由于公司項目使用了Django框架進行開發(fā)的, django框架有一個channels庫是支持websocket服務的, 所以在進行開發(fā)之前需要進行安裝channels

? ? 打開終端, 輸入:

? ??pip install -U channels

? ? 在django配置文件中安裝channels應用.

? ? 使用命令django-admin startapp chat創(chuàng)建一個子應用chat, 并在項目的應用的上級目錄新建一個routing.py文件(與wsgi.py同級), 作為websocket服務的路由規(guī)則.?


? ? 在routing.py中輸入以下代碼:

? ? 其實routing.py文件和django的總urls.py文件的原理是一樣的,? django服務在運行的時候, 如果接收到的是websocket請求, 就會跳到routing.py文件中websocket應用, 接收到http請求時會跳到urls中找http應用.?

? ? ?當然, 再此之前需要在配置文件中配置websocket所支持的asgi協議, 配置如下:? ?

????本次使用了redis數據庫作為websocket的管道,? 單點通信的信息以及組間的通信數據都緩存在了redis數據庫中, 配置redis管道如下:

配置完這些之后運行manage.py, 如截圖所示說明成功了

之后正式開始編寫業(yè)務代碼了, 首先需要在剛剛創(chuàng)建的應用chat中創(chuàng)建一個consumers.py文件, 作為channels的消費者, websocket的消息通信都在這個文件中執(zhí)行.?

復制channels官方文檔中(https://channels.readthedocs.io/en/latest/tutorial/part_3.html)的組間消息通信代碼到這個文件中,? 然后根據這個基礎代碼進行業(yè)務代碼的調整.

? ? 其里面使用了python的async庫來支持異步操作, 其實現了connect, disconnect,receive,chat_message等方法, connect用來連接用戶組, 也可以理解為開始連接進行websocket通信; disconnect正好時斷開連接, 客戶端通過手動的斷開websocket通信會經過此方法; chat_message就是用來發(fā)送消息的, receive方法就是進行消息的接收.?

? ? 在chat應用中創(chuàng)建未讀消息模型類, 用來記錄聊天記錄, 在models.py文件中添加如下代碼:

from django.db import models

from users.models import User

# Create your models here.

class ChatRecords(models.Model):

? ? '''在線客服聊天記錄模型類'''

? ? sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='send_records', verbose_name='發(fā)送者', null=False)

? ? receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='receive_records', verbose_name='接收者', null=False)

? ? message = models.TextField(verbose_name='聊天信息')

? ? create_time = models.DateTimeField(auto_now_add=True, verbose_name='創(chuàng)建時間')

? ? class Meta:

? ? ? ? db_table = "tb_chat_records"

? ? ? ? verbose_name = '客服聊天記錄'

? ? ? ? verbose_name_plural = verbose_name

? ? def __str__(self):

? ? ? ? return self.id

? ? 接下來執(zhí)行數據庫遷移命令

????python manage.py makemigrations?

? ? 數據庫中生成表命令

? ? python manage.py migrate

? ? 在consumers.py文件中添加業(yè)務代碼, 添加結果如下:

# chat/consumers.py

import re

from django.conf import settings

from fdfs_client.client import Fdfs_client

from channels.exceptions import *

from calendar import timegm

from django_redis import get_redis_connection

from base64 import b64decode

from rest_framework_jwt.authentication import jwt_decode_handler

from channels.db import database_sync_to_async

from channels.generic.websocket import AsyncWebsocketConsumer

import json

class ChatConsumer(AsyncWebsocketConsumer):

? ? async def connect(self):

? ? ? ? # 獲取url中的參數

? ? ? ? query_string = self.scope['query_string'].decode()

? ? ? ? params = query_string.split('&')

? ? ? ? item = {}

? ? ? ? for param in params:

? ? ? ? ? ? item[param.split('=')[0]] = param.split('=')[1]

? ? ? ? token = item.get('token')

? ? ? ? self.user_group = 'chat_'

? ? ? ? if token:

? ? ? ? ? ? try:

? ? ? ? ? ? ? ? payload = jwt_decode_handler(token)

? ? ? ? ? ? except:

? ? ? ? ? ? ? ? raise DenyConnection("簽證錯誤")

? ? ? ? ? ? user_id = payload['user_id']

? ? ? ? ? ? user = await self.get_user(id=user_id)

? ? ? ? ? ? last_login = payload.get('last_login')

? ? ? ? ? ? if last_login != timegm(user.last_login.utctimetuple()):

? ? ? ? ? ? ? ? raise DenyConnection("簽證已過期")

? ? ? ? else:

? ? ? ? ? ? user = self.scope['user']

? ? ? ? if not user:

? ? ? ? ? ? raise DenyConnection("用戶不存在")

? ? ? ? receiver_name = item.get('receiver')

? ? ? ? if not receiver_name:

? ? ? ? ? ? raise DenyConnection("接收者名稱錯誤")

? ? ? ? receiver = await self.get_user(username=receiver_name)

? ? ? ? if not receiver:

? ? ? ? ? ? raise DenyConnection("接收者不存在")

? ? ? ? self.receiver = receiver

? ? ? ? self.user = user

? ? ? ? # 遠程組

? ? ? ? self.receiver_group = 'chat_%s_%s' % (self.receiver.username, self.user.username)

? ? ? ? # 用戶組

? ? ? ? self.user_group = 'chat_%s_%s' % (self.user.username, self.receiver.username)

? ? ? ? # Join room group

? ? ? ? await self.channel_layer.group_add(

? ? ? ? ? ? self.user_group,

? ? ? ? ? ? self.channel_name

? ? ? ? )

? ? ? ? await self.accept()

? ? async def disconnect(self, close_code):

? ? ? ? # Leave room group

? ? ? ? await self.channel_layer.group_discard(

? ? ? ? ? ? self.user_group,

? ? ? ? ? ? self.channel_name

? ? ? ? )

? ? # Receive message from WebSocket

? ? async def receive(self, text_data):

? ? ? ? text_data_json = json.loads(text_data)

? ? ? ? message = text_data_json['message']

? ? ? ? if message:

? ? ? ? ? ? # data:image/png;base64,i

? ? ? ? ? ? ret = re.findall('data:image/.*;base64,(.*)', message)

? ? ? ? ? ? if ret:

? ? ? ? ? ? ? ? user_pic_str = ret[0]

? ? ? ? ? ? ? ? image_src = await self.save_image_to_fdfs(user_pic_str)

? ? ? ? ? ? ? ? # 構造message

? ? ? ? ? ? ? ? message = '<img style="width: 80px; height: 60px" src="'+ image_src +'" data-preview-src="">'

? ? ? ? ? ? # Send message to room group

? ? ? ? ? ? chat_record = await self.save_model(self.user, self.receiver, message)

? ? ? ? ? ? if self.receiver.username == 'admin':

? ? ? ? ? ? ? ? '''為管理員添加消息提示'''

? ? ? ? ? ? ? ? await self.save_unread_records(chat_record, self.user)

? ? ? ? ? ? await self.channel_layer.group_send(

? ? ? ? ? ? ? ? # websocket發(fā)送消息

? ? ? ? ? ? ? ? self.receiver_group,

? ? ? ? ? ? ? ? {

? ? ? ? ? ? ? ? ? ? 'type': 'chat_message',

? ? ? ? ? ? ? ? ? ? 'message': message

? ? ? ? ? ? ? ? }

? ? ? ? ? ? )

? ? # Receive message from room group

? ? async def chat_message(self, event):

? ? ? ? message = event['message']

? ? ? ? # Send message to WebSocket

? ? ? ? await self.send(text_data=json.dumps({

? ? ? ? ? ? 'message': message

? ? ? ? }))

? ? @database_sync_to_async

? ? def save_model(self, sender, receiver, message):

? ? ? ? # 保存消息記錄到數據庫中

? ? ? ? from .models import ChatRecords

? ? ? ? return ChatRecords.objects.create(sender=sender, receiver=receiver, message=message)

? ? @database_sync_to_async

? ? def get_user(self, id=None, username=None):

? ? ? ? # 異步獲取用戶

? ? ? ? from users.models import User

? ? ? ? user = None

? ? ? ? if id:

? ? ? ? ? ? try:

? ? ? ? ? ? ? ? user = User.objects.get(id=id)

? ? ? ? ? ? except:

? ? ? ? ? ? ? ? return None

? ? ? ? if username:

? ? ? ? ? ? try:

? ? ? ? ? ? ? ? user = User.objects.get(username=username)

? ? ? ? ? ? except:

? ? ? ? ? ? ? ? return None

? ? ? ? return user

? ? async def save_unread_records(self, chat_record, sender):

? ? ? ? # 保存未讀消息

? ? ? ? redis_conn = get_redis_connection('chatRecord')

? ? ? ? p = redis_conn.pipeline()

? ? ? ? p.rpush(sender.id, chat_record.id)

? ? ? ? p.set('new_records', 1)? # 在redis中添加未讀標記

? ? ? ? p.execute()

? ? async def save_image_to_fdfs(self, pic_str):

? ? ? ? # 把圖片存儲到fastdfs文件系統(tǒng)中

? ? ? ? client = Fdfs_client(settings.FDFS_CLIENT_CONF)

? ? ? ? ret = client.upload_appender_by_buffer(b64decode(pic_str))

? ? ? ? if ret.get("Status") != "Upload successed.":

? ? ? ? ? ? raise Exception("upload file failed")

? ? ? ? file_name = ret.get("Remote file_id")

? ? ? ? return settings.FDFS_URL + file_name

? ? 接下來還需要在chat應用中創(chuàng)建一個routing.py文件, 將子路由會分發(fā)到這個文件中, 輸入以下代碼:

通過上面的步驟, django channels搭建websocket的任務已經完成, 這個時候需要為后臺管理頁面提供一個聊天界面,? 在項目的模板目錄中創(chuàng)建一個chat文件夾, 在里面創(chuàng)建一個index.html文件

復制以下代碼進去

<!DOCTYPE html>

<html>

<head>

? ? <meta charset="UTF-8">

? ? <title>koalas 客服</title>

? ? <style type="text/css">

? ? ? ? body{

? ? ? ? ? ? background: url("http://47.75.156.19:8020/static/chat/images/bg.jpg") no-repeat ;

? ? ? ? ? ? background-size: cover;

? ? ? ? }

? ? ? ? * {

? ? ? ? ? ? margin: 0;

? ? ? ? ? ? padding: 0;

? ? ? ? }

? ? ? ? li,

? ? ? ? img,

? ? ? ? label,

? ? ? ? input {

? ? ? ? ? ? vertical-align: middle;

? ? ? ? }

? ? ? ? img {

? ? ? ? ? ? display: block;

? ? ? ? ? ? max-height: 100%;

? ? ? ? ? ? margin: 0;

? ? ? ? ? ? padding: 0;

? ? ? ? }

? ? ? ? a {

? ? ? ? ? ? text-decoration: none;

? ? ? ? ? ? color: black;

? ? ? ? }

? ? ? ? ul, li {

? ? ? ? ? ? list-style: none;

? ? ? ? }

? ? ? ? -webkit-scrollbar-track {

? ? ? ? ? ? background-color: transparent;

? ? ? ? }

? ? ? ? .defaultPage {

? ? ? ? / / 缺省頁 width: 200 px;

? ? ? ? ? ? height: 300px;

? ? ? ? ? ? margin-top: 120px;

? ? ? ? }

? ? ? ? .defaultPage > .img-box {

? ? ? ? ? ? width: 120px;

? ? ? ? ? ? height: 120px;

? ? ? ? ? ? margin: 20px auto;

? ? ? ? }

? ? ? ? .defaultPage > .img-box > img {

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? height: 100%;

? ? ? ? }

? ? ? ? .defaultPage > .noMsg {

? ? ? ? ? ? margin: 20px 0;

? ? ? ? ? ? text-align: center;

? ? ? ? ? ? font-size: 14px;

? ? ? ? ? ? color: rgba(153, 153, 153, 1);

? ? ? ? }

? ? ? ? .defaultPage > .helpButton {

? ? ? ? ? ? margin: 20px 40px;

? ? ? ? ? ? text-align: center;

? ? ? ? ? ? font-size: 14px;

? ? ? ? ? ? line-height: 42px;

? ? ? ? ? ? color: #fff;

? ? ? ? ? ? background-color: #000000;

? ? ? ? }

? ? ? ? .kefu {

? ? ? ? ? ? display: flex;

? ? ? ? ? ? margin: auto;

? ? ? ? ? ? border: 1px solid #333;

? ? ? ? ? ? position: absolute;

? ? ? ? ? ? top: 50px;

? ? ? ? ? ? left: 250px;

? ? ? ? ? ? right: 250px;

? ? ? ? ? ? bottom: 50px;

? ? ? ? }

? ? ? ? .left {

? ? ? ? ? ? width: 18%;

? ? ? ? ? ? overflow-x:hidden; overflow-y:auto;

? ? ? ? ? ? border-right: 1px solid #999;

? ? ? ? ? ? background-color: #333;

? ? ? ? }

? ? ? ? .left .list {

? ? ? ? ? ? height: 100%;

? ? ? ? }

? ? ? ? .left .list .list-item {

? ? ? ? ? ? display: flex;flex-wrap: nowrap;

? ? ? ? ? ? padding: 15px;overflow: hidden;

? ? ? ? ? ? border-bottom: 1px solid #999;

? ? ? ? ? ? color: #fff;

? ? ? ? }

? ? ? ? .cur{

? ? ? ? ? ? background-color: #333;

? ? ? ? ? ? opacity:0.6;

? ? ? ? }

? ? ? ? .left .list .list-item .img-box {

? ? ? ? ? ? position: relative;

? ? ? ? ? ? width: 36px; height: 36px;

? ? ? ? ? ? margin-right: 10px;

? ? ? ? ? ? border-radius: 4px;

? ? ? ? }

? ? ? ? .left .list .list-item .img-box img {

? ? ? ? ? ? width: 100%;height: 100%;

? ? ? ? ? ? display: inline-block;

? ? ? ? ? ? vertical-align: middle;

? ? ? ? }

? ? ? ? .left .list .list-item .bandage{

? ? ? ? ? ? display:inline-block ;

? ? ? ? ? ? position: absolute;right: -8px;top:-8px;

? ? ? ? ? ? width:16px;height: 16px;border-radius: 12px;

? ? ? ? ? ? text-align: center;

? ? ? ? ? ? font-size:12px;line-height: 16px;

? ? ? ? ? ? background-color: red;

? ? ? ? }

? ? ? ? .left .list .list-item .bandage span{

? ? ? ? ? ? color: #fff;

? ? ? ? }

? ? ? ? .left .list .list-item .info{

? ? ? ? ? ? flex: 1;padding-left: 10px;

? ? ? ? }

? ? ? ? .left .list .list-item .name {

? ? ? ? ? ? font-size: 14px;line-height: 22px;

? ? ? ? }

? ? ? ? .left .list .list-item .text{

? ? ? ? ? ? display: flex;flex-wrap: nowrap;

? ? ? ? ? ? font-size:12px;

? ? ? ? ? ? text-align: left;

? ? ? ? }

? ? ? ? .left .list .list-item .text span{

? ? ? ? ? ? flex:1;

? ? ? ? }

? ? ? ? .right {

? ? ? ? ? ? flex: 1;

? ? ? ? ? ? height: 100%;

? ? ? ? ? ? display: flex;

? ? ? ? ? ? flex-direction: column;

? ? ? ? ? ? background-color: #c3c3c3;

? ? ? ? }

? ? ? ? .right .name {

? ? ? ? ? ? position: relative;

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? height: 50px;

? ? ? ? ? ? line-height: 50px;

? ? ? ? ? ? text-align: center;

? ? ? ? ? ? border-bottom: 1px solid #999;

? ? ? ? }

? ? ? ? .name .moreMsg{

? ? ? ? ? ? position: absolute; left:46%;top: 61px;

? ? ? ? ? ? height: 20px;

? ? ? ? ? ? padding: 0 6px;

? ? ? ? ? ? border-radius: 6px;

? ? ? ? ? ? background-color: #FFF;

? ? ? ? ? ? color: red;

? ? ? ? ? ? font-size:12px;line-height: 20px;

? ? ? ? ? ? z-index:98;

? ? ? ? }

? ? ? ? .right .content {

? ? ? ? ? ? flex:1;

? ? ? ? ? ? overflow-x:hidden; overflow-y:auto;

? ? ? ? ? ? display: flex;flex-direction: column;

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? padding: 10px;

? ? ? ? ? ? box-sizing: border-box;

? ? ? ? ? ? z-index: 88;

? ? ? ? }

? ? ? ? .right .content .message{

? ? ? ? ? ? margin-top: 24px;

? ? ? ? ? ? flex: 1;

? ? ? ? }

? ? ? ? .right .send_msg {

? ? ? ? ? ? position: relative;

? ? ? ? ? ? height: 160px;

? ? ? ? ? ? overflow: hidden;

? ? ? ? ? ? padding: 10px;

? ? ? ? ? ? border-top: 1px solid #999;

? ? ? ? ? ? z-index: 2;

? ? ? ? }

? ? ? ? .right .send_msg .input_text {

? ? ? ? ? ? display: block;

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? height: 60%;

? ? ? ? ? ? box-sizing: border-box;

? ? ? ? ? ? padding: 10px;

? ? ? ? ? ? background-color: #c3c3c3;

? ? ? ? }

? ? ? ? .right .send_msg .btn_send {

? ? ? ? ? ? position: absolute;

? ? ? ? ? ? bottom: 10px;

? ? ? ? ? ? right: 20px;

? ? ? ? ? ? width: 70px;

? ? ? ? ? ? height: 22px;

? ? ? ? ? ? margin-top: 100px;

? ? ? ? ? ? border-radius: 4px;

? ? ? ? ? ? font-size: 14px;

? ? ? ? ? ? line-height: 22px;

? ? ? ? ? ? text-align: center;

? ? ? ? ? ? border: 1px solid #333;

? ? ? ? }

? ? ? ? .content .time{

? ? ? ? ? ? display: flex;

? ? ? ? ? ? justify-content: center;

? ? ? ? ? ? color: #fff;

? ? ? ? ? ? line-height: 40px;

? ? ? ? ? ? z-index: 4;

? ? ? ? }

? ? ? ? .content .reply {

? ? ? ? ? ? display: flex;

? ? ? ? ? ? width: 80%;

? ? ? ? ? ? margin-bottom: 15px;

? ? ? ? }

? ? ? ? .content .reply .img-box, .content .user .img-box {

? ? ? ? ? ? display: inline-block;

? ? ? ? ? ? width: 40px;

? ? ? ? ? ? height: 40px;

? ? ? ? }

? ? ? ? .content .reply .img-box img, .content .user .img-box img {

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? height: 100%;

? ? ? ? }

? ? ? ? .content .reply .text-cont {

? ? ? ? ? ? flex: 1;

? ? ? ? ? ? padding: 4px 0;

? ? ? ? ? ? margin-left: 15px;

? ? ? ? }

? ? ? ? .content .reply .text-cont .text, .content .user .text-cont .text {

? ? ? ? ? ? display: inline-block;

? ? ? ? ? ? padding: 4px 6px;

? ? ? ? ? ? font-size: 14px;

? ? ? ? ? ? line-height: 28px;

? ? ? ? ? ? border-radius: 4px;

? ? ? ? ? ? border: 1px solid #999;

? ? ? ? ? ? background-color: #fff;

? ? ? ? }

? ? ? ? .content .user .text-cont img{

? ? ? ? ? ? float: right;

? ? ? ? }

? ? ? ? .content .show {

? ? ? ? ? ? visibility: hidden;

? ? ? ? }

? ? ? ? .content .user {

? ? ? ? ? ? float: right;

? ? ? ? ? ? display: flex;

? ? ? ? ? ? width: 80%;

? ? ? ? ? ? margin-bottom: 15px;

? ? ? ? }

? ? ? ? .content .user .text-cont {

? ? ? ? ? ? flex: 1;

? ? ? ? ? ? text-align: right;

? ? ? ? ? ? padding: 4px;

? ? ? ? ? ? margin-right: 15px;

? ? ? ? }

? ? ? ? .content .user .text-cont .text {

? ? ? ? ? ? background-color: #b2e281;

? ? ? ? }

? ? ? ? .content .user .img-box {

? ? ? ? ? ? width: 40px;

? ? ? ? ? ? height: 40px;

? ? ? ? }

? ? ? ? .content .user .img-box img {

? ? ? ? ? ? width: 100%;

? ? ? ? ? ? height: 100%;

? ? ? ? }

? ? ? ? .bigimg{width:600px;position: fixed;left: 0;top: 0; right: 0;bottom: 0;margin:auto;display: none;z-index:9999;}

.mask{position: fixed;left: 0;top: 0; right: 0;bottom: 0;background-color: #000;opacity:0.5;filter: Alpha(opacity=50);z-index: 98;transition:all 1s;display: none}

? ? .bigbox{width:840px;background: #fff;border:1px solid #ededed;margin:0 auto;border-radius: 10px;overflow: hidden;padding:10px;}

? ? .bigbox>.text-cont{width:400px;height:250px;float:left;border-radius:5px;overflow: hidden;margin: 0 10px 10px 10px;}

? ? .bigbox>.imgbox>img{width:100%;}

? ? ? ? .text-cont:hover{cursor:zoom-in}

? ? ? ? .mask:hover{cursor:zoom-out}

? ? ? ? .mask>img{position: fixed;right:10px;top: 10px;width: 60px;}

? ? ? ? .mask>img:hover{cursor:pointer}

? ? </style>

</head>

<body>

<img class="bigimg">

<div class="mask">

? ? <img src="http://47.75.156.19:8020/static/chat/images/close.png" alt="">

</div>

<div class="kefu">

? ? <div class="left">

? ? ? ? <ul class="list">

? ? ? ? ? ? <li class="list-item" :class="{cur: iscur === idx}" @click="handleSelect(item);iscur=idx" v-for="(item,idx) in senderLis" :key="idx">

? ? ? ? ? ? ? ? <div class="img-box">

? ? ? ? ? ? ? ? ? ? <img :src="[[ item.user_pic ]]">

? ? ? ? ? ? ? ? ? ? <div class="bandage" v-if="item.count !== 0">

? ? ? ? ? ? ? ? ? ? ? ? <span >[[item.count]]</span>

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? <div class="info">

? ? ? ? ? ? ? ? ? <span class="name">[[ item.username ]]</span>

? ? ? ? ? ? ? ? ? ? <div class="text" v-html="item.last_send_message">

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? </li>

? ? ? ? </ul>

? ? </div>

? ? <div class="right">

? ? ? ? <div class="name" v-if="!resultShow">

? ? ? ? ? ? <span>[[ name ]]</span>

? ? ? ? ? ? <div class="moreMsg" @click="getMoreMsg" v-if="next">

? ? ? ? ? ? ? ? <span>更多消息</span>

? ? ? ? ? ? </div>

? ? ? ? </div>

? ? ? ? <div class="defaultPage" v-if="resultShow">

? ? ? ? ? ? <div class="img-box">

? ? ? ? ? ? ? ? <img :src="defaultPage">

? ? ? ? ? ? </div>

? ? ? ? ? ? <div class="noMsg">

? ? ? ? ? ? ? ? <span>未選擇聊天!</span>

? ? ? ? ? ? </div>

? ? ? ? </div>

? ? ? ? <div class="content" v-show="!resultShow">

? ? ? ? ? ? <div v-for="(item,idx) in chatList" :key="idx" class="message">

? ? ? ? ? ? ? ? ? ? <div class="time">

? ? ? ? ? ? ? ? ? ? ? ? <span>[[item.create_time]]</span>

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? <div class="reply" v-if="item.sender_name !== 'admin'">

? ? ? ? ? ? ? ? ? ? <div class="img-box">

? ? ? ? ? ? ? ? ? ? ? ? <img :src="clientImg"/>

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? ? ? <div class="text-cont" v-html="item.message">

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? <div class="user" v-else>

? ? ? ? ? ? ? ? ? ? <div class="text-cont" v-html="item.message">

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? ? ? <div class="img-box">

? ? ? ? ? ? ? ? ? ? ? ? <img :src="userImg"/>

? ? ? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? ? ? </div>

? ? ? ? ? ? </div>

? ? ? ? </div>

? ? ? ? <div class="send_msg" v-show="!resultShow">

? ? ? ? ? ? <textarea id="dropBox" class="input_text" autofocus v-model.trim="value" @keyup.enter="sendMsg"></textarea>

? ? ? ? ? ? <div class="btn_send" @click="sendMsg">發(fā)送</div>

? ? ? ? </div>

? ? </div>

</div>

</body>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>

<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>

<script src="http://47.75.156.19:8020/static/chat/js/zoom.js"></script>

<script type="text/javascript">

? ? var obj;

? ? var global_host = "127.0.0.1:8000";

? ? axios.defaults.withCredentials=true;

? ? window.onbeforeunload= function(event) {

? ? ? ? ? var flag =? confirm("確定離開此頁面嗎旺入?");

? ? ? ? ? if(flag==true){

? ? ? ? ? ? ? vm.chatSocket.close();

? ? ? ? ? }

? ? ? ? return flag

? ? };

? ? var vm = new Vue({

? ? ? ? el: '.kefu',

? ? ? ? delimiters: ['[[', ']]'], // 修改vue模板符號丹鸿,防止與django沖突

? ? ? ? data: {

? ? ? ? ? ? 'resultShow': true,

? ? ? ? ? ? 'defaultPage': '',

? ? ? ? ? ? 'clientImg': '',

? ? ? ? ? ? 'userImg': 'http://47.75.156.19:8020/static/chat/images/user02.jpg',

? ? ? ? ? ? 'name': "",

//? ? ? ? ? ? "senders": senders,

? ? ? ? ? ? 'iscur': '',

? ? ? ? ? ? "chatList": "",

? ? ? ? ? ? "next": "",

? ? ? ? ? ? "value": '',

? ? ? ? ? ? 'chatSocket': '',

? ? ? ? ? ? 'senderLis': '',

? ? ? ? ? ? 'current_id': '',

? ? ? ? },

? ? ? ? methods: {

? ? ? ? ? ? getCookie (name) {

? ? ? ? ? ? ? var value = '; ' + document.cookie;

? ? ? ? ? ? ? var parts = value.split('; ' + name + '=');

? ? ? ? ? ? ? if (parts.length === 2) return parts.pop().split(';').shift()

? ? ? ? ? ? },

? ? ? ? ? ? getData(timeout){

? ? ? ? ? ? ? ? var url = 'http://'+ global_host +'/api/chat/unread/records/';

? ? ? ? ? ? ? ? axios.post(url, {timeout:timeout}, {headers:{'X-CSRFToken': this.getCookie('csrftoken')}}).then(res =>{

? ? ? ? ? ? ? ? ? ? this.senderLis = res.data;

? ? ? ? ? ? ? ? ? ? setTimeout(this.getData(30),300)

? ? ? ? ? ? ? ? })

? ? ? ? ? ? },

? ? ? ? ? ? handleSelect(item){

? ? ? ? ? ? ? ? this.clientImg = item.user_pic;

? ? ? ? ? ? ? ? if(this.current_id === item.sender_id){

? ? ? ? ? ? ? ? ? ? return

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? else {this.current_id = item.sender_id}

? ? ? ? ? ? ? ? if(this.chatSocket){

? ? ? ? ? ? ? ? ? ? this.chatSocket.close()

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? this.chatSocket = new WebSocket('ws://'+ global_host +'/ws/chat/?receiver=' + item.username );

? ? ? ? ? ? ? ? $('.content').html('');

? ? ? ? ? ? ? ? this.chatList = '';

? ? ? ? ? ? ? ? this.resultShow = false;

? ? ? ? ? ? ? ? this.name = item.username;

? ? ? ? ? ? ? ? axios.get('http://'+ global_host +'/api/chat/records/' + item.sender_id + "/?page_size=10").then(res => {

? ? ? ? ? ? ? ? ? ? var data = res.data;

? ? ? ? ? ? ? ? ? ? this.chatList = data.results;

? ? ? ? ? ? ? ? ? ? this.next = data.next;

? ? ? ? ? ? ? ? ? ? this.$nextTick(function () {

? ? ? ? ? ? ? ? ? ? ? ? obj = new zoom('mask', 'bigimg','text-cont img');

? ? ? ? ? ? ? ? ? ? ? ? obj.init();

? ? ? ? ? ? ? ? ? ? ? ? $('.text-cont img').click(function () {

? ? ? ? ? ? ? ? ? ? ? ? ? ? $('.bigimg').attr('src', $(this).attr('src'))

? ? ? ? ? ? ? ? ? ? ? ? })

? ? ? ? ? ? ? ? ? ? })

? ? ? ? ? ? ? ? }).then(function () {

? ? ? ? ? ? ? ? ? ? // 滾動到底部

? ? ? ? ? ? ? ? ? ? var h = $('.content')[0].scrollHeight;

? ? ? ? ? ? ? ? ? ? $('.content').scrollTop(h);

? ? ? ? ? ? ? ? });

? ? ? ? ? ? ? ? this.chatSocket.onmessage = function (e) {

? ? ? ? ? ? ? ? ? ? // 接收消息

? ? ? ? ? ? ? ? ? ? var data = JSON.parse(e.data);

? ? ? ? ? ? ? ? ? ? // 發(fā)起請求刪除redis中記錄

? ? ? ? ? ? ? ? ? ? axios({

? ? ? ? ? ? ? ? ? ? ? ? method:'delete',

? ? ? ? ? ? ? ? ? ? ? ? url:'http://'+ global_host +'/api/chat/records/' + item.sender_id + "/",

? ? ? ? ? ? ? ? ? ? ? ? headers: {'X-CSRFToken': vm.getCookie('csrftoken')}

? ? ? ? ? ? ? ? ? ? }).then().catch(error =>{

? ? ? ? ? ? ? ? ? ? ? ? console.log(error.response.data)

? ? ? ? ? ? ? ? ? ? });

? ? ? ? ? ? ? ? ? ? var message = data['message'];

? ? ? ? ? ? ? ? ? ? $('.content').append('<div class="message"><div class="reply"><div class="img-box"><img src="'+ vm.clientImg +'"></div><div class="text-cont" >'+ message +'</div></div></div>');

? ? ? ? ? ? ? ? ? ? obj.init();

? ? ? ? ? ? ? ? ? ? $('.text-cont img').click(function () {

? ? ? ? ? ? ? ? ? ? ? ? $('.bigimg').attr('src', $(this).attr('src'))});

? ? ? ? ? ? ? ? ? ? // 滾動到底部

? ? ? ? ? ? ? ? ? ? var h = $('.content')[0].scrollHeight;

? ? ? ? ? ? ? ? ? ? $('.content').scrollTop(h);

? ? ? ? ? ? ? ? };

? ? ? ? ? ? ? ? this.chatSocket.onclose = function (e) {

? ? ? ? ? ? ? ? ? ? console.error('Chat socket closed unexpectedly');

? ? ? ? ? ? ? ? };

? ? ? ? ? ? ? ? // 發(fā)起請求刪除redis中記錄

? ? ? ? ? ? ? ? axios({

? ? ? ? ? ? ? ? ? ? method:'delete',

? ? ? ? ? ? ? ? ? ? url:'http://'+ global_host +'/api/chat/records/' + item.sender_id + "/",

? ? ? ? ? ? ? ? ? ? headers: {'X-CSRFToken': this.getCookie('csrftoken')}

? ? ? ? ? ? ? ? }).then().catch(error =>{

? ? ? ? ? ? ? ? ? ? console.log(error.response.data)

? ? ? ? ? ? ? ? })

? ? ? ? ? ? },

? ? ? ? ? ? getMoreMsg(){

? ? ? ? ? ? ? ? var url = this.next;

//? ? ? ? ? ? ? ? alert(url)

? ? ? ? ? ? ? ? axios.get(url).then(res =>{

? ? ? ? ? ? ? ? ? ? var data = res.data;

? ? ? ? ? ? ? ? ? ? this.next = data.next;

? ? ? ? ? ? ? ? ? ? this.chatList = data.results.concat(this.chatList)

? ? ? ? ? ? ? ? })

? ? ? ? ? ? },

? ? ? ? ? ? sendMsg(){

? ? ? ? ? ? ? ? if(!this.value||this.value ===''){

? ? ? ? ? ? ? ? ? ? alert("請輸入消息");

? ? ? ? ? ? ? ? ? ? return

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? this.chatSocket.send(JSON.stringify({

? ? ? ? ? ? ? ? 'message': '<span class="text" >'+ this.value +'</span>'

? ? ? ? ? ? ? ? }));

? ? ? ? ? ? ? ? $('.content').append('<div class="message"><div class="user"><div class="text-cont"><span class="text">' + this.value + '</span></div> <div class="img-box"><img src="'+ this.userImg +'"></div></div></div>' );

? ? ? ? ? ? ? ? this.value = '';

? ? ? ? ? ? ? ? // 滾動到底部

? ? ? ? ? ? ? ? var h = $('.content')[0].scrollHeight;

? ? ? ? ? ? ? ? $('.content').scrollTop(h);

? ? ? ? ? ? }

? ? ? ? },

? ? ? ? created(){

? ? ? ? ? ? this.getData(0);

? ? ? ? },

? ? ? ? mounted(){

? ? ? ? }

? ? })

? ? var dropBox;

? window.onload=function(){

? dropBox = document.getElementById("dropBox");

? // 鼠標進入放置區(qū)時

? dropBox.ondragenter = ignoreDrag;

? // 拖動文件的鼠標指針位置放置區(qū)之上時發(fā)生

? dropBox.ondragover = ignoreDrag;

? dropBox.ondrop = drop;

? }

? function ignoreDrag(e){

? // 確保其他元素不會取得該事件

? e.stopPropagation();

? e.preventDefault();

? }

? function drop(e){

? e.stopPropagation();

? e.preventDefault();

? // 取得拖放進來的文件

? var data = e.dataTransfer;

? var files = data.files;

? // 將其傳給真正的處理文件的函數

? var file = files[0];

? var reader = new FileReader();

? reader.onload=function(e){

? ? $('.content').append('<div class="message"><div class="user"><div class="text-cont"><img style="width: 80px; height: 60px;" src="'+e.target.result+'" data-preview-src=""></div> <div class="img-box"><img src="'+ vm.userImg +'"></div></div></div>' );

? ? obj.init();

? ? $('.text-cont img').click(function () {

? ? ? ? $('.bigimg').attr('src', $(this).attr('src'))});

? vm.chatSocket.send(JSON.stringify({

? ? ? ? 'message': e.target.result

? ? }));

? };

? reader.readAsDataURL(file);

? }

</script>

</html>

之后為django Xadmin后臺添加一個模塊, 把后臺聊天頁面添加到xadmin中.

首先在xadmin全局配置中添加get_site_menu方法, 為聊天頁面添加一個導航框, 代碼如下:

之后進行注冊全局配置類

from xadmin import views

xadmin.site.register(views.CommAdminView, GlobalSetting)

運行manage.py, 打開谷歌瀏覽器輸入127.0.0.1:8000/xadmin, 打開截圖如下:


多了一個chat模塊, 點開進去, 進入聊天界面(小編的這個界面是模仿了微信界面), 怎么進入到了微信網頁界面呢? 哈哈哈.

由于這個是app端和后臺客服管理端的聊天應用, 所以要把本地代碼部署到服務器上, 然后借助前端混合開發(fā)小哥的移動端才能進行消息通訊, 不過各位朋友也可以相應的制作一款網頁版的聊天系統(tǒng)來玩玩.

下一期內容就是部署這個channels到服務器上, 敬請期待...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末计福,一起剝皮案震驚了整個濱河市砰琢,隨后出現的幾起案子厦酬,更是在濱河造成了極大的恐慌融撞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件指煎,死亡現場離奇詭異蹋偏,居然都是意外死亡,警方通過查閱死者的電腦和手機至壤,發(fā)現死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門威始,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人像街,你說我怎么就攤上這事黎棠。” “怎么了镰绎?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵脓斩,是天一觀的道長。 經常有香客問我跟狱,道長俭厚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任驶臊,我火速辦了婚禮挪挤,結果婚禮上,老公的妹妹穿的比我還像新娘关翎。我一直安慰自己扛门,他們只是感情好,可當我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布纵寝。 她就那樣靜靜地躺著论寨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪爽茴。 梳的紋絲不亂的頭發(fā)上葬凳,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音室奏,去河邊找鬼火焰。 笑死,一個胖子當著我的面吹牛胧沫,可吹牛的內容都是我干的昌简。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼绒怨,長吁一口氣:“原來是場噩夢啊……” “哼纯赎!你這毒婦竟也來了?” 一聲冷哼從身側響起南蹂,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤犬金,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后六剥,有當地人在樹林里發(fā)現了一具尸體佑附,經...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年仗考,在試婚紗的時候發(fā)現自己被綠了音同。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡秃嗜,死狀恐怖权均,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情锅锨,我是刑警寧澤叽赊,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站必搞,受9級特大地震影響必指,放射性物質發(fā)生泄漏。R本人自食惡果不足惜恕洲,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一塔橡、第九天 我趴在偏房一處隱蔽的房頂上張望梅割。 院中可真熱鬧,春花似錦葛家、人聲如沸户辞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽底燎。三九已至,卻和暖如春弹砚,著一層夾襖步出監(jiān)牢的瞬間双仍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工桌吃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留朱沃,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓读存,卻偏偏與公主長得像为流,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子让簿,可洞房花燭夜當晚...
    茶點故事閱讀 42,802評論 2 345

推薦閱讀更多精彩內容

  • ¥開啟¥ 【iAPP實現進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程敬察,因...
    小菜c閱讀 6,358評論 0 17
  • H5移動端知識點總結 閱讀目錄 移動開發(fā)基本知識點 calc基本用法 box-sizing的理解及使用 理解dis...
    Mx勇閱讀 4,395評論 0 26
  • 移動開發(fā)基本知識點 一.使用rem作為單位 html { font-size: 100px; } @media(m...
    橫沖直撞666閱讀 3,453評論 0 6
  • 今天去三姑家了,大姑家的幾個女兒女婿來拜年尔当,大家一起吃了個年飯莲祸。今年過節(jié)在老家沒有去一家我的親戚家,都是老...
    棟姐閱讀 155評論 0 1
  • 圖片上的是我的爸爸媽媽椭迎,爸爸抱著我的女兒锐帜,媽媽抱著姐姐的女兒,你們都是我的最愛畜号,在這里我就先謝謝我的爸爸 1.謝謝...
    傾斜5度閱讀 318評論 0 3