背景
最近遇到的实际业务需求:实现评论内容并且可以在聊天框中@人员,发表评论后实时推送消息给相关@人员钉钉消息。当然这篇文章的重点不是如何实现实时推送钉钉消息,重点在于实现满足这样需求的聊天框,并且整理好数据传输给后端同学
思考
- 【思考1】:下意识第一个想法是要不监听键盘事件,通过$event 可以获取键盘的keyCode,然后拉起人员款,选人之后追加到内容区...,但这样会出现几个问题
1. 怎么将搜索内容盒子定位到当前光标位置呢
2. 假如【问题1】解决了,删除@人又该怎么去实时监听呢,最终是要传输数据的,怎么整合数据呢
复制代码
- 【思考2】:通过富文本的形式,往富文本里追加可编辑标签,这样@XX就是一个整体标签了,比【思考1】是要简单许多(有兴趣的可以看看这篇文章),但实践一番之后我发现几个问题
1. @XX之后输入框不会自动聚焦
2. 文章采用的是抽屉选人的交互,个人不是很喜欢
复制代码
- 【思考3】:实现思路跟【思路2】类似,实现方式不同而已。也解决了【思路2】实践之后出现的几个问题,接下来实践一下吧
准备工作
-
quill: 为兼容性和可扩展性而构建的现代富文本编辑器
-
quill-mention: 一个为Quill富文本编辑器提供@mentions或#标签功能的模块
// 需要先安装依赖
npm i quill
npm i quill-mention
复制代码
<template>
<div ref="editor"></div>
<el-button type="primary" style="margin-top: 20px" @click="sendComment">
发布
</el-button>
</template>
<script>
import { ElMessage } from "element-plus";
import Quill from "quill";
import mention from "quill-mention"; // 引入mention 组件
import "quill-mention/dist/quill.mention.min.css";
export default {
mounted() {
// 官网demo使用的是id(不建议用id),因为容易重复-采用ref
new Quill(this.$refs.editor, {
placeholder: "输入讨论内容,可通过@提醒指定人员",
modules: {
mention: {
// allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/, // 匹配字母搜索
allowedChars: /^[\u4e00-\u9fa5]*$/, //匹配中文搜索
mentionDenotationChars: ["@"], // 通过@拉起人员框
positioningStrategy: "fixed",
// 人员框渲染内容
renderItem: (data) => {
return `
<div class="member-item">
<image src=${data.avatar} class="member-avator"/>
<span class="member-name">${data.value}</span>
</div>
`;
},
// 加载人员框loading
renderLoading: () => {
return "Loading...";
},
source: (searchTerm, renderList, mentionChar) => {
// ... any code
},
},
},
});
},
};
</script>
复制代码
先初始化聊天框,这里用的是@字符去识别的,只能用中文去进行模糊搜索人员,可以根据自己的实际需求修改正则去匹配
准备数据
demo用的是mockjs ,返回的数据格式如下
export const memberList = [
{
id: "10000",
value: "胡歌",
avatar:
"https://tvax1.sinaimg.cn/crop.0.0.1080.1080.180/001kMkx0ly8h15lia0t74j60u00u0mxy02.jpg?KID=imgbed,tva&Expires=1655289369&ssig=C32T5vwyty",
},
{
id: "10001",
value: "林更新",
avatar:
"https://tvax3.sinaimg.cn/crop.0.0.1080.1080.180/6728caedly8ggwo83jfncj20u00u0q4m.jpg?KID=imgbed,tva&Expires=1655289370&ssig=BXH5%2BoE%2FkD",
},
{
id: "10002",
value: "陈坤",
avatar:
"https://tvax1.sinaimg.cn/crop.0.0.1080.1080.180/40d61044ly8gbhxwgy419j20u00u0goc.jpg?KID=imgbed,tva&Expires=1655289370&ssig=9gvaxkXGae",
},
{
id: "10003",
value: "迪丽热巴",
avatar:
"https://tvax1.sinaimg.cn/crop.0.0.1080.1080.180/63885668ly8geyrcrw0zjj20u00u0mz6.jpg?KID=imgbed,tva&Expires=1655289675&ssig=0iHAeIqsdq",
},
{
id: "10004",
value: "杨幂",
avatar:
"https://tvax2.sinaimg.cn/crop.0.0.512.512.180/001iT7gJly8gn5l4rr9gdj60e80e8aaf02.jpg?KID=imgbed,tva&Expires=1655289706&ssig=5KfZA8%2B8fl",
},
{
id: "10005",
value: "白敬亭",
avatar:
"https://tvax2.sinaimg.cn/crop.0.0.996.996.180/7dea235bly8gdj2nhclzuj20ro0rowgq.jpg?KID=imgbed,tva&Expires=1655289706&ssig=5LqL4jU0oh",
},
{
id: "10006",
value: "周杰伦",
avatar:
"https://tvax1.sinaimg.cn/crop.0.0.512.512.180/6e48db9ely8gyioyombb8j20e80e8jrs.jpg?KID=imgbed,tva&Expires=1655289786&ssig=ofoG%2FTZ5GM",
},
{
id: "10007",
value: "杨超越",
avatar:
"https://tvax2.sinaimg.cn/crop.0.0.512.512.180/006a0Rdhly8h2jkkpqymzj30e80e80tl.jpg?KID=imgbed,tva&Expires=1655289859&ssig=DtD341ZC6q",
},
];
复制代码
填充数据【重点】
// ... any code
source: (searchTerm, renderList, mentionChar) => {
// 第一种方式:调取接口(实际场景)
// this.suggestPeople(searchTerm).then((res) => {
// renderList(res.rows);
// });
// 第二种方式:静态数据(方便调试)
const cloneMemberList = deepClone(memberList).filter(
(item) => item.value.indexOf(searchTerm) !== -1
);
// renderList 方法,将数据填充到选人框中
renderList(cloneMemberList);
},
复制代码
上述代码中source(searchTerm, renderList, mentionChar)方法,三个参数代表什么意思呢:
- searchTerm:指的是@字符之后输入的内容,比例@李四,指的李四,而且是逐字监听
- renderList:填充数据方法
- mentionChar: 拉起人员框字符,这里是’@‘
通过source方法,可以初始化人员框list,也实现了模糊搜索list,到这里是不是感觉成功一半了,接着干
获取@人员数据
methods: {
// 获取评论区@XX人员
/*
最终想得到的数据接口是
[{userId:1000},{userId:1001},....]
*/
getAts() {
let domStr = this.$refs.editor.outerHTML;
let patt = /<span[^>]+data-id=['"]([^'"]+)['"]+/g;
let result = [],
temp;
while ((temp = patt.exec(domStr)) != null) {
result.push({ userId: temp[1] });
}
return result;
},
}
复制代码
是不是感觉离成功又近了一步呢
发布聊天内容
先看看真正输入框内容到底长什么样
返回的数据实际通过v-html展示是会有问题的,需要把一些类名和属性屏蔽了才能正常显示,如果直接把这个丢给后端,肯定是不行的,需要处理。具体处理如下:
methods: {
// ...any code
// 发布评论
sendComment() {
const atUsers = this.getAts();
const content = this.$refs.editor.innerHTML
.replace("ql-editor", "")
.replace("contenteditable", "a")
.replace(regHttp, function (item) {
// 这里识别了链接
return "<a href='" + item + "' target='_blank'>" + item + "</a>";
});
const contentText = this.getContentText();
if (contentText == "\n") {
ElMessage.error("请输入讨论内容");
return;
}
// 清空内容区文本
this.clear();
},
// 获取输入框的文本内容
getContentText() {
const text = this.$refs.editor.innerText;
return text;
},
// 清除输入框内容
clear() {
const element = document.getElementsByClassName("ql-editor")[0];
element.textContent = "";
},
}
复制代码
tips: 因为输入框内容其实是富文本的格式,需要把一些样式都屏蔽了,并且识别链接,否则显示的评论内容会带上一些样式(ps:小伙伴可以试试,看看效果).
最终处理过想要的数据格式如下: