ant-vue搭建一个聊天窗口

该博客详细介绍了基于WebSocket构建的实时聊天系统的设计思路和实现细节,包括页面布局模仿微信,后台接口定义,消息列表展示,以及聊天详情页的交互设计。使用Vue.js进行前端开发,并通过WebSocket保持长连接,确保消息的即时推送。此外,还解决了滚动条置底的问题,以及在切换会话时清空消息列表以避免监听异常。
摘要由CSDN通过智能技术生成

效果图:

设计思路:

1、首先是页面布局,参考了微信,左边是会话列表,右边是具体消息

2、后台接口:

会话列表

 {
        "addTime": "2021-08-19T07:48:06.501Z",
        "clientType": 0,
        "content": "string",
        "deleted": true,
        "id": 0,
        "inservice": 0, 
        "mobile": "string",
        "msgType": 0, // 0:文字 1:图片 -3:保持ws链接状态
        "updateTime": "2021-08-19T07:48:06.501Z",
        "userid": 0, // 用户ID
        "username": "string",
        "viewed": 0 // 是否看过,看过取消红点
      }

消息列表

      {
        "addTime": "2021-08-19T07:52:58.552Z",
        "clientType": 0,
        "content": "string", // 内容
        "deleted": true,
        "fromUserid": 0, // 发消息者
        "id": 0,
        "msgType": 0,
        "toUserid": 0, // 接受者
        "updateTime": "2021-08-19T07:52:58.552Z",
        "viewed": 0
      }

3、webSocket建立前后端通信,设置定时器发送msgType=-3保持连接

4、一些基本细节:

发消息者居右且绿色背景,接收者居左且白色背景;

图片大小缩放,可单击查看放大的图片;

聊天栏若有滚动条,滚动条置底,这里有个坑会详细讲下;

如果有历史消息,聊天栏置顶增加“查看更多消息”按钮;

页面:

<template>
  <page-header-wrapper>
    <div class="table-page-search-wrapper">
      <a-form layout="inline">
        <a-row :gutter="16">
          <a-col :xl="6" :lg="8" :md="12" :sm="24">
            <a-card :bordered="false" class="height-1000">
              <a-table
                :rowKey="(record, index) => index"
                :dataSource="conversationForm"
                :columns="columns"
                :pagination="paginationFlag"
                :customRow="click"
              >
                <span slot="serial" slot-scope="text, record, index">
                  <div>
                    <strong v-if="conversationFlagList[index]" class="unRead">·</strong>
                    <strong @click="selectChat(text, record, index)"> {{ text.username }}</strong>
                    <a style="float: right" @click="selectChat(text, record, index)"> {{ text.addTime }}</a>
                  </div>
                  <a @click="selectChat(text, record, index)">
                    <span v-if="text.msgType === 0">
                      {{
                        text.content ? (text.content.length > 20 ? text.content.slice(0, 20) : text.content) : ''
                      }}</span
                    >
                    <span v-if="text.msgType === 1">[ 图片 ]</span>
                  </a>
                </span>
              </a-table>
            </a-card>
          </a-col>
          <a-col :xl="18" :lg="8" :md="12" :sm="24">
            <div class="height-1000">
              <a-card :bordered="false" class="topCard">
                <div>
                  <strong>{{ this.userInfo.name ? this.userInfo.name : '' }}</strong>
                  <span>{{ ' ' + this.userInfo.mobile ? this.userInfo.mobile : '' }}</span>
                  <span>{{ ' ' + this.userInfo.from ? this.userInfo.from : '' }}</span>
                </div>
              </a-card>
              <a-card :bordered="false" class="midCard" ref="midCard">
                <div v-if="moreRecordsFlag" class="moreRecords">
                  <a-button @click="getMoreMsg">查看更多消息</a-button>
                </div>
                <div :key="index" v-for="(item, index) in messageList">
                  <a-form-item :class="parseInt(item.fromUserid) === userInfo.userId ? 'leftPart' : 'rightPart'">
                    <div :class="parseInt(item.fromUserid) === userInfo.userId ? '' : 'time'">
                      <span>{{ item.addTime }}</span>
                    </div>
                    <div class="chatDiv">
                      <span v-if="item.msgType === 0">{{ item.content }}</span>
                      <img
                        v-if="item.msgType === 1"
                        :src="item.content"
                        class="img"
                        @click="viewPicture(item.content)"
                      />
                    </div>
                  </a-form-item>
                </div>
              </a-card>
              <a-card :bordered="false" class="bottomCard">
                <a-form-item v-show="userInfo.userId !== ''">
                  <a-textarea class="w100" v-model="form.response" placeholder="请输入回复内容" :rows="3" />
                  <a-button type="primary" style="float: right" @click="sendMsg">发送</a-button>
                </a-form-item>
              </a-card>
            </div>
          </a-col>
        </a-row>
      </a-form>
      <a-modal v-model="handleVisible" :footer="null" width="600px" wrapClassName="modalBody">
        <img :src="viewPictureSrc" class="largeImg" />
      </a-modal>
    </div>
  </page-header-wrapper>
