Browse Source

!52 加入移动端音视频功能

Merge pull request !52 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
4fa83467c0
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 10
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  2. 9
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  3. 6
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  4. 17
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  5. 17
      im-ui/src/api/enums.js
  6. BIN
      im-ui/src/assets/audio/call.wav
  7. 10
      im-ui/src/assets/iconfont/iconfont.css
  8. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  9. 48
      im-ui/src/components/chat/ChatBox.vue
  10. 76
      im-ui/src/components/chat/ChatMessageItem.vue
  11. 260
      im-ui/src/components/chat/ChatPrivateVideo.vue
  12. 155
      im-ui/src/components/chat/ChatVideoAcceptor.vue
  13. 8
      im-ui/src/store/chatStore.js
  14. 28
      im-ui/src/store/uiStore.js
  15. 22
      im-ui/src/store/userStore.js
  16. 2
      im-ui/src/view/Friend.vue
  17. 61
      im-ui/src/view/Home.vue
  18. 22
      im-uniapp/App.vue
  19. 5
      im-uniapp/common/enums.js
  20. 48
      im-uniapp/components/chat-message-item/chat-message-item.vue
  21. 13
      im-uniapp/hybrid/html/index.html
  22. 20
      im-uniapp/manifest.json
  23. 7
      im-uniapp/pages.json
  24. 57
      im-uniapp/pages/chat/chat-box.vue
  25. 113
      im-uniapp/pages/chat/chat-video.vue
  26. 14
      im-uniapp/static/icon/iconfont.css
  27. BIN
      im-uniapp/static/icon/iconfont.ttf
  28. 10
      im-uniapp/store/chatStore.js

10
im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java

@ -21,8 +21,8 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "呼叫视频通话") @ApiOperation(httpMethod = "POST", value = "呼叫视频通话")
@PostMapping("/call") @PostMapping("/call")
public Result call(@RequestParam Long uid, @RequestBody String offer) { public Result call(@RequestParam Long uid, @RequestParam(defaultValue = "video") String mode, @RequestBody String offer) {
webrtcService.call(uid, offer); webrtcService.call(uid, mode, offer);
return ResultUtils.success(); return ResultUtils.success();
} }
@ -57,15 +57,15 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "挂断") @ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup") @PostMapping("/handup")
public Result leave(@RequestParam Long uid) { public Result handup(@RequestParam Long uid) {
webrtcService.leave(uid); webrtcService.handup(uid);
return ResultUtils.success(); return ResultUtils.success();
} }
@PostMapping("/candidate") @PostMapping("/candidate")
@ApiOperation(httpMethod = "POST", value = "同步candidate") @ApiOperation(httpMethod = "POST", value = "同步candidate")
public Result forwardCandidate(@RequestParam Long uid, @RequestBody String candidate) { public Result candidate(@RequestParam Long uid, @RequestBody String candidate) {
webrtcService.candidate(uid, candidate); webrtcService.candidate(uid, candidate);
return ResultUtils.success(); return ResultUtils.success();
} }

