聊天页面(vue、语音发送、websocket等)

前言

最近在做一个h5端的聊天app,在聊天框这一块有很多细节,特此进行记录一下
涉及的知识点有:
	vue语音的录制插件使用,
	websocket连接通信,
	语音播放动画样式组件封装,
	语音发送取消发送逻辑,
	发送消息滚动条置底,
	向上滚屏逻辑,
	时间显示逻辑

效果图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

vue语音的录制插件recorderx使用

  1. 安装
    npm install recorderx -S

  2. 使用

    在聊天组件引入
    
    	import Recorderx, { ENCODE_TYPE } from 'recorderx';
    	const rc = new Recorderx()
    
    录制语音函数
    
    	 rc.start()
        .then(() => {
          this.maikef = true
          // that.news_img = !that.news_img
          console.log('start recording')
        })
        .catch(error => {
          this.$toast.fail('获取麦克风失败')
          this.maikef = false
          this.reset()
          this.timeShow = false
          console.log('Recording failed.', error)
        })
    
    	取消录音	
    
    // 取消语音
    cancel: function () {
      rc.clear()
    },
    // 暂停语音
    cancel_mp3: function () {
      rc.pause()
    },
    
    获取录音文件上传
    
    	async send_voice () {
          rc.pause() // 先暂停录音
          const wav = rc.getRecord({
            encodeTo: ENCODE_TYPE.WAV,
            compressible: true
          }) // 获取录音文件
          console.log('wav', wav)
          try {
            const formData = new FormData()
            // formData.append('file',wav);
            formData.append('type', 2)
            formData.append('file', wav, Date.parse(new Date()) + '.wav')
            // formData.append('file', wav, Date.parse(new Date()) + '.mp3')
            // const headers = { headers: { 'Content-Type': 'multipart/form-data' } }
            const res = await setAudio(formData)  // setAudio是封装的上传文件方法
            console.log(res)
            if (res.data.code === 200) {
              this.sendtheVoice(res.data.data.url)
            } else {
              this.$toast.fail(res.data.msg)
            }
          } catch (err) {
            console.log(err)
            this.$toast.fail('网络错误请稍后重试')
          }
       }
    

websocket连接通信

  1. created钩子
    	if ('WebSocket' in window) {
          this.initWebSocket()
        } else {
          alert('当前浏览器不支持websocketio连接')
    }
    

2、destroyed钩子

	destroyed () {
	    // 组件销毁时,关闭与服务器的连接
	    if (this.socketio) {
	      // this.socketio.close()
	      // this.socketio.onclose = function () {
	      //   console.log('连接已关闭...')
	      // }
	      this.socketio.close() // 离开路由之后断开websocket连接
	    }
	    clearInterval(this.timer)
	  },
  1. method下

    initWebSocket () {
      let protocol = 'ws'
      if (location.protocol === 'https:') {
        protocol = 'wss'
      }
      var url = protocol + '://192.168.100.33:8080' + `/websocket/${this.$route.query.chatRoom}/${this.user.id}/${encodeURI(this.user.name)}`
      // decodeURI
      this.socketio = new WebSocket(url)
      // 建立连接
      this.socketio.onopen = function () {
        // this.socketio.send('已经上线了')
        console.log('已经连通了websocket')
      }
      // 接收消息
      // 接收后台服务端的消息    接收到消息
      this.socketio.onmessage = (evt) => {
        console.log('数据已接收:', evt)
        const obj = JSON.parse(evt.data)
        console.log('obj', obj)
        this.list.push(obj)
        this.scrollToBottom()
      }
      // 连接建立失败重连
      this.socketio.onerror = this.websocketonerror
      // 关闭
      this.socketio.onclose = this.websocketclose
    },
    websocketonerror () { // 连接建立失败重连
      console.log('websocket连接断开')
      this.initWebSocket()
    },
    websocketclose (e) { // 关闭
      console.log('断开连接', e)
    },
    
    发送消息
     this.socketio.send(JSON.stringify(data))
    

语音播放动画样式组件封装

```
	思路:封装语音消息:
	1、接收父组件传递参:
			src 语音文件路径 
			value播放状态控制,当其他组件播放时当前组件停止播放
	2、点击播放时动画样式
```
  1. template

    	<div class="audio__wrap">
    	    <audio controls :src="src" ref="audioPlayer" style="display:none"></audio>
    	    <div class="self__audio" @click="playAudioHandler">
    	    	<!-- 时间显示 -->
    	      <div class="audio__duration">{{duration}}"</div>
    	      <!-- 动画样式 -->
    	      <div class="audio__trigger">
    	        <div
    	          :class="{
    	            'wifi-symbol':true,
    	            'wifi-symbol--avtive':isPlaying
    	        }"
    	        >
    	          <div class="wifi-circle second"></div>
    	          <div class="wifi-circle third"></div>
    	          <div class="wifi-circle first"></div>
    	        </div>
    	      </div>
    	    </div>
    	  </div>
    	```
    
  2. script

    	export default {
    		  data () {
    		    return {
    		      isPlaying: false,
    		      duration: ''
    		    }
    		  },
    		  props: {
    		    src: {
    		      type: String,
    		      required: true
    		    },
    		    value: {
    		      type: Boolean,
    		      required: true
    		    }
    		  },
    		  watch: {
    		    value: { // 监听控制播放停止
    		      handler (newValue, oldValue) {
    		        if (!this.value) {
    		          this.isPlaying = false
    		          this.$refs.audioPlayer.load()
    		        }
    		      }
    		    }
    		  },
    		  methods: {
    		    playAudioHandler () {
    		      this.isPlaying = !this.isPlaying
    		      const player = this.$refs.audioPlayer
    		      if (this.isPlaying) {
    		        player.load()
    		        player.play()
    		      } else {
    		        player.pause()
    		      }
    		      setTimeout(() => {
    		        this.isPlaying = false
    		        this.$emit('input', false)
    		      }, (this.duration ? this.duration : 0) * 1000)
    		    }
    		  },
    		  mounted () {
    		    const player = this.$refs.audioPlayer
    		    player.load()
    		    const vm = this
    		    player.oncanplay = function () {
    		      vm.duration = Math.ceil(player.duration)
    		    }
    		  }
    	}
    
  3. style

    .audio__wrap {
      .self__audio {
        .audio__duration {
          display: inline-block;
          line-height: 32px;
          height: 32px;
          padding-right: 6px;
          color: #888888;
        }
        .audio__trigger {
          cursor: pointer;
          vertical-align: top;
          display: inline-block;
          line-height: 32px;
          height: 32px;
          width: 100px;
          background-color: #e0effb;
          border-radius: 4px;
          position: relative;
          .wifi-symbol {
            position: absolute;
            right: 4px;
            top: -8px;
            width: 50px;
            height: 50px;
            box-sizing: border-box;
            overflow: hidden;
            transform: rotate(-45deg) scale(0.5);
            .wifi-circle {
              border: 5px solid #999999;
              border-radius: 50%;
              position: absolute;
            }
    
            .first {
              width: 5px;
              height: 5px;
              background: #cccccc;
              top: 45px;
              left: 45px;
            }
            .second {
              width: 25px;
              height: 25px;
              top: 35px;
              left: 35px;
            }
            .third {
              width: 40px;
              height: 40px;
              top: 25px;
              left: 25px;
            }
          }
          .wifi-symbol--avtive {
            .second {
              animation: bounce 1s infinite 0.2s;
            }
            .third {
              animation: bounce 1s infinite 0.4s;
            }
          }
        }
        @keyframes bounce {
          0% {
            opacity: 0; /*初始状态 透明度为0*/
          }
          100% {
            opacity: 1; /*结尾状态 透明度为1*/
          }
        }
      }
    }	
    

语音发送取消发送逻辑

1、监听语音按钮的点击事件
根据点击上移距离判断是否发送or取消发送
2、当点击语音后面板开始进行计时
  <input type="button" id="messageBtn" v-show="checked" class="btn" :value="value" />
    <!-- 计时器区域 -->
    <van-popup v-model="timeShow" :overlay="false">
      <div>{{minute>=10?minute:'0'+minute}}:{{second>=10?second:'0'+second}}</div>
      <div>手指上滑,取消发送</div>
    </van-popup>
 watch: {
    checked: {
      // username 监听输入框输入
      handler (newValue, oldValue) {
        if (this.checked === true) {
          this.saveYyxiaoxi()
        }
      }
    }
  },

saveYyxiaoxi () {
      // 获取语音发送变量
      console.log('ahsadgh1')
      if (this.jianting === 0) {
        this.btnElem = document.getElementById('messageBtn')
        this.initEvent()
      }
      this.jianting = 1 // 变量控制只执行一次initEvent()
    },
    // 点击发送语音 上滑取消语音逻辑
    initEvent () {
      this.btnElem.addEventListener('touchstart', (event) => {
        // event.preventDefault()// 阻止浏览器默认行为
        this.posStart = 0
        this.posStart = event.touches[0].pageY// 获取起点坐标
        this.value = '松开 发送'
        this.timeShow = true
        clearInterval(this.time)
        this.time = setInterval(this.timer1, 1000)
        this.handleBtnClick() // 开始录制语音
        console.log('start')
        console.log(this.posStart + '---------开始坐标')
      })
      this.btnElem.addEventListener('touchmove', (event) => {
        event.preventDefault()// 阻止浏览器默认行为
        this.posMove = 0
        this.posMove = event.targetTouches[0].pageY// 获取滑动实时坐标
        if (this.posStart - this.posMove < 30) {
          this.value = '松开 发送'
        } else {
          this.value = '松开手指,取消发送'
        }
      })
      this.btnElem.addEventListener('touchend', (event) => {
        event.preventDefault()
        this.posEnd = 0
        this.posEnd = event.changedTouches[0].pageY// 获取终点坐标
        this.value = '按住 说话'
        console.log('End')
        console.log(this.posEnd + '---------结束坐标')
        if (this.posStart - this.posEnd < 30) {
          console.log('发送成功')
          this.save() // 语音获取上传事件
        } else {
          console.log('取消发送')
          console.log('Cancel')
        };
        this.cancel() // 录制语音清除缓存
        this.timeShow = false
        this.reset() // 时间归零
      })
    },
    
    // 发送语音时计时器函数
    timer1 () { // 定义计时函数
      console.log(this.second)
      this.second = this.second + 1 // 秒
      if (this.second >= 60) {
        this.second = 0
        this.minute = this.minute + 1 // 分钟
      }

      if (this.minute >= 60) {
        this.minute = 0
        this.hour = this.hour + 1 // 小时
      }
    },
    
	// 录制语音
    handleBtnClick: function () {
      // const that = this
      // that.news_img = !that.news_img
      rc.start()
        .then(() => {
          this.maikef = true
          // that.news_img = !that.news_img
          console.log('start recording')
        })
        .catch(error => {
          this.$toast.fail('获取麦克风失败')
          this.maikef = false
          this.reset()
          this.timeShow = false
          console.log('Recording failed.', error)
        })
    },

	 // 发送语音
    async send_voice () {
      // if (!this.maikef) { // 是否获取麦克风
      //   return
      // }
      rc.pause()
      const wav = rc.getRecord({
        encodeTo: ENCODE_TYPE.WAV,
        compressible: true
      })
      console.log('wav', wav)
      try {
        const formData = new FormData()
        formData.append('type', 2)
        formData.append('file', wav, Date.parse(new Date()) + '.wav')
        const res = await setAudio(formData)
        console.log(res)
        if (res.data.code === 200) {
          this.sendtheVoice(res.data.data.url)
        } else {
          this.$toast.fail(res.data.msg)
        }
      } catch (err) {
        console.log(err)
        this.$toast.fail('网络错误请稍后重试')
      }
    save () { // 发送语音消息
      console.log('开始发送吧')
      this.send_voice()
    },
    // 取消语音
    cancel: function () {
      rc.clear()
    },
     reset () { // 重置
      clearInterval(this.time)
      this.time = null
      this.hour = 0
      this.minute = 0
      this.second = 0
    },

发送消息滚动条置底

每次接收消息、发送消息后调用scrollToBottom 函数
// 滚动事件
    scrollToBottom () {
      this.$nextTick(() => {
        const dom = this.$refs.refList
        // scrollTop 是dom元素的属性,可以去手动设置
        //   它表示当前dom元素中的滚动条距离元素顶部的距离
        dom.scrollTop = dom.scrollHeight
      })
    },

向上滚屏

	1、给聊天列表区域加ref=refList
	2、当滚动条滚动到距离顶部还有50距离时,获取新的数据
		scrollTop () {
		      this.dom = this.$refs.refList
		      this.dom.onscroll = () => {
		        console.log(this.list, this.total)
		        if (this.list.length >= this.total) { // 当列表数据和总数相等时停止
		          return
		        }
		        if (this.dom.scrollTop < 50) {
		          if (this.scollRight) {
		            return
		          }
		          this.scollRight = true // scollRight 控制调取接口的频率,一次只执行一遍
		          setTimeout(() => {
		            this.pageSize = this.pageSize + 10 // 扩大每页请求数据条数
		            this._Chatrecord() // 调取接口获取数据
		            this.dom.scrollTop = this.dom.scrollTop + this.$refs.refList.clientHeight
		          }, 1000)
		        }
		      }
		    },

时间处理

vue+原生js仿钉钉做聊天时间处理

全部代码

template

<template>
  <div class="container">
    <!-- 固定导航 -->
    <van-nav-bar fixed left-arrow @click-left="$router.back()" :title="$route.query.friendName"></van-nav-bar>

    <!-- 聊天主体区域 -->
    <div class="chat-list" ref="refList">
      <div
        v-for="(item,idx) in list"
        :key="idx"
        :class="item.senderId === user.id ? 'right' : 'left'"
        class="chat-item">
          <!-- template是一个逻辑上的容器,页面在渲染时,它不会产生dom -->
          <template v-if="item.senderId === user.id">
            <div class="chat-pao" @click="isRight=!isRight">
              <Audio v-if="item.contentType==='voice'" :src="item.content" v-model="isRight"></Audio>
              <van-image v-else-if="item.contentType==='image'" fit="cover" :src="item.content"/>
              <div v-else style="text-align:left">{{item.content}}</div>
              <span>{{_timeChange(item.senderTime)}}</span>
            </div>
            <!-- <van-image fit="cover" round :src="Common.api+userPhoto" /> -->
            <van-image fit="cover" round :src="item.senderAvatar" />
          </template>
          <template v-else>
            <van-image fit="cover" round :src="item.senderAvatar" @click="$router.push('/perInfo')" />
            <div class="chat-pao" @click="isRight=!isRight">
             <Audio1 v-if="item.contentType==='voice'" :src="item.content" v-model="isRight"></Audio1>
              <van-image style="width: 100%;height: 100%;" v-else-if="item.contentType==='image'" fit="cover" :src="item.content"/>
              <div v-else style="text-align:left">{{item.content}}</div>
              <span>{{_timeChange(item.senderTime)}}</span>
            </div>
          </template>
      </div>
    </div>

    <!-- 对话区域 说话-->
    <div class="reply-container van-hairline--top">
      <div class="row">
        <i class="iconfont" :class="checked?'icon-jianpan':'icon-yuyin'" @click="checked=!checked;photoShow=false"></i>
        <van-field v-show="!checked" v-model.trim="word" @focus="photoShow=false" @input="send_hc" placeholder="说点什么...">
          <!-- <span  @click="send()" slot="button" style="font-size:12px;color:#999">提交</span> -->
        </van-field>
        <input type="button" id="messageBtn" v-show="checked" class="btn" :value="value" />
        <i class="iconfont icon-tianjia" @click="photoShow=true"></i>
      </div>
      <div class="photoShow" v-show="photoShow">
        <span><i class="iconfont icon-camera"></i> 拍照</span>
        <span @click="uploadPhone"><i class="iconfont icon-tupian"></i> 图片</span>
      </div>
      <input type="file" hidden @change="hChangeImage" ref="refFile"/>
    </div>
    <!-- 计时器区域 -->
    <van-popup v-model="timeShow" :overlay="false">
      <div>{{minute>=10?minute:'0'+minute}}:{{second>=10?second:'0'+second}}</div>
      <div>手指上滑,取消发送</div>
    </van-popup>
  </div>
</template>

script

<script>
import Audio from '../common/Audio'
import Audio1 from '../common/Audio1'
import { timeChange } from '../../assets/js/common'
// import io from 'socket.io-client'
import { mapGetters } from 'vuex'
import Recorderx, { ENCODE_TYPE } from 'recorderx'
import { setAvater, setAudio } from '../../api/user.js'
import { Chatrecord } from '../../api/chat'
const rc = new Recorderx()
export default {
  name: 'UserChat',
  components: {
    Audio,
    Audio1
  },
  data () {
    return {
      list: [ // 对话记录
        { name: 'xz', msg: '哦,你cv一定很熟!', timestamp: Date.now() },
        { name: 'xz', msg: '您好,怎么和青春期叛逆的孩子沟通 呢?', timestamp: Date.now() },
        { name: 'me', msg: '在孩子面前不要再扮演全知全能的父母角色,适当地装傻,不再讲究权威,沟通时候要培养孩子的权威和尊严,否则孩子凭什么买你的帐?许多孩子在叛逆的时期是对父母横挑鼻子竖挑眼的示弱。', timestamp: Date.now() },
        { name: 'xz', msg: '我有点晕了', timestamp: Date.now() },
        { name: 'me', msg: '我是一个伟大的程序员', timestamp: Date.now() },
        { name: 'xz', msg: '哦,你cv一定很熟!', timestamp: Date.now() },
        { name: 'xz', msg: '您好,怎么和青春期叛逆的孩子沟通 呢?', timestamp: Date.now() },
        { name: 'me', msg: '在孩子面前不要再扮演全知全能的父母角色,适当地装傻,不再讲究权威,沟通时候要培养孩子的权威和尊严,否则孩子凭什么买你的帐?许多孩子在叛逆的时期是对父母横挑鼻子竖挑眼的示弱。', timestamp: Date.now() },
        { name: 'xz', msg: '我有点晕了', timestamp: Date.now() },
        { name: 'me', msg: '我是一个伟大的程序员', timestamp: Date.now() },
        { name: 'xz', msg: '哦,你cv一定很熟!', timestamp: Date.now() }
      ],
      // 语音测试
      audioSrc: 'http://192.168.10.44:81/video/2021/04/08/1617848616598815.mp3',
      audioSrc1: 'http://192.168.10.44:81/video/2021/04/08/1617848824184466.wav',
      isRight: false,
      word: '',
      checked: false,
      photoShow: false,
      // 上滑结束取消语音发送的坐标变量
      posStart: 0, // 初始化起点坐标
      posEnd: 0, // 初始化终点坐标
      posMove: 0, // 初始化滑动坐标
      btnElem: null,
      value: '按住 说话',
      // 计时器相关参数
      time: '',
      // 分,秒
      minute: 0,
      second: 0, // 秒
      timeShow: false,
      jianting: 0, // 防止创建多个监听函数
      contentType: 'word',
      maikef: true,
      pageNum: 1,
      pageSize: 13,
      total: 20,
      scollRight: true
    }
  },
  watch: {
    checked: {
      // username 监听输入框输入
      handler (newValue, oldValue) {
        if (this.checked === true) {
          this.saveYyxiaoxi()
        }
      }
    }
  },
  computed: {
    ...mapGetters(['userPhoto']),
    user () {
      return this.$store.state.userInfo
    }
  },
  created () {
    // 设置监听函数
    this.timer = setInterval(() => {
      console.log(Date.now())
    }, 1000)
    // 1. 创建webscoket连接
    // 格式:io(url, 参数)
    // http://47.114.163.79:3003
    // http://ttapi.research.itcast.cn
    // ${this.$route.query.chatRoom}/${this.user.id}/${this.user.name}/${this.user.photo}/
    // this.socketio = io('http://47.114.163.79:3003'
    // // var url = `ws://192.168.100.33:8080/${this.$route.query.chatRoom}/${this.user.id}/${this.user.name}/${this.user.photo}/socket.io/`
    // // this.socketio = io(url
    //   // /${this.user.id}/${this.user.name}/${this.user.photo}/

    // )
    if ('WebSocket' in window) {
      this.initWebSocket()
    } else {
      alert('当前浏览器不支持websocketio连接')
    }
    // this.socketio.on('concat', () => {
    //   console.log('连接成功')
    //   // 小爱同学打招呼
    //   this.list.push({
    //     name: 'xz', msg: '你好,你的小爱同学上线了!', timestamp: Date.now()
    //   })
    //   this.scrollToBottom()
    // })

    // this.socketio.on('message', (obj) => {
    //   console.log('从服务器发回来的数据', obj)
    //   // {msg: "我的长的好看!", timestamp: 1602229081886}
    //   const msg = {
    //     ...obj, name: 'xz'
    //   }
    //   this.list.push(msg)
    //   this.scrollToBottom()
    // })
  },
  // 组件销毁时,关闭与服务器的连接
  destroyed () {
    // 组件销毁时,关闭与服务器的连接
    if (this.socketio) {
      // this.socketio.close()
      // this.socketio.onclose = function () {
      //   console.log('连接已关闭...')
      // }
      this.socketio.close() // 离开路由之后断开websocket连接
    }

    clearInterval(this.timer)
  },
  mounted () {
    this._Chatrecord()
    this.scrollTop()
  },
  methods: {
    scrollTop () {
      this.dom = this.$refs.refList
      this.dom.onscroll = () => {
        console.log(this.list, this.total)
        if (this.list.length >= this.total) {
          return
        }
        if (this.dom.scrollTop < 50) {
          if (this.scollRight) {
            return
          }
          this.scollRight = true
          setTimeout(() => {
            this.pageSize = this.pageSize + 10
            this._Chatrecord()
            this.dom.scrollTop = this.dom.scrollTop + this.$refs.refList.clientHeight
            console.log('111this.dom.scrollTop', this.dom.scrollTop, this.$refs.refList.clientHeight)
          }, 1000)
        }
      }
    },
    async _Chatrecord () { // 查询聊天记录
      const data = {
        receiverId: this.user.id,
        receiverName: this.user.name,
        chatRoom: this.$route.query.chatRoom,
        pageNum: this.pageNum,
        pageSize: this.pageSize
      }
      try {
        const res = await Chatrecord(data)
        console.log(res)
        if (res.data.code === 200) {
          this.total = res.data.data.total
          this.list = res.data.data.rows ? res.data.data.rows.map(item => {
            item.senderAvatar = this.Common.api + item.senderAvatar
            return {
              ...item,
              senderTime: item.createTime
            }
          }).reverse() : [] // 倒序 最新消息在下面
          this.scollRight = false
          if (this.pageSize < 15) {
            console.log('111')
            this.scrollToBottom()
          }
        } else {
          this.$toast.fail(res.data.msg)
        }
      } catch (err) {
        console.log(err)
        this.$toast.fail('网络错误,请稍后重试')
      }
    },
    initWebSocket () {
      let protocol = 'ws'
      if (location.protocol === 'https:') {
        protocol = 'wss'
      }
      // const url = `${protocol}://192.168.100.33:8080/websocket/${this.$route.query.chatRoom}/${this.user.id}/${this.user.name}${this.user.photo}`
      // console.log(`${this.$route.query.chatRoom}/${this.user.id}/${this.user.name}${this.user.photo}`)
      console.log(encodeURI(this.user.name))
      // var url = 'ws://192.168.100.33:8080' ?senderAvatar=${this.Common.api + this.user.photo}
      var url = protocol + '://192.168.100.33:8080' + `/websocket/${this.$route.query.chatRoom}/${this.user.id}/${encodeURI(this.user.name)}`
      // decodeURI
      this.socketio = new WebSocket(url)
      // 建立连接
      this.socketio.onopen = function () {
        // this.socketio.send('已经上线了')
        console.log('已经连通了websocket')
      }
      // 接收消息
      // 接收后台服务端的消息    接收到消息
      this.socketio.onmessage = (evt) => {
        console.log('数据已接收:', evt)
        const obj = JSON.parse(evt.data)
        console.log('obj', obj)
        this.list.push(obj)
        this.scrollToBottom()
      }
      // 连接建立失败重连
      this.socketio.onerror = this.websocketonerror
      // 关闭
      this.socketio.onclose = this.websocketclose
    },
    websocketonerror () { // 连接建立失败重连
      console.log('websocket连接断开')
      this.initWebSocket()
    },
    websocketclose (e) { // 关闭
      console.log('断开连接', e)
    },
    uploadPhone () {
      this.$refs.refFile.click()
    },
    async hChangeImage () {
      // 获取用户选中的文件
      console.dir(this.$refs.refFile)
      // this.$refs.refFile 获取对input type="file" 的引用
      // 用户选中文件之后,它会自动放在 files 集合中
      // files[0] : 是用户选中的第一个文件
      const file = this.$refs.refFile.files[0]
      // console.log('file')
      // console.dir(file)
      if (!file) {
        return
      }
      try {
        // 上传文件
        const fd = new FormData()
        // fd.append('avatarfile', file) // photo是接口需要的参数名,file是文件
        fd.append('file', file) // photo是接口需要的参数名,file是文件
        fd.append('type', 1)

        const result = await setAvater(fd)
        console.log(result)
        // 调用接口,上传这个文件
        // this.$store.commit('mUpdatePhoto', result.data.url)
        // this.$toast.success('操作成功')
        // this.list.push({ name: 'me', msg: `${this.Common.api}${result.data.url}`, timestamp: Date.now() })
        if (result.data.code === 200) {
          this.photoShow = false
          this.sendPhoto(result.data.data.url)
        } else {
          this.$toast.fail(result.data.msg)
        }
        // this.list.push({ name: 'me', msg: `${result.data.data.url}`, timestamp: Date.now() })
        // this.scrollToBottom()
      } catch (err) {
        console.log(err)
        this.$toast.fail('操作失败')
      }
    },
    _timeChange (val) {
      return timeChange(val)
    },
    saveYyxiaoxi () {
      // 获取语音发送变量
      console.log('ahsadgh1')
      if (this.jianting === 0) {
        this.btnElem = document.getElementById('messageBtn')
        this.initEvent()
      }
      this.jianting = 1
    },
    // 点击发送语音 上滑取消语音逻辑
    initEvent () {
      this.btnElem.addEventListener('touchstart', (event) => {
        // event.preventDefault()// 阻止浏览器默认行为
        this.posStart = 0
        this.posStart = event.touches[0].pageY// 获取起点坐标
        this.value = '松开 发送'
        this.timeShow = true
        clearInterval(this.time)
        this.time = setInterval(this.timer1, 1000)
        this.handleBtnClick()
        console.log('start')
        console.log(this.posStart + '---------开始坐标')
      })
      this.btnElem.addEventListener('touchmove', (event) => {
        event.preventDefault()// 阻止浏览器默认行为
        this.posMove = 0
        this.posMove = event.targetTouches[0].pageY// 获取滑动实时坐标
        if (this.posStart - this.posMove < 30) {
          this.value = '松开 发送'
        } else {
          this.value = '松开手指,取消发送'
        }
      })
      this.btnElem.addEventListener('touchend', (event) => {
        event.preventDefault()
        this.posEnd = 0
        this.posEnd = event.changedTouches[0].pageY// 获取终点坐标
        this.value = '按住 说话'
        console.log('End')
        console.log(this.posEnd + '---------结束坐标')
        if (this.posStart - this.posEnd < 30) {
          console.log('发送成功')
          this.save()
        } else {
          console.log('取消发送')
          console.log('Cancel')
        };
        this.cancel()
        this.timeShow = false
        this.reset()
      })
    },
    // 录制语音
    handleBtnClick: function () {
      // const that = this
      // that.news_img = !that.news_img
      rc.start()
        .then(() => {
          this.maikef = true
          // that.news_img = !that.news_img
          console.log('start recording')
        })
        .catch(error => {
          this.$toast.fail('获取麦克风失败')
          this.maikef = false
          this.reset()
          this.timeShow = false
          console.log('Recording failed.', error)
        })
    },
    // 取消语音
    cancel: function () {
      rc.clear()
      // rc.close()
    },
    // 暂停语音
    cancel_mp3: function () {
      rc.pause()
    },
    // 发送语音
    async send_voice () {
      // if (!this.maikef) { // 是否获取麦克风
      //   return
      // }
      rc.pause()
      const wav = rc.getRecord({
        encodeTo: ENCODE_TYPE.WAV,
        compressible: true
      })
      console.log('wav', wav)
      try {
        const formData = new FormData()
        // formData.append('file',wav);
        formData.append('type', 2)
        formData.append('file', wav, Date.parse(new Date()) + '.wav')
        // formData.append('file', wav, Date.parse(new Date()) + '.mp3')
        // const headers = { headers: { 'Content-Type': 'multipart/form-data' } }
        const res = await setAudio(formData)
        console.log(res)
        if (res.data.code === 200) {
          this.sendtheVoice(res.data.data.url)
        } else {
          this.$toast.fail(res.data.msg)
        }
      } catch (err) {
        console.log(err)
        this.$toast.fail('网络错误请稍后重试')
      }
      // this.cancel()
      // this.timeShow = false
      // this.reset()
      // axios.post(this.https + '/admin/api/send_reply', formData, headers).then(data => {
      //   that.news_img = !that.news_img
      //   // this.reload();
      //   rc.clear()
      // })
      //   .catch(err => {
      //     console.log(err)
      //   })
    },
    save () { // 发送语音消息
      console.log('开始发送吧')
      this.send_voice()
    },
    // 发送语音时计时器函数
    timer1 () { // 定义计时函数
      console.log(this.second)
      this.second = this.second + 1 // 秒
      if (this.second >= 60) {
        this.second = 0
        this.minute = this.minute + 1 // 分钟
      }

      if (this.minute >= 60) {
        this.minute = 0
        this.hour = this.hour + 1 // 小时
      }
    },
    reset () { // 重置
      clearInterval(this.time)
      this.time = null
      this.hour = 0
      this.minute = 0
      this.second = 0
    },
    // 滚动事件
    scrollToBottom () {
      this.$nextTick(() => {
        const dom = this.$refs.refList
        // scrollTop 是dom元素的属性,可以去手动设置
        //   它表示当前dom元素中的滚动条距离元素顶部的距离
        dom.scrollTop = dom.scrollHeight
      })
    },
    send_hc () { // 监听回车事件
      document.onkeydown = (e) => {
        const _key = window.event.keyCode
        console.log(_key)
        //! this.clickState是防止用户重复点击回车
        if (_key === 13) {
          this.send()
        }
      }
    },
    send () {
      if (this.word === '') {
        return
      }
      // 1. 把我要说的话发给服务器接口
      console.log(this.word)
      // this.socketio.emit(消息名称,内容)
      // this.socketio.send('message', {
      //   msg: this.word,
      //   timestamp: Date.now()
      // })
      this.contentType = 'word'
      const data = {
        chatType: 'personal_chat',
        receiverId: this.$route.query.id,
        receiverName: this.$route.query.friendName,
        senderAvatar: this.Common.api + this.user.photo,
        content: this.word,
        contentType: this.contentType
      }
      this.socketio.send(JSON.stringify(data))
      // 2.在本地添加消息
      // this.list.push({ name: 'me', msg: this.word, timestamp: Date.now() })
      // this.scrollToBottom()
      // 3. 清空
      this.word = ''
    },
    sendPhoto (url) {
      this.contentType = 'image'
      const data = {
        chatType: 'personal_chat',
        receiverId: this.$route.query.id,
        receiverName: this.$route.query.friendName,
        senderAvatar: this.Common.api + this.user.photo,
        content: url,
        contentType: this.contentType
      }
      this.socketio.send(JSON.stringify(data))
      // 2.在本地添加消息
      // this.list.push({ name: 'me', msg: this.word, timestamp: Date.now() })
      // this.scrollToBottom()
      // 3. 清空
      // this.word = ''
    },
    sendtheVoice (url) {
      this.contentType = 'voice'
      const data = {
        chatType: 'personal_chat',
        receiverId: this.$route.query.id,
        receiverName: this.$route.query.friendName,
        senderAvatar: this.Common.api + this.user.photo,
        content: url,
        contentType: this.contentType
      }
      this.socketio.send(JSON.stringify(data))
    }
  }
}
</script>

style

<style lang="less" scoped>
.container {
  height: 100%;
  width: 100%;
  position: absolute;
  left: 0;
  top: 0;
  box-sizing: border-box;
  background:#fafafa;
  padding: 46px 0 50px 0;
  .chat-list {
    height: 100%;
    overflow-y: scroll;
    .chat-item{
      padding: 10px;
      .van-image{
        vertical-align: top;
        width: 40px;
        height: 40px;
      }
      .chat-pao{
        vertical-align: top;
        display: inline-block;
        min-width: 40px;
        max-width: 70%;
        min-height: 40px;
        line-height: 38px;
        border: 0.5px solid #c2d9ea;
        border-radius: 4px;
        position: relative;
        padding: 0 10px;
        background-color: #e0effb;
        word-break: break-all;
        font-size: 14px;
        color: #333;
        /deep/.van-image{
          width: 100%;height: 100%;
        }
        &::before{
          content: "";
          width: 10px;
          height: 10px;
          position: absolute;
          top: 12px;
          border-top:0.5px solid #c2d9ea;
          border-right:0.5px solid #c2d9ea;
          background: #e0effb;
        }
      }
    }
  }
}
.chat-item.right{
  text-align: right;
  .chat-pao{
    margin-left: 0;
    margin-right: 15px;
    &::before{
      right: -6px;
      transform: rotate(45deg);
    }
    span{
      right: 0;
      left: auto;
    }
  }
}
.chat-item.left{
  text-align: left;
  .chat-pao{
    margin-left: 15px;
    margin-right: 0;
    &::before{
      left: -5px;
      transform: rotate(-135deg);
    }
  }
}
.chat-pao{
  span{
    position: absolute;
    width: fit-content; // 内容宽度
    // min-width: 2.2rem;
    // text-align: center;
    height: 0.4rem;
    line-height: 0.4rem;
    bottom: -18px;
    left: 0;
    background: #CECECE;
    color: #fff;
    font-size: 0.22rem;
    font-family: PingFang SC;
    font-weight: 400;
  }
}
.reply-container {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  background: #fff;
  z-index: 9999;
  div{
    height: 44px;
  }
  .row{
    box-sizing: border-box;
    display: flex;
    align-items: center;
    input{
      height: 0.6rem;
    }
  }
  .photoShow{
    height: 44px;
    display: flex;
    justify-content: space-around;
    align-items: center;
    font-size: 0.24rem;
    font-family: PingFang SC;
    font-weight: 400;
    color: #1B1E27;
    i{
      vertical-align: middle;
      font-size: 0.4rem;
    }
  }
  .row{
    i{
      display: inline-block;
      width: 10%;
      text-align: center;
    }
  }
  .btn{
    border: 0;
    width: 80%;
    background: #3964E6;
    border-radius: 0.16rem;
    font-size: 0.24rem;
    font-family: PingFang SC;
    font-weight: 400;
    color: #FFFFFF;
  }
  /deep/.van-cell{
    display: inline-block;
    box-sizing: border-box;
    vertical-align: middle;
    width: 80%;
  }
}
// 时间弹层
.van-popup{
  width: 45%;
  height: 3rem;
  background: rgba(0, 0, 0, .7);
  border-radius: 0.2rem;
  color: #fff;
  text-align: center;
  div{
    margin: 0.4rem;
    font-size: 0.3rem;
    font-family: PingFang SC;
    font-weight: 400;
    &:nth-child(1){
      font-size: 0.6rem;
    }
  }
}
</style>

