????最近公司業(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到服務器上, 敬請期待...