Vue 评论@人功能实现

 

 

 

<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>
posted @ 2022-11-24 11:22  吃饭七分饱  阅读(781)  评论(0编辑  收藏  举报