Browse Source

ui优化

master
libaogang 1 year ago
parent
commit
0444c71e3c
  1. 24
      im-web/package.json
  2. 117
      im-web/src/App.vue
  3. 2
      im-web/src/api/emotion.js
  4. BIN
      im-web/src/assets/image/online_app.png
  5. BIN
      im-web/src/assets/image/online_web.png
  6. 112
      im-web/src/assets/style/element.scss
  7. 43
      im-web/src/assets/style/global.css
  8. 91
      im-web/src/assets/style/im.scss
  9. 6
      im-web/src/assets/style/thems.scss
  10. 8
      im-web/src/components/chat/ChatAtBox.vue
  11. 68
      im-web/src/components/chat/ChatBox.vue
  12. 13
      im-web/src/components/chat/ChatGroupMember.vue
  13. 4
      im-web/src/components/chat/ChatGroupReaded.vue
  14. 422
      im-web/src/components/chat/ChatGroupSide.vue
  15. 9
      im-web/src/components/chat/ChatInput.vue
  16. 94
      im-web/src/components/chat/ChatItem.vue
  17. 72
      im-web/src/components/chat/ChatMessageItem.vue
  18. 43
      im-web/src/components/common/Emotion.vue
  19. 24
      im-web/src/components/common/FullImage.vue
  20. 32
      im-web/src/components/common/HeadImage.vue
  21. 24
      im-web/src/components/common/RightMenu.vue
  22. 16
      im-web/src/components/common/UserInfo.vue
  23. 85
      im-web/src/components/friend/AddFriend.vue
  24. 49
      im-web/src/components/friend/FriendItem.vue
  25. 308
      im-web/src/components/group/AddGroupMember.vue
  26. 24
      im-web/src/components/group/GroupItem.vue
  27. 3
      im-web/src/components/group/GroupMember.vue
  28. 7
      im-web/src/components/group/GroupMemberItem.vue
  29. 17
      im-web/src/components/group/GroupMemberSelector.vue
  30. 10
      im-web/src/components/rtc/RtcPrivateAcceptor.vue
  31. 964
      im-web/src/components/rtc/RtcPrivateVideo.vue
  32. 254
      im-web/src/components/setting/Setting.vue
  33. 4
      im-web/src/main.js
  34. 177
      im-web/src/view/Chat.vue
  35. 54
      im-web/src/view/Friend.vue
  36. 770
      im-web/src/view/Group.vue
  37. 904
      im-web/src/view/Home.vue

24
im-web/package.json

@ -8,18 +8,18 @@
"lint": "vue-cli-service lint"
},
"dependencies": {
"axios": "^1.1.3",
"core-js": "^3.6.5",
"element-ui": "^2.15.10",
"js-audio-recorder": "^1.0.7",
"localforage": "^1.10.0",
"sass": "^1.47.0",
"sass-loader": "^10.1.1",
"vue": "^2.6.11",
"vue-axios": "^3.5.0",
"vue-router": "^3.3.3",
"vuex": "^3.6.2",
"vuex-persist": "^3.1.3"
"axios": "1.7.7",
"core-js": "3.38.1",
"element-ui": "2.15.14",
"js-audio-recorder": "1.0.7",
"localforage": "1.10.0",
"sass": "1.32.12",
"sass-loader": "10.1.1",
"vue": "2.7.16",
"vue-axios": "3.5.2",
"vue-router": "3.6.5",
"vuex": "3.6.2",
"vuex-persist": "3.1.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.12",

117
im-web/src/App.vue

@ -1,115 +1,26 @@
<template>
<div id="app">
<router-view></router-view>
</div>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {}
}
export default {
name: 'App',
components: {}
}
</script>
<style lang="scss">
@import './assets/style/global.css';
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: absolute;
height: 100%;
width: 100%;
}
.el-message {
z-index: 99999999 !important;
}
.el-scrollbar__thumb {
background-color: #A0A8AF !important;
}
.el-dialog {
border-radius: 8px !important;
overflow: hidden !important;
}
.el-dialog__header {
background-color: #5870e6 !important;
}
.el-dialog__title {
color: #f8f8f8 !important;
}
.el-dialog__close {
color: white !important;
font-size: 20px;
}
.el-checkbox__inner {
border-radius: 50% !important;
}
.el-input__inner {
border-radius: 5px !important;
border: #587FF0 1px solid !important;
}
.el-textarea__inner{
border-radius: 5px !important;
border: #587FF0 1px solid !important;
}
.el-icon-search {
color:#587FF0 !important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: absolute;
height: 100%;
width: 100%;
color: var(--im-text-color);
font-family: var(--im-font-family);
}
.el-button--primary {
background-color: #687Ff0 !important;
border-color: #687Ff0 !important;
}
.el-button--success {
background-color: #4cae1b !important;
border-color: #4cae1b !important;
}
.el-button--danger {
background-color: #ea4949 !important;
border-color: #ea4949 !important;
}
.el-button {
padding: 8px 15px !important;
}
.el-checkbox {
display: flex;
align-items: center;
//
.el-checkbox__inner {
width: 20px;
height: 20px;
//
&::after {
height: 12px;
left: 7px;
}
}
//
.el-checkbox__input.is-checked+.el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
}
</style>

2
im-web/src/api/emotion.js

@ -19,7 +19,7 @@ let textToImg = (emoText) => {
return emoText;
}
let url = require(`@/assets/emoji/${idx}.gif`);
return `<img src="${url}" style="width:35px;height:35px;vertical-align:bottom;"/>`
return `<img src="${url}" style="width:32px;height:32px;vertical-align:bottom;"/>`
}
let textToUrl = (emoText) => {

BIN
im-web/src/assets/image/online_app.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

BIN
im-web/src/assets/image/online_web.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

112
im-web/src/assets/style/element.scss

@ -0,0 +1,112 @@
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
// 文字
$--font-family: Microsoft YaHei, 'Avenir', Helvetica, Arial, sans-serif;
@import "thems";
@import "~element-ui/packages/theme-chalk/src/index";
.el-message {
z-index: 99999999 !important;
background: #fff !important;
box-shadow: 0 4px 12px 0 rgb(0 0 0 / 15%);
border: none !important;
min-width: unset !important;
border-radius: 3px !important;
padding: 14px 18px 14px 16px !important;
.el-message__content {
color: #000 !important;
}
}
.el-scrollbar__thumb {
background-color: #A0A8AF !important;
}
.el-dialog__title {
font-size: var(--im-font-size-larger);
color: var(--im-text-color);
}
.el-dialog__header {
padding: 12px 18px !important;
}
.el-dialog__headerbtn {
top: 15px;
right: 20px;
font-size: 18px;
}
.el-checkbox__inner {
border-radius: 50% !important;
}
.el-button--success {
//background-color: #688758 !important;
//border-color: #4cae1b !important;
}
.el-button--danger {
//background-color: #ea4949 !important;
//border-color: #ea4949 !important;
}
.el-button {
padding: 8px 15px !important;
}
.el-checkbox {
display: flex;
align-items: center;
//修改选中框的大小
.el-checkbox__inner {
width: 16px;
height: 16px;
//修改选中框中的对勾的大小和位置
&::after {
height: 7px;
left: 5px;
top: 2px;
}
}
// 修改点击文字颜色不变
.el-checkbox__input.is-checked + .el-checkbox__label {
color: #333333;
}
.el-checkbox__label {
line-height: 20px;
padding-left: 8px;
}
}
.el-form-item {
margin-bottom: 15px !important;
}
.el-input--small {
font-size: $--font-size-base;
}
.el-input__inner {
padding: 0 10px;
}
.el-textarea__inner {
padding: 5px 10px;
font-family: $--font-family;
}
.el-tag--mini {
height: 18px;
padding: 0 2px;
line-height: 16px;
border-radius: 2px;
}

43
im-web/src/assets/style/global.css

@ -1,43 +0,0 @@
@charset "UTF-8";
html {
height: 100%;
overflow: hidden;
}
body {
height: 100%;
margin: 0;
overflow: hidden;
}
section {
height: 100%;
}
.el-dialog__body{
padding: 10px 15px !important;
}
::-webkit-scrollbar {
width: 6px;
height: 1px;
}
::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 2px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
background: #535353;
}
::-webkit-scrollbar-track {
/*滚动条里面轨道*/
-webkit-box-shadow: inset 0 0 5px transparent;
border-radius: 2px;
background: #ededed;
}
/*# sourceMappingURL=v-im.cssss.map */

91
im-web/src/assets/style/im.scss

@ -0,0 +1,91 @@
@charset "UTF-8";
@import "element";
// im全局样式变量
:root {
// 主色
--im-color-primary: #{$--color-primary};
--im-color-primary-light-1: #{$--color-primary-light-1};
--im-color-primary-light-2: #{$--color-primary-light-2};
--im-color-primary-light-3: #{$--color-primary-light-3};
--im-color-primary-light-4: #{$--color-primary-light-4};
--im-color-primary-light-5: #{$--color-primary-light-5};
--im-color-primary-light-6: #{$--color-primary-light-6};
--im-color-primary-light-7: #{$--color-primary-light-7};
--im-color-primary-light-8: #{$--color-primary-light-8};
--im-color-primary-light-9: #{$--color-primary-light-9};
--im-color-sucess: #{$--color-success};
--im-color-warning: #{$--color-warning};
--im-color-danger: #{$--color-danger};
--im-color-info: #{$--color-info};
// 文字颜色
--im-text-color: #{$--color-text-regular};
--im-text-color-light: #999999;
--im-text-color-lighter: #C0C4CC;
// 文字大小
--im-font-size: #{$--font-size-base};
--im-font-size-small: #{$--font-size-small};
--im-font-size-smaller: #{$--font-size-extra-small};
--im-font-size-large: #{$--font-size-medium};
--im-font-size-larger: #{$--font-size-large};
--im-font-family: #{$--font-family};
// 边框颜色
--im-border: 1px solid #EBEEF5;
// 阴影
--im-box-shadow: #{$--box-shadow-base};
--im-box-shadow-light: #{$--box-shadow-light};
--im-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, .12);
--im-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, .08), 0px 12px 32px rgba(0, 0, 0, .12), 0px 8px 16px -8px rgba(0, 0, 0, .16);
// 背景色
--im-background: #F3F3F3;
--im-background-active: #F1F1F1;
--im-background-active-dark: #E9E9E9;
}
html {
height: 100%;
overflow: hidden;
}
body {
height: 100%;
margin: 0;
overflow: hidden;
}
section {
height: 100%;
}
.el-dialog__body {
padding: 10px 20px !important;
}
// 滚动条样式
::-webkit-scrollbar {
width: 8px;
height: 1px;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: hsla(0, 0%, 73%, .5);
}
::-webkit-scrollbar-track {
border-radius: 4px;
}
.search-input {
.el-input__inner {
border: unset !important;
}
}

6
im-web/src/assets/style/thems.scss

@ -0,0 +1,6 @@
// 主题色
$--color-primary: #2830d3;
//$--color-primary: #687ff0;
//$--color-primary: #096bff;
$--font-size-base: 14px;
$--color-text-regular: #000000;

8
im-web/src/components/chat/ChatAtBox.vue

@ -123,9 +123,9 @@
position: fixed;
width: 200px;
height: 300px;
border: 1px solid #53a0e79c;
border-radius: 5px;
background-color: #f5f5f5;
box-shadow: 0px 0px 10px #ccc;
//border: 1px solid #53a0e79c;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
}
</style>

68
im-web/src/components/chat/ChatBox.vue

