首页 > 其他分享 >Vue 评论@人功能实现

Vue 评论@人功能实现

时间:2022-11-24 11:34:47浏览次数:55  
标签:node index 功能 Vue const name 评论 user return

 

 

 

<template>
  <div class="content-container">
    <el-dialog
      :title="title"
      :visible.sync="dialogVisible"
      width="540px"
      :append-to-body="true"
      :close-on-click-modal="false"
      :before-close="handleClose"
      custom-class="bpm-message-dialog"
    >
      <el-form
        ref="form"
        :rules="rules"
        :model="form"
        label-position="top"
      >
        <el-form-item label="内容" prop="note">
          <div
            ref="editor"
            class="message"
            spellcheck="false"
            :contenteditable="true"
            @keyup="handkeKeyUp"
            @keydown="handleKeyDown"
          />
        </el-form-item>
        <el-form-item label="附件">
          <MyUpload
            ref="myUpload"
            v-model="form.fileList"
            type="img"
            :accept="['.png','.jpg','.jpeg','.gif']"
          />
        </el-form-item>
      </el-form>
      <span slot="footer" class="dialog-footer">
        <el-button @click="handleClose">取 消</el-button>
        <el-button type="primary" @click="submit">确 定</el-button>
      </span>
      <AtDialog
        v-if="showDialog"
        ref="atDialog"
        :visible="showDialog"
        :position="position"
        :query-string="queryString"
        @onPickUser="handlePickUser"
        @onHide="handleHide"
        @onShow="handleShow"
        @close="showDialog = false"
      />
    </el-dialog>
  </div>
</template>

