vue3这些常见指令你封装了吗

vue3这些常见指令你封装了吗

👉指令搭建

vue3之中会有一些常见的指令操作,接下来我们就写一下,之前我们写了权限按钮,其实是类似的

指令的最主要文件如下,我们主要是主模块之中使用,其他的模块之中分割写好方法即可

指令主要文件

javascript 复制代码
src\utils\directive\index.ts

import type { App, Directive } from 'vue'
const directives={};
// 导出插件对象
export const registerDirectives = {
  install(app: App) {
    Object.keys(directives).forEach((key) => {
      app.directive(key, directives[key])
    })
  }
}

指令使用

javascript 复制代码
// 指令使用
import {registerDirectives} from '@/utils/directive'// 导入全局指令
app.use(registerDirectives);//全局指令注册

👉指令编写

复制指令

指令编写
javascript 复制代码
import type { Directive, App } from 'vue'

// 扩展 HTMLElement 接口
declare global {
  interface HTMLElement {
    copyData?: string
  }
}

// 定义指令值的类型
interface CopyBinding {
  value: string
}

// 复制指令配置
const copy: Directive<HTMLElement, string> = {
  mounted(el: HTMLElement, binding: CopyBinding) {
    // 保存要复制的值
    el.copyData = binding.value
    // 添加点击事件监听
    el.addEventListener('click', handleClick)
  },
  updated(el: HTMLElement, binding: CopyBinding) {
    // 更新要复制的值
    el.copyData = binding.value
  },
  beforeUnmount(el: HTMLElement) {
    // 移除事件监听
    el.removeEventListener('click', handleClick)
  }
}

// 处理复制功能
const handleClick = async (event: Event) => {
  const el = event.currentTarget as HTMLElement
  if (!el.copyData) return

  try {
    // 使用现代的 Clipboard API
    await navigator.clipboard.writeText(el.copyData)
    // 可以在这里添加成功提示
    console.log('复制成功')
  } catch (err) {
    // 降级方案:使用传统方法
    const input = document.createElement('input')
    input.value = el.copyData
    document.body.appendChild(input)
    input.select()
    try {
      document.execCommand('Copy')
      console.log('复制成功')
    } catch (err) {
      console.error('复制失败:', err)
    }
    document.body.removeChild(input)
  }
}
// 导出指令对象
export { copy }
引入指令
javascript 复制代码
// 复制指令
import {copy} from './modules/copy'

// 定义所有指令
const directives: Record<string, Directive> = {

  // 复制指令
  copy,
}
使用指令

接下来演示一下在项目之中进行使用指令

javascript 复制代码
<template>
  <div class="flex gap-3">
    <input 
      class="flex-1 px-4 py-2 bg-gray-50 border-0 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" 
      placeholder="请输入要复制的内容" 
      type="text" 
      v-model="data"
    >
    <el-button 
      class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-200"
      v-copy="data"
    >
      复制
    </el-button>
  </div>
</template>

<script setup>
const data = ref('我是被复制的内容 🍒 🍉 🍊')
</script>

水印指令

接下来写一个水印指令,我们设置的是采取canvas实现的水印效果,接下来我们就编写一下

引入指令

接下来我们就在这里编写水印

javascript 复制代码
src\utils\directive\modules\watermark .ts
javascript 复制代码
// 水印指令
import {watermark} from './modules/watermark'

// 定义所有指令
const directives: Record<string, Directive> = {
  // 水印指令
  watermark,
}
指令编写
javascript 复制代码
// modules/watermark.ts

export interface WatermarkConfig {
  text?: string
  color?: string
  fontSize?: number
  fontFamily?: string
  width?: number
  height?: number
  rotate?: number
  zIndex?: number
}

interface HTMLElementWithWatermark extends HTMLElement {
  _watermarkElement?: HTMLDivElement
}

const defaultConfig: Required<WatermarkConfig> = {
  text: 'Watermark',
  color: 'rgba(0, 0, 0, 0.15)',
  fontSize: 16,
  fontFamily: 'Arial',
  width: 200,
  height: 200,
  rotate: -20,
  zIndex: 9999
}

const createWatermark = (config: WatermarkConfig): string => {
  const finalConfig = { ...defaultConfig, ...config }
  
  const canvas = document.createElement('canvas')
  canvas.width = finalConfig.width
  canvas.height = finalConfig.height
  const ctx = canvas.getContext('2d')!

  // 设置画布样式
  ctx.rotate((finalConfig.rotate * Math.PI) / 180)
  ctx.font = `${finalConfig.fontSize}px ${finalConfig.fontFamily}`
  ctx.fillStyle = finalConfig.color
  ctx.textAlign = 'center'
  ctx.textBaseline = 'middle'
  
  // 绘制水印文本
  ctx.fillText(finalConfig.text, finalConfig.width / 2, finalConfig.height / 2)

  return canvas.toDataURL()
}

