输入框实现@别人功能

支持复制粘贴

概要
开发任务系统中,业务需求:需要在任务描述、评论等地方支持@人员功能,可以将任务外部人员添加至当前任务中。

功能:1、支持输入@展开下拉,可通过鼠标点击或键盘上下移动 + 回车选中人员;

           2、支持@后继续输入以搜索人员列表;

           3、输入框@内容支持复制粘贴;

           4、添加输入框最大输入限制;

整体架构流程

因时间紧迫,虽然看到了el-input支持Autocomplete属性,但是没空去研究它了,还是用div + contenteditable="true"开撸吧;

1.使用div(ref[customInput]),添加contenteditable="true"可编辑状态,得到一个自定义输入框;
2.使用el-popover,用于展示@人员列表;
3.监听customInput 的input事件,当输入为@时,保存当前输入框光标位置,打开el-popover;
4.打开el-popover后,继续监听customInput 的input事件,使用diff.diffChars对比输入差异,拿到差异输入进行@人员列表的搜索;
5.选中人时,为了方便,我用的方案是:拿到人员数据,生成一个span标签(给span添加contenteditable="false",不然插入的人员块也能被编辑),将人员的信息绑定在span标签上,我绑定的为data-id(工号)、data-realname(姓名);span标签创建好后,根据步骤3保存的光标位置,将span插入至customInput中,同时后移光标至插入的元素后;
6.为了实现复制粘贴功能,需要对customInput的复制粘贴事件(copy、paste)进行自定义;
7.组件内写了格式化数据的方法,通过changeChosen将处理好的@人员数组数据传递给父组件;

如业务需求包含重新编辑功能(如表单提交后需要二次编辑或修改),需要告知后端需额外保存一个存html代码的字段,因为当前组件的输入框内容,因为@人员块的缘故,只能通过html保持原本的正确格式,否则就是纯文本,通俗易懂就是两个字段分别存储customInput的textContent和innerHTML字段;编辑时只需用到保存的innerHTML字段,其余的textContent字段以及处理好的@人员数组,对于我们编辑而言,是没有用的。

技术细节

  • 在监听input事件的同时,需要同时监听keydown,用来限制用户的输入;
  • 插入span以及处理自定义粘贴事件时,需要注意光标位置;
  • 还有el-popover的弹出位置,我这里是针对我们的业务需求进行的调整,有具体需要可以自己手动调整

自定义输入框代码

<template>
  <div class="custom-at-box">
    <div class="custom-textarea-box">
      <div
        :class="[
          'custom-textarea custom-scroll',
          { 'show-word-limit': showWordLimit && maxlength },
          { 'custom-textarea-disabled': disabled },
        ]"
        :style="{ height: height }"
        ref="customInput"
        :contenteditable="!disabled"
        :placeholder="placeholder"
        @input="onInput($event)"
        @keydown="onKeyDownInput($event)"
        @paste="onPaste($event)"
        @copy="onCopy($event)"
        @click="showList(false)"
      ></div>
      <div class="custom-at-limit" v-if="showWordLimit && maxlength">
        {{ inputValueLen }}/{{ maxlength }}
      </div>
    </div>
    <div :key="`customInput${taskPanelIsInFullScreen}`">
      <el-popover
        v-model="showPopover"
        trigger="click"
        class="custom-select-box"
        ref="popoverRef"
        :append-to-body="taskPanelPopoverAppendToBody"
        @hide="hidePoppver"
        :style="{ top: popoverOffset + 'px' }"
      >
        <div
          class="custom-select-content custom-scroll"
          ref="customSelectContent"
        >
          <div class="custom-select-empty load" v-if="searchOperatorLoad">
            <i class="el-icon-loading"></i>
            <span>加载中</span>
          </div>
          <div
            class="custom-select-empty"
            v-else-if="searchOperatorList.length === 0"
          >
            没有查询到该用户
          </div>
          <div
            v-else
            :class="[
              'custom-select-item',
              { hoverItem: selectedIndex === index },
            ]"
            v-for="(item, index) in searchOperatorList"
            :key="item.employeeNo"
            @click="handleClickOperatorItem(item)"
          >
            <div class="custom-select-item-content">
              {{ item.realname }}({{ item.employeeNo }})
            </div>
          </div>
        </div>
      </el-popover>
    </div>
  </div>
</template>
<script>
const diff = require("diff");
import {
  queryEmployeeByParam,
  addRemark,
} from "@/api/recruitmentSystem/childUtils.js";
export default {
  props: {
    // 输入框placeholder
    placeholder: {
      type: String,
      default: "请输入...",
    },
    // 是否显示输入字数统计
    showWordLimit: {
      type: Boolean,
      default: true,
    },
    // 是否禁用
    disabled: {
      type: Boolean,
      default: false,
    },
    // 最大输入长度
    maxlength: {
      type: [Number, String],
      default: "300",
    },
    // 输入框高度
    height: {
      type: String,
      default: "100px",
    },
    setRefresh: {
      type: Object,
      default: () => {},
    },
    // 输入框输入的内容
    value: {
      type: String,
      default: "",
    },
  },
  data() {
    return {
      // 已输入内容的长度
      inputValueLen: 0,
      top: "",
      left: "",
      message: "",
      startOffset: 0,
      // @搜索人dom
      searSpan: null,
      // 筛选人数据
      searchOperatorList: [],
      // 筛选人数据加载状态
      searchOperatorLoad: false,
      // @插入位置
      selectionIndex: 0,
      // 当前编辑的dom
      dom: null,
      // 当前编辑dom的index
      domIndex: 0,
      // 当前编辑dom的childNodes的index
      childDomIndex: 0,
      // 编辑前dom内容
      beforeDomVal: "",
      // 筛选人选择框
      showPopover: false,
      // 筛选人选择框偏移量
      popoverOffset: 0,
      listInput: false,
      listInputValue: "",
      // 防抖
      timer: null,
      // 保存弹窗加载状态
      addDataLoad: false,
 
      // 鼠标选择人的索引
      selectedIndex: 0,
    };
  },
  mounted() {
    this.setNativeInputValue();
  },
  computed: {
    // 计算属性,用于同步父组件的数据
    model: {
      get() {
        return this.value;
      },
      set(newValue) {
        this.$emit("input", newValue);
        if (this.$refs.customInput) {
          this.$emit("inputText", this.$refs.customInput.textContent);
        }
        const nodeList = this.$refs.customInput.childNodes;
        let list = [];
        nodeList.forEach((e) => {
          if(e.childNodes) {
            e.childNodes.forEach(i => {
              if (i.className === "active-text") {
                list.push({
                  jobNumber: i.getAttribute("data-id"),
                  name: i.textContent.replace(/@/g, "").replace(/\s/g, ""),
                });
              }
            })
          }
          if (e.className === "active-text") {
            list.push({
              jobNumber: e.getAttribute("data-id"),
              name: e.textContent.replace(/@/g, "").replace(/\s/g, ""),
            });
          }
        });
        this.$emit("changeChosen", list);
      },
    },
    taskPanelIsInFullScreen() {
      return this.$store.getters.taskPanelIsInFullScreen;
    },
    taskPanelPopoverAppendToBody() {
      return this.$store.getters.taskPanelPopoverAppendToBody;
    },
  },
  methods: {
    // 设置输入框的值
    setNativeInputValue() {
      if (this.$refs.customInput) {
        if (this.value === this.$refs.customInput.innerHTML) return;
        this.$refs.customInput.innerHTML = this.value;
        this.inputValueLen = this.$refs.customInput.innerText.length;
      }
    },
    // 筛选人弹窗数据选择
    handleClickOperatorItem(item) {
      this.addData(JSON.parse(JSON.stringify(item)));
      this.$refs.customSelectContent.scrollTop = 0;
      this.selectedIndex = 0;
      this.showPopover = false;
      this.listInput = false;
      this.listInputValue = "";
    },
    // 艾特人弹窗关闭
    hidePoppver() {
      this.$refs.customSelectContent.scrollTop = 0;
      this.selectedIndex = 0;
      this.showPopover = false;
      this.listInput = false;
      this.listInputValue = "";
    },
    // 创建艾特需要插入的元素
    createAtDom(item) {
      // 先判断剩余输入长度是否能够完整插入元素
      const dom = document.createElement("span");
 
      dom.classList.add("active-text");
      // 这里的contenteditable属性设置为false,删除时可以整块删除
      dom.setAttribute("contenteditable", "false");
      // 将id存储在dom元素的标签上,便于后续数据处理
      dom.setAttribute("data-id", item.employeeNo);
 
      dom.innerHTML = `@${item.realname}&nbsp;`;
 
      return dom;
    },
    // 插入元素
    addData(item) {
      const spanElement = this.createAtDom(item);
 
      const maxlength = Number(this.maxlength) || 300;
      // 因为插入后需要删除之前输入的@,所以判断长度时需要减去这个1
      if (maxlength - this.inputValueLen < spanElement.innerText.length - 1) {
        this.$message("剩余字数不足");
        return;
      }
      this.$refs.customInput.focus();
 
      // 获取当前光标位置的范围
      const selection = window.getSelection();
      const range = selection.getRangeAt(0);
 
      // 找到要插入的节点
      const nodes = Array.from(this.$refs.customInput.childNodes);
      let insertNode = "";
      // 是否是子元素
      let domIsCustomInputChild = true;
      if (nodes[this.domIndex].nodeType === Node.TEXT_NODE) {
        insertNode = nodes[this.domIndex];
      } else {
        const childNodeList = nodes[this.domIndex].childNodes;
        insertNode = childNodeList[this.childDomIndex];
        domIsCustomInputChild = false;
      }
 
      // 如果前一个节点是空的文本节点,@用户无法删除
      // 添加判断条件:如果前一个节点是空的文本节点,则插入一个空的<span>节点
      const html = insertNode.textContent;
      // 左边的节点
      const textLeft = document.createTextNode(
        html.substring(0, this.selectionIndex - 1) + ""
      );
      const emptySpan = document.createElement("span");
 
      // 如果找到了要插入的节点,则在其前面插入新节点
      if (insertNode) {
        if (!textLeft.textContent) {
          if (domIsCustomInputChild) {
            this.$refs.customInput.insertBefore(emptySpan, insertNode);
          } else {
            nodes[this.domIndex].insertBefore(emptySpan, insertNode);
          }
        }
        insertNode.parentNode.insertBefore(spanElement, insertNode.nextSibling);
        // 删除多余的@以及搜索条件
        const textContent = insertNode.textContent.slice(
          0,
          -(1 + this.listInputValue.length)
        );
        if (!textContent && insertNode.nodeName === "#text") {
          insertNode.remove();
        } else {
          insertNode.textContent = textContent;
        }
      } else {
        // 如果未找到要插入的节点,则将新节点直接追加到末尾
        this.$refs.customInput.appendChild(spanElement);
      }
 
      // 将光标移动到 span 元素之后
      const nextNode = spanElement.nextSibling;
      range.setStart(
        nextNode || spanElement.parentNode,
        nextNode ? 0 : spanElement.parentNode.childNodes.length
      );
      range.setEnd(
        nextNode || spanElement.parentNode,
        nextNode ? 0 : spanElement.parentNode.childNodes.length
      );
      selection.removeAllRanges();
      selection.addRange(range);
 
      this.model = this.$refs.customInput.innerHTML;
      this.inputValueLen = this.$refs.customInput.innerText.length;
      this.showList(false);
    },
    // 检查是否发生了全选操作
    isSelectAll() {
      const selection = window.getSelection();
      return selection.toString() === this.$refs.customInput.innerText;
    },
    // 获取输入框是否选中文字
    isSelect() {
      try {
        const selection = window.getSelection();
        return selection.toString().length;
      } catch (error) {
        return 0;
      }
    },
    // 输入事件
    onKeyDownInput(event) {
      // 获取当前输入框的长度
      let currentLength = this.$refs.customInput.innerText.length;
      // 获取最大输入长度限制
      let maxLength = Number(this.maxlength) || 300;
 
      // 如果按下的键是非控制键并且当前长度已经达到了最大长度限制
      if (currentLength >= maxLength) {
        // 获取按键的 keyCode
        var keyCode = event.keyCode || event.which;
 
        // 检查是否按下了 Ctrl 键
        var ctrlKey = event.ctrlKey || event.metaKey; // metaKey 用于 macOS 上的 Command 键
 
        // 允许的按键:Backspace(8)、Delete(46)、方向键和 
        var allowedKeys = [8, 46, 37, 38, 39, 40];
 
        // 允许的按键 Ctrl+A、Ctrl+C、Ctrl+V
        let allowedCtrlKey = [65, 67, 86]
 
        // 检查按键是否在允许列表中并且没有执行选中操作
        if (!allowedKeys.includes(keyCode) && !this.isSelect()) {
          if((allowedCtrlKey.includes(keyCode) && ctrlKey)) {
            return;
          }
          // 阻止默认行为
          event.preventDefault();
          return false;
        }
      }
 
      if (this.showPopover) {
        let listElement = this.$refs.customSelectContent;
        let itemHeight = listElement.children[0].clientHeight;
        if (event.key === "ArrowDown") {
          // 防止光标移动
          event.preventDefault();
          // 移动选中索引
          if (this.selectedIndex === this.searchOperatorList.length - 1) {
            this.selectedIndex = 0; // 跳转到第一项
            listElement.scrollTop = 0; // 滚动到列表顶部
          } else {
            this.selectedIndex++;
            let itemBottom = (this.selectedIndex + 1) * itemHeight;
            let scrollBottom = listElement.scrollTop + listElement.clientHeight;
            if (itemBottom > scrollBottom) {
              listElement.scrollTop += itemHeight;
            }
          }
        } else if (event.key === "ArrowUp") {
          event.preventDefault();
          if (this.selectedIndex === 0) {
            this.selectedIndex = this.searchOperatorList.length - 1; // 跳转到最后一项
            listElement.scrollTop = listElement.scrollHeight; // 滚动到列表底部
          } else {
            this.selectedIndex--;
            let itemTop = this.selectedIndex * itemHeight;
            if (itemTop < listElement.scrollTop) {
              listElement.scrollTop -= itemHeight;
            }
          }
        } else if (event.key === "Enter") {
          event.preventDefault();
          if (!this.searchOperatorLoad) {
            this.handleClickOperatorItem(
              this.searchOperatorList[this.selectedIndex]
            );
          }
        }
      } else if (event.key === "Backspace" && this.isSelectAll()) {
        // 如果执行了全选操作并删除,清空输入框内容
        this.$refs.customInput.innerText = "";
        this.model = this.$refs.customInput.innerHTML;
        this.inputValueLen = 0;
      }
    },
    // 监听输入事件
    onInput(e) {
      this.inputValueLen = this.$refs.customInput.innerText.length;
      if (
        ["<div><br></div>", "<br>", "<span></span><br>"].includes(
          this.$refs.customInput.innerHTML
        )
      ) {
        this.$refs.customInput.innerHTML = "";
        this.inputValueLen = 0;
      } else if (e.data === "@") {
        // 保存焦点位置
        this.saveIndex();
        this.showList();
        this.listInput = true;
      } else if (this.showPopover) {
        const diffResult = diff.diffChars(
          this.beforeDomVal,
          this.dom.textContent
        );
        let result = "";
        // 遍历差异信息数组
        for (let i = 0; i < diffResult.length; i++) {
          const change = diffResult[i];
 
          // 如果当前差异是添加或修改类型,则将其添加到结果字符串中
          if (change.added) {
            result += change.value;
          } else if (change.removed && change.value === "@") {
            this.showList(false);
            this.listInputValue = "";
          }
        }
        if (this.timer) {
          clearTimeout(this.timer);
        }
        this.listInputValue = result;
        this.timer = setTimeout(() => {
          this.remoteMethod();
        }, 300);
      }
      this.model = this.$refs.customInput.innerHTML;
    },
    onPaste(event) {
      event.preventDefault();
      // 获取剪贴板中的 HTML 和文本内容
      const html = (event.clipboardData || window.clipboardData).getData(
        "text/html"
      );
      const text = (event.clipboardData || window.clipboardData).getData(
        "text/plain"
      );
 
      // 设置最大输入限制
      const maxLength = Number(this.maxlength) || 300;
 
      // 此时加个条件  看鼠标选中的文本长度,剩余可输入长度加上选中文本长度
      const selection1 = window.getSelection();
      const range1 = selection1.getRangeAt(0);
      const clonedSelection = range1.cloneContents();
      let selectTextLen = 0
      if(clonedSelection.textContent && clonedSelection.textContent.length) {
        selectTextLen = clonedSelection.textContent.length;
      }
 
      // 剩余可输入长度
      const remainingLength = maxLength - this.inputValueLen + selectTextLen;
 
      // 过滤掉不可见字符
      const cleanText = text.replace(/\s/g, "");
 
      // 创建一个临时 div 用于处理粘贴的 HTML 内容
      const tempDiv = document.createElement("div");
      tempDiv.innerHTML = html;
 
      // 过滤掉不需要的内容,例如注释和换行符
      const fragment = document.createDocumentFragment();
      let totalLength = 0;
 
      
      if (cleanText) {
        if (remainingLength >= cleanText.length) {
          fragment.appendChild(document.createTextNode(cleanText));
        } else {
          const truncatedText = cleanText.substr(0, remainingLength);
          fragment.appendChild(document.createTextNode(truncatedText));
        }
      }else {
        Array.from(tempDiv.childNodes).forEach((node) => {
          const regex = /<span class="active-text" contenteditable="false" data-id="(\d+)">@([^<]+)<\/span>/g;
          // 过滤注释和空白节点
          if (
            node.nodeType !== 8 &&
            !(node.nodeType === 3 && !/\S/.test(node.textContent))
          ) {
            const childText = node.textContent || "";
            const childLength = childText.length;
            const childHtml = node.outerHTML || node.innerHTML;
            // 如果剩余空间足够,插入节点
            if ((regex.exec(childHtml) !== null) && totalLength + childLength <= remainingLength) {
              fragment.appendChild(node.cloneNode(true));
              totalLength += childLength;
            } else if (remainingLength - totalLength > 0) {
              // 如果还有剩余长度,不插入节点,插入文本内容
              const lastNodeLength = remainingLength - totalLength;
              const truncatedText = childText.substr(0, lastNodeLength);
              fragment.appendChild(document.createTextNode(truncatedText));
              totalLength += truncatedText.length;
            } else {
              // 如果添加当前节点的内容会超出剩余可插入长度,则结束循环
              return;
            }
          }
        });
      }
 
      // 插入处理后的内容到光标位置
      const selection = window.getSelection();
      const range = selection.getRangeAt(0);
      range.deleteContents();
      range.insertNode(fragment);
 
      // 更新输入框内容和长度
      this.model = this.$refs.customInput.innerHTML;
      this.inputValueLen = this.$refs.customInput.innerText.length;
 
      // 设置光标位置为插入内容的后面一位
      const newRange = document.createRange();
      newRange.setStart(range.endContainer, range.endOffset);
      newRange.collapse(true);
      selection.removeAllRanges();
      selection.addRange(newRange);
    },
    // 修改默认复制事件
    onCopy(e) {
      e.preventDefault();
      const selection = window.getSelection();
      const range = selection.getRangeAt(0);
      const clonedSelection = range.cloneContents();
 
      // 检查复制的内容是否包含符合条件的元素
      const hasActiveText =
        clonedSelection.querySelector(
          '.active-text[contenteditable="false"][data-id]'
        ) !== null;
 
      const clipboardData = e.clipboardData || window.clipboardData;
      if(hasActiveText) {
        const div = document.createElement("div");
        div.appendChild(clonedSelection);
        const selectedHtml = div.innerHTML;
        clipboardData.setData("text/html", selectedHtml);
      }else {
        clipboardData.setData("text/plain", clonedSelection.textContent || "");
      }
    },
    // 保存焦点位置
    async saveIndex() {
      const selection = getSelection();
      this.selectionIndex = selection.anchorOffset;
      const nodeList = this.$refs.customInput.childNodes;
      const range = selection.getRangeAt(0);
 
      // 保存当前编辑的dom节点
      for (const [index, value] of nodeList.entries()) {
        // 这里第二个参数要配置成true,没配置有其他的一些小bug
        // (range.startContainer.contains(value) && range.endContainer.contains(value))  是为了处理兼容性问题
        if (
          selection.containsNode(value, true) ||
          (range.startContainer.contains(value) &&
            range.endContainer.contains(value))
        ) {
          if (value.nodeType === Node.TEXT_NODE) {
            this.dom = value;
            this.beforeDomVal = value.textContent;
            this.domIndex = index;
            const selection = window.getSelection();
            const range = selection.getRangeAt(0);
            this.startOffset = range.startOffset - 1;
          } else {
            const childNodeList = value.childNodes;
            for (const [childIndex, childValue] of childNodeList.entries()) {
              if (selection.containsNode(childValue, true)) {
                this.dom = value;
                this.beforeDomVal = value.textContent;
                this.domIndex = index;
                this.childDomIndex = childIndex;
                const selection = window.getSelection();
                const range = selection.getRangeAt(0);
                this.startOffset = range.startOffset - 1;
              }
            }
          }
        }
      }
    },
    // 筛选人弹窗
    showList(bool = true) {
      this.showPopover = bool;
      if (bool) {
        const offset =
          this.getCursorDistanceFromDivBottom(this.$refs.customInput) || -1;
        if (offset < 0) {
          this.popoverOffset = 0;
        } else {
          this.popoverOffset = -(offset - 1);
        }
      }
      if (!bool) {
        this.listInputValue = "";
        this.remoteMethod();
      }
    },
    // 获取光标位置
    getCursorDistanceFromDivBottom(editableDiv) {
      // 获取选区
      const selection = window.getSelection();
      // 获取选区的范围
      const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
 
      if (range) {
        // 创建一个临时元素来标记范围的结束位置
        const markerElement = document.createElement("span");
        // 插入临时标记元素
        range.insertNode(markerElement);
        markerElement.appendChild(document.createTextNode("\u200B")); // 零宽空格
 
        // 获取标记元素的位置信息
        const markerOffsetTop = markerElement.offsetTop;
        const markerHeight = markerElement.offsetHeight;
 
        // 计算光标距离div底部的距离
        const cursorDistanceFromBottom =
          editableDiv.offsetHeight - (markerOffsetTop + markerHeight);
 
        // 滚动条距顶部的高度
        const scrollTop = editableDiv.scrollTop || 0;
        // 移除临时标记元素
        markerElement.parentNode.removeChild(markerElement);
 
        // 返回光标距离底部的距离
        return cursorDistanceFromBottom + scrollTop;
      }
 
      // 如果没有选区,则返回-1或者其他错误值
      return -1;
    },
    // 搜索筛选人
    async remoteMethod() {
      let query = this.listInputValue;
      this.searchOperatorLoad = true;
      let params = {
        keyword: query,
        pageNo: 1,
        pageSize: 500,
      };
      await queryEmployeeByParam(params)
        .then((res) => {
          this.searchOperatorList = res.list
            .filter((i) => i.employeeStatusId === 1)
            .map((e) => {
              e.value = e.employeeNo + "_" + e.realname;
              return e;
            });
        })
        .catch(() => {});
 
      this.searchOperatorLoad = false;
    },
    handleNameShift(item) {
      const name = item.realname || "";
      if (!name) return "--";
      if (name.length > 1) {
        return name.slice(0, 1);
      } else {
        return name;
      }
    },
    // 按钮div点击 聚焦textarea
    handleBtnBoxClick() {
      this.$refs.customInput.focus();
    },
    // 获取@人的姓名
    getInnerText() {
      const customInput = this.$refs.customInput;
      if (!customInput) return;
      return customInput.innerText;
    },
    // 获取@人的工号
    getJobId() {
      const nodeList = this.$refs.customInput.childNodes;
      let list = [];
      nodeList.forEach((e) => {
        if (e.className === "active-text") {
          list.push(e.getAttribute("data-id"));
        }
      });
      return list;
    },
    clearInput() {
      this.$refs.customInput.innerText = "";
      this.$refs.customInput.innerHTML = "";
      this.inputValueLen = 0;
      this.$emit("input", "");
      this.$emit("inputText", "");
      this.$emit("changeChosen", []);
    },
  },
};
</script>
<style lang="scss" scoped>
.custom-textarea-btn {
  position: absolute;
  bottom: 1px;
  right: 4px;
  left: 4px;
  text-align: right;
  // background: #fff;
  padding-bottom: 3px;
  .el-button {
    font-size: 12px;
    padding: 4px 10px;
  }
}
.custom-textarea-box {
  position: relative;
}
.custom-at-limit {
  position: absolute;
  right: 12px;
  bottom: 4px;
  font-size: 12px;
  color: #999;
  line-height: 12px;
}
::v-deep.custom-textarea {
  width: 100%;
  min-height: 50px;
  max-height: 200px;
  border: 1px solid #dcdfe6;
  border-radius: 4px;
  background-color: #ffffff;
  padding: 5px 15px;
  color: #606266;
  overflow-y: auto;
  line-height: 20px;
  font-size: 14px;
  transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  position: relative;
  word-break: break-all;
  &.show-word-limit {
    padding-bottom: 16px;
  }
  &.custom-textarea-disabled {
    cursor: not-allowed;
    background-color: #f5f7fa;
    border-color: #e4e7ed;
    color: #c0c4cc;
  }
  &:focus {
    border-color: #f98600 !important;
  }
  &:empty::before {
    content: attr(placeholder);
    font-size: 14px;
    color: #c0c4cc;
  }
  .active-text {
    color: #909399;
    // padding: 2px 6px;
    // background: #f4f4f5;
    margin-right: 4px;
    // border-radius: 4px;
    // font-size: 12px;
  }
  // &:focus::before {
  //   content: "";
  // }
}
 