9
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -53,10 +53,15 @@ public enum MessageType {
* 消息加载标记 * 消息加载标记
*/ */
LOADDING(30,"加载中"), LOADDING(30,"加载中"),
/**
* 语音呼叫
*/
RTC_CALL_VOICE(100, "语音呼叫"),
/** /**
* 呼叫 * 视频呼叫
*/ */
RTC_CALL(101, "呼叫"), RTC_CALL_VIDEO(101, "视频呼叫"),
/** /**
* 接受 * 接受
*/ */

6
im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java

@ -12,9 +12,9 @@ import java.util.List;
*/ */
public interface IWebrtcService { public interface IWebrtcService {
void call(Long uid, String offer); void call(Long uid, String mode,String offer);
void accept(Long uid, @RequestBody String answer); void accept(Long uid, String answer);
void reject(Long uid); void reject(Long uid);
@ -22,7 +22,7 @@ public interface IWebrtcService {
void failed(Long uid, String reason); void failed(Long uid, String reason);
void leave(Long uid); void handup(Long uid);
void candidate(Long uid, String candidate); void candidate(Long uid, String candidate);

17
im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java

@ -33,7 +33,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
private final ICEServerConfig iceServerConfig; private final ICEServerConfig iceServerConfig;
@Override @Override
public void call(Long uid, String offer) { public void call(Long uid, String mode, String offer) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
if (!imClient.isOnline(uid)) { if (!imClient.isOnline(uid)) {
throw new GlobalException("对方目前不在线"); throw new GlobalException("对方目前不在线");
@ -46,7 +46,8 @@ public class WebrtcServiceImpl implements IWebrtcService {
redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS); redisTemplate.opsForValue().set(key, webrtcSession, 12, TimeUnit.HOURS);
// 向对方所有终端发起呼叫 // 向对方所有终端发起呼叫
PrivateMessageVO messageInfo = new PrivateMessageVO(); PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_CALL.code()); MessageType messageType = mode.equals("video") ? MessageType.RTC_CALL_VIDEO : MessageType.RTC_CALL_VOICE;
messageInfo.setType(messageType.code());
messageInfo.setRecvId(uid); messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId()); messageInfo.setSendId(session.getUserId());
messageInfo.setContent(offer); messageInfo.setContent(offer);
@ -120,7 +121,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
removeWebrtcSession(session.getUserId(), uid); removeWebrtcSession(session.getUserId(), uid);
// 向对方所有终端推送取消通话信令 // 向对方所有终端推送取消通话信令
PrivateMessageVO messageInfo = new PrivateMessageVO(); PrivateMessageVO messageInfo = new PrivateMessageVO();
messageInfo.setType(MessageType.RTC_ACCEPT.code()); messageInfo.setType(MessageType.RTC_CANCEL.code());
messageInfo.setRecvId(uid); messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId()); messageInfo.setSendId(session.getUserId());
@ -146,12 +147,12 @@ public class WebrtcServiceImpl implements IWebrtcService {
messageInfo.setType(MessageType.RTC_FAILED.code()); messageInfo.setType(MessageType.RTC_FAILED.code());
messageInfo.setRecvId(uid); messageInfo.setRecvId(uid);
messageInfo.setSendId(session.getUserId()); messageInfo.setSendId(session.getUserId());
messageInfo.setContent(reason);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>(); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(uid); sendMessage.setRecvId(uid);
// 告知其他终端已经会话失败,中止呼叫 sendMessage.setSendToSelf(false);
sendMessage.setSendToSelf(true);
sendMessage.setSendResult(false); sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal())); sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo); sendMessage.setData(messageInfo);
@ -161,7 +162,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
} }
@Override @Override
public void leave(Long uid) { public void handup(Long uid) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 查询webrtc会话 // 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid); WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
@ -215,9 +216,9 @@ public class WebrtcServiceImpl implements IWebrtcService {
private WebrtcSession getWebrtcSession(Long userId, Long uid) { private WebrtcSession getWebrtcSession(Long userId, Long uid) {
String key = getSessionKey(userId, uid); String key = getSessionKey(userId, uid);
WebrtcSession webrtcSession = (WebrtcSession) redisTemplate.opsForValue().get(key); WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key);
if (webrtcSession == null) { if (webrtcSession == null) {
throw new GlobalException("视频通话已结束"); throw new GlobalException("通话已结束");
} }
return webrtcSession; return webrtcSession;
} }

17
im-ui/src/api/enums.js

@ -5,13 +5,16 @@ const MESSAGE_TYPE = {
FILE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12, RECEIPT:12,
TIP_TIME:20, TIP_TIME:20,
TIP_TEXT:21, TIP_TEXT:21,
LOADDING:30, LOADDING:30,
RTC_CALL: 101, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,
RTC_CANCEL: 104, RTC_CANCEL: 104,
@ -20,10 +23,12 @@ const MESSAGE_TYPE = {
RTC_CANDIDATE: 107 RTC_CANDIDATE: 107
} }
const USER_STATE = { const RTC_STATE = {
OFFLINE: 0, FREE: 0, //空闲,可以被呼叫
FREE: 1, WAIT_CALL: 1, // 呼叫后等待
BUSY: 2 WAIT_ACCEPT: 2, // 被呼叫后等待
ACCEPTED: 3, // 已接受聊天,等待建立连接
CHATING:4 // 聊天中
} }
const TERMINAL_TYPE = { const TERMINAL_TYPE = {
@ -41,7 +46,7 @@ const MESSAGE_STATUS = {
export { export {
MESSAGE_TYPE, MESSAGE_TYPE,
USER_STATE, RTC_STATE,
TERMINAL_TYPE, TERMINAL_TYPE,
MESSAGE_STATUS MESSAGE_STATUS
} }

BIN
im-ui/src/assets/audio/call.wav

Binary file not shown.

10
im-ui/src/assets/iconfont/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3791506 */ font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1706022894868') format('truetype'); src: url('iconfont.ttf?t=1710567233281') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-ok:before { .icon-ok:before {
content: "\e6ac"; content: "\e6ac";
} }

BIN
im-ui/src/assets/iconfont/iconfont.ttf

Binary file not shown.

48
im-ui/src/components/chat/ChatBox.vue

@ -13,7 +13,9 @@
<div class="im-chat-box"> <div class="im-chat-box">
<ul> <ul>
<li v-for="(msgInfo, idx) in chat.messages" :key="idx"> <li v-for="(msgInfo, idx) in chat.messages" :key="idx">
<chat-message-item v-show="idx >= showMinIdx" :mine="msgInfo.sendId == mine.id" <chat-message-item v-show="idx >= showMinIdx"
@call="onCall(msgInfo.type)"
:mine="msgInfo.sendId == mine.id"
:headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo"
:groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage"> :groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item> </chat-message-item>
@ -44,8 +46,11 @@
</div> </div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()"> <div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div> </div>
<div title="视频聊天" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline" <div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showVideoBox()"> @click="showChatVideo('voice')">
</div>
<div title="视频通话" v-show="chat.type == 'PRIVATE'" class="el-icon-video-camera"
@click="showChatVideo('video')">
</div> </div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div> <div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div> </div>
@ -133,13 +138,19 @@ export default {
methods: { methods: {
moveChatToTop(){ moveChatToTop(){
let chatIdx = this.$store.getters.findChatIdx(this.chat); let chatIdx = this.$store.getters.findChatIdx(this.chat);
console.log(chatIdx);
this.$store.commit("moveTop",chatIdx); this.$store.commit("moveTop",chatIdx);
}, },
closeRefBox() { closeRefBox() {
this.$refs.emoBox.close(); this.$refs.emoBox.close();
this.$refs.atBox.close(); this.$refs.atBox.close();
}, },
onCall(type){
if(type == this.$enums.MESSAGE_TYPE.RT_VOICE){
this.showChatVideo('voice');
}else if(type == this.$enums.MESSAGE_TYPE.RT_VIDEO){
this.showChatVideo('video');
}
},
onKeyDown() { onKeyDown() {
if (this.$refs.atBox.show) { if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown() this.$refs.atBox.moveDown()
@ -433,11 +444,17 @@ export default {
closeVoiceBox() { closeVoiceBox() {
this.showVoice = false; this.showVoice = false;
}, },
showVideoBox() { showChatVideo(mode) {
this.$store.commit("showChatPrivateVideoBox", { let rtcInfo = {
mode: mode,
isHost: true,
friend: this.friend, friend: this.friend,
master: true sendId: this.$store.state.userStore.userInfo.id,
}); recvId: this.friend.id,
offer: "",
state: this.$enums.RTC_STATE.WAIT_CALL
}
this.$store.commit("setRtcInfo",rtcInfo);
}, },
showHistoryBox() { showHistoryBox() {
this.showHistory = true; this.showHistory = true;
@ -686,6 +703,12 @@ export default {
}, },
unreadCount() { unreadCount() {
return this.chat.unreadCount; return this.chat.unreadCount;
},
messageSize() {
if (!this.chat || !this.chat.messages) {
return 0;
}
return this.chat.messages.length;
} }
}, },
watch: { watch: {
@ -716,9 +739,9 @@ export default {
}, },
immediate: true immediate: true
}, },
unreadCount: { messageSize: {
handler(newCount, oldCount) { handler(newSize, oldSize) {
if (newCount > 0) { if (newSize > oldSize) {
// //
this.scrollToBottom(); this.scrollToBottom();
} }
@ -812,7 +835,6 @@ export default {
} }
} }
.send-content-area { .send-content-area {
position: relative; position: relative;
display: flex; display: flex;
@ -820,8 +842,6 @@ export default {
height: 100%; height: 100%;
background-color: white !important; background-color: white !important;
.send-text-area { .send-text-area {
box-sizing: border-box; box-sizing: border-box;
padding: 5px; padding: 5px;

76
im-ui/src/components/chat/ChatMessageItem.vue

@ -1,6 +1,9 @@
<template> <template>
<div class="chat-msg-item"> <div class="chat-msg-item">
<div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">{{ msgInfo.content }}</div> <div class="chat-msg-tip"
v-show="msgInfo.type == $enums.MESSAGE_TYPE.RECALL || msgInfo.type == $enums.MESSAGE_TYPE.TIP_TEXT">
{{ msgInfo.content }}
</div>
<div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME"> <div class="chat-msg-tip" v-show="msgInfo.type == $enums.MESSAGE_TYPE.TIP_TIME">
{{ $date.toTimeText(msgInfo.sendTime) }} {{ $date.toTimeText(msgInfo.sendTime) }}
</div> </div>
@ -48,30 +51,39 @@
<div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()"> <div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio> <audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div> </div>
<div class="chat-realtime chat-msg-text" v-if="isRealtime">
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VOICE" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-voice"></span>
<span v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VIDEO" title="重新呼叫"
@click="$emit('call')" class="iconfont icon-chat-video"></span>
<span>{{msgInfo.content}}</span>
</div>
<div class="chat-msg-status" v-if="!isRealtime">
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span> && msgInfo.status == $enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span> && msgInfo.status != $enums.MESSAGE_STATUS.READED">未读</span>
</div>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox"> <div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span> <span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span> <span v-else>{{msgInfo.readedCount}}人已读</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show = false" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@select="onSelectMenu"></right-menu> @close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed> <chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed>
</div> </div>
</template> </template>
<script> <script>
import HeadImage from "../common/HeadImage.vue"; import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue'; import ChatGroupReaded from './ChatGroupReaded.vue';
export default { export default {
name: "messageItem", name: "messageItem",
components: { components: {
HeadImage, HeadImage,
@ -188,13 +200,17 @@ export default {
}); });
} }
return items; return items;
},
isRealtime() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE ||
this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO
}
} }
} }
}
</script> </script>
<style lang="scss"> <style lang="scss">
.chat-msg-item { .chat-msg-item {
.chat-msg-tip { .chat-msg-tip {
line-height: 50px; line-height: 50px;
@ -245,14 +261,14 @@ export default {
.chat-msg-text { .chat-msg-text {
display: block; display: block;
position: relative; position: relative;
line-height: 30px; line-height: 26px;
margin-top: 3px; margin-top: 3px;
padding: 7px; padding: 7px;
background-color: white; background-color: white;
border-radius: 10px; border-radius: 10px;
color: black; color: black;
display: block; display: block;
font-size: 16px; font-size: 14px;
text-align: left; text-align: left;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-all; word-break: break-all;
@ -298,6 +314,7 @@ export default {
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
padding-bottom: 5px; padding-bottom: 5px;
.chat-file-box { .chat-file-box {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@ -352,19 +369,34 @@ export default {
} }
} }
.chat-unread { .chat-realtime {
font-size: 12px; display: flex;
color: #f23c0f; align-items: center;
font-weight: 600;
.iconfont {
cursor: pointer;
font-size: 22px;
padding-right: 8px;
}
} }
.chat-msg-status {
display: block;
.chat-readed { .chat-readed {
font-size: 12px; font-size: 12px;
color: #888; color: #888;
font-weight: 600; font-weight: 600;
} }
.chat-receipt{ .chat-unread {
font-size: 12px;
color: #f23c0f;
font-weight: 600;
}
}
.chat-receipt {
font-size: 13px; font-size: 13px;
color: blue; color: blue;
cursor: pointer; cursor: pointer;
@ -425,10 +457,18 @@ export default {
.chat-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.chat-realtime {
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
} }
} }
} }
} }
} }
</style> </style>

260
im-ui/src/components/chat/ChatPrivateVideo.vue

@ -1,13 +1,12 @@
<template> <template>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" <el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="visible" width="50%" height="70%" :before-close="handleClose"> :visible="isShow" width="50%" height="70%" :before-close="handleClose">
<div class="chat-video"> <div class="chat-video">
<div class="chat-video-box"> <div v-show="rtcInfo.mode=='video'" class="chat-video-box">
<div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..." <div class="chat-video-friend" v-loading="loading" element-loading-text="等待对方接听..."
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.5)"> element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" <head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="80" :name="rtcInfo.friend.nickName"
:id="friend.id" :size="80" :name="friend.nickName" :url="rtcInfo.friend.headImage">
:url="friend.headImage">
</head-image> </head-image>
<video ref="friendVideo" autoplay=""></video> <video ref="friendVideo" autoplay=""></video>
</div> </div>
@ -15,11 +14,18 @@
<video ref="mineVideo" autoplay=""></video> <video ref="mineVideo" autoplay=""></video>
</div> </div>
</div> </div>
<div v-show="rtcInfo.mode=='voice'" class="chat-voice-box" v-loading="loading" element-loading-text="等待对方接听..."
element-loading-spinner="el-icon-loading" element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" :id="rtcInfo.friend.id" :size="200" :name="rtcInfo.friend.nickName"
:url="rtcInfo.friend.headImage">
<div class="chat-voice-name">{{rtcInfo.friend.nickName}}</div>
</head-image>
</div>
<div class="chat-video-controllbar"> <div class="chat-video-controllbar">
<div v-show="state=='CONNECTING'" title="取消呼叫" class="icon iconfont icon-phone-reject reject" <div v-show="isWaiting" title="取消呼叫" class="icon iconfont icon-phone-reject reject" style="color: red;"
style="color: red;" @click="cancel()"></div> @click="cancel()"></div>
<div v-show="state=='CONNECTED'" title="挂断" class="icon iconfont icon-phone-reject reject" <div v-show="isAccepted" title="挂断" class="icon iconfont icon-phone-reject reject" style="color: red;"
style="color: red;" @click="handup()"></div> @click="handup()"></div>
</div> </div>
</div> </div>
</el-dialog> </el-dialog>
@ -34,30 +40,15 @@
components: { components: {
HeadImage HeadImage
}, },
props: {
visible: {
type: Boolean
},
friend: {
type: Object
},
master: {
type: Boolean
},
offer: {
type: Object
}
},
data() { data() {
return { return {
callerId: null, isShow: false,
stream: null, stream: null,
audio: new Audio(), audio: new Audio(),
loading: false, loading: false,
peerConnection: null, peerConnection: null,
videoTime: 0, videoTime: 0,
videoTimer: null, videoTimer: null,
state: 'NOT_CONNECTED',
candidates: [], candidates: [],
configuration: { configuration: {
iceServers: [] iceServers: []
@ -66,10 +57,12 @@
}, },
methods: { methods: {
init() { init() {
this.isShow = true;
if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) { if (!this.hasUserMedia() || !this.hasRTCPeerConnection()) {
this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC"); this.$message.error("初始化失败,原因可能是: 1.未部署ssl证书 2.您的浏览器不支持WebRTC");
if (!this.master) { this.insertMessage("设备不支持通话");
this.sendFailed("对方浏览器不支持WebRTC") if (!this.rtcInfo.isHost) {
this.sendFailed("对方设备不支持通话")
} }
return; return;
} }
@ -77,30 +70,30 @@
this.openCamera((stream) => { this.openCamera((stream) => {
// webrtc // webrtc
this.setupPeerConnection(stream); this.setupPeerConnection(stream);
if (this.master) { if (this.rtcInfo.isHost) {
// //
this.call(); this.call();
} else { } else {
// //
this.accept(this.offer); this.accept(this.rtcInfo.offer);
} }
}); });
}, },
openCamera(callback) { openCamera(callback) {
navigator.getUserMedia({ navigator.getUserMedia({
video: true, video: this.isVideo,
audio: true audio: true
}, }, (stream) => {
(stream) => { console.log(this.loading)
this.stream = stream; this.stream = stream;
this.$refs.mineVideo.srcObject = stream; this.$refs.mineVideo.srcObject = stream;
this.$refs.mineVideo.muted = true; this.$refs.mineVideo.muted = true;
callback(stream) callback(stream)
}, }, (error) => {
(error) => { let devText = this.isVideo ? "摄像头" : "麦克风"
this.$message.error("打开摄像头失败:" + error); this.$message.error(`打开${devText}失败:${error}`);
callback() callback()
}); })
}, },
closeCamera() { closeCamera() {
if (this.stream) { if (this.stream) {
@ -110,7 +103,6 @@
this.$refs.mineVideo.srcObject = null; this.$refs.mineVideo.srcObject = null;
this.stream = null; this.stream = null;
} }
}, },
setupPeerConnection(stream) { setupPeerConnection(stream) {
this.peerConnection = new RTCPeerConnection(this.configuration); this.peerConnection = new RTCPeerConnection(this.configuration);
@ -119,7 +111,7 @@
}; };
this.peerConnection.onicecandidate = (event) => { this.peerConnection.onicecandidate = (event) => {
if (event.candidate) { if (event.candidate) {
if (this.state == 'CONNECTED') { if (this.isAccepted) {
// , // ,
this.sendCandidate(event.candidate); this.sendCandidate(event.candidate);
} else { } else {
@ -140,78 +132,104 @@
this.resetTime(); this.resetTime();
} }
}; };
}, },
handleMessage(msg) { insertMessage(messageTip) {
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) { //
if (msg.selfSend) { let chat = {
// type: 'PRIVATE',
this.$message.success("已在其他设备接听"); targetId: this.rtcInfo.friend.id,
this.close(); showName: this.rtcInfo.friend.nickName,
} else { headImage: this.rtcInfo.friend.headImageThumb,
// };
this.$store.commit("openChat", chat);
//
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE;
let msgInfo = {
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE,
sendId: this.rtcInfo.sendId,
recvId: this.rtcInfo.recvId,
content: this.isChating ? "通话时长 " + this.currentTime : messageTip,
status: 1,
selfSend: this.rtcInfo.isHost,
sendTime: new Date().getTime()
}
this.$store.commit("insertMessage", msgInfo);
},
onRTCMessage(msg) {
if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
//
this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content))); this.peerConnection.setRemoteDescription(new RTCSessionDescription(JSON.parse(msg.content)));
// //
this.loading = false; this.loading = false;
// //
this.state = 'CONNECTED'; this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
// //
this.audio.pause(); this.audio.pause();
// candidate // candidate
this.candidates.forEach((candidate) => { this.candidates.forEach((candidate) => {
this.sendCandidate(candidate); this.sendCandidate(candidate);
}) })
} } else if (!msg.selfSend && msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
//
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) { this.$message.error("对方拒绝了您的通话请求");
if (msg.selfSend) { //
// this.insertMessage("对方已拒绝")
this.$message.success("已在其他设备拒绝通话"); //
this.close(); this.close();
} else {
//
this.$message.error("对方拒绝了您的视频请求");
this.close();
}
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_FAILED) {
//
this.$message.error(msg.content) this.$message.error(msg.content)
//
this.insertMessage(msg.content)
this.close(); this.close();
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
// 线
this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content))); this.peerConnection.addIceCandidate(new RTCIceCandidate(JSON.parse(msg.content)));
} else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) { } else if (msg.type == this.$enums.MESSAGE_TYPE.RTC_HANDUP) {
this.$message.success("对方挂断了视频通话"); //
this.$message.success("对方已挂断");
//
this.insertMessage("对方已挂断")
this.close(); this.close();
} }
}, },
call() { call() {
this.peerConnection.createOffer((offer) => { let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
}
this.peerConnection.createOffer(offerParam).then((offer) => {
this.peerConnection.setLocalDescription(offer); this.peerConnection.setLocalDescription(offer);
this.$http({ this.$http({
url: `/webrtc/private/call?uid=${this.friend.id}`, url: `/webrtc/private/call?uid=${this.rtcInfo.friend.id}&mode=${this.rtcInfo.mode}`,
method: 'post', method: 'post',
data: JSON.stringify(offer) data: JSON.stringify(offer)
}).then(() => { }).then(() => {
this.callId = this.$store.state.userStore.userInfo.id;
this.loading = true; this.loading = true;
this.state = 'CONNECTING'; //
this.audio.play(); this.audio.play();
}) })
},(error) => { }, (error) => {
this.insertMessage("未接通")
this.$message.error(error); this.$message.error(error);
}); });
}, },
accept(offer) { accept(offer) {
let offerParam = {
offerToRecieveAudio: 1,
offerToRecieveVideo: this.isVideo ? 1 : 0
}
this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer)); this.peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
this.peerConnection.createAnswer((answer) => { this.peerConnection.createAnswer(offerParam).then((answer) => {
this.peerConnection.setLocalDescription(answer); this.peerConnection.setLocalDescription(answer);
this.$http({ this.$http({
url: `/webrtc/private/accept?uid=${this.friend.id}`, url: `/webrtc/private/accept?uid=${this.rtcInfo.friend.id}`,
method: 'post', method: 'post',
data: JSON.stringify(answer) data: JSON.stringify(answer)
}).then(() => { }).then(() => {
this.state = 'CONNECTED'; //
this.callerId = this.friend.id; this.$store.commit("setRtcState", this.$enums.RTC_STATE.CHATING)
}) })
}, },
@ -222,43 +240,43 @@
}, },
handup() { handup() {
this.$http({ this.$http({
url: `/webrtc/private/handup?uid=${this.friend.id}`, url: `/webrtc/private/handup?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
this.insertMessage("已挂断")
this.close(); this.close();
this.$message.success("已挂断视频通话") this.$message.success("您已挂断,通话结束")
}, },
cancel() { cancel() {
this.$http({ this.$http({
url: `/webrtc/private/cancel?uid=${this.friend.id}`, url: `/webrtc/private/cancel?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
this.insertMessage("已取消")
this.close(); this.close();
this.$message.success("已停止呼叫视频通话") this.$message.success("已取消呼叫,通话结束")
}, },
sendFailed(reason) { sendFailed(reason) {
this.$http({ this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post' method: 'post'
}) })
}, },
sendCandidate(candidate) { sendCandidate(candidate) {
this.$http({ this.$http({
url: `/webrtc/private/candidate?uid=${this.friend.id}`, url: `/webrtc/private/candidate?uid=${this.rtcInfo.friend.id}`,
method: 'post', method: 'post',
data: JSON.stringify(candidate) data: JSON.stringify(candidate)
}) })
}, },
close() { close() {
this.$emit("close"); this.isShow = false;
this.closeCamera(); this.closeCamera();
this.loading = false; this.loading = false;
this.state = 'NOT_CONNECTED';
this.videoTime = 0; this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer); this.videoTimer && clearInterval(this.videoTimer);
this.audio.pause(); this.audio.pause();
this.candidates = []; this.candidates = [];
this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
if (this.peerConnection) { if (this.peerConnection) {
this.peerConnection.close(); this.peerConnection.close();
this.peerConnection.onicecandidate = null; this.peerConnection.onicecandidate = null;
@ -267,7 +285,8 @@
if (this.$refs.friendVideo) { if (this.$refs.friendVideo) {
this.$refs.friendVideo.srcObject = null; this.$refs.friendVideo.srcObject = null;
} }
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
}, },
resetTime() { resetTime() {
this.videoTime = 0; this.videoTime = 0;
@ -277,9 +296,9 @@
}, 1000) }, 1000)
}, },
handleClose() { handleClose() {
if (this.state == 'CONNECTED') { if (this.isAccepted) {
this.handup() this.handup()
} else if (this.state == 'CONNECTING') { } else if (this.isWaiting) {
this.cancel(); this.cancel();
} else { } else {
this.close(); this.close();
@ -316,39 +335,57 @@
}, },
watch: { watch: {
visible: { rtcState: {
handler(newValue, oldValue) { handler(newState, oldState) {
if (newValue) { // WAIT_CALLACCEPTED
if (newState == this.$enums.RTC_STATE.WAIT_CALL ||
newState == this.$enums.RTC_STATE.ACCEPTED) {
this.init(); this.init();
//
this.$store.commit("setUserState", this.$enums.USER_STATE.BUSY);
} }
} }
} }
}, },
computed: { computed: {
title() { title() {
let strTitle = `视频聊天-${this.friend.nickName}`; let strTitle = `${this.modeText}通话-${this.rtcInfo.friend.nickName}`;
if (this.state == 'CONNECTED') { if (this.isChating) {
strTitle += `(${this.currentTime})`; strTitle += `(${this.currentTime})`;
} else if (this.state == 'CONNECTING') { } else if (this.isWaiting) {
strTitle += `(呼叫中)`; strTitle += `(呼叫中)`;
} }
return strTitle; return strTitle;
}, },
currentTime() { currentTime() {
let currentTime = 0; let min = Math.floor(this.videoTime / 60);
if (this.state == 'CONNECTED' && this.videoTime) { let sec = this.videoTime % 60;
currentTime = Math.floor(this.videoTime);
}
let min = Math.floor(currentTime / 60);
let sec = currentTime % 60;
let strTime = min < 10 ? "0" : ""; let strTime = min < 10 ? "0" : "";
strTime += min; strTime += min;
strTime += ":" strTime += ":"
strTime += sec < 10 ? "0" : ""; strTime += sec < 10 ? "0" : "";
strTime += sec; strTime += sec;
return strTime; return strTime;
},
rtcInfo() {
return this.$store.state.userStore.rtcInfo;
},
rtcState() {
return this.rtcInfo.state;
},
isVideo() {
return this.rtcInfo.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isAccepted() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING ||
this.rtcInfo.state == this.$enums.RTC_STATE.ACCEPTED
},
isWaiting() {
return this.rtcInfo.state == this.$enums.RTC_STATE.WAIT_CALL;
},
isChating() {
return this.rtcInfo.state == this.$enums.RTC_STATE.CHATING;
} }
}, },
mounted() { mounted() {
@ -358,10 +395,20 @@
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.chat-video { .chat-video {
position: relative; position: relative;
.el-loading-text {
color: white !important;
font-size: 16px !important;
}
.el-icon-loading {
color: white !important;
font-size: 30px !important;
}
.chat-video-box { .chat-video-box {
position: relative; position: relative;
border: #4880b9 solid 1px; border: #4880b9 solid 1px;
@ -396,6 +443,23 @@
} }
} }
.chat-voice-box {
position: relative;
display: flex;
justify-content: center;
border: #4880b9 solid 1px;
width: 100%;
height: 50vh;
padding-top: 10vh;
background-color: aliceblue;
.chat-voice-name {
text-align: center;
font-size: 22px;
font-weight: 600;
}
}
.chat-video-controllbar { .chat-video-controllbar {
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;

155
im-ui/src/components/chat/ChatVideoAcceptor.vue

@ -1,10 +1,8 @@
<template> <template>
<div class="chat-video-acceptor"> <div v-show="isShow" class="chat-video-acceptor">
<head-image :id="rtcInfo.friend.id" :name="rtcInfo.friend.nickName" :url="rtcInfo.friend.headImage" :size="100"></head-image>
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<div class="acceptor-text"> <div class="acceptor-text">
{{friend.nickName}} 请求和您进行视频通话... {{tip}}
</div> </div>
<div class="acceptor-btn-group"> <div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div> <div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div>
@ -21,70 +19,154 @@
components: { components: {
HeadImage HeadImage
}, },
props: {
friend: {
type: Object
}
},
data() { data() {
return { return {
offer: {}, isShow: false,
audio: new Audio() audio: new Audio()
} }
}, },
methods: { methods: {
accpet() { accpet() {
let info = { //
friend: this.friend, this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED);
master: false, //
offer: this.offer
}
this.$store.commit("showChatPrivateVideoBox", info);
this.close(); this.close();
}, },
reject() { reject() {
this.$http({ this.$http({
url: `/webrtc/private/reject?uid=${this.friend.id}`, url: `/webrtc/private/reject?uid=${this.rtcInfo.friend.id}`,
method: 'post' method: 'post'
}) })
//
this.insertMessage("已拒绝");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
failed(reason) { failed(reason) {
this.$http({ this.$http({
url: `/webrtc/private/failed?uid=${this.friend.id}&reason=${reason}`, url: `/webrtc/private/failed?uid=${this.rtcInfo.friend.id}&reason=${reason}`,
method: 'post' method: 'post'
}) })
//
this.insertMessage("未接听");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
onCall(msgInfo) { onRtcCall(msgInfo, friend, mode) {
this.offer = JSON.parse(msgInfo.content); console.log("onRtcCall")
if (this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY) { //
this.failed("对方正忙,暂时无法接听"); if (this.rtcInfo.state != this.$enums.RTC_STATE.FREE) {
//
let reason = "对方忙,无法与您通话";
this.$http({
url: `/webrtc/private/failed?uid=${msgInfo.sendId}&reason=${reason}`,
method: 'post'
})
return; return;
} }
//
this.isShow = true;
// RTC
let rtcInfo = {
mode: mode,
isHost: false,
friend: friend,
sendId: msgInfo.sendId,
recvId: msgInfo.recvId,
offer: JSON.parse(msgInfo.content),
state: this.$enums.RTC_STATE.WAIT_ACCEPT
}
this.$store.commit("setRtcInfo", rtcInfo);
//
this.audio.play();
// //
this.timer && clearTimeout(this.timer); this.timer && clearTimeout(this.timer);
this.timer = setTimeout(() => { this.timer = setTimeout(() => {
this.failed("对方未接听"); this.failed("对方无应答");
}, 30000) }, 30000)
this.audio.play();
}, },
onCancel() { onRtcCancel(msgInfo) {
//
if (msgInfo.sendId != this.rtcInfo.friend.id) {
return;
}
// //
this.$message.success("对方取消了呼叫"); this.$message.success("对方取消了呼叫");
//
this.insertMessage("对方已取消");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close(); this.close();
}, },
handleMessage(msgInfo) { onRtcAccept(msgInfo) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL) { //
this.onCall(msgInfo); if (msgInfo.selfSend) {
this.$message.success("已在其他设备接听");
//
this.insertMessage("已在其他设备接听")
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
}
},
onRtcReject(msgInfo){
//
if (msgInfo.selfSend) {
this.$message.success("已在其他设备拒绝通话");
//
this.insertMessage("已在其他设备拒绝")
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
}
},
onRTCMessage(msgInfo, friend) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE) {
this.onRtcCall(msgInfo, friend, "voice");
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO) {
this.onRtcCall(msgInfo, friend, "video");
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) { } else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.onCancel(); this.onRtcCancel(msgInfo);
} else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_ACCEPT) {
this.onRtcAccept(msgInfo);
}else if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_REJECT) {
this.onRtcReject(msgInfo);
} }
}, },
insertMessage(messageTip) {
//
let chat = {
type: 'PRIVATE',
targetId: this.rtcInfo.friend.id,
showName: this.rtcInfo.friend.nickName,
headImage: this.rtcInfo.friend.headImageThumb,
};
this.$store.commit("openChat", chat);
//
let MESSAGE_TYPE = this.$enums.MESSAGE_TYPE;
let msgInfo = {
type: this.rtcInfo.mode == "video" ? MESSAGE_TYPE.RT_VIDEO : MESSAGE_TYPE.RT_VOICE,
sendId: this.rtcInfo.sendId,
recvId: this.rtcInfo.recvId,
content: messageTip,
status: 1,
selfSend: this.rtcInfo.isHost,
sendTime: new Date().getTime()
}
this.$store.commit("insertMessage", msgInfo);
},
close() { close() {
this.timer && clearTimeout(this.timer); this.timer && clearTimeout(this.timer);
this.audio.pause(); this.audio.pause();
this.$emit("close"); this.isShow = false;
}, },
initAudio() { initAudio() {
let url = require(`@/assets/audio/call.wav`); let url = require(`@/assets/audio/call.wav`);
@ -92,10 +174,18 @@
this.audio.loop = true; this.audio.loop = true;
} }
}, },
computed: {
tip() {
let modeText = this.mode == "video" ? "视频" : "语音"
return `${this.rtcInfo.friend.nickName} 请求和您进行${modeText}通话...`
},
rtcInfo(){
return this.$store.state.userStore.rtcInfo;
}
},
mounted() { mounted() {
// //
this.initAudio(); this.initAudio();
} }
} }
</script> </script>
@ -131,6 +221,7 @@
font-size: 60px; font-size: 60px;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
&.accept { &.accept {
color: green; color: green;
animation: anim 2s ease-in infinite, vibration 2s ease-in infinite; animation: anim 2s ease-in infinite, vibration 2s ease-in infinite;

8
im-ui/src/store/chatStore.js

@ -128,6 +128,10 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
} }
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} else if (msgInfo.type == MESSAGE_TYPE.RT_VOICE) {
chat.lastContent = "[语音通话]";
} else if (msgInfo.type == MESSAGE_TYPE.RT_VIDEO) {
chat.lastContent = "[视频通话]";
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;

28
im-ui/src/store/uiStore.js

@ -11,18 +11,7 @@ export default {
fullImage: { // 全屏大图 fullImage: { // 全屏大图
show: false, show: false,
url: "" url: ""
},
chatPrivateVideo:{ // 私人视频聊天
show: false,
master: false, // 是否房主
friend:{},
offer:{} // 对方发起带过过来的sdp信息
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
} }
}, },
mutations: { mutations: {
showUserInfoBox(state,user){ showUserInfoBox(state,user){
@ -45,23 +34,6 @@ export default {
}, },
closeFullImageBox(state){ closeFullImageBox(state){
state.fullImage.show = false; state.fullImage.show = false;
},
showChatPrivateVideoBox(state,info){
state.chatPrivateVideo.show = true;
state.chatPrivateVideo.friend = info.friend;
state.chatPrivateVideo.master = info.master;
state.chatPrivateVideo.offer = info.offer;
},
closeChatPrivateVideoBox(state){
state.chatPrivateVideo.show = false;
},
showVideoAcceptorBox(state,friend){
state.videoAcceptor.show = true;
state.videoAcceptor.friend = friend;
},
closeVideoAcceptorBox(state){
state.videoAcceptor.show = false;
} }
} }
} }

