37 changed files with 2593 additions and 2409 deletions
@ -1,115 +1,26 @@ |
|||
<template> |
|||
<div id="app"> |
|||
<router-view></router-view> |
|||
</div> |
|||
<div id="app"> |
|||
<router-view></router-view> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'App', |
|||
components: {} |
|||
} |
|||
export default { |
|||
name: 'App', |
|||
components: {} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
@import './assets/style/global.css'; |
|||
|
|||
#app { |
|||
font-family: 'Avenir', Helvetica, Arial, sans-serif; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 100%; |
|||
} |
|||
|
|||
.el-message { |
|||
z-index: 99999999 !important; |
|||
} |
|||
|
|||
.el-scrollbar__thumb { |
|||
background-color: #A0A8AF !important; |
|||
} |
|||
|
|||
.el-dialog { |
|||
border-radius: 8px !important; |
|||
overflow: hidden !important; |
|||
} |
|||
|
|||
.el-dialog__header { |
|||
background-color: #5870e6 !important; |
|||
|
|||
} |
|||
|
|||
.el-dialog__title { |
|||
color: #f8f8f8 !important; |
|||
} |
|||
|
|||
.el-dialog__close { |
|||
color: white !important; |
|||
font-size: 20px; |
|||
} |
|||
|
|||
.el-checkbox__inner { |
|||
border-radius: 50% !important; |
|||
} |
|||
|
|||
.el-input__inner { |
|||
border-radius: 5px !important; |
|||
border: #587FF0 1px solid !important; |
|||
} |
|||
|
|||
.el-textarea__inner{ |
|||
border-radius: 5px !important; |
|||
border: #587FF0 1px solid !important; |
|||
} |
|||
|
|||
.el-icon-search { |
|||
color:#587FF0 !important; |
|||
-webkit-font-smoothing: antialiased; |
|||
-moz-osx-font-smoothing: grayscale; |
|||
position: absolute; |
|||
height: 100%; |
|||
width: 100%; |
|||
color: var(--im-text-color); |
|||
font-family: var(--im-font-family); |
|||
} |
|||
|
|||
.el-button--primary { |
|||
background-color: #687Ff0 !important; |
|||
border-color: #687Ff0 !important; |
|||
} |
|||
|
|||
.el-button--success { |
|||
background-color: #4cae1b !important; |
|||
border-color: #4cae1b !important; |
|||
} |
|||
.el-button--danger { |
|||
background-color: #ea4949 !important; |
|||
border-color: #ea4949 !important; |
|||
} |
|||
|
|||
.el-button { |
|||
padding: 8px 15px !important; |
|||
} |
|||
|
|||
.el-checkbox { |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
//修改选中框的大小 |
|||
.el-checkbox__inner { |
|||
width: 20px; |
|||
height: 20px; |
|||
|
|||
//修改选中框中的对勾的大小和位置 |
|||
&::after { |
|||
height: 12px; |
|||
left: 7px; |
|||
} |
|||
} |
|||
|
|||
//修改点击文字颜色不变 |
|||
.el-checkbox__input.is-checked+.el-checkbox__label { |
|||
color: #333333; |
|||
} |
|||
|
|||
.el-checkbox__label { |
|||
line-height: 20px; |
|||
padding-left: 8px; |
|||
} |
|||
} |
|||
</style> |
|||
|
Before Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
@ -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; |
|||
} |
|||
@ -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 */ |
|||
@ -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; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
// 主题色 |
|||
$--color-primary: #2830d3; |
|||
//$--color-primary: #687ff0; |
|||
//$--color-primary: #096bff; |
|||
$--font-size-base: 14px; |
|||
$--color-text-regular: #000000; |
|||
@ -1,206 +1,232 @@ |
|||
<template> |
|||
<div class="chat-group-side"> |
|||
<div v-show="!group.quit" class="group-side-search"> |
|||
<el-input placeholder="搜索群成员" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
</div> |
|||
<el-scrollbar class="group-side-scrollbar"> |
|||
<div v-show="!group.quit" class="group-side-member-list"> |
|||
<div class="group-side-invite"> |
|||
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true"> |
|||
<i class="el-icon-plus"></i> |
|||
</div> |
|||
<div class="invite-member-text">邀请</div> |
|||
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers" @reload="$emit('reload')" |
|||
@close="showAddGroupMember=false"></add-group-member> |
|||
</div> |
|||
<div v-for="(member) in groupMembers" :key="member.id"> |
|||
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)" :member="member" |
|||
:showDel="false"></group-member> |
|||
</div> |
|||
</div> |
|||
<el-divider v-if="!group.quit" content-position="center"></el-divider> |
|||
<el-form labelPosition="top" class="group-side-form" :model="group"> |
|||
<el-form-item label="群聊名称"> |
|||
<el-input v-model="group.name" disabled maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群主"> |
|||
<el-input :value="ownerName" disabled></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群公告"> |
|||
<el-input v-model="group.notice" disabled type="textarea" maxlength="1024" placeholder="群主未设置"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="备注"> |
|||
<el-input v-model="group.remarkGroupName" :disabled="!editing" :placeholder="group.name" maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="我在本群的昵称"> |
|||
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20" |
|||
:placeholder="$store.state.userStore.userInfo.nickName" ></el-input> |
|||
</el-form-item> |
|||
<div v-show="!group.quit" class="btn-group"> |
|||
<el-button v-show="editing" type="success" @click="onSaveGroup()">提交</el-button> |
|||
<el-button v-show="!editing" type="primary" @click="editing=!editing">编辑</el-button> |
|||
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button> |
|||
</div> |
|||
</el-form> |
|||
</el-scrollbar> |
|||
|
|||
</div> |
|||
<div class="chat-group-side"> |
|||
<div v-show="!group.quit" class="group-side-search"> |
|||
<el-input placeholder="搜索群成员" v-model="searchText" size="small"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
</div> |
|||
<div class="group-side-scrollbar"> |
|||
<div v-show="!group.quit" class="group-side-member-list"> |
|||
<div class="group-side-invite"> |
|||
<div class="invite-member-btn" title="邀请好友进群聊" @click="showAddGroupMember=true"> |
|||
<i class="el-icon-plus"></i> |
|||
</div> |
|||
<div class="invite-member-text">邀请</div> |
|||
<add-group-member :visible="showAddGroupMember" :groupId="group.id" :members="groupMembers" |
|||
@reload="$emit('reload')" |
|||
@close="showAddGroupMember=false"></add-group-member> |
|||
</div> |
|||
<div v-for="(member) in groupMembers" :key="member.id"> |
|||
<group-member class="group-side-member" v-show="!member.quit && member.showNickName.includes(searchText)" |
|||
:member="member" |
|||
:showDel="false"></group-member> |
|||
</div> |
|||
</div> |
|||
<el-divider v-if="!group.quit" content-position="center"></el-divider> |
|||
<el-form labelPosition="top" class="group-side-form" :model="group" size="small"> |
|||
<el-form-item label="群聊名称"> |
|||
<el-input v-model="group.name" disabled maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群主"> |
|||
<el-input :value="ownerName" disabled></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群公告"> |
|||
<el-input v-model="group.notice" disabled type="textarea" maxlength="1024"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="备注"> |
|||
<el-input v-model="group.remarkGroupName" :disabled="!editing" |
|||
maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="我在本群的昵称"> |
|||
<el-input v-model="group.remarkNickName" :disabled="!editing" maxlength="20" |
|||
></el-input> |
|||
</el-form-item> |
|||
<div v-show="!group.quit" class="btn-group"> |
|||
<el-button v-if="editing" type="success" @click="onSaveGroup()">保存</el-button> |
|||
<el-button v-if="!editing" type="primary" @click="editing=!editing">编辑</el-button> |
|||
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button> |
|||
</div> |
|||
</el-form> |
|||
</div> |
|||
|
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import AddGroupMember from '../group/AddGroupMember.vue'; |
|||
import GroupMember from '../group/GroupMember.vue'; |
|||
|
|||
export default { |
|||
name: "chatGroupSide", |
|||
components: { |
|||
AddGroupMember, |
|||
GroupMember |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
editing: false, |
|||
showAddGroupMember: false |
|||
} |
|||
}, |
|||
props: { |
|||
group: { |
|||
type: Object |
|||
}, |
|||
groupMembers: { |
|||
type: Array |
|||
} |
|||
}, |
|||
methods: { |
|||
onClose() { |
|||
this.$emit('close'); |
|||
}, |
|||
loadGroupMembers() { |
|||
this.$http({ |
|||
url: `/group/members/${this.group.id}`, |
|||
method: "get" |
|||
}).then((members) => { |
|||
this.groupMembers = members; |
|||
}) |
|||
}, |
|||
onSaveGroup() { |
|||
let vo = this.group; |
|||
this.$http({ |
|||
url: "/group/modify", |
|||
method: "put", |
|||
data: vo |
|||
}).then((group) => { |
|||
this.$store.commit("updateGroup", group); |
|||
this.$emit('reload'); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
}, |
|||
onQuit() { |
|||
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/quit/${this.group.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$store.commit("removeGroup", this.group.id); |
|||
this.$store.commit("activeGroup", -1); |
|||
this.$store.commit("removeGroupChat", this.group.id); |
|||
}); |
|||
}) |
|||
}, |
|||
|
|||
}, |
|||
computed: { |
|||
ownerName() { |
|||
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId); |
|||
return member && member.showNickName; |
|||
}, |
|||
isOwner() { |
|||
return this.group.ownerId == this.$store.state.userStore.userInfo.id; |
|||
} |
|||
|
|||
} |
|||
} |
|||
import AddGroupMember from '../group/AddGroupMember.vue'; |
|||
import GroupMember from '../group/GroupMember.vue'; |
|||
|
|||
export default { |
|||
name: "chatGroupSide", |
|||
components: { |
|||
AddGroupMember, |
|||
GroupMember |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
editing: false, |
|||
showAddGroupMember: false |
|||
} |
|||
}, |
|||
props: { |
|||
group: { |
|||
type: Object |
|||
}, |
|||
groupMembers: { |
|||
type: Array |
|||
} |
|||
}, |
|||
methods: { |
|||
onClose() { |
|||
this.$emit('close'); |
|||
}, |
|||
loadGroupMembers() { |
|||
this.$http({ |
|||
url: `/group/members/${this.group.id}`, |
|||
method: "get" |
|||
}).then((members) => { |
|||
this.groupMembers = members; |
|||
}) |
|||
}, |
|||
onSaveGroup() { |
|||
let vo = this.group; |
|||
this.$http({ |
|||
url: "/group/modify", |
|||
method: "put", |
|||
data: vo |
|||
}).then((group) => { |
|||
this.editing = !this.editing |
|||
this.$store.commit("updateGroup", group); |
|||
this.$emit('reload'); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
}, |
|||
onQuit() { |
|||
this.$confirm('退出群聊后将不再接受群里的消息,确认退出吗?', '确认退出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/quit/${this.group.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$store.commit("removeGroup", this.group.id); |
|||
this.$store.commit("activeGroup", -1); |
|||
this.$store.commit("removeGroupChat", this.group.id); |
|||
}); |
|||
}) |
|||
}, |
|||
|
|||
}, |
|||
computed: { |
|||
ownerName() { |
|||
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId); |
|||
return member && member.showNickName; |
|||
}, |
|||
isOwner() { |
|||
return this.group.ownerId == this.$store.state.userStore.userInfo.id; |
|||
} |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.chat-group-side { |
|||
position: relative; |
|||
|
|||
.group-side-member-list { |
|||
padding: 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: wrap; |
|||
font-size: 16px; |
|||
text-align: center; |
|||
|
|||
.group-side-member { |
|||
margin-left: 15px; |
|||
} |
|||
|
|||
.group-side-invite { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
width: 50px; |
|||
margin-left: 15px; |
|||
|
|||
.invite-member-btn { |
|||
width: 100%; |
|||
height: 50px; |
|||
line-height: 50px; |
|||
border: #cccccc solid 1px; |
|||
font-size: 25px; |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
|
|||
&:hover { |
|||
border: #aaaaaa solid 1px; |
|||
} |
|||
} |
|||
|
|||
.invite-member-text { |
|||
font-size: 16px; |
|||
text-align: center; |
|||
width: 100%; |
|||
height: 30px; |
|||
line-height: 30px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden |
|||
} |
|||
} |
|||
} |
|||
|
|||
.group-side-form { |
|||
text-align: left; |
|||
padding: 10px; |
|||
height: 30%; |
|||
|
|||
.el-form-item { |
|||
margin-bottom: 12px; |
|||
|
|||
.el-form-item__label { |
|||
padding: 0; |
|||
line-height: 30px; |
|||
} |
|||
|
|||
.el-textarea__inner { |
|||
min-height: 100px !important; |
|||
} |
|||
} |
|||
|
|||
.btn-group { |
|||
text-align: center; |
|||
} |
|||
} |
|||
|
|||
} |
|||
.chat-group-side { |
|||
position: relative; |
|||
|
|||
.group-side-search { |
|||
padding: 10px; |
|||
} |
|||
|
|||
.group-side-scrollbar { |
|||
overflow: auto; |
|||
} |
|||
|
|||
.el-divider--horizontal { |
|||
margin: 0; |
|||
} |
|||
|
|||
.el-form-item { |
|||
margin-bottom: 0px !important; |
|||
} |
|||
|
|||
.group-side-member-list { |
|||
padding: 10px; |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: wrap; |
|||
font-size: 14px; |
|||
text-align: center; |
|||
|
|||
.group-side-member { |
|||
margin-left: 5px; |
|||
} |
|||
|
|||
.group-side-invite { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
width: 50px; |
|||
margin-left: 5px; |
|||
|
|||
.invite-member-btn { |
|||
width: 38px; |
|||
height: 38px; |
|||
line-height: 38px; |
|||
border: var(--im-border); |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
|
|||
&:hover { |
|||
border: #aaaaaa solid 1px; |
|||
} |
|||
} |
|||
|
|||
.invite-member-text { |
|||
font-size: 12px; |
|||
text-align: center; |
|||
width: 100%; |
|||
height: 30px; |
|||
line-height: 30px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden |
|||
} |
|||
} |
|||
} |
|||
|
|||
.group-side-form { |
|||
text-align: left; |
|||
padding: 10px; |
|||
height: 30%; |
|||
|
|||
.el-form-item { |
|||
margin-bottom: 12px; |
|||
|
|||
.el-form-item__label { |
|||
padding: 0; |
|||
line-height: 30px; |
|||
} |
|||
|
|||
.el-textarea__inner { |
|||
min-height: 100px !important; |
|||
} |
|||
} |
|||
|
|||
.el-input__inner, .el-textarea__inner { |
|||
color: var(--im-text-color) !important; |
|||
} |
|||
|
|||
|
|||
.btn-group { |
|||
text-align: center; |
|||
margin-top: 12px; |
|||
} |
|||
} |
|||
|
|||
} |
|||
</style> |
|||
|
|||
@ -1,166 +1,180 @@ |
|||
<template> |
|||
<el-dialog title="邀请好友" :visible.sync="visible" width="50%" :before-close="onClose"> |
|||
<div class="agm-container"> |
|||
<div class="agm-l-box"> |
|||
<el-input placeholder="搜索好友" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="suffix"> </i> |
|||
</el-input> |
|||
<el-scrollbar style="height:400px;"> |
|||
<div v-for="(friend,index) in friends" :key="friend.id"> |
|||
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false" |
|||
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" |
|||
:active="false"> |
|||
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" |
|||
v-model="friend.isCheck" size="medium"></el-checkbox> |
|||
</friend-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
<div class="agm-arrow el-icon-d-arrow-right"></div> |
|||
<div class="agm-r-box"> |
|||
<div class="agm-select-tip"> 已勾选{{checkCount}}位好友</div> |
|||
<el-scrollbar style="height:400px;"> |
|||
<div v-for="(friend,index) in friends" :key="friend.id"> |
|||
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" |
|||
:active="false" @del="onRemoveFriend(friend,index)" :menu="false"> |
|||
</friend-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
</div> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-dialog title="邀请好友" :visible.sync="visible" width="620px" :before-close="onClose"> |
|||
<div class="agm-container"> |
|||
<div class="agm-l-box"> |
|||
<div class="search"> |
|||
<el-input placeholder="搜索好友" v-model="searchText" size="small"> |
|||
<i class="el-icon-search el-input__icon" slot="suffix"> </i> |
|||
</el-input> |
|||
</div> |
|||
<el-scrollbar style="height:400px;"> |
|||
<div v-for="(friend,index) in friends" :key="friend.id"> |
|||
<friend-item v-show="friend.nickName.includes(searchText)" :showDelete="false" |
|||
@click.native="onSwitchCheck(friend)" :menu="false" :friend="friend" :index="index" |
|||
:active="false"> |
|||
<el-checkbox :disabled="friend.disabled" @click.native.stop="" class="agm-friend-checkbox" |
|||
v-model="friend.isCheck" size="medium"></el-checkbox> |
|||
</friend-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
<div class="agm-arrow el-icon-d-arrow-right"></div> |
|||
<div class="agm-r-box"> |
|||
<div class="agm-select-tip"> 已勾选{{ checkCount }}位好友</div> |
|||
<el-scrollbar style="height:400px;"> |
|||
<div v-for="(friend,index) in friends" :key="friend.id"> |
|||
<friend-item v-if="friend.isCheck && !friend.disabled" :friend="friend" :index="index" |
|||
:active="false" @del="onRemoveFriend(friend,index)" :menu="false"> |
|||
</friend-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
</div> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="onClose()">取 消</el-button> |
|||
<el-button type="primary" @click="onOk()">确 定</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
import FriendItem from '../friend/FriendItem.vue'; |
|||
import FriendItem from '../friend/FriendItem.vue'; |
|||
|
|||
export default { |
|||
name: "addGroupMember", |
|||
components: { |
|||
FriendItem |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
friends: [] |
|||
} |
|||
}, |
|||
methods: { |
|||
onClose() { |
|||
this.$emit("close"); |
|||
}, |
|||
onOk() { |
|||
export default { |
|||
name: "addGroupMember", |
|||
components: { |
|||
FriendItem |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
friends: [] |
|||
} |
|||
}, |
|||
methods: { |
|||
onClose() { |
|||
this.$emit("close"); |
|||
}, |
|||
onOk() { |
|||
|
|||
let inviteVO = { |
|||
groupId: this.groupId, |
|||
friendIds: [] |
|||
} |
|||
this.friends.forEach((f) => { |
|||
if (f.isCheck && !f.disabled) { |
|||
inviteVO.friendIds.push(f.id); |
|||
} |
|||
}) |
|||
if (inviteVO.friendIds.length > 0) { |
|||
this.$http({ |
|||
url: "/group/invite", |
|||
method: 'post', |
|||
data: inviteVO |
|||
}).then(() => { |
|||
this.$message.success("邀请成功"); |
|||
this.$emit("reload"); |
|||
this.$emit("close"); |
|||
}) |
|||
} |
|||
}, |
|||
onRemoveFriend(friend, index) { |
|||
friend.isCheck = false; |
|||
}, |
|||
onSwitchCheck(friend) { |
|||
if (!friend.disabled) { |
|||
friend.isCheck = !friend.isCheck |
|||
} |
|||
} |
|||
}, |
|||
props: { |
|||
visible: { |
|||
type: Boolean |
|||
}, |
|||
groupId: { |
|||
type: Number |
|||
}, |
|||
members: { |
|||
type: Array |
|||
} |
|||
}, |
|||
computed: { |
|||
checkCount() { |
|||
return this.friends.filter((f) => f.isCheck && !f.disabled).length; |
|||
} |
|||
}, |
|||
watch: { |
|||
visible: function(newData, oldData) { |
|||
if (newData) { |
|||
this.friends = []; |
|||
this.$store.state.friendStore.friends.forEach((f) => { |
|||
let friend = JSON.parse(JSON.stringify(f)) |
|||
let m = this.members.filter((m) => !m.quit) |
|||
.find((m) => m.userId == f.id); |
|||
if (m) { |
|||
// 好友已经在群里 |
|||
friend.disabled = true; |
|||
friend.isCheck = true |
|||
} else { |
|||
friend.disabled = false; |
|||
friend.isCheck = false; |
|||
} |
|||
this.friends.push(friend); |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
let inviteVO = { |
|||
groupId: this.groupId, |
|||
friendIds: [] |
|||
} |
|||
this.friends.forEach((f) => { |
|||
if (f.isCheck && !f.disabled) { |
|||
inviteVO.friendIds.push(f.id); |
|||
} |
|||
}) |
|||
if (inviteVO.friendIds.length > 0) { |
|||
this.$http({ |
|||
url: "/group/invite", |
|||
method: 'post', |
|||
data: inviteVO |
|||
}).then(() => { |
|||
this.$message.success("邀请成功"); |
|||
this.$emit("reload"); |
|||
this.$emit("close"); |
|||
}) |
|||
} |
|||
}, |
|||
onRemoveFriend(friend, index) { |
|||
friend.isCheck = false; |
|||
}, |
|||
onSwitchCheck(friend) { |
|||
if (!friend.disabled) { |
|||
friend.isCheck = !friend.isCheck |
|||
} |
|||
} |
|||
}, |
|||
props: { |
|||
visible: { |
|||
type: Boolean |
|||
}, |
|||
groupId: { |
|||
type: Number |
|||
}, |
|||
members: { |
|||
type: Array |
|||
} |
|||
}, |
|||
computed: { |
|||
checkCount() { |
|||
return this.friends.filter((f) => f.isCheck && !f.disabled).length; |
|||
} |
|||
}, |
|||
watch: { |
|||
visible: function (newData, oldData) { |
|||
if (newData) { |
|||
this.friends = []; |
|||
this.$store.state.friendStore.friends.forEach((f) => { |
|||
let friend = JSON.parse(JSON.stringify(f)) |
|||
let m = this.members.filter((m) => !m.quit) |
|||
.find((m) => m.userId == f.id); |
|||
if (m) { |
|||
// 好友已经在群里 |
|||
friend.disabled = true; |
|||
friend.isCheck = true |
|||
} else { |
|||
friend.disabled = false; |
|||
friend.isCheck = false; |
|||
} |
|||
this.friends.push(friend); |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.agm-container { |
|||
display: flex; |
|||
.agm-l-box { |
|||
flex: 1; |
|||
border: #587FF0 solid 1px; |
|||
border-radius: 5px; |
|||
overflow: hidden; |
|||
.agm-container { |
|||
display: flex; |
|||
|
|||
.agm-l-box { |
|||
flex: 1; |
|||
overflow: hidden; |
|||
border: var(--im-border); |
|||
|
|||
.agm-friend-checkbox { |
|||
margin-right: 20px; |
|||
} |
|||
} |
|||
.search { |
|||
height: 40px; |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.agm-arrow { |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 20px; |
|||
padding: 10px; |
|||
font-weight: 600; |
|||
color: #687Ff0; |
|||
} |
|||
.el-input__inner { |
|||
border: unset; |
|||
border-bottom: var(--im-border); |
|||
} |
|||
|
|||
.agm-r-box { |
|||
flex: 1; |
|||
border: #587FF0 solid 1px; |
|||
border-radius: 5px; |
|||
} |
|||
|
|||
.agm-select-tip { |
|||
text-align: left; |
|||
height: 40px; |
|||
line-height: 40px; |
|||
text-indent: 5px; |
|||
} |
|||
} |
|||
} |
|||
.agm-friend-checkbox { |
|||
margin-right: 20px; |
|||
} |
|||
} |
|||
|
|||
.agm-arrow { |
|||
display: flex; |
|||
align-items: center; |
|||
font-size: 18px; |
|||
padding: 10px; |
|||
font-weight: 600; |
|||
color: var(--im-color-primary); |
|||
} |
|||
|
|||
.agm-r-box { |
|||
flex: 1; |
|||
border: var(--im-border); |
|||
|
|||
.agm-select-tip { |
|||
text-align: left; |
|||
height: 40px; |
|||
line-height: 40px; |
|||
text-indent: 6px; |
|||
color: var(--im-text-color-light) |
|||
|
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,500 +1,508 @@ |
|||
<template> |
|||
<div> |
|||
<el-dialog v-dialogDrag :title="title" top="5vh" :close-on-click-modal="false" :close-on-press-escape="false" |
|||
:visible.sync="showRoom" width="50%" height="70%" :before-close="onQuit"> |
|||
<div class="rtc-private-video"> |
|||
<div v-show="isVideo" class="rtc-video-box"> |
|||
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..." |
|||
element-loading-background="rgba(0, 0, 0, 0.3)" > |
|||
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName" |
|||
:url="friend.headImage"> |
|||
</head-image> |
|||
<video ref="remoteVideo" autoplay=""></video> |
|||
</div> |
|||
<div class="rtc-video-mine"> |
|||
<video ref="localVideo" autoplay=""></video> |
|||
</div> |
|||
</div> |
|||
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..." |
|||
element-loading-background="rgba(0, 0, 0, 0.3)"> |
|||
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName" |
|||
:url="friend.headImage"> |
|||
<div class="rtc-voice-name">{{friend.nickName}}</div> |
|||
</head-image> |
|||
</div> |
|||
<div class="rtc-control-bar"> |
|||
<div title="取消" class="icon iconfont icon-phone-reject reject" |
|||
style="color: red;" @click="onQuit()"></div> |
|||
</div> |
|||
</div> |
|||
</el-dialog> |
|||
<rtc-private-acceptor v-if="!isHost&&isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept" |
|||
@reject="onReject"></rtc-private-acceptor> |
|||
</div> |
|||
<div> |
|||
<el-dialog |
|||
v-dialogDrag |
|||
top="5vh" |
|||
custom-class="rtc-private-video-dialog" |
|||
:title="title" |
|||
:width="width" |
|||
:visible.sync="showRoom" |
|||
:close-on-click-modal="false" |
|||
:close-on-press-escape="false" |
|||
:before-close="onQuit"> |
|||
<div class="rtc-private-video"> |
|||
<div v-show="isVideo" class="rtc-video-box"> |
|||
<div class="rtc-video-friend" v-loading="!isChating" element-loading-text="等待对方接听..." |
|||
element-loading-background="rgba(0, 0, 0, 0.1)"> |
|||
<head-image class="friend-head-image" :id="friend.id" :size="80" :name="friend.nickName" |
|||
:url="friend.headImage" :isShowUserInfo="false"> |
|||
</head-image> |
|||
<video ref="remoteVideo" autoplay=""></video> |
|||
</div> |
|||
<div class="rtc-video-mine"> |
|||
<video ref="localVideo" autoplay=""></video> |
|||
</div> |
|||
</div> |
|||
<div v-show="!isVideo" class="rtc-voice-box" v-loading="!isChating" element-loading-text="等待对方接听..." |
|||
element-loading-background="rgba(0, 0, 0, 0.1)"> |
|||
<head-image class="friend-head-image" :id="friend.id" :size="200" :name="friend.nickName" |
|||
:url="friend.headImage" :isShowUserInfo="false"> |
|||
<div class="rtc-voice-name">{{ friend.nickName }}</div> |
|||
</head-image> |
|||
</div> |
|||
<div class="rtc-control-bar"> |
|||
<div title="取消" class="icon iconfont icon-phone-reject reject" |
|||
style="color: red;" @click="onQuit()"></div> |
|||
</div> |
|||
</div> |
|||
</el-dialog> |
|||
<rtc-private-acceptor v-if="!isHost && isWaiting" ref="acceptor" :friend="friend" :mode="mode" @accept="onAccept" |
|||
@reject="onReject"></rtc-private-acceptor> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import HeadImage from '../common/HeadImage.vue'; |
|||
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue'; |
|||
import ImWebRtc from '@/api/webrtc'; |
|||
import ImCamera from '@/api/camera'; |
|||
import RtcPrivateApi from '@/api/rtcPrivateApi' |
|||
import HeadImage from '../common/HeadImage.vue'; |
|||
import RtcPrivateAcceptor from './RtcPrivateAcceptor.vue'; |
|||
import ImWebRtc from '@/api/webrtc'; |
|||
import ImCamera from '@/api/camera'; |
|||
import RtcPrivateApi from '@/api/rtcPrivateApi' |
|||
|
|||
export default { |
|||
name: 'rtcPrivateVideo', |
|||
components: { |
|||
HeadImage, |
|||
RtcPrivateAcceptor |
|||
}, |
|||
data() { |
|||
return { |
|||
camera: new ImCamera(), // 摄像头和麦克风 |
|||
webrtc: new ImWebRtc(), // webrtc相关 |
|||
API: new RtcPrivateApi(), // API |
|||
audio: new Audio(), // 呼叫音频 |
|||
showRoom: false, |
|||
friend: {}, |
|||
isHost: false, // 是否发起人 |
|||
state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常 |
|||
mode: 'video', // 模式 video:视频聊 voice:语音聊天 |
|||
localStream: null, // 本地视频流 |
|||
remoteStream: null, // 对方视频流 |
|||
videoTime: 0, |
|||
videoTimer: null, |
|||
heartbeatTimer: null, |
|||
candidates: [], |
|||
} |
|||
}, |
|||
methods: { |
|||
open(rtcInfo) { |
|||
this.showRoom = true; |
|||
this.mode = rtcInfo.mode; |
|||
this.isHost = rtcInfo.isHost; |
|||
this.friend = rtcInfo.friend; |
|||
if (this.isHost) { |
|||
this.onCall(); |
|||
} |
|||
}, |
|||
initAudio() { |
|||
let url = require(`@/assets/audio/call.wav`); |
|||
this.audio.src = url; |
|||
this.audio.loop = true; |
|||
}, |
|||
initRtc() { |
|||
this.webrtc.init(this.configuration) |
|||
this.webrtc.setupPeerConnection((stream) => { |
|||
this.$refs.remoteVideo.srcObject = stream; |
|||
this.remoteStream = stream; |
|||
}) |
|||
// 监听候选信息 |
|||
this.webrtc.onIcecandidate((candidate) => { |
|||
if (this.state == "CHATING") { |
|||
// 连接已就绪,直接发送 |
|||
this.API.sendCandidate(this.friend.id, candidate); |
|||
} else { |
|||
// 连接未就绪,缓存起来,连接后再发送 |
|||
this.candidates.push(candidate) |
|||
} |
|||
}) |
|||
// 监听连接成功状态 |
|||
this.webrtc.onStateChange((state) => { |
|||
if (state == "connected") { |
|||
console.log("webrtc连接成功") |
|||
} else if (state == "disconnected") { |
|||
console.log("webrtc连接断开") |
|||
} |
|||
}) |
|||
}, |
|||
onCall() { |
|||
if (!this.checkDevEnable()) { |
|||
this.close(); |
|||
} |
|||
// 初始化webrtc |
|||
this.initRtc(); |
|||
// 启动心跳 |
|||
this.startHeartBeat(); |
|||
// 打开摄像头 |
|||
this.openStream().then(() => { |
|||
this.webrtc.setStream(this.localStream); |
|||
this.webrtc.createOffer().then((offer) => { |
|||
// 发起呼叫 |
|||
this.API.call(this.friend.id, this.mode, offer).then(() => { |
|||
// 直接进入聊天状态 |
|||
this.state = "WAITING"; |
|||
// 播放呼叫铃声 |
|||
this.audio.play(); |
|||
}).catch(()=>{ |
|||
this.close(); |
|||
}) |
|||
}) |
|||
}).catch(()=>{ |
|||
// 呼叫方必须能打开摄像头,否则无法正常建立连接 |
|||
this.close(); |
|||
}) |
|||
}, |
|||
onAccept() { |
|||
if (!this.checkDevEnable()) { |
|||
this.API.failed(this.friend.id, "对方设备不支持通话") |
|||
this.close(); |
|||
return; |
|||
} |
|||
// 进入房间 |
|||
this.showRoom = true; |
|||
this.state = "CHATING"; |
|||
// 停止呼叫铃声 |
|||
this.audio.pause(); |
|||
// 初始化webrtc |
|||
this.initRtc(); |
|||
// 打开摄像头 |
|||
this.openStream().finally(() => { |
|||
this.webrtc.setStream(this.localStream); |
|||
this.webrtc.createAnswer(this.offer).then((answer) => { |
|||
this.API.accept(this.friend.id, answer); |
|||
// 记录时长 |
|||
this.startChatTime(); |
|||
// 清理定时器 |
|||
this.waitTimer && clearTimeout(this.waitTimer); |
|||
}) |
|||
}) |
|||
}, |
|||
onReject() { |
|||
console.log("onReject") |
|||
// 退出通话 |
|||
this.API.reject(this.friend.id); |
|||
// 退出 |
|||
this.close(); |
|||
}, |
|||
onHandup() { |
|||
this.API.handup(this.friend.id) |
|||
this.$message.success("您已挂断,通话结束") |
|||
this.close(); |
|||
}, |
|||
onCancel() { |
|||
this.API.cancel(this.friend.id) |
|||
this.$message.success("已取消呼叫,通话结束") |
|||
this.close(); |
|||
}, |
|||
onRTCMessage(msg) { |
|||
// 除了发起通话,如果在关闭状态就无需处理 |
|||
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE && |
|||
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO && |
|||
this.isClose) { |
|||
return; |
|||
} |
|||
// RTC信令处理 |
|||
switch (msg.type) { |
|||
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE: |
|||
this.onRTCCall(msg, 'voice') |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO: |
|||
this.onRTCCall(msg, 'video') |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT: |
|||
this.onRTCAccept(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_REJECT: |
|||
this.onRTCReject(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CANCEL: |
|||
this.onRTCCancel(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_FAILED: |
|||
this.onRTCFailed(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_HANDUP: |
|||
this.onRTCHandup(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE: |
|||
this.onRTCCandidate(msg) |
|||
break; |
|||
} |
|||
}, |
|||
onRTCCall(msg, mode) { |
|||
this.offer = JSON.parse(msg.content); |
|||
this.isHost = false; |
|||
this.mode = mode; |
|||
this.$http({ |
|||
url: `/friend/find/${msg.sendId}`, |
|||
method: 'get' |
|||
}).then((friend) => { |
|||
this.friend = friend; |
|||
this.state = "WAITING"; |
|||
this.audio.play(); |
|||
this.startHeartBeat(); |
|||
// 30s未接听自动挂掉 |
|||
this.waitTimer = setTimeout(() => { |
|||
this.API.failed(this.friend.id,"对方无应答"); |
|||
this.$message.error("您未接听"); |
|||
this.close(); |
|||
}, 30000) |
|||
}) |
|||
}, |
|||
onRTCAccept(msg) { |
|||
if (msg.selfSend) { |
|||
// 在其他设备接听 |
|||
this.$message.success("已在其他设备接听"); |
|||
this.close(); |
|||
} else { |
|||
// 对方接受了的通话 |
|||
let offer = JSON.parse(msg.content); |
|||
this.webrtc.setRemoteDescription(offer); |
|||
// 状态为聊天中 |
|||
this.state = 'CHATING' |
|||
// 停止播放语音 |
|||
this.audio.pause(); |
|||
// 发送candidate |
|||
this.candidates.forEach((candidate) => { |
|||
this.API.sendCandidate(this.friend.id, candidate); |
|||
}) |
|||
// 开始计时 |
|||
this.startChatTime(); |
|||
} |
|||
}, |
|||
onRTCReject(msg) { |
|||
if (msg.selfSend) { |
|||
this.$message.success("已在其他设备拒绝"); |
|||
this.close(); |
|||
} else { |
|||
this.$message.error("对方拒绝了您的通话请求"); |
|||
this.close(); |
|||
} |
|||
}, |
|||
onRTCFailed(msg) { |
|||
// 呼叫失败 |
|||
this.$message.error(msg.content) |
|||
this.close(); |
|||
}, |
|||
onRTCCancel() { |
|||
// 对方取消通话 |
|||
this.$message.success("对方取消了呼叫"); |
|||
this.close(); |
|||
}, |
|||
onRTCHandup() { |
|||
// 对方挂断 |
|||
this.$message.success("对方已挂断"); |
|||
this.close(); |
|||
}, |
|||
onRTCCandidate(msg) { |
|||
let candidate = JSON.parse(msg.content); |
|||
this.webrtc.addIceCandidate(candidate); |
|||
}, |
|||
export default { |
|||
name: 'rtcPrivateVideo', |
|||
components: { |
|||
HeadImage, |
|||
RtcPrivateAcceptor |
|||
}, |
|||
data() { |
|||
return { |
|||
camera: new ImCamera(), // 摄像头和麦克风 |
|||
webrtc: new ImWebRtc(), // webrtc相关 |
|||
API: new RtcPrivateApi(), // API |
|||
audio: new Audio(), // 呼叫音频 |
|||
showRoom: false, |
|||
friend: {}, |
|||
isHost: false, // 是否发起人 |
|||
state: "CLOSE", // CLOSE:关闭 WAITING:等待呼叫或接听 CHATING:聊天中 ERROR:出现异常 |
|||
mode: 'video', // 模式 video:视频聊 voice:语音聊天 |
|||
localStream: null, // 本地视频流 |
|||
remoteStream: null, // 对方视频流 |
|||
videoTime: 0, |
|||
videoTimer: null, |
|||
heartbeatTimer: null, |
|||
candidates: [], |
|||
} |
|||
}, |
|||
methods: { |
|||
open(rtcInfo) { |
|||
this.showRoom = true; |
|||
this.mode = rtcInfo.mode; |
|||
this.isHost = rtcInfo.isHost; |
|||
this.friend = rtcInfo.friend; |
|||
if (this.isHost) { |
|||
this.onCall(); |
|||
} |
|||
}, |
|||
initAudio() { |
|||
let url = require(`@/assets/audio/call.wav`); |
|||
this.audio.src = url; |
|||
this.audio.loop = true; |
|||
}, |
|||
initRtc() { |
|||
this.webrtc.init(this.configuration) |
|||
this.webrtc.setupPeerConnection((stream) => { |
|||
this.$refs.remoteVideo.srcObject = stream; |
|||
this.remoteStream = stream; |
|||
}) |
|||
// 监听候选信息 |
|||
this.webrtc.onIcecandidate((candidate) => { |
|||
if (this.state == "CHATING") { |
|||
// 连接已就绪,直接发送 |
|||
this.API.sendCandidate(this.friend.id, candidate); |
|||
} else { |
|||
// 连接未就绪,缓存起来,连接后再发送 |
|||
this.candidates.push(candidate) |
|||
} |
|||
}) |
|||
// 监听连接成功状态 |
|||
this.webrtc.onStateChange((state) => { |
|||
if (state == "connected") { |
|||
console.log("webrtc连接成功") |
|||
} else if (state == "disconnected") { |
|||
console.log("webrtc连接断开") |
|||
} |
|||
}) |
|||
}, |
|||
onCall() { |
|||
if (!this.checkDevEnable()) { |
|||
this.close(); |
|||
} |
|||
// 初始化webrtc |
|||
this.initRtc(); |
|||
// 启动心跳 |
|||
this.startHeartBeat(); |
|||
// 打开摄像头 |
|||
this.openStream().then(() => { |
|||
this.webrtc.setStream(this.localStream); |
|||
this.webrtc.createOffer().then((offer) => { |
|||
// 发起呼叫 |
|||
this.API.call(this.friend.id, this.mode, offer).then(() => { |
|||
// 直接进入聊天状态 |
|||
this.state = "WAITING"; |
|||
// 播放呼叫铃声 |
|||
this.audio.play(); |
|||
}).catch(() => { |
|||
this.close(); |
|||
}) |
|||
}) |
|||
}).catch(() => { |
|||
// 呼叫方必须能打开摄像头,否则无法正常建立连接 |
|||
this.close(); |
|||
}) |
|||
}, |
|||
onAccept() { |
|||
if (!this.checkDevEnable()) { |
|||
this.API.failed(this.friend.id, "对方设备不支持通话") |
|||
this.close(); |
|||
return; |
|||
} |
|||
// 进入房间 |
|||
this.showRoom = true; |
|||
this.state = "CHATING"; |
|||
// 停止呼叫铃声 |
|||
this.audio.pause(); |
|||
// 初始化webrtc |
|||
this.initRtc(); |
|||
// 打开摄像头 |
|||
this.openStream().finally(() => { |
|||
this.webrtc.setStream(this.localStream); |
|||
this.webrtc.createAnswer(this.offer).then((answer) => { |
|||
this.API.accept(this.friend.id, answer); |
|||
// 记录时长 |
|||
this.startChatTime(); |
|||
// 清理定时器 |
|||
this.waitTimer && clearTimeout(this.waitTimer); |
|||
}) |
|||
}) |
|||
}, |
|||
onReject() { |
|||
console.log("onReject") |
|||
// 退出通话 |
|||
this.API.reject(this.friend.id); |
|||
// 退出 |
|||
this.close(); |
|||
}, |
|||
onHandup() { |
|||
this.API.handup(this.friend.id) |
|||
this.$message.success("您已挂断,通话结束") |
|||
this.close(); |
|||
}, |
|||
onCancel() { |
|||
this.API.cancel(this.friend.id) |
|||
this.$message.success("已取消呼叫,通话结束") |
|||
this.close(); |
|||
}, |
|||
onRTCMessage(msg) { |
|||
// 除了发起通话,如果在关闭状态就无需处理 |
|||
if (msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE && |
|||
msg.type != this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO && |
|||
this.isClose) { |
|||
return; |
|||
} |
|||
// RTC信令处理 |
|||
switch (msg.type) { |
|||
case this.$enums.MESSAGE_TYPE.RTC_CALL_VOICE: |
|||
this.onRTCCall(msg, 'voice') |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CALL_VIDEO: |
|||
this.onRTCCall(msg, 'video') |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_ACCEPT: |
|||
this.onRTCAccept(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_REJECT: |
|||
this.onRTCReject(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CANCEL: |
|||
this.onRTCCancel(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_FAILED: |
|||
this.onRTCFailed(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_HANDUP: |
|||
this.onRTCHandup(msg) |
|||
break; |
|||
case this.$enums.MESSAGE_TYPE.RTC_CANDIDATE: |
|||
this.onRTCCandidate(msg) |
|||
break; |
|||
} |
|||
}, |
|||
onRTCCall(msg, mode) { |
|||
this.offer = JSON.parse(msg.content); |
|||
this.isHost = false; |
|||
this.mode = mode; |
|||
this.$http({ |
|||
url: `/friend/find/${msg.sendId}`, |
|||
method: 'get' |
|||
}).then((friend) => { |
|||
this.friend = friend; |
|||
this.state = "WAITING"; |
|||
this.audio.play(); |
|||
this.startHeartBeat(); |
|||
// 30s未接听自动挂掉 |
|||
this.waitTimer = setTimeout(() => { |
|||
this.API.failed(this.friend.id, "对方无应答"); |
|||
this.$message.error("您未接听"); |
|||
this.close(); |
|||
}, 30000) |
|||
}) |
|||
}, |
|||
onRTCAccept(msg) { |
|||
if (msg.selfSend) { |
|||
// 在其他设备接听 |
|||
this.$message.success("已在其他设备接听"); |
|||
this.close(); |
|||
} else { |
|||
// 对方接受了的通话 |
|||
let offer = JSON.parse(msg.content); |
|||
this.webrtc.setRemoteDescription(offer); |
|||
// 状态为聊天中 |
|||
this.state = 'CHATING' |
|||
// 停止播放语音 |
|||
this.audio.pause(); |
|||
// 发送candidate |
|||
this.candidates.forEach((candidate) => { |
|||
this.API.sendCandidate(this.friend.id, candidate); |
|||
}) |
|||
// 开始计时 |
|||
this.startChatTime() |
|||
} |
|||
}, |
|||
onRTCReject(msg) { |
|||
if (msg.selfSend) { |
|||
this.$message.success("已在其他设备拒绝"); |
|||
this.close(); |
|||
} else { |
|||
this.$message.error("对方拒绝了您的通话请求"); |
|||
this.close(); |
|||
} |
|||
}, |
|||
onRTCFailed(msg) { |
|||
// 呼叫失败 |
|||
this.$message.error(msg.content) |
|||
this.close(); |
|||
}, |
|||
onRTCCancel() { |
|||
// 对方取消通话 |
|||
this.$message.success("对方取消了呼叫"); |
|||
this.close(); |
|||
}, |
|||
onRTCHandup() { |
|||
// 对方挂断 |
|||
this.$message.success("对方已挂断"); |
|||
this.close(); |
|||
}, |
|||
onRTCCandidate(msg) { |
|||
let candidate = JSON.parse(msg.content); |
|||
this.webrtc.addIceCandidate(candidate); |
|||
}, |
|||
|
|||
openStream() { |
|||
return new Promise((resolve, reject) => { |
|||
if (this.isVideo) { |
|||
// 打开摄像头+麦克风 |
|||
this.camera.openVideo().then((stream) => { |
|||
this.localStream = stream; |
|||
this.$nextTick(() => { |
|||
this.$refs.localVideo.srcObject = stream; |
|||
this.$refs.localVideo.muted = true; |
|||
}) |
|||
resolve(stream); |
|||
}).catch((e) => { |
|||
this.$message.error("打开摄像头失败") |
|||
console.log("本摄像头打开失败:" + e.message) |
|||
reject(e); |
|||
}) |
|||
} else { |
|||
// 打开麦克风 |
|||
this.camera.openAudio().then((stream) => { |
|||
this.localStream = stream; |
|||
this.$refs.localVideo.srcObject = stream; |
|||
resolve(stream); |
|||
}).catch((e) => { |
|||
this.$message.error("打开麦克风失败") |
|||
console.log("打开麦克风失败:" + e.message) |
|||
reject(e); |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
startChatTime() { |
|||
this.videoTime = 0; |
|||
this.videoTimer && clearInterval(this.videoTimer); |
|||
this.videoTimer = setInterval(() => { |
|||
this.videoTime++; |
|||
}, 1000) |
|||
}, |
|||
checkDevEnable() { |
|||
// 检测摄像头 |
|||
if (!this.camera.isEnable()) { |
|||
this.message.error("访问摄像头失败"); |
|||
return false; |
|||
} |
|||
// 检测webrtc |
|||
if (!this.webrtc.isEnable()) { |
|||
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC"); |
|||
return false; |
|||
} |
|||
return true; |
|||
}, |
|||
startHeartBeat() { |
|||
// 每15s推送一次心跳 |
|||
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
|||
this.heartbeatTimer = setInterval(() => { |
|||
this.API.heartbeat(this.friend.id); |
|||
}, 15000) |
|||
}, |
|||
close() { |
|||
this.showRoom = false; |
|||
this.camera.close(); |
|||
this.webrtc.close(); |
|||
this.audio.pause(); |
|||
this.videoTime = 0; |
|||
this.videoTimer && clearInterval(this.videoTimer); |
|||
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
|||
this.waitTimer && clearTimeout(this.waitTimer); |
|||
this.videoTimer = null; |
|||
this.heartbeatTimer = null; |
|||
this.waitTimer = null; |
|||
this.state = 'CLOSE'; |
|||
this.candidates = []; |
|||
}, |
|||
onQuit() { |
|||
if (this.isChating) { |
|||
this.onHandup() |
|||
} else if (this.isWaiting) { |
|||
this.onCancel(); |
|||
} else { |
|||
this.close(); |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
title() { |
|||
let strTitle = `${this.modeText}通话-${this.friend.nickName}`; |
|||
if (this.isChating) { |
|||
strTitle += `(${this.currentTime})`; |
|||
} else if (this.isWaiting) { |
|||
strTitle += `(呼叫中)`; |
|||
} |
|||
return strTitle; |
|||
}, |
|||
currentTime() { |
|||
let min = Math.floor(this.videoTime / 60); |
|||
let sec = this.videoTime % 60; |
|||
let strTime = min < 10 ? "0" : ""; |
|||
strTime += min; |
|||
strTime += ":" |
|||
strTime += sec < 10 ? "0" : ""; |
|||
strTime += sec; |
|||
return strTime; |
|||
}, |
|||
configuration() { |
|||
const iceServers = this.$store.state.configStore.webrtc.iceServers; |
|||
return { |
|||
iceServers: iceServers |
|||
} |
|||
}, |
|||
isVideo() { |
|||
return this.mode == "video" |
|||
}, |
|||
modeText() { |
|||
return this.isVideo ? "视频" : "语音"; |
|||
}, |
|||
isChating() { |
|||
return this.state == "CHATING"; |
|||
}, |
|||
isWaiting() { |
|||
return this.state == "WAITING"; |
|||
}, |
|||
isClose() { |
|||
return this.state == "CLOSE"; |
|||
} |
|||
}, |
|||
mounted() { |
|||
// 初始化音频文件 |
|||
this.initAudio(); |
|||
}, |
|||
created() { |
|||
// 监听页面刷新事件 |
|||
window.addEventListener('beforeunload', () => { |
|||
this.onQuit(); |
|||
}); |
|||
}, |
|||
beforeUnmount() { |
|||
this.onQuit(); |
|||
} |
|||
} |
|||
openStream() { |
|||
return new Promise((resolve, reject) => { |
|||
if (this.isVideo) { |
|||
// 打开摄像头+麦克风 |
|||
this.camera.openVideo().then((stream) => { |
|||
this.localStream = stream; |
|||
this.$nextTick(() => { |
|||
this.$refs.localVideo.srcObject = stream; |
|||
this.$refs.localVideo.muted = true; |
|||
}) |
|||
resolve(stream); |
|||
}).catch((e) => { |
|||
this.$message.error("打开摄像头失败") |
|||
console.log("本摄像头打开失败:" + e.message) |
|||
reject(e); |
|||
}) |
|||
} else { |
|||
// 打开麦克风 |
|||
this.camera.openAudio().then((stream) => { |
|||
this.localStream = stream; |
|||
this.$refs.localVideo.srcObject = stream; |
|||
resolve(stream); |
|||
}).catch((e) => { |
|||
this.$message.error("打开麦克风失败") |
|||
console.log("打开麦克风失败:" + e.message) |
|||
reject(e); |
|||
}) |
|||
} |
|||
}) |
|||
}, |
|||
startChatTime() { |
|||
this.videoTime = 0; |
|||
this.videoTimer && clearInterval(this.videoTimer); |
|||
this.videoTimer = setInterval(() => { |
|||
this.videoTime++; |
|||
}, 1000) |
|||
}, |
|||
checkDevEnable() { |
|||
// 检测摄像头 |
|||
if (!this.camera.isEnable()) { |
|||
this.message.error("访问摄像头失败"); |
|||
return false; |
|||
} |
|||
// 检测webrtc |
|||
if (!this.webrtc.isEnable()) { |
|||
this.message.error("初始化RTC失败,原因可能是: 1.服务器缺少ssl证书 2.您的设备不支持WebRTC"); |
|||
return false; |
|||
} |
|||
return true; |
|||
}, |
|||
startHeartBeat() { |
|||
// 每15s推送一次心跳 |
|||
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
|||
this.heartbeatTimer = setInterval(() => { |
|||
this.API.heartbeat(this.friend.id); |
|||
}, 15000) |
|||
}, |
|||
close() { |
|||
this.showRoom = false; |
|||
this.camera.close(); |
|||
this.webrtc.close(); |
|||
this.audio.pause(); |
|||
this.videoTime = 0; |
|||
this.videoTimer && clearInterval(this.videoTimer); |
|||
this.heartbeatTimer && clearInterval(this.heartbeatTimer); |
|||
this.waitTimer && clearTimeout(this.waitTimer); |
|||
this.videoTimer = null; |
|||
this.heartbeatTimer = null; |
|||
this.waitTimer = null; |
|||
this.state = 'CLOSE'; |
|||
this.candidates = []; |
|||
}, |
|||
onQuit() { |
|||
if (this.isChating) { |
|||
this.onHandup() |
|||
} else if (this.isWaiting) { |
|||
this.onCancel(); |
|||
} else { |
|||
this.close(); |
|||
} |
|||
} |
|||
}, |
|||
computed: { |
|||
width() { |
|||
return this.isVideo ? '960px' : '360px' |
|||
}, |
|||
title() { |
|||
let strTitle = `${this.modeText}通话-${this.friend.nickName}`; |
|||
if (this.isChating) { |
|||
strTitle += `(${this.currentTime})`; |
|||
} else if (this.isWaiting) { |
|||
strTitle += `(呼叫中)`; |
|||
} |
|||
return strTitle; |
|||
}, |
|||
currentTime() { |
|||
let min = Math.floor(this.videoTime / 60); |
|||
let sec = this.videoTime % 60; |
|||
let strTime = min < 10 ? "0" : ""; |
|||
strTime += min; |
|||
strTime += ":" |
|||
strTime += sec < 10 ? "0" : ""; |
|||
strTime += sec; |
|||
return strTime; |
|||
}, |
|||
configuration() { |
|||
const iceServers = this.$store.state.configStore.webrtc.iceServers; |
|||
return { |
|||
iceServers: iceServers |
|||
} |
|||
}, |
|||
isVideo() { |
|||
return this.mode == "video" |
|||
}, |
|||
modeText() { |
|||
return this.isVideo ? "视频" : "语音"; |
|||
}, |
|||
isChating() { |
|||
return this.state == "CHATING"; |
|||
}, |
|||
isWaiting() { |
|||
return this.state == "WAITING"; |
|||
}, |
|||
isClose() { |
|||
return this.state == "CLOSE"; |
|||
} |
|||
}, |
|||
mounted() { |
|||
// 初始化音频文件 |
|||
this.initAudio(); |
|||
}, |
|||
created() { |
|||
// 监听页面刷新事件 |
|||
window.addEventListener('beforeunload', () => { |
|||
this.onQuit(); |
|||
}); |
|||
}, |
|||
beforeUnmount() { |
|||
this.onQuit(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.rtc-private-video { |
|||
position: relative; |
|||
.rtc-private-video { |
|||
position: relative; |
|||
|
|||
.el-loading-text { |
|||
color: white !important; |
|||
font-size: 16px !important; |
|||
} |
|||
.el-loading-text { |
|||
color: white !important; |
|||
font-size: 16px !important; |
|||
} |
|||
|
|||
.path { |
|||
stroke: white !important; |
|||
} |
|||
.path { |
|||
stroke: white !important; |
|||
} |
|||
|
|||
.rtc-video-box { |
|||
position: relative; |
|||
border: #4880b9 solid 1px; |
|||
background-color: #eeeeee; |
|||
.rtc-video-box { |
|||
position: relative; |
|||
background-color: #eeeeee; |
|||
|
|||
.rtc-video-friend { |
|||
height: 70vh; |
|||
.rtc-video-friend { |
|||
height: 70vh; |
|||
|
|||
.friend-head-image { |
|||
position: absolute; |
|||
} |
|||
.friend-head-image { |
|||
position: absolute; |
|||
} |
|||
|
|||
video { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
transform: rotateY(180deg); |
|||
} |
|||
} |
|||
video { |
|||
width: 100%; |
|||
height: 100%; |
|||
object-fit: cover; |
|||
transform: rotateY(180deg); |
|||
} |
|||
} |
|||
|
|||
.rtc-video-mine { |
|||
position: absolute; |
|||
z-index: 99999; |
|||
width: 25vh; |
|||
right: 0; |
|||
bottom: 0; |
|||
box-shadow: 0px 0px 5px #ccc; |
|||
background-color: #cccccc; |
|||
.rtc-video-mine { |
|||
position: absolute; |
|||
z-index: 99999; |
|||
width: 25vh; |
|||
right: 0; |
|||
bottom: -1px; |
|||
|
|||
video { |
|||
width: 100%; |
|||
object-fit: cover; |
|||
transform: rotateY(180deg); |
|||
} |
|||
} |
|||
} |
|||
video { |
|||
width: 100%; |
|||
object-fit: cover; |
|||
transform: rotateY(180deg); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.rtc-voice-box { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
border: #4880b9 solid 1px; |
|||
width: 100%; |
|||
height: 50vh; |
|||
padding-top: 10vh; |
|||
background-color: aliceblue; |
|||
.rtc-voice-box { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100%; |
|||
height: 300px; |
|||
background-color: var(--im-color-primary-light-9); |
|||
|
|||
.rtc-voice-name { |
|||
text-align: center; |
|||
font-size: 22px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
.rtc-voice-name { |
|||
text-align: center; |
|||
font-size: 20px; |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
|
|||
.rtc-control-bar { |
|||
display: flex; |
|||
justify-content: space-around; |
|||
padding: 10px; |
|||
.rtc-control-bar { |
|||
display: flex; |
|||
justify-content: space-around; |
|||
padding: 10px; |
|||
|
|||
.icon { |
|||
font-size: 50px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.icon { |
|||
font-size: 50px; |
|||
cursor: pointer; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,144 +1,144 @@ |
|||
<template> |
|||
<el-dialog class="setting" title="设置" :visible.sync="visible" width="500px" :before-close="onClose"> |
|||
<el-form :model="userInfo" label-width="70px" :rules="rules" ref="settingForm"> |
|||
<el-form-item label="头像"> |
|||
<file-upload class="avatar-uploader" |
|||
:action="imageAction" |
|||
:showLoading="true" |
|||
:maxSize="maxSize" |
|||
@success="onUploadSuccess" |
|||
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> |
|||
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar"> |
|||
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|||
</file-upload> |
|||
</el-form-item> |
|||
<el-form-item label="用户名"> |
|||
<el-input disabled v-model="userInfo.userName" autocomplete="off"></el-input> |
|||
</el-form-item> |
|||
<el-form-item prop="nickName" label="昵称"> |
|||
<el-input v-model="userInfo.nickName" autocomplete="off"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="性别"> |
|||
<el-radio-group v-model="userInfo.sex"> |
|||
<el-radio :label="0">男</el-radio> |
|||
<el-radio :label="1">女</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="个性签名"> |
|||
<el-input type="textarea" v-model="userInfo.signature"></el-input> |
|||
</el-form-item> |
|||
</el-form> |
|||
<el-dialog class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose"> |
|||
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small"> |
|||
<el-form-item label="头像" style="margin-bottom: 0 !important;"> |
|||
<file-upload class="avatar-uploader" |
|||
:action="imageAction" |
|||
:showLoading="true" |
|||
:maxSize="maxSize" |
|||
@success="onUploadSuccess" |
|||
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> |
|||
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar"> |
|||
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|||
</file-upload> |
|||
</el-form-item> |
|||
<el-form-item label="用户名"> |
|||
<el-input disabled v-model="userInfo.userName" autocomplete="off" size="small"></el-input> |
|||
</el-form-item> |
|||
<el-form-item prop="nickName" label="昵称"> |
|||
<el-input v-model="userInfo.nickName" autocomplete="off" size="small"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="性别"> |
|||
<el-radio-group v-model="userInfo.sex"> |
|||
<el-radio :label="0">男</el-radio> |
|||
<el-radio :label="1">女</el-radio> |
|||
</el-radio-group> |
|||
</el-form-item> |
|||
<el-form-item label="个性签名"> |
|||
<el-input type="textarea" v-model="userInfo.signature" :rows="3"></el-input> |
|||
</el-form-item> |
|||
</el-form> |
|||
|
|||
<span slot="footer" class="dialog-footer"> |
|||
<span slot="footer" class="dialog-footer"> |
|||
<el-button @click="onClose()">取 消</el-button> |
|||
<el-button type="primary" @click="onSubmit()">确 定</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script> |
|||
import FileUpload from "../common/FileUpload.vue"; |
|||
import FileUpload from "../common/FileUpload.vue"; |
|||
|
|||
export default { |
|||
name: "setting", |
|||
components: { |
|||
FileUpload |
|||
}, |
|||
data() { |
|||
return { |
|||
userInfo: { |
|||
export default { |
|||
name: "setting", |
|||
components: { |
|||
FileUpload |
|||
}, |
|||
data() { |
|||
return { |
|||
userInfo: {}, |
|||
maxSize: 5 * 1024 * 1024, |
|||
action: "/image/upload", |
|||
rules: { |
|||
nickName: [{ |
|||
required: true, |
|||
message: '请输入昵称', |
|||
trigger: 'blur' |
|||
}] |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
|
|||
}, |
|||
maxSize: 5*1024*1024, |
|||
action: "/image/upload", |
|||
rules: { |
|||
nickName: [{ |
|||
required: true, |
|||
message: '请输入昵称', |
|||
trigger: 'blur' |
|||
}] |
|||
} |
|||
} |
|||
}, |
|||
methods: { |
|||
|
|||
onClose() { |
|||
this.$emit("close"); |
|||
}, |
|||
onSubmit() { |
|||
this.$refs['settingForm'].validate((valid) => { |
|||
if (!valid) { |
|||
return false; |
|||
} |
|||
this.$http({ |
|||
url: "/user/update", |
|||
method: "put", |
|||
data: this.userInfo |
|||
}).then(()=>{ |
|||
this.$store.commit("setUserInfo",this.userInfo); |
|||
this.$emit("close"); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
}); |
|||
}, |
|||
onUploadSuccess(data, file) { |
|||
this.userInfo.headImage = data.originUrl; |
|||
this.userInfo.headImageThumb = data.thumbUrl; |
|||
} |
|||
}, |
|||
props: { |
|||
visible: { |
|||
type: Boolean |
|||
} |
|||
}, |
|||
computed:{ |
|||
imageAction(){ |
|||
return `/image/upload`; |
|||
} |
|||
}, |
|||
watch: { |
|||
visible: function(newData, oldData) { |
|||
// 深拷贝 |
|||
let mine = this.$store.state.userStore.userInfo; |
|||
this.userInfo = JSON.parse(JSON.stringify(mine)); |
|||
} |
|||
} |
|||
} |
|||
onClose() { |
|||
this.$emit("close"); |
|||
}, |
|||
onSubmit() { |
|||
this.$refs['settingForm'].validate((valid) => { |
|||
if (!valid) { |
|||
return false; |
|||
} |
|||
this.$http({ |
|||
url: "/user/update", |
|||
method: "put", |
|||
data: this.userInfo |
|||
}).then(() => { |
|||
this.$store.commit("setUserInfo", this.userInfo); |
|||
this.$emit("close"); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
}); |
|||
}, |
|||
onUploadSuccess(data, file) { |
|||
this.userInfo.headImage = data.originUrl; |
|||
this.userInfo.headImageThumb = data.thumbUrl; |
|||
} |
|||
}, |
|||
props: { |
|||
visible: { |
|||
type: Boolean |
|||
} |
|||
}, |
|||
computed: { |
|||
imageAction() { |
|||
return `/image/upload`; |
|||
} |
|||
}, |
|||
watch: { |
|||
visible: function (newData, oldData) { |
|||
// 深拷贝 |
|||
let mine = this.$store.state.userStore.userInfo; |
|||
this.userInfo = JSON.parse(JSON.stringify(mine)); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" > |
|||
.setting { |
|||
.el-form { |
|||
padding: 30px; |
|||
} |
|||
.avatar-uploader { |
|||
<style lang="scss"> |
|||
.setting { |
|||
.el-form { |
|||
padding: 10px 0 0 10px; |
|||
} |
|||
|
|||
.avatar-uploader { |
|||
--width: 112px; |
|||
|
|||
.el-upload { |
|||
border: 1px dashed #d9d9d9 !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
.el-upload { |
|||
border: 1px dashed #d9d9d9 !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.el-upload:hover { |
|||
border-color: #409EFF; |
|||
} |
|||
.el-upload:hover { |
|||
border-color: #409EFF; |
|||
} |
|||
|
|||
.avatar-uploader-icon { |
|||
font-size: 28px; |
|||
color: #8c939d; |
|||
width: 178px; |
|||
height: 178px; |
|||
line-height: 178px; |
|||
text-align: center; |
|||
} |
|||
.avatar-uploader-icon { |
|||
font-size: 24px; |
|||
color: #8c939d; |
|||
width: var(--width); |
|||
height: var(--width); |
|||
line-height: var(--width); |
|||
text-align: center; |
|||
} |
|||
|
|||
.avatar { |
|||
width: 178px; |
|||
height: 178px; |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
.avatar { |
|||
width: var(--width); |
|||
height: var(--width); |
|||
display: block; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
|
|||
@ -1,104 +1,103 @@ |
|||
<template> |
|||
<el-container class="chat-page"> |
|||
<el-aside width="280px" class="chat-list-box"> |
|||
<div class="chat-list-header"> |
|||
<el-input class="search-text" placeholder="搜索" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
</div> |
|||
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..." |
|||
element-loading-spinner="el-icon-loading" element-loading-background="#eee"> |
|||
<div class="chat-loading-box"></div> |
|||
</div> |
|||
<el-scrollbar class="chat-list-items"> |
|||
<div v-for="(chat,index) in chatStore.chats" :key="index"> |
|||
<chat-item v-show="!chat.delete&&chat.showName.includes(searchText)" :chat="chat" :index="index" |
|||
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)" |
|||
:active="chat === chatStore.activeChat"></chat-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-aside> |
|||
<el-container class="chat-box"> |
|||
<chat-box v-if="chatStore.activeChat" :chat="chatStore.activeChat"></chat-box> |
|||
</el-container> |
|||
</el-container> |
|||
<el-container class="chat-page"> |
|||
<el-aside width="260px" class="chat-list-box"> |
|||
<div class="chat-list-header"> |
|||
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
</div> |
|||
<div class="chat-list-loading" v-if="loading" v-loading="true" element-loading-text="消息接收中..." |
|||
element-loading-spinner="el-icon-loading" element-loading-background="#F9F9F9" element-loading-size="24"> |
|||
</div> |
|||
<el-scrollbar class="chat-list-items" v-else> |
|||
<div v-for="(chat,index) in chatStore.chats" :key="index"> |
|||
<chat-item v-show="!chat.delete&&chat.showName.includes(searchText)" :chat="chat" :index="index" |
|||
@click.native="onActiveItem(index)" @delete="onDelItem(index)" @top="onTop(index)" |
|||
:active="chat === chatStore.activeChat"></chat-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-aside> |
|||
<el-container class="chat-box"> |
|||
<chat-box v-if="chatStore.activeChat" :chat="chatStore.activeChat"></chat-box> |
|||
</el-container> |
|||
</el-container> |
|||
</template> |
|||
|
|||
<script> |
|||
import ChatItem from "../components/chat/ChatItem.vue"; |
|||
import ChatBox from "../components/chat/ChatBox.vue"; |
|||
import ChatItem from "../components/chat/ChatItem.vue"; |
|||
import ChatBox from "../components/chat/ChatBox.vue"; |
|||
|
|||
export default { |
|||
name: "chat", |
|||
components: { |
|||
ChatItem, |
|||
ChatBox |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
messageContent: "", |
|||
group: {}, |
|||
groupMembers: [] |
|||
} |
|||
}, |
|||
methods: { |
|||
onActiveItem(index) { |
|||
this.$store.commit("activeChat", index); |
|||
}, |
|||
onDelItem(index) { |
|||
this.$store.commit("removeChat", index); |
|||
}, |
|||
onTop(chatIdx) { |
|||
this.$store.commit("moveTop", chatIdx); |
|||
}, |
|||
}, |
|||
computed: { |
|||
chatStore() { |
|||
return this.$store.state.chatStore; |
|||
}, |
|||
loading(){ |
|||
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg |
|||
} |
|||
} |
|||
} |
|||
export default { |
|||
name: "chat", |
|||
components: { |
|||
ChatItem, |
|||
ChatBox |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
messageContent: "", |
|||
group: {}, |
|||
groupMembers: [] |
|||
} |
|||
}, |
|||
methods: { |
|||
onActiveItem(index) { |
|||
this.$store.commit("activeChat", index); |
|||
}, |
|||
onDelItem(index) { |
|||
this.$store.commit("removeChat", index); |
|||
}, |
|||
onTop(chatIdx) { |
|||
this.$store.commit("moveTop", chatIdx); |
|||
}, |
|||
}, |
|||
computed: { |
|||
chatStore() { |
|||
return this.$store.state.chatStore; |
|||
}, |
|||
loading() { |
|||
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.chat-page { |
|||
.chat-list-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
border-right: #53a0e79c solid 1px; |
|||
background: white; |
|||
width: 3rem; |
|||
.chat-page { |
|||
.chat-list-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
background: var(--im-background); |
|||
|
|||
.chat-list-header { |
|||
padding: 3px 8px; |
|||
line-height: 50px; |
|||
border-bottom: 1px #ddd solid; |
|||
.chat-list-header { |
|||
height: 50px; |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 0 8px; |
|||
} |
|||
|
|||
.el-input__inner { |
|||
border-radius: 10px !important; |
|||
background-color: #F8F8F8; |
|||
} |
|||
.chat-list-loading { |
|||
height: 50px; |
|||
background-color: #eee; |
|||
|
|||
} |
|||
.el-icon-loading { |
|||
font-size: 24px; |
|||
color: var(--im-text-color-light); |
|||
} |
|||
|
|||
.chat-list-loading{ |
|||
height: 50px; |
|||
background-color: #eee; |
|||
.el-loading-text { |
|||
color: var(--im-text-color-light); |
|||
} |
|||
|
|||
.chat-loading-box{ |
|||
height: 100%; |
|||
} |
|||
} |
|||
.chat-loading-box { |
|||
height: 100%; |
|||
} |
|||
} |
|||
|
|||
.chat-list-items { |
|||
flex: 1; |
|||
background: #F8F8F8; |
|||
margin: 0 3px; |
|||
} |
|||
} |
|||
} |
|||
.chat-list-items { |
|||
flex: 1; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,422 +1,416 @@ |
|||
<template> |
|||
<el-container class="group-page"> |
|||
<el-aside width="280px" class="group-list-box"> |
|||
<div class="group-list-header"> |
|||
<el-input class="search-text" placeholder="搜索" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button> |
|||
</div> |
|||
<el-scrollbar class="group-list-items"> |
|||
<div v-for="(group,index) in groupStore.groups" :key="index"> |
|||
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group" |
|||
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)"> |
|||
</group-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-aside> |
|||
<el-container class="group-box"> |
|||
<div class="group-header" v-show="activeGroup.id"> |
|||
{{activeGroup.showGroupName}}({{groupMembers.length}}) |
|||
</div> |
|||
<el-scrollbar class="group-container"> |
|||
<div v-show="activeGroup.id"> |
|||
<div class="group-info"> |
|||
<div> |
|||
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" |
|||
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess" |
|||
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> |
|||
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> |
|||
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|||
</file-upload> |
|||
<head-image v-show="!isOwner" class="avatar" :size="200" :url="activeGroup.headImage" |
|||
radius="10%" :name="activeGroup.showGroupName"> |
|||
</head-image> |
|||
<el-button class="send-btn" icon="el-icon-position" type="primary" |
|||
@click="onSendMessage()">发消息</el-button> |
|||
</div> |
|||
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" |
|||
ref="groupForm"> |
|||
<el-form-item label="群聊名称" prop="name"> |
|||
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群主"> |
|||
<el-input :value="ownerName" disabled></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群名备注"> |
|||
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name" |
|||
maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="我在本群的昵称"> |
|||
<el-input v-model="activeGroup.remarkNickName" maxlength="20" |
|||
:placeholder="$store.state.userStore.userInfo.nickName"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群公告"> |
|||
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" |
|||
maxlength="1024" placeholder="群主未设置"></el-input> |
|||
</el-form-item> |
|||
<div> |
|||
<el-button type="success" @click="onSaveGroup()">保存</el-button> |
|||
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出群聊</el-button> |
|||
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散群聊</el-button> |
|||
</div> |
|||
</el-form> |
|||
</div> |
|||
<el-divider content-position="center"></el-divider> |
|||
<el-scrollbar style="height:200px;"> |
|||
<div class="group-member-list"> |
|||
<div v-for="(member) in groupMembers" :key="member.id"> |
|||
<group-member v-show="!member.quit" class="group-member" :member="member" |
|||
:showDel="isOwner&&member.userId!=activeGroup.ownerId" @del="onKick"></group-member> |
|||
</div> |
|||
<div class="group-invite"> |
|||
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> |
|||
<i class="el-icon-plus"></i> |
|||
</div> |
|||
<div class="invite-member-text">邀请</div> |
|||
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" |
|||
:members="groupMembers" @reload="loadGroupMembers" |
|||
@close="onCloseAddGroupMember"></add-group-member> |
|||
</div> |
|||
</div> |
|||
</el-scrollbar> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-container> |
|||
</el-container> |
|||
<el-container class="group-page"> |
|||
<el-aside width="260px" class="group-list-box"> |
|||
<div class="group-list-header"> |
|||
<el-input class="search-text" size="small" placeholder="搜索" v-model="searchText"> |
|||
<i class="el-icon-search el-input__icon" slot="prefix"> </i> |
|||
</el-input> |
|||
<el-button plain class="add-btn" icon="el-icon-plus" title="创建群聊" @click="onCreateGroup()"></el-button> |
|||
</div> |
|||
<el-scrollbar class="group-list-items"> |
|||
<div v-for="(group,index) in groupStore.groups" :key="index"> |
|||
<group-item v-show="!group.quit&&group.showGroupName.includes(searchText)" :group="group" |
|||
:active="group === groupStore.activeGroup" @click.native="onActiveItem(group,index)"> |
|||
</group-item> |
|||
</div> |
|||
</el-scrollbar> |
|||
</el-aside> |
|||
<el-container class="group-box"> |
|||
<div class="group-header" v-show="activeGroup.id"> |
|||
{{ activeGroup.showGroupName }}({{ groupMembers.length }}) |
|||
</div> |
|||
<div class="group-container"> |
|||
<div v-show="activeGroup.id"> |
|||
<div class="group-info"> |
|||
<div> |
|||
<file-upload v-show="isOwner" class="avatar-uploader" :action="imageAction" |
|||
:showLoading="true" :maxSize="maxSize" @success="onUploadSuccess" |
|||
:fileTypes="['image/jpeg', 'image/png', 'image/jpg','image/webp']"> |
|||
<img v-if="activeGroup.headImage" :src="activeGroup.headImage" class="avatar"> |
|||
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|||
</file-upload> |
|||
<head-image v-show="!isOwner" class="avatar" :size="120" :url="activeGroup.headImage" |
|||
:name="activeGroup.showGroupName"> |
|||
</head-image> |
|||
<el-button class="send-btn" icon="el-icon-position" type="primary" |
|||
@click="onSendMessage()">发消息 |
|||
</el-button> |
|||
</div> |
|||
<el-form class="group-form" label-width="130px" :model="activeGroup" :rules="rules" size="small" |
|||
ref="groupForm"> |
|||
<el-form-item label="群聊名称" prop="name"> |
|||
<el-input v-model="activeGroup.name" :disabled="!isOwner" maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群主"> |
|||
<el-input :value="ownerName" disabled></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群名备注"> |
|||
<el-input v-model="activeGroup.remarkGroupName" :placeholder="activeGroup.name" |
|||
maxlength="20"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="我在本群的昵称"> |
|||
<el-input v-model="activeGroup.remarkNickName" maxlength="20" |
|||
:placeholder="$store.state.userStore.userInfo.nickName"></el-input> |
|||
</el-form-item> |
|||
<el-form-item label="群公告"> |
|||
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" |
|||
maxlength="1024" placeholder="群主未设置"></el-input> |
|||
</el-form-item> |
|||
<div> |
|||
<el-button type="warning" v-show="isOwner" @click="onInviteMember()">邀请</el-button> |
|||
<el-button type="success" @click="onSaveGroup()">保存</el-button> |
|||
<el-button type="danger" v-show="!isOwner" @click="onQuit()">退出</el-button> |
|||
<el-button type="danger" v-show="isOwner" @click="onDissolve()">解散</el-button> |
|||
</div> |
|||
</el-form> |
|||
</div> |
|||
<el-divider content-position="center"></el-divider> |
|||
<div class="group-member-list"> |
|||
<div v-for="(member) in groupMembers" :key="member.id"> |
|||
<group-member v-show="!member.quit" class="group-member" :member="member" |
|||
:showDel="isOwner && member.userId!=activeGroup.ownerId" @del="onKick"></group-member> |
|||
</div> |
|||
<div class="group-invite"> |
|||
<div class="invite-member-btn" title="邀请好友进群聊" @click="onInviteMember()"> |
|||
<i class="el-icon-plus"></i> |
|||
</div> |
|||
<div class="invite-member-text">邀请</div> |
|||
<add-group-member :visible="showAddGroupMember" :groupId="activeGroup.id" |
|||
:members="groupMembers" @reload="loadGroupMembers" |
|||
@close="onCloseAddGroupMember"></add-group-member> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</el-container> |
|||
</el-container> |
|||
</template> |
|||
|
|||
|
|||
<script> |
|||
import GroupItem from '../components/group/GroupItem'; |
|||
import FileUpload from '../components/common/FileUpload'; |
|||
import GroupMember from '../components/group/GroupMember.vue'; |
|||
import AddGroupMember from '../components/group/AddGroupMember.vue'; |
|||
import HeadImage from '../components/common/HeadImage.vue'; |
|||
export default { |
|||
name: "group", |
|||
components: { |
|||
GroupItem, |
|||
GroupMember, |
|||
FileUpload, |
|||
AddGroupMember, |
|||
HeadImage |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
maxSize: 5 * 1024 * 1024, |
|||
activeGroup: {}, |
|||
groupMembers: [], |
|||
showAddGroupMember: false, |
|||
rules: { |
|||
name: [{ |
|||
required: true, |
|||
message: '请输入群聊名称', |
|||
trigger: 'blur' |
|||
}] |
|||
} |
|||
}; |
|||
}, |
|||
methods: { |
|||
onCreateGroup() { |
|||
this.$prompt('请输入群聊名称', '创建群聊', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
inputPattern: /\S/, |
|||
inputErrorMessage: '请输入群聊名称' |
|||
}).then((o) => { |
|||
let userInfo = this.$store.state.userStore.userInfo; |
|||
let data = { |
|||
name: o.value |
|||
} |
|||
this.$http({ |
|||
url: `/group/create?groupName=${o.value}`, |
|||
method: 'post', |
|||
data: data |
|||
}).then((group) => { |
|||
this.$store.commit("addGroup", group); |
|||
}) |
|||
}) |
|||
}, |
|||
onActiveItem(group, index) { |
|||
this.$store.commit("activeGroup", index); |
|||
// store数据不能直接修改,所以深拷贝一份内存 |
|||
this.activeGroup = JSON.parse(JSON.stringify(group)); |
|||
// 重新加载群成员 |
|||
this.loadGroupMembers(); |
|||
}, |
|||
onInviteMember() { |
|||
this.showAddGroupMember = true; |
|||
}, |
|||
onCloseAddGroupMember() { |
|||
this.showAddGroupMember = false; |
|||
}, |
|||
onUploadSuccess(data) { |
|||
this.activeGroup.headImage = data.originUrl; |
|||
this.activeGroup.headImageThumb = data.thumbUrl; |
|||
}, |
|||
onSaveGroup() { |
|||
this.$refs['groupForm'].validate((valid) => { |
|||
if (valid) { |
|||
let vo = this.activeGroup; |
|||
this.$http({ |
|||
url: "/group/modify", |
|||
method: "put", |
|||
data: vo |
|||
}).then((group) => { |
|||
this.$store.commit("updateGroup", group); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
onDissolve() { |
|||
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/delete/${this.activeGroup.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); |
|||
this.$store.commit("removeGroup", this.activeGroup.id); |
|||
this.$store.commit("removeGroupChat", this.activeGroup.id); |
|||
this.reset(); |
|||
}); |
|||
}) |
|||
import GroupItem from '../components/group/GroupItem'; |
|||
import FileUpload from '../components/common/FileUpload'; |
|||
import GroupMember from '../components/group/GroupMember.vue'; |
|||
import AddGroupMember from '../components/group/AddGroupMember.vue'; |
|||
import HeadImage from '../components/common/HeadImage.vue'; |
|||
|
|||
}, |
|||
onKick(member) { |
|||
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/kick/${this.activeGroup.id}`, |
|||
method: 'delete', |
|||
params: { |
|||
userId: member.userId |
|||
} |
|||
}).then(() => { |
|||
this.$message.success(`已将${member.showNickName}移出群聊`); |
|||
member.quit = true; |
|||
}); |
|||
}) |
|||
export default { |
|||
name: "group", |
|||
components: { |
|||
GroupItem, |
|||
GroupMember, |
|||
FileUpload, |
|||
AddGroupMember, |
|||
HeadImage |
|||
}, |
|||
data() { |
|||
return { |
|||
searchText: "", |
|||
maxSize: 5 * 1024 * 1024, |
|||
activeGroup: {}, |
|||
groupMembers: [], |
|||
showAddGroupMember: false, |
|||
rules: { |
|||
name: [{ |
|||
required: true, |
|||
message: '请输入群聊名称', |
|||
trigger: 'blur' |
|||
}] |
|||
} |
|||
}; |
|||
}, |
|||
methods: { |
|||
onCreateGroup() { |
|||
this.$prompt('请输入群聊名称', '创建群聊', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
inputPattern: /\S/, |
|||
inputErrorMessage: '请输入群聊名称' |
|||
}).then((o) => { |
|||
let userInfo = this.$store.state.userStore.userInfo; |
|||
let data = { |
|||
name: o.value |
|||
} |
|||
this.$http({ |
|||
url: `/group/create?groupName=${o.value}`, |
|||
method: 'post', |
|||
data: data |
|||
}).then((group) => { |
|||
this.$store.commit("addGroup", group); |
|||
}) |
|||
}) |
|||
}, |
|||
onActiveItem(group, index) { |
|||
this.$store.commit("activeGroup", index); |
|||
// store数据不能直接修改,所以深拷贝一份内存 |
|||
this.activeGroup = JSON.parse(JSON.stringify(group)); |
|||
// 重新加载群成员 |
|||
this.loadGroupMembers(); |
|||
}, |
|||
onInviteMember() { |
|||
this.showAddGroupMember = true; |
|||
}, |
|||
onCloseAddGroupMember() { |
|||
this.showAddGroupMember = false; |
|||
}, |
|||
onUploadSuccess(data) { |
|||
this.activeGroup.headImage = data.originUrl; |
|||
this.activeGroup.headImageThumb = data.thumbUrl; |
|||
}, |
|||
onSaveGroup() { |
|||
this.$refs['groupForm'].validate((valid) => { |
|||
if (valid) { |
|||
let vo = this.activeGroup; |
|||
this.$http({ |
|||
url: "/group/modify", |
|||
method: "put", |
|||
data: vo |
|||
}).then((group) => { |
|||
this.$store.commit("updateGroup", group); |
|||
this.$message.success("修改成功"); |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
onDissolve() { |
|||
this.$confirm(`确认要解散'${this.activeGroup.name}'吗?`, '确认解散?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/delete/${this.activeGroup.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); |
|||
this.$store.commit("removeGroup", this.activeGroup.id); |
|||
this.$store.commit("removeGroupChat", this.activeGroup.id); |
|||
this.reset(); |
|||
}); |
|||
}) |
|||
|
|||
}, |
|||
onQuit() { |
|||
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/quit/${this.activeGroup.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$message.success(`您已退出'${this.activeGroup.name}'`); |
|||
this.$store.commit("removeGroup", this.activeGroup.id); |
|||
this.$store.commit("removeGroupChat", this.activeGroup.id); |
|||
this.reset(); |
|||
}); |
|||
}) |
|||
}, |
|||
onKick(member) { |
|||
this.$confirm(`确定将成员'${member.showNickName}'移出群聊吗?`, '确认移出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/kick/${this.activeGroup.id}`, |
|||
method: 'delete', |
|||
params: { |
|||
userId: member.userId |
|||
} |
|||
}).then(() => { |
|||
this.$message.success(`已将${member.showNickName}移出群聊`); |
|||
member.quit = true; |
|||
}); |
|||
}) |
|||
|
|||
}, |
|||
onSendMessage() { |
|||
let chat = { |
|||
type: 'GROUP', |
|||
targetId: this.activeGroup.id, |
|||
showName: this.activeGroup.showGroupName, |
|||
headImage: this.activeGroup.headImage, |
|||
}; |
|||
this.$store.commit("openChat", chat); |
|||
this.$store.commit("activeChat", 0); |
|||
this.$router.push("/home/chat"); |
|||
}, |
|||
loadGroupMembers() { |
|||
this.$http({ |
|||
url: `/group/members/${this.activeGroup.id}`, |
|||
method: "get" |
|||
}).then((members) => { |
|||
this.groupMembers = members; |
|||
}) |
|||
}, |
|||
reset() { |
|||
this.activeGroup = {}; |
|||
this.groupMembers = []; |
|||
} |
|||
}, |
|||
computed: { |
|||
groupStore() { |
|||
return this.$store.state.groupStore; |
|||
}, |
|||
ownerName() { |
|||
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId); |
|||
return member && member.showNickName; |
|||
}, |
|||
isOwner() { |
|||
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id; |
|||
}, |
|||
imageAction() { |
|||
return `/image/upload`; |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
onQuit() { |
|||
this.$confirm(`确认退出'${this.activeGroup.showGroupName}',并清空聊天记录吗?`, '确认退出?', { |
|||
confirmButtonText: '确定', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}).then(() => { |
|||
this.$http({ |
|||
url: `/group/quit/${this.activeGroup.id}`, |
|||
method: 'delete' |
|||
}).then(() => { |
|||
this.$message.success(`您已退出'${this.activeGroup.name}'`); |
|||
this.$store.commit("removeGroup", this.activeGroup.id); |
|||
this.$store.commit("removeGroupChat", this.activeGroup.id); |
|||
this.reset(); |
|||
}); |
|||
}) |
|||
|
|||
}, |
|||
onSendMessage() { |
|||
let chat = { |
|||
type: 'GROUP', |
|||
targetId: this.activeGroup.id, |
|||
showName: this.activeGroup.showGroupName, |
|||
headImage: this.activeGroup.headImage, |
|||
}; |
|||
this.$store.commit("openChat", chat); |
|||
this.$store.commit("activeChat", 0); |
|||
this.$router.push("/home/chat"); |
|||
}, |
|||
loadGroupMembers() { |
|||
this.$http({ |
|||
url: `/group/members/${this.activeGroup.id}`, |
|||
method: "get" |
|||
}).then((members) => { |
|||
this.groupMembers = members; |
|||
}) |
|||
}, |
|||
reset() { |
|||
this.activeGroup = {}; |
|||
this.groupMembers = []; |
|||
} |
|||
}, |
|||
computed: { |
|||
groupStore() { |
|||
return this.$store.state.groupStore; |
|||
}, |
|||
ownerName() { |
|||
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId); |
|||
return member && member.showNickName; |
|||
}, |
|||
isOwner() { |
|||
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id; |
|||
}, |
|||
imageAction() { |
|||
return `/image/upload`; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss"> |
|||
.group-page { |
|||
.group-list-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
border-right: #53a0e79c solid 1px; |
|||
background: #F8F8F8; |
|||
.group-page { |
|||
.group-list-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
background: var(--im-background); |
|||
|
|||
.group-list-header { |
|||
height: 50px; |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 0 8px; |
|||
|
|||
.add-btn { |
|||
padding: 5px !important; |
|||
margin: 5px; |
|||
font-size: 16px; |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
|
|||
.group-list-header { |
|||
height: 50px; |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 3px 8px; |
|||
background-color: white; |
|||
border-bottom: 1px #ddd solid; |
|||
.group-list-items { |
|||
flex: 1; |
|||
} |
|||
} |
|||
|
|||
.el-input__inner { |
|||
border-radius: 10px !important; |
|||
} |
|||
.group-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
|
|||
.add-btn { |
|||
padding: 5px !important; |
|||
margin: 5px; |
|||
font-size: 20px; |
|||
color: #587FF0; |
|||
border: #587FF0 1px solid; |
|||
background-color: #F0F8FF; |
|||
border-radius: 50%; |
|||
} |
|||
} |
|||
.group-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
padding: 0 12px; |
|||
line-height: 50px; |
|||
font-size: var(--im-font-size-larger); |
|||
border-bottom: var(--im-border); |
|||
} |
|||
|
|||
.group-list-items { |
|||
flex: 1; |
|||
margin: 0 3px; |
|||
background: #F8F8F8; |
|||
} |
|||
} |
|||
.el-divider--horizontal { |
|||
margin: 16px 0; |
|||
} |
|||
|
|||
.group-box { |
|||
display: flex; |
|||
flex-direction: column; |
|||
.group-container { |
|||
overflow: auto; |
|||
padding: 20px; |
|||
flex: 1; |
|||
|
|||
.group-header { |
|||
padding: 3px; |
|||
height: 50px; |
|||
line-height: 50px; |
|||
font-size: 20px; |
|||
font-weight: 600; |
|||
text-align: center; |
|||
background-color: white; |
|||
border-bottom: 1px #ddd solid; |
|||
} |
|||
.group-info { |
|||
display: flex; |
|||
padding: 5px 20px; |
|||
|
|||
.group-container { |
|||
padding: 20px; |
|||
flex: 1; |
|||
.group-form { |
|||
flex: 1; |
|||
padding-left: 40px; |
|||
max-width: 700px; |
|||
} |
|||
|
|||
.group-info { |
|||
display: flex; |
|||
padding: 5px 20px; |
|||
.avatar-uploader { |
|||
--width: 120px; |
|||
text-align: left; |
|||
|
|||
.group-form { |
|||
flex: 1; |
|||
padding-left: 40px; |
|||
max-width: 700px; |
|||
} |
|||
.el-upload { |
|||
border: 1px dashed #d9d9d9 !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.avatar-uploader { |
|||
text-align: left; |
|||
.el-upload:hover { |
|||
border-color: #409EFF; |
|||
} |
|||
|
|||
.el-upload { |
|||
border: 1px dashed #d9d9d9 !important; |
|||
border-radius: 6px; |
|||
cursor: pointer; |
|||
position: relative; |
|||
overflow: hidden; |
|||
} |
|||
.avatar-uploader-icon { |
|||
font-size: 28px; |
|||
color: #8c939d; |
|||
width: var(--width); |
|||
height: var(--width); |
|||
line-height: var(--width); |
|||
text-align: center; |
|||
} |
|||
|
|||
.el-upload:hover { |
|||
border-color: #409EFF; |
|||
} |
|||
.avatar { |
|||
width: var(--width); |
|||
height: var(--width); |
|||
display: block; |
|||
} |
|||
} |
|||
|
|||
.avatar-uploader-icon { |
|||
font-size: 28px; |
|||
color: #8c939d; |
|||
width: 200px; |
|||
height: 200px; |
|||
line-height: 200px; |
|||
text-align: center; |
|||
} |
|||
.send-btn { |
|||
margin-top: 12px; |
|||
} |
|||
} |
|||
|
|||
.avatar { |
|||
width: 200px; |
|||
height: 200px; |
|||
display: block; |
|||
} |
|||
} |
|||
.group-member-list { |
|||
padding: 0 12px; |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: wrap; |
|||
text-align: center; |
|||
|
|||
.send-btn { |
|||
margin-top: 20px; |
|||
} |
|||
} |
|||
.group-member { |
|||
margin-right: 5px; |
|||
} |
|||
|
|||
.group-member-list { |
|||
padding: 5px 20px; |
|||
display: flex; |
|||
align-items: center; |
|||
flex-wrap: wrap; |
|||
font-size: 16px; |
|||
text-align: center; |
|||
.group-invite { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
width: 60px; |
|||
|
|||
.group-member { |
|||
margin-right: 15px; |
|||
} |
|||
.invite-member-btn { |
|||
width: 38px; |
|||
height: 38px; |
|||
line-height: 38px; |
|||
border: var(--im-border); |
|||
font-size: 14px; |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
|
|||
.group-invite { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
width: 60px; |
|||
&:hover { |
|||
border: #aaaaaa solid 1px; |
|||
} |
|||
} |
|||
|
|||
.invite-member-btn { |
|||
width: 100%; |
|||
height: 60px; |
|||
line-height: 60px; |
|||
border: #cccccc solid 1px; |
|||
font-size: 25px; |
|||
cursor: pointer; |
|||
box-sizing: border-box; |
|||
.invite-member-text { |
|||
font-size: var(--im-font-size-smaller); |
|||
text-align: center; |
|||
width: 100%; |
|||
height: 30px; |
|||
line-height: 30px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
border: #aaaaaa solid 1px; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.invite-member-text { |
|||
font-size: 16px; |
|||
text-align: center; |
|||
width: 100%; |
|||
height: 30px; |
|||
line-height: 30px; |
|||
white-space: nowrap; |
|||
text-overflow: ellipsis; |
|||
overflow: hidden |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -1,431 +1,515 @@ |
|||
<template> |
|||
<el-container class="home-page"> |
|||
<el-aside width="80px" class="navi-bar"> |
|||
<div class="user-head-image"> |
|||
<head-image :name="$store.state.userStore.userInfo.nickName" |
|||
:url="$store.state.userStore.userInfo.headImageThumb" :size="60" |
|||
@click.native="showSettingDialog = true"> |
|||
</head-image> |
|||
</div> |
|||
<el-menu background-color="#E8F2FF" style="margin-top: 25px;"> |
|||
<el-menu-item title="聊天"> |
|||
<router-link class="link" v-bind:to="'/home/chat'"> |
|||
<span class="icon iconfont icon-chat"></span> |
|||
<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div> |
|||
</router-link> |
|||
</el-menu-item> |
|||
<el-menu-item title="好友"> |
|||
<router-link class="link" v-bind:to="'/home/friend'"> |
|||
<span class="icon iconfont icon-friend"></span> |
|||
</router-link> |
|||
</el-menu-item> |
|||
<el-menu-item title="群聊"> |
|||
<router-link class="link" v-bind:to="'/home/group'"> |
|||
<span class="icon iconfont icon-group"></span> |
|||
</router-link> |
|||
</el-menu-item> |
|||
<el-menu-item title="设置" @click="showSetting()"> |
|||
<span class="icon iconfont icon-setting"></span> |
|||
</el-menu-item> |
|||
</el-menu> |
|||
<div class="exit-box" @click="onExit()" title="退出"> |
|||
<span class="icon iconfont icon-exit"></span> |
|||
</div> |
|||
</el-aside> |
|||
<el-main class="content-box"> |
|||
<router-view></router-view> |
|||
</el-main> |
|||
<setting :visible="showSettingDialog" @close="closeSetting()"></setting> |
|||
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" |
|||
@close="$store.commit('closeUserInfoBox')"></user-info> |
|||
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" |
|||
@close="$store.commit('closeFullImageBox')"></full-image> |
|||
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video> |
|||
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video> |
|||
</el-container> |
|||
<div class="home-page"> |
|||
<div class="app-container" :class="{fullscreen: isFullscreen}"> |
|||
<div class="navi-bar"> |
|||
<div class="navi-bar-box"> |
|||
<div class="top"> |
|||
<div class="user-head-image"> |
|||
<head-image :name="$store.state.userStore.userInfo.nickName" |
|||
:size="38" |
|||
:url="$store.state.userStore.userInfo.headImageThumb" |
|||
@click.native="showSettingDialog = true"> |
|||
</head-image> |
|||
</div> |
|||
|
|||
<div class="menu"> |
|||
<router-link class="link" v-bind:to="'/home/chat'"> |
|||
<div class="menu-item"> |
|||
<span class="icon iconfont icon-chat"></span> |
|||
<div v-show="unreadCount > 0" class="unread-text">{{ unreadCount }}</div> |
|||
</div> |
|||
</router-link> |
|||
<router-link class="link" v-bind:to="'/home/friend'"> |
|||
<div class="menu-item"> |
|||
<span class="icon iconfont icon-friend"></span> |
|||
</div> |
|||
</router-link> |
|||
<router-link class="link" v-bind:to="'/home/group'"> |
|||
<div class="menu-item"> |
|||
<span class="icon iconfont icon-group" style="font-size: 28px"></span> |
|||
</div> |
|||
</router-link> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="botoom"> |
|||
<div class="botoom-item" @click="isFullscreen = !isFullscreen"> |
|||
<i class="el-icon-full-screen"></i> |
|||
</div> |
|||
<div class="botoom-item" @click="showSetting"> |
|||
<span class="icon iconfont icon-setting" style="font-size: 20px"></span> |
|||
</div> |
|||
<div class="botoom-item" @click="onExit()" title="退出"> |
|||
<span class="icon iconfont icon-exit"></span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="content-box"> |
|||
<router-view></router-view> |
|||
</div> |
|||
<setting :visible="showSettingDialog" @close="closeSetting()"></setting> |
|||
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" |
|||
@close="$store.commit('closeUserInfoBox')"></user-info> |
|||
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" |
|||
@close="$store.commit('closeFullImageBox')"></full-image> |
|||
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video> |
|||
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video> |
|||
</div> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
import HeadImage from '../components/common/HeadImage.vue'; |
|||
import Setting from '../components/setting/Setting.vue'; |
|||
import UserInfo from '../components/common/UserInfo.vue'; |
|||
import FullImage from '../components/common/FullImage.vue'; |
|||
import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue'; |
|||
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue'; |
|||
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue'; |
|||
import HeadImage from '../components/common/HeadImage.vue'; |
|||
import Setting from '../components/setting/Setting.vue'; |
|||
import UserInfo from '../components/common/UserInfo.vue'; |
|||
import FullImage from '../components/common/FullImage.vue'; |
|||
import RtcPrivateVideo from '../components/rtc/RtcPrivateVideo.vue'; |
|||
import RtcPrivateAcceptor from '../components/rtc/RtcPrivateAcceptor.vue'; |
|||
import RtcGroupVideo from '../components/rtc/RtcGroupVideo.vue'; |
|||
|
|||
export default { |
|||
components: { |
|||
HeadImage, |
|||
Setting, |
|||
UserInfo, |
|||
FullImage, |
|||
RtcPrivateVideo, |
|||
RtcPrivateAcceptor, |
|||
RtcGroupVideo |
|||
}, |
|||
data() { |
|||
return { |
|||
showSettingDialog: false, |
|||
lastPlayAudioTime: new Date().getTime() - 1000, |
|||
isFullscreen: false |
|||
} |
|||
}, |
|||
methods: { |
|||
init() { |
|||
this.$eventBus.$on('openPrivateVideo', (rctInfo) => { |
|||
// 进入单人视频通话 |
|||
this.$refs.rtcPrivateVideo.open(rctInfo); |
|||
}); |
|||
this.$eventBus.$on('openGroupVideo', (rctInfo) => { |
|||
// 进入多人视频通话 |
|||
this.$refs.rtcGroupVideo.open(rctInfo); |
|||
}); |
|||
|
|||
this.$store.dispatch("load").then(() => { |
|||
// ws初始化 |
|||
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); |
|||
this.$wsApi.onConnect(() => { |
|||
// 加载离线消息 |
|||
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); |
|||
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); |
|||
}); |
|||
this.$wsApi.onMessage((cmd, msgInfo) => { |
|||
if (cmd == 2) { |
|||
// 关闭ws |
|||
this.$wsApi.close(3000) |
|||
// 异地登录,强制下线 |
|||
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", { |
|||
confirmButtonText: '确定', |
|||
callback: action => { |
|||
location.href = "/"; |
|||
} |
|||
}); |
|||
|
|||
export default { |
|||
components: { |
|||
HeadImage, |
|||
Setting, |
|||
UserInfo, |
|||
FullImage, |
|||
RtcPrivateVideo, |
|||
RtcPrivateAcceptor, |
|||
RtcGroupVideo |
|||
}, |
|||
data() { |
|||
return { |
|||
showSettingDialog: false, |
|||
lastPlayAudioTime: new Date().getTime() - 1000 |
|||
} |
|||
}, |
|||
methods: { |
|||
init() { |
|||
this.$eventBus.$on('openPrivateVideo', (rctInfo) => { |
|||
// 进入单人视频通话 |
|||
this.$refs.rtcPrivateVideo.open(rctInfo); |
|||
}); |
|||
this.$eventBus.$on('openGroupVideo', (rctInfo) => { |
|||
// 进入多人视频通话 |
|||
this.$refs.rtcGroupVideo.open(rctInfo); |
|||
}); |
|||
} else if (cmd == 3) { |
|||
// 插入私聊消息 |
|||
this.handlePrivateMessage(msgInfo); |
|||
} else if (cmd == 4) { |
|||
// 插入群聊消息 |
|||
this.handleGroupMessage(msgInfo); |
|||
} else if (cmd == 5) { |
|||
// 处理系统消息 |
|||
this.handleSystemMessage(msgInfo); |
|||
} |
|||
}); |
|||
this.$wsApi.onClose((e) => { |
|||
console.log(e); |
|||
if (e.code != 3000) { |
|||
// 断线重连 |
|||
this.$message.error("连接断开,正在尝试重新连接..."); |
|||
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem( |
|||
"accessToken")); |
|||
} |
|||
}); |
|||
}).catch((e) => { |
|||
console.log("初始化失败", e); |
|||
}) |
|||
}, |
|||
pullPrivateOfflineMessage(minId) { |
|||
this.$store.commit("loadingPrivateMsg", true) |
|||
this.$http({ |
|||
url: "/message/private/pullOfflineMessage?minId=" + minId, |
|||
method: 'GET' |
|||
}).catch(() => { |
|||
this.$store.commit("loadingPrivateMsg", false) |
|||
}) |
|||
}, |
|||
pullGroupOfflineMessage(minId) { |
|||
this.$store.commit("loadingGroupMsg", true) |
|||
this.$http({ |
|||
url: "/message/group/pullOfflineMessage?minId=" + minId, |
|||
method: 'GET' |
|||
}).catch(() => { |
|||
this.$store.commit("loadingGroupMsg", false) |
|||
}) |
|||
}, |
|||
handlePrivateMessage(msg) { |
|||
// 消息加载标志 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { |
|||
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content)) |
|||
return; |
|||
} |
|||
// 消息已读处理,清空已读数量 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { |
|||
this.$store.commit("resetUnreadCount", { |
|||
type: 'PRIVATE', |
|||
targetId: msg.recvId |
|||
}) |
|||
return; |
|||
} |
|||
// 消息回执处理,改消息状态为已读 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { |
|||
this.$store.commit("readedMessage", { |
|||
friendId: msg.sendId |
|||
}) |
|||
return; |
|||
} |
|||
// 标记这条消息是不是自己发的 |
|||
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; |
|||
// 单人webrtc 信令 |
|||
if (this.$msgType.isRtcPrivate(msg.type)) { |
|||
this.$refs.rtcPrivateVideo.onRTCMessage(msg) |
|||
return; |
|||
} |
|||
// 好友id |
|||
let friendId = msg.selfSend ? msg.recvId : msg.sendId; |
|||
this.loadFriendInfo(friendId).then((friend) => { |
|||
this.insertPrivateMessage(friend, msg); |
|||
}) |
|||
}, |
|||
insertPrivateMessage(friend, msg) { |
|||
|
|||
this.$store.dispatch("load").then(() => { |
|||
// ws初始化 |
|||
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); |
|||
this.$wsApi.onConnect(() => { |
|||
// 加载离线消息 |
|||
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); |
|||
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); |
|||
}); |
|||
this.$wsApi.onMessage((cmd, msgInfo) => { |
|||
if (cmd == 2) { |
|||
// 关闭ws |
|||
this.$wsApi.close(3000) |
|||
// 异地登录,强制下线 |
|||
this.$alert("您已在其他地方登陆,将被强制下线", "强制下线通知", { |
|||
confirmButtonText: '确定', |
|||
callback: action => { |
|||
location.href = "/"; |
|||
} |
|||
}); |
|||
let chatInfo = { |
|||
type: 'PRIVATE', |
|||
targetId: friend.id, |
|||
showName: friend.nickName, |
|||
headImage: friend.headImage |
|||
}; |
|||
// 打开会话 |
|||
this.$store.commit("openChat", chatInfo); |
|||
// 插入消息 |
|||
this.$store.commit("insertMessage", msg); |
|||
// 播放提示音 |
|||
if (!msg.selfSend && this.$msgType.isNormal(msg.type) && |
|||
msg.status != this.$enums.MESSAGE_STATUS.READED) { |
|||
this.playAudioTip(); |
|||
} |
|||
}, |
|||
handleGroupMessage(msg) { |
|||
// 消息加载标志 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { |
|||
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content)) |
|||
return; |
|||
} |
|||
// 消息已读处理 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { |
|||
// 我已读对方的消息,清空已读数量 |
|||
let chatInfo = { |
|||
type: 'GROUP', |
|||
targetId: msg.groupId |
|||
} |
|||
this.$store.commit("resetUnreadCount", chatInfo) |
|||
return; |
|||
} |
|||
// 消息回执处理 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { |
|||
// 更新消息已读人数 |
|||
let msgInfo = { |
|||
id: msg.id, |
|||
groupId: msg.groupId, |
|||
readedCount: msg.readedCount, |
|||
receiptOk: msg.receiptOk |
|||
}; |
|||
this.$store.commit("updateMessage", msgInfo) |
|||
return; |
|||
} |
|||
// 标记这条消息是不是自己发的 |
|||
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; |
|||
// 群视频信令 |
|||
if (this.$msgType.isRtcGroup(msg.type)) { |
|||
this.$nextTick(() => { |
|||
this.$refs.rtcGroupVideo.onRTCMessage(msg); |
|||
}) |
|||
return; |
|||
} |
|||
this.loadGroupInfo(msg.groupId).then((group) => { |
|||
// 插入群聊消息 |
|||
this.insertGroupMessage(group, msg); |
|||
}) |
|||
}, |
|||
insertGroupMessage(group, msg) { |
|||
|
|||
} else if (cmd == 3) { |
|||
// 插入私聊消息 |
|||
this.handlePrivateMessage(msgInfo); |
|||
} else if (cmd == 4) { |
|||
// 插入群聊消息 |
|||
this.handleGroupMessage(msgInfo); |
|||
} else if (cmd == 5){ |
|||
// 处理系统消息 |
|||
this.handleSystemMessage(msgInfo); |
|||
} |
|||
}); |
|||
this.$wsApi.onClose((e) => { |
|||
console.log(e); |
|||
if (e.code != 3000) { |
|||
// 断线重连 |
|||
this.$message.error("连接断开,正在尝试重新连接..."); |
|||
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem( |
|||
"accessToken")); |
|||
} |
|||
}); |
|||
}).catch((e) => { |
|||
console.log("初始化失败", e); |
|||
}) |
|||
}, |
|||
pullPrivateOfflineMessage(minId) { |
|||
this.$store.commit("loadingPrivateMsg", true) |
|||
this.$http({ |
|||
url: "/message/private/pullOfflineMessage?minId=" + minId, |
|||
method: 'GET' |
|||
}).catch(() => { |
|||
this.$store.commit("loadingPrivateMsg", false) |
|||
}) |
|||
}, |
|||
pullGroupOfflineMessage(minId) { |
|||
this.$store.commit("loadingGroupMsg", true) |
|||
this.$http({ |
|||
url: "/message/group/pullOfflineMessage?minId=" + minId, |
|||
method: 'GET' |
|||
}).catch(() => { |
|||
this.$store.commit("loadingGroupMsg", false) |
|||
}) |
|||
}, |
|||
handlePrivateMessage(msg) { |
|||
// 消息加载标志 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { |
|||
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content)) |
|||
return; |
|||
} |
|||
// 消息已读处理,清空已读数量 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { |
|||
this.$store.commit("resetUnreadCount", { |
|||
type: 'PRIVATE', |
|||
targetId: msg.recvId |
|||
}) |
|||
return; |
|||
} |
|||
// 消息回执处理,改消息状态为已读 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { |
|||
this.$store.commit("readedMessage", { |
|||
friendId: msg.sendId |
|||
}) |
|||
return; |
|||
} |
|||
// 标记这条消息是不是自己发的 |
|||
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; |
|||
// 单人webrtc 信令 |
|||
if (this.$msgType.isRtcPrivate(msg.type)) { |
|||
this.$refs.rtcPrivateVideo.onRTCMessage(msg) |
|||
return; |
|||
} |
|||
// 好友id |
|||
let friendId = msg.selfSend ? msg.recvId : msg.sendId; |
|||
this.loadFriendInfo(friendId).then((friend) => { |
|||
this.insertPrivateMessage(friend, msg); |
|||
}) |
|||
}, |
|||
insertPrivateMessage(friend, msg) { |
|||
let chatInfo = { |
|||
type: 'PRIVATE', |
|||
targetId: friend.id, |
|||
showName: friend.nickName, |
|||
headImage: friend.headImage |
|||
}; |
|||
// 打开会话 |
|||
this.$store.commit("openChat", chatInfo); |
|||
// 插入消息 |
|||
this.$store.commit("insertMessage", msg); |
|||
// 播放提示音 |
|||
if (!msg.selfSend && this.$msgType.isNormal(msg.type) && |
|||
msg.status != this.$enums.MESSAGE_STATUS.READED) { |
|||
this.playAudioTip(); |
|||
} |
|||
}, |
|||
handleGroupMessage(msg) { |
|||
// 消息加载标志 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { |
|||
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content)) |
|||
return; |
|||
} |
|||
// 消息已读处理 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { |
|||
// 我已读对方的消息,清空已读数量 |
|||
let chatInfo = { |
|||
type: 'GROUP', |
|||
targetId: msg.groupId |
|||
} |
|||
this.$store.commit("resetUnreadCount", chatInfo) |
|||
return; |
|||
} |
|||
// 消息回执处理 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { |
|||
// 更新消息已读人数 |
|||
let msgInfo = { |
|||
id: msg.id, |
|||
groupId: msg.groupId, |
|||
readedCount: msg.readedCount, |
|||
receiptOk: msg.receiptOk |
|||
}; |
|||
this.$store.commit("updateMessage", msgInfo) |
|||
return; |
|||
} |
|||
// 标记这条消息是不是自己发的 |
|||
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; |
|||
// 群视频信令 |
|||
if (this.$msgType.isRtcGroup(msg.type)) { |
|||
this.$nextTick(() => { |
|||
this.$refs.rtcGroupVideo.onRTCMessage(msg); |
|||
}) |
|||
return; |
|||
} |
|||
this.loadGroupInfo(msg.groupId).then((group) => { |
|||
// 插入群聊消息 |
|||
this.insertGroupMessage(group, msg); |
|||
}) |
|||
}, |
|||
insertGroupMessage(group, msg) { |
|||
let chatInfo = { |
|||
type: 'GROUP', |
|||
targetId: group.id, |
|||
showName: group.showGroupName, |
|||
headImage: group.headImageThumb |
|||
}; |
|||
// 打开会话 |
|||
this.$store.commit("openChat", chatInfo); |
|||
// 插入消息 |
|||
this.$store.commit("insertMessage", msg); |
|||
// 播放提示音 |
|||
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO && |
|||
msg.status != this.$enums.MESSAGE_STATUS.READED) { |
|||
this.playAudioTip(); |
|||
} |
|||
}, |
|||
handleSystemMessage(msg){ |
|||
// 用户被封禁 |
|||
if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) { |
|||
this.$wsApi.close(3000); |
|||
this.$alert("您的账号已被管理员封禁,原因:"+ msg.content, "账号被封禁", { |
|||
confirmButtonText: '确定', |
|||
callback: action => { |
|||
this.onExit(); |
|||
} |
|||
}); |
|||
return; |
|||
} |
|||
}, |
|||
onExit() { |
|||
this.$wsApi.close(3000); |
|||
sessionStorage.removeItem("accessToken"); |
|||
location.href = "/"; |
|||
}, |
|||
playAudioTip() { |
|||
// 离线消息不播放铃声 |
|||
if(this.$store.getters.isLoading()){ |
|||
return; |
|||
} |
|||
// 防止过于密集播放 |
|||
if (new Date().getTime() - this.lastPlayAudioTime > 1000) { |
|||
this.lastPlayAudioTime = new Date().getTime(); |
|||
let audio = new Audio(); |
|||
let url = require(`@/assets/audio/tip.wav`); |
|||
audio.src = url; |
|||
audio.play(); |
|||
} |
|||
}, |
|||
showSetting() { |
|||
this.showSettingDialog = true; |
|||
}, |
|||
closeSetting() { |
|||
this.showSettingDialog = false; |
|||
}, |
|||
loadFriendInfo(id) { |
|||
return new Promise((resolve, reject) => { |
|||
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id); |
|||
if (friend) { |
|||
resolve(friend); |
|||
} else { |
|||
this.$http({ |
|||
url: `/friend/find/${id}`, |
|||
method: 'get' |
|||
}).then((friend) => { |
|||
this.$store.commit("addFriend", friend); |
|||
resolve(friend) |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
loadGroupInfo(id) { |
|||
return new Promise((resolve, reject) => { |
|||
let group = this.$store.state.groupStore.groups.find((g) => g.id == id); |
|||
if (group) { |
|||
resolve(group); |
|||
} else { |
|||
this.$http({ |
|||
url: `/group/find/${id}`, |
|||
method: 'get' |
|||
}).then((group) => { |
|||
resolve(group) |
|||
this.$store.commit("addGroup", group); |
|||
}) |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
computed: { |
|||
uiStore() { |
|||
return this.$store.state.uiStore; |
|||
}, |
|||
unreadCount() { |
|||
let unreadCount = 0; |
|||
let chats = this.$store.state.chatStore.chats; |
|||
chats.forEach((chat) => { |
|||
if(!chat.delete){ |
|||
unreadCount += chat.unreadCount |
|||
} |
|||
}); |
|||
return unreadCount; |
|||
} |
|||
}, |
|||
watch: { |
|||
unreadCount: { |
|||
handler(newCount, oldCount) { |
|||
let tip = newCount > 0 ? `${newCount}条未读` : ""; |
|||
this.$elm.setTitleTip(tip); |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
}, |
|||
unmounted() { |
|||
this.$wsApi.close(); |
|||
} |
|||
} |
|||
let chatInfo = { |
|||
type: 'GROUP', |
|||
targetId: group.id, |
|||
showName: group.showGroupName, |
|||
headImage: group.headImageThumb |
|||
}; |
|||
// 打开会话 |
|||
this.$store.commit("openChat", chatInfo); |
|||
// 插入消息 |
|||
this.$store.commit("insertMessage", msg); |
|||
// 播放提示音 |
|||
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO && |
|||
msg.status != this.$enums.MESSAGE_STATUS.READED) { |
|||
this.playAudioTip(); |
|||
} |
|||
}, |
|||
handleSystemMessage(msg) { |
|||
// 用户被封禁 |
|||
|
|||
if (msg.type == this.$enums.MESSAGE_TYPE.USER_BANNED) { |
|||
this.$wsApi.close(3000); |
|||
this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", { |
|||
confirmButtonText: '确定', |
|||
callback: action => { |
|||
this.onExit(); |
|||
} |
|||
}); |
|||
return; |
|||
} |
|||
}, |
|||
onExit() { |
|||
this.$wsApi.close(3000); |
|||
sessionStorage.removeItem("accessToken"); |
|||
location.href = "/"; |
|||
}, |
|||
playAudioTip() { |
|||
// 离线消息不播放铃声 |
|||
if (this.$store.getters.isLoading()) { |
|||
return; |
|||
} |
|||
// 防止过于密集播放 |
|||
if (new Date().getTime() - this.lastPlayAudioTime > 1000) { |
|||
this.lastPlayAudioTime = new Date().getTime(); |
|||
let audio = new Audio(); |
|||
let url = require(`@/assets/audio/tip.wav`); |
|||
audio.src = url; |
|||
audio.play(); |
|||
} |
|||
|
|||
}, |
|||
showSetting() { |
|||
this.showSettingDialog = true; |
|||
}, |
|||
closeSetting() { |
|||
this.showSettingDialog = false; |
|||
}, |
|||
loadFriendInfo(id) { |
|||
return new Promise((resolve, reject) => { |
|||
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id); |
|||
if (friend) { |
|||
resolve(friend); |
|||
} else { |
|||
this.$http({ |
|||
url: `/friend/find/${id}`, |
|||
method: 'get' |
|||
}).then((friend) => { |
|||
this.$store.commit("addFriend", friend); |
|||
resolve(friend) |
|||
}) |
|||
} |
|||
}); |
|||
}, |
|||
loadGroupInfo(id) { |
|||
return new Promise((resolve, reject) => { |
|||
let group = this.$store.state.groupStore.groups.find((g) => g.id == id); |
|||
if (group) { |
|||
resolve(group); |
|||
} else { |
|||
this.$http({ |
|||
url: `/group/find/${id}`, |
|||
method: 'get' |
|||
}).then((group) => { |
|||
resolve(group) |
|||
this.$store.commit("addGroup", group); |
|||
}) |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
computed: { |
|||
uiStore() { |
|||
return this.$store.state.uiStore; |
|||
}, |
|||
unreadCount() { |
|||
let unreadCount = 0; |
|||
let chats = this.$store.state.chatStore.chats; |
|||
chats.forEach((chat) => { |
|||
if (!chat.delete) { |
|||
unreadCount += chat.unreadCount |
|||
} |
|||
}); |
|||
return unreadCount; |
|||
} |
|||
}, |
|||
watch: { |
|||
unreadCount: { |
|||
handler(newCount, oldCount) { |
|||
let tip = newCount > 0 ? `${newCount}条未读` : ""; |
|||
this.$elm.setTitleTip(tip); |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.init(); |
|||
}, |
|||
unmounted() { |
|||
this.$wsApi.close(); |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.navi-bar { |
|||
background: #E8F2FF; |
|||
padding: 10px; |
|||
padding-top: 20px; |
|||
border-right: #53a0e79c solid 1px; |
|||
.home-page { |
|||
height: 100vh; |
|||
width: 100vw; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
border-radius: 4px; |
|||
overflow: hidden; |
|||
background: #e8f2ff; |
|||
//background-image: url('../assets/image/background.jpg'); |
|||
|
|||
.app-container { |
|||
width: 62vw; |
|||
height: 80vh; |
|||
display: flex; |
|||
min-height: 600px; |
|||
min-width: 970px; |
|||
position: absolute; |
|||
border-radius: 4px; |
|||
overflow: hidden; |
|||
box-shadow: var(--im-box-shadow-dark); |
|||
transition: 0.2s; |
|||
|
|||
&.fullscreen { |
|||
transition: 0.2s; |
|||
width: 100vw; |
|||
height: 100vh; |
|||
} |
|||
} |
|||
|
|||
.navi-bar { |
|||
--icon-font-size: 22px; |
|||
--width: 56px; |
|||
width: var(--width); |
|||
background: var(--im-color-primary); |
|||
padding-top: 20px; |
|||
|
|||
.navi-bar-box { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: space-between; |
|||
|
|||
.botoom { |
|||
margin-bottom: 30px; |
|||
} |
|||
} |
|||
|
|||
.user-head-image { |
|||
display: flex; |
|||
justify-content: center; |
|||
} |
|||
|
|||
--menu-color: #9DC4FF; |
|||
.menu { |
|||
height: 200px; |
|||
//margin-top: 10px; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-content: center; |
|||
|
|||
.link { |
|||
text-decoration: none; |
|||
} |
|||
|
|||
.router-link-active .menu-item { |
|||
color: #fff; |
|||
background: var(--im-color-primary-light-2); |
|||
} |
|||
|
|||
.el-menu { |
|||
border: none; |
|||
flex: 1; |
|||
.link:not(.router-link-active) .menu-item:hover { |
|||
color: #fff; |
|||
} |
|||
|
|||
.el-menu-item { |
|||
margin: 25px 0; |
|||
background-color: #E8F2FF !important; |
|||
padding: 0 !important; |
|||
text-align: center; |
|||
.menu-item { |
|||
position: relative; |
|||
color: var(--menu-color); |
|||
width: var(--width); |
|||
height: 46px; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 12px; |
|||
|
|||
.link { |
|||
text-decoration: none; |
|||
.icon { |
|||
font-size: var(--icon-font-size) |
|||
} |
|||
|
|||
&.router-link-active .icon { |
|||
color: #195ee2; |
|||
font-size: 28px; |
|||
} |
|||
} |
|||
.unread-text { |
|||
position: absolute; |
|||
background-color: var(--im-color-danger); |
|||
left: 28px; |
|||
top: 8px; |
|||
color: white; |
|||
border-radius: 30px; |
|||
padding: 0 5px; |
|||
font-size: 12px; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
border: 1px solid #f1e5e5; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.icon { |
|||
font-size: 26px; |
|||
color: #666; |
|||
} |
|||
.botoom-item { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
height: 50px; |
|||
width: 100%; |
|||
cursor: pointer; |
|||
color: var(--menu-color); |
|||
font-size: var(--icon-font-size); |
|||
|
|||
.unread-text { |
|||
position: absolute; |
|||
line-height: 20px; |
|||
background-color: #f56c6c; |
|||
left: 36px; |
|||
top: 7px; |
|||
color: white; |
|||
border-radius: 30px; |
|||
padding: 0 5px; |
|||
font-size: 10px; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
border: 1px solid #f1e5e5; |
|||
} |
|||
} |
|||
} |
|||
.icon { |
|||
font-size: var(--icon-font-size) |
|||
} |
|||
|
|||
.exit-box { |
|||
position: absolute; |
|||
width: 60px; |
|||
bottom: 40px; |
|||
text-align: center; |
|||
cursor: pointer; |
|||
&:hover { |
|||
font-weight: 600; |
|||
color: #fff; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.icon { |
|||
font-size: 28px; |
|||
} |
|||
.content-box { |
|||
flex: 1; |
|||
padding: 0; |
|||
background-color: #fff; |
|||
text-align: center; |
|||
} |
|||
} |
|||
|
|||
&:hover { |
|||
font-weight: 600; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.content-box { |
|||
padding: 0; |
|||
background-color: #f8f8f8; |
|||
color: black; |
|||
text-align: center; |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue