查看原文
其他

利用 Vue 实现评论板块:发表情,@某人消息推送

前端大全
2024-08-23

推荐关注↓

作者: 糖墨夕

https://juejin.cn/post/7200689071261106233

简介

在接到这个需求的时候,就纳闷,为啥要搞这么多花样,评论就评论吧,它还要你实现艾特某人的时候,还要调用后台的IM接口,给相关人员发送通知推送;说到这里,有点经验的JY应该就想到了,数据参数如何组装和传递这是个关键点,后面再细说。

评论区主要实现的功能点有:表情包选择,艾特符识别并弹出人员选择,还有就是图片选择(篇幅有限,这个放在第二篇文章述说),还有就是支持表情包,艾特人,文本组合显示的文本区域(这个是难点)

组合显示的文本区域,除了能正常显示三种元素节点,还需要将其关联的数据包裹起来,方便在点击发送的时候,将这些数据提取出来进行组装,然后发送给后台;

刚开始,毫无头绪,不知从何下手的时候,想到的就是去寻找一劳永逸的插件,然后发现,比较适合做这方面的插件,无非就是富文本编辑器吧,比如最常用的富文本编辑器Vue-Quill-Editor[1] ,但是研究了一番这开源插件的文档发现,开发的API并不能满足我实际的需求,比如艾特人的情况下数据存储,还有改写后光标的显示和跟踪问题,,😮‍💨唉,一番折腾之后又陷入了苦思当中;但问题总归是要解决的,于是我开始萌生了不再依赖插件的想法;经过我一番的努力思索,还真给我整出来了,废话不多说,先上图:

接下来我会从以下几个方面分享我的开发思路喝实现方案

  1. 原型描述
  2. 思路分析
  • 表情模块实现
  • 艾特符模块实现
  • 数据交互模块实现
  • 代码实现
  • 原型需求

    具体的需求是这样子的,请看下方描述:

    A.鼠标点击输入框即可开始输入内容

    B.当在输入的过程发送了@消息时,被@的人会通过IM收到被@的消息。

    1. 输入@后弹出人员选择框,这里的人员包括“所有人”、“创建人”、“负责人”、“子任务负责人”、“参与人”。排序顺序按照此顺序进行展示。
    2. 排列在第一个的为“所有人”这里的所有人不包括自己。
    3. 下面的其他人员中同样不会显示出自己。
    4. 勾选人后,被勾选的人会显示到输入框中。
    5. 注意:如果@了所有人,又@了单个人,此时发送出去的消息针对个人只会发送一次。

    C.通过@发送出去的消息除了会在当前页面进行展示外,还会通过IM将消息发送给对方:

    1. @了几个人就会把消息单独发给那几个人。如果有相同的需要去重,只会发送一次。
    2. 如果@了所有人,那么就会单独发送给所有人(不包含自己)。
    3. 被@的人会在IM中收到消息。这条消息来自发起人与被@的人之间的私聊。如果聊天的内容中包含了附件,则附件不会发送到IM,只会发送本文内容和表情。做出了的效果如图:

    思路分析

    表情模块实现

    选择表情包弹层有两种方案,一种是直接图片链接展示,图片命名用中文,另一种是提交给后台的时候是要转译成【微笑】的形式进行保存,而不是图片链接

    这里涉及到表情选择后,回显到文本框中。我们所知道的textarea标签只能传进文本内容,所要将div改写成可编辑的文本框,增加contenteditable="true" 属性,通过获取div的Dom,对其进行光标定位,让表情包进行插入,再操作光标移动;可能看到这里的JY有点懵,在代码实现那里会细说。



    艾特符模块实现

    当监听到用户输入@的时候我们弹出人员选择器,这时候我们需要记住现在光标所在的位置,当用户选择人员完毕之后,我们创建一个Dom在插入到我们刚刚记录光标的位置,并且把我们输入的@删除,将光标放在这个节点的最后;似乎过程描述得还蛮简单的,但不过实现起来还是有点难度;

    一、使用到的JavaScript对象:

    1.Range Range对象表示的包含节点或者文本节点一段片段
    2.Selection Selection对象表示用户的光标开始位置到结束位置的选区
    (以上是我的个人理解,具体的需要到MDN上查阅)

    二、实现的原理

    1. 首先通过const selection = Window.getSelection(),
    2. const range = selection.getrangeAt(0)获取光标的位置
    3. 然后监听键盘事件,阻止输入@的默认行为,并且创建一个SPAN标签,内容为@,然后到光标处;
    4. 创建两个新的span标签,把@+选中的内容让放到其中一个新建的span标签中,另外一个span标签插入空格
    5. 创建一个fragment片段,把第四步中两个span标签一次插入fragment中
    6. 最后使用Range对象中的insertNode()方法插入富文本中
    7. 第6步完成之后,找到第4步创建并带有@的SPAN节点,然后移除
    8. 删除时,首先找到包含@+内容的节点,然后把整个节点一起删除

    数据交互模块实现

    比如我要评论这样一条信息

    那么我们需要怎样存储后端接口需要到的参数格式呢,比如IM推送,需要知道你艾特的是谁,还有表情包,到底要的是链接,还是标识符,这些需要分析如何存储和传递的, 而我这个项目的接口需要的数据格式如下:


    • 其中imText是用于在IM聊天窗口中,发送消息的数据格式,
    • text部分是直接用于评论任务记录的显示,
    • users是用于存储艾特相关人员的用户id存储

    显而易见,我们需要做的就是将users—id存储到艾特符元素里面,imText需要我们从内容框中得到的html,再做个正则匹配将其内容转换和替换成我们需要的数据格式;至于如何实现,待会在代码实现栏上详说

    代码实现

    表情包评论代码解析

    选中表情

    appendEmoji(imgSrc) {
      // 拿到dom获取光标
      const editor = this.$refs.jsEditorElement;
      if (editor) {
        this.isFocus()
        console.log(editor.focus);
        this.selectEmoji(imgSrc);
        console.log('onChangeJsEditor')
        this.onChangeJsEditor('emoji')
      }
      this.visibleEmoj = false;
    }

    将文本框的光标位置移动到添加表情后的位置

     selectEmoji(url) {
      const editor = this.$refs.jsEditorElement;
      if (editor) {
        editor.focus();
        // this.editorRange.selection.collapseToEnd();
        // 删掉草稿start
        const editorRange = this.editorRange.range;
        console.log(
          "editorRange",
          editorRange,
          editorRange.startOffset,
          editorRange.endOffset
        );
        if (!editorRange) {
          return;
        }
        const textNode = editorRange.endContainer; // 拿到末尾文本节点
        const endOffset = editorRange.endOffset; // 光标位置
        // 找出光标前的at符号位置
        // const textNodeValue = textNode.nodeValue
        // const expRes = (/@([^@]*)$/).exec(textNodeValue)
        // if (expRes && expRes.length > 1) {
        // editorRange.setStart(textNode, expRes.index)
        editorRange.setEnd(textNode, endOffset);
        editorRange.deleteContents(); // 删除草稿end
        const dom = this.createInsterImgData(url);
        console.log(dom);
        console.log(this.editorRange.selection);
        console.log(this.editorRange.range);
        this.insertHtmlImgAtCaret(
          dom,
          this.editorRange.selection,
          this.editorRange.range
        );
        // }
      }
    },

    将表情包以加装后img标签的形式累加到文本显示

    createInsterImgData(url) {
      const btn = document.createElement("img");
      btn.setAttribute("src", url);
      btn.setAttribute("class""emo");
      btn.setAttribute("style""width: 26px;height:26px;");
      return btn;
    },

    @艾特某人代码解析

    一、选择@按钮事件

    可以阅读源码中的selectPerson()方法,这里主要说下,保存@某人信息的问题

    给每一个选择的人员,构造一个Dom,设置为a标签元素,然后将用户信息存放在dataset里面,并设置一个classs属性(用于提交的时候通过正则匹配出来提取用户信息), 如下:

    this.insertCaret(
      `<a data-id="${some.member_id}" data-mid="${some.im_user_id}" class="userSetClass" 
      data-name="${some.member_name}" contenteditable="false">@${some.member_name}</a>&nbsp;`

    );
    createInsterData(personArr) {
      const temp = [];
      for (const person of personArr) {
        const btn = document.createElement("a");
        btn.dataset.id = person.member_id;
        btn.dataset.mid = person.im_user_id;
        btn.dataset.name = person.member_name || person.name;
        btn.contentEditable = false;
        btn.setAttribute("href""javascript:void(0)");
        if(this.allowSelectMembers.length === personArr.length) {
          btn.textContent = ` &${person.member_name} `;
          btn.setAttribute('style''display:none')
          btn.setAttribute("class""userHiddenSetClass");
        } else {
          btn.textContent = ` @${person.member_name} `;
          btn.setAttribute("class""userSetClass");
        }
        btn.addEventListener(
          "click",
          () => {
            return false;
          },
          false
        );
        btn.tabindex = "-1";
        const bSpaceNode = document.createTextNode("\u200B"); // 不可见字符,为了放光标方便
        temp.push(btn);
        temp.push(bSpaceNode);
      }
      // 将所有添加进去@所有人
      if(this.allowSelectMembers.length === personArr.length) {
        const btn = document.createElement("a");

        btn.type = "link";
        btn.textContent = ` @所有人 `;
        btn.contentEditable = false;
        btn.setAttribute("class""userSetAllClass");
        btn.setAttribute("href""javascript:void(0)");
        btn.tabindex = "-1";
        const bSpaceNode = document.createTextNode("\u200B"); // 不可见字符,为了放光标方便
        temp.push(btn);
        temp.push(bSpaceNode);
      }
      return temp;
    },

    二、输入@符号监听

    这里要写个监听函数,当用户按 shift + @ 的时候会被检索到,然后执行获取光标事件,并同时更新人员弹层选择列表getAllNewMembers(),

    onInputText(e) {
      this.onChangeJsEditor(e.target.innerHTML)
      console.log(e.target.innerHTML)
      // 这是输入了@,那就直接弹选人浮层
      this.doToggleDialog();
      console.log(this.editorRange);
      if (e.code === "Digit2" && e.shiftKey) {
        this.mockInput = false;
        console.log("输入@");
        // 获取新的参与人alt列表
        this.getAllNewMembers()
      }
    },

    三、发布评论前的数据转换

    刚才在前面两个模块做的数据组装就是为了最后一步,发表评论的数据提取和传递问题

    首先提取用户信息,将其存放在一个数组中

    let collect = editor.getElementsByClassName("userSetClass");
    console.log(collect, Array.from(collect).length);
    for (const child of collect) {
      atidsss.push(child.dataset.id);
      atnames.push(child.dataset.name);
      atmid.push(child.dataset.mid);
    }
    // @所有人
    let AltAll = editor.getElementsByClassName("userSetAllClass")
    // if(AltAll.)
    if(AltAll && Array.from(AltAll).length > 0) {
      let userHiddenSetClass = editor.getElementsByClassName("userHiddenSetClass")
      for (const child of userHiddenSetClass) {
        atidsss.push(child.dataset.id);
        atnames.push(child.dataset.name);
        atmid.push(child.dataset.mid);
      }
    }

    其次,将带有img标签的html文本,通过正则表达式将其提取出图片名称,用于IM的消息推送格式

    params.imText = params.imText.replace(
      /<img [^>]*src=['"]([^'"]+)[^>]*>/gi,
      function (match, capture) {
        console.log(capture);
        let name = that.getLastFileName(capture)
        if(name) {
          return `[${name}]`
        }
        return ''
      }
    );
    复制代码

    最后就是整理数据,将后端定义好的数据格式传递给他们

    let params = {
      task_idthis.activeTaskItemDetailID,
      textthis.$refs.jsEditorElement.innerHTML,
      imTextthis.$refs.jsEditorElement.innerHTML,
      imagesthis.imgList,
      users: atidsss ? atidsss.filter((v) => v) : [],
      attachmentthis.fileList,
    };

    完结 

    如果您还有力气,麻烦点个小指头,支持下感谢!

    参考资料

    [1]

    Vue-Quill-Editor: https://kang-bing-kui.gitbook.io/quill/wen-dang-document/delta

    - EOF -


    加主页君微信,不仅前端技能+1

    主页君日常还会在个人微信分享前端开发学习资源技术文章精选,不定期分享一些有意思的活动岗位内推以及如何用技术做业余项目

    加个微信,打开一扇窗



    推荐阅读  点击标题可跳转

    1、了解 Vue3 的响应式利器,让你开发效率大大提升

    2、细说 Vue 响应式原理的 10 个细节!

    3、在调用 createApp 时,Vue 为我们做了那些工作?


    觉得本文对你有帮助?请分享给更多人

    推荐关注「前端大全」,提升前端技能

    点赞和在看就是最大的支持❤️

    继续滑动看下一个
    前端大全
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存