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>