committed by
Gitee
26 changed files with 792 additions and 755 deletions
@ -1,6 +1,6 @@ |
|||||
ENV = 'production' |
ENV = 'production' |
||||
|
|
||||
# 接口地址 |
# 接口地址 |
||||
VUE_APP_BASE_API = 'http://8.134.92.70/api' |
VUE_APP_BASE_API = 'https://8.134.92.70:443/api' |
||||
|
|
||||
VUE_APP_WS_URL = 'ws://8.134.92.70:81/im' |
VUE_APP_WS_URL = 'wss://8.134.92.70:81/im' |
||||
@ -0,0 +1,494 @@ |
|||||
|
<template> |
||||
|
<el-container class="chat-box"> |
||||
|
<el-header height="60px"> |
||||
|
<span>{{title}}</span> |
||||
|
<span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more" |
||||
|
@click="showSide=!showSide"></span> |
||||
|
</el-header> |
||||
|
<el-main style="padding: 0;"> |
||||
|
<el-container> |
||||
|
<el-container class="content-box"> |
||||
|
<el-main class="im-chat-main" id="chatScrollBox"> |
||||
|
<div class="im-chat-box"> |
||||
|
<ul> |
||||
|
<li v-for="(msgInfo,idx) in chat.messages" :key="idx"> |
||||
|
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" |
||||
|
:showName="showName(msgInfo)" :msgInfo="msgInfo"> |
||||
|
</message-item> |
||||
|
</li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</el-main> |
||||
|
<el-footer height="200px" class="im-chat-footer"> |
||||
|
<div class="chat-tool-bar"> |
||||
|
<div title="表情" class="el-icon-eleme" ref="emotion" @click="switchEmotionBox()"> |
||||
|
</div> |
||||
|
<div title="发送图片"> |
||||
|
<file-upload :action="imageAction" :maxSize="5*1024*1024" |
||||
|
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']" |
||||
|
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail"> |
||||
|
<i class="el-icon-picture-outline"></i> |
||||
|
</file-upload> |
||||
|
</div> |
||||
|
<div title="发送文件"> |
||||
|
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" |
||||
|
@success="handleFileSuccess" @fail="handleFileFail"> |
||||
|
<i class="el-icon-wallet"></i> |
||||
|
</file-upload> |
||||
|
</div> |
||||
|
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> |
||||
|
</div> |
||||
|
<div title="聊天记录" class="el-icon-chat-dot-round"></div> |
||||
|
</div> |
||||
|
<textarea v-model="sendText" ref="sendBox" class="send-text-area" |
||||
|
@keydown.enter="sendTextMessage()"></textarea> |
||||
|
<div class="im-chat-send"> |
||||
|
<el-button type="primary" @click="sendTextMessage()">发送</el-button> |
||||
|
</div> |
||||
|
</el-footer> |
||||
|
</el-container> |
||||
|
<el-aside class="chat-group-side-box" width="300px" v-show="showSide"> |
||||
|
<chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)"> |
||||
|
</chat-group-side> |
||||
|
</el-aside> |
||||
|
</el-container> |
||||
|
</el-main> |
||||
|
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> |
||||
|
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice> |
||||
|
</el-container> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import ChatGroupSide from "./ChatGroupSide.vue"; |
||||
|
import MessageItem from "./MessageItem.vue"; |
||||
|
import FileUpload from "../common/FileUpload.vue"; |
||||
|
import Emotion from "../common/Emotion.vue"; |
||||
|
import ChatVoice from "./ChatVoice.vue"; |
||||
|
|
||||
|
export default { |
||||
|
name: "chatPrivate", |
||||
|
components: { |
||||
|
MessageItem, |
||||
|
FileUpload, |
||||
|
ChatGroupSide, |
||||
|
Emotion, |
||||
|
ChatVoice |
||||
|
}, |
||||
|
props: { |
||||
|
chat: { |
||||
|
type: Object |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
friend: {}, |
||||
|
group: {}, |
||||
|
groupMembers: [], |
||||
|
sendText: "", |
||||
|
showVoice: false, // 是否显示语音录制弹窗 |
||||
|
showSide: false, // 是否显示群聊信息栏 |
||||
|
showEmotion: false, // 是否显示emoji表情 |
||||
|
emoBoxPos: { // emoji表情弹出位置 |
||||
|
x: 0, |
||||
|
y: 0 |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
|
||||
|
handleImageSuccess(res, file) { |
||||
|
let msgInfo = { |
||||
|
recvId: file.raw.targetId, |
||||
|
content: JSON.stringify(res.data), |
||||
|
type: 1 |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
this.$http({ |
||||
|
url: this.messageAction, |
||||
|
method: 'post', |
||||
|
data: msgInfo |
||||
|
}).then((data) => { |
||||
|
let info = { |
||||
|
type: this.chat.type, |
||||
|
targetId: file.raw.targetId, |
||||
|
fileId: file.raw.uid, |
||||
|
content: JSON.stringify(res.data), |
||||
|
loadStatus: "ok" |
||||
|
} |
||||
|
this.$store.commit("handleFileUpload", info); |
||||
|
}) |
||||
|
}, |
||||
|
handleImageFail(res, file) { |
||||
|
let info = { |
||||
|
type: this.chat.type, |
||||
|
targetId: file.raw.targetId, |
||||
|
fileId: file.raw.uid, |
||||
|
loadStatus: "fail" |
||||
|
} |
||||
|
this.$store.commit("handleFileUpload", info); |
||||
|
}, |
||||
|
handleImageBefore(file) { |
||||
|
let url = URL.createObjectURL(file); |
||||
|
let data = { |
||||
|
originUrl: url, |
||||
|
thumbUrl: url |
||||
|
} |
||||
|
let msgInfo = { |
||||
|
fileId: file.uid, |
||||
|
sendId: this.mine.id, |
||||
|
content: JSON.stringify(data), |
||||
|
sendTime: new Date().getTime(), |
||||
|
selfSend: true, |
||||
|
type: 1, |
||||
|
loadStatus: "loading" |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
// 插入消息 |
||||
|
this.$store.commit("insertMessage", msgInfo); |
||||
|
// 滚动到底部 |
||||
|
this.scrollToBottom(); |
||||
|
// 借助file对象保存对方id |
||||
|
file.targetId = this.chat.targetId; |
||||
|
}, |
||||
|
handleFileSuccess(res, file) { |
||||
|
let data = { |
||||
|
name: file.name, |
||||
|
size: file.size, |
||||
|
url: res.data |
||||
|
} |
||||
|
let msgInfo = { |
||||
|
content: JSON.stringify(data), |
||||
|
type: 2 |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
this.$http({ |
||||
|
url: this.messageAction, |
||||
|
method: 'post', |
||||
|
data: msgInfo |
||||
|
}).then(() => { |
||||
|
let info = { |
||||
|
type: this.chat.type, |
||||
|
targetId: file.raw.targetId, |
||||
|
fileId: file.raw.uid, |
||||
|
content: JSON.stringify(data), |
||||
|
loadStatus: "ok" |
||||
|
} |
||||
|
this.$store.commit("handleFileUpload", info); |
||||
|
}) |
||||
|
}, |
||||
|
handleFileFail(res, file) { |
||||
|
let info = { |
||||
|
type: this.chat.type, |
||||
|
targetId: file.raw.targetId, |
||||
|
fileId: file.raw.uid, |
||||
|
loadStatus: "fail" |
||||
|
} |
||||
|
this.$store.commit("handleFileUpload", info); |
||||
|
}, |
||||
|
handleFileBefore(file) { |
||||
|
let url = URL.createObjectURL(file); |
||||
|
let data = { |
||||
|
name: file.name, |
||||
|
size: file.size, |
||||
|
url: url |
||||
|
} |
||||
|
let msgInfo = { |
||||
|
fileId: file.uid, |
||||
|
sendId: this.mine.id, |
||||
|
content: JSON.stringify(data), |
||||
|
sendTime: new Date().getTime(), |
||||
|
selfSend: true, |
||||
|
type: 2, |
||||
|
loadStatus: "loading" |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
// 插入消息 |
||||
|
this.$store.commit("insertMessage", msgInfo); |
||||
|
// 滚动到底部 |
||||
|
this.scrollToBottom(); |
||||
|
// 借助file对象保存对方id |
||||
|
file.targetId = this.chat.targetId; |
||||
|
}, |
||||
|
handleCloseSide() { |
||||
|
this.showSide = false; |
||||
|
}, |
||||
|
switchEmotionBox() { |
||||
|
this.showEmotion = !this.showEmotion; |
||||
|
let width = this.$refs.emotion.offsetWidth; |
||||
|
let left = this.$elm.fixLeft(this.$refs.emotion); |
||||
|
let top = this.$elm.fixTop(this.$refs.emotion); |
||||
|
this.emoBoxPos.y = top; |
||||
|
this.emoBoxPos.x = left + width / 2; |
||||
|
}, |
||||
|
handleEmotion(emoText) { |
||||
|
this.sendText += emoText; |
||||
|
this.showEmotion = false; |
||||
|
// 保持输入框焦点 |
||||
|
this.$refs.sendBox.focus(); |
||||
|
}, |
||||
|
showVoiceBox() { |
||||
|
this.showVoice = true; |
||||
|
|
||||
|
}, |
||||
|
closeVoiceBox() { |
||||
|
this.showVoice = false; |
||||
|
}, |
||||
|
handleSendVoice(data) { |
||||
|
let msgInfo = { |
||||
|
content: JSON.stringify(data), |
||||
|
type: 3 |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
this.$http({ |
||||
|
url: this.messageAction, |
||||
|
method: 'post', |
||||
|
data: msgInfo |
||||
|
}).then(() => { |
||||
|
this.$message.success("发送成功"); |
||||
|
msgInfo.sendTime = new Date().getTime(); |
||||
|
msgInfo.sendId = this.$store.state.userStore.userInfo.id; |
||||
|
msgInfo.selfSend = true; |
||||
|
this.$store.commit("insertMessage", msgInfo); |
||||
|
// 保持输入框焦点 |
||||
|
this.$refs.sendBox.focus(); |
||||
|
// 滚动到底部 |
||||
|
this.scrollToBottom(); |
||||
|
// 关闭录音窗口 |
||||
|
this.showVoice = false; |
||||
|
}) |
||||
|
}, |
||||
|
setTargetId(msgInfo, targetId) { |
||||
|
if (this.chat.type == "GROUP") { |
||||
|
msgInfo.groupId = targetId; |
||||
|
} else { |
||||
|
msgInfo.recvId = targetId; |
||||
|
} |
||||
|
}, |
||||
|
sendTextMessage() { |
||||
|
|
||||
|
if (!this.sendText.trim()) { |
||||
|
this.$message.error("不能发送空白信息"); |
||||
|
return |
||||
|
} |
||||
|
let msgInfo = { |
||||
|
content: this.sendText, |
||||
|
type: 0 |
||||
|
} |
||||
|
// 填充对方id |
||||
|
this.setTargetId(msgInfo, this.chat.targetId); |
||||
|
this.$http({ |
||||
|
url: this.messageAction, |
||||
|
method: 'post', |
||||
|
data: msgInfo |
||||
|
}).then((data) => { |
||||
|
this.$message.success("发送成功"); |
||||
|
this.sendText = ""; |
||||
|
msgInfo.sendTime = new Date().getTime(); |
||||
|
msgInfo.sendId = this.$store.state.userStore.userInfo.id; |
||||
|
msgInfo.selfSend = true; |
||||
|
this.$store.commit("insertMessage", msgInfo); |
||||
|
// 保持输入框焦点 |
||||
|
this.$refs.sendBox.focus(); |
||||
|
// 滚动到底部 |
||||
|
this.scrollToBottom(); |
||||
|
}) |
||||
|
const e = window.event || arguments[0]; |
||||
|
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) { |
||||
|
e.returnValue = false; |
||||
|
e.preventDefault(); |
||||
|
return false; |
||||
|
} |
||||
|
}, |
||||
|
loadGroup(groupId) { |
||||
|
this.$http({ |
||||
|
url: `/group/find/${groupId}`, |
||||
|
method: 'get' |
||||
|
}).then((group) => { |
||||
|
this.group = group; |
||||
|
this.$store.commit("updateChatFromGroup", group); |
||||
|
this.$store.commit("updateGroup", group); |
||||
|
|
||||
|
}); |
||||
|
|
||||
|
this.$http({ |
||||
|
url: `/group/members/${groupId}`, |
||||
|
method: 'get' |
||||
|
}).then((groupMembers) => { |
||||
|
this.groupMembers = groupMembers; |
||||
|
}); |
||||
|
}, |
||||
|
loadFriend(friendId) { |
||||
|
// 获取对方最新信息 |
||||
|
this.$http({ |
||||
|
url: `/user/find/${friendId}`, |
||||
|
method: 'get' |
||||
|
}).then((friend) => { |
||||
|
this.friend = friend; |
||||
|
this.$store.commit("updateChatFromFriend", friend); |
||||
|
this.$store.commit("updateFriend", friend); |
||||
|
}) |
||||
|
}, |
||||
|
showName(msgInfo) { |
||||
|
if (this.chat.type == 'GROUP') { |
||||
|
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
||||
|
return member ? member.aliasName : ""; |
||||
|
} else { |
||||
|
return msgInfo.sendId == this.mine.id ? this.mine.nickName : this.chat.showName |
||||
|
} |
||||
|
|
||||
|
}, |
||||
|
headImage(msgInfo) { |
||||
|
if (this.chat.type == 'GROUP') { |
||||
|
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
||||
|
return member ? member.headImage : ""; |
||||
|
} else { |
||||
|
return msgInfo.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage |
||||
|
} |
||||
|
}, |
||||
|
scrollToBottom() { |
||||
|
this.$nextTick(() => { |
||||
|
const div = document.getElementById("chatScrollBox"); |
||||
|
div.scrollTop = div.scrollHeight; |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
computed: { |
||||
|
mine() { |
||||
|
return this.$store.state.userStore.userInfo; |
||||
|
}, |
||||
|
title() { |
||||
|
let title = this.chat.showName; |
||||
|
if (this.chat.type == "GROUP") { |
||||
|
let size = this.groupMembers.filter(m => !m.quit).length; |
||||
|
title += `(${size})`; |
||||
|
} |
||||
|
return title; |
||||
|
}, |
||||
|
imageAction() { |
||||
|
return `${process.env.VUE_APP_BASE_API}/image/upload`; |
||||
|
}, |
||||
|
fileAction() { |
||||
|
return `${process.env.VUE_APP_BASE_API}/file/upload`; |
||||
|
}, |
||||
|
messageAction() { |
||||
|
return `/message/${this.chat.type.toLowerCase()}/send`; |
||||
|
} |
||||
|
}, |
||||
|
watch: { |
||||
|
chat: { |
||||
|
handler(newChat, oldChat) { |
||||
|
if (newChat.targetId > 0 && (newChat.type != oldChat.type || newChat.targetId != oldChat.targetId)) { |
||||
|
if (this.chat.type == "GROUP") { |
||||
|
this.loadGroup(this.chat.targetId); |
||||
|
} else { |
||||
|
this.loadFriend(this.chat.targetId); |
||||
|
} |
||||
|
this.scrollToBottom(); |
||||
|
this.sendText = ""; |
||||
|
// 保持输入框焦点 |
||||
|
this.$refs.sendBox.focus(); |
||||
|
} |
||||
|
}, |
||||
|
deep: true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.chat-box { |
||||
|
background: white; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.el-header { |
||||
|
padding: 5px; |
||||
|
background-color: white; |
||||
|
line-height: 50px; |
||||
|
font-size: 20px; |
||||
|
font-weight: 600; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.btn-side { |
||||
|
position: absolute; |
||||
|
right: 20px; |
||||
|
line-height: 60px; |
||||
|
font-size: 22px; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
font-size: 30px; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.im-chat-main { |
||||
|
padding: 0; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.im-chat-box { |
||||
|
ul { |
||||
|
padding: 20px; |
||||
|
|
||||
|
li { |
||||
|
list-style-type: none; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.im-chat-footer { |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
padding: 0; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
.chat-tool-bar { |
||||
|
|
||||
|
display: flex; |
||||
|
position: relative; |
||||
|
width: 100%; |
||||
|
height: 40px; |
||||
|
text-align: left; |
||||
|
box-sizing: border-box; |
||||
|
border: #dddddd solid 1px; |
||||
|
|
||||
|
>div { |
||||
|
margin-left: 10px; |
||||
|
font-size: 22px; |
||||
|
cursor: pointer; |
||||
|
color: #333333; |
||||
|
line-height: 40px; |
||||
|
|
||||
|
&:hover { |
||||
|
color: black; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.send-text-area { |
||||
|
box-sizing: border-box; |
||||
|
padding: 5px; |
||||
|
width: 100%; |
||||
|
flex: 1; |
||||
|
resize: none; |
||||
|
background-color: #f8f8f8 !important; |
||||
|
outline-color: rgba(83, 160, 231, 0.61); |
||||
|
} |
||||
|
|
||||
|
.im-chat-send { |
||||
|
text-align: right; |
||||
|
padding: 7px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.chat-group-side-box { |
||||
|
border: #dddddd solid 1px; |
||||
|
animation: rtl-drawer-in .3s 1ms; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
@ -1,319 +0,0 @@ |
|||||
<template> |
|
||||
<el-container class="r-chat-box"> |
|
||||
<el-header height="60px"> |
|
||||
<span>{{title}}</span> |
|
||||
<span title="群聊信息" class="btn-side el-icon-more" @click="showSide=!showSide"></span> |
|
||||
</el-header> |
|
||||
<el-container> |
|
||||
<el-container class="content-box"> |
|
||||
<el-main class="im-chat-main" id="chatScrollBox"> |
|
||||
<div class="im-chat-box"> |
|
||||
<ul> |
|
||||
<li v-for="msgInfo in chat.messages" :key="msgInfo.id"> |
|
||||
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" |
|
||||
:msgInfo="msgInfo"> |
|
||||
</message-item> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
</el-main> |
|
||||
<el-footer height="25%" class="im-chat-footer"> |
|
||||
<div class="chat-tool-bar"> |
|
||||
<div title="表情" class="el-icon-eleme" ref="emotion" @click="switchEmotionBox()"> |
|
||||
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> |
|
||||
</div> |
|
||||
<div title="发送图片"> |
|
||||
<file-upload :action="imageAction" :maxSize="5*1024*1024" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']" |
|
||||
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail"> |
|
||||
<i class="el-icon-picture-outline"></i> |
|
||||
</file-upload> |
|
||||
</div> |
|
||||
<div title="发送文件"> |
|
||||
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" @success="handleFileSuccess" |
|
||||
@fail="handleFileFail"> |
|
||||
<i class="el-icon-wallet"></i> |
|
||||
</file-upload> |
|
||||
</div> |
|
||||
<div title="聊天记录" class="el-icon-chat-dot-round"></div> |
|
||||
</div> |
|
||||
<textarea v-model="sendText" ref="sendBox" class="send-text-area" @keydown.enter="sendTextMessage()"></textarea> |
|
||||
<div class="im-chat-send"> |
|
||||
<el-button type="primary" @click="sendTextMessage()">发送</el-button> |
|
||||
</div> |
|
||||
</el-footer> |
|
||||
</el-container> |
|
||||
<el-aside class="chat-group-side-box" width="20%" v-show="showSide"> |
|
||||
<chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)"></chat-group-side> |
|
||||
</el-aside> |
|
||||
</el-container> |
|
||||
</el-container> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import ChatGroupSide from "./ChatGroupSide.vue"; |
|
||||
import MessageItem from "./MessageItem.vue"; |
|
||||
import FileUpload from "../common/FileUpload.vue"; |
|
||||
import Emotion from "../common/Emotion.vue"; |
|
||||
|
|
||||
export default { |
|
||||
name: "chatPrivate", |
|
||||
components: { |
|
||||
MessageItem, |
|
||||
FileUpload, |
|
||||
ChatGroupSide, |
|
||||
Emotion |
|
||||
}, |
|
||||
props: { |
|
||||
chat: { |
|
||||
type: Object |
|
||||
} |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
sendText: "", |
|
||||
showSide: false, |
|
||||
group: {}, |
|
||||
groupMembers: [], |
|
||||
showEmotion: false, |
|
||||
emoBoxPos: { |
|
||||
x: 0, |
|
||||
y: 0 |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
handleImageSuccess(res, file) { |
|
||||
let msgInfo = { |
|
||||
groupId: file.raw.targetId, |
|
||||
content: JSON.stringify(res.data), |
|
||||
type: 1 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/group/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then((data) => { |
|
||||
let info = { |
|
||||
type: 'GROUP', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
content: JSON.stringify(res.data), |
|
||||
loadStatus: "ok" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}) |
|
||||
}, |
|
||||
handleImageFail(res, file) { |
|
||||
let info = { |
|
||||
type: 'GROUP', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
loadStatus: "fail" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}, |
|
||||
handleImageBefore(file) { |
|
||||
let url = URL.createObjectURL(file); |
|
||||
let data = { |
|
||||
originUrl: url, |
|
||||
thumbUrl: url |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
fileId: file.uid, |
|
||||
sendId: this.mine.id, |
|
||||
groupId: this.chat.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
sendTime: new Date().getTime(), |
|
||||
selfSend: true, |
|
||||
type: 1, |
|
||||
loadStatus: "loading" |
|
||||
} |
|
||||
// 插入消息 |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
// 借助file对象保存对方id |
|
||||
file.targetId = this.chat.targetId; |
|
||||
}, |
|
||||
handleFileSuccess(res, file) { |
|
||||
let data = { |
|
||||
name: file.name, |
|
||||
size: file.size, |
|
||||
url: res.data |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
groupId: file.raw.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
type: 2 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/group/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then(() => { |
|
||||
let info = { |
|
||||
type: 'GROUP', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
content: JSON.stringify(data), |
|
||||
loadStatus: "ok" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}) |
|
||||
}, |
|
||||
handleFileFail(res, file) { |
|
||||
let info = { |
|
||||
type: 'GROUP', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
loadStatus: "fail" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}, |
|
||||
handleFileBefore(file) { |
|
||||
let url = URL.createObjectURL(file); |
|
||||
let data = { |
|
||||
name: file.name, |
|
||||
size: file.size, |
|
||||
url: url |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
fileId: file.uid, |
|
||||
sendId: this.mine.id, |
|
||||
groupId: this.chat.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
sendTime: new Date().getTime(), |
|
||||
selfSend: true, |
|
||||
type: 2, |
|
||||
loadStatus: "loading" |
|
||||
} |
|
||||
// 插入消息 |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
// 借助file对象保存对方id |
|
||||
file.targetId = this.chat.targetId; |
|
||||
}, |
|
||||
handleCloseSide() { |
|
||||
this.showSide = false; |
|
||||
}, |
|
||||
switchEmotionBox() { |
|
||||
this.showEmotion = !this.showEmotion; |
|
||||
let width = this.$refs.emotion.offsetWidth; |
|
||||
let left = this.$elm.fixLeft(this.$refs.emotion); |
|
||||
let top = this.$elm.fixTop(this.$refs.emotion); |
|
||||
this.emoBoxPos.y = top; |
|
||||
this.emoBoxPos.x = left + width / 2; |
|
||||
}, |
|
||||
handleEmotion(emoText) { |
|
||||
this.sendText += emoText; |
|
||||
}, |
|
||||
sendTextMessage() { |
|
||||
|
|
||||
if (!this.sendText.trim()) { |
|
||||
this.$message.error("不能发送空白信息"); |
|
||||
return |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
groupId: this.chat.targetId, |
|
||||
content: this.sendText, |
|
||||
type: 0 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/group/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then((data) => { |
|
||||
this.$message.success("发送成功"); |
|
||||
this.sendText = ""; |
|
||||
msgInfo.sendTime = new Date().getTime(); |
|
||||
msgInfo.sendId = this.$store.state.userStore.userInfo.id; |
|
||||
msgInfo.selfSend = true; |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 保持输入框焦点 |
|
||||
this.$refs.sendBox.focus(); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
}) |
|
||||
const e = window.event || arguments[0] |
|
||||
if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) { |
|
||||
e.returnValue = false; |
|
||||
e.preventDefault(); |
|
||||
return false; |
|
||||
} |
|
||||
}, |
|
||||
loadGroup(groupId) { |
|
||||
this.$http({ |
|
||||
url: `/group/find/${groupId}`, |
|
||||
method: 'get' |
|
||||
}).then((group) => { |
|
||||
this.group = group; |
|
||||
this.$store.commit("updateChatFromGroup", group); |
|
||||
}); |
|
||||
|
|
||||
this.$http({ |
|
||||
url: `/group/members/${groupId}`, |
|
||||
method: 'get' |
|
||||
}).then((groupMembers) => { |
|
||||
this.groupMembers = groupMembers; |
|
||||
}); |
|
||||
|
|
||||
}, |
|
||||
showName(msgInfo) { |
|
||||
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
|
||||
return member ? member.aliasName : ""; |
|
||||
}, |
|
||||
headImage(msgInfo) { |
|
||||
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); |
|
||||
return member ? member.headImage : ""; |
|
||||
}, |
|
||||
scrollToBottom() { |
|
||||
this.$nextTick(() => { |
|
||||
const div = document.getElementById("chatScrollBox"); |
|
||||
div.scrollTop = div.scrollHeight; |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
computed: { |
|
||||
mine() { |
|
||||
return this.$store.state.userStore.userInfo; |
|
||||
}, |
|
||||
title() { |
|
||||
let size = this.groupMembers.filter(m => !m.quit).length; |
|
||||
return `${this.chat.showName}(${size})`; |
|
||||
}, |
|
||||
imageAction() { |
|
||||
return `${process.env.VUE_APP_BASE_API}/image/upload`; |
|
||||
}, |
|
||||
fileAction() { |
|
||||
return `${process.env.VUE_APP_BASE_API}/file/upload`; |
|
||||
} |
|
||||
|
|
||||
}, |
|
||||
mounted() { |
|
||||
console.log("group mount...") |
|
||||
this.loadGroup(this.chat.targetId); |
|
||||
this.scrollToBottom(); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style> |
|
||||
.btn-side { |
|
||||
position: absolute; |
|
||||
right: 20px; |
|
||||
line-height: 60px; |
|
||||
font-size: 22px; |
|
||||
cursor: pointer; |
|
||||
|
|
||||
&:hover { |
|
||||
font-size: 30px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
.chat-group-side-box { |
|
||||
border: #dddddd solid 1px; |
|
||||
animation: rtl-drawer-in .3s 1ms; |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,251 +0,0 @@ |
|||||
<template> |
|
||||
<el-container class="r-chat-box"> |
|
||||
<el-header height="60px"> |
|
||||
{{chat.showName}} |
|
||||
</el-header> |
|
||||
<el-main class="im-chat-main" id="chatScrollBox"> |
|
||||
<div class="im-chat-box"> |
|
||||
<ul> |
|
||||
<li v-for="msgInfo in chat.messages" :key="msgInfo.id"> |
|
||||
<message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" |
|
||||
:msgInfo="msgInfo"> |
|
||||
</message-item> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</div> |
|
||||
</el-main> |
|
||||
<el-footer height="25%" class="im-chat-footer"> |
|
||||
<div class="chat-tool-bar"> |
|
||||
<div class="el-icon-eleme" ref="emotion" @click="switchEmotionBox()"> |
|
||||
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion> |
|
||||
</div> |
|
||||
<div> |
|
||||
<file-upload :action="imageAction" :maxSize="5*1024*1024" :fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp', 'image/gif']" |
|
||||
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail"> |
|
||||
<i class="el-icon-picture-outline"></i> |
|
||||
</file-upload> |
|
||||
</div> |
|
||||
<div> |
|
||||
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" @success="handleFileSuccess" |
|
||||
@fail="handleFileFail"> |
|
||||
<i class="el-icon-wallet"></i> |
|
||||
</file-upload> |
|
||||
</div> |
|
||||
<div class="el-icon-chat-dot-round"></div> |
|
||||
</div> |
|
||||
<textarea v-model="sendText" ref="sendBox" class="send-text-area" @keyup.enter="sendTextMessage()"></textarea> |
|
||||
<div class="im-chat-send"> |
|
||||
<el-button type="primary" @click="sendTextMessage()">发送</el-button> |
|
||||
</div> |
|
||||
</el-footer> |
|
||||
</el-container> |
|
||||
</template> |
|
||||
|
|
||||
<script> |
|
||||
import MessageItem from "./MessageItem.vue"; |
|
||||
import FileUpload from "../common/FileUpload.vue"; |
|
||||
import Emotion from "../common/Emotion.vue"; |
|
||||
|
|
||||
export default { |
|
||||
name: "chatPrivate", |
|
||||
components: { |
|
||||
MessageItem, |
|
||||
FileUpload, |
|
||||
Emotion |
|
||||
}, |
|
||||
props: { |
|
||||
chat: { |
|
||||
type: Object |
|
||||
} |
|
||||
}, |
|
||||
data() { |
|
||||
return { |
|
||||
sendText: "", |
|
||||
showEmotion: false, |
|
||||
emoBoxPos: { |
|
||||
x: 0, |
|
||||
y: 0 |
|
||||
} |
|
||||
} |
|
||||
}, |
|
||||
methods: { |
|
||||
handleImageSuccess(res, file) { |
|
||||
let msgInfo = { |
|
||||
recvId: file.raw.targetId, |
|
||||
content: JSON.stringify(res.data), |
|
||||
type: 1 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/private/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then((data) => { |
|
||||
let info = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
content: JSON.stringify(res.data), |
|
||||
loadStatus: "ok" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}) |
|
||||
}, |
|
||||
handleImageFail(res, file) { |
|
||||
let info = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
loadStatus: "fail" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}, |
|
||||
handleImageBefore(file) { |
|
||||
let url = URL.createObjectURL(file); |
|
||||
let data = { |
|
||||
originUrl: url, |
|
||||
thumbUrl: url |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
fileId: file.uid, |
|
||||
sendId: this.mine.id, |
|
||||
recvId: this.chat.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
sendTime: new Date().getTime(), |
|
||||
selfSend: true, |
|
||||
type: 1, |
|
||||
loadStatus: "loading" |
|
||||
} |
|
||||
// 插入消息 |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
// 借助file对象保存对方id |
|
||||
file.targetId = this.chat.targetId; |
|
||||
}, |
|
||||
handleFileSuccess(res, file) { |
|
||||
console.log(res.data); |
|
||||
let data = { |
|
||||
name: file.name, |
|
||||
size: file.size, |
|
||||
url: res.data |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
recvId: file.raw.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
type: 2 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/private/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then(() => { |
|
||||
let info = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
content: JSON.stringify(data), |
|
||||
loadStatus: "ok" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}) |
|
||||
}, |
|
||||
handleFileFail(res, file) { |
|
||||
let info = { |
|
||||
type: 'PRIVATE', |
|
||||
targetId: file.raw.targetId, |
|
||||
fileId: file.raw.uid, |
|
||||
loadStatus: "fail" |
|
||||
} |
|
||||
this.$store.commit("handleFileUpload", info); |
|
||||
}, |
|
||||
handleFileBefore(file) { |
|
||||
let url = URL.createObjectURL(file); |
|
||||
let data = { |
|
||||
name: file.name, |
|
||||
size: file.size, |
|
||||
url: url |
|
||||
} |
|
||||
let msgInfo = { |
|
||||
fileId: file.uid, |
|
||||
sendId: this.mine.id, |
|
||||
recvId: this.chat.targetId, |
|
||||
content: JSON.stringify(data), |
|
||||
sendTime: new Date().getTime(), |
|
||||
selfSend: true, |
|
||||
type: 2, |
|
||||
loadStatus: "loading" |
|
||||
} |
|
||||
// 插入消息 |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
// 借助file对象保存对方id |
|
||||
file.targetId = this.chat.targetId; |
|
||||
}, |
|
||||
switchEmotionBox() { |
|
||||
this.showEmotion = !this.showEmotion; |
|
||||
let width = this.$refs.emotion.offsetWidth; |
|
||||
let left = this.$elm.fixLeft(this.$refs.emotion); |
|
||||
let top = this.$elm.fixTop(this.$refs.emotion); |
|
||||
this.emoBoxPos.y = top; |
|
||||
this.emoBoxPos.x = left + width/2; |
|
||||
}, |
|
||||
handleEmotion(emoText) { |
|
||||
this.sendText += emoText; |
|
||||
}, |
|
||||
sendTextMessage() { |
|
||||
let msgInfo = { |
|
||||
recvId: this.chat.targetId, |
|
||||
content: this.sendText, |
|
||||
type: 0 |
|
||||
} |
|
||||
this.$http({ |
|
||||
url: '/message/private/send', |
|
||||
method: 'post', |
|
||||
data: msgInfo |
|
||||
}).then((data) => { |
|
||||
this.$message.success("发送成功"); |
|
||||
this.sendText = ""; |
|
||||
msgInfo.sendTime = new Date().getTime(); |
|
||||
msgInfo.sendId = this.$store.state.userStore.userInfo.id; |
|
||||
msgInfo.selfSend = true; |
|
||||
this.$store.commit("insertMessage", msgInfo); |
|
||||
// 保持输入框焦点 |
|
||||
this.$refs.sendBox.focus(); |
|
||||
// 滚动到底部 |
|
||||
this.scrollToBottom(); |
|
||||
}) |
|
||||
}, |
|
||||
showName(msg) { |
|
||||
return msg.sendId == this.mine.id ? this.mine.nickName : this.chat.showName |
|
||||
}, |
|
||||
headImage(msg) { |
|
||||
return msg.sendId == this.mine.id ? this.mine.headImageThumb : this.chat.headImage |
|
||||
}, |
|
||||
scrollToBottom() { |
|
||||
this.$nextTick(() => { |
|
||||
const div = document.getElementById("chatScrollBox"); |
|
||||
div.scrollTop = div.scrollHeight; |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
computed: { |
|
||||
mine() { |
|
||||
return this.$store.state.userStore.userInfo; |
|
||||
}, |
|
||||
imageAction() { |
|
||||
return `${process.env.VUE_APP_BASE_API}/image/upload`; |
|
||||
}, |
|
||||
fileAction() { |
|
||||
return `${process.env.VUE_APP_BASE_API}/file/upload`; |
|
||||
} |
|
||||
}, |
|
||||
mounted() { |
|
||||
console.log("private mount...") |
|
||||
this.scrollToBottom(); |
|
||||
} |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<style> |
|
||||
</style> |
|
||||
@ -0,0 +1,141 @@ |
|||||
|
<template> |
||||
|
<el-dialog class="chat-voice" title="语音录制" :visible.sync="visible" width="600px" :before-close="handleClose"> |
||||
|
<div v-show="mode=='RECORD'"> |
||||
|
<div class="chat-voice-tip">{{stateTip}}</div> |
||||
|
<div>时长: {{state=='STOP'?0:parseInt(rc.duration)}}s</div> |
||||
|
</div> |
||||
|
<audio v-show="mode=='PLAY'" :src="url" controls ref="audio" @ended="handleStopAudio()"></audio> |
||||
|
<el-divider content-position="center"></el-divider> |
||||
|
<el-row class="chat-voice-btn-group"> |
||||
|
<el-button round type="primary" v-show="state=='STOP'" @click="handleStartRecord()">开始录音</el-button> |
||||
|
<el-button round type="warning" v-show="state=='RUNNING'" @click="handlePauseRecord()">暂停录音</el-button> |
||||
|
<el-button round type="primary" v-show="state=='PAUSE'" @click="handleResumeRecord()">继续录音</el-button> |
||||
|
<el-button round type="danger" v-show="state=='RUNNING'||state=='PAUSE'" @click="handleCompleteRecord()"> |
||||
|
结束录音</el-button> |
||||
|
<el-button round type="success" v-show="state=='COMPLETE' && mode!='PLAY'" @click="handlePlayAudio()">播放录音 |
||||
|
</el-button> |
||||
|
<el-button round type="warning" v-show="state=='COMPLETE' && mode=='PLAY'" @click="handleStopAudio()">停止播放 |
||||
|
</el-button> |
||||
|
<el-button round type="primary" v-show="state=='COMPLETE'" @click="handleRestartRecord()">重新录音</el-button> |
||||
|
<el-button round type="primary" v-show="state=='COMPLETE'" @click="handleSendRecord()">立即发送</el-button> |
||||
|
</el-row> |
||||
|
|
||||
|
</el-dialog> |
||||
|
|
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import Recorder from 'js-audio-recorder'; |
||||
|
|
||||
|
export default { |
||||
|
name: 'chatVoice', |
||||
|
props: { |
||||
|
visible: { |
||||
|
type: Boolean |
||||
|
} |
||||
|
}, |
||||
|
data() { |
||||
|
return { |
||||
|
rc: new Recorder(), |
||||
|
audio: new Audio(), |
||||
|
state: 'STOP', // STOP、RUNNING、PAUSE、COMPLETE |
||||
|
stateTip: "未开始", |
||||
|
mode: 'RECORD', // RECORD 、PLAY |
||||
|
duration: 0, |
||||
|
url: "" |
||||
|
} |
||||
|
}, |
||||
|
methods: { |
||||
|
handleClose() { |
||||
|
// 关闭前清除数据 |
||||
|
this.rc.destroy(); |
||||
|
this.rc = new Recorder(); |
||||
|
this.audio.pause(); |
||||
|
this.mode = 'RECORD'; |
||||
|
this.state = 'STOP'; |
||||
|
this.stateTip = '未开始'; |
||||
|
this.$emit("close"); |
||||
|
}, |
||||
|
handleStartRecord() { |
||||
|
this.rc.start().then((stream) => { |
||||
|
this.state = 'RUNNING'; |
||||
|
this.stateTip = "正在录音..."; |
||||
|
}).catch(error => { |
||||
|
this.$message.error("录音失败"); |
||||
|
console.log(error); |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
}, |
||||
|
handlePauseRecord() { |
||||
|
this.rc.pause(); |
||||
|
this.state = 'PAUSE'; |
||||
|
this.stateTip = "已暂停录音"; |
||||
|
}, |
||||
|
handleResumeRecord() { |
||||
|
this.rc.resume(); |
||||
|
this.state = 'RUNNING'; |
||||
|
this.stateTip = "正在录音..."; |
||||
|
}, |
||||
|
handleCompleteRecord() { |
||||
|
this.rc.pause(); |
||||
|
this.state = 'COMPLETE'; |
||||
|
this.stateTip = "已结束录音"; |
||||
|
}, |
||||
|
handlePlayAudio() { |
||||
|
let wav = this.rc.getWAVBlob(); |
||||
|
let url = URL.createObjectURL(wav); |
||||
|
this.$refs.audio.src = url; |
||||
|
this.$refs.audio.play(); |
||||
|
this.mode = 'PLAY'; |
||||
|
}, |
||||
|
handleStopAudio() { |
||||
|
console.log(this.$refs.audio); |
||||
|
this.$refs.audio.pause(); |
||||
|
this.mode = 'RECORD'; |
||||
|
}, |
||||
|
handleRestartRecord() { |
||||
|
this.rc.destroy(); |
||||
|
this.rc.start(); |
||||
|
this.state = 'RUNNING'; |
||||
|
this.mode = 'RECORD'; |
||||
|
this.stateTip = "正在录音..."; |
||||
|
}, |
||||
|
handleSendRecord() { |
||||
|
let wav = this.rc.getWAVBlob(); |
||||
|
let name = new Date().getDate() + '.wav'; |
||||
|
var formData = new window.FormData() |
||||
|
formData.append('file', wav, name); |
||||
|
this.$http({ |
||||
|
url: '/file/upload', |
||||
|
data: formData, |
||||
|
method: 'post', |
||||
|
headers: { |
||||
|
'Content-Type': 'multipart/form-data' |
||||
|
} |
||||
|
}).then((url) => { |
||||
|
let data = { |
||||
|
duration: parseInt(this.rc.duration), |
||||
|
url: url |
||||
|
} |
||||
|
this.$emit("send", data); |
||||
|
this.handleClose(); |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss"> |
||||
|
.chat-voice { |
||||
|
|
||||
|
.chat-voice-tip { |
||||
|
font-size: 18px; |
||||
|
} |
||||
|
|
||||
|
.chat-voice-btn-group { |
||||
|
margin-bottom: 20px; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue