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