- 个人代码,该文章只有聊天窗口主体,其它部份的组件都比较简单就不发出来了。
- 个人觉得比较难的地方是将富文本的图片base64转成文件传给后端。
- 这个不是教学文章,只是提供一个思路,所以懒得解释那么多了。反正我自己是肯定能看懂的
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/7dae10c2763c9120f302b8a86763676d.gif)
<template>
<div class="edit_container" style="">
<div class="header">
<div class="title" style="position:relative">
<span>
<i class="iconfont iconpindaofangjian"></i>
{{quillTitle}}-聊天室
</span>
<i class="iconfont iconyoucedaohang-tongxunlu _btn-suspend biankuang"
@click="popoverMembers=true"/>
<transition name="el-zoom-in-top">>
<div class="members" style="" v-show="popoverMembers">
<div class="members-title" >
<span>
<i class="iconfont iconyoucedaohang-tongxunlu"
style="border: 0px solid rgba(242, 242, 244, 1);"></i>
房间成员
</span>
<span style="float: right">
<i class="iconfont iconguanbi _btn-suspend" @click="popoverMembers=false"></i>
</span>
</div>
<div style="height: 1px;width: 360px;background-color: #EDEDED"></div>
<div class="members-main">
<span class="members-count">
房间人数{{members.length}}
</span>
<div class="members-users _btn-suspend">
<div v-for="(medium,index) in members" :key="index"
class="members-user"
@click="onBusinessCard(medium.ID)">
<!--头像-->
<img
class="icon-size "
:src=" $store.state.business[medium.Business].icon "/>
<div :style="medium.Light?'-webkit-filter: grayscale(100%);':''">
<el-avatar
size="medium"
:src="medium.PhotoUrl"
slot="reference"/>
</div>
<span>
{{medium.Name}}
</span>
</div>
</div>
</div>
</div>
</transition>
</div>
</div>
<div class="dialog-box">
<el-scrollbar wrapStyle="overflow-x: hidden;" ref="myScrollbar">
<div v-for="(message,index) in messageList"
:key="index"
:id="'msg'+message.Id">
<div class="time">
<span v-html="message.TimeHtml"></span>
</div>
<div class="message"
:class="isMy(message.Sender.ID)?'my-message':'opposite-message'">
<!--头像-->
<span @click="onBusinessCard(message.Sender.ID)" class="_btn-suspend">
<el-avatar
size="medium"
:src="message.Sender.FullPhotoUrl"
slot="reference"/>
</span>
<div class="message-main">
<span class="username" v-if="!isMy(message.Sender.ID)">
{{message.Sender.RealName}}
<el-image :src="$store.state.business[message.Sender.Business].icon" class="_icon" fit="cover"/>
{{$store.state.business[message.Sender.Business].title}}
</span>
<!--聊天内容-->
<div v-html="message.Content" :class="message.Content.includes('img')?'imgMessage':'text'"/>
</div>
</div>
</div>
<p id="bottom"/>
</el-scrollbar>
<!--个人信息-->
<BusinessCard :id="businessCardUserId"
:visible="businessCardBisible"
@close="businessCardBisible=false"/>
</div>
<div class="inputWindow">
<div
v-loading="imageLoading"
element-loading-text="请稍等,图片上传中">
<quill-editor
v-model="content"
ref="myQuillEditor"
:options="editorOption"
@blur="onEditorBlur($event)"
@focus="onEditorFocus($event)"
@change="onEditorChange($event)">
</quill-editor>
<input style="display: none"
id="imgInput"
type="file"
name="avator"
multiple accept="image/jpg,image/jpeg,image/png,image/gif"
@change="handleUpload">
</div>
<div class="footer">
<span>Enter 发送,Ctrl+Shift 换行</span>
<button @click="saveHtml" class="event">发送</button>
</div>
</div>
</div>
</template>
<script>
import * as Quill from 'quill' //引入编辑器
import BusinessCard from './BusinessCard' //引入编辑器
import {ImageDrop} from 'quill-image-drop-module';
Quill.register('modules/imageDrop', ImageDrop);
/*['bold', 'italic', 'underline', 'strike'], //加粗,斜体,下划线,删除线
['blockquote', 'code-block'], //引用,代码块
[{ 'header': 1 }, { 'header': 2 }], // 标题,键值对的形式;1、2表示字体大小
[{ 'list': 'ordered'}, { 'list': 'bullet' }], //列表
[{ 'script': 'sub'}, { 'script': 'super' }], // 上下标
[{ 'indent': '-1'}, { 'indent': '+1' }], // 缩进
[{ 'direction': 'rtl' }], // 文本方向
[{ 'size': ['small', false, 'large', 'huge'] }], // 字体大小
[{ 'header': [1, 2, 3, 4, 5, 6, false] }], //几级标题
[{ 'color': [] }, { 'background': [] }], // 字体颜色,字体背景颜色
[{ 'font': [] }], //字体
[{ 'align': [] }], //对齐方式
['clean'], //清除字体样式
['image','video'] //上传图片、上传视频*/
export default {
name: "Quill",
components: {
BusinessCard
},
props: {
strMessageMap: {
type: String
},
id: {
type: Number
},
quillTitle: {
type: String,
default: ""
}
},
data() {
return {
content: ``,
editorOption: {
modules: {
imageDrop: false,
toolbar: [
['image']
]
},
theme: 'snow',
placeholder: '',
scrollTop: 0,
},
messageList: [],
Sender: {},
businessCardBisible: false,
businessCardUserId: 0,
userID: this.$store.state.userInfo.ID,
scrollbarEl: {},
/*时间标记*/
timeSign: 0,
addImgRange: {},
imageLoading: false,
/*聊天域对象*/
domain:{},
/*聊天成员*/
members:{},
popoverMembers:false,
}
},
watch: {
id(val) {
this.messageList = []
this.domainInit()
},
//监听的变量名发生改变后触发
messageMap(val) {
let scrollbarEl = this.scrollbarEl
this.getNewMessage().then(res => {
if (scrollbarEl.scrollHeight < (scrollbarEl.scrollTop + scrollbarEl.clientHeight) + 1080) {
this.bottomScrollIntoView()
}
})
},
},
computed: {
messageMap() {
//监听传进来的vuex变量名
return this.$store.state[this.strMessageMap]
},
editor() {
return this.$refs.myQuillEditor.quill
},
},
mounted() {
/*滚动到顶部加载数据*/
this.handleScroll()
//绑定回车键事件
this.editor.keyboard.bindings[13].splice(0, 0, {
key: 13, handler: () => {
this.saveHtml()
}
})
let _this = this
let imgHandler = async function (image) {
this.addImgRange = _this.editor.getSelection()
if (image) {
let fileInput = document.getElementById("imgInput") //隐藏的file文本ID
fileInput.click() //加一个触发事件
}
}
this.editor.getModule("toolbar").addHandler("image", imgHandler)
this.domainInit()
},
methods: {
/*平滑滚动到底部*/
bottomScrollIntoView(){
/*scrollTop为滚动条在Y轴上的滚动距离。clientHeight为内容可视区域的高度。scrollHeight为内容可视区域的高度加上溢出(滚动)的距离。*/
setTimeout(()=>{
/*平滑滚动*/
document.querySelector("#bottom").scrollIntoView({behavior:"smooth"})
},50)
},
/*初始化domain*/
domainInit(){
let r = (res)=>{
this.domain=res
/*获取房间人数*/
this.$IChatProxy.GetChatMembers(res.ID,res=>{
this.members=res
console.log('成员')
console.log(res.members)
})
this.getMessageArray()
}
if (this.strMessageMap === 'houseMessageMap') {
this.$IChatProxy.GetChatDomainByHouse(this.id,res=>r(res))
} else if (this.strMessageMap === 'roomMessageMap') {
this.$IChatProxy.GetChatDomainByRoom(this.id,res=>r(res))
} else if (this.strMessageMap === 'channelIDMessageMap') {
this.$IChatProxy.GetChatDomainByChannel(this.id,res=>r(res))
} else if (this.strMessageMap === 'userMessageMap') {
this.$IChatProxy.GetChatDomainByUser(this.id,res=>r(res))
}
},
handleUpload(e) {
let files = e.target.files;
if (files.length == 0) return;
let pic_list = []
Object.keys(files).forEach(key => {
let isLt20M = files[key].size / 1024 / 1024 < 20;
if (!isLt20M) {
this.$message.warning('上传图片大小不能超过 20M!');
return;
}
pic_list.push(files[key])
});
this.imageLoading = true
try {
pic_list.forEach(file => {
this.uploadImgReq(file).then(res=>{
/*做图文混编时使用*/
/*let url = res.data
this.addImgRange = this.editor.getSelection()
url = url.indexOf('http') != -1 ? url : 'http:' + url
this.editor.insertEmbed(this.addImgRange != null ? this.addImgRange.index : 0,
'image',
url,
Quill.sources.USER)*/
})
})
} catch (e) {
this.$message.warning("图片上传失败")
}
this.imageLoading = false
},
uploadImgReq(file) {
// 这里实现你自己的图片上传
return new Promise((resolve, reject) => {
if (true) {
//创建form对象
let param = new FormData();
//通过append向form对象添加数据
param.append('chatDomainID',this.domain.ID);
param.append('image',file);
let config = {
headers:{
'AccessToken': localStorage.getItem('token')
}
}
this.$axios.post(`${this.$IChatProxy.url}31/SendImageMessage`,param,config).then(res=>{
resolve(res)
})
} else {
reject({message: '图片上传失败'})
}
})
},
showText(timeString) {
let date = new Date(timeString)
let hh = date.getHours()
let mm = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
let str = timeString;
let today = new Date()
today.setHours(0)
today.setMinutes(0)
today.setSeconds(0)
// today 为今天凌晨的时间
let dayTime = 24 * 60 * 60 * 1000
let delta = today - date // 得到相差的时间 ms
if (delta > 0) {
if (delta <= dayTime) {
str = '昨天 ' + hh + ':' + mm
} else if (delta <= 2 * dayTime) {
str = '前天 ' + hh + ':' + mm
}
} else if (-delta < dayTime) {
str = hh + ':' + mm
}
if (this.timeSign == 0 || (date.getMinutes() - this.timeSign > 5 || date.getMinutes() - this.timeSign < 0)) {
this.timeSign = date.getMinutes()
return str
} else {
return ''
}
},
/*滚动到顶部加载新数据*/
handleScroll() {
this.scrollbarEl = this.$refs.myScrollbar.wrap
this.scrollbarEl.onscroll = () => {
if (this.scrollbarEl.scrollTop == 0 && this.messageList.length > 0) {
this.getMessageArray(this.messageList[0].Id)
}
}
},
/*打开个人信息*/
onBusinessCard(id) {
this.businessCardBisible = !this.businessCardBisible
this.businessCardUserId = id
},
/*判断是否为自己发出的数据*/
isMy(id) {
return id == this.userID
},
/*获取消息*/
getMessageArray(startId = 0) {
let addmessage = (res) => {
if (res.length == 0) return //this.$message.warning('已经没有历史消息了')
this.messageList.push()
let data = this.megProcessing(res)
this.messageList.splice(0, 0, ...data)
this.$nextTick(() => {
if(startId){
document.querySelector("#msg" + startId).scrollIntoView(true)
}else{
document.querySelector("#bottom").scrollIntoView(true)
// this.bottomScrollIntoView()
}
})
}
this.$IChatProxy.GetChatWithDomain(this.domain.ID, startId, res => {
addmessage(res)
})
},
/*获取新消息*/
getNewMessage() {
return new Promise(resolve => {
this.$IChatProxy.GetChatWithDomain(this.domain.ID, -1, res => {
let data = this.megProcessing(res)
this.messageList.push(...data)
this.$nextTick(() => {
resolve(res)
})
})
})
},
/*数据处理*/
megProcessing(res) {
for (let message of res) {
message.Content = decodeURIComponent(message.Content)
message.TimeHtml = this.showText(message.Time)
}
return res
},
onEditorReady(editor) { // 准备编辑器
},
// 失去焦点事件
onEditorBlur() {
},
// 获得焦点事件
onEditorFocus() {
},
// 内容改变事件
onEditorChange(val) {
// console.log(val); // 富文本获得焦点时的内容
//editor.enable(false); // 在获取焦点的时候禁用
},
saveHtml: function (event) {
if (this.content.length <= 0) return
this.content = encodeURIComponent(this.content)
this.$IChatProxy.SendTextMessage(this.domain.ID,this.content,res=>{
})
this.content = ""
}
}
}
</script>
<style scoped lang="scss">
.el-popup-parent--hidden /deep/ .quill-popover {
background-color: #2c3e50;
}
.dialog-box /deep/ .el-scrollbar {
background: #F3F3F7;
border:1px solid #EEEDF0;
box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
height: 100%;
width: 100%;
}
.el-scrollbar-main /deep/ .el-scrollbar__wrap {
width: 110%;
height: 110%;
}
.edit_container {
height: auto;
width: 100%;
text-align: left;
.header {
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(237, 237, 237, 1);
box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
border-radius: 6px 6px 0px 0px;
display: flex;
align-items: center;
padding: 0 10px;
height: 52px;
.title {
width: 100%;
display: flex;
justify-content: space-between;
font-weight: bold;
color: $color-text;
line-height: 14px;
.iconpindaofangjian:before {
color: $color-primary;
}
.iconyoucedaohang-tongxunlu:before {
font-size: 22px;
padding: 4px;
color: $color-primary;
border-radius: 50%;
}
.biankuang:before{
border: 1px solid rgba(242, 242, 244, 1);
}
.iconguanbi:before{
border-radius: 50%;
font-size: 22px;
padding: 4px;
border: 1px solid rgba(242, 242, 244, 1);
color: $color-shadow;
}
}
.members{
border-radius:6px;
box-shadow:0px 2px 8px 0px rgba(0, 0, 0, 0.06);
border:1px solid rgba(237, 237, 237, 1);
position:absolute;
z-index: 100;
right: -10px;
top: -18px;
background-color: $color-popup;
display: flex;
width: 360px;
height: 480px;
flex-direction:column;
.members-title{
line-height:25px;
padding: 11px 10px 12px 10px;
}
.members-main{
padding: 20px 10px;
.members-count{
color: $color-text;
font-weight:400;
}
/*用户组*/
.members-users{
display: flex;
flex-wrap:wrap;
/*用户*/
.members-user{
width: 48px;
padding: 10px 10px;
font-size: 12px;
text-align: center;
position: relative;
.icon-size {
position: absolute;
width:15px;
height:15px;
right: 12px;
top: 5px;
}
}
}
}
div{
line-height:15px;
}
}
.btn-users {
width: 36px;
height: 36px;
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(242, 242, 244, 1);
border-radius: 50%;
}
}
/*对话窗口*/
.dialog-box {
height: 450px;
width: 100%;
._icon {
width: 15px;
margin-left: 5px;
}
/*其他人的消息*/
.opposite-message {
display: flex;
flex-direction: row;
/*信息主体*/
.message-main {
text-align: left;
}
.text {
background: rgba(255, 255, 255, 1);
border-radius: 2px 10px 10px 10px;
font-weight: 400;
color: rgba(20, 35, 56, 1);
}
}
.time {
display: flex;
justify-content: center;
color: $color-shadow;
}
/*我的消息*/
.my-message {
display: flex;
flex-direction: row-reverse;
/*信息主体*/
.message-main {
text-align: right;
}
.username {
justify-content: flex-end;
}
.text {
border-radius: 10px 2px 10px 10px;
background: rgba(1, 123, 255, 1);
font-weight: 400;
color: rgba(242, 242, 244, 1);
}
}
.message {
padding: 10px 20px;
.message-main {
.username {
display: flex;
align-items: center;
margin: 0 13px 5px 13px;
font-size: 12px;
color: $color-font;
}
}
/deep/ p {
margin: 0;
line-height: 1;
}
.imgMessage{
/deep/ img {
margin: 5px 10px;
max-width: 40%;
border-radius: 10px;
}
}
.text {
display:inline-block !important;
margin: 0 13px;
/*max-width: 80%;*/
padding: 12px 15px;
}
}
}
/*输入窗口*/
.inputWindow {
height: 170px;
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(237, 237, 237, 1);
box-shadow: 0px -1px 0px 0px rgba(237, 237, 237, 1);
border-radius: 0px 0px 6px 6px;
/*富文本工具栏*/
/deep/ .ql-toolbar {
padding: 2px;
height: 30px;
/deep/ .ql-formats {
display: flex;
}
}
/*输入框*/
/deep/ .ql-container {
height: 100px;
/deep/ img {
height: 80px;
}
}
/deep/ .ql-snow {
border: 0px solid #ccc;
}
/deep/ .ql-editor {
padding: 5px;
}
}
/*底部*/
.footer {
display: flex;
justify-content: flex-end;
align-items: center;
.event {
width: 75px;
height: 32px;
background: rgba(255, 255, 255, 1);
border: 1px solid rgba(237, 237, 237, 1);
border-radius: 6px;
margin-right: 20px;
}
span {
font-size: 12px;
font-family: Microsoft YaHei;
font-weight: 400;
color: rgba(171, 172, 176, 1);
line-height: 2px;
}
}
/*汉化*/
.editor {
line-height: normal !important;
height: 800px;
}
.ql-snow .ql-tooltip[data-mode=link]::before {
content: "请输入链接地址:";
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: '保存';
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode=video]::before {
content: "请输入视频地址:";
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before {
content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before {
content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before {
content: '32px';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
content: '标题6';
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before {
content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before {
content: '等宽字体';
}
.ql-align-center {
text-align: center;
}
.ql-align-right {
text-align: right;
}
.ql-align-left {
text-align: left;
}
}
</style>