Browse Source

feat:音视频结果加入会话消息

master
xsx 2 years ago
parent
commit
37d1075116
  1. 4
      im-platform/src/main/java/com/bx/implatform/controller/WebrtcController.java
  2. 2
      im-platform/src/main/java/com/bx/implatform/service/IWebrtcService.java
  3. 7
      im-platform/src/main/java/com/bx/implatform/service/impl/WebrtcServiceImpl.java
  4. 17
      im-ui/src/api/enums.js
  5. 10
      im-ui/src/assets/iconfont/iconfont.css
  6. BIN
      im-ui/src/assets/iconfont/iconfont.ttf
  7. 48
      im-ui/src/components/chat/ChatBox.vue
  8. 638
      im-ui/src/components/chat/ChatMessageItem.vue
  9. 312
      im-ui/src/components/chat/ChatPrivateVideo.vue
  10. 155
      im-ui/src/components/chat/ChatVideoAcceptor.vue
  11. 8
      im-ui/src/store/chatStore.js
  12. 28
      im-ui/src/store/uiStore.js
  13. 22
      im-ui/src/store/userStore.js
  14. 2
      im-ui/src/view/Friend.vue
  15. 609
      im-ui/src/view/Home.vue
  16. 2
      im-uniapp/common/enums.js
  17. 66
      im-uniapp/components/chat-message-item/chat-message-item.vue
  18. 20
      im-uniapp/manifest.json
  19. 18
      im-uniapp/pages/chat/chat-box.vue
  20. 113
      im-uniapp/pages/chat/chat-video.vue
  21. 10
      im-uniapp/static/icon/iconfont.css
  22. BIN
      im-uniapp/static/icon/iconfont.ttf
  23. 10
      im-uniapp/store/chatStore.js

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