@ -1,7 +1,7 @@
<template>
<div class="chat-box" @click="closeRefBox()" @mousemove="readedMessage()">
<el-container>
<el-header height="56px">
<el-header height="50px">
<span>{{ title }}</span>
<span title="群聊信息" v-show="this.chat.type == 'GROUP'" class="btn-side el-icon-more"
@click="showSide = !showSide"></span>
@ -23,7 +23,7 @@
</ul>
</div>
</el-main>
<el-footer height="240px" class="im-chat-footer">
<el-footer height="220px" class="im-chat-footer">
<div class="chat-tool-bar">
<div title="表情" class="icon iconfont icon-emoji" ref="emotion"
@click.stop="showEmotionBox()">
@ -61,12 +61,12 @@
<ChatInput :ownerId="group.ownerId" ref="chatInputEditor" :group-members="groupMembers"
@submit="sendMessage" />
<div class="send-btn-area">
<el-button type="primary" size="small" @click="notifySend()">发送</el-button>
<el-button type="primary" icon="el-icon-s-promotion" @click="notifySend()">发送</el-button>
</div>
</div>
</el-footer>
</el-container>
<el-aside class="chat-group-side-box" width="300px" v-if="showSide">
<el-aside class="chat-group-side-box" width="260px" v-if="showSide">
<chat-group-side :group="group" :groupMembers="groupMembers" @reload="loadGroup(group.id)">
</chat-group-side>
</el-aside>
@ -667,29 +667,30 @@
.chat-box {
position: relative;
width: 100%;
background: #f8f8f8;
border: #dddddd solid 1px;
background: #fff;
.el-header {
padding: 3px;
background-color: white;
display: flex;
justify-content: space-between;
padding: 0 12px;
line-height: 50px;
font-size: 20px;
font-weight: 600;
border-bottom: 1px #ddd solid;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
.btn-side {
position: absolute;
right: 20px;
line-height: 50px;
font-size: 25px;
font-size: 20px;
cursor: pointer;
color: var(--im-text-color-light);
}
}
.im-chat-main {
padding: 0;
background-color: white;
background-color: #fff;
.im-chat-box {
>ul {
@ -711,36 +712,34 @@
display: flex;
position: relative;
width: 100%;
height: 40px;
height: 36px;
text-align: left;
box-sizing: border-box;
border-top: #ccc solid 1px;
padding: 2px;
background-color: #f8faff;
border-top: var(--im-border);
padding: 4px 2px 2px 8px;
>div {
> div {
font-size: 22px;
cursor: pointer;
color: black;
line-height: 30px;
width: 30px;
height: 30px;
text-align: center;
border-radius: 3px;
margin: 3px 5px;
color: #0f46ae;
&:hover {
font-weight: 600;
color: #042259;
}
border-radius: 2px;
margin-right: 8px;
color: #999;
transition: 0.3s;
&.chat-tool-active {
color: white;
background-color: #195ee2;
font-weight: 600;
color: var(--im-color-primary);
background-color: #ddd;
}
}
> div:hover {
color: #333;
}
}
.send-content-area {
@ -757,7 +756,6 @@
flex: 1;
resize: none;
font-size: 16px;
color: black;
outline: none;
text-align: left;
@ -820,15 +818,17 @@
.send-btn-area {
padding: 10px;
position: absolute;
bottom: 0;
right: 0;
bottom: 4px;
right: 6px;
}
}
}
.chat-group-side-box {
border: #dddddd solid 1px;
animation: rtl-drawer-in .3s 1ms;
border-left: var(--im-border);
//animation: rtl-drawer-in .3s 1ms;
}
}
</style>

13
im-web/src/components/chat/ChatGroupMember.vue

@ -42,30 +42,19 @@ export default {
<style lang="scss">
.chat-group-member {
display: flex;
margin-bottom: 1px;
position: relative;
padding: 0 5px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
box-sizing: border-box;
&:hover {
background-color: #F8FAFF;
}
&.active {
background-color: #E8F2FF;
}
.member-name {
padding-left: 10px;
height: 100%;
text-align: left;
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 600;
font-size: var(--im-font-size);
}
}
</style>

4
im-web/src/components/chat/ChatGroupReaded.vue

@ -130,17 +130,13 @@ export default {
.chat-group-readed {
position: fixed;
box-shadow: 0px 0px 10px #ccc;
width: 300px;
background-color: #fafafa;
border-radius: 8px;
.scroll-box {
height: 400px;
}
.arrow-left {
position: absolute;
left: -15px;
width: 0;

422
im-web/src/components/chat/ChatGroupSide.vue

@ -1,206 +1,232 @@
<template>
<div class="chat-group-side">
<div v-show="!group.quit" class="group-side-search">
<el-input placeholder="搜索群成员" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<el-scrollbar class="group-side-scrollbar">
<div v-show="!group.quit" class="group-side-member-list">
<div class="group-side-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers" @reload="$emit('reload')"
@close="showAddGroupMember=false"></add-group-member>
</div>
<div v-for="(member) in groupMembers" :key="member.id">
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)" :member="member"
:showDel="false"></group-member>
</div>
</div>
<el-divider v-if="!group.quit" content-position="center"></el-divider>
<el-form labelPosition="top" class="group-side-form" :model="group">
<el-form-item label="群聊名称">
<el-input v-model="group.name" disabled maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="group.notice" disabled type="textarea" maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing" :placeholder="group.name" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName" ></el-input>
</el-form-item>
<div v-show="!group.quit" class="btn-group">
<el-button v-show="editing" type="success" @click="onSaveGroup()">提交</el-button>
<el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div>
</el-form>
</el-scrollbar>
</div>
<div class="chat-group-side">
<div v-show="!group.quit" class="group-side-search">
<el-input placeholder="搜索群成员" v-model="searchText" size="small">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<div class="group-side-scrollbar">
<div v-show="!group.quit" class="group-side-member-list">
<div class="group-side-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers"
@reload="$emit('reload')"
@close="showAddGroupMember=false"></add-group-member>
</div>
<div v-for="(member) in groupMembers" :key="member.id">
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)"
:member="member"
:showDel="false"></group-member>
</div>
</div>
<el-divider v-if="!group.quit" content-position="center"></el-divider>
<el-form labelPosition="top" class="group-side-form" :model="group" size="small">
<el-form-item label="群聊名称">
<el-input v-model="group.name" disabled maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="group.notice" disabled type="textarea" maxlength="1024"></el-input>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="group.remarkGroupName" :disabled="!editing"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20"
></el-input>
</el-form-item>
<div v-show="!group.quit" class="btn-group">
<el-button v-if="editing" type="success" @click="onSaveGroup()">保存</el-button>
<el-button v-if="!editing" type="primary" @click="editing=!editing">编辑</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
</div>
</el-form>
</div>
</div>
</template>
<script>
import AddGroupMember from '../group/AddGroupMember.vue';
import GroupMember from '../group/GroupMember.vue';
export default {
name: "chatGroupSide",
components: {
AddGroupMember,
GroupMember
},
data() {
return {
searchText: "",
editing: false,
showAddGroupMember: false
}
},
props: {
group: {
type: Object
},
groupMembers: {
type: Array
}
},
methods: {
onClose() {
this.$emit('close');
},
loadGroupMembers() {
this.$http({
url: `/group/members/${this.group.id}`,
method: "get"
}).then((members) => {
this.groupMembers = members;
})
},
onSaveGroup() {
let vo = this.group;
this.$http({
url: "/group/modify",
method: "put",
data: vo
}).then((group) => {
this.$store.commit("updateGroup", group);
this.$emit('reload');
this.$message.success("修改成功");
})
},
onQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/quit/${this.group.id}`,
method: 'delete'
}).then(() => {
this.$store.commit("removeGroup", this.group.id);
this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.group.id);
});
})
},
},
computed: {
ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId);
return member && member.showNickName;
},
isOwner() {
return this.group.ownerId == this.$store.state.userStore.userInfo.id;
}
}
}
import AddGroupMember from '../group/AddGroupMember.vue';
import GroupMember from '../group/GroupMember.vue';
export default {
name: "chatGroupSide",
components: {
AddGroupMember,
GroupMember
},
data() {
return {
searchText: "",
editing: false,
showAddGroupMember: false
}
},
props: {
group: {
type: Object
},
groupMembers: {
type: Array
}
},
methods: {
onClose() {
this.$emit('close');
},
loadGroupMembers() {
this.$http({
url: `/group/members/${this.group.id}`,
method: "get"
}).then((members) => {
this.groupMembers = members;
})
},
onSaveGroup() {
let vo = this.group;
this.$http({
url: "/group/modify",
method: "put",
data: vo
}).then((group) => {
this.editing = !this.editing
this.$store.commit("updateGroup", group);
this.$emit('reload');
this.$message.success("修改成功");
})
},
onQuit() {
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/quit/${this.group.id}`,
method: 'delete'
}).then(() => {
this.$store.commit("removeGroup", this.group.id);
this.$store.commit("activeGroup", -1);
this.$store.commit("removeGroupChat", this.group.id);
});
})
},
},
computed: {
ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId);
return member && member.showNickName;
},
isOwner() {
return this.group.ownerId == this.$store.state.userStore.userInfo.id;
}
}
}
</script>
<style lang="scss">
.chat-group-side {
position: relative;
.group-side-member-list {
padding: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 16px;
text-align: center;
.group-side-member {
margin-left: 15px;
}
.group-side-invite {
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
margin-left: 15px;
.invite-member-btn {
width: 100%;
height: 50px;
line-height: 50px;
border: #cccccc solid 1px;
font-size: 25px;
cursor: pointer;
box-sizing: border-box;
&:hover {
border: #aaaaaa solid 1px;
}
}
.invite-member-text {
font-size: 16px;
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
}
.group-side-form {
text-align: left;
padding: 10px;
height: 30%;
.el-form-item {
margin-bottom: 12px;
.el-form-item__label {
padding: 0;
line-height: 30px;
}
.el-textarea__inner {
min-height: 100px !important;
}
}
.btn-group {
text-align: center;
}
}
}
.chat-group-side {
position: relative;
.group-side-search {
padding: 10px;
}
.group-side-scrollbar {
overflow: auto;
}
.el-divider--horizontal {
margin: 0;
}
.el-form-item {
margin-bottom: 0px !important;
}
.group-side-member-list {
padding: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 14px;
text-align: center;
.group-side-member {
margin-left: 5px;
}
.group-side-invite {
display: flex;
flex-direction: column;
align-items: center;
width: 50px;
margin-left: 5px;
.invite-member-btn {
width: 38px;
height: 38px;
line-height: 38px;
border: var(--im-border);
font-size: 14px;
cursor: pointer;
box-sizing: border-box;
&:hover {
border: #aaaaaa solid 1px;
}
}
.invite-member-text {
font-size: 12px;
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
}
.group-side-form {
text-align: left;
padding: 10px;
height: 30%;
.el-form-item {
margin-bottom: 12px;
.el-form-item__label {
padding: 0;
line-height: 30px;
}
.el-textarea__inner {
min-height: 100px !important;
}
}
.el-input__inner, .el-textarea__inner {
color: var(--im-text-color) !important;
}
.btn-group {
text-align: center;
margin-top: 12px;
}
}
}
</style>

9
im-web/src/components/chat/ChatInput.vue

@ -482,10 +482,10 @@
bottom: 0;
outline: none;
padding: 5px;
line-height: 30px;
font-size: 16px;
line-height: 1.5;
font-size: var(--im-font-size);
text-align: left;
overflow-y: scroll;
overflow-y: auto;
// bug
>div:before {
@ -544,15 +544,12 @@
.file-size {
font-size: 14px;
font-weight: 600;
color: black;
}
}
}
.chat-at-user {
color: #00f;
font-weight: 600;
border-radius: 3px;
}
}

94
im-web/src/components/chat/ChatItem.vue

@ -1,20 +1,20 @@
<template>
<div class="chat-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="chat-left">
<head-image :url="chat.headImage" :name="chat.showName" :size="45"
:id="chat.type=='PRIVATE'?chat.targetId:0"></head-image>
<head-image :url="chat.headImage" :name="chat.showName" :size="42"
:id="chat.type=='PRIVATE'?chat.targetId:0" :isShowUserInfo="false"></head-image>
<div v-show="chat.unreadCount>0" class="unread-text">{{chat.unreadCount}}</div>
</div>
<div class="chat-right">
<div class="chat-name">
<div class="chat-name-text">
<div>{{chat.showName}}</div>
<el-tag v-if="chat.type=='GROUP'" size="mini" effect="dark"></el-tag>
</div>
<div class="chat-name-text">
<div>{{chat.showName}}</div>
<el-tag v-if="chat.type=='GROUP'" size="mini" effect="dark"></el-tag>
</div>
<div class="chat-time-text">{{showTime}}</div>
<div class="chat-time-text">{{showTime}}</div>
</div>
</div>
<div class="chat-content">
<div class="chat-at-text">{{atText}}</div>
<div class="chat-send-name" v-show="isShowSendName">{{chat.sendNickName+':&nbsp;'}}</div>
@ -112,34 +112,32 @@
.chat-item {
height: 50px;
display: flex;
margin-bottom: 1px;
position: relative;
padding: 5px 10px;
align-items: center;
background-color: white;
background-color: var(--im-background);
white-space: nowrap;
color: black;
cursor: pointer;
&:hover {
background-color: #F8FAFF;
background-color: var(--im-background-active);
}
&.active {
background-color: #F4F9FF;
background-color: var(--im-background-active-dark);
}
.chat-left {
position: relative;
display: flex;
width: 45px;
height: 45x;
justify-content: center;
align-items: center;
.unread-text {
position: absolute;
background-color: #f56c6c;
right: -5px;
top: -5px;
right: -4px;
top: -8px;
color: white;
border-radius: 30px;
padding: 1px 5px;
@ -161,36 +159,36 @@
.chat-name {
display: flex;
line-height: 25px;
height: 25px;
.chat-name-text {
flex: 1;
display: flex;
align-items: center;
font-size: 15px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
.el-tag {
background-color: #2830d3;
border-radius: 10px;
border: 0;
height: 16px;
line-height: 16px;
font-size: 10px;
margin-left: 2px;
opacity: 0.8;
}
}
line-height: 20px;
height: 20px;
.chat-name-text {
flex: 1;
display: flex;
align-items: center;
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
.el-tag {
min-width: 22px;
text-align: center;
background-color: #2830d3;
border-radius: 10px;
border: 0;
height: 16px;
line-height: 16px;
font-size: 10px;
margin-left: 2px;
opacity: 0.8;
}
}
.chat-time-text {
font-size: 13px;
font-size: var(--im-font-size-smaller);
text-align: right;
color: #888888;
color: var(--im-text-color-light);
white-space: nowrap;
overflow: hidden;
padding-left: 10px;
@ -203,11 +201,12 @@
.chat-at-text {
color: #c70b0b;
font-size: 12px;
font-size: var(--im-font-size-smaller);
}
.chat-send-name {
font-size: 13px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
@ -216,7 +215,8 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 13px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
img {
width: 20px !important;

72
im-web/src/components/chat/ChatMessageItem.vue

@ -9,7 +9,7 @@
</div>
<div class="chat-msg-normal" v-if="isNormal" :class="{ 'chat-msg-mine': mine }">
<div class="head-image">
<head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image>
<head-image :name="showName" :size="38" :url="headImage" :id="msgInfo.sendId"></head-image>
</div>
<div class="chat-msg-content">
<div v-show="mode == 1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
@ -213,13 +213,14 @@
.chat-msg-tip {
line-height: 50px;
font-size: 14px;
font-size: var(--im-font-size-small);
color: var(--im-text-color-light);
}
.chat-msg-normal {
position: relative;
font-size: 0;
padding-left: 60px;
padding-left: 48px;
min-height: 50px;
margin-top: 10px;
@ -244,8 +245,8 @@
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
color: #333;
font-size: 14px;
color: var(--im-text-color-light);
font-size: var(--im-font-size);
line-height: 20px;
span {
@ -261,13 +262,11 @@
display: block;
position: relative;
line-height: 26px;
margin-top: 3px;
padding: 7px;
background-color: #eee;
//margin-top: 3px;
padding: 6px 10px;
background-color: var(--im-background);
border-radius: 10px;
color: black;
display: block;
font-size: 14px;
font-size: var(--im-font-size);
text-align: left;
white-space: pre-wrap;
word-break: break-all;
@ -298,9 +297,7 @@
min-height: 150px;
max-width: 400px;
max-height: 300px;
border: #dddddd solid 1px;
border: 5px solid #ccc;
border-radius: 6px;
border-radius: 8px;
cursor: pointer;
}
@ -312,17 +309,15 @@
flex-direction: row;
align-items: center;
cursor: pointer;
padding-bottom: 5px;
margin-bottom: 2px;
.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;
min-height: 60px;
box-shadow: var(--im-box-shadow-light);
border-radius: 4px;
padding: 10px 15px;
.chat-file-info {
@ -330,21 +325,26 @@
height: 100%;
text-align: left;
font-size: 14px;
margin-right: 10px;
.chat-file-name {
display: inline-block;
min-width: 150px;
max-width: 300px;
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
min-width: 160px;
max-width: 220px;
font-size: 14px;
margin-bottom: 4px;
white-space: pre-wrap;
word-break: break-all;
}
.chat-file-size {
font-size: var(--im-font-size-smaller);
color: var(--im-text-color-light);
}
}
.chat-file-icon {
font-size: 50px;
font-size: 44px;
color: #d42e07;
}
}
@ -384,32 +384,29 @@
.chat-readed {
font-size: 12px;
color: #888;
font-weight: 600;
color: var(--im-text-color-light);
}
.chat-unread {
font-size: 12px;
color: #f23c0f;
font-weight: 600;
font-size: var(--im-font-size-smaller);
color: var(--im-color-danger);
}
}
.chat-receipt {
font-size: 13px;
color: blue;
font-size: var(--im-font-size-smaller);
cursor: pointer;
color: var(--im-text-color-light);
.icon-ok {
font-size: 20px;
color: #329432;
color: var(--im-color-sucess);
}
}
.chat-at-user {
padding: 2px 5px;
border-radius: 3px;
font-weight: 600;
cursor: pointer;
}
}
@ -419,7 +416,7 @@
&.chat-msg-mine {
text-align: right;
padding-left: 0;
padding-right: 60px;
padding-right: 48px;
.head-image {
left: auto;
@ -444,9 +441,8 @@
.chat-msg-text {
margin-left: 10px;
background-color: rgb(88, 127, 240);
background-color: var(--im-color-primary-light-2);
color: #fff;
vertical-align: top;
&:after {
left: auto;

43
im-web/src/components/common/Emotion.vue

@ -1,7 +1,7 @@
<template>
<div v-show="show" @click="close()">
<div class="emotion-box" :style="{'left':x+'px','top':y+'px'}">
<el-scrollbar style="height:250px">
<el-scrollbar style="height: 220px">
<div class="emotion-item-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)">
@ -39,26 +39,23 @@
},
computed: {
x() {
return this.pos.x - 200;
return this.pos.x - 22;
},
y() {
return this.pos.y - 280;
return this.pos.y - 234;
}
}
}
</script>
<style scoped lang="scss">
.emotion-box {
position: fixed;
width: 500px;
width: 372px;
box-sizing: border-box;
padding: 5px;
border: 1px solid #53a0e79c;
border-radius: 5px;
background-color: #f5f5f5;
box-shadow: 0px 0px 10px #ccc;
//border-radius: 5px;
background-color: #fff;
box-shadow: var(--im-box-shadow);
.emotion-item-list {
display: flex;
@ -67,22 +64,22 @@
.emotion-item {
text-align: center;
cursor: pointer;
padding: 5px;
padding: 2px;
}
}
&:after {
content: "";
position: absolute;
left: 185px;
bottom: -30px;
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: #f5f5f5 transparent transparent;
overflow: hidden;
border-width: 15px;
}
//&:after {
// content: "";
// position: absolute;
// left: 185px;
// bottom: -30px;
// width: 0;
// height: 0;
// border-style: solid dashed dashed;
// border-color: #f5f5f5 transparent transparent;
// overflow: hidden;
// border-width: 15px;
//}
}
</style>

24
im-web/src/components/common/FullImage.vue

@ -2,9 +2,9 @@
<div class="full-image" v-show="visible" :before-close="onClose" :modal="true">
<div class="mask"></div>
<div class="image-box">
<img :src="url"/>
<img :src="url"/>
</div>
<div class="close" @click="onClose">x</div>
<div class="close" @click="onClose"><i class="el-icon-close"></i></div>
</div>
</template>
@ -37,14 +37,18 @@
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
.mask{
position: fixed;
width: 100%;
height: 100%;
background: black;
opacity: 0.9;
opacity: 0.5;
}
.image-box {
@ -54,11 +58,11 @@
img{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
}
@ -69,8 +73,6 @@
color: white;
font-size: 25px;
cursor: pointer;
}
}

32
im-web/src/components/common/HeadImage.vue

@ -1,16 +1,15 @@
<template>
<div class="head-image" @click="showUserInfo($event)">
<div class="head-image" @click="showUserInfo($event)" :style="{cursor : isShowUserInfo ? 'pointer': null}">
<img class="avatar-image" v-show="url" :src="url"
:style="avatarImageStyle" loading="lazy" />
<div class="avatar-text" v-show="!url" :style="avatarTextStyle">
{{name.substring(0,2).toUpperCase()}}</div>
{{name?.substring(0,2).toUpperCase()}}</div>
<div v-show="online" class="online" title="用户当前在线"></div>
<slot></slot>
</div>
</template>
<script>
export default {
name: "headImage",
data() {
@ -26,7 +25,7 @@
},
size: {
type: Number,
default: 50
default: 42
},
width: {
type: Number
@ -43,15 +42,20 @@
},
name:{
type: String,
default: "?"
default: null
},
online:{
type: Boolean,
default:false
}
},
isShowUserInfo: {
type: Boolean,
default: true
}
},
methods:{
showUserInfo(e){
if(!this.isShowUserInfo) return;
if(this.id && this.id>0){
this.$http({
url: `/user/find/${this.id}`,
@ -73,10 +77,12 @@
avatarTextStyle() {
let w = this.width ? this.width : this.size;
let h = this.height ? this.height : this.size;
return `width: ${w}px;height:${h}px;
background-color:${this.textColor};
return `
width: ${w}px;height:${h}px;
background-color: ${this.name ? this.textColor : '#fff'};
font-size:${w*0.35}px;
border-radius: ${this.radius};`
border-radius: ${this.radius};
`
},
textColor(){
let hash = 0;
@ -92,7 +98,8 @@
<style scoped lang="scss">
.head-image {
position: relative;
cursor: pointer;
//cursor: pointer;
.avatar-image {
position: relative;
overflow: hidden;
@ -104,7 +111,8 @@
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #ccc;
//border: 1px solid #ccc;
//box-shadow: var(--im-box-shadow);
}
.online{
@ -115,7 +123,7 @@
height: 12px;
background: limegreen;
border-radius: 50%;
border: 3px solid white;
border: 2px solid white;
}
}
</style>

24
im-web/src/components/common/RightMenu.vue

@ -4,9 +4,8 @@
<el-menu text-color="#333333">
<el-menu-item v-for="(item) in items" :key="item.key" :title="item.name"
@click.native.stop="onSelectMenu(item)">
<span :class="item.icon"></span>
<!-- <span :class="item.icon"></span>-->
<span>{{item.name}}</span>
</el-menu-item>
</el-menu>
</div>
@ -53,21 +52,24 @@
.right-menu {
position: fixed;
box-shadow: 0px 0px 10px #ccc;
border-radius: 8px;
overflow: hidden;
box-shadow: var(--im-box-shadow-light);
.el-menu {
border: 1px solid #b4b4b4;
border-radius: 7px;
border-radius: 4px;
overflow: hidden;
.el-menu-item {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #d0d0d0;
height: 36px;
line-height: 36px;
min-width: 100px;
text-align: left;
padding: 0 0 0 20px;
span {
font-weight: 600;
}
&:hover {
background-color: var(--im-background-active);
}
}
}
}

16
im-web/src/components/common/UserInfo.vue

@ -100,17 +100,19 @@
<style lang="scss">
.user-info-mask {
background-color: rgba($color: #000000, $alpha: 0);
position: absolute;
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.user-info {
position: absolute;
width: 300px;
background-color: white;
border: #dddddd solid 1px;
border-radius: 5px;
box-shadow: var(--im-box-shadow);
border-radius: 4px;
padding: 15px;
.user-info-box {
@ -136,6 +138,10 @@
}
}
.el-divider--horizontal {
margin: 18px 0;
}
.user-btn-group {
text-align: center;
}

85
im-web/src/components/friend/AddFriend.vue

@ -1,6 +1,6 @@
<template>
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="30%" :before-close="onClose">
<el-input placeholder="输入用户名或昵称进行,最多展示20条" class="input-with-select" v-model="searchText" @keyup.enter.native="onSearch()">
<el-dialog title="添加好友" :visible.sync="dialogVisible" width="400px" :before-close="onClose" custom-class="add-friend-dialog">
<el-input placeholder="输入用户名或昵称按下enter搜索,最多展示20条" class="input-with-select" v-model="searchText" size="small" @keyup.enter.native="onSearch()">
<i class="el-icon-search el-input__icon" slot="suffix"
@click="onSearch()"> </i>
</el-input>
@ -53,6 +53,10 @@
this.$emit("close");
},
onSearch() {
if(!this.searchText){
this.users = [];
return;
}
this.$http({
url: "/user/findByName",
method: "get",
@ -91,48 +95,47 @@
</script>
<style lang="scss">
.el-dialog {
min-width: 400px;
}
.item {
height: 65px;
display: flex;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 25px;
.add-friend-dialog {
.item {
height: 65px;
display: flex;
position: relative;
padding-left: 15px;
align-items: center;
padding-right: 25px;
.add-friend-text {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
.add-friend-text {
margin-left: 15px;
flex: 3;
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: hidden;
.text-user-name{
display: flex;
flex-direction: row;
font-weight: 600;
font-size: 16px;
line-height: 25px;
.text-user-name{
display: flex;
flex-direction: row;
font-weight: 600;
font-size: 16px;
line-height: 25px;
.online-status{
font-size: 12px;
font-weight: 600;
&.online{
color: #5fb878;
}
}
}
.online-status{
font-size: 12px;
font-weight: 600;
&.online{
color: #5fb878;
}
}
}
.text-nick-name{
display: flex;
flex-direction: row;
font-size: 12px;
line-height: 20px;
}
.text-nick-name{
display: flex;
flex-direction: row;
font-size: 12px;
line-height: 20px;
}
}
}
}
}
}
</style>

49
im-web/src/components/friend/FriendItem.vue

@ -1,16 +1,18 @@
<template>
<div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="friend-avatar">
<head-image :size="45" :name="friend.nickName" :url="friend.headImage" :online="friend.online">
<head-image :size="42" :name="friend.nickName" :url="friend.headImage" :online="friend.online">
</head-image>
</div>
<div class="friend-info">
<div class="friend-name">{{ friend.nickName}}</div>
<div class="friend-online">
<el-image v-show="friend.onlineWeb" class="online" :src="require('@/assets/image/online_web.png')"
title="电脑设备在线" />
<el-image v-show="friend.onlineApp" class="online" :src="require('@/assets/image/online_app.png')"
title="移动设备在线" />
<i class="el-icon-monitor online" v-show="friend.onlineWeb" title="电脑设备在线">
<span class="online-icon"></span>
</i>
<i class="el-icon-mobile-phone online" v-show="friend.onlineApp" title="移动设备在线">
<span class="online-icon"></span>
</i>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@ -86,28 +88,24 @@
.friend-item {
height: 50px;
display: flex;
margin-bottom: 1px;
position: relative;
padding: 5px 10px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: #F8FAFF;
}
&:hover {
background-color: var(--im-background-active);
}
&.active {
background-color: #F4F9FF;
}
&.active {
background-color: var(--im-background-active-dark);
}
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 45px;
height: 45px;
}
.friend-info {
@ -118,20 +116,29 @@
text-align: left;
.friend-name {
font-size: 15px;
font-weight: 600;
line-height: 30px;
font-size: var(--im-font-size);
white-space: nowrap;
overflow: hidden;
}
.friend-online {
.online {
font-weight: bold;
padding-right: 2px;
width: 15px;
height: 15px;
font-size: 16px;
position: relative;
}
.online-icon{
position: absolute;
right: 0;
bottom: 0;
width: 6px;
height: 6px;
background: limegreen;
border-radius: 50%;
border: 1px solid white;
}
}
}
}

308
im-web/src/components/group/AddGroupMember.vue

@ -1,166 +1,180 @@
<template>
<el-dialog title="邀请好友" :visible.sync="visible" width="50%" :before-close="onClose">
<div class="agm-container">
<div class="agm-l-box">
<el-input placeholder="搜索好友" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="suffix"> </i>
</el-input>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index"
:active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item>
</div>
</el-scrollbar>
</div>
<div class="agm-arrow el-icon-d-arrow-right"></div>
<div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{checkCount}}位好友</div>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index"
:active="false" @del="onRemoveFriend(friend,index)" :menu="false">
</friend-item>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-dialog title="邀请好友" :visible.sync="visible" width="620px" :before-close="onClose">
<div class="agm-container">
<div class="agm-l-box">
<div class="search">
<el-input placeholder="搜索好友" v-model="searchText" size="small">
<i class="el-icon-search el-input__icon" slot="suffix"> </i>
</el-input>
</div>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index"
:active="false">
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
v-model="friend.isCheck" size="medium"></el-checkbox>
</friend-item>
</div>
</el-scrollbar>
</div>
<div class="agm-arrow el-icon-d-arrow-right"></div>
<div class="agm-r-box">
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<el-scrollbar style="height:400px;">
<div v-for="(friend,index) in friends" :key="friend.id">
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index"
:active="false" @del="onRemoveFriend(friend,index)" :menu="false">
</friend-item>
</div>
</el-scrollbar>
</div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button>
</span>
</el-dialog>
</el-dialog>
</template>
<script>
import FriendItem from '../friend/FriendItem.vue';
import FriendItem from '../friend/FriendItem.vue';
export default {
name: "addGroupMember",
components: {
FriendItem
},
data() {
return {
searchText: "",
friends: []
}
},
methods: {
onClose() {
this.$emit("close");
},
onOk() {
export default {
name: "addGroupMember",
components: {
FriendItem
},
data() {
return {
searchText: "",
friends: []
}
},
methods: {
onClose() {
this.$emit("close");
},
onOk() {
let inviteVO = {
groupId: this.groupId,
friendIds: []
}
this.friends.forEach((f) => {
if (f.isCheck && !f.disabled) {
inviteVO.friendIds.push(f.id);
}
})
if (inviteVO.friendIds.length > 0) {
this.$http({
url: "/group/invite",
method: 'post',
data: inviteVO
}).then(() => {
this.$message.success("邀请成功");
this.$emit("reload");
this.$emit("close");
})
}
},
onRemoveFriend(friend, index) {
friend.isCheck = false;
},
onSwitchCheck(friend) {
if (!friend.disabled) {
friend.isCheck = !friend.isCheck
}
}
},
props: {
visible: {
type: Boolean
},
groupId: {
type: Number
},
members: {
type: Array
}
},
computed: {
checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length;
}
},
watch: {
visible: function(newData, oldData) {
if (newData) {
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
if (m) {
//
friend.disabled = true;
friend.isCheck = true
} else {
friend.disabled = false;
friend.isCheck = false;
}
this.friends.push(friend);
})
}
}
}
let inviteVO = {
groupId: this.groupId,
friendIds: []
}
this.friends.forEach((f) => {
if (f.isCheck && !f.disabled) {
inviteVO.friendIds.push(f.id);
}
})
if (inviteVO.friendIds.length > 0) {
this.$http({
url: "/group/invite",
method: 'post',
data: inviteVO
}).then(() => {
this.$message.success("邀请成功");
this.$emit("reload");
this.$emit("close");
})
}
},
onRemoveFriend(friend, index) {
friend.isCheck = false;
},
onSwitchCheck(friend) {
if (!friend.disabled) {
friend.isCheck = !friend.isCheck
}
}
},
props: {
visible: {
type: Boolean
},
groupId: {
type: Number
},
members: {
type: Array
}
},
computed: {
checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length;
}
},
watch: {
visible: function (newData, oldData) {
if (newData) {
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id);
if (m) {
//
friend.disabled = true;
friend.isCheck = true
} else {
friend.disabled = false;
friend.isCheck = false;
}
this.friends.push(friend);
})
}
}
}
}
}
</script>
<style lang="scss">
.agm-container {
display: flex;
.agm-l-box {
flex: 1;
border: #587FF0 solid 1px;
border-radius: 5px;
overflow: hidden;
.agm-container {
display: flex;
.agm-l-box {
flex: 1;
overflow: hidden;
border: var(--im-border);
.agm-friend-checkbox {
margin-right: 20px;
}
}
.search {
height: 40px;
display: flex;
align-items: center;
.agm-arrow {
display: flex;
align-items: center;
font-size: 20px;
padding: 10px;
font-weight: 600;
color: #687Ff0;
}
.el-input__inner {
border: unset;
border-bottom: var(--im-border);
}
.agm-r-box {
flex: 1;
border: #587FF0 solid 1px;
border-radius: 5px;
}
.agm-select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 5px;
}
}
}
.agm-friend-checkbox {
margin-right: 20px;
}
}
.agm-arrow {
display: flex;
align-items: center;
font-size: 18px;
padding: 10px;
font-weight: 600;
color: var(--im-color-primary);
}
.agm-r-box {
flex: 1;
border: var(--im-border);
.agm-select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 6px;
color: var(--im-text-color-light)
}
}
}
</style>

24
im-web/src/components/group/GroupItem.vue

@ -1,7 +1,7 @@
<template>
<div class="group-item" :class="active ? 'active' : ''">
<div class="group-avatar">
<head-image :size="45" :name="group.showGroupName" :url="group.headImage"> </head-image>
<head-image :size="42" :name="group.showGroupName" :url="group.headImage"> </head-image>
</div>
<div class="group-name">
<div>{{group.showGroupName}}</div>
@ -36,26 +36,19 @@
.group-item {
height: 50px;
display: flex;
margin-bottom: 1px;
position: relative;
padding: 5px 10px;
align-items: center;
background-color: white;
white-space: nowrap;
cursor: pointer;
&:hover {
background-color: #F8FAFF;
}
&.active {
background-color: #F4F9FF;
}
&:hover {
background-color: var(--im-background-active);
}
.group-avatar {
width: 45px;
height: 45px;
}
&.active {
background-color: var(--im-background-active-dark);
}
.group-name {
padding-left: 10px;
@ -64,8 +57,7 @@
line-height: 50px;
white-space: nowrap;
overflow: hidden;
font-size: 15px;
font-weight: 600;
font-size: var(--im-font-size);
}
}
</style>

3
im-web/src/components/group/GroupMember.vue

@ -1,12 +1,11 @@
<template>
<div class="group-member">
<head-image :id="member.userId" :name="member.showNickName"
:url="member.headImage" :size="50"
:url="member.headImage" :size="38"
:online="member.online" >
<div v-if="showDel" @click.stop="onDelete()" class="btn-kick el-icon-error"></div>
</head-image>
<div class="member-name">{{member.showNickName}}</div>
</div>
</template>

7
im-web/src/components/group/GroupMemberItem.vue

@ -40,16 +40,14 @@ export default {
<style lang="scss">
.group-member-item {
display: flex;
margin-bottom: 1px;
position: relative;
padding: 0 15px;
align-items: center;
background-color: #fafafa;
white-space: nowrap;
box-sizing: border-box;
&:hover {
background-color: #eeeeee;
background-color: var(--im-background-active);
}
&.active {
@ -63,8 +61,7 @@ export default {
text-align: left;
white-space: nowrap;
overflow: hidden;
font-size: 14px;
font-weight: 600;
font-size: var(--im-font-size);
}
}
</style>

17
im-web/src/components/group/GroupMemberSelector.vue

@ -1,5 +1,5 @@
<template>
<el-dialog title="选择成员" :visible.sync="isShow" width="50%">
<el-dialog title="选择成员" :visible.sync="isShow" width="700px">
<div class="group-member-selector">
<div class="left-box">
<el-input placeholder="搜索" v-model="searchText">
@ -118,9 +118,13 @@
.left-box {
width: 48%;
border: #587FF0 solid 1px;
border-radius: 5px;
overflow: hidden;
border: var(--im-border);
.el-input__inner {
border: none;
border-bottom: var(--im-border);
}
}
@ -130,20 +134,19 @@
font-size: 20px;
padding: 10px;
font-weight: 600;
color: #687Ff0;
color: var(--im-color-primary);
}
.right-box {
width: 48%;
border: #587FF0 solid 1px;
border-radius: 5px;
border: var(--im-border);
.select-tip {
text-align: left;
height: 40px;
line-height: 40px;
text-indent: 5px;
color: var(--im-text-color-light)
}
.checked-member-list {

10
im-web/src/components/rtc/RtcPrivateAcceptor.vue

@ -1,6 +1,6 @@
<template>
<div class="rtc-private-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100"></head-image>
<div class="rtc-private-acceptor">
<head-image :id="friend.id" :name="friend.nickName" :url="friend.headImage" :size="100" :isShowUserInfo="false"></head-image>
<div class="acceptor-text">
{{tip}}
</div>
@ -50,9 +50,9 @@
width: 250px;
height: 250px;
padding: 20px;
background-color: #eeeeee;
border: #dddddd solid 5px;
border-radius: 3%;
background-color: #fff;
box-shadow: var(--im-box-shadow-dark);
border-radius: 4px;
.acceptor-text {
padding: 10px;

964
im-web/src/components/rtc/RtcPrivateVideo.vue

@ -1,500 +1,508 @@
<template>
<div>
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false"
:visible.sync="showRoom" width="50%" height="70%" :before-close="onQuit">
<div class="rtc-private-video">
<div v-show="isVideo" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.3)" >
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
:url="friend.headImage">
</head-image>
<video ref="remoteVideo" autoplay=""></video>
</div>
<div class="rtc-video-mine">
<video ref="localVideo" autoplay=""></video>
</div>
</div>
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.3)">
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
:url="friend.headImage">
<div class="rtc-voice-name">{{friend.nickName}}</div>
</head-image>
</div>
<div class="rtc-control-bar">
<div title="取消" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onQuit()"></div>
</div>
</div>
</el-dialog>
<rtc-private-acceptor v-if="!isHost&&isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
@reject="onReject"></rtc-private-acceptor>
</div>
<div>
<el-dialog
v-dialogDrag
top="5vh"
custom-class="rtc-private-video-dialog"
:title="title"
:width="width"
:visible.sync="showRoom"
:close-on-click-modal="false"
:close-on-press-escape="false"
:before-close="onQuit">
<div class="rtc-private-video">
<div v-show="isVideo" class="rtc-video-box">
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.1)">
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName"
:url="friend.headImage" :isShowUserInfo="false">
</head-image>
<video ref="remoteVideo" autoplay=""></video>
</div>
<div class="rtc-video-mine">
<video ref="localVideo" autoplay=""></video>
</div>
</div>
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..."
element-loading-background="rgba(0, 0, 0, 0.1)">
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName"
:url="friend.headImage" :isShowUserInfo="false">
<div class="rtc-voice-name">{{ friend.nickName }}</div>
</head-image>
</div>
<div class="rtc-control-bar">
<div title="取消" class="icon iconfont icon-phone-reject reject"
style="color: red;" @click="onQuit()"></div>
</div>
</div>
</el-dialog>
<rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept"
@reject="onReject"></rtc-private-acceptor>
</div>
</template>
<script>
import HeadImage from '../common/HeadImage.vue';
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue';
import ImWebRtc from '@/api/webrtc';
import ImCamera from '@/api/camera';
import RtcPrivateApi from '@/api/rtcPrivateApi'
import HeadImage from '../common/HeadImage.vue';
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue';
import ImWebRtc from '@/api/webrtc';
import ImCamera from '@/api/camera';
import RtcPrivateApi from '@/api/rtcPrivateApi'
export default {
name: 'rtcPrivateVideo',
components: {
HeadImage,
RtcPrivateAcceptor
},
data() {
return {
camera: new ImCamera(), //
webrtc: new ImWebRtc(), // webrtc
API: new RtcPrivateApi(), // API
audio: new Audio(), //
showRoom: false,
friend: {},
isHost: false, //
state: "CLOSE", // CLOSE: WAITING: CHATING: ERROR:
mode: 'video', // video: voice:
localStream: null, //
remoteStream: null, //
videoTime: 0,
videoTimer: null,
heartbeatTimer: null,
candidates: [],
}
},
methods: {
open(rtcInfo) {
this.showRoom = true;
this.mode = rtcInfo.mode;
this.isHost = rtcInfo.isHost;
this.friend = rtcInfo.friend;
if (this.isHost) {
this.onCall();
}
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
},
initRtc() {
this.webrtc.init(this.configuration)
this.webrtc.setupPeerConnection((stream) => {
this.$refs.remoteVideo.srcObject = stream;
this.remoteStream = stream;
})
//
this.webrtc.onIcecandidate((candidate) => {
if (this.state == "CHATING") {
// ,
this.API.sendCandidate(this.friend.id, candidate);
} else {
// ,
this.candidates.push(candidate)
}
})
//
this.webrtc.onStateChange((state) => {
if (state == "connected") {
console.log("webrtc连接成功")
} else if (state == "disconnected") {
console.log("webrtc连接断开")
}
})
},
onCall() {
if (!this.checkDevEnable()) {
this.close();
}
// webrtc
this.initRtc();
//
this.startHeartBeat();
//
this.openStream().then(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createOffer().then((offer) => {
//
this.API.call(this.friend.id, this.mode, offer).then(() => {
//
this.state = "WAITING";
//
this.audio.play();
}).catch(()=>{
this.close();
})
})
}).catch(()=>{
//
this.close();
})
},
onAccept() {
if (!this.checkDevEnable()) {
this.API.failed(this.friend.id, "对方设备不支持通话")
this.close();
return;
}
//
this.showRoom = true;
this.state = "CHATING";
//
this.audio.pause();
// webrtc
this.initRtc();
//
this.openStream().finally(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createAnswer(this.offer).then((answer) => {
this.API.accept(this.friend.id, answer);
//
this.startChatTime();
//
this.waitTimer && clearTimeout(this.waitTimer);
})
})
},
onReject() {
console.log("onReject")
// 退
this.API.reject(this.friend.id);
// 退
this.close();
},
onHandup() {
this.API.handup(this.friend.id)
this.$message.success("您已挂断,通话结束")
this.close();
},
onCancel() {
this.API.cancel(this.friend.id)
this.$message.success("已取消呼叫,通话结束")
this.close();
},
onRTCMessage(msg) {
//
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) {
return;
}
// RTC
switch (msg.type) {
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE:
this.onRTCCall(msg, 'voice')
break;
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO:
this.onRTCCall(msg, 'video')
break;
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT:
this.onRTCAccept(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_REJECT:
this.onRTCReject(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANCEL:
this.onRTCCancel(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_FAILED:
this.onRTCFailed(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_HANDUP:
this.onRTCHandup(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE:
this.onRTCCandidate(msg)
break;
}
},
onRTCCall(msg, mode) {
this.offer = JSON.parse(msg.content);
this.isHost = false;
this.mode = mode;
this.$http({
url: `/friend/find/${msg.sendId}`,
method: 'get'
}).then((friend) => {
this.friend = friend;
this.state = "WAITING";
this.audio.play();
this.startHeartBeat();
// 30s
this.waitTimer = setTimeout(() => {
this.API.failed(this.friend.id,"对方无应答");
this.$message.error("您未接听");
this.close();
}, 30000)
})
},
onRTCAccept(msg) {
if (msg.selfSend) {
//
this.$message.success("已在其他设备接听");
this.close();
} else {
//
let offer = JSON.parse(msg.content);
this.webrtc.setRemoteDescription(offer);
//
this.state = 'CHATING'
//
this.audio.pause();
// candidate
this.candidates.forEach((candidate) => {
this.API.sendCandidate(this.friend.id, candidate);
})
//
this.startChatTime();
}
},
onRTCReject(msg) {
if (msg.selfSend) {
this.$message.success("已在其他设备拒绝");
this.close();
} else {
this.$message.error("对方拒绝了您的通话请求");
this.close();
}
},
onRTCFailed(msg) {
//
this.$message.error(msg.content)
this.close();
},
onRTCCancel() {
//
this.$message.success("对方取消了呼叫");
this.close();
},
onRTCHandup() {
//
this.$message.success("对方已挂断");
this.close();
},
onRTCCandidate(msg) {
let candidate = JSON.parse(msg.content);
this.webrtc.addIceCandidate(candidate);
},
export default {
name: 'rtcPrivateVideo',
components: {
HeadImage,
RtcPrivateAcceptor
},
data() {
return {
camera: new ImCamera(), //
webrtc: new ImWebRtc(), // webrtc
API: new RtcPrivateApi(), // API
audio: new Audio(), //
showRoom: false,
friend: {},
isHost: false, //
state: "CLOSE", // CLOSE: WAITING: CHATING: ERROR:
mode: 'video', // video: voice:
localStream: null, //
remoteStream: null, //
videoTime: 0,
videoTimer: null,
heartbeatTimer: null,
candidates: [],
}
},
methods: {
open(rtcInfo) {
this.showRoom = true;
this.mode = rtcInfo.mode;
this.isHost = rtcInfo.isHost;
this.friend = rtcInfo.friend;
if (this.isHost) {
this.onCall();
}
},
initAudio() {
let url = require(`@/assets/audio/call.wav`);
this.audio.src = url;
this.audio.loop = true;
},
initRtc() {
this.webrtc.init(this.configuration)
this.webrtc.setupPeerConnection((stream) => {
this.$refs.remoteVideo.srcObject = stream;
this.remoteStream = stream;
})
//
this.webrtc.onIcecandidate((candidate) => {
if (this.state == "CHATING") {
// ,
this.API.sendCandidate(this.friend.id, candidate);
} else {
// ,
this.candidates.push(candidate)
}
})
//
this.webrtc.onStateChange((state) => {
if (state == "connected") {
console.log("webrtc连接成功")
} else if (state == "disconnected") {
console.log("webrtc连接断开")
}
})
},
onCall() {
if (!this.checkDevEnable()) {
this.close();
}
// webrtc
this.initRtc();
//
this.startHeartBeat();
//
this.openStream().then(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createOffer().then((offer) => {
//
this.API.call(this.friend.id, this.mode, offer).then(() => {
//
this.state = "WAITING";
//
this.audio.play();
}).catch(() => {
this.close();
})
})
}).catch(() => {
//
this.close();
})
},
onAccept() {
if (!this.checkDevEnable()) {
this.API.failed(this.friend.id, "对方设备不支持通话")
this.close();
return;
}
//
this.showRoom = true;
this.state = "CHATING";
//
this.audio.pause();
// webrtc
this.initRtc();
//
this.openStream().finally(() => {
this.webrtc.setStream(this.localStream);
this.webrtc.createAnswer(this.offer).then((answer) => {
this.API.accept(this.friend.id, answer);
//
this.startChatTime();
//
this.waitTimer && clearTimeout(this.waitTimer);
})
})
},
onReject() {
console.log("onReject")
// 退
this.API.reject(this.friend.id);
// 退
this.close();
},
onHandup() {
this.API.handup(this.friend.id)
this.$message.success("您已挂断,通话结束")
this.close();
},
onCancel() {
this.API.cancel(this.friend.id)
this.$message.success("已取消呼叫,通话结束")
this.close();
},
onRTCMessage(msg) {
//
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE &&
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO &&
this.isClose) {
return;
}
// RTC
switch (msg.type) {
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE:
this.onRTCCall(msg, 'voice')
break;
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO:
this.onRTCCall(msg, 'video')
break;
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT:
this.onRTCAccept(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_REJECT:
this.onRTCReject(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANCEL:
this.onRTCCancel(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_FAILED:
this.onRTCFailed(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_HANDUP:
this.onRTCHandup(msg)
break;
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE:
this.onRTCCandidate(msg)
break;
}
},
onRTCCall(msg, mode) {
this.offer = JSON.parse(msg.content);
this.isHost = false;
this.mode = mode;
this.$http({
url: `/friend/find/${msg.sendId}`,
method: 'get'
}).then((friend) => {
this.friend = friend;
this.state = "WAITING";
this.audio.play();
this.startHeartBeat();
// 30s
this.waitTimer = setTimeout(() => {
this.API.failed(this.friend.id, "对方无应答");
this.$message.error("您未接听");
this.close();
}, 30000)
})
},
onRTCAccept(msg) {
if (msg.selfSend) {
//
this.$message.success("已在其他设备接听");
this.close();
} else {
//
let offer = JSON.parse(msg.content);
this.webrtc.setRemoteDescription(offer);
//
this.state = 'CHATING'
//
this.audio.pause();
// candidate
this.candidates.forEach((candidate) => {
this.API.sendCandidate(this.friend.id, candidate);
})
//
this.startChatTime()
}
},
onRTCReject(msg) {
if (msg.selfSend) {
this.$message.success("已在其他设备拒绝");
this.close();
} else {
this.$message.error("对方拒绝了您的通话请求");
this.close();
}
},
onRTCFailed(msg) {
//
this.$message.error(msg.content)
this.close();
},
onRTCCancel() {
//
this.$message.success("对方取消了呼叫");
this.close();
},
onRTCHandup() {
//
this.$message.success("对方已挂断");
this.close();
},
onRTCCandidate(msg) {
let candidate = JSON.parse(msg.content);
this.webrtc.addIceCandidate(candidate);
},
openStream() {
return new Promise((resolve, reject) => {
if (this.isVideo) {
// +
this.camera.openVideo().then((stream) => {
this.localStream = stream;
this.$nextTick(() => {
this.$refs.localVideo.srcObject = stream;
this.$refs.localVideo.muted = true;
})
resolve(stream);
}).catch((e) => {
this.$message.error("打开摄像头失败")
console.log("本摄像头打开失败:" + e.message)
reject(e);
})
} else {
//
this.camera.openAudio().then((stream) => {
this.localStream = stream;
this.$refs.localVideo.srcObject = stream;
resolve(stream);
}).catch((e) => {
this.$message.error("打开麦克风失败")
console.log("打开麦克风失败:" + e.message)
reject(e);
})
}
})
},
startChatTime() {
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.videoTimer = setInterval(() => {
this.videoTime++;
}, 1000)
},
checkDevEnable() {
//
if (!this.camera.isEnable()) {
this.message.error("访问摄像头失败");
return false;
}
// webrtc
if (!this.webrtc.isEnable()) {
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC");
return false;
}
return true;
},
startHeartBeat() {
// 15s
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
this.API.heartbeat(this.friend.id);
}, 15000)
},
close() {
this.showRoom = false;
this.camera.close();
this.webrtc.close();
this.audio.pause();
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.waitTimer && clearTimeout(this.waitTimer);
this.videoTimer = null;
this.heartbeatTimer = null;
this.waitTimer = null;
this.state = 'CLOSE';
this.candidates = [];
},
onQuit() {
if (this.isChating) {
this.onHandup()
} else if (this.isWaiting) {
this.onCancel();
} else {
this.close();
}
}
},
computed: {
title() {
let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
if (this.isChating) {
strTitle += `(${this.currentTime})`;
} else if (this.isWaiting) {
strTitle += `(呼叫中)`;
}
return strTitle;
},
currentTime() {
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;
},
configuration() {
const iceServers = this.$store.state.configStore.webrtc.iceServers;
return {
iceServers: iceServers
}
},
isVideo() {
return this.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isChating() {
return this.state == "CHATING";
},
isWaiting() {
return this.state == "WAITING";
},
isClose() {
return this.state == "CLOSE";
}
},
mounted() {
//
this.initAudio();
},
created() {
//
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
this.onQuit();
}
}
openStream() {
return new Promise((resolve, reject) => {
if (this.isVideo) {
// +
this.camera.openVideo().then((stream) => {
this.localStream = stream;
this.$nextTick(() => {
this.$refs.localVideo.srcObject = stream;
this.$refs.localVideo.muted = true;
})
resolve(stream);
}).catch((e) => {
this.$message.error("打开摄像头失败")
console.log("本摄像头打开失败:" + e.message)
reject(e);
})
} else {
//
this.camera.openAudio().then((stream) => {
this.localStream = stream;
this.$refs.localVideo.srcObject = stream;
resolve(stream);
}).catch((e) => {
this.$message.error("打开麦克风失败")
console.log("打开麦克风失败:" + e.message)
reject(e);
})
}
})
},
startChatTime() {
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.videoTimer = setInterval(() => {
this.videoTime++;
}, 1000)
},
checkDevEnable() {
//
if (!this.camera.isEnable()) {
this.message.error("访问摄像头失败");
return false;
}
// webrtc
if (!this.webrtc.isEnable()) {
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC");
return false;
}
return true;
},
startHeartBeat() {
// 15s
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.heartbeatTimer = setInterval(() => {
this.API.heartbeat(this.friend.id);
}, 15000)
},
close() {
this.showRoom = false;
this.camera.close();
this.webrtc.close();
this.audio.pause();
this.videoTime = 0;
this.videoTimer && clearInterval(this.videoTimer);
this.heartbeatTimer && clearInterval(this.heartbeatTimer);
this.waitTimer && clearTimeout(this.waitTimer);
this.videoTimer = null;
this.heartbeatTimer = null;
this.waitTimer = null;
this.state = 'CLOSE';
this.candidates = [];
},
onQuit() {
if (this.isChating) {
this.onHandup()
} else if (this.isWaiting) {
this.onCancel();
} else {
this.close();
}
}
},
computed: {
width() {
return this.isVideo ? '960px' : '360px'
},
title() {
let strTitle = `${this.modeText}通话-${this.friend.nickName}`;
if (this.isChating) {
strTitle += `(${this.currentTime})`;
} else if (this.isWaiting) {
strTitle += `(呼叫中)`;
}
return strTitle;
},
currentTime() {
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;
},
configuration() {
const iceServers = this.$store.state.configStore.webrtc.iceServers;
return {
iceServers: iceServers
}
},
isVideo() {
return this.mode == "video"
},
modeText() {
return this.isVideo ? "视频" : "语音";
},
isChating() {
return this.state == "CHATING";
},
isWaiting() {
return this.state == "WAITING";
},
isClose() {
return this.state == "CLOSE";
}
},
mounted() {
//
this.initAudio();
},
created() {
//
window.addEventListener('beforeunload', () => {
this.onQuit();
});
},
beforeUnmount() {
this.onQuit();
}
}
</script>
<style lang="scss">
.rtc-private-video {
position: relative;
.rtc-private-video {
position: relative;
.el-loading-text {
color: white !important;
font-size: 16px !important;
}
.el-loading-text {
color: white !important;
font-size: 16px !important;
}
.path {
stroke: white !important;
}
.path {
stroke: white !important;
}
.rtc-video-box {
position: relative;
border: #4880b9 solid 1px;
background-color: #eeeeee;
.rtc-video-box {
position: relative;
background-color: #eeeeee;
.rtc-video-friend {
height: 70vh;
.rtc-video-friend {
height: 70vh;
.friend-head-image {
position: absolute;
}
.friend-head-image {
position: absolute;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
transform: rotateY(180deg);
}
}
video {
width: 100%;
height: 100%;
object-fit: cover;
transform: rotateY(180deg);
}
}
.rtc-video-mine {
position: absolute;
z-index: 99999;
width: 25vh;
right: 0;
bottom: 0;
box-shadow: 0px 0px 5px #ccc;
background-color: #cccccc;
.rtc-video-mine {
position: absolute;
z-index: 99999;
width: 25vh;
right: 0;
bottom: -1px;
video {
width: 100%;
object-fit: cover;
transform: rotateY(180deg);
}
}
}
video {
width: 100%;
object-fit: cover;
transform: rotateY(180deg);
}
}
}
.rtc-voice-box {
position: relative;
display: flex;
justify-content: center;
border: #4880b9 solid 1px;
width: 100%;
height: 50vh;
padding-top: 10vh;
background-color: aliceblue;
.rtc-voice-box {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 300px;
background-color: var(--im-color-primary-light-9);
.rtc-voice-name {
text-align: center;
font-size: 22px;
font-weight: 600;
}
}
.rtc-voice-name {
text-align: center;
font-size: 20px;
font-weight: 600;
}
}
.rtc-control-bar {
display: flex;
justify-content: space-around;
padding: 10px;
.rtc-control-bar {
display: flex;
justify-content: space-around;
padding: 10px;
.icon {
font-size: 50px;
cursor: pointer;
}
}
}
.icon {
font-size: 50px;
cursor: pointer;
}
}
}
</style>

254
im-web/src/components/setting/Setting.vue

@ -1,144 +1,144 @@
<template>
<el-dialog class="setting" title="设置" :visible.sync="visible" width="500px" :before-close="onClose">
<el-form :model="userInfo" label-width="70px" :rules="rules" ref="settingForm">
<el-form-item label="头像">
<file-upload class="avatar-uploader"
:action="imageAction"
:showLoading="true"
:maxSize="maxSize"
@success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
</el-form-item>
<el-form-item label="用户名">
<el-input disabled v-model="userInfo.userName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item prop="nickName" label="昵称">
<el-input v-model="userInfo.nickName" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userInfo.sex">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="个性签名">
<el-input type="textarea" v-model="userInfo.signature"></el-input>
</el-form-item>
</el-form>
<el-dialog class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose">
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small">
<el-form-item label="头像" style="margin-bottom: 0 !important;">
<file-upload class="avatar-uploader"
:action="imageAction"
:showLoading="true"
:maxSize="maxSize"
@success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
</el-form-item>
<el-form-item label="用户名">
<el-input disabled v-model="userInfo.userName" autocomplete="off" size="small"></el-input>
</el-form-item>
<el-form-item prop="nickName" label="昵称">
<el-input v-model="userInfo.nickName" autocomplete="off" size="small"></el-input>
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="userInfo.sex">
<el-radio :label="0"></el-radio>
<el-radio :label="1"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="个性签名">
<el-input type="textarea" v-model="userInfo.signature" :rows="3"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onSubmit()"> </el-button>
</span>
</el-dialog>
</el-dialog>
</template>
<script>
import FileUpload from "../common/FileUpload.vue";
import FileUpload from "../common/FileUpload.vue";
export default {
name: "setting",
components: {
FileUpload
},
data() {
return {
userInfo: {
export default {
name: "setting",
components: {
FileUpload
},
data() {
return {
userInfo: {},
maxSize: 5 * 1024 * 1024,
action: "/image/upload",
rules: {
nickName: [{
required: true,
message: '请输入昵称',
trigger: 'blur'
}]
}
}
},
methods: {
},
maxSize: 5*1024*1024,
action: "/image/upload",
rules: {
nickName: [{
required: true,
message: '请输入昵称',
trigger: 'blur'
}]
}
}
},
methods: {
onClose() {
this.$emit("close");
},
onSubmit() {
this.$refs['settingForm'].validate((valid) => {
if (!valid) {
return false;
}
this.$http({
url: "/user/update",
method: "put",
data: this.userInfo
}).then(()=>{
this.$store.commit("setUserInfo",this.userInfo);
this.$emit("close");
this.$message.success("修改成功");
})
});
},
onUploadSuccess(data, file) {
this.userInfo.headImage = data.originUrl;
this.userInfo.headImageThumb = data.thumbUrl;
}
},
props: {
visible: {
type: Boolean
}
},
computed:{
imageAction(){
return `/image/upload`;
}
},
watch: {
visible: function(newData, oldData) {
//
let mine = this.$store.state.userStore.userInfo;
this.userInfo = JSON.parse(JSON.stringify(mine));
}
}
}
onClose() {
this.$emit("close");
},
onSubmit() {
this.$refs['settingForm'].validate((valid) => {
if (!valid) {
return false;
}
this.$http({
url: "/user/update",
method: "put",
data: this.userInfo
}).then(() => {
this.$store.commit("setUserInfo", this.userInfo);
this.$emit("close");
this.$message.success("修改成功");
})
});
},
onUploadSuccess(data, file) {
this.userInfo.headImage = data.originUrl;
this.userInfo.headImageThumb = data.thumbUrl;
}
},
props: {
visible: {
type: Boolean
}
},
computed: {
imageAction() {
return `/image/upload`;
}
},
watch: {
visible: function (newData, oldData) {
//
let mine = this.$store.state.userStore.userInfo;
this.userInfo = JSON.parse(JSON.stringify(mine));
}
}
}
</script>
<style lang="scss" >
.setting {
.el-form {
padding: 30px;
}
.avatar-uploader {
<style lang="scss">
.setting {
.el-form {
padding: 10px 0 0 10px;
}
.avatar-uploader {
--width: 112px;
.el-upload {
border: 1px dashed #d9d9d9 !important;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload {
border: 1px dashed #d9d9d9 !important;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-upload:hover {
border-color: #409EFF;
}
.el-upload:hover {
border-color: #409EFF;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar-uploader-icon {
font-size: 24px;
color: #8c939d;
width: var(--width);
height: var(--width);
line-height: var(--width);
text-align: center;
}
.avatar {
width: 178px;
height: 178px;
display: block;
}
}
}
.avatar {
width: var(--width);
height: var(--width);
display: block;
}
}
}
</style>

4
im-web/src/main.js

@ -2,8 +2,9 @@ import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import './assets/style/im.scss';
import './assets/iconfont/iconfont.css';
import httpRequest from './api/httpRequest';
import * as socketApi from './api/wssocket';
import * as messageType from './api/messageType';
@ -33,3 +34,4 @@ new Vue({
store,
render: h=>h(App)
})

177
im-web/src/view/Chat.vue

@ -1,104 +1,103 @@
<template>
<el-container class="chat-page">
<el-aside width="280px" class="chat-list-box">
<div class="chat-list-header">
<el-input class="search-text" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#eee">
<div class="chat-loading-box"></div>
</div>
<el-scrollbar class="chat-list-items">
<div v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete&&chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="chat-box">
<chat-box v-if="chatStore.activeChat" :chat="chatStore.activeChat"></chat-box>
</el-container>
</el-container>
<el-container class="chat-page">
<el-aside width="260px" class="chat-list-box">
<div class="chat-list-header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
</div>
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#F9F9F9" element-loading-size="24">
</div>
<el-scrollbar class="chat-list-items" v-else>
<div v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item v-show="!chat.delete&&chat.showName.includes(searchText)" :chat="chat" :index="index"
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)"
:active="chat === chatStore.activeChat"></chat-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="chat-box">
<chat-box v-if="chatStore.activeChat" :chat="chatStore.activeChat"></chat-box>
</el-container>
</el-container>
</template>
<script>
import ChatItem from "../components/chat/ChatItem.vue";
import ChatBox from "../components/chat/ChatBox.vue";
import ChatItem from "../components/chat/ChatItem.vue";
import ChatBox from "../components/chat/ChatBox.vue";
export default {
name: "chat",
components: {
ChatItem,
ChatBox
},
data() {
return {
searchText: "",
messageContent: "",
group: {},
groupMembers: []
}
},
methods: {
onActiveItem(index) {
this.$store.commit("activeChat", index);
},
onDelItem(index) {
this.$store.commit("removeChat", index);
},
onTop(chatIdx) {
this.$store.commit("moveTop", chatIdx);
},
},
computed: {
chatStore() {
return this.$store.state.chatStore;
},
loading(){
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
}
}
}
export default {
name: "chat",
components: {
ChatItem,
ChatBox
},
data() {
return {
searchText: "",
messageContent: "",
group: {},
groupMembers: []
}
},
methods: {
onActiveItem(index) {
this.$store.commit("activeChat", index);
},
onDelItem(index) {
this.$store.commit("removeChat", index);
},
onTop(chatIdx) {
this.$store.commit("moveTop", chatIdx);
},
},
computed: {
chatStore() {
return this.$store.state.chatStore;
},
loading() {
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
}
}
}
</script>
<style lang="scss">
.chat-page {
.chat-list-box {
display: flex;
flex-direction: column;
border-right: #53a0e79c solid 1px;
background: white;
width: 3rem;
.chat-page {
.chat-list-box {
display: flex;
flex-direction: column;
background: var(--im-background);
.chat-list-header {
padding: 3px 8px;
line-height: 50px;
border-bottom: 1px #ddd solid;
.chat-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
}
.el-input__inner {
border-radius: 10px !important;
background-color: #F8F8F8;
}
.chat-list-loading {
height: 50px;
background-color: #eee;
}
.el-icon-loading {
font-size: 24px;
color: var(--im-text-color-light);
}
.chat-list-loading{
height: 50px;
background-color: #eee;
.el-loading-text {
color: var(--im-text-color-light);
}
.chat-loading-box{
height: 100%;
}
}
.chat-loading-box {
height: 100%;
}
}
.chat-list-items {
flex: 1;
background: #F8F8F8;
margin: 0 3px;
}
}
}
.chat-list-items {
flex: 1;
}
}
}
</style>

54
im-web/src/view/Friend.vue

@ -1,8 +1,8 @@
<template>
<el-container class="friend-page">
<el-aside width="280px" class="friend-list-box">
<el-aside width="260px" class="friend-list-box">
<div class="friend-list-header">
<el-input class="search-text" placeholder="搜索" v-model="searchText">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="添加好友"
@ -24,8 +24,8 @@
</div>
<div v-show="userInfo.id">
<div class="friend-detail">
<head-image :size="200" :name="userInfo.nickName" :url="userInfo.headImage"
@click.native="showFullImage()" radius="10%"></head-image>
<head-image :size="120" :name="userInfo.nickName" :url="userInfo.headImage"
@click.native="showFullImage()"></head-image>
<div>
<div class="info-item">
<el-descriptions title="好友信息" class="description" :column="1">
@ -36,7 +36,6 @@
<el-descriptions-item label="性别">{{ userInfo.sex==0?"男":"女" }}</el-descriptions-item>
<el-descriptions-item label="签名">{{ userInfo.signature }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="frient-btn-group">
<el-button v-show="isFriend" icon="el-icon-position" type="primary"
@ -48,7 +47,7 @@
</div>
</div>
</div>
<el-divider content-position="center"></el-divider>
<!-- <el-divider content-position="center"></el-divider>-->
</div>
</el-container>
@ -182,36 +181,24 @@
.friend-list-box {
display: flex;
flex-direction: column;
border-right: #53a0e79c solid 1px;
background: white;
background: var(--im-background);
.friend-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 3px 8px;
border-bottom: 1px #ddd solid;
.el-input__inner {
border-radius: 10px !important;
background-color: #F8F8F8;
}
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
.add-btn {
padding: 5px !important;
margin: 5px;
font-size: 20px;
color: #587FF0;
border: #587FF0 1px solid;
background-color: #F0F8FF;
font-size: 16px;
border-radius: 50%;
}
}
.friend-list-items {
flex: 1;
margin: 0 3px;
background: #F8F8F8;
}
}
@ -220,14 +207,14 @@
flex-direction: column;
.friend-header {
padding: 3px;
height: 50px;
line-height: 50px;
font-size: 20px;
font-weight: 600;
text-align: center;
background-color: white;
border-bottom: 1px #ddd solid;
height: 50px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 12px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
box-sizing: border-box;
}
.friend-detail {
@ -239,12 +226,11 @@
.info-item {
margin-left: 20px;
background-color: #ffffff;
border-radius: 10px ;
border: 1px #ddd solid;
}
.description {
padding: 20px 20px 0px 20px;
padding: 20px 20px 0 20px;
}
}

770
im-web/src/view/Group.vue

@ -1,422 +1,416 @@
<template>
<el-container class="group-page">
<el-aside width="280px" class="group-list-box">
<div class="group-list-header">
<el-input class="search-text" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div>
<el-scrollbar class="group-list-items">
<div v-for="(group,index) in groupStore.groups" :key="index">
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)">
</group-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="group-box">
<div class="group-header" v-show="activeGroup.id">
{{activeGroup.showGroupName}}({{groupMembers.length}})
</div>
<el-scrollbar class="group-container">
<div v-show="activeGroup.id">
<div class="group-info">
<div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
<head-image v-show="!isOwner" class="avatar" :size="200" :url="activeGroup.headImage"
radius="10%" :name="activeGroup.showGroupName">
</head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary"
@click="onSendMessage()">发消息</el-button>
</div>
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules"
ref="groupForm">
<el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea"
maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item>
<div>
<el-button type="success" @click="onSaveGroup()">保存</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散群聊</el-button>
</div>
</el-form>
</div>
<el-divider content-position="center"></el-divider>
<el-scrollbar style="height:200px;">
<div class="group-member-list">
<div v-for="(member) in groupMembers" :key="member.id">
<group-member v-show="!member.quit" class="group-member" :member="member"
:showDel="isOwner&&member.userId!=activeGroup.ownerId" @del="onKick"></group-member>
</div>
<div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
:members="groupMembers" @reload="loadGroupMembers"
@close="onCloseAddGroupMember"></add-group-member>
</div>
</div>
</el-scrollbar>
</div>
</el-scrollbar>
</el-container>
</el-container>
<el-container class="group-page">
<el-aside width="260px" class="group-list-box">
<div class="group-list-header">
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText">
<i class="el-icon-search el-input__icon" slot="prefix"> </i>
</el-input>
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button>
</div>
<el-scrollbar class="group-list-items">
<div v-for="(group,index) in groupStore.groups" :key="index">
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group"
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)">
</group-item>
</div>
</el-scrollbar>
</el-aside>
<el-container class="group-box">
<div class="group-header" v-show="activeGroup.id">
{{ activeGroup.showGroupName }}({{ groupMembers.length }})
</div>
<div class="group-container">
<div v-show="activeGroup.id">
<div class="group-info">
<div>
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction"
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']">
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload>
<head-image v-show="!isOwner" class="avatar" :size="120" :url="activeGroup.headImage"
:name="activeGroup.showGroupName">
</head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary"
@click="onSendMessage()">发消息
</el-button>
</div>
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small"
ref="groupForm">
<el-form-item label="群聊名称" prop="name">
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input>
</el-form-item>
<el-form-item label="群主">
<el-input :value="ownerName" disabled></el-input>
</el-form-item>
<el-form-item label="群名备注">
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name"
maxlength="20"></el-input>
</el-form-item>
<el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input>
</el-form-item>
<el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
maxlength="1024" placeholder="群主未设置"></el-input>
</el-form-item>
<div>
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button>
<el-button type="success" @click="onSaveGroup()">保存</el-button>
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button>
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button>
</div>
</el-form>
</div>
<el-divider content-position="center"></el-divider>
<div class="group-member-list">
<div v-for="(member) in groupMembers" :key="member.id">
<group-member v-show="!member.quit" class="group-member" :member="member"
:showDel="isOwner && member.userId!=activeGroup.ownerId" @del="onKick"></group-member>
</div>
<div class="group-invite">
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()">
<i class="el-icon-plus"></i>
</div>
<div class="invite-member-text">邀请</div>
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id"
:members="groupMembers" @reload="loadGroupMembers"
@close="onCloseAddGroupMember"></add-group-member>
</div>
</div>
</div>
</div>
</el-container>
</el-container>
</template>
<script>
import GroupItem from '../components/group/GroupItem';
import FileUpload from '../components/common/FileUpload';
import GroupMember from '../components/group/GroupMember.vue';
import AddGroupMember from '../components/group/AddGroupMember.vue';
import HeadImage from '../components/common/HeadImage.vue';
export default {
name: "group",
components: {
GroupItem,
GroupMember,
FileUpload,
AddGroupMember,
HeadImage
},
data() {
return {
searchText: "",
maxSize: 5 * 1024 * 1024,
activeGroup: {},
groupMembers: [],
showAddGroupMember: false,
rules: {
name: [{
required: true,
message: '请输入群聊名称',
trigger: 'blur'
}]
}
};
},
methods: {
onCreateGroup() {
this.$prompt('请输入群聊名称', '创建群聊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '请输入群聊名称'
}).then((o) => {
let userInfo = this.$store.state.userStore.userInfo;
let data = {
name: o.value
}
this.$http({
url: `/group/create?groupName=${o.value}`,
method: 'post',
data: data
}).then((group) => {
this.$store.commit("addGroup", group);
})
})
},
onActiveItem(group, index) {
this.$store.commit("activeGroup", index);
// store
this.activeGroup = JSON.parse(JSON.stringify(group));
//
this.loadGroupMembers();
},
onInviteMember() {
this.showAddGroupMember = true;
},
onCloseAddGroupMember() {
this.showAddGroupMember = false;
},
onUploadSuccess(data) {
this.activeGroup.headImage = data.originUrl;
this.activeGroup.headImageThumb = data.thumbUrl;
},
onSaveGroup() {
this.$refs['groupForm'].validate((valid) => {
if (valid) {
let vo = this.activeGroup;
this.$http({
url: "/group/modify",
method: "put",
data: vo
}).then((group) => {
this.$store.commit("updateGroup", group);
this.$message.success("修改成功");
})
}
});
},
onDissolve() {
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/delete/${this.activeGroup.id}`,
method: 'delete'
}).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset();
});
})
import GroupItem from '../components/group/GroupItem';
import FileUpload from '../components/common/FileUpload';
import GroupMember from '../components/group/GroupMember.vue';
import AddGroupMember from '../components/group/AddGroupMember.vue';
import HeadImage from '../components/common/HeadImage.vue';
},
onKick(member) {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/kick/${this.activeGroup.id}`,
method: 'delete',
params: {
userId: member.userId
}
}).then(() => {
this.$message.success(`已将${member.showNickName}移出群聊`);
member.quit = true;
});
})
export default {
name: "group",
components: {
GroupItem,
GroupMember,
FileUpload,
AddGroupMember,
HeadImage
},
data() {
return {
searchText: "",
maxSize: 5 * 1024 * 1024,
activeGroup: {},
groupMembers: [],
showAddGroupMember: false,
rules: {
name: [{
required: true,
message: '请输入群聊名称',
trigger: 'blur'
}]
}
};
},
methods: {
onCreateGroup() {
this.$prompt('请输入群聊名称', '创建群聊', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /\S/,
inputErrorMessage: '请输入群聊名称'
}).then((o) => {
let userInfo = this.$store.state.userStore.userInfo;
let data = {
name: o.value
}
this.$http({
url: `/group/create?groupName=${o.value}`,
method: 'post',
data: data
}).then((group) => {
this.$store.commit("addGroup", group);
})
})
},
onActiveItem(group, index) {
this.$store.commit("activeGroup", index);
// store
this.activeGroup = JSON.parse(JSON.stringify(group));
//
this.loadGroupMembers();
},
onInviteMember() {
this.showAddGroupMember = true;
},
onCloseAddGroupMember() {
this.showAddGroupMember = false;
},
onUploadSuccess(data) {
this.activeGroup.headImage = data.originUrl;
this.activeGroup.headImageThumb = data.thumbUrl;
},
onSaveGroup() {
this.$refs['groupForm'].validate((valid) => {
if (valid) {
let vo = this.activeGroup;
this.$http({
url: "/group/modify",
method: "put",
data: vo
}).then((group) => {
this.$store.commit("updateGroup", group);
this.$message.success("修改成功");
})
}
});
},
onDissolve() {
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/delete/${this.activeGroup.id}`,
method: 'delete'
}).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset();
});
})
},
onQuit() {
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/quit/${this.activeGroup.id}`,
method: 'delete'
}).then(() => {
this.$message.success(`您已退出'${this.activeGroup.name}'`);
this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset();
});
})
},
onKick(member) {
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/kick/${this.activeGroup.id}`,
method: 'delete',
params: {
userId: member.userId
}
}).then(() => {
this.$message.success(`已将${member.showNickName}移出群聊`);
member.quit = true;
});
})
},
onSendMessage() {
let chat = {
type: 'GROUP',
targetId: this.activeGroup.id,
showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");
},
loadGroupMembers() {
this.$http({
url: `/group/members/${this.activeGroup.id}`,
method: "get"
}).then((members) => {
this.groupMembers = members;
})
},
reset() {
this.activeGroup = {};
this.groupMembers = [];
}
},
computed: {
groupStore() {
return this.$store.state.groupStore;
},
ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId);
return member && member.showNickName;
},
isOwner() {
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id;
},
imageAction() {
return `/image/upload`;
}
}
}
},
onQuit() {
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: `/group/quit/${this.activeGroup.id}`,
method: 'delete'
}).then(() => {
this.$message.success(`您已退出'${this.activeGroup.name}'`);
this.$store.commit("removeGroup", this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id);
this.reset();
});
})
},
onSendMessage() {
let chat = {
type: 'GROUP',
targetId: this.activeGroup.id,
showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.$router.push("/home/chat");
},
loadGroupMembers() {
this.$http({
url: `/group/members/${this.activeGroup.id}`,
method: "get"
}).then((members) => {
this.groupMembers = members;
})
},
reset() {
this.activeGroup = {};
this.groupMembers = [];
}
},
computed: {
groupStore() {
return this.$store.state.groupStore;
},
ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId);
return member && member.showNickName;
},
isOwner() {
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id;
},
imageAction() {
return `/image/upload`;
}
}
}
</script>
<style lang="scss">
.group-page {
.group-list-box {
display: flex;
flex-direction: column;
border-right: #53a0e79c solid 1px;
background: #F8F8F8;
.group-page {
.group-list-box {
display: flex;
flex-direction: column;
background: var(--im-background);
.group-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 0 8px;
.add-btn {
padding: 5px !important;
margin: 5px;
font-size: 16px;
border-radius: 50%;
}
}
.group-list-header {
height: 50px;
display: flex;
align-items: center;
padding: 3px 8px;
background-color: white;
border-bottom: 1px #ddd solid;
.group-list-items {
flex: 1;
}
}
.el-input__inner {
border-radius: 10px !important;
}
.group-box {
display: flex;
flex-direction: column;
.add-btn {
padding: 5px !important;
margin: 5px;
font-size: 20px;
color: #587FF0;
border: #587FF0 1px solid;
background-color: #F0F8FF;
border-radius: 50%;
}
}
.group-header {
display: flex;
justify-content: space-between;
padding: 0 12px;
line-height: 50px;
font-size: var(--im-font-size-larger);
border-bottom: var(--im-border);
}
.group-list-items {
flex: 1;
margin: 0 3px;
background: #F8F8F8;
}
}
.el-divider--horizontal {
margin: 16px 0;
}
.group-box {
display: flex;
flex-direction: column;
.group-container {
overflow: auto;
padding: 20px;
flex: 1;
.group-header {
padding: 3px;
height: 50px;
line-height: 50px;
font-size: 20px;
font-weight: 600;
text-align: center;
background-color: white;
border-bottom: 1px #ddd solid;
}
.group-info {
display: flex;
padding: 5px 20px;
.group-container {
padding: 20px;
flex: 1;
.group-form {
flex: 1;
padding-left: 40px;
max-width: 700px;
}
.group-info {
display: flex;
padding: 5px 20px;
.avatar-uploader {
--width: 120px;
text-align: left;
.group-form {
flex: 1;
padding-left: 40px;
max-width: 700px;
}
.el-upload {
border: 1px dashed #d9d9d9 !important;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader {
text-align: left;
.el-upload:hover {
border-color: #409EFF;
}
.el-upload {
border: 1px dashed #d9d9d9 !important;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: var(--width);
height: var(--width);
line-height: var(--width);
text-align: center;
}
.el-upload:hover {
border-color: #409EFF;
}
.avatar {
width: var(--width);
height: var(--width);
display: block;
}
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 200px;
height: 200px;
line-height: 200px;
text-align: center;
}
.send-btn {
margin-top: 12px;
}
}
.avatar {
width: 200px;
height: 200px;
display: block;
}
}
.group-member-list {
padding: 0 12px;
display: flex;
align-items: center;
flex-wrap: wrap;
text-align: center;
.send-btn {
margin-top: 20px;
}
}
.group-member {
margin-right: 5px;
}
.group-member-list {
padding: 5px 20px;
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 16px;
text-align: center;
.group-invite {
display: flex;
flex-direction: column;
align-items: center;
width: 60px;
.group-member {
margin-right: 15px;
}
.invite-member-btn {
width: 38px;
height: 38px;
line-height: 38px;
border: var(--im-border);
font-size: 14px;
cursor: pointer;
box-sizing: border-box;
.group-invite {
display: flex;
flex-direction: column;
align-items: center;
width: 60px;
&:hover {
border: #aaaaaa solid 1px;
}
}
.invite-member-btn {
width: 100%;
height: 60px;
line-height: 60px;
border: #cccccc solid 1px;
font-size: 25px;
cursor: pointer;
box-sizing: border-box;
.invite-member-text {
font-size: var(--im-font-size-smaller);
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
&:hover {
border: #aaaaaa solid 1px;
}
}
}
}
.invite-member-text {
font-size: 16px;
text-align: center;
width: 100%;
height: 30px;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
}
}
}
}
}
}
}
</style>

904
im-web/src/view/Home.vue

@ -1,431 +1,515 @@
<template>
<el-container class="home-page">
<el-aside width="80px" class="navi-bar">
<div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb" :size="60"
@click.native="showSettingDialog = true">
</head-image>
</div>
<el-menu background-color="#E8F2FF" style="margin-top: 25px;">
<el-menu-item title="聊天">
<router-link class="link" v-bind:to="'/home/chat'">
<span class="icon iconfont icon-chat"></span>
<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
</router-link>
</el-menu-item>
<el-menu-item title="好友">
<router-link class="link" v-bind:to="'/home/friend'">
<span class="icon iconfont icon-friend"></span>
</router-link>
</el-menu-item>
<el-menu-item title="群聊">
<router-link class="link" v-bind:to="'/home/group'">
<span class="icon iconfont icon-group"></span>
</router-link>
</el-menu-item>
<el-menu-item title="设置" @click="showSetting()">
<span class="icon iconfont icon-setting"></span>
</el-menu-item>
</el-menu>
<div class="exit-box" @click="onExit()" title="退出">
<span class="icon iconfont icon-exit"></span>
</div>
</el-aside>
<el-main class="content-box">
<router-view></router-view>
</el-main>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
</el-container>
<div class="home-page">
<div class="app-container" :class="{fullscreen: isFullscreen}">
<div class="navi-bar">
<div class="navi-bar-box">
<div class="top">
<div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName"
:size="38"
:url="$store.state.userStore.userInfo.headImageThumb"
@click.native="showSettingDialog = true">
</head-image>
</div>
<div class="menu">
<router-link class="link" v-bind:to="'/home/chat'">
<div class="menu-item">
<span class="icon iconfont icon-chat"></span>
<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
</div>
</router-link>
<router-link class="link" v-bind:to="'/home/friend'">
<div class="menu-item">
<span class="icon iconfont icon-friend"></span>
</div>
</router-link>
<router-link class="link" v-bind:to="'/home/group'">
<div class="menu-item">
<span class="icon iconfont icon-group" style="font-size: 28px"></span>
</div>
</router-link>
</div>
</div>
<div class="botoom">
<div class="botoom-item" @click="isFullscreen = !isFullscreen">
<i class="el-icon-full-screen"></i>
</div>
<div class="botoom-item" @click="showSetting">
<span class="icon iconfont icon-setting" style="font-size: 20px"></span>
</div>
<div class="botoom-item" @click="onExit()" title="退出">
<span class="icon iconfont icon-exit"></span>
</div>
</div>
</div>
</div>
<div class="content-box">
<router-view></router-view>
</div>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
</div>
</div>
</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 RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue';
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.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 RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue';
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue';
export default {
components: {
HeadImage,
Setting,
UserInfo,
FullImage,
RtcPrivateVideo,
RtcPrivateAcceptor,
RtcGroupVideo
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date().getTime() - 1000,
isFullscreen: false
}
},
methods: {
init() {
this.$eventBus.$on('openPrivateVideo', (rctInfo) => {
//
this.$refs.rtcPrivateVideo.open(rctInfo);
});
this.$eventBus.$on('openGroupVideo', (rctInfo) => {
//
this.$refs.rtcGroupVideo.open(rctInfo);
});
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,
RtcPrivateVideo,
RtcPrivateAcceptor,
RtcGroupVideo
},
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date().getTime() - 1000
}
},
methods: {
init() {
this.$eventBus.$on('openPrivateVideo', (rctInfo) => {
//
this.$refs.rtcPrivateVideo.open(rctInfo);
});
this.$eventBus.$on('openGroupVideo', (rctInfo) => {
//
this.$refs.rtcGroupVideo.open(rctInfo);
});
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
} else if (cmd == 5) {
//
this.handleSystemMessage(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.$store.commit("loadingPrivateMsg", true)
this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'GET'
}).catch(() => {
this.$store.commit("loadingPrivateMsg", false)
})
},
pullGroupOfflineMessage(minId) {
this.$store.commit("loadingGroupMsg", true)
this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'GET'
}).catch(() => {
this.$store.commit("loadingGroupMsg", false)
})
},
handlePrivateMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
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;
// webrtc
if (this.$msgType.isRtcPrivate(msg.type)) {
this.$refs.rtcPrivateVideo.onRTCMessage(msg)
return;
}
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
},
insertPrivateMessage(friend, msg) {
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 = "/";
}
});
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 && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
handleGroupMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
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;
//
if (this.$msgType.isRtcGroup(msg.type)) {
this.$nextTick(() => {
this.$refs.rtcGroupVideo.onRTCMessage(msg);
})
return;
}
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
} else if (cmd == 5){
//
this.handleSystemMessage(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.$store.commit("loadingPrivateMsg", true)
this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'GET'
}).catch(() => {
this.$store.commit("loadingPrivateMsg", false)
})
},
pullGroupOfflineMessage(minId) {
this.$store.commit("loadingGroupMsg", true)
this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'GET'
}).catch(() => {
this.$store.commit("loadingGroupMsg", false)
})
},
handlePrivateMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
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;
// webrtc
if (this.$msgType.isRtcPrivate(msg.type)) {
this.$refs.rtcPrivateVideo.onRTCMessage(msg)
return;
}
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
},
insertPrivateMessage(friend, msg) {
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 && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
handleGroupMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
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;
//
if (this.$msgType.isRtcGroup(msg.type)) {
this.$nextTick(() => {
this.$refs.rtcGroupVideo.onRTCMessage(msg);
})
return;
}
this.loadGroupInfo(msg.groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.showGroupName,
headImage: group.headImageThumb
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
handleSystemMessage(msg){
//
if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
this.$wsApi.close(3000);
this.$alert("您的账号已被管理员封禁,原因:"+ msg.content, "账号被封禁", {
confirmButtonText: '确定',
callback: action => {
this.onExit();
}
});
return;
}
},
onExit() {
this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken");
location.href = "/";
},
playAudioTip() {
// 线
if(this.$store.getters.isLoading()){
return;
}
//
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);
})
}
});
}
},
computed: {
uiStore() {
return this.$store.state.uiStore;
},
unreadCount() {
let unreadCount = 0;
let chats = this.$store.state.chatStore.chats;
chats.forEach((chat) => {
if(!chat.delete){
unreadCount += chat.unreadCount
}
});
return unreadCount;
}
},
watch: {
unreadCount: {
handler(newCount, oldCount) {
let tip = newCount > 0 ? `${newCount}条未读` : "";
this.$elm.setTitleTip(tip);
},
immediate: true
}
},
mounted() {
this.init();
},
unmounted() {
this.$wsApi.close();
}
}
let chatInfo = {
type: 'GROUP',
targetId: group.id,
showName: group.showGroupName,
headImage: group.headImageThumb
};
//
this.$store.commit("openChat", chatInfo);
//
this.$store.commit("insertMessage", msg);
//
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip();
}
},
handleSystemMessage(msg) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
this.$wsApi.close(3000);
this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
confirmButtonText: '确定',
callback: action => {
this.onExit();
}
});
return;
}
},
onExit() {
this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken");
location.href = "/";
},
playAudioTip() {
// 线
if (this.$store.getters.isLoading()) {
return;
}
//
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);
})
}
});
}
},
computed: {
uiStore() {
return this.$store.state.uiStore;
},
unreadCount() {
let unreadCount = 0;
let chats = this.$store.state.chatStore.chats;
chats.forEach((chat) => {
if (!chat.delete) {
unreadCount += chat.unreadCount
}
});
return unreadCount;
}
},
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: #E8F2FF;
padding: 10px;
padding-top: 20px;
border-right: #53a0e79c solid 1px;
.home-page {
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
border-radius: 4px;
overflow: hidden;
background: #e8f2ff;
//background-image: url('../assets/image/background.jpg');
.app-container {
width: 62vw;
height: 80vh;
display: flex;
min-height: 600px;
min-width: 970px;
position: absolute;
border-radius: 4px;
overflow: hidden;
box-shadow: var(--im-box-shadow-dark);
transition: 0.2s;
&.fullscreen {
transition: 0.2s;
width: 100vw;
height: 100vh;
}
}
.navi-bar {
--icon-font-size: 22px;
--width: 56px;
width: var(--width);
background: var(--im-color-primary);
padding-top: 20px;
.navi-bar-box {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
.botoom {
margin-bottom: 30px;
}
}
.user-head-image {
display: flex;
justify-content: center;
}
--menu-color: #9DC4FF;
.menu {
height: 200px;
//margin-top: 10px;
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
.link {
text-decoration: none;
}
.router-link-active .menu-item {
color: #fff;
background: var(--im-color-primary-light-2);
}
.el-menu {
border: none;
flex: 1;
.link:not(.router-link-active) .menu-item:hover {
color: #fff;
}
.el-menu-item {
margin: 25px 0;
background-color: #E8F2FF !important;
padding: 0 !important;
text-align: center;
.menu-item {
position: relative;
color: var(--menu-color);
width: var(--width);
height: 46px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
.link {
text-decoration: none;
.icon {
font-size: var(--icon-font-size)
}
&.router-link-active .icon {
color: #195ee2;
font-size: 28px;
}
}
.unread-text {
position: absolute;
background-color: var(--im-color-danger);
left: 28px;
top: 8px;
color: white;
border-radius: 30px;
padding: 0 5px;
font-size: 12px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
}
.icon {
font-size: 26px;
color: #666;
}
.botoom-item {
display: flex;
justify-content: center;
align-items: center;
height: 50px;
width: 100%;
cursor: pointer;
color: var(--menu-color);
font-size: var(--icon-font-size);
.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;
}
}
}
.icon {
font-size: var(--icon-font-size)
}
.exit-box {
position: absolute;
width: 60px;
bottom: 40px;
text-align: center;
cursor: pointer;
&:hover {
font-weight: 600;
color: #fff;
}
}
}
.icon {
font-size: 28px;
}
.content-box {
flex: 1;
padding: 0;
background-color: #fff;
text-align: center;
}
}
&:hover {
font-weight: 600;
}
}
}
.content-box {
padding: 0;
background-color: #f8f8f8;
color: black;
text-align: center;
}
</style>
Loading…
Cancel
Save