const watermark = {
  mounted(el: HTMLElementWithWatermark, binding: { value: WatermarkConfig }) {
    const config = binding.value || {}
    const dataURL = createWatermark(config)

    // 创建水印层
    const watermarkDiv = document.createElement('div')
    watermarkDiv.style.position = 'absolute'
    watermarkDiv.style.top = '0'
    watermarkDiv.style.left = '0'
    watermarkDiv.style.width = '100%'
    watermarkDiv.style.height = '100%'
    watermarkDiv.style.pointerEvents = 'none'
    watermarkDiv.style.backgroundImage = `url(${dataURL})`
    watermarkDiv.style.backgroundRepeat = 'repeat'
    watermarkDiv.style.zIndex = String(config.zIndex || defaultConfig.zIndex)

    // 设置父元素为相对定位
    el.style.position = 'relative'
    // 添加水印层
    el.appendChild(watermarkDiv)

    // 保存水印元素引用
    el._watermarkElement = watermarkDiv
  },

  updated(el: HTMLElementWithWatermark, binding: { value: WatermarkConfig; oldValue: WatermarkConfig }) {
    // 如果配置发生变化,重新渲染水印
    if (JSON.stringify(binding.value) !== JSON.stringify(binding.oldValue)) {
      // 移除旧水印
      if (el._watermarkElement) {
        el.removeChild(el._watermarkElement)
      }
      // 创建新水印
      watermark.mounted(el, binding)
    }
  },

  unmounted(el: HTMLElementWithWatermark) {
    // 组件卸载时移除水印
    if (el._watermarkElement) {
      el.removeChild(el._watermarkElement)
      delete el._watermarkElement
    }
  }
}
export { watermark }
export default watermark;
指令使用

这个时候使用我们的指令,可以看到我们的效果

javascript 复制代码
<template>
  <div class="flex gap-3 content" v-watermark="watermarkConfig">
    <h3 class="text-lg font-semibold mb-4 text-gray-800">水印指令</h3>
    <input 
      class="flex-1 px-4 py-2  border-0 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all duration-200" 
      placeholder="请输入要复制的内容" 
      type="text" 
      v-model="data"
    >
  </div>
</template>

<script setup>
import { ref,computed } from 'vue'
// 将原来的 compute 方法改为计算属性
const watermarkText = computed(() => data.value)

const data = ref('水印内容🍒 🍉 🍊')

// 然后在 watermarkConfig 中使用这个计算属性
const watermarkConfig = computed(() => ({
  text: watermarkText.value,
  color: 'rgba(0, 0, 0, 0.15)',
  fontSize: 16,
  fontFamily: 'Arial',
  width: 200,
  height: 200,
  rotate: -20,
  zIndex: 9999,
}))
</script>
<style scoped>
.content {
  position: relative;
  width: 100%;
  height: 100%;
  background: #fff;
}
</style>

拖拽指令

指令编写
javascript 复制代码
src\utils\directive\modules\draggable.ts

指令内容,这里需要注意一个部分,指令的位置是相对于我们父元素位置,而不是相对于我们视口的位置

javascript 复制代码
// 记录初始位置
const rect = el.getBoundingClientRect()
dragData.initialLeft = rect.left
dragData.initialTop = rect.top

=>更改为

// 获取当前位置,如果没有设置则默认为0
dragData.initialLeft = parseInt(el.style.left) || 0
dragData.initialTop = parseInt(el.style.top) || 0

完整修改以后我们的版本如下

javascript 复制代码
import type { Directive, DirectiveBinding } from 'vue'

interface DraggableElement extends HTMLElement {
  _dragData?: {
    isDragging: boolean
    startX: number
    startY: number
    initialLeft: number
    initialTop: number
    initialPosition: string
    zIndex: string
  }
  _cleanup?: () => void  // 添加这一行
}


const draggable: Directive<DraggableElement, boolean> = {
  mounted(el: DraggableElement, binding: DirectiveBinding<boolean>) {
    if (binding.value === false) return

    const dragData = {
      isDragging: false,
      startX: 0,
      startY: 0,
      initialLeft: 0,
      initialTop: 0,
      initialPosition: '',
      zIndex: ''
    }
    el._dragData = dragData

    // 设置初始样式
    el.style.cursor = 'move'
    el.style.position = el.style.position || 'absolute'

    const handleMouseDown = (e: MouseEvent) => {
      dragData.isDragging = true
      dragData.startX = e.clientX
      dragData.startY = e.clientY
      dragData.initialPosition = el.style.position
      dragData.zIndex = el.style.zIndex
      
      // 获取当前位置,如果没有设置则默认为0
      dragData.initialLeft = parseInt(el.style.left) || 0
      dragData.initialTop = parseInt(el.style.top) || 0

      // 提高层级
      el.style.zIndex = '9999'
      
      // 添加移动时的样式
      el.style.transition = 'none'
      el.style.userSelect = 'none'
    }

    const handleMouseMove = (e: MouseEvent) => {
      if (!dragData.isDragging) return

      const deltaX = e.clientX - dragData.startX
      const deltaY = e.clientY - dragData.startY

      el.style.left = `${dragData.initialLeft + deltaX}px`
      el.style.top = `${dragData.initialTop + deltaY}px`
    }

    const handleMouseUp = () => {
      if (!dragData.isDragging) return
      
      dragData.isDragging = false
      
      // 恢复样式
      el.style.zIndex = dragData.zIndex
      el.style.userSelect = ''
      el.style.transition = ''
    }

    // 添加事件监听
    el.addEventListener('mousedown', handleMouseDown)
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)

    // 保存清理函数
    el._cleanup = () => {
      el.removeEventListener('mousedown', handleMouseDown)
      document.removeEventListener('mousemove', handleMouseMove)
      document.removeEventListener('mouseup', handleMouseUp)
    }
  },

  unmounted(el: DraggableElement) {
    // 清理事件监听
    if (el._cleanup) {
      el._cleanup()
    }
    delete el._dragData
  },

  updated(el: DraggableElement, binding: DirectiveBinding<boolean>) {
    // 如果指令值改变,更新状态
    if (binding.value === false && el._dragData) {
      el.style.cursor = ''
    } else if (binding.value === true) {
      el.style.cursor = 'move'
    }
  }
}

export {draggable}
指令使用

我们在指令之中进行使用,效果ok

javascript 复制代码
<template>
  <div class="relative">
    <div v-draggable class="draggable-box">
      可拖拽的内容
    </div>
    <!-- 也可以动态控制是否可拖拽 -->
    <div v-draggable="isDraggable" class="draggable-box">
      条件拖拽的内容
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const isDraggable = ref(true)
</script>
<style>
.draggable-box {
  width: 200px;
  height: 200px;
  background-color: #409EFF;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
}
</style>

防抖指令

指令编写
javascript 复制代码
// modules/debounce.ts
/**
 * 防抖函数
 * @param fn 需要防抖的函数
 * @param delay 延迟时间,单位毫秒,默认300ms
 * @param immediate 是否立即执行,默认false
 * @returns 返回防抖处理后的函数
 */
interface DebounceBinding {
  value: Function;
  arg?: string; // 延迟时间参数
}

// 防抖函数
function debounceFn(func: Function, wait: number) {
  let timeout: NodeJS.Timeout;
  return function(this: any, ...args: any[]) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, wait);
  };
}

export const debounce = {
  mounted(el: HTMLElement, binding: DebounceBinding) {
    // 获取延迟时间,默认为 500ms
    const delay = Number(binding.arg) || 500;
    
    // 创建防抖函数
    const debouncedFn = debounceFn(binding.value, delay);
    
    // 保存原始函数和防抖函数到元素的 dataset 中
    el.dataset.debounceFn = JSON.stringify({
      original: binding.value.toString(),
      debounced: debouncedFn.toString()
    });
    
    // 添加事件监听器
    el.addEventListener('click', debouncedFn);
  },
  
  updated(el: HTMLElement, binding: DebounceBinding) {
    // 如果值发生变化,更新防抖函数
    const delay = Number(binding.arg) || 500;
    const debouncedFn = debounceFn(binding.value, delay);
    
    // 移除旧的事件监听器
    const oldFn = new Function('return ' + JSON.parse(el.dataset.debounceFn || '{}').debounced)();
    el.removeEventListener('click', oldFn);
    
    // 更新 dataset
    el.dataset.debounceFn = JSON.stringify({
      original: binding.value.toString(),
      debounced: debouncedFn.toString()
    });
    
    // 添加新的事件监听器
    el.addEventListener('click', debouncedFn);
  },
  
  unmounted(el: HTMLElement) {
    // 组件卸载时移除事件监听器
    const fn = new Function('return ' + JSON.parse(el.dataset.debounceFn || '{}').debounced)();
    el.removeEventListener('click', fn);
    delete el.dataset.debounceFn;
  }
};