@ -57,8 +57,8 @@ public class WebrtcController {
@ApiOperation(httpMethod = "POST", value = "挂断")
@PostMapping("/handup")
public Result leave(@RequestParam Long uid) {
webrtcService.leave(uid);
public Result handup(@RequestParam Long uid) {
webrtcService.handup(uid);
return ResultUtils.success();
}

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

@ -22,7 +22,7 @@ public interface IWebrtcService {
void failed(Long uid, String reason);
void leave(Long uid);
void handup(Long uid);
void candidate(Long uid, String candidate);

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

@ -152,8 +152,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(uid);
// 告知其他终端已经会话失败,中止呼叫
sendMessage.setSendToSelf(true);
sendMessage.setSendToSelf(false);
sendMessage.setSendResult(false);
sendMessage.setRecvTerminals(Collections.singletonList(webrtcSession.getCallerTerminal()));
sendMessage.setData(messageInfo);
@ -163,7 +162,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
}
@Override
public void leave(Long uid) {
public void handup(Long uid) {
UserSession session = SessionContext.getSession();
// 查询webrtc会话
WebrtcSession webrtcSession = getWebrtcSession(session.getUserId(), uid);
@ -219,7 +218,7 @@ public class WebrtcServiceImpl implements IWebrtcService {
String key = getSessionKey(userId, uid);
WebrtcSession webrtcSession = (WebrtcSession)redisTemplate.opsForValue().get(key);
if (webrtcSession == null) {
throw new GlobalException("视频通话已结束");
throw new GlobalException("通话已结束");
}
return webrtcSession;
}

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

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

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

@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 3791506 */
src: url('iconfont.ttf?t=1706022894868') format('truetype');
src: url('iconfont.ttf?t=1710567233281') format('truetype');
}
.iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-chat-video:before {
content: "\e73b";
}
.icon-chat-voice:before {
content: "\e633";
}
.icon-ok:before {
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">
<ul>
<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"
:groupMembers="groupMembers" @delete="deleteMessage" @recall="recallMessage">
</chat-message-item>
@ -44,8 +46,11 @@
</div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div>
<div title="视频聊天" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showVideoBox()">
<div title="语音通话" v-show="chat.type == 'PRIVATE'" class="el-icon-phone-outline"
@click="showChatVideo('voice')">
</div>
<div title="视频通话" v-show="chat.type == 'PRIVATE'" class="el-icon-video-camera"
@click="showChatVideo('video')">
</div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div>
@ -133,13 +138,19 @@ export default {
methods: {
moveChatToTop(){
let chatIdx = this.$store.getters.findChatIdx(this.chat);
console.log(chatIdx);
this.$store.commit("moveTop",chatIdx);
},
closeRefBox() {
this.$refs.emoBox.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() {
if (this.$refs.atBox.show) {
this.$refs.atBox.moveDown()
@ -433,11 +444,17 @@ export default {
closeVoiceBox() {
this.showVoice = false;
},
showVideoBox() {
this.$store.commit("showChatPrivateVideoBox", {
showChatVideo(mode) {
let rtcInfo = {
mode: mode,
isHost: true,
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() {
this.showHistory = true;
@ -686,6 +703,12 @@ export default {
},
unreadCount() {
return this.chat.unreadCount;
},
messageSize() {
if (!this.chat || !this.chat.messages) {
return 0;
}
return this.chat.messages.length;
}
},
watch: {
@ -716,9 +739,9 @@ export default {
},
immediate: true
},
unreadCount: {
handler(newCount, oldCount) {
if (newCount > 0) {
messageSize: {
handler(newSize, oldSize) {
if (newSize > oldSize) {
//
this.scrollToBottom();
}
@ -812,7 +835,6 @@ export default {
}
}
.send-content-area {
position: relative;
display: flex;
@ -820,8 +842,6 @@ export default {
height: 100%;
background-color: white !important;
.send-text-area {
box-sizing: border-box;
padding: 5px;

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

@ -1,6 +1,9 @@
<template>
<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">
{{ $date.toTimeText(msgInfo.sendTime) }}
</div>
@ -48,387 +51,424 @@
<div class="chat-msg-voice" v-if="msgInfo.type == $enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
<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
&& 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>
</div>
<div class="chat-receipt" v-show="msgInfo.receipt" @click="onShowReadedBox">
<span v-if="msgInfo.receiptOk" class="icon iconfont icon-ok" title="全体已读"></span>
<span v-else>{{msgInfo.readedCount}}人已读</span>
</div>
</div>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems" @close="rightMenu.show = false"
@select="onSelectMenu"></right-menu>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show = false" @select="onSelectMenu"></right-menu>
<chat-group-readed ref="chatGroupReadedBox" :msgInfo="msgInfo" :groupMembers="groupMembers"></chat-group-readed>
</div>
</template>
<script>
import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default {
name: "messageItem",
components: {
HeadImage,
RightMenu,
ChatGroupReaded
},
props: {
mode: {
type: Number,
default: 1
},
mine: {
type: Boolean,
required: true
},
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
import HeadImage from "../common/HeadImage.vue";
import RightMenu from '../common/RightMenu.vue';
import ChatGroupReaded from './ChatGroupReaded.vue';
export default {
name: "messageItem",
components: {
HeadImage,
RightMenu,
ChatGroupReaded
},
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
props: {
mode: {
type: Number,
default: 1
},
mine: {
type: Boolean,
required: true
},
headImage: {
type: String,
required: true
},
showName: {
type: String,
required: true
},
msgInfo: {
type: Object,
required: true
},
groupMembers: {
type: Array
},
menu: {
type: Boolean,
default: true
}
},
menu: {
type: Boolean,
default: true
}
},
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
data() {
return {
audioPlayState: 'STOP',
rightMenu: {
show: false,
pos: {
x: 0,
y: 0
}
}
}
}
},
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
},
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
},
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play();
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
},
onShowReadedBox() {
let rect = this.$refs.chatMsgBox.getBoundingClientRect();
this.$refs.chatGroupReadedBox.open(rect);
}
},
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
methods: {
onSendFail() {
this.$message.error("该文件已发送失败,目前不支持自动重新发送,建议手动重新发送")
},
showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl);
}
},
onPlayVoice() {
if (!this.audio) {
this.audio = new Audio();
}
this.audio.src = JSON.parse(this.msgInfo.content).url;
this.audio.play();
this.onPlayVoice = 'RUNNING';
},
showRightMenu(e) {
this.rightMenu.pos = {
x: e.x,
y: e.y
};
this.rightMenu.show = "true";
},
onSelectMenu(item) {
this.$emit(item.key.toLowerCase(), this.msgInfo);
},
onShowReadedBox() {
let rect = this.$refs.chatMsgBox.getBoundingClientRect();
this.$refs.chatGroupReadedBox.open(rect);
}
return size + "B";
},
menuItems() {
let items = [];
items.push({
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
computed: {
loading() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
},
fileSize() {
let size = this.data.size;
if (size > 1024 * 1024) {
return Math.round(size / 1024 / 1024) + "M";
}
if (size > 1024) {
return Math.round(size / 1024) + "KB";
}
return size + "B";
},
menuItems() {
let items = [];
items.push({
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
key: 'DELETE',
name: '删除',
icon: 'el-icon-delete'
});
if (this.msgInfo.selfSend && this.msgInfo.id > 0) {
items.push({
key: 'RECALL',
name: '撤回',
icon: 'el-icon-refresh-left'
});
}
return items;
},
isRealtime() {
return this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VOICE ||
this.msgInfo.type == this.$enums.MESSAGE_TYPE.RT_VIDEO
}
return items;
}
}
}
</script>
<style lang="scss">
.chat-msg-item {
.chat-msg-tip {
line-height: 50px;
font-size: 14px;
}
.chat-msg-item {
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 60px;
min-height: 50px;
margin-top: 10px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
.chat-msg-tip {
line-height: 50px;
font-size: 14px;
}
.chat-msg-content {
text-align: left;
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 60px;
min-height: 50px;
margin-top: 10px;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
.head-image {
position: absolute;
width: 40px;
height: 40px;
top: 0;
left: 0;
}
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
color: #333;
font-size: 14px;
line-height: 20px;
.chat-msg-content {
text-align: left;
span {
margin-right: 12px;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
}
.chat-msg-bottom {
display: inline-block;
padding-right: 300px;
.chat-msg-text {
display: block;
position: relative;
line-height: 30px;
margin-top: 3px;
padding: 7px;
background-color: white;
border-radius: 10px;
color: black;
display: block;
font-size: 16px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 1px 1px 1px #c0c0f0;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: white transparent transparent;
overflow: hidden;
border-width: 10px;
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
color: #333;
font-size: 14px;
line-height: 20px;
span {
margin-right: 12px;
}
}
.chat-msg-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.send-image {
min-width: 200px;
min-height: 150px;
max-width: 400px;
max-height: 300px;
border: #dddddd solid 1px;
border: 5px solid #ccc;
border-radius: 6px;
cursor: pointer;
.chat-msg-bottom {
display: inline-block;
padding-right: 300px;
.chat-msg-text {
display: block;
position: relative;
line-height: 26px;
margin-top: 3px;
padding: 7px;
background-color: white;
border-radius: 10px;
color: black;
display: block;
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 1px 1px 1px #c0c0f0;
&:after {
content: "";
position: absolute;
left: -10px;
top: 13px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: white transparent transparent;
overflow: hidden;
border-width: 10px;
}
}
}
.chat-msg-image {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
.chat-msg-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
cursor: pointer;
padding-bottom: 5px;
.chat-file-box {
.send-image {
min-width: 200px;
min-height: 150px;
max-width: 400px;
max-height: 300px;
border: #dddddd solid 1px;
border: 5px solid #ccc;
border-radius: 6px;
cursor: pointer;
}
}
.chat-msg-file {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
align-items: center;
min-height: 80px;
box-shadow: 5px 5px 2px #c0c0c0;
border: #dddddd solid 1px;
border-radius: 6px;
background-color: #eeeeee;
padding: 10px 15px;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
.chat-file-name {
display: inline-block;
min-width: 150px;
max-width: 300px;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
white-space: pre-wrap;
word-break: break-all;
cursor: pointer;
padding-bottom: 5px;
.chat-file-box {
display: flex;
flex-wrap: nowrap;
align-items: center;
min-height: 80px;
box-shadow: 5px 5px 2px #c0c0c0;
border: #dddddd solid 1px;
border-radius: 6px;
background-color: #eeeeee;
padding: 10px 15px;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
.chat-file-name {
display: inline-block;
min-width: 150px;
max-width: 300px;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
white-space: pre-wrap;
word-break: break-all;
}
}
.chat-file-icon {
font-size: 50px;
color: #d42e07;
}
}
.chat-file-icon {
font-size: 50px;
color: #d42e07;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
}
.send-fail {
color: #e60c0c;
font-size: 30px;
.chat-msg-voice {
font-size: 14px;
cursor: pointer;
margin: 0 20px;
}
}
audio {
height: 45px;
padding: 5px 0;
}
}
.chat-msg-voice {
font-size: 14px;
cursor: pointer;
.chat-realtime {
display: flex;
align-items: center;
audio {
height: 45px;
padding: 5px 0;
.iconfont {
cursor: pointer;
font-size: 22px;
padding-right: 8px;
}
}
}
.chat-unread {
font-size: 12px;
color: #f23c0f;
font-weight: 600;
}
.chat-msg-status {
display: block;
.chat-readed {
font-size: 12px;
color: #888;
font-weight: 600;
}
.chat-readed {
font-size: 12px;
color: #888;
font-weight: 600;
}
.chat-receipt{
font-size: 13px;
color: blue;
cursor: pointer;
.chat-unread {
font-size: 12px;
color: #f23c0f;
font-weight: 600;
}
}
.chat-receipt {
font-size: 13px;
color: blue;
cursor: pointer;
.icon-ok {
font-size: 20px;
color: #329432;
.icon-ok {
font-size: 20px;
color: #329432;
}
}
}
}
}
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
.head-image {
left: auto;
right: 0;
}
.head-image {
left: auto;
right: 0;
}
.chat-msg-content {
text-align: right;
.chat-msg-content {
text-align: right;
.chat-msg-top {
flex-direction: row-reverse;
.chat-msg-top {
flex-direction: row-reverse;
span {
margin-left: 12px;
margin-right: 0;
span {
margin-left: 12px;
margin-right: 0;
}
}
}
.chat-msg-bottom {
padding-left: 180px;
padding-right: 0;
.chat-msg-bottom {
padding-left: 180px;
padding-right: 0;
.chat-msg-text {
margin-left: 10px;
background-color: rgb(88, 127, 240);
color: #fff;
vertical-align: top;
box-shadow: 1px 1px 1px #ccc;
&:after {
left: auto;
right: -10px;
border-top-color: rgb(88, 127, 240);
}
}
.chat-msg-text {
margin-left: 10px;
background-color: rgb(88, 127, 240);
color: #fff;
vertical-align: top;
box-shadow: 1px 1px 1px #ccc;
.chat-msg-image {
flex-direction: row-reverse;
}
&:after {
left: auto;
right: -10px;
border-top-color: rgb(88, 127, 240);
.chat-msg-file {
flex-direction: row-reverse;
}
}
.chat-msg-image {
flex-direction: row-reverse;
}
.chat-realtime {
flex-direction: row-reverse;
.chat-msg-file {
flex-direction: row-reverse;
.iconfont {
transform: rotateY(180deg);
}
}
}
}
}
}
}
}
}
</style>

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

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

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

@ -1,10 +1,8 @@
<template>
<div class="chat-video-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<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>
<div class="acceptor-text">
{{friend.nickName}} 请求和您进行视频通话...
{{tip}}
</div>
<div class="acceptor-btn-group">
<div class="icon iconfont icon-phone-accept accept" @click="accpet()" title="接受"></div>
@ -21,70 +19,154 @@
components: {
HeadImage
},
props: {
friend: {
type: Object
}
},
data() {
return {
offer: {},
isShow: false,
audio: new Audio()
}
},
methods: {
accpet() {
let info = {
friend: this.friend,
master: false,
offer: this.offer
}
this.$store.commit("showChatPrivateVideoBox", info);
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.ACCEPTED);
//
this.close();
},
reject() {
this.$http({
url: `/webrtc/private/reject?uid=${this.friend.id}`,
url: `/webrtc/private/reject?uid=${this.rtcInfo.friend.id}`,
method: 'post'
})
//
this.insertMessage("已拒绝");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
failed(reason) {
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'
})
//
this.insertMessage("未接听");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
onCall(msgInfo) {
this.offer = JSON.parse(msgInfo.content);
if (this.$store.state.userStore.state == this.$enums.USER_STATE.BUSY) {
this.failed("对方正忙,暂时无法接听");
onRtcCall(msgInfo, friend, mode) {
console.log("onRtcCall")
//
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;
}
//
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 = setTimeout(() => {
this.failed("对方未接听");
this.failed("对方无应答");
}, 30000)
this.audio.play();
},
onCancel() {
onRtcCancel(msgInfo) {
//
if (msgInfo.sendId != this.rtcInfo.friend.id) {
return;
}
//
this.$message.success("对方取消了呼叫");
//
this.insertMessage("对方已取消");
//
this.$store.commit("setRtcState", this.$enums.RTC_STATE.FREE);
//
this.close();
},
handleMessage(msgInfo) {
if (msgInfo.type == this.$enums.MESSAGE_TYPE.RTC_CALL) {
this.onCall(msgInfo);
onRtcAccept(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) {
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() {
this.timer && clearTimeout(this.timer);
this.audio.pause();
this.$emit("close");
this.isShow = false;
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
@ -92,10 +174,18 @@
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() {
//
this.initAudio();
}
}
</script>
@ -131,6 +221,7 @@
font-size: 60px;
cursor: pointer;
border-radius: 50%;
&.accept {
color: green;
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);
if (message) {
Object.assign(message, msgInfo);
// 撤回消息需要显示
if (msgInfo.type == MESSAGE_TYPE.RECALL) {
chat.lastContent = msgInfo.content;
}
this.commit("saveToStorage");
return;
}
@ -140,6 +144,10 @@ export default {
chat.lastContent = "[语音]";
} else if (msgInfo.type == MESSAGE_TYPE.TEXT || msgInfo.type == MESSAGE_TYPE.RECALL) {
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.sendNickName = msgInfo.sendNickName;

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

@ -11,18 +11,7 @@ export default {
fullImage: { // 全屏大图
show: false,
url: ""
},
chatPrivateVideo:{ // 私人视频聊天
show: false,
master: false, // 是否房主
friend:{},
offer:{} // 对方发起带过过来的sdp信息
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
}
},
mutations: {
showUserInfoBox(state,user){
@ -45,23 +34,6 @@ export default {
},
closeFullImageBox(state){
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 {RTC_STATE} from "../api/enums.js"
export default {
state: {
userInfo: {
},
state: USER_STATE.FREE
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
},
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
},
setUserState(state, userState) {
state.state = userState;
setRtcInfo(state, rtcInfo ){
state.rtcInfo = rtcInfo;
},
setRtcState(state,rtcState){
state.rtcInfo.state = rtcState;
},
clear(state){
state.userInfo = {};
state.state = USER_STATE.FREE;
state.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
}
},
actions:{

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

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

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

@ -40,354 +40,353 @@
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show"
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master"
: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>
<chat-private-video ref="privateVideo"></chat-private-video>
<chat-video-acceptor ref="videoAcceptor"></chat-video-acceptor>
</el-container>
</template>
<script>
import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue';
import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue';
import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue';
import ChatPrivateVideo from '../components/chat/ChatPrivateVideo.vue';
import ChatVideoAcceptor from '../components/chat/ChatVideoAcceptor.vue';
export default {
components: {
HeadImage,
Setting,
UserInfo,
FullImage,
ChatPrivateVideo,
ChatVideoAcceptor
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date() - 1000
}
},
methods: {
init() {
this.$store.dispatch("load").then(() => {
// ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(() => {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
});
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// ws
this.$wsApi.close(3000)
// 线
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", {
confirmButtonText: '确定',
callback: action => {
location.href = "/";
}
});
export default {
components: {
HeadImage,
Setting,
UserInfo,
FullImage,
ChatPrivateVideo,
ChatVideoAcceptor
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date().getTime() - 1000
}
},
methods: {
init() {
this.$store.dispatch("load").then(() => {
// ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(() => {
// 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId);
});
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// ws
this.$wsApi.close(3000)
// 线
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", {
confirmButtonText: '确定',
callback: action => {
location.href = "/";
}
});
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
}
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
}
});
this.$wsApi.onClose((e) => {
console.log(e);
if (e.code != 3000) {
// 线
this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
"accessToken"));
}
});
}).catch((e) => {
console.log("初始化失败", e);
})
},
pullPrivateOfflineMessage(minId) {
this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'get'
});
this.$wsApi.onClose((e) => {
console.log(e);
if (e.code != 3000) {
// 线
this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
}
},
pullGroupOfflineMessage(minId) {
this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'get'
});
}).catch((e) => {
console.log("初始化失败", e);
})
},
pullPrivateOfflineMessage(minId) {
this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'get'
});
},
pullGroupOfflineMessage(minId) {
this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'get'
});
},
handlePrivateMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
this.$store.commit("resetUnreadCount", {
type: 'PRIVATE',
targetId: msg.recvId
},
handlePrivateMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
this.$store.commit("resetUnreadCount", {
type: 'PRIVATE',
targetId: msg.recvId
})
return;
}
// ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", {
friendId: msg.sendId
})
return;
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
return;
}
// ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", { friendId: msg.sendId })
return;
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
this.$store.commit("showVideoAcceptorBox", friend);
this.$refs.videoAcceptor.handleMessage(msg)
} else {
this.$refs.videoAcceptor.close()
this.$refs.privateVideo.handleMessage(msg)
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
let rtcInfo = this.$store.state.userStore.rtcInfo;
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO ||
rtcInfo.state == this.$enums.RTC_STATE.FREE ||
rtcInfo.state == this.$enums.RTC_STATE.WAIT_ACCEPT) {
this.$refs.videoAcceptor.onRTCMessage(msg,friend)
} else {
this.$refs.privateVideo.onRTCMessage(msg)
}
return;
}
return;
}
let chatInfo = {
type: 'PRIVATE',
targetId: friend.id,
showName: friend.nickName,
headImage: friend.headImage
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
handleGroupMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
//
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
type: 'PRIVATE',
targetId: friend.id,
showName: friend.nickName,
headImage: friend.headImage
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
this.$store.commit("resetUnreadCount", chatInfo)
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
//
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount,
receiptOk: msg.receiptOk
},
handleGroupMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADDING) {
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content))
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
//
let chatInfo = {
type: 'GROUP',
targetId: msg.groupId
}
this.$store.commit("resetUnreadCount", chatInfo)
return;
}
//
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
//
let msgInfo = {
id: msg.id,
groupId: msg.groupId,
readedCount: msg.readedCount,
receiptOk: msg.receiptOk
};
this.$store.commit("updateMessage", msgInfo)
return;
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.remark,
headImage: group.headImageThumb
};
this.$store.commit("updateMessage", msgInfo)
return;
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
onExit() {
this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken");
location.href = "/";
},
playAudioTip() {
if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date().getTime();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
}
},
showSetting() {
this.showSettingDialog = true;
},
closeSetting() {
this.showSettingDialog = false;
},
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
this.$http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
this.$store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
this.$http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
this.$store.commit("addGroup", group);
})
}
});
}
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.remark,
headImage: group.headImageThumb
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
computed: {
uiStore() {
return this.$store.state.uiStore;
},
unreadCount() {
let unreadCount = 0;
let chats = this.$store.state.chatStore.chats;
chats.forEach((chat) => {
unreadCount += chat.unreadCount
});
return unreadCount;
}
},
onExit() {
this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken");
location.href = "/";
},
playAudioTip() {
if (new Date() - this.lastPlayAudioTime > 1000) {
this.lastPlayAudioTime = new Date();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
watch: {
unreadCount: {
handler(newCount, oldCount) {
let tip = newCount > 0 ? `${newCount}条未读` : "";
this.$elm.setTitleTip(tip);
},
immediate: true
}
},
showSetting() {
this.showSettingDialog = true;
},
closeSetting() {
this.showSettingDialog = false;
},
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
this.$http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
this.$store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
this.$http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
this.$store.commit("addGroup", group);
})
}
});
}
},
computed: {
uiStore() {
return this.$store.state.uiStore;
mounted() {
this.init();
},
unreadCount() {
let unreadCount = 0;
let chats = this.$store.state.chatStore.chats;
chats.forEach((chat) => {
unreadCount += chat.unreadCount
});
return unreadCount;
unmounted() {
this.$wsApi.close();
}
},
watch: {
unreadCount: {
handler(newCount, oldCount) {
let tip = newCount > 0 ? `${newCount}条未读` : "";
this.$elm.setTitleTip(tip);
},
immediate: true
}
},
mounted() {
this.init();
},
unmounted() {
this.$wsApi.close();
}
}
</script>
<style scoped lang="scss">
.navi-bar {
background: #333333;
padding: 10px;
padding-top: 50px;
.navi-bar {
background: #333333;
padding: 10px;
padding-top: 50px;
.el-menu {
border: none;
flex: 1;
.el-menu {
border: none;
flex: 1;
.el-menu-item {
margin: 25px 0;
.el-menu-item {
margin: 25px 0;
.router-link-exact-active span {
color: white !important;
}
.router-link-exact-active span {
color: white !important;
}
span {
font-size: 24px !important;
color: #aaaaaa;
span {
font-size: 24px !important;
color: #aaaaaa;
&:hover {
color: white !important;
&:hover {
color: white !important;
}
}
}
.unread-text {
position: absolute;
line-height: 20px;
background-color: #f56c6c;
left: 36px;
top: 7px;
color: white;
border-radius: 30px;
padding: 0 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
.unread-text {
position: absolute;
line-height: 20px;
background-color: #f56c6c;
left: 36px;
top: 7px;
color: white;
border-radius: 30px;
padding: 0 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
}
}
.exit-box {
position: absolute;
width: 60px;
bottom: 40px;
color: #aaaaaa;
font-size: 24px;
text-align: center;
cursor: pointer;
.exit-box {
position: absolute;
width: 60px;
bottom: 40px;
color: #aaaaaa;
font-size: 24px;
text-align: center;
cursor: pointer;
&:hover {
color: white !important;
&:hover {
color: white !important;
}
}
}
}
.content-box {
padding: 0;
background-color: #E9EEF3;
color: #333;
text-align: center;
.content-box {
padding: 0;
background-color: #E9EEF3;
color: #333;
text-align: center;
}
}
</style>

2
im-uniapp/common/enums.js

@ -5,6 +5,8 @@ const MESSAGE_TYPE = {
FILE:2,
AUDIO:3,
VIDEO:4,
RT_VOICE:5,
RT_VIDEO:6,
RECALL:10,
READED:11,
RECEIPT:12,

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

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

20
im-uniapp/manifest.json

@ -2,8 +2,8 @@
"name" : "盒子IM",
"appid" : "__UNI__69DD57A",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"versionName" : "1.0.6",
"versionCode" : 106,
"transformPx" : false,
/* 5+App */
"app-plus" : {
@ -18,7 +18,9 @@
},
/* */
"modules" : {
"Camera" : {}
"Camera" : {},
"Record" : {},
"Bluetooth" : {}
},
/* */
"distribute" : {
@ -39,14 +41,20 @@
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<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" ],
"minSdkVersion" : 21
},
/* ios */
"ios" : {
"dSYMs" : false
"dSYMs" : false,
"privacyDescription" : {
"NSMicrophoneUsageDescription" : "",
"NSCameraUsageDescription" : ""
}
},
/* SDK */
"sdkConfigs" : {
@ -117,3 +125,5 @@
}
}
}
/* ios *//* SDK */

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

@ -10,6 +10,7 @@
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)"
@click="onClickMessage(msgInfo)"
:showName="showName(msgInfo)" @recall="onRecallMessage" @delete="onDeleteMessage"
@longPressHead="onLongPressHead(msgInfo)" @download="onDownloadFile" :id="'chat-item-'+idx"
:msgInfo="msgInfo" :groupMembers="groupMembers">
@ -76,11 +77,11 @@
<view class="tool-icon iconfont icon-receipt" :class="isReceipt?'active':''"></view>
<view class="tool-name">回执消息</view>
</view>
<view class="chat-tools-item" @click="onVideoCall()">
<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 class="chat-tools-item" @click="onVoiceCall()">
<view v-if="chat.type == 'PRIVATE'" class="chat-tools-item" @click="onVoiceCall()">
<view class="tool-icon iconfont icon-call"></view>
<view class="tool-name">语音通话</view>
</view>
@ -125,6 +126,13 @@
icon: "none"
})
},
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({
@ -599,8 +607,6 @@
this.showMinIdx = size > 30 ? size - 30 : 0;
//
this.$store.commit("activeChat", options.chatIdx);
//
this.scrollToBottom();
//
this.readedMessage()
//
@ -613,6 +619,10 @@
//
this.isReceipt = false;
},
onShow() {
//
this.scrollToBottom();
},
onUnload() {
this.$store.commit("activeChat", -1);
}

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>

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

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

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

Loading…
Cancel
Save