最近在做一个h5端的聊天app,在聊天框这一块有很多细节,特此进行记录一下
涉及的知识点有:
vue语音的录制插件使用,
websocket连接通信,
语音播放动画样式组件封装,
语音发送取消发送逻辑,
发送消息滚动条置底,
向上滚屏逻辑,
时间显示逻辑
安装
npm install recorderx -S
使用
在聊天组件引入
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('网络错误请稍后重试')
}
}
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)
},
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、点击播放时动画样式
```
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>
```
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)
}
}
}
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 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="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="说点什么...">
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
语音组件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>