<script>
import MyUpload from '@/components/approveUpload/approve.vue'
import AtDialog from './AtDialog'
export default {
  name: 'RejectA',
  components: { MyUpload, AtDialog },
  props: {
    title: {
      type: String,
      default: '留言'
    }
  },
  data() {
    return {
      dialogVisible: false,
      form: {
        note: '',
        fileList: ''
      },
      rules: {
        note: [{ required: true, message: '请输入备注信息', trigger: 'blur' }]
      },
      fileList: [],
      allowSize: 50 * 1024,
      limit: 5,
      operateType: null, // 操作类型
      node: '', // 获取到节点
      user: '', // 选中项的内容
      endIndex: '', // 光标最后停留位置
      queryString: '', // 搜索值
      showDialog: false, // 是否显示弹窗
      position: {
        x: 0,
        y: 0
      }// 弹窗显示位置
    }
  },
  computed: {},
  watch: {},
  created() {},
  mounted() {},
  methods: {
    // 限制输入框换行操作
    handleTextareaKeydown() {
      const e = window.event || arguments[0]
      if (e.key === 'Enter' || e.code === 'Enter' || e.keyCode === 13) {
        e.returnValue = false
        return false
      }
    },
    openDialog() {
      this.dialogVisible = true
    },
    handleClose(done) {
      console.log('handleClose')
      this.$refs.form.resetFields()
      this.$set(this.form, 'fileList', null)
      this.$refs.myUpload.clearFileList()
      this.dialogVisible = false
    },
    // 获取光标位置
    getCursorIndex() {
      const selection = window.getSelection()
      return selection.focusOffset // 选择开始处 focusNode 的偏移量
    },
    // 获取节点
    getRangeNode() {
      const selection = window.getSelection()
      return selection.focusNode // 选择的结束节点
    },
    // 弹窗出现的位置
    getRangeRect() {
      const selection = window.getSelection()
      const range = selection.getRangeAt(0) // 是用于管理选择范围的通用对象
      const rect = range.getClientRects()[0] // 择一些文本并将获得所选文本的范围
      const LINE_HEIGHT = 30
      return {
        x: rect.x,
        y: rect.y + LINE_HEIGHT
      }
    },
    // 是否展示 @
    showAt() {
      const node = this.getRangeNode()
      if (!node || node.nodeType !== Node.TEXT_NODE) return false
      const content = node.textContent || ''
      const regx = /@([^@\s]*)$/
      const match = regx.exec(content.slice(0, this.getCursorIndex()))
      console.log('match', match)
      return match && match.length === 2
    },
    // 获取 @ 用户
    getAtUser() {
      const content = this.getRangeNode().textContent || ''
      const regx = /@([^@\s]*)$/
      const match = regx.exec(content.slice(0, this.getCursorIndex()))
      if (match && match.length === 2) {
        return match[1]
      }
      return undefined
    },
    // 创建标签
    createAtButton(user) {
      const btn = document.createElement('span')
      btn.style.display = 'inline-block'
      btn.dataset.user = JSON.stringify(user)
      btn.className = 'bpm-at-button'
      btn.contentEditable = 'false'
      btn.textContent = `@${user.name}`
      const wrapper = document.createElement('span')
      wrapper.style.display = 'inline-block'
      wrapper.contentEditable = 'false'
      const spaceElem = document.createElement('span')
      spaceElem.style.whiteSpace = 'pre'
      spaceElem.textContent = '\u200b'
      spaceElem.contentEditable = 'false'
      const clonedSpaceElem = spaceElem.cloneNode(true)
      wrapper.appendChild(spaceElem)
      wrapper.appendChild(btn)
      wrapper.appendChild(clonedSpaceElem)
      return wrapper
    },
    replaceString(raw, replacer) {
      return raw.replace(/@([^@\s]*)$/, replacer)
    },
    // 插入@标签
    replaceAtUser(user) {
      const node = this.node
      if (node && user) {
        const content = node.textContent || ''
        const endIndex = this.endIndex
        const preSlice = this.replaceString(content.slice(0, endIndex), '')
        const restSlice = content.slice(endIndex)
        const parentNode = node.parentNode
        const nextNode = node.nextSibling
        const previousTextNode = new Text(preSlice)
        const nextTextNode = new Text('\u200b' + restSlice) // 添加 0 宽字符
        const atButton = this.createAtButton(user)
        parentNode.removeChild(node)
        // 插在文本框中
        if (nextNode) {
          parentNode.insertBefore(previousTextNode, nextNode)
          parentNode.insertBefore(atButton, nextNode)
          parentNode.insertBefore(nextTextNode, nextNode)
        } else {
          parentNode.appendChild(previousTextNode)
          parentNode.appendChild(atButton)
          parentNode.appendChild(nextTextNode)
        }
        // 重置光标的位置
        const range = new Range()
        const selection = window.getSelection()
        range.setStart(nextTextNode, 0)
        range.setEnd(nextTextNode, 0)
        selection.removeAllRanges()
        selection.addRange(range)
      }
    },
    // 键盘抬起事件
    handkeKeyUp() {
      if (this.showAt()) {
        const node = this.getRangeNode()
        const endIndex = this.getCursorIndex()
        this.node = node
        this.endIndex = endIndex
        this.position = this.getRangeRect()
        this.queryString = this.getAtUser() || ''
        this.showDialog = true
      } else {
        this.showDialog = false
      }
    },
    // 键盘按下事件
    handleKeyDown(e) {
      if (this.showDialog) {
        if (e.code === 'ArrowUp' ||
          e.code === 'ArrowDown' ||
          e.code === 'Enter') {
          e.preventDefault()
        }
      }
    },
    resetAtDialog() {
      this.queryString = ''
      this.$refs.index = -1
    },
    // 插入标签后隐藏选择框
    handlePickUser(user) {
      this.replaceAtUser(user)
      this.user = user
      this.showDialog = false
    },
    // 隐藏选择框
    handleHide() {
      this.showDialog = false
    },
    // 显示选择框
    handleShow() {
      this.queryString = ''
      this.$refs.index = -1
      this.showDialog = true
    },
    submit() {
      switch (this.title) {
        case '驳回到发起人':
          this.operateType = 4
          break
        case '驳回到指定节点':
          this.operateType = 5
          break
        case '驳回到上一节点':
          this.operateType = 6
          break
        case '同意':
          this.operateType = 7
          break
      }
      this.$refs.form.validate((valid) => {
        if (valid) {
          // 判断是否存在文件上传
          this.isUploading().then((res) => {
            if (res) {
              console.log('res', res)
            } else {
              console.log('')
            }
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
  }
}
</script>

<style lang="scss">
  .bpm-message-dialog {
    .message {
      min-height: 96px;
      display: block;
      resize: vertical;
      padding: 5px 15px;
      line-height: 1.5;
      -webkit-box-sizing: border-box;
      box-sizing: border-box;
      width: 100%;
      font-size: inherit;
      color: #606266;
      background-color: #FFFFFF;
      background-image: none;
      border: 1px solid #DCDFE6;
      border-radius: 4px;
      -webkit-transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
      transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
    }
  }
</style>
<style lang="scss" scoped>
@import "~@/views/taskManage/css/dialog.scss";
</style>
<template>   <div     class="wrapper"     :style="{position:'fixed',top:position.y +'px',left:position.x+'px'}"   >     <div v-if="!mockList.length" class="empty">       <p>无搜索结果</p>     </div>     <div       v-for="(item,i) in mockList"       :key="item.id"       ref="usersRef"       class="item"       :class="{'active': i === index}"       @click="clickAt($event,item)"       @mouseenter="hoverAt(i)"     >       <div class="name">{{ item.name }}</div>     </div>   </div> </template>
<script> const mockData = [   { name: 'HTML', id: 'HTML' },   { name: 'CSS', id: 'CSS' },   { name: 'Java', id: 'Java' },   { name: 'JavaScript', id: 'JavaScript' } ] export default {   name: 'AtDialog',   props: {     visible: {       type: Boolean     },     position: {       type: Object,       default: () => {}     },     queryString: {       type: String,       default: ''     }   },   data() {     return {       users: [],       index: -1,       mockList: mockData     }   },   watch: {     queryString(val) {       val ? this.mockList = mockData.filter(({ name }) => name.startsWith(val)) : this.mockList = mockData.slice(0)     }   },   mounted() {     document.addEventListener('keyup', this.keyDownHandler)   },   destroyed() {     document.removeEventListener('keyup', this.keyDownHandler)   },   methods: {     closeDialog() {       this.index = -1       this.$emit('close')     },     keyDownHandler(e) {       if (e.code === 'Escape') {         this.$emit('onHide')         return       }       // 键盘按下 => ↓       if (e.code === 'ArrowDown') {         if (this.index >= this.mockList.length - 1) {           this.index = 0         } else {           this.index = this.index + 1         }       }       // 键盘按下 => ↑       if (e.code === 'ArrowUp') {         if (this.index <= 0) {           this.index = this.mockList.length - 1         } else {           this.index = this.index - 1         }       }       // 键盘按下 => 回车       if (e.code === 'Enter') {         if (this.mockList.length) {           const user = {             name: this.mockList[this.index].name,             id: this.mockList[this.index].id           }           this.$emit('onPickUser', user)           this.index = -1         }       }     },     clickAt(e, item) {       const user = {         name: item.name,         id: item.id       }       this.$emit('onPickUser', user)       this.index = -1     },     hoverAt(index) {       this.index = index     }   } } </script>
  <style scoped lang="scss">     .wrapper {       width: 238px;       border: 1px solid #e4e7ed;       border-radius: 4px;       background-color: #fff;       box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);       box-sizing: border-box;       padding: 6px 0;     }     .empty{       font-size: 14px;       padding: 0 20px;       color: #999;       height: 24px;       line-height: 24px;       position: relative;       svg {         position: absolute;         width: 18px;         height: 18px;         right: 6px;         top: 4px;         cursor: pointer;       }     }     .item {       font-size: 14px;       padding: 0 20px;       line-height: 34px;       cursor: pointer;       color: #606266;       &.active {         background: #f5f7fa;         color: blue;         .id {           color: blue;         }       }       &:first-child {         border-radius: 5px 5px 0 0;       }       &:last-child {         border-radius: 0 0 5px 5px;       }       .id {         font-size: 12px;         color: rgb(83, 81, 81);       }     }   </style>

标签:node,index,功能,Vue,const,name,评论,user,return
From: https://www.cnblogs.com/zerofan/p/16921266.html

相关文章

  • vue table直接定位到指定元素
    vue+element中的表格,直接定位到指定的元素。需求:点击某一个节点,弹窗,直接定位到点击的节点,高亮并显示数据。<el-tableref="highTable":data="treeData"highli......
  • 嵌入式系统软件的功能安全与信息安全
    前言 当代的科技正在以前所未有的速度发展,每天都有崭新的产品与功能出现,完成难以想象的任务。这种情况不再局限于手机APP和计算机,同时也包括了对我们日常生活来说更普遍......
  • [未解决] vue 重启电脑后项目报错,某个全局变量未定义
    问题昨天上班还运行得好好的,今天启动就报错了,无法进入系统。尝试删除node_modules和package-lock.json,重新npminstall,无效;再次重启电脑,无效;清除浏览器缓存,清除cook......
  • vue表单必填项前面添加红色*
    vue表单必填项前面添加红色*和提示信息1.效果图2.实现代码(1)from表单定义:rules="rules",:model="headerForm"(2)文本框或者下拉框定义:rules="rules.XXXX",prop="XX......
  • vue详细教程
    原文链接:https://www.cnblogs.com/MrFlySand/p/16921017.html02vue的安装<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv=......
  • 9 个功能强大的 JavaScript 技巧
    英文|https://dev.to/razgandeanu/9-extremely-powerful-javascript-hacks-4g3p​1、全部替换我们知道string.replace() 函数仅替换第一次出现的情况。你可以通过在正则......
  • 记笔记 vue创建项目
    困难的工作才有价值。你的问题不解决吗?要解决问题。自强不息。困难就是进步,还有进步空间。因为我学这个遇见了很多帮助我的人,所以我必须学这个,前面的努力才没白费。项目......
  • VUE3 自定义 轻量级全局数据共享方案之一 Provide&inject (简单快速实现vuex功能)
    在vue2中,提供了provide和inject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用除了兼容vue2的配置式注入,vue3在compositionapi中添加了provide和inject方法......
  • 我的Vue之旅 11 Vuex 实现购物车
    VueCartView.vuescript数组的filter函数需要return显式返回布尔值,该方法得到一个新数组。使用Vuexstore的modules方式,注意读取状态的方式this.$store.state.cart.i......
  • String.join()方法的功能简介说明
    转自:http://www.java265.com/JavaCourse/202206/3733.htmlString简介:  string是C++、java、VB等编程语言中的字符串,字符串是一个特殊的对象,属于引用类型。在java......