</template>

JS:

<script>
import { STable, Ellipsis } from '@/components'
import { msgList, conversationList } from '@/api/customerService'
const columns = [
  {
    scopedSlots: { customRender: 'serial' }
  }]
export default {
  name: 'CurrentChat',
  components: {
    STable,
    Ellipsis
  },
  data() {
    this.columns = columns
    return {
      // 大于10条显示分页
      messageList: [],
      timestamp: undefined,
      moreRecordsFlag: false,
      conversationForm: [],
      conversationFlagList: [],
      paginationFlag: false,
      midCard: undefined,
      clearTimeSet: undefined, // 定时器
      handleVisible: false,
      viewPictureSrc: undefined,
      fromUserid: '',
      userInfo: {
        name: '',
        mobile: '',
        from: '',
        userId: '',
        clientType: ''
      },
      path: '',
      socket: '',
      // 查询参数
      queryParam: {
        saleSn: undefined,
        consignee: undefined,
        mobile: undefined,
        saleStatus: undefined,
        sort: 'add_time',
        order: 'desc'
      },
      form: {
        response: ''
      },
      params: {
        clientType: 0,
        limit: 10,
        order: 'desc',
        page: 1,
        userId: undefined,
        sort: 'add_time',
        timestamp: undefined
      }
    }
  },
  mounted() {
    this.getMsgList()
    this.getConversationList()
    // setInterval(() => {
    //   this.reConnect()
    // }, 60000)
  },
  watch: {
    messageList: function (val) {
      if (val.length > 0) {
        this.refresh()
      }
    }
  },
  methods: {
    getMsgList() {
      msgList(this.params).then(res => {
        const { errno } = res
        if (errno === 0) {
          const tempMessageList = res.data && res.data.list ? res.data.list : []
          this.moreRecordsFlag = true
          if (tempMessageList.length === 0) {
            this.moreRecordsFlag = false
          }
          tempMessageList.reverse()
          this.messageList = tempMessageList.concat(this.messageList)
        }
      }).catch(err => {
        console.log(err)
      })
    },
    getMoreMsg() {
      this.params.timestamp = new Date(this.messageList[0].addTime).getTime()
      this.getMsgList()
    },
    async sendMsg() {
      if (this.form.response.replace(/ /g, '').length === 0) {
        this.$message.error('请勿回复空白')
        return false
      }
      if (!this.isOnlineCurrUser(this.socket)) {
        this.socket = ''
        this.init()
        this.$message.info('已离线,正在重连')
        // this.socket = ''
        // setInterval(this.init(), 1000)
        // this.init()
        this.$message.success('连接成功')
        return
        // this.sendMsg()
        // this.socket.send(JSON.stringify(parmas))
      } else {
        const parmas = {
          clientType: this.params.clientType,
          content: this.form.response,
          fromUserid: sessionStorage.getItem('adminId'),
          msgType: 0,
          toUserid: this.userInfo.userId
        }

        this.socket.send(JSON.stringify(parmas))
        const yy = new Date().getFullYear()
        const mm = new Date().getMonth() + 1
        const dd = new Date().getDate()
        const hh = new Date().getHours()
        const mf = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() : new Date().getMinutes()
        const time = yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mf
        parmas.addTime = time
        this.messageList.push(parmas)
      }
      // await sendToUserId(parmas)
      // this.params.timestamp = undefined
      // msgList(this.params).then(res => {
      //   const { errno } = res
      //   if (errno === 0) {
      //     const tempMessageList = res.data && res.data.list ? res.data.list : []
      //     if (tempMessageList.length === 0) {
      //       this.moreRecordsFlag = false
      //     }
      //     this.messageList.push(tempMessageList[0])
      //   }
      // }).catch(err => {
      //   console.log(err)
      // })
      this.form.response = undefined
    },
    getConversationList() {
      const params = {
        limit: 10,
        order: 'desc',
        page: 1,
        sort: 'add_time'
      }
      conversationList(params).then(res => {
        const { errno } = res
        if (errno === 0) {
          this.conversationForm = res.data && res.data.list ? res.data.list : []
          this.conversationForm.forEach(e => {
            this.conversationFlagList.push(e.inservice === 0 && e.viewed === 0)
          })
          this.paginationFlag = (res.data.total > 9)
        }
      }).catch(err => {
        console.log(err)
      })
    },
    reConnect() {
      if (!this.isOnlineCurrUser(this.socket) && this.userInfo.userId !== '') {
        this.socket = ''
        this.init()
      }
    },
    init: function () {
      if (typeof (WebSocket) === 'undefined') {
        this.$message.error('您的浏览器不支持socket')
      } else {
        // 实例化socket
        this.path = 'wss://dev.tianmiwo.com/api/webSocket/' + this.userInfo.userId + '-' + sessionStorage.getItem('adminId')
        this.socket = new WebSocket(this.path)
        // 监听socket连接
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
        const parmas = {
          clientType: this.params.clientType,
          fromUserid: sessionStorage.getItem('adminId'),
          msgType: -3,
          toUserid: this.userInfo.userId
        }
        this.setTimer(parmas)
      }
    },
    open: function () {
      console.log('socket连接成功')
    },
    error: function () {
      console.log('连接错误')
    },
    getMessage: function (msg) {
      console.log('---------------------------')
      // console.log(msg)
      console.log(msg.data)
      const yy = new Date().getFullYear()
      const mm = new Date().getMonth() + 1
      const dd = new Date().getDate()
      const hh = new Date().getHours()
      const mf = new Date().getMinutes() < 10 ? '0' + new Date().getMinutes() : new Date().getMinutes()
      const time = yy + '-' + mm + '-' + dd + ' ' + hh + ':' + mf
      const data = JSON.parse(msg.data)
      if (data.content === 'pong') {
        return
      }
      data.addTime = time
      this.messageList.push(data)
      console.log('---------------------------')
    },
    send: function () {
      // this.socket.send(params)
    },
    close: function () {
      console.log('socket已经关闭')
    },
    // 选择会话
    click(record, index) {
      return {
        on: {
          click: () => {
            this.selectChat(record, index)
          }
        }
      }
    },
    // 选择会话
    selectChat(record, index) {
      if (this.socket !== '') {
        this.socket.onclose = this.close
        this.socket.close()
        this.clearTimer()
      }
      this.$set(this.conversationFlagList, index, false)
      this.messageList = []
      this.params.userId = record.userid
      this.params.clientType = record.clientType
      this.getMsgList()
      this.userInfo.name = record.username
      this.userInfo.mobile = '(' + (record.mobile ? record.mobile : '--') + ')'
      this.userInfo.userId = record.userid
      this.userInfo.clientType = record.clientType
      switch (record.clientType) {
        case 0:
          this.userInfo.from = '来自会员端'
          break
        case 1:
          this.userInfo.from = '来自管理端'
          break
        case 2:
          this.userInfo.from = '来自收银台'
          break
        case 3:
          this.userInfo.from = '来自后台'
          break
      }
      // 初始化
      this.init()
      this.refresh()
    },
    // 滚动条置底
    refresh() {
      this.$nextTick(() => {
        // this.midCard = this.$refs.midCard
        // console.log(this.midCard.scrollHeight)
        document.getElementsByClassName('midCard')[0].scrollTop = document.getElementsByClassName('midCard')[0].scrollHeight
        // console.log(document.getElementsByClassName('midCard')[0].scrollTop)
        // console.log(document.getElementsByClassName('midCard')[0].scrollHeight)
      })
    },
    /**
   * 判断当前用户是否 还在线
   */
    isOnlineCurrUser(ws) {
      if (ws) {
        if (ws.readyState === WebSocket.OPEN) {
          return true
        } else {
          return false
        }
      } else {
        return false
      }
    },
    // 设置定时器
    setTimer(parmas) {
      this.clearTimeSet = setInterval(() => {
        this.socket.send(JSON.stringify(parmas))
      }, 20000)
    },
    // 清除定时器
    clearTimer() {
      clearInterval(this.clearTimeSet)
    },
    // 查看大图
    viewPicture(content) {
      this.handleVisible = true
      this.viewPictureSrc = content
    }
  },
  destroyed() {
    // 销毁监听
    if (this.socket !== '') {
      this.socket.onclose = this.close
      this.socket.close()
      // console.log('退出页面时关闭连接')
      this.clearTimer()
    }
  }
}
</script>

 

