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

117
im-web/src/App.vue

@ -1,115 +1,26 @@
<template> <template>
<div id="app"> <div id="app">
<router-view></router-view> <router-view></router-view>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'App', name: 'App',
components: {} components: {}
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import './assets/style/global.css';
#app { #app {
font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
-moz-osx-font-smoothing: grayscale; position: absolute;
position: absolute; height: 100%;
height: 100%; width: 100%;
width: 100%; color: var(--im-text-color);
} font-family: var(--im-font-family);
.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;
} }
.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> </style>

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

@ -19,7 +19,7 @@ let textToImg = (emoText) => {
return emoText; return emoText;
} }
let url = require(`@/assets/emoji/${idx}.gif`); 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) => { 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; position: fixed;
width: 200px; width: 200px;
height: 300px; height: 300px;
border: 1px solid #53a0e79c; //border: 1px solid #53a0e79c;
border-radius: 5px; //border-radius: 5px;
background-color: #f5f5f5; background-color: #fff;
box-shadow: 0px 0px 10px #ccc; box-shadow: var(--im-box-shadow);
} }
</style> </style>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
<template> <template>
<div v-show="show" @click="close()"> <div v-show="show" @click="close()">
<div class="emotion-box" :style="{'left':x+'px','top':y+'px'}"> <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-list">
<div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i" <div class="emotion-item" v-for="(emoText, i) in $emo.emoTextList" :key="i"
@click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)"> @click="onClickEmo(emoText)" v-html="$emo.textToImg(emoText)">
@ -39,26 +39,23 @@
}, },
computed: { computed: {
x() { x() {
return this.pos.x - 200; return this.pos.x - 22;
}, },
y() { y() {
return this.pos.y - 280; return this.pos.y - 234;
} }
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.emotion-box { .emotion-box {
position: fixed; position: fixed;
width: 500px; width: 372px;
box-sizing: border-box; box-sizing: border-box;
padding: 5px; padding: 5px;
border: 1px solid #53a0e79c; //border-radius: 5px;
border-radius: 5px; background-color: #fff;
background-color: #f5f5f5; box-shadow: var(--im-box-shadow);
box-shadow: 0px 0px 10px #ccc;
.emotion-item-list { .emotion-item-list {
display: flex; display: flex;
@ -67,22 +64,22 @@
.emotion-item { .emotion-item {
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
padding: 5px; padding: 2px;
} }
} }
&:after { //&:after {
content: ""; // content: "";
position: absolute; // position: absolute;
left: 185px; // left: 185px;
bottom: -30px; // bottom: -30px;
width: 0; // width: 0;
height: 0; // height: 0;
border-style: solid dashed dashed; // border-style: solid dashed dashed;
border-color: #f5f5f5 transparent transparent; // border-color: #f5f5f5 transparent transparent;
overflow: hidden; // overflow: hidden;
border-width: 15px; // border-width: 15px;
} //}
} }
</style> </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="full-image" v-show="visible" :before-close="onClose" :modal="true">
<div class="mask"></div> <div class="mask"></div>
<div class="image-box"> <div class="image-box">
<img :src="url"/> <img :src="url"/>
</div> </div>
<div class="close" @click="onClose">x</div> <div class="close" @click="onClose"><i class="el-icon-close"></i></div>
</div> </div>
</template> </template>
@ -37,14 +37,18 @@
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
left: 0;
top: 0;
bottom: 0;
right: 0;
.mask{ .mask{
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: black; background: black;
opacity: 0.9; opacity: 0.5;
} }
.image-box { .image-box {
@ -54,11 +58,11 @@
img{ img{
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
max-height: 100%; max-height: 100%;
max-width: 100%; max-width: 100%;
} }
} }
@ -69,8 +73,6 @@
color: white; color: white;
font-size: 25px; font-size: 25px;
cursor: pointer; cursor: pointer;
} }
} }

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

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

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

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

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

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

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

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

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

@ -1,16 +1,18 @@
<template> <template>
<div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)"> <div class="friend-item" :class="active ? 'active' : ''" @contextmenu.prevent="showRightMenu($event)">
<div class="friend-avatar"> <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> </head-image>
</div> </div>
<div class="friend-info"> <div class="friend-info">
<div class="friend-name">{{ friend.nickName}}</div> <div class="friend-name">{{ friend.nickName}}</div>
<div class="friend-online"> <div class="friend-online">
<el-image v-show="friend.onlineWeb" class="online" :src="require('@/assets/image/online_web.png')" <i class="el-icon-monitor online" v-show="friend.onlineWeb" title="电脑设备在线">
title="电脑设备在线" /> <span class="online-icon"></span>
<el-image v-show="friend.onlineApp" class="online" :src="require('@/assets/image/online_app.png')" </i>
title="移动设备在线" /> <i class="el-icon-mobile-phone online" v-show="friend.onlineApp" title="移动设备在线">
<span class="online-icon"></span>
</i>
</div> </div>
</div> </div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items" <right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="rightMenu.items"
@ -86,28 +88,24 @@
.friend-item { .friend-item {
height: 50px; height: 50px;
display: flex; display: flex;
margin-bottom: 1px;
position: relative; position: relative;
padding: 5px 10px; padding: 5px 10px;
align-items: center; align-items: center;
background-color: #fafafa;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: #F8FAFF; background-color: var(--im-background-active);
} }
&.active { &.active {
background-color: #F4F9FF; background-color: var(--im-background-active-dark);
} }
.friend-avatar { .friend-avatar {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 45px;
height: 45px;
} }
.friend-info { .friend-info {
@ -118,20 +116,29 @@
text-align: left; text-align: left;
.friend-name { .friend-name {
font-size: 15px; font-size: var(--im-font-size);
font-weight: 600;
line-height: 30px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
} }
.friend-online { .friend-online {
.online { .online {
font-weight: bold;
padding-right: 2px; padding-right: 2px;
width: 15px; font-size: 16px;
height: 15px; 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> <template>
<el-dialog title="邀请好友" :visible.sync="visible" width="50%" :before-close="onClose"> <el-dialog title="邀请好友" :visible.sync="visible" width="620px" :before-close="onClose">
<div class="agm-container"> <div class="agm-container">
<div class="agm-l-box"> <div class="agm-l-box">
<el-input placeholder="搜索好友" v-model="searchText"> <div class="search">
<i class="el-icon-search el-input__icon" slot="suffix"> </i> <el-input placeholder="搜索好友" v-model="searchText" size="small">
</el-input> <i class="el-icon-search el-input__icon" slot="suffix"> </i>
<el-scrollbar style="height:400px;"> </el-input>
<div v-for="(friend,index) in friends" :key="friend.id"> </div>
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false" <el-scrollbar style="height:400px;">
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" <div v-for="(friend,index) in friends" :key="friend.id">
:active="false"> <friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false"
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" @click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index"
v-model="friend.isCheck" size="medium"></el-checkbox> :active="false">
</friend-item> <el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox"
</div> v-model="friend.isCheck" size="medium"></el-checkbox>
</el-scrollbar> </friend-item>
</div> </div>
<div class="agm-arrow el-icon-d-arrow-right"></div> </el-scrollbar>
<div class="agm-r-box"> </div>
<div class="agm-select-tip"> 已勾选{{checkCount}}位好友</div> <div class="agm-arrow el-icon-d-arrow-right"></div>
<el-scrollbar style="height:400px;"> <div class="agm-r-box">
<div v-for="(friend,index) in friends" :key="friend.id"> <div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div>
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" <el-scrollbar style="height:400px;">
:active="false" @del="onRemoveFriend(friend,index)" :menu="false"> <div v-for="(friend,index) in friends" :key="friend.id">
</friend-item> <friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index"
</div> :active="false" @del="onRemoveFriend(friend,index)" :menu="false">
</el-scrollbar> </friend-item>
</div> </div>
</div> </el-scrollbar>
<span slot="footer" class="dialog-footer"> </div>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="onClose()"> </el-button> <el-button @click="onClose()"> </el-button>
<el-button type="primary" @click="onOk()"> </el-button> <el-button type="primary" @click="onOk()"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
import FriendItem from '../friend/FriendItem.vue'; import FriendItem from '../friend/FriendItem.vue';
export default { export default {
name: "addGroupMember", name: "addGroupMember",
components: { components: {
FriendItem FriendItem
}, },
data() { data() {
return { return {
searchText: "", searchText: "",
friends: [] friends: []
} }
}, },
methods: { methods: {
onClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
onOk() { onOk() {
let inviteVO = { let inviteVO = {
groupId: this.groupId, groupId: this.groupId,
friendIds: [] friendIds: []
} }
this.friends.forEach((f) => { this.friends.forEach((f) => {
if (f.isCheck && !f.disabled) { if (f.isCheck && !f.disabled) {
inviteVO.friendIds.push(f.id); inviteVO.friendIds.push(f.id);
} }
}) })
if (inviteVO.friendIds.length > 0) { if (inviteVO.friendIds.length > 0) {
this.$http({ this.$http({
url: "/group/invite", url: "/group/invite",
method: 'post', method: 'post',
data: inviteVO data: inviteVO
}).then(() => { }).then(() => {
this.$message.success("邀请成功"); this.$message.success("邀请成功");
this.$emit("reload"); this.$emit("reload");
this.$emit("close"); this.$emit("close");
}) })
} }
}, },
onRemoveFriend(friend, index) { onRemoveFriend(friend, index) {
friend.isCheck = false; friend.isCheck = false;
}, },
onSwitchCheck(friend) { onSwitchCheck(friend) {
if (!friend.disabled) { if (!friend.disabled) {
friend.isCheck = !friend.isCheck friend.isCheck = !friend.isCheck
} }
} }
}, },
props: { props: {
visible: { visible: {
type: Boolean type: Boolean
}, },
groupId: { groupId: {
type: Number type: Number
}, },
members: { members: {
type: Array type: Array
} }
}, },
computed: { computed: {
checkCount() { checkCount() {
return this.friends.filter((f) => f.isCheck && !f.disabled).length; return this.friends.filter((f) => f.isCheck && !f.disabled).length;
} }
}, },
watch: { watch: {
visible: function(newData, oldData) { visible: function (newData, oldData) {
if (newData) { if (newData) {
this.friends = []; this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => { this.$store.state.friendStore.friends.forEach((f) => {
let friend = JSON.parse(JSON.stringify(f)) let friend = JSON.parse(JSON.stringify(f))
let m = this.members.filter((m) => !m.quit) let m = this.members.filter((m) => !m.quit)
.find((m) => m.userId == f.id); .find((m) => m.userId == f.id);
if (m) { if (m) {
// //
friend.disabled = true; friend.disabled = true;
friend.isCheck = true friend.isCheck = true
} else { } else {
friend.disabled = false; friend.disabled = false;
friend.isCheck = false; friend.isCheck = false;
} }
this.friends.push(friend); this.friends.push(friend);
}) })
} }
} }
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">
.agm-container { .agm-container {
display: flex; display: flex;
.agm-l-box {
flex: 1;
border: #587FF0 solid 1px;
border-radius: 5px;
overflow: hidden;
.agm-l-box {
flex: 1;
overflow: hidden;
border: var(--im-border);
.agm-friend-checkbox { .search {
margin-right: 20px; height: 40px;
} display: flex;
} align-items: center;
.agm-arrow { .el-input__inner {
display: flex; border: unset;
align-items: center; border-bottom: var(--im-border);
font-size: 20px; }
padding: 10px;
font-weight: 600;
color: #687Ff0;
}
.agm-r-box { }
flex: 1;
border: #587FF0 solid 1px;
border-radius: 5px;
.agm-select-tip { .agm-friend-checkbox {
text-align: left; margin-right: 20px;
height: 40px; }
line-height: 40px; }
text-indent: 5px;
} .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> </style>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4
im-web/src/main.js

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

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

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

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

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

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

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

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

@ -1,431 +1,515 @@
<template> <template>
<el-container class="home-page"> <div class="home-page">
<el-aside width="80px" class="navi-bar"> <div class="app-container" :class="{fullscreen: isFullscreen}">
<div class="user-head-image"> <div class="navi-bar">
<head-image :name="$store.state.userStore.userInfo.nickName" <div class="navi-bar-box">
:url="$store.state.userStore.userInfo.headImageThumb" :size="60" <div class="top">
@click.native="showSettingDialog = true"> <div class="user-head-image">
</head-image> <head-image :name="$store.state.userStore.userInfo.nickName"
</div> :size="38"
<el-menu background-color="#E8F2FF" style="margin-top: 25px;"> :url="$store.state.userStore.userInfo.headImageThumb"
<el-menu-item title="聊天"> @click.native="showSettingDialog = true">
<router-link class="link" v-bind:to="'/home/chat'"> </head-image>
<span class="icon iconfont icon-chat"></span> </div>
<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
</router-link> <div class="menu">
</el-menu-item> <router-link class="link" v-bind:to="'/home/chat'">
<el-menu-item title="好友"> <div class="menu-item">
<router-link class="link" v-bind:to="'/home/friend'"> <span class="icon iconfont icon-chat"></span>
<span class="icon iconfont icon-friend"></span> <div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div>
</router-link> </div>
</el-menu-item> </router-link>
<el-menu-item title="群聊"> <router-link class="link" v-bind:to="'/home/friend'">
<router-link class="link" v-bind:to="'/home/group'"> <div class="menu-item">
<span class="icon iconfont icon-group"></span> <span class="icon iconfont icon-friend"></span>
</router-link> </div>
</el-menu-item> </router-link>
<el-menu-item title="设置" @click="showSetting()"> <router-link class="link" v-bind:to="'/home/group'">
<span class="icon iconfont icon-setting"></span> <div class="menu-item">
</el-menu-item> <span class="icon iconfont icon-group" style="font-size: 28px"></span>
</el-menu> </div>
<div class="exit-box" @click="onExit()" title="退出"> </router-link>
<span class="icon iconfont icon-exit"></span> </div>
</div> </div>
</el-aside>
<el-main class="content-box"> <div class="botoom">
<router-view></router-view> <div class="botoom-item" @click="isFullscreen = !isFullscreen">
</el-main> <i class="el-icon-full-screen"></i>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting> </div>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" <div class="botoom-item" @click="showSetting">
@close="$store.commit('closeUserInfoBox')"></user-info> <span class="icon iconfont icon-setting" style="font-size: 20px"></span>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" </div>
@close="$store.commit('closeFullImageBox')"></full-image> <div class="botoom-item" @click="onExit()" title="退出">
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video> <span class="icon iconfont icon-exit"></span>
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video> </div>
</el-container> </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> </template>
<script> <script>
import HeadImage from '../components/common/HeadImage.vue'; import HeadImage from '../components/common/HeadImage.vue';
import Setting from '../components/setting/Setting.vue'; import Setting from '../components/setting/Setting.vue';
import UserInfo from '../components/common/UserInfo.vue'; import UserInfo from '../components/common/UserInfo.vue';
import FullImage from '../components/common/FullImage.vue'; import FullImage from '../components/common/FullImage.vue';
import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue'; import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue';
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue'; import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue';
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.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 { } else if (cmd == 3) {
components: { //
HeadImage, this.handlePrivateMessage(msgInfo);
Setting, } else if (cmd == 4) {
UserInfo, //
FullImage, this.handleGroupMessage(msgInfo);
RtcPrivateVideo, } else if (cmd == 5) {
RtcPrivateAcceptor, //
RtcGroupVideo this.handleSystemMessage(msgInfo);
}, }
data() { });
return { this.$wsApi.onClose((e) => {
showSettingDialog: false, console.log(e);
lastPlayAudioTime: new Date().getTime() - 1000 if (e.code != 3000) {
} // 线
}, this.$message.error("连接断开,正在尝试重新连接...");
methods: { this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
init() { "accessToken"));
this.$eventBus.$on('openPrivateVideo', (rctInfo) => { }
// });
this.$refs.rtcPrivateVideo.open(rctInfo); }).catch((e) => {
}); console.log("初始化失败", e);
this.$eventBus.$on('openGroupVideo', (rctInfo) => { })
// },
this.$refs.rtcGroupVideo.open(rctInfo); 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(() => { let chatInfo = {
// ws type: 'PRIVATE',
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); targetId: friend.id,
this.$wsApi.onConnect(() => { showName: friend.nickName,
// 线 headImage: friend.headImage
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); };
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); //
}); this.$store.commit("openChat", chatInfo);
this.$wsApi.onMessage((cmd, msgInfo) => { //
if (cmd == 2) { this.$store.commit("insertMessage", msg);
// ws //
this.$wsApi.close(3000) if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
// 线 msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", { this.playAudioTip();
confirmButtonText: '确定', }
callback: action => { },
location.href = "/"; 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) { let chatInfo = {
// type: 'GROUP',
this.handlePrivateMessage(msgInfo); targetId: group.id,
} else if (cmd == 4) { showName: group.showGroupName,
// headImage: group.headImageThumb
this.handleGroupMessage(msgInfo); };
} else if (cmd == 5){ //
// this.$store.commit("openChat", chatInfo);
this.handleSystemMessage(msgInfo); //
} this.$store.commit("insertMessage", msg);
}); //
this.$wsApi.onClose((e) => { if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
console.log(e); msg.status != this.$enums.MESSAGE_STATUS.READED) {
if (e.code != 3000) { this.playAudioTip();
// 线 }
this.$message.error("连接断开,正在尝试重新连接..."); },
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem( handleSystemMessage(msg) {
"accessToken")); //
}
}); if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) {
}).catch((e) => { this.$wsApi.close(3000);
console.log("初始化失败", e); this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
}) confirmButtonText: '确定',
}, callback: action => {
pullPrivateOfflineMessage(minId) { this.onExit();
this.$store.commit("loadingPrivateMsg", true) }
this.$http({ });
url: "/message/private/pullOfflineMessage?minId=" + minId, return;
method: 'GET' }
}).catch(() => { },
this.$store.commit("loadingPrivateMsg", false) onExit() {
}) this.$wsApi.close(3000);
}, sessionStorage.removeItem("accessToken");
pullGroupOfflineMessage(minId) { location.href = "/";
this.$store.commit("loadingGroupMsg", true) },
this.$http({ playAudioTip() {
url: "/message/group/pullOfflineMessage?minId=" + minId, // 线
method: 'GET' if (this.$store.getters.isLoading()) {
}).catch(() => { return;
this.$store.commit("loadingGroupMsg", false) }
}) //
}, if (new Date().getTime() - this.lastPlayAudioTime > 1000) {
handlePrivateMessage(msg) { this.lastPlayAudioTime = new Date().getTime();
// let audio = new Audio();
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { let url = require(`@/assets/audio/tip.wav`);
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content)) audio.src = url;
return; audio.play();
} }
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { },
this.$store.commit("resetUnreadCount", { showSetting() {
type: 'PRIVATE', this.showSettingDialog = true;
targetId: msg.recvId },
}) closeSetting() {
return; this.showSettingDialog = false;
} },
// , loadFriendInfo(id) {
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { return new Promise((resolve, reject) => {
this.$store.commit("readedMessage", { let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
friendId: msg.sendId if (friend) {
}) resolve(friend);
return; } else {
} this.$http({
// url: `/friend/find/${id}`,
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; method: 'get'
// webrtc }).then((friend) => {
if (this.$msgType.isRtcPrivate(msg.type)) { this.$store.commit("addFriend", friend);
this.$refs.rtcPrivateVideo.onRTCMessage(msg) resolve(friend)
return; })
} }
// id });
let friendId = msg.selfSend ? msg.recvId : msg.sendId; },
this.loadFriendInfo(friendId).then((friend) => { loadGroupInfo(id) {
this.insertPrivateMessage(friend, msg); return new Promise((resolve, reject) => {
}) let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
}, if (group) {
insertPrivateMessage(friend, msg) { resolve(group);
let chatInfo = { } else {
type: 'PRIVATE', this.$http({
targetId: friend.id, url: `/group/find/${id}`,
showName: friend.nickName, method: 'get'
headImage: friend.headImage }).then((group) => {
}; resolve(group)
// this.$store.commit("addGroup", group);
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) { computed: {
this.playAudioTip(); uiStore() {
} return this.$store.state.uiStore;
}, },
handleGroupMessage(msg) { unreadCount() {
// let unreadCount = 0;
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { let chats = this.$store.state.chatStore.chats;
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content)) chats.forEach((chat) => {
return; if (!chat.delete) {
} unreadCount += chat.unreadCount
// }
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { });
// return unreadCount;
let chatInfo = { }
type: 'GROUP', },
targetId: msg.groupId watch: {
} unreadCount: {
this.$store.commit("resetUnreadCount", chatInfo) handler(newCount, oldCount) {
return; let tip = newCount > 0 ? `${newCount}条未读` : "";
} this.$elm.setTitleTip(tip);
// },
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { immediate: true
// }
let msgInfo = { },
id: msg.id, mounted() {
groupId: msg.groupId, this.init();
readedCount: msg.readedCount, },
receiptOk: msg.receiptOk unmounted() {
}; this.$wsApi.close();
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();
}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.navi-bar { .home-page {
background: #E8F2FF; height: 100vh;
padding: 10px; width: 100vw;
padding-top: 20px; display: flex;
border-right: #53a0e79c solid 1px; 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 { .link:not(.router-link-active) .menu-item:hover {
border: none; color: #fff;
flex: 1; }
.el-menu-item { .menu-item {
margin: 25px 0; position: relative;
background-color: #E8F2FF !important; color: var(--menu-color);
padding: 0 !important; width: var(--width);
text-align: center; height: 46px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 12px;
.link { .icon {
text-decoration: none; font-size: var(--icon-font-size)
}
&.router-link-active .icon { .unread-text {
color: #195ee2; position: absolute;
font-size: 28px; 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 { .botoom-item {
font-size: 26px; display: flex;
color: #666; justify-content: center;
} align-items: center;
height: 50px;
width: 100%;
cursor: pointer;
color: var(--menu-color);
font-size: var(--icon-font-size);
.unread-text { .icon {
position: absolute; font-size: var(--icon-font-size)
line-height: 20px; }
background-color: #f56c6c;
left: 36px;
top: 7px;
color: white;
border-radius: 30px;
padding: 0 5px;
font-size: 10px;
text-align: center;
white-space: nowrap;
border: 1px solid #f1e5e5;
}
}
}
.exit-box { &:hover {
position: absolute; font-weight: 600;
width: 60px; color: #fff;
bottom: 40px; }
text-align: center; }
cursor: pointer; }
.icon { .content-box {
font-size: 28px; 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> </style>
Loading…
Cancel
Save