VUE配合textarea实现艾特@功能

背景

为了实现流程中的提醒功能,实现类似于聊天中的艾特功能,在此做一个VUE+textarea标签实现艾特功能的分享。

基本要求:

  1. 当删除’@xxx’中其中的任意一个字符或字符串的时候 ‘@xxx’ 这段字符串均被删除

  2. ‘@xxx’字符串中不得穿插其他内容 即当用户光标在’@xxx’中输入的时候,是无效的

  3. 艾特的内容成聊天记录形式

实现思路

  1. 首先绑定textarea v-model=“text”
  2. 标记@出现的地方 这里我采用’ @ ' 标记选人开始的地方,选人完成后 @xxx 最后一个 x 标记为 ' x ’ ,那么根据开始和`结束则标记了 一个完成的@xxxx 字段,这里我维护为一个数组
  3. 删除的时候
    普通删除 :即删除非@xxx的字符或字符串
    删除@xxx当中的字符或字符串
    关键是对比 看删除的内容是什么 从哪里开始 删除的长度是多少
  4. 正是因为每次操作是单一连续的(即每次的操作均在某处插入或删除而非多处,这样就只需要维护两个变量,插入的位置和数量),所以才有可能完成此需求

下面直接上代码

html部分

<template>
  <div class="content">
    <el-dialog
      title="备注"
      :visible.sync="dialog"
      append-to-body
      @close="handcloseDialog()"
      width="600px"
      class="initDialog"
      :close-on-click-modal="false"
    >
      <div id="app" class="app">
        <div class="btncontainer">
          <textarea
            class="editor"
            id="textarea"
            ref="textarea"
            @keydown="handleKeyDown"
            v-model="text"
            @input.prevent="handleInput"
            @focus="handleFocus"
            @mouseup="handleMouseUp"
        ></textarea>
          <div class="board" v-show="showboard">
            <div
              v-for="(item, index) in list"
              @click="handleItemClick(index,item)"
              :key="index"
            >
              {{ item.approvalManName }}
            </div>
          </div>
        </div>
        <div class="addBtn" @click="getAddRemarks()">添加</div>
      </div>

      <div class="remarksList" v-if="dataSource.length>0">
        <div class="remarksBox" v-for="(item, i) in dataSource" :key="i">
          <div class="remarkContent">{{ item.content }}</div>
          <div class="line"></div>
          <div class="remarkTips">
            <span>{{ item.createUserName }}</span>
            <div>{{ item.createTime }}</div>
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

JS部分,记得声明变量,业务代码自动过滤(当做没看见,懒人懒得删)

重点,需要在mounted周期中进行初始化

  mounted() {
    this.id = this.domainId;
    this.firstAuditInfo = this.firstAuditInfoData
    this.dialog = this.dialogVisible
    window.vm0 = this;
    this.watchText();

    if(this.domainType&&this.firstAuditInfo) {
      this.getAddPeopleData()
      this.loadRemoteList()
    }
  },

函数部分,业务代码请自动过滤

methods: {
    getAddRemarks() {
      const parameter = {
        auditType: this.firstAuditInfo.auditType,
        content: this.text,
        domainId: this.id || "",
        domainType: this.domainType,                     
        entityName: this.firstAuditInfo.entityName,
        relationApprovalRemarksList: this.relationApprovalRemarksList,
        submitCreateTime: this.firstAuditInfo.submitCreateTime
      };
      this.loading = true;
      httpActionData(this.url.add, parameter, "POST").then((res) => {
        if (Number(res.code) === 200) {
          this.loading = false;
          this.$message(res.msg);
          this.loadRemoteList()
          this.text= ''
          EventBus.$emit("loadRemoteList",this.id,this.domainType,this.firstAuditInfo)
        } else {
          this.loading = false;
          this.$message(res.msg || "数据获取失败!");
        }
      });
    },
    getAddPeopleData() {
      const parameter = {
        auditType: this.firstAuditInfo.auditType,
        domainId: this.id || "",
        flowId: this.firstAuditInfo.flowId,
        submitCreateTime: this.firstAuditInfo.submitCreateTime,
      };
      this.loading = true;
      httpAction(this.url.peopleList, parameter, "GET").then((res) => {
        if (Number(res.code) === 200 && res.data) {
          this.loading = false;
          this.list = res.data || [];
        } else {
          this.loading = false;
          this.$message(res.msg || "数据获取失败!");
        }
      });
    },
    // 获取列表数据
    loadRemoteList() {
      const parameter = {
        domainId: this.id || "",
        domainType: this.domainType,
        auditType: this.firstAuditInfo.auditType,
        submitCreateTime: this.firstAuditInfo.submitCreateTime,
      };
      this.loading = true;
      httpAction(this.url.list, parameter, "GET").then((res) => {
        if (Number(res.code) === 200 && res.data) {
          this.loading = false;
          this.dataSource = res.data || [];
        } else {
          this.loading = false;
          this.$message(res.msg || "数据获取失败!");
        }
      });
    },
    handcloseDialog() {
      this.dialog = false
      this.text=''
      this.$emit('handcloseDialog',false)
    },
    watchText() {
      this.unwatch = this.$watch("text", function (cv, ov) {
        if (ov.length > cv.length) {
          let ovlist = [...ov];
          let cvlist = [...cv];
          let startremove = this.findDiffStart(cv, ov);
          let removestr = "";
          let difflength = ovlist.length - cvlist.length;
          for (let j = startremove; j <= startremove + difflength - 1; j++) {
            removestr += ovlist[j];
          }
          console.log("对比结果", startremove, ov, cv, removestr);

          console.log(removestr, "匹配器结果");
          let atnamelist = this.findAtNameList();
          console.log(
            "atnamelist",
            atnamelist,
            removestr,
            startremove
          );
          for (let j = 0; j < atnamelist.length; j++) {
            for (
              let k = atnamelist[j].startindex;
              k <= atnamelist[j].endindex;
              k++
            ) {
              if (k >= startremove && k <= startremove + removestr.length - 1) {
                atnamelist[j].remove = true;
              }
            }
          }
          let temp = [...ov];
          let tempstr = [...ov];
          let finalstr = "";
          let temptextlist = [...this.textlist];
          console.log("temp", temp);
          for (let j = 0; j < temp.length; j++) {
            // 拿出@xxx并标记
            for (let k = 0; k < atnamelist.length; k++) {
              if (
                atnamelist[k].remove &&
                j >= atnamelist[k].startindex &&
                j <= atnamelist[k].endindex
              ) {
                // 使用ᑒ特殊符号进行标记
                tempstr[j] = "ᑒ";
                temptextlist[j] = "ᑒ";
              }
            }
            // 拿出正常删除的并标记
            if (j >= startremove && j <= startremove + removestr.length - 1) {
              tempstr[j] = "ᑒ";
              temptextlist[j] = "ᑒ";
            }
          }
          for (let j = 0; j < tempstr.length; j++) {
            if (tempstr[j] != "ᑒ") {
              finalstr += tempstr[j];
            }
          }
          this.textlist = [];
          for (let j = 0; j < temptextlist.length; j++) {
            if (temptextlist[j] != "ᑒ") {
              this.textlist.push(temptextlist[j]);
            }
          }
          if (finalstr !== ov) {
            console.log("finalstr", finalstr);
            this.text = finalstr;
            console.log("之后的this.textlist", this.textlist);
            // 重新赋值 textlist
            this.unwatch();
            setTimeout(() => {
              this.watchText();
            });
          } else {
            // 此时校验长度
          }

          console.log(finalstr, "最终");
          this.markdisable = false;
        } else {
          if (this.markdisable) {
            this.text = ov;
            this.unwatch();
            this.watchText();
            return;
          }
          let startremove = this.findDiffForcvmoreOv(cv, ov);

          let removestr = "";
          let difflength = cv.length - ov.length;

          for (let j = startremove; j <= startremove + difflength - 1; j++) {
            removestr += cv[j];
          }
          console.log("对比结果" + removestr);
          let beforelinelist = this.textlist.slice(0, startremove);
          let endlinelist = this.textlist.slice(startremove);
          let namelist = [...removestr];
          this.textlist = [...beforelinelist, ...namelist, ...endlinelist];
        }
      });
    },
    // 当cv大于ov时不一样
    findDiffForcvmoreOv(cv, ov) {
      let shorter = ov;
      let longer = cv;
      let longerlist = [...longer];
      let shorterlist = [...shorter];
      let thestartindex = null;
      for (let j = 0; j < shorterlist.length + 1; j++) {
        let insertindex = j;
        for (let k = 0; k < longerlist.length; k++) {
          let sliced = longerlist.slice(k, k + longer.length - shorter.length);
          let begin = shorterlist.slice(0, j);
          let center = sliced;
          let end = shorterlist.slice(j);
          let finalstr = [...begin, ...center, ...end].join("");
          if (finalstr == longer) {
            return j;
          }
        }
      }
    },

    // 查找开始不同的index
    findDiffStart(cv, ov) {
      let str1 = ov;
      let str2 = cv;
      let str1list = [...str1];
      let str2list = [...str2];
      let thestartindex = null;
      for (let j = 0; j < str1list.length; j++) {
        let sliced = str1list.slice(j, j + str1.length - str2.length);
        let find = false;
        for (let k = 0; k < str2list.length; k++) {
          let beforestr = str2list.slice(0, j);
          let centerstr = sliced;
          let endstr = str2list.slice(j);
          console.log(
            [...beforestr, ...centerstr, ...endstr].join(""),
            "最终结果"
          );
          if ([...beforestr, ...centerstr, ...endstr].join("") == str1) {
            find = true;

            break;
          }
        }
        if (find) {
          thestartindex = j;
          console.log(j, "哈哈哈");
          break;
        }
      }
      return thestartindex;
    },
    setCaret() {
      var el = document.getElementById("editable");
      var range = document.createRange();
      var sel = window.getSelection();
      console.log(el.childNodes);
      range.setStart(el.childNodes[2], 1);
      range.collapse(true);

      sel.removeAllRanges();
      sel.addRange(range);
    },
    // 监听方向键
    handleKeyDown(e) {
      if (e.keyCode >= 37 && e.keyCode <= 40) {
        // alert(4)
        setTimeout(() => {
          console.log("位置", e.keyCode, e.target.selectionStart);
          let index = e.target.selectionStart - 1;
          console.log(index);
          let atgroup = this.findAtNameList();
          let disabled = false;
          for (let j = 0; j < atgroup.length; j++) {
            if (
              index >= atgroup[j].startindex &&
              index < atgroup[j].endindex &&
              index != this.text.length - 1
            ) {
              // e.target.selectionStart = atgroup[j].endindex
              // e.target.disabled = true
              disabled = true;
              break;
            }
          }
          this.markdisable = disabled;
        }, 5);
      }
    },
    // 处理鼠标左键按下
    handleMouseUp(e) {
      let index = e.target.selectionStart - 1;
      console.log(index);
      let atgroup = this.findAtNameList();
      let disabled = false;
      for (let j = 0; j < atgroup.length; j++) {
        if (
          index >= atgroup[j].startindex &&
          index < atgroup[j].endindex &&
          index != this.text.length - 1
        ) {
          // e.target.selectionStart = atgroup[j].endindex
          // e.target.disabled = true
          disabled = true;
          break;
        }
      }
      this.markdisable = disabled;
      e.stopPropagation();
      e.preventDefault();
    },
    handleFocus(e) {
      if (this.markselectpeople) {
        // 表明用户未选择内容
        this.textlist.splice(this.operationindex - 1, 0, "@");
        console.log("聚焦后的textlist", this.textlist);
        this.showboard = false;
        this.watchText();
        this.markselectpeople = false;
      } else {
        // 聚焦到非@xxxx的地方
        console.log(e.target.selectionStart, e.target.selectionEnd);
      }
      e.stopPropagation();
      e.preventDefault();
    },
    handleInput(e) {
      if (e.data == "@") {
        if (this.markdisable) {
          return;
        }
        this.showboard = true;
        this.markselectpeople = true;
        this.unwatch();
        setTimeout(() => {
          e.target.blur();
        }, 10);
        this.operationindex = e.target.selectionStart;
      } else {
        // this.placeCaretAtEnd(e.target);
        // e.target.selectionEnd= 2
        // var el = e.target;
        // var range = document.createRange();
        // console.log(range);
        // var sel = window.getSelection();
        // console.log(el.childNodes, "元素");
        // debugger;
        // range.setStart(el, 3);
        // range.collapse(true);
        // sel.removeAllRanges();
        // sel.addRange(range);
      }
      this.operationindex = e.target.selectionStart;
      console.log(this.operationindex);
      e.stopPropagation();
      e.preventDefault();
    },
    handleItemClick(index,val) {
      let textlist = [...this.text];
      let beforeline = textlist.slice(0, this.operationindex);
      let endline = textlist.slice(this.operationindex);
      console.log(beforeline, endline);
      this.text =
        beforeline.join("") + this.list[index].approvalManName + endline.join("");
      // console.log(JSON.stringify(this.textlist),"操作之前")
      this.textlist.splice(this.operationindex - 1, 0, "`@");
      // console.log(JSON.stringify(this.textlist),"插入之后")
      let beforelinelist = this.textlist.slice(0, this.operationindex);
      let endlinelist = this.textlist.slice(this.operationindex);
      let namelist = [...this.list[index].approvalManName];
      // console.log(beforelinelist,namelist,endlinelist)
      namelist[namelist.length - 1] = namelist[namelist.length - 1] + "`";
      this.textlist = [...beforelinelist, ...namelist, ...endlinelist];
      console.log("点击后的this.textlist:" + this.textlist);

      let remarksListData = []
      if(this.relationApprovalRemarksList.length>0) {
        this.relationApprovalRemarksList.forEach((data)=>{
          remarksListData.push(data.nodeId)
        })
      }

      let isAt = remarksListData.indexOf(val.nodeId)
      if(isAt == -1 ) {
        this.relationApprovalRemarksList.push({
          approvalMan: val.approvalMan,
          approvalManName: val.approvalManName,
          nodeId: val.nodeId,
          nodeName: val.nodeName,
        })
      }


      // TODO 添加响应式
      setTimeout(() => {
        this.watchText();
        this.showboard = false;
        this.markselectpeople = false;
      }, 10);
    },
    // 找寻@名称列表
    findAtNameList() {
      let atgroup = [];
      let textlist = this.textlist;
      let startindex = null;
      let endindex = null;
      console.log("findAtNameList", [...textlist]);
      for (let j = 0; j < textlist.length; j++) {
        if (textlist[j] == "`@") {
          startindex = j;
          // 开始标记
          // str += textlist[j]
          endindex = null;
        }
        if (textlist[j][textlist[j].length - 1] == "`") {
          // 结束符号
          if (startindex !== null) {
            endindex = j;
          }
        }
        if (startindex !== null && endindex !== null) {
          let item = {
            startindex: startindex,
            endindex: endindex,
          };
          startindex = null;
          endindex = null;
          atgroup.push(item);
        }
      }
      return atgroup;
    },
  },