语音组件audio

<template>
  <div class="audio__wrap">
    <audio controls :src="src" ref="audioPlayer" style="display:none"></audio>
    <div class="self__audio" @click="playAudioHandler">
      <div class="audio__duration">{{duration}}"</div>
      <div class="audio__trigger">
        <div
          :class="{
            'wifi-symbol':true,
            'wifi-symbol--avtive':isPlaying
        }"
        >
          <div class="wifi-circle second"></div>
          <div class="wifi-circle third"></div>
          <div class="wifi-circle first"></div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      isPlaying: false,
      duration: ''
    }
  },
  props: {
    src: {
      type: String,
      required: true
    },
    value: {
      type: Boolean,
      required: true
    }
  },
  watch: {
    value: {
      // username 监听输入框输入
      handler (newValue, oldValue) {
        if (!this.value) {
          this.isPlaying = false
          this.$refs.audioPlayer.load()
        }
      }
    }
  },
  methods: {
    playAudioHandler () {
      this.isPlaying = !this.isPlaying
      const player = this.$refs.audioPlayer
      if (this.isPlaying) {
        player.load()
        player.play()
      } else {
        player.pause()
      }
      setTimeout(() => {
        this.isPlaying = false
        this.$emit('input', false)
      }, (this.duration ? this.duration : 0) * 1000)
    }
  },
  mounted () {
    const player = this.$refs.audioPlayer
    player.load()
    const vm = this
    player.oncanplay = function () {
      vm.duration = Math.ceil(player.duration)
    }
  }
}
</script>
<style lang="less" scoped>
.audio__wrap {
  .self__audio {
    .audio__duration {
      display: inline-block;
      line-height: 32px;
      height: 32px;
      padding-right: 6px;
      color: #888888;
    }
    .audio__trigger {
      cursor: pointer;
      vertical-align: top;
      display: inline-block;
      line-height: 32px;
      height: 32px;
      width: 100px;
      background-color: #e0effb;
      border-radius: 4px;
      position: relative;
      .wifi-symbol {
        position: absolute;
        right: 4px;
        top: -8px;
        width: 50px;
        height: 50px;
        box-sizing: border-box;
        overflow: hidden;
        transform: rotate(-45deg) scale(0.5);
        .wifi-circle {
          border: 5px solid #999999;
          border-radius: 50%;
          position: absolute;
        }

        .first {
          width: 5px;
          height: 5px;
          background: #cccccc;
          top: 45px;
          left: 45px;
        }
        .second {
          width: 25px;
          height: 25px;
          top: 35px;
          left: 35px;
        }
        .third {
          width: 40px;
          height: 40px;
          top: 25px;
          left: 25px;
        }
      }
      .wifi-symbol--avtive {
        .second {
          animation: bounce 1s infinite 0.2s;
        }
        .third {
          animation: bounce 1s infinite 0.4s;
        }
      }
    }
    @keyframes bounce {
      0% {
        opacity: 0; /*初始状态 透明度为0*/
      }
      100% {
        opacity: 1; /*结尾状态 透明度为1*/
      }
    }
  }
}
</style>

语音组件audio1,两个语音组件区别在于样式,一个是左边播放,一个是右边播放,目前没有做整合

<template>
  <div class="audio__wrap">
    <audio controls :src="src" ref="audioPlayer" style="display:none"></audio>
    <div class="self__audio" @click="playAudioHandler">
      <div class="audio__trigger">
        <div
          :class="{
            'wifi-symbol':true,
            'wifi-symbol--avtive':isPlaying
        }"
        >
          <div class="wifi-circle third"></div>
          <div class="wifi-circle second"></div>
          <div class="wifi-circle first"></div>
        </div>
      </div>
       <div class="audio__duration">{{duration}}"</div>
    </div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      isPlaying: false,
      duration: ''
    }
  },
  props: {
    src: {
      type: String,
      required: true
    },
    value: {
      type: Boolean,
      required: true
    }
  },
  watch: {
    value: {
      // username 监听输入框输入
      handler (newValue, oldValue) {
        if (!this.value) {
          this.isPlaying = false
          this.$refs.audioPlayer.load()
        }
      }
      // },
      // immediate: true
    }
  },
  methods: {
    playAudioHandler () {
      this.isPlaying = !this.isPlaying
      const player = this.$refs.audioPlayer
      if (this.isPlaying) {
        player.load()
        player.play()
      } else {
        player.pause()
      }
      setTimeout(() => {
        this.isPlaying = false
        this.$emit('input', false)
      }, (this.duration ? this.duration : 0) * 1000)
    }
  },
  mounted () {
    const player = this.$refs.audioPlayer
    player.load()
    const vm = this
    player.oncanplay = function () {
      vm.duration = Math.ceil(player.duration)
    }
  }
}
</script>
<style lang="less" scoped>
.audio__wrap {
  .self__audio {
    .audio__duration {
      display: inline-block;
      line-height: 32px;
      height: 32px;
      padding-right: 6px;
      color: #888888;
    }
    .audio__trigger {
      cursor: pointer;
      vertical-align: top;
      display: inline-block;
      line-height: 32px;
      height: 32px;
      width: 100px;
      background-color: #e0effb;
      border-radius: 4px;
      position: relative;
      .wifi-symbol {
        position: absolute;
        left: 4px;
        top: -8px;
        width: 50px;
        height: 50px;
        box-sizing: border-box;
        overflow: hidden;
        transform: rotate(-225deg) scale(0.5);
        .wifi-circle {
          border: 5px solid #999999;
          border-radius: 50%;
          position: absolute;
        }

        .first {
          width: 5px;
          height: 5px;
          background: #cccccc;
          top: 45px;
          left: 45px;
        }
        .second {
          width: 25px;
          height: 25px;
          top: 35px;
          left: 35px;
        }
        .third {
          width: 40px;
          height: 40px;
          top: 25px;
          left: 25px;
        }
      }
      .wifi-symbol--avtive {
        .second {
          animation: bounce 1s infinite 0.2s;
        }
        .third {
          animation: bounce 1s infinite 0.4s;
        }
      }
    }
    @keyframes bounce {
      0% {
        opacity: 0; /*初始状态 透明度为0*/
      }
      100% {
        opacity: 1; /*结尾状态 透明度为1*/
      }
    }
  }
}
</style>

  • 12
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 21
    评论
聊天室是一个实时通信的应用程序,而Vue是一个以数据驱动的JavaScript框架。WebSocket是一种实现服务器和客户端之间双向通信的网络协议。下面是使用Vue下载WebSocket的简单步骤。 首先,你需要在Vue项目中安装WebSocket库。在终端中使用以下命令: ``` npm install vue-native-websocket ``` 然后,你需要在Vue项目的主文件(通常是main.js)中引入WebSocket库并进行配置。在这个文件中,添加以下代码: ```javascript import VueNativeSock from 'vue-native-websocket' Vue.use(VueNativeSock, 'wss://your-websocket-url.com', { reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 3000 }) ``` 在这段代码中,将'wss://your-websocket-url.com'替换为你实际的WebSocket服务器URL。reconnection相关的配置是可选的,用于处理与服务器的断开连接和重新连接。 配置完成后,你可以在Vue组件中使用WebSocket。例如,你可以在聊天室组件中使用WebSocket来建立与服务器的连接并实时接收和发送聊天消息。你可以使用Vue的生命周期钩子方法来处理WebSocket的连接和消息处理。以下是一个简单的示例: ```javascript export default { mounted() { this.$socket.onmessage = (event) => { // 收到聊天消息后的处理逻辑 } }, methods: { sendMessage(message) { this.$socket.send(message) } } } ``` 在这个示例中,`mounted()`生命周期钩子方法用于建立与WebSocket服务器的连接,并使用`onmessage`属性监听来自服务器的消息。`sendMessage()`方法用于向服务器发送聊天消息。 这样,你就可以使用VueWebSocket来实现一个简单的聊天室应用程序了。记得根据你的具体需求进行额外的配置和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值