实现@Mention 提及聊天框

背景

最近遇到的实际业务需求:实现评论内容并且可以在聊天框中@人员,发表评论后实时推送消息给相关@人员钉钉消息。当然这篇文章的重点不是如何实现实时推送钉钉消息,重点在于实现满足这样需求的聊天框,并且整理好数据传输给后端同学

思考

  • 【思考1】:下意识第一个想法是要不监听键盘事件,通过$event 可以获取键盘的keyCode,然后拉起人员款,选人之后追加到内容区...,但这样会出现几个问题
1. 怎么将搜索内容盒子定位到当前光标位置呢
2. 假如【问题1】解决了,删除@人又该怎么去实时监听呢,最终是要传输数据的,怎么整合数据呢
复制代码
  • 【思考2】:通过富文本的形式,往富文本里追加可编辑标签,这样@XX就是一个整体标签了,比【思考1】是要简单许多(有兴趣的可以看看这篇文章),但实践一番之后我发现几个问题
1. @XX之后输入框不会自动聚焦
2. 文章采用的是抽屉选人的交互,个人不是很喜欢
复制代码
  • 【思考3】:实现思路跟【思路2】类似,实现方式不同而已。也解决了【思路2】实践之后出现的几个问题,接下来实践一下吧

准备工作

  1. quill: 为兼容性和可扩展性而构建的现代富文本编辑器

  2. 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)方法,三个参数代表什么意思呢:

  1. searchTerm:指的是@字符之后输入的内容,比例@李四,指的李四,而且是逐字监听
  2. renderList:填充数据方法
  3. 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;
    },
}
复制代码

是不是感觉离成功又近了一步呢

发布聊天内容

先看看真正输入框内容到底长什么样

image.png

返回的数据实际通过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:小伙伴可以试试,看看效果).

最终处理过想要的数据格式如下:

image.png

效果展示

lv_0_20220616100335_.gif

源码

awesome-vue-at

参考内容

quill quill-mention

猜你喜欢

转载自juejin.im/post/7109756910772961317