css部分

<style scoped lang="scss">
#editable {
  width: 100px;
  height: 100px;
}
.app{

}
.remarksList{
  display: flex;
  flex-direction: column;
  margin-top: 30px;
  height: 400px;
  overflow-y: auto;
  .remarksBox{
    display: flex;
    flex-direction: column;
    background: #F7F9FC;
    border-radius: 3px;
    padding: 15px;
    margin-bottom: 20px;
    .remarkContent{
      color: #313840;
      font-size: 14px;
      line-height: 22px;
      span{
        color: #1F5FB1;
      }
    }
    .line{
      width: 506px;
      height: 1px;
      border: 1px solid #E5EAEF;
      margin: 15px 0;
    }
    .remarkTips{
      display: flex;
      align-items: center;
      span{
        color: #838A92;
        font-size: 14px;
        line-height: 20px;
        margin-right: 10px;
      }
      div{
        color: #AEB6C0;
        font-size: 14px;
        line-height: 20px;
      }
    }
  }
}
.btncontainer {
  display: flex;
  position: relative;
  flex-direction: column;
  textarea:disabled {
    background-color: white;
  }
  .totest,
  .tohome {
    font-size: 12px;
    height: 30px;
    width: 80px;
    border: 1px solid gray;
    border-radius: 10px;
    margin-top: 10px;
    text-align: center;
    line-height: 30px;
    margin-left: 10px;
  }
  .text {
    width: 200px;
    height: 200px;
  }
  .board {
    width: 200px;
    max-height: 181px;
    overflow: scroll;
    cursor: pointer;
    margin-top: 5px;
    background: #FFFFFF;
    box-shadow: 0px 3px 8px 0px rgba(166, 166, 166, 0.15);
    div {
      height: 32px;
      line-height: 32px;
      cursor: pointer;
      font-size: 16px;
      padding: 0 12px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    div:hover{
      background: #1F5FB1;
      color: #fff;
    }

  }
}
.addBtn{
  width: 60px;
  height: 36px;
  background: #1F5FB1;
  border-radius: 4px;
  font-size: 14px;
  color: #fff;
  line-height: 36px;
  text-align: center;
  margin-top: 12px;
  cursor: pointer;
}
.editor {
  margin: 0 auto;
  width: 536px;
  height: 102px;
  background: #fff;
  border: 1px solid #E5EAEF;
  border-radius: 4px;
  text-align: left;
  padding: 8px 12px;
  overflow: auto;
  line-height: 20px;
  &:focus {
    outline: none;
  }
}
::v-deep .el-dialog__body{
  padding: 20px 32px 30px 32px;
}
</style>

总结

在PC端实现@功能,主要的点在于在文本框输入@的时候,通过键盘事件进行操作,然后在@的位置插入对应的值,最后组装数据,通过输入框的事件input拿到数据传给后端。经个人研究,移动端,小程序H5暂时还没有好的方法实现艾特功能,有知道的大佬可以分享一波,谢谢!!!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值