22
im-ui/src/store/userStore.js

@ -1,25 +1,35 @@
import {USER_STATE} from "../api/enums.js"
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
import {RTC_STATE} from "../api/enums.js"
export default { export default {
state: { state: {
userInfo: { userInfo: {
}, },
state: USER_STATE.FREE rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
}, },
mutations: { mutations: {
setUserInfo(state, userInfo) { setUserInfo(state, userInfo) {
state.userInfo = userInfo state.userInfo = userInfo
}, },
setUserState(state, userState) { setRtcInfo(state, rtcInfo ){
state.state = userState; state.rtcInfo = rtcInfo;
},
setRtcState(state,rtcState){
state.rtcInfo.state = rtcState;
}, },
clear(state){ clear(state){
state.userInfo = {}; state.userInfo = {};
state.state = USER_STATE.FREE; state.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
} }
}, },
actions:{ actions:{

2
im-ui/src/view/Friend.vue

@ -129,7 +129,7 @@
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: user.id,
showName: user.nickName, showName: user.nickName,
headImage: user.headImage, headImage: user.headImageThumb,
}; };
this.$store.commit("openChat", chat); this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0); this.$store.commit("activeChat", 0);

61
im-ui/src/view/Home.vue

@ -40,25 +40,20 @@
@close="$store.commit('closeUserInfoBox')"></user-info> @close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" <full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image> @close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show" <chat-private-video ref="privateVideo"></chat-private-video>
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master" <chat-video-acceptor ref="videoAcceptor"></chat-video-acceptor>
:offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
</chat-private-video>
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" :friend="uiStore.videoAcceptor.friend"
@close="$store.commit('closeVideoAcceptorBox')">
</chat-video-acceptor>
</el-container> </el-container>
</template> </template>
<script> <script>
import HeadImage from '../components/common/HeadImage.vue'; import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue'; import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue'; import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue'; import FullImage from '../components/common/FullImage.vue';
import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue'; import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue';
import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue'; import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
export default { export default {
components: { components: {
HeadImage, HeadImage,
Setting, Setting,
@ -70,7 +65,7 @@ export default {
data() { data() {
return { return {
showSettingDialog: false, showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000 lastPlayAudioTime: new Date().getTime() - 1000
} }
}, },
methods: { methods: {
@ -108,7 +103,8 @@ export default {
if (e.code != 3000) { if (e.code != 3000) {
// 线 // 线
this.$message.error("连接断开,正在尝试重新连接..."); this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
} }
}); });
}).catch((e) => { }).catch((e) => {
@ -143,7 +139,9 @@ export default {
} }
// , // ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", { friendId: msg.sendId }) this.$store.commit("readedMessage", {
friendId: msg.sendId
})
return; return;
} }
// //
@ -156,16 +154,17 @@ export default {
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// webrtc // webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL && if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) { msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
// //
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL || if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) { msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO ||
this.$store.commit("showVideoAcceptorBox", friend); rtcInfo.state == this.$enums.RTC_STATE.FREE ||
this.$refs.videoAcceptor.handleMessage(msg) rtcInfo.state == this.$enums.RTC_STATE.WAIT_ACCEPT) {
this.$refs.videoAcceptor.onRTCMessage(msg,friend)
} else { } else {
this.$refs.videoAcceptor.close() this.$refs.privateVideo.onRTCMessage(msg)
this.$refs.privateVideo.handleMessage(msg)
} }
return; return;
} }
@ -242,8 +241,8 @@ export default {
location.href = "/"; location.href = "/";
}, },
playAudioTip() { playAudioTip() {
if (new Date() - this.lastPlayAudioTime > 1000) { if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date(); this.lastPlayAudioTime = new Date().getTime();
let audio = new Audio(); let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`); let url = require(`@/assets/audio/tip.wav`);
audio.src = url; audio.src = url;
@ -318,11 +317,11 @@ export default {
unmounted() { unmounted() {
this.$wsApi.close(); this.$wsApi.close();
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.navi-bar { .navi-bar {
background: #333333; background: #333333;
padding: 10px; padding: 10px;
padding-top: 50px; padding-top: 50px;
@ -381,13 +380,13 @@ export default {
color: white !important; color: white !important;
} }
} }
} }
.content-box { .content-box {
padding: 0; padding: 0;
background-color: #E9EEF3; background-color: #E9EEF3;
color: #333; color: #333;
text-align: center; text-align: center;
} }
</style> </style>

22
im-uniapp/App.vue

@ -104,8 +104,26 @@
}, },
insertPrivateMessage(friend, msg) { insertPrivateMessage(friend, msg) {
// webrtc // webrtc
if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL && if (msg.type >= enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) {} msg.type <= enums.MESSAGE_TYPE.RTC_CANDIDATE) {
//
if(msg.type == enums.MESSAGE_TYPE.RTC_CALL_VOICE
|| msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO){
let mode = msg.type == enums.MESSAGE_TYPE.RTC_CALL_VIDEO? "video":"voice";
let pages = getCurrentPages();
let curPage = pages[pages.length-1].route;
if(curPage != "pages/chat/chat-video"){
const friendInfo = encodeURIComponent(JSON.stringify(friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=${mode}&friend=${friendInfo}&isHost=false`
})
}
}
setTimeout(() => {
uni.$emit('WS_RTC',msg);
},500)
return;
}
let chatInfo = { let chatInfo = {
type: 'PRIVATE', type: 'PRIVATE',

5
im-uniapp/common/enums.js

@ -5,13 +5,16 @@ const MESSAGE_TYPE = {
FILE:2, FILE:2,
AUDIO:3, AUDIO:3,
VIDEO:4, VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10, RECALL:10,
READED:11, READED:11,
RECEIPT:12, RECEIPT:12,
TIP_TIME:20, TIP_TIME:20,
TIP_TEXT:21, TIP_TEXT:21,
LOADDING:30, LOADDING:30,
RTC_CALL: 101, RTC_CALL_VOICE: 100,
RTC_CALL_VIDEO: 101,
RTC_ACCEPT: 102, RTC_ACCEPT: 102,
RTC_REJECT: 103, RTC_REJECT: 103,
RTC_CANCEL: 104, RTC_CANCEL: 104,

48
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -40,14 +40,24 @@
<text title="发送失败" v-if="loadFail" @click="onSendFail" <text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text> class="send-fail iconfont icon-warning-circle-fill"></text>
</view> </view>
<view class="chat-realtime chat-msg-text" v-if="isRTMessage">
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VOICE"
class="iconfont icon-chat-voice"></text>
<text v-if="msgInfo.type==$enums.MESSAGE_TYPE.RT_VIDEO"
class="iconfont icon-chat-video"></text>
<text>{{msgInfo.content}}</text>
</view>
<view class="chat-msg-status" v-if="!isRTMessage">
<text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text> && msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId <text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text> && msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text>
</view>
<view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox"> <view class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text> <text v-if="msgInfo.receiptOk" class="tool-icon iconfont icon-ok"></text>
<text v-else>{{msgInfo.readedCount}}人已读</text> <text v-else>{{msgInfo.readedCount}}人已读</text>
</view> </view>
<!-- <!--
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()"> <view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
@ -188,6 +198,10 @@
}); });
} }
return items; return items;
},
isRTMessage() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE ||
this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO
} }
} }
@ -343,17 +357,33 @@
} }
.chat-unread {
font-size: 10px;
color: #f23c0f; .chat-realtime {
font-weight: 600; display: flex;
align-items: center;
.iconfont {
font-size: 20px;
padding-right: 8px;
}
} }
.chat-msg-status {
display: block;
.chat-readed { .chat-readed {
font-size: 10px; font-size: 10px;
color: #ccc; color: #ccc;
font-weight: 600; font-weight: 600;
} }
.chat-unread {
font-size: 10px;
color: #f23c0f;
font-weight: 600;
}
}
.chat-receipt { .chat-receipt {
font-size: 13px; font-size: 13px;
color: darkblue; color: darkblue;
@ -405,6 +435,14 @@
.chat-msg-file { .chat-msg-file {
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.chat-realtime {
display: flex;
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
} }
} }
} }

13
im-uniapp/hybrid/html/index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="icon" href="favicon.ico">
<title>视频通话</title>
</head>
<body>
<div style="padding-top:10px; text-align: center;font-size: 16px;">音视频通话为付费功能,有需要请联系作者...</div>
</body>
</html>

20
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM", "name" : "盒子IM",
"appid" : "__UNI__69DD57A", "appid" : "__UNI__69DD57A",
"description" : "", "description" : "",
"versionName" : "1.0.0", "versionName" : "1.0.6",
"versionCode" : "100", "versionCode" : 106,
"transformPx" : false, "transformPx" : false,
/* 5+App */ /* 5+App */
"app-plus" : { "app-plus" : {
@ -18,7 +18,9 @@
}, },
/* */ /* */
"modules" : { "modules" : {
"Camera" : {} "Camera" : {},
"Record" : {},
"Bluetooth" : {}
}, },
/* */ /* */
"distribute" : { "distribute" : {
@ -39,14 +41,20 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>", "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>", "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>", "<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>" "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" />",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\" />"
], ],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ], "abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21 "minSdkVersion" : 21
}, },
/* ios */ /* ios */
"ios" : { "ios" : {
"dSYMs" : false "dSYMs" : false,
"privacyDescription" : {
"NSMicrophoneUsageDescription" : "",
"NSCameraUsageDescription" : ""
}
}, },
/* SDK */ /* SDK */
"sdkConfigs" : { "sdkConfigs" : {
@ -117,3 +125,5 @@
} }
} }
} }
/* ios *//* SDK */

7
im-uniapp/pages.json

@ -1,5 +1,5 @@
{ {
"lazyCodeLoading":"requiredComponents",
"pages": [{ "pages": [{
"path": "pages/login/login" "path": "pages/login/login"
}, { }, {
@ -16,6 +16,8 @@
"path": "pages/common/user-info" "path": "pages/common/user-info"
}, { }, {
"path": "pages/chat/chat-box" "path": "pages/chat/chat-box"
},{
"path": "pages/chat/chat-video"
}, { }, {
"path": "pages/friend/friend-add" "path": "pages/friend/friend-add"
}, { }, {
@ -30,7 +32,8 @@
"path": "pages/mine/mine-edit" "path": "pages/mine/mine-edit"
},{ },{
"path": "pages/mine/mine-password" "path": "pages/mine/mine-password"
}], }
],
"globalStyle": { "globalStyle": {
"navigationBarTitleText": "盒子IM", "navigationBarTitleText": "盒子IM",
"navigationBarTextStyle": "black", "navigationBarTextStyle": "black",

57
im-uniapp/pages/chat/chat-box.vue

@ -10,6 +10,7 @@
:scroll-into-view="'chat-item-'+scrollMsgIdx"> :scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx"> <view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" <chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
@click="onClickMessage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage" :showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx" @longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo" :groupMembers="groupMembers"> :msgInfo="msgInfo" :groupMembers="groupMembers">
@ -30,9 +31,9 @@
<view class="send-bar"> <view class="send-bar">
<view class="send-text"> <view class="send-text">
<textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false" <textarea class="send-text-area" v-model="sendText" auto-height :show-confirm-bar="false"
:placeholder="isReceipt?'[回执消息]':''" :placeholder="isReceipt?'[回执消息]':''" :adjust-position="false" @confirm="sendTextMessage()"
:adjust-position="false" @confirm="sendTextMessage()" @keyboardheightchange="onKeyboardheightchange" @keyboardheightchange="onKeyboardheightchange" @input="onTextInput" confirm-type="send" confirm-hold
@input="onTextInput" confirm-type="send" confirm-hold :hold-keyboard="true"></textarea> :hold-keyboard="true"></textarea>
</view> </view>
<view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view> <view v-if="chat.type=='GROUP'" class="iconfont icon-at" @click="openAtBox()"></view>
<view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view> <view class="iconfont icon-icon_emoji" @click="switchChatTabBox('emo',true)"></view>
@ -76,9 +77,13 @@
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view> <view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view> <view class="tool-name">回执消息</view>
</view> </view>
<view class="chat-tools-item" @click="showTip()"> <view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVideoCall()">
<view class="tool-icon iconfont icon-video"></view>
<view class="tool-name">视频通话</view>
</view>
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVoiceCall()">
<view class="tool-icon iconfont icon-call"></view> <view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">呼叫</view> <view class="tool-name">语音通话</view>
</view> </view>
</view> </view>
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true"> <scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
@ -116,17 +121,35 @@
}, },
methods: { methods: {
showTip() { showTip() {
uni.showToast({ uni.showToast({
title: "暂未支持...", title: "暂未支持...",
icon: "none" icon: "none"
}) })
}, },
moveChatToTop(){ onClickMessage(msgInfo){
if(msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE){
this.onVoiceCall();
}else if(msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO){
this.onVideoCall();
}
},
onVideoCall(){
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=video&friend=${friendInfo}&isHost=true`
})
},
onVoiceCall(){
const friendInfo = encodeURIComponent(JSON.stringify(this.friend));
uni.navigateTo({
url: `/pages/chat/chat-video?mode=voice&friend=${friendInfo}&isHost=true`
})
},
moveChatToTop() {
let chatIdx = this.$store.getters.findChatIdx(this.chat); let chatIdx = this.$store.getters.findChatIdx(this.chat);
this.$store.commit("moveTop",chatIdx); this.$store.commit("moveTop", chatIdx);
}, },
switchReceipt(){ switchReceipt() {
this.isReceipt = !this.isReceipt; this.isReceipt = !this.isReceipt;
}, },
openAtBox() { openAtBox() {
@ -164,12 +187,12 @@
icon: "none" icon: "none"
}); });
} }
let receiptText = this.isReceipt? "【回执消息】":""; let receiptText = this.isReceipt ? "【回执消息】" : "";
let atText = this.createAtText(); let atText = this.createAtText();
let msgInfo = { let msgInfo = {
content: receiptText + this.sendText + atText, content: receiptText + this.sendText + atText,
atUserIds: this.atUserIds, atUserIds: this.atUserIds,
receipt : this.isReceipt, receipt: this.isReceipt,
type: 0 type: 0
} }
// id // id
@ -584,8 +607,6 @@
this.showMinIdx = size > 30 ? size - 30 : 0; this.showMinIdx = size > 30 ? size - 30 : 0;
// //
this.$store.commit("activeChat", options.chatIdx); this.$store.commit("activeChat", options.chatIdx);
//
this.scrollToBottom();
// //
this.readedMessage() this.readedMessage()
// //
@ -598,6 +619,10 @@
// //
this.isReceipt = false; this.isReceipt = false;
}, },
onShow() {
//
this.scrollToBottom();
},
onUnload() { onUnload() {
this.$store.commit("activeChat", -1); this.$store.commit("activeChat", -1);
} }
@ -736,12 +761,12 @@
align-items: center; align-items: center;
.tool-icon { .tool-icon {
padding: 18rpx; padding: 28rpx;
font-size: 80rpx; font-size: 60rpx;
background-color: white; background-color: white;
border-radius: 20%; border-radius: 20%;
&.active{ &.active {
background-color: #ddd; background-color: #ddd;
} }
} }

113
im-uniapp/pages/chat/chat-video.vue

@ -0,0 +1,113 @@
<template>
<view class="page chat-video">
<web-view id="chat-video-wv" @message="onMessage" :src="url"></web-view>
</view>
</template>
<script>
import UNI_APP from '@/.env.js'
export default {
data() {
return {
url: "",
wv: '',
mode: "video",
isHost: true,
friend: {}
}
},
methods: {
onMessage(e) {
this.onWebviewMessage(e.detail.data[0]);
},
onInsertMessage(msgInfo){
let chat = {
type: 'PRIVATE',
targetId: this.friend.id,
showName: this.friend.nickName,
headImage: this.friend.headImageThumb,
};
this.$store.commit("openChat",chat);
this.$store.commit("insertMessage", msgInfo);
},
onWebviewMessage(event) {
console.log("来自webview的消息:" + JSON.stringify(event))
switch (event.key) {
case "WV_READY":
this.initWebView();
break;
case "WV_CLOSE":
uni.navigateBack();
break;
case "INSERT_MESSAGE":
this.onInsertMessage(event.data);
break;
}
},
sendMessageToWebView(key, message) {
// webview100ms
if (!this.wv) {
setTimeout(() => this.sendMessageToWebView(key, message), 100)
return;
}
let event = {
key: key,
data: message
}
// #ifdef APP-PLUS
this.wv.evalJS(`onEvent('${encodeURIComponent(JSON.stringify(event))}')`)
// #endif
// #ifdef H5
this.wv.postMessage(event, '*');
// #endif
},
initWebView() {
// #ifdef APP-PLUS
// APPwebview
this.wv = this.$scope.$getAppWebview().children()[0]
// #endif
// #ifdef H5
// H5webviewiframe
this.wv = document.getElementById('chat-video-wv').contentWindow
// #endif
},
initUrl(){
this.url = "/hybrid/html/index.html";
this.url += "?mode="+this.mode;
this.url += "&isHost="+this.isHost;
this.url += "&baseUrl="+UNI_APP.BASE_URL;
this.url += "&loginInfo="+JSON.stringify(uni.getStorageSync("loginInfo"));
this.url += "&userInfo="+JSON.stringify(this.$store.state.userStore.userInfo);
this.url += "&friend="+JSON.stringify(this.friend);
},
},
onBackPress() {
this.sendMessageToWebView("NAV_BACK",{})
},
onLoad(options) {
uni.$on('WS_RTC', msg => {
// web-view
this.sendMessageToWebView("RTC_MESSAGE", msg);
})
// #ifdef H5
window.onmessage = (e) => {
this.onWebviewMessage(e.data.data.arg);
}
// #endif
// /
this.mode = options.mode;
//
this.isHost = JSON.parse(options.isHost);
//
this.friend = JSON.parse(decodeURIComponent(options.friend));
// url
this.initUrl();
},
onUnload() {
uni.$off('WS_RTC')
}
}
</script>
<style lang="scss" scoped>
</style>

14
im-uniapp/static/icon/iconfont.css

@ -1,6 +1,6 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4272106 */ font-family: "iconfont"; /* Project id 4272106 */
src: url('iconfont.ttf?t=1706027587101') format('truetype'); src: url('iconfont.ttf?t=1710556421604') format('truetype');
} }
.iconfont { .iconfont {
@ -11,6 +11,18 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-video:before {
content: "\e685";
}
.icon-receipt:before { .icon-receipt:before {
content: "\e61a"; content: "\e61a";
} }

BIN
im-uniapp/static/icon/iconfont.ttf

Binary file not shown.

10
im-uniapp/store/chatStore.js

@ -127,6 +127,10 @@ export default {
let message = this.getters.findMessage(chat, msgInfo); let message = this.getters.findMessage(chat, msgInfo);
if(message){ if(message){
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
// 撤回消息需要显示
if(msgInfo.type == MESSAGE_TYPE.RECALL){
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage"); this.commit("saveToStorage");
return; return;
} }
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]"; chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) { } else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content; chat.lastContent = msgInfo.content;
} else if (msgInfo.type == MESSAGE_TYPE.RT_VOICE) {
chat.lastContent = "[语音通话]";
} else if (msgInfo.type == MESSAGE_TYPE.RT_VIDEO) {
chat.lastContent = "[视频通话]";
} }
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
chat.sendNickName = msgInfo.sendNickName; chat.sendNickName = msgInfo.sendNickName;
@ -250,7 +258,7 @@ export default {
chat.lastSendTime = msgInfo.sendTime; chat.lastSendTime = msgInfo.sendTime;
}else{ }else{
chat.lastContent = ""; chat.lastContent = "";
chat.lastSendTime = new Date() chat.lastSendTime = new Date().getTime()
} }
}) })
state.chats.sort((chat1, chat2) => { state.chats.sort((chat1, chat2) => {

Loading…
Cancel
Save