// 导出防抖函数供其他地方使用
export { debounceFn };
指令使用
javascript 复制代码
<template>
  <div class="flex flex-wrap gap-4 p-6">
    <!-- 基础防抖按钮 -->
    <button 
      v-debounce="handleClick"
      class="px-6 py-2.5 bg-blue-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      防抖按钮
    </button>

    <!-- 500ms防抖按钮 -->
    <button 
      v-debounce:500="handleClick"
      class="px-6 py-2.5 bg-green-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      500ms防抖按钮
    </button>

    <!-- 立即执行防抖按钮 -->
    <button 
      v-debounce.immediate="handleClick"
      class="px-6 py-2.5 bg-purple-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-purple-700 hover:shadow-lg focus:bg-purple-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-purple-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      立即执行防抖按钮
    </button>
  </div>
</template>
<script setup>
const handleClick = () => {
  console.log('防抖按钮点击');
}
</script>

节流指令

指令编写
javascript 复制代码
/**
 * v-throttle 指令
 * @param {Function} fn 需要节流的函数
 * @param {Number} delay 延迟时间
 * @param {Boolean} immediate 是否立即执行
 * @returns {Function} 返回一个节流后的函数
 */

// modules/throttle.ts

interface ThrottleBinding {
  value: Function;
  arg?: string | number; // 延迟时间参数
  modifiers?: {
    immediate?: boolean;
  };
}

// 节流函数
function throttleFn(
  func: Function,
  wait: number,
  immediate: boolean = false
) {
  let timeout: NodeJS.Timeout | null = null;
  let previous = 0;

  return function(this: any, ...args: any[]) {
    const now = Date.now();
    const remaining = wait - (now - previous);

    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(this, args);
    } else if (!timeout && !immediate) {
      timeout = setTimeout(() => {
        previous = immediate ? 0 : Date.now();
        timeout = null;
        if (!immediate) {
          func.apply(this, args);
        }
      }, remaining);
    }

    if (immediate && !timeout) {
      func.apply(this, args);
      previous = now;
    }
  };
}

export const throttle = {
  mounted(el: HTMLElement, binding: ThrottleBinding) {
    const delay = Number(binding.arg) || 500;
    const immediate = binding.modifiers?.immediate || false;
    
    const throttledFn = throttleFn(binding.value, delay, immediate);
    
    el.dataset.throttleFn = JSON.stringify({
      original: binding.value.toString(),
      throttled: throttledFn.toString()
    });
    
    el.addEventListener('click', throttledFn);
  },
  
  updated(el: HTMLElement, binding: ThrottleBinding) {
    const delay = Number(binding.arg) || 500;
    const immediate = binding.modifiers?.immediate || false;
    const throttledFn = throttleFn(binding.value, delay, immediate);
    
    const oldFn = new Function('return ' + JSON.parse(el.dataset.throttleFn || '{}').throttled)();
    el.removeEventListener('click', oldFn);
    
    el.dataset.throttleFn = JSON.stringify({
      original: binding.value.toString(),
      throttled: throttledFn.toString()
    });
    
    el.addEventListener('click', throttledFn);
  },
  
  unmounted(el: HTMLElement) {
    const fn = new Function('return ' + JSON.parse(el.dataset.throttleFn || '{}').throttled)();
    el.removeEventListener('click', fn);
    delete el.dataset.throttleFn;
  }
};

export { throttleFn };
指令使用
javascript 复制代码
<template>
  <div class="flex flex-wrap gap-4 p-6">
    <!-- 基础节流按钮 -->
    <button 
      v-throttle="handleClick"
      class="px-6 py-2.5 bg-blue-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      节流按钮
    </button>

    <!-- 500ms节流按钮 -->
    <button 
      v-throttle:500="handleClick"
      class="px-6 py-2.5 bg-green-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-green-700 hover:shadow-lg focus:bg-green-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-green-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      500ms节流按钮
    </button>

    <!-- 立即执行节流按钮 -->
    <button 
      v-throttle.immediate="handleClick"
      class="px-6 py-2.5 bg-purple-600 text-white font-medium text-sm leading-tight uppercase rounded shadow-md hover:bg-purple-700 hover:shadow-lg focus:bg-purple-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-purple-800 active:shadow-lg transition duration-150 ease-in-out"
    >
      立即执行节流按钮
    </button>
  </div>
</template>

<script setup lang="ts">
const handleClick = () => {
  console.log('按钮被点击');
};
</script>

长按指令

指令编写
javascript 复制代码
src\utils\directive\modules\longPress.ts
javascript 复制代码
// modules/longPress.ts

interface LongPressBinding {
  value: Function;
  arg?: number; // 长按时间,单位毫秒,默认500ms
  modifiers?: {
    stop?: boolean; // 是否阻止事件冒泡
    prevent?: boolean; // 是否阻止默认事件
  };
}

export const longPress = {
  mounted(el: HTMLElement, binding: LongPressBinding) {
    if (typeof binding.value !== 'function') {
      console.warn('v-longPress 指令需要一个函数作为值');
      return;
    }

    let pressTimer: NodeJS.Timeout | null = null;
    let startTime: number = 0;
    const duration = Number(binding.arg) || 500;
    const isStop = binding.modifiers?.stop || false;
    const isPrevent = binding.modifiers?.prevent || false;

    const start = (e: MouseEvent | TouchEvent) => {
      if (isPrevent) {
        e.preventDefault();
      }
      if (isStop) {
        e.stopPropagation();
      }

      startTime = Date.now();
      
      pressTimer = setTimeout(() => {
        binding.value(e);
      }, duration);
    };

    const cancel = () => {
      if (pressTimer) {
        clearTimeout(pressTimer);
        pressTimer = null;
      }
    };

    const end = (e: MouseEvent | TouchEvent) => {
      const endTime = Date.now();
      const timeDiff = endTime - startTime;
      
      // 如果按住时间小于设定时间,则视为普通点击
      if (timeDiff < duration && pressTimer) {
        cancel();
        return;
      }
      
      cancel();
    };

    // 添加事件监听器
    el.addEventListener('mousedown', start);
    el.addEventListener('touchstart', start);
    el.addEventListener('mouseup', end);
    el.addEventListener('touchend', end);
    el.addEventListener('mouseleave', cancel);
    el.addEventListener('touchcancel', cancel);

    // 保存清理函数到元素上
    (el as any)._longPressCleanup = () => {
      el.removeEventListener('mousedown', start);
      el.removeEventListener('touchstart', start);
      el.removeEventListener('mouseup', end);
      el.removeEventListener('touchend', end);
      el.removeEventListener('mouseleave', cancel);
      el.removeEventListener('touchcancel', cancel);
      cancel();
    };
  },

  unmounted(el: HTMLElement) {
    // 清理事件监听器
    if ((el as any)._longPressCleanup) {
      (el as any)._longPressCleanup();
    }
  }
};
指令使用

测试一下我们的按钮指令,效果ok

javascript 复制代码
<template>
  <div class="p-6 space-y-4">
    <!-- 基础用法,默认500ms -->
    <button 
      v-longPress="handleLongPress"
      class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
    >
      长按按钮
    </button>

    <!-- 自定义长按时间 -->
    <button 
      v-longPress:1000="handleLongPress"
      class="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"
    >
      1秒长按按钮
    </button>

    <!-- 阻止事件冒泡 -->
    <button 
      v-longPress.stop="handleLongPress"
      class="px-4 py-2 bg-purple-500 text-white rounded hover:bg-purple-600"
    >
      阻止冒泡长按按钮
    </button>

    <!-- 阻止默认事件 -->
    <button 
      v-longPress.prevent="handleLongPress"
      class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
    >
      阻止默认事件长按按钮
    </button>
  </div>
</template>
<script setup lang="ts">
const handleLongPress = (event: MouseEvent | TouchEvent) => {
  console.log('长按触发', new Date().toISOString());
  // 这里可以添加你的长按处理逻辑
  if (event instanceof MouseEvent) {
    console.log('鼠标事件');
  } else {
    console.log('触摸事件');
  }
};
</script>
相关推荐
Tzarevich2 小时前
算法效率的核心:时间复杂度与空间复杂度
javascript·算法
傻啦嘿哟2 小时前
Python在Excel中创建与优化数据透视表的完整指南
java·前端·spring
拜晨2 小时前
用流式 JSON 解析让 AI 产品交互提前
前端·javascript
浩男孩2 小时前
🍀vue3 + Typescript +Tdesign + HiPrint 打印下载解决方案
前端
andwhataboutit?2 小时前
LANGGRAPH
java·服务器·前端
无限大62 小时前
为什么"Web3"是下一代互联网?——从中心化到去中心化的转变
前端·后端·程序员
cypking2 小时前
CSS 常用特效汇总
前端·css
程序媛小鱼2 小时前
openlayers撤销与恢复
前端·js
Thomas游戏开发2 小时前
如何基于全免费素材,0美术成本开发游戏
前端·后端·架构