::v-deep.custom-select-box {
  position: relative;
 
  .el-popover {
    padding: 0;
    top: 0;
    box-shadow: 0 4px 8px 0 rgba(89, 88, 88, 0.8);
  }
  .custom-select-content {
    width: 259px;
    padding: 8px;
    max-height: 260px;
    overflow-y: auto;
  }
 
  .custom-select-item {
    // font-size: 14px;
    // padding: 0 20px;
    // position: relative;
    // height: 34px;
    // line-height: 34px;
    // box-sizing: border-box;
    display: flex;
    padding: 8px 12px;
    border-bottom: 1px solid #ebebeb;
    align-items: center;
    color: #606266;
    cursor: pointer;
    &:last-child {
      border-bottom: none;
    }
    .avatar-box {
      flex-shrink: 0;
      .custom-select-item-avatar {
        width: 24px;
        height: 24px;
        background-color: #ffb803;
        border-radius: 50%;
        text-align: center;
        line-height: 24px;
        color: #ffffff;
      }
    }
    .custom-select-item-content {
      flex: 1;
      padding-left: 12px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    &:hover {
      background-color: #f5f7fa;
    }
    &.hoverItem {
      background-color: #dbdbdb;
    }
  }
  .custom-select-empty {
    padding: 10px 0;
    text-align: center;
    color: #999;
    font-size: 14px;
 
    &.load {
      display: flex;
      align-items: center;
      justify-content: center;
    }
  }
  .custom-scroll {
    overflow: auto;
    &::-webkit-scrollbar {
      width: 8px;
      height: 8px;
    }
    &::-webkit-scrollbar-thumb {
      border-radius: 8px;
      background-color: #b4b9bf;
    }
  }
}
</style>

外部调用示例

<template>
  <div>
    <CustomInput
      v-model="customInputHTML"
      placeholder="这是一个支持@的输入框组件"
      height="unset"
      @inputText="handleChangeInputText"
      @changeChosen="handleChangeChosen"
    />
  </div>
</template>
<script>
import CustomInput from "@/components/CustomInput/index.vue";
export default {
  components: { CustomInput },
  data() {
    return {
      // 输入框的html代码
      customInputHTML: "",
      // 输入框的文本,可让后端使用,如不使用,也没啥用
      customInputText: "",
      // 输入框返回的@人员数据
      customInputMentions: []
    }
  },
  methods: {
    // 获取输入框返回的文本
    handleChangeInputText(val) {
      this.customInputText = val;
    },
    // 获取输入框返回的文本
    handleChangeChosen(val) {
      this.customInputMentions = val;
    },
  }
}
</script>

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要使用 Ant Design Vue 组件库中的 Input 和 Button 组件。我们可以在页面上放置一个输入框和一个搜索按钮,并绑定相应的事件处理函数。 接着,我们需要调用百度搜索 API 进行搜索。可以使用 axios 库来发送 HTTP 请求,获取搜索结果。具体实现如下: ```vue <template> <div> <a-input v-model="searchText" style="width: 200px; margin-right: 10px;" placeholder="请输入关键词"></a-input> <a-button type="primary" @click="search">搜索</a-button> <ul v-if="searchResult"> <li v-for="item in searchResult"> <a :href="item.link" target="_blank">{{ item.title }}</a> </li> </ul> </div> </template> <script> import axios from 'axios'; export default { data() { return { searchText: '', searchResult: null }; }, methods: { search() { if (!this.searchText) { return; } axios.get('https://www.baidu.com/s', { params: { wd: this.searchText } }).then((response) => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(response.data, 'text/html'); const results = htmlDoc.querySelectorAll('.result'); const searchResult = []; for (let i = 0; i < results.length; i++) { const titleEle = results[i].querySelector('.t a'); const link = titleEle.href; const title = titleEle.innerText.trim(); searchResult.push({ link, title }); } this.searchResult = searchResult; }); } } }; </script> ``` 在上述代码中,我们使用了 axios 库发送 GET 请求,将搜索关键词作为参数传递给百度搜索 API。然后,我们解析返回的 HTML 页面,提取出搜索结果中的标题和链接,并展示在页面上。 需要注意的是,百度搜索 API 的使用需要遵守其相关规定,避免滥用和侵犯他人权益。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值