样式:

<style lang="less" scoped>
.height-1000 {
  min-height: 600px;
}
.topCard {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 50px;
}
.midCard {
  height: 400px;
  // 透明背景
  background-color: rgba(255, 255, 255, 0.2);
  overflow-y: auto;
  .leftPart {
    width: 100%;
    .chatDiv {
      padding: 0 10px 0 10px;
      float: left;
      // CSS3内容自适应
      width: fit-content;
      background-color: white;
      border-radius: 5px;
    }
  }
  .rightPart {
    width: 100%;
    .chatDiv {
      padding: 0 10px 0 10px;
      float: right;
      width: fit-content;
      background-color: #98e165;
      border-radius: 5px;
    }
  }
}
.bottomCard {
  height: 150px;
}

// div {
//   border: 1px solid black;
// }
.time {
  display: flex;
  justify-content: flex-end;
  background-color: rgba(255, 255, 255, 0);
}
.moreRecords {
  display: flex;
  justify-content: center;
  .ant-btn {
    border: 0;
    background-color: rgba(255, 255, 255, 0);
    color: cornflowerblue;
  }
}
::v-deep .ant-table-thead {
  display: none;
}
.unRead {
  color: red;
  left: -1px;
  font-size: 30px;
  line-height: 5px;
  position: absolute;
}
.img {
  width: 100px;
  height: 100px;
}
.largeImg {
  width: 600px;
  height: 600px;
}
::v-deep .ant-modal-body {
  padding: 0 !important;
  font-size: 0 !important;
  line-height: 1 !important;
}

