安装
yarn add easemob-websdk
在utils文件中写一个WebIMConfig.js (appkey换成自己的)
var config = {
socketServer: (window.location.protocol === 'https:' ? 'https:' : 'http:') + '//im-api-v2-hsb.easemob.com/ws',
restServer: (window.location.protocol === 'https:' ? 'https:' : 'http:') + '//a1-hsb.easemob.com',
appkey: '1100210506200509#single-structure',
Host: 'easemob.com',
https: true,
isHttpDNS: true,
isMultiLoginSessions: true,
isSandBox: false,
isDebug: true,
autoReconnectNumMax: 10,
isWebRTC: window.RTCPeerConnection && /^https\\:$/.test(window.location.protocol),
useOwnUploadFun: false,
i18n: 'cn',
isAutoLogin: false,
p2pMessageCacheSize: 500,
delivery: true,
groupMessageCacheSize: 200,
loglevel: 'ERROR',
enableLocalStorage: true,
deviceId: 'webim',
AgoraAppId: ''
}
export default config
在utils文件中写一个WebIM.js
import websdk from 'easemob-websdk'
import config from './WebIMConfig'
const rtc = {
client: null,
localAudioTrack: null,
localVideoTrack: null
}
const WebIM = (window.WebIM = {})
WebIM.config = config
WebIM.message = websdk.message
WebIM.statusCode = websdk.statusCode
WebIM.utils = websdk.utils
WebIM.logger = websdk.logger
const options = {
isMultiLoginSessions: WebIM.config.isMultiLoginSessions,
isDebug: WebIM.config.isDebug,
https: WebIM.config.https,
isAutoLogin: false,
heartBeatWait: WebIM.config.heartBeatWait,
autoReconnectNumMax: WebIM.config.autoReconnectNumMax,
delivery: WebIM.config.delivery,
appKey: WebIM.config.appkey,
useOwnUploadFun: WebIM.config.useOwnUploadFun,
deviceId: WebIM.config.deviceId,
isHttpDNS: WebIM.config.isHttpDNS,
onOffline: () => {
console.log('onOffline')
},
onOnline: () => {
console.log('onOnline')
}
}
WebIM.conn = new websdk.connection(options)
WebIM.rtc = rtc
export default WebIM
main.js
import WebIM from './utils/WebIM'
Vue.prototype.$webIM = WebIM
表情组件
<template>
<div style="display: flex;justify-content: space-between">
<el-popover v-model="showPopover" placement="top" title="" trigger="click">
<div class="emoji-box">
<img v-for="(v,i) in emojiList" :key="i" :src="require(`../../../assets/faces/${v}`)" class="img-style"
@click="selectEmoji(i)"/>
</div>
<img slot="reference" :src="require('@/assets/happyemoji.png')" alt="" height="16" width="16">
</el-popover>
<div style="display: flex">
<div style="cursor:pointer;color:#fff;border-radius: 4px;padding:5px 16px;background:#86C895;margin-right: 8px" @click="launchConsultation">发起专业问诊</div>
<div style="cursor:pointer;color:#fff;border-radius: 4px;padding:5px 16px;background:#EA5D56;" @click="endConsultation">结束专业问诊</div>
</div>
</div>
</template>
<script>
import emoji from '@/utils/emoji'
export default {
data () {
return {
emojiList: emoji.obj,
showPopover: false
}
},
methods: {
selectEmoji (e) {
const value = (this.inpMessage || '') + e
this.showPopover = false
this.$emit('selectEmoji', value)
},
launchConsultation () {
this.$emit('launchConsultation')
},
endConsultation () {
this.$emit('endConsultation')
}
},
props: {
inpMessage: String
}
}
</script>
<style scoped>
.emoji-box {
width: 360px;
}
.img-style {
width: 22px;
margin: 5px;
cursor: pointer;
}
.img-style:hover {
background-color: aquamarine;
}
</style>
@/utils/emoji (记得assest中准备好图片)
module.exports = {
path: '../../../static/faces',
obj: {
'[):]': 'ee_1.png',
'[:D]': 'ee_2.png',
'[;)]': 'ee_3.png',
'[:-o]': 'ee_4.png',
'[:p]': 'ee_5.png',
'[(H)]': 'ee_6.png',
'[:@]': 'ee_7.png',
'[:s]': 'ee_8.png',
'[:$]': 'ee_9.png',
'[:(]': 'ee_10.png',
'[:\'(]': 'ee_11.png',
'[:|]': 'ee_18.png',
'[(a)]': 'ee_13.png',
'[8o|]': 'ee_14.png',
'[8-|]': 'ee_15.png',
'[+o(]': 'ee_16.png',
'[: 'ee_12.png',
'[|-)]': 'ee_17.png',
'[*-)]': 'ee_19.png',
'[:-#]': 'ee_20.png',
'[:-*]': 'ee_22.png',
'[^o)]': 'ee_21.png',
'[8-)]': 'ee_23.png',
'[(|)]': 'ee_24.png',
'[(u)]': 'ee_25.png',
'[(S)]': 'ee_26.png',
'[(*)]': 'ee_27.png',
'[(#)]': 'ee_28.png',
'[(R)]': 'ee_29.png',
'[({)]': 'ee_30.png',
'[(})]': 'ee_31.png',
'[(k)]': 'ee_32.png',
'[(F)]': 'ee_33.png',
'[(W)]': 'ee_34.png',
'[(D)]': 'ee_35.png'
}
}
聊天.vue
<template>
<div class="onlineService">
<div style="height: 100%;">
<div class="chat">
<div class="chat_l">
<div class="inConversation" @click="showChatList=!showChatList">
<span style="font-size: 14px;">会话中</span>
<i :style="{transform: showChatList? '':'rotate(180deg)'}" class="el-icon-arrow-up"></i>
</div>
<div style="max-height: 248px;overflow-y: auto">
<el-collapse-transition>
<div v-show="showChatList">
<div v-for="(item, i) in chatList" :key="i"
:style="{cursor:'pointer', background: item.name===chatting.name?'#eff2f7': ''}"
class="chatList"
@click="chatWithHim(item)">
<img :src="require('@/assets/headPortrait.jpg')" alt="" height="40" width="40">
<div>
<div class="flex">
<div class="chatName">{{ item.name }}</div>
<div v-show="item.unread_num!==0" :class="['badge']"
:style="{padding: item.unread_num>10?'2px 6px':''}">
{{ item.unread_num }}
</div>
</div>
<div class="chatTime">{{ timestampConversion(item.time) }}</div>
</div>
</div>
</div>
</el-collapse-transition>
</div>
<div class="newSession" @click="showNewChatList=!showNewChatList">
<span style="font-size: 14px;">新会话</span>
<i :style="{transform: showNewChatList? '':'rotate(180deg)'}" class="el-icon-arrow-up"></i>
</div>
<el-collapse-transition>
<div v-show="showNewChatList" style="max-height: 248px;overflow-y: auto">
<div v-for="(item, i) in newChatList" :key="i" class="chatList" style="cursor:pointer;"
@click="startConversation(item)">
<img :src="VUE_APP_BASE_API + '/api/v1/Files/GetAttachments?fileName=' + item.headPortrait" alt=""
height="40" width="40">
<div>
<div class="flex">
<div class="chatName">{{ item.name }}</div>
<div :class="['badge']" :style="{padding: item.unread_num>10?'2px 6px':''}">{{
item.unread_num
}}
</div>
</div>
<div style="display: flex;justify-content: space-between">
<div class="chatTime">{{ item.createDate }}</div>
<div
style="font-size:12px;cursor:pointer;color:#fff;border-radius: 15px;padding:2px 8px;background:#86C895;margin-right: 8px;display: inline-block"
@click="startSession(item)">
开始会话
</div>
</div>
</div>
</div>
</div>
</el-collapse-transition>
</div>
<div class="chat_m">
<div class="chat_m_top flex">
<div class="name">{{ chatting.name }}</div>
<div class="ending" @click="endConversation">结束交谈</div>
</div>
<div id="messgaeContent" class="chat_m_m">
<div v-for="(item,i) in message" :key="i">
<div v-if="!item.host">
<div class="flex">
<i class="el-icon-user photo"></i>
<div class="phone">{{ item.name }}</div>
<div class="time">{{ timestampConversion(item.time) }}</div>
</div>
<div :key="timer" class="chatContent" v-html="renderTxt(item.chatContent)">
</div>
</div>
<div v-else class="host">
<div class="flex">
<div>我</div>
<div class="time">{{ timestampConversion(item.time) }}</div>
</div>
<div class="chatContent">
<div v-html="renderTxt(item.chatContent)"></div>
<div v-show="item.chatContent==='确认进行专业问诊?'"
style="color:#fff;border-radius: 4px;padding:5px 16px;background:#86C895;display: inline-block;margin-top: 14px">
待确认
</div>
</div>
</div>
<!-- <img class="photo" :src="require('@/assets/user-ico.png')" alt="" width="30" height="30"> -->
</div>
</div>
<div class="chat_m_bottom">
<!-- 表情组件 -->
<div class="emoji">
<ChatEmoji :inpMessage="messageChat" @endConsultation="endConsultation"
@launchConsultation="launchConsultation"
@selectEmoji="selectEmoji"/>
</div>
<div style="padding: 20px">
<el-input ref="txtDom" v-model="messageChat" rows="6" type="textarea"></el-input>
<el-button type="success" @click="sendMessage('')">发送</el-button>
</div>
</div>
</div>
<div class="chat_r">
<div class="chatTitle">用户信息</div>
<div><span style="width: 80px;display: inline-block">账号</span>15234561897</div>
<div><span style="width: 80px;display: inline-block">姓名</span>Barbara Hudson</div>
<div><span style="width: 80px;display: inline-block">管理周期</span>健康减脂餐-冲刺7日餐</div>
<div><span style="width: 80px;display: inline-block">专业问诊</span>7次</div>
<el-button type="text">查看详情</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
import ChatEmoji from './chatEmoji/index.vue'
import emoji from '@/utils/emoji'
import dayjs from 'dayjs'
import api from '@/api/api'
export default {
components: {
ChatEmoji
},
watch: {
$route (to, from) {
console.log('route', to, from)
if (to.path !== '/' && to.path !== '/login') {
console.log('进入IM')
var that = this
if (!this.isOpened) {
this.$webIM.conn.listen({
onOpened: function () {
console.log('登录成功')
that.isOpened = true
},
onClosed: function () {
that.isOpened = false
},
onTextMessage: function (message) {
console.log('收到文本消息:', message)
that.receiveMessage(message)
},
onCmdMessage: function (message) {
console.log('当前用户收到透传消息: ', message)
},
onPictureMessage: function (message) {
console.log(message)
that.receiveMessage(message)
},
onAudioMessage: function (message) {
console.log(message)
},
onPresence: function (message) {
switch (message.type) {
case 'direct_joined':
break
}
}
})
const options = {
user: 'zrymishu',
pwd: '123456',
appKey: this.$WebIM.config.appkey
}
this.$webIM.conn.open(options)
}
}
}
},
data () {
return {
VUE_APP_BASE_API: process.env.VUE_APP_BASE_API,
timer: Date.parse(new Date()),
isMessage: false,
showChatList: true,
showNewChatList: true,
selectCustomer: {},
defaultselectedkeys: ['1'],
desc: '',
chatList: this.$store.state.chatList,
newChatList: [],
messageChat: '',
isOpened: false,
group: null,
message: [
],
chatting: {
name: ''
},
conversationalist: []
}
},
beforeMount () {
const options = {
user: 'zrymishu',
pwd: '123456',
appKey: this.$webIM.config.appkey
}
this.$webIM.conn.open(options)
var that = this
this.$webIM.conn.listen({
onOpened: function (message) {
console.log('登录成功', message)
that.isOpened = true
that.$webIM.conn.getGroup().then(async (res) => {
console.log(res, 'res')
})
that.$webIM.conn.getRoster().then((res) => {
console.log('获取好友列表', res)
})
},
onClosed: function () {
that.isOpened = false
},
onTextMessage: function (message) {
console.log(message)
that.receiveMessage(message)
},
onCmdMessage: function (message) {
console.log('当前用户收到透传消息: ', message)
},
onEmojiMessage: function (message) {
console.log('onEmojiMessage', message)
const { type } = message
type === 'chat' && this.ack(message)
},
onPictureMessage: function (message) {
console.log(message)
that.receiveMessage(message)
},
onAudioMessage: function (message) {
console.log(message)
},
onPresence: function (message) {
switch (message.type) {
case 'direct_joined':
break
}
}
})
},
mounted () {
this.$nextTick(() => {
})
this.getUserSendLists()
},
methods: {
async getUserSendLists () {
const res = await api.getUserSendList()
if (!res.state) return false
this.newChatList = res.data
},
getConversation () {
const that = this
this.$webIM.conn.getConversationlist().then(res => {
console.log('res.data.channel_infos', res.data.channel_infos)
let newList = []
console.log('chatList.length', that.chatList.length)
if (!that.chatList.length) {
res.data.channel_infos.map(item => {
newList.push({
...item,
chatting: false
})
})
console.log('没有有正在谈话', newList)
that.getConversationalist(newList)
return false
} else {
newList = res.data.channel_infos.filter(
(x) => !that.chatList.some((i) => i.id === x.lastMessage.from)
)
console.log('有正在谈话', newList)
that.getConversationalist(newList)
}
})
},
getConversationalist (data) {
this.newChatList = []
this.conversationalist = []
console.log('datadatadatadatadata', data)
data.map(ele => {
if (!ele.chatting) {
if (this.conversationalist.indexOf(ele.lastMessage.from) !== -1) {
const index = this.newChatList.findIndex(item => {
return item.name === ele.lastMessage.from
})
this.newChatList[index].unread_num += 1
} else {
this.conversationalist.push(ele.lastMessage.from)
this.newChatList.push({
id: ele.lastMessage.from,
name: ele.lastMessage.from,
time: ele.lastMessage.time,
unread_num: 1
})
}
}
})
this.chatWithHim(this.chatList[0])
},
startTalk (value) {
this.statistics = '1'
const that = this
that.isMessage = value.state
if (that.isMessage) {
that.group.forEach(item => {
if (item.customerID === value.customerId) {
that.selectCustomer = item
}
})
that.defaultselectedkeys[0] = that.selectCustomer.name
that.selectCustomer.unread = 0
that.$nextTick(() => {
var div = document.getElementById('messgaeContent')
div.scrollTop = div.scrollHeight
})
} else {
that.selectCustomer = {}
}
},
sendMessage (data) {
console.log('发给:', this.chatting.name)
if (this.messageChat !== '' || data !== '') {
var that = this
var id = this.$webIM.conn.getUniqueId()
var msg = new this.$webIM.message('txt', id)
var message = this.messageChat || data
console.log(data === '' ? {} : { ACTION_INITIATE: 0 }, '确认进行专业问诊')
var option = {
msg: message,
to: that.chatting.name,
chatType: 'singleChat',
ext: data === '' ? {} : { ACTION_INITIATE: 0 },
success: function () {
console.log('发送成功:', that.chatting.name, message)
that.send({
from: 'zrymishu',
to: that.chatting.name,
chatType: 'chat',
payload: JSON.stringify({
msg: message,
type: 'txt'
})
})
that.message.push({
name: 'zrymishu',
time: new Date(),
chatContent: message,
host: true
})
that.$nextTick(() => {
const div = document.getElementById('messgaeContent')
div.scrollTop = div.scrollHeight
})
that.messageChat = ''
}
}
msg.set(option)
this.$webIM.conn.send(msg.body)
}
},
customEmoji (value) {
return `${value}" style="width:20px; height: 20px;"/>`
},
renderTxt (txt = '') {
const rnTxt = []
let match = null
const regex = /(\[.*?\])/g
let start = 0
let index = 0
while ((match = regex.exec(txt))) {
index = match.index
if (index > start) {
rnTxt.push(txt.substring(start, index))
}
if (match[1] in emoji.obj) {
const v = emoji.obj[match[1]]
rnTxt.push(this.customEmoji(v))
} else {
rnTxt.push(match[1])
}
start = index + match[1].length
}
rnTxt.push(txt.substring(start, txt.length))
this.timer = Date.parse(new Date())
return rnTxt.toString().replace(/,/g, '')
},
selectEmoji (v) {
this.$data.messageChat = v
this.$refs.txtDom.focus()
},
receiveMessage (message) {
console.log('收到文本消息', message)
if (this.chatting.name === message.from) {
this.message.push({
name: message.from,
time: message.time,
chatContent: message.data,
host: false
})
}
console.log('who来的消息', this.conversationalist, message.from)
console.log('this.chatList', this.newChatList)
const isChatList = this.chatList.some(ele => {
return ele.name === message.from
})
if (isChatList) {
console.log('正在会话的人来了一条消息')
if (this.chatting.name === message.from) {
this.chatList.map((ele, i) => {
if (ele.name === message.from) {
this.chatList[i].unread_num = 0
}
})
} else {
this.chatList.map((ele, i) => {
if (ele.name === message.from) {
this.chatList[i].unread_num += 1
}
})
}
} else {
if (this.conversationalist.indexOf(message.from) !== -1) {
const index = this.newChatList.findIndex(item => {
return item.name === message.from
})
console.log('存在', index)
this.newChatList[index].unread_num += 1
} else {
console.log('nono存在')
this.conversationalist.push(message.from)
this.newChatList.push({
name: message.from,
time: message.time,
unread_num: 1
})
}
}
this.$nextTick(() => {
var div = document.getElementById('messgaeContent')
div.scrollTop = div.scrollHeight
})
},
chatWithHim (row) {
this.chatting.name = row.name
row.unread_num = 0
},
startConversation (item) {
item.unread_num = 0
this.$store.commit('chatList', item)
console.log(this.chatList, 'this.chatListthis.chatList')
this.chatWithHim(item)
},
timestampConversion (val) {
return dayjs(Number(val)).format('YYYY-MM-DD HH:mm:ss')
},
async endConversation () {
const res = await api.endSession({
imUserId: this.chatting.name,
secretaryId: this.$store.state.user_info.userId
})
if (!res.state) return false
this.chatList = this.chatList.filter(ele => {
return ele.name !== this.chatting.name
})
},
launchConsultation () {
this.sendMessage('确认进行专业问诊?')
},
endConsultation () {
},
async startSession (item) {
const res = await api.receiveUser({
id: item.id,
secretaryId: this.$store.state.user_info.userId,
personId: item.personId
})
if (!res.state) return false
this.getUserSendLists()
this.startConversation(item)
},
async send (data) {
const res = await api.send(data)
if (!res.state) return false
}
}
}
</script>
<style lang='less'>
.onlineService {
background-color: #fff;
height: 100%;
overflow: hidden;
.scroll {
overflow: auto;
height: calc(100% - 80px);
.box {
display: flex;
margin: 30px 0 0 40px;
.box_l {
width: 40px;
height: 40px;
background: #bac2cb;
border-radius: 50%;
margin-right: 10px;
}
.box_r {
width: 100%;
position: relative;
.userName {
font-weight: bold;
}
.time {
margin-top: 4px;
color: #69747e;
}
.ques {
margin: 10px 0;
font-size: 14px;
}
.el-input {
}
img {
margin: 10px 10px 10px 0;
}
.el-button.is-round {
position: absolute;
top: 8px;
right: 40px;
padding: 6px 14px;
}
}
}
}
.chat {
display: flex;
width: 100%;
height: 100%;
.chat_l {
width: 280px;
padding: 0 16px;
box-sizing: border-box;
border-right: 1px solid #E7ECF0;
.inConversation {
margin: 24px 0 16px;
background-color: #F2F3F5;
padding: 6px 8px;
display: flex;
justify-content: space-between;
cursor: pointer;
}
.newSession {
margin: 16px 0;
background-color: #F2F3F5;
padding: 6px 8px;
display: flex;
justify-content: space-between;
cursor: pointer;
}
.chatList {
display: flex;
padding: 10px 20px;
.flex {
width: 155px;
justify-content: space-between;
}
img {
width: 40px;
height: 40px;
margin-right: 8px;
border-radius: 25px;
}
.chatName {
font-size: 14px;
margin-bottom: 6px;
}
.badge {
color: #fff;
height: 20px;
line-height: 20px;
background: #ea5d56;
border-radius: 25px;
min-width: 20px;
text-align: center;
font-size: 12px;
}
.chatTime {
font-size: 12px;
color: #69747E;
}
}
}
.chat_m {
width: calc(100% - 560px);
.chat_m_top {
border-bottom: 2px solid #E7ECF0;
box-shadow: 0px 6px 10px -5px rgb(178 178 178 / 40%);
justify-content: space-between;
align-items: center;
box-sizing: border-box;
padding: 19px;
.name {
font-weight: bold;
font-size: 16px;
}
.ending {
color: #fff;
padding: 6px 14px;
background: linear-gradient(to right, #ff6e00, #ea5d56);
border-radius: 15px;
cursor: pointer;
}
}
.chat_m_m {
padding: 20px;
box-sizing: border-box;
height: calc(100% - 390px);
overflow: auto;
.photo {
background-color: #afb9c6;
border-radius: 50%;
width: 30px;
height: 30px;
text-align: center;
line-height: 30px;
}
.el-icon-user {
color: #fff;
font-size: 16px;
}
.flex {
align-items: center;
margin-bottom: 12px;
.phone {
margin: 0 10px;
font-size: 14px;
color: #69747E;
}
.time {
font-size: 14px;
color: #69747E;
}
}
.chatContent {
margin-bottom: 25px;
display: inline-block;
margin-left: 40px;
border: 1px solid #D7DDE5;
background-color: #EFF2F7;
color: #171d25;
padding: 11px 20px;
border-radius: 0px 8px 8px 8px;
}
.host {
display: flex;
flex-direction: column;
align-items: flex-end;
.phone {
margin: 0 10px;
font-size: 14px;
color: #171D25;
}
.time {
margin-left: 10px;
color: #171D25;
}
.chatContent {
background-color: #fff;
border-radius: 8px 0px 8px 8px;
}
}
}
.chat_m_bottom {
height: 230px;
.emoji {
border-top: 1px solid #E7ECF0;
border-bottom: 1px solid #E7ECF0;
padding: 15px 20px;
}
.el-textarea__inner {
border: none;
padding: 0;
font-size: 14px;
resize: none;
}
.el-button--success {
border-radius: 3px;
float: right;
}
}
}
.chat_r {
width: 280px;
padding-left: 20px;
border-left: 1px solid #EFF2F5;
div {
margin-top: 20px;
}
div:first-child {
font-weight: bold;
}
.el-button {
padding-top: 20px;
}
}
}
.el-tabs {
padding-top: 10px;
}
.el-tabs__header {
margin: 0;
}
.el-tabs__nav-wrap::after {
background-color: #eff2f5;
}
.el-tabs--top .el-tabs__item.is-top:nth-child(2) {
padding-left: 20px !important;
}
.flex {
display: flex;
}
}
</style>
完善中。。。