从零开始Vue3+Element Plus后台管理系统(22)——新增5个常用自定义指令

6,201 阅读5分钟

今天又写了5个比较常用的自定义指令,感觉Vue3写起来确实比Vue2舒服一些。

  1. 限制数字输入 v-inputNumber,可以设置小数位数

  2. 提示信息v-tooltip,使用el-tooltip做的指令

  3. 拷贝 v-copy,复制文本到剪切板

  4. 节流 v-throttle

  5. 防抖 v-debounce

    image.png

    不知不觉加了这么多自定义指令了,所以把入口文件index.ts改了一下,更加自动化,无须重复写import和app.directive注册指令

import { App } from 'vue'

const modules = import.meta.glob('../directive/**/*.ts', {
  eager: true
})

let mapDirective = new Map()

Object.keys(modules).forEach((key) => {
  if (modules[key] && modules[key].default) {
    const newKey = key.replace(/^\.\/|\.ts|\.js/g, '')
    mapDirective.set(newKey, modules[key].default)
  }
})

export default (app: App) => {
  mapDirective.forEach((value, key) => {
    app.directive(key, value)
  })

晚上了,dark模式自动变黑,所以截的图都是黑的。

v-inputNumber

Element Plus的el-number很好用,但还是有一些场景会用指令或方法来处理。

此指令限制用户只能输入数字,可以设置小数位数限制

2.gif

可选指令值

属性名说明类型默认值
decimal小数位数数字2

在模板中使用

<el-input v-model="inputValue1" v-inputNumber />
<el-input v-model="inputValue2" v-inputNumber="{ decimal: 3 }" />

指令代码

// 限制输入数字
import { DirectiveBinding } from 'vue'

interface ExHTMLElement extends HTMLElement {
  inputListener: EventListener
}

export default {
  mounted(el: ExHTMLElement, binding: DirectiveBinding) {
    const decimal = binding.value?.decimal || 2
    const elInput = el.getElementsByTagName('input')[0]

    let regDecimal: RegExp
    if (decimal > 0) regDecimal = new RegExp(`^\\d*(.?\\d{0,${decimal}})`, 'g')
    else regDecimal = new RegExp(`^\\d*`, 'g')

    el.inputListener = () => {
      let val = elInput.value

      elInput.value =
        val
          .replace(/[^\d^\.]+/g, '')
          .replace(/^0+(\d)/, '$1')
          .replace(/^\./, '0.')
          .match(regDecimal)[0] || ''
    }
    elInput.addEventListener('input', el.inputListener)
  },

  unmounted(el: ExHTMLElement) {
    el.getElementsByTagName('input')[0].removeEventListener('input', el.inputListener)
  }
}

v-tooltip

2.gif

在元素上使用该指令,元素左边/右边会自动加上问号图标,图标附带tooltip。因为使用el-tooltip来开发的这个指令,所以可选指令可以根据el-tooltip的属性继续扩展。

可选指令值

属性名说明类型默认值
message提示文字内容,为空不会显示tooltip图标string-
position提示图标位置enum:left\rightleft
effecttooltip主题enum:light\darklight
placementtooptip出现的位置enum:top\top-start\top-end\bottom\bottom-start\bottom-end\left\left-start\left-end\right\right-start\right-endtop

在模板中使用

在模板中使用,比直接使用el-tooltip能减少一点代码量,稍微简洁一些。

<div v-tooptip="{ message: '我是一个说明', effect: 'dark' }">我有一个说明文字</div>
<div v-tooptip="{ message: '我是一个说明', position: 'right' }">我有一个说明文字在右边</div>

指令代码

此指令实现的核心是Vue3的h函数,可以让我们在js中生成vnode并渲染,这样就可以使用Element plus的组件进行渲染。如果光靠html node的createElement、append这些方法无法实现此指令。

虽然走了一些弯路,不过折磨自己写完了也就明白了,而且对h函数更熟悉了,它是让我今天收获最多的。

专门写了一篇文章学习渲染函数 :入门Vue3使用渲染函数、h函数

import { DirectiveBinding, h, render } from 'vue'
import { ElTooltip, ElTag } from 'element-plus'
import { QuestionFilled } from '@element-plus/icons-vue'

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const message = binding.value.message
    const placement = binding.value.placement || 'top'
    const effect = binding.value.effect || 'light'
    const position = binding.value.position || 'left'
    if (binding.value.message) {
      const vnode = h(
        ElTooltip,
        { content: message, placement, effect },
        h(QuestionFilled, { style: { width: '16px' } })
      )

      const dom = document.createElement('span')
      if (position === 'left') el.prepend(dom)
      else el.append(dom)

      render(vnode, dom)
    }
  }
}

可以说这个是今天最费劲的一个指令,虽然代码看着很简单。因为用到了h函数,平时用得少,放在指令还是第一次,好几次想放弃,还是坚持写完了。

v-copy 复制文本到剪切板

2.gif

此指令用于复制文本到剪贴板中

可选指令值

属性名说明类型默认值
position相对目标元素的位置,可选'out'string-

模板中使用


<div v-copy>222</div>
<div><el-tag v-copy="{ position: 'out' }">343434</el-tag></div>

需要先安装vue3-clipboard

图标处理这块不太优雅,可以改成用h函数渲染一个elicon,先放着有时间改改。

import { DirectiveBinding } from 'vue'
import { copyText } from 'vue3-clipboard'
import { ElMessage } from 'element-plus'

interface ExHTMLElement extends HTMLElement {
  clickListener: EventListener
  trigger?: HTMLElement
}

// 复制图标
const svg =
  '<svg viewBox="0 0 1024 1024" width="1.25em" height="1.2em"><path fill="currentColor" d="M768 832a128 128 0 0 1-128 128H192A128 128 0 0 1 64 832V384a128 128 0 0 1 128-128v64a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64h64z"></path><path fill="currentColor" d="M384 128a64 64 0 0 0-64 64v448a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64V192a64 64 0 0 0-64-64H384zm0-64h448a128 128 0 0 1 128 128v448a128 128 0 0 1-128 128H384a128 128 0 0 1-128-128V192A128 128 0 0 1 384 64z"></path></svg>'

export default {
  mounted(el: ExHTMLElement, binding: DirectiveBinding) {
    // 动态增加复制图标
    el.trigger = document.createElement('span')
    el.trigger.style.marginLeft = '4px'
    el.trigger.style.cursor = 'pointer'
    el.trigger.innerHTML = svg

    // 复制图标的位置
    if (binding.value?.position === 'out') el.after(el.trigger)
    else el.append(el.trigger)

    el.clickListener = () => {
      const text = el.innerText
      copyText(text, undefined, (error: string, event: Event) => {
        if (error) {
          ElMessage({ type: 'error', message: '未能复制', duration: 2000 })
          console.log(error)
        } else {
          ElMessage({ type: 'success', message: '复制成功', duration: 2000 })
          console.log(event)
        }
      })
    }
    el.trigger.addEventListener('click', el.clickListener)
  },

  unmounted(el: ExHTMLElement) {
    el.trigger?.removeEventListener('resize', el.clickListener)
  }
}

v-throttle

节流策略(throttle),顾名思义,可以减少一段时间内事件的触发频率

此指令应用在按钮上,当用户重复点击按钮,指定时间内只执行一次操作或请求。

模板中使用

属性名说明类型默认值
time时间间隔(毫秒)数字1000

2.gif

模板中使用

<el-button type="primary" v-throttle @click="testThrottle(100)">
    点击我(间隔1秒)!
</el-button>
    <el-button type="primary" v-throttle="{ time: 3000 }" click="testThrottle(200)">点击我(间隔3秒)!
</el-button >
// 节流
// 防止按钮多次点击,多次请求
import { DirectiveBinding } from 'vue'

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const time = binding.value?.time || 1000
    el.timer = null

    el.addEventListener('click', () => {
      el.disabled = true

      if (el.timer !== null) {
        clearTimeout(el.timer)
        el.timer = null
        el.disabled = true
      }
      el.timer = setTimeout(() => {
        el.disabled = false
      }, time)
    })
  }
}

v-debounce

防抖策略(debounce)是当事件被触发后,延迟n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时

此指令接收一个Function,用于延迟该方法的执行。

2.gif

可选指令值

属性名说明类型默认值
time时间间隔(毫秒)数字1000
func延迟执行的方法Function-
// 节流
// 防止按钮多次点击,多次请求
import { DirectiveBinding } from 'vue'

export default {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const time = binding.value?.time || 1000
    const func = binding.value?.func || null
    el.timer = null

    el.addEventListener('click', () => {
      if (el.timer !== null) {
        clearTimeout(el.timer)
        el.timer = null
      }
      el.timer = setTimeout(() => {
        func && func()
      }, time)
    })
  }
}

以上5个指令都比较简单,扩展和优化的空间也有不少,可以根据实际需要修改并完善。

在此抛砖引玉,期待大家的想法和好的建议。

项目地址

本项目GIT地址:github.com/lucidity99/…

如果有帮助,给个star ✨ 点个赞👍