关于滚动条置底

一开始想的比较简单,直接硬怼

    // 滚动条置底
    refresh() {
      this.$nextTick(() => {
           this.midCard = this.$refs.midCard
        // console.log(this.midCard.scrollHeight)
           this.midCard.scrollTop = this.midCard.scrollHeight
      })
    },
    // 选择会话
    selectChat(record, index) {
      if (this.socket !== '') {
        this.socket.onclose = this.close
        this.socket.close()
        this.clearTimer()
      }
      this.$set(this.conversationFlagList, index, false)
      this.messageList = []
      this.params.userId = record.userid
      this.params.clientType = record.clientType
      this.getMsgList()
      this.userInfo.name = record.username
      this.userInfo.mobile = '(' + (record.mobile ? record.mobile : '--') + ')'
      this.userInfo.userId = record.userid
      this.userInfo.clientType = record.clientType
      switch (record.clientType) {
        case 0:
          this.userInfo.from = '来自会员端'
          break
        case 1:
          this.userInfo.from = '来自管理端'
          break
        case 2:
          this.userInfo.from = '来自收银台'
          break
        case 3:
          this.userInfo.from = '来自后台'
          break
      }
      // 初始化
      this.init()
      this.refresh()
    },

后来发现this.midCard.scrollHeight始终是undefined,debug发现this.midCard设置滚动条时dom渲染还没有完成,所以只好为消息列表加上监听事件

  watch: {
    messageList: function (val) {
      if (val.length > 0) {
        this.refresh()
      }
    }
  },

这里注意每次切换会话的时候清一下messageList免得发生一些意料之外的无法监听。

最后,this.midCard.scrollTop赋值居然不生效!非常怪,可能是封装的组件的问题?我用 document.getElementsByClassName('midCard')[0].scrollTop = document.getElementsByClassName('midCard')[0].scrollHeight这样解决的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值