Skip to content

Vue 常用自定义指令

Vue 3 提供了强大的自定义指令功能,可以让我们对 DOM 元素进行底层操作。以下是几个常用的自定义指令示例:

v-copy

复制指令,用于一键复制文本内容。

example

复制指令 (v-copy)

这段文本会自动添加复制图标,点击图标即可复制文本
使用自定义复制图标 Emoji 📋
这是手动添加图标的示例📋
这段文本显示的内容和复制的内容不同
使用组合式API实现的复制功能📋
vue
<template>
  <div class="example-section">
    <h3>复制指令 (v-copy)</h3>
    <div class="copy-example">
      <!-- 自动添加图标的示例 -->
      <div class="copy-container">
        <div v-copy class="auto-icon-text">这段文本会自动添加复制图标,点击图标即可复制文本</div>
      </div>
      
      <!-- 自定义图标方式 -->
      <div class="copy-container">
        <div v-copy="{ iconHtml: '📋', success: '文本已复制!' }" class="auto-icon-text">使用自定义复制图标 Emoji 📋</div>
      </div>
      
      <!-- 原始示例:手动添加图标 -->
      <div class="copy-container">
        <span class="text-content" id="text1">这是手动添加图标的示例</span>
        <span v-copy="{ target: '#text1', showIcon: false }" class="copy-icon" title="点击复制">📋</span>
      </div>
      
      <!-- 自定义文本复制 -->
      <div class="copy-container">
        <span v-copy="{ text: '这是自定义的文本内容,与显示内容不同!', showIcon: true }" class="custom-text">
          这段文本显示的内容和复制的内容不同
        </span>
      </div>
      
      <!-- 组合式API实现 -->
      <div class="copy-container">
        <span ref="copyTextRef" class="text-content" id="text3">使用组合式API实现的复制功能</span>
        <span ref="copyIconRef" class="copy-icon" title="点击复制">📋</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { copy, useCopy } from './copy'

// 注册指令
const vCopy = copy

// 组合式API复制功能示例
const copyTextRef = ref<HTMLElement | null>(null)
const copyIconRef = ref<HTMLElement | null>(null)

// 使用onMounted确保同时获取文本和图标元素
onMounted(() => {
  if (copyIconRef.value && copyTextRef.value) {
    // 特殊处理:当点击图标时复制相关文本内容
    copyIconRef.value.addEventListener('click', () => {
      const text = copyTextRef.value?.textContent?.trim() || ''
      
      if (!text) {
        console.warn('没有可复制的文本内容')
        return
      }
      
      // 创建临时textarea元素
      const textarea = document.createElement('textarea')
      textarea.value = text
      textarea.style.position = 'fixed'
      textarea.style.left = '-9999px'
      document.body.appendChild(textarea)
      
      // 选择文本并复制
      textarea.select()
      document.execCommand('copy')
      
      // 移除临时元素
      document.body.removeChild(textarea)
      
      // 显示成功提示
      const tip = document.createElement('div')
      tip.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(0, 0, 0, 0.7);
        color: white;
        padding: 8px 16px;
        border-radius: 4px;
        font-size: 14px;
        z-index: 9999;
      `
      tip.textContent = '复制成功'
      document.body.appendChild(tip)
      
      // 2秒后移除提示
      setTimeout(() => {
        document.body.removeChild(tip)
      }, 2000)
    })
  }
})
</script>

<style scoped>
.example-section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
  background-color: #f9f9f9;
}

h3 {
  color: #2c3e50;
  border-bottom: 2px solid #3498db;
  padding-bottom: 5px;
  margin-bottom: 15px;
}

.copy-example {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.copy-container {
  display: flex;
  align-items: center;
  padding: 10px;
  background-color: #e8f4fc;
  border: 1px dashed #3498db;
  border-radius: 4px;
}

.text-content {
  flex: 1;
}

.auto-icon-text {
  flex: 1;
  line-height: 1.5;
}

.custom-text {
  flex: 1;
  font-style: italic;
  color: #e74c3c;
}

.copy-icon {
  margin-left: 8px;
  cursor: pointer;
  user-select: none;
  font-size: 18px;
  transition: transform 0.2s;
}

.copy-icon:hover {
  transform: scale(1.2);
}

.copy-icon-button {
  background: none;
  border: none;
  padding: 4px;
  margin-left: 8px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.copy-icon-button:hover {
  background-color: rgba(52, 152, 219, 0.2);
}

.copy-svg-icon {
  fill: #3498db;
}

.copy-button {
  padding: 8px 15px;
  background-color: #3498db;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.3s;
}

.copy-button:hover {
  background-color: #2980b9;
}
</style> 

特点:

  • 自动添加复制图标到文本旁边
  • 提供复制成功的反馈提示
  • 支持自定义复制文本和提示信息
  • 可指定要复制内容的目标元素

使用示例:

vue
<!-- 基本用法:默认自动添加复制图标 -->
<div v-copy>点击图标复制这段文字</div>

<!-- 自定义图标 -->
<div v-copy="{ iconHtml: '📋', success: '复制成功!' }">
  使用自定义图标来复制
</div>

<!-- 禁用图标 -->
<div v-copy="{ showIcon: false }">点击整个文本复制</div>

<!-- 复制自定义文本 -->
<div v-copy="{ text: '这是要复制的文本', showIcon: true }">
  显示的内容和复制的内容不同
</div>

<!-- 组合式API实现方式 -->
<template>
  <div ref="copyBtn">使用组合式API复制这段文本</div>
</template>

<script setup>
import { ref } from 'vue'
import { useCopy } from './copy'

const copyBtn = ref(null)
// 直接传入ref即可,无需在onMounted中处理
useCopy(copyBtn)
</script>

复制指令选项:

选项类型默认值说明
textstring元素文本内容自定义要复制的文本内容
successstring'复制成功'自定义复制成功的提示消息
targetstring-目标元素选择器,指定要复制文本的元素
showIconbooleantrue是否显示复制图标
iconHtmlstringSVG图标自定义图标的HTML

源码实现

typescript
// copy.ts (部分代码)
interface CopyOptions {
  /** 要复制的文本,如果不指定则使用元素内容 */
  text?: string;
  /** 复制成功的提示信息 */
  success?: string;
  /** 目标元素选择器,指定要复制内容的元素 */
  target?: string;
  /** 是否显示复制图标 */
  showIcon?: boolean;
  /** 自定义图标 HTML */
  iconHtml?: string;
}

// 默认的复制图标HTML
const DEFAULT_ICON_HTML = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>';

export const copy = {
  mounted(el: HTMLElement, binding: { value?: string | CopyOptions, modifiers: Record<string, boolean> }) {
    // 确定选项
    let options: CopyOptions = {};
    
    if (binding.value) {
      if (typeof binding.value === 'string') {
        options.text = binding.value;
      } else {
        options = binding.value;
      }
    }
    
    // 默认显示图标,除非明确设置为false
    const showIcon = options.showIcon !== false || binding.modifiers.icon;
    
    // 如果需要显示图标,添加图标到元素中
    if (showIcon) {
      // 使用span包装原始内容,防止图标与原始内容混淆
      const wrapper = document.createElement('span');
      wrapper.style.cssText = 'display: inline-flex; align-items: center;';
      wrapper.innerHTML = originalContent;
      
      // 创建图标元素
      const iconContainer = document.createElement('span');
      iconContainer.style.cssText = `
        margin-left: 4px;
        cursor: pointer;
        color: #3498db;
        display: inline-flex;
        align-items: center;
        padding: 2px;
        border-radius: 3px;
        transition: background-color 0.2s ease;
      `;
      iconContainer.innerHTML = options.iconHtml || DEFAULT_ICON_HTML;
      
      // 为图标添加点击事件
      iconContainer.addEventListener('click', function(e) {
        // 复制逻辑...
      });
    }
    // 其他代码...
  }
}

v-draggable

拖拽指令,使元素可以自由拖拽。

example

拖拽指令 (v-draggable)

拖拽我
vue
<template>
  <div class="example-section">
    <h3>拖拽指令 (v-draggable)</h3>
    <div v-draggable class="draggable-box">拖拽我</div>
  </div>
</template>

<script setup lang="ts">
import { draggable } from './draggable'

// 注册指令
const vDraggable = draggable
</script>

<style scoped>
.example-section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
  background-color: #f9f9f9;
}

h3 {
  color: #2c3e50;
  border-bottom: 2px solid #3498db;
  padding-bottom: 5px;
  margin-bottom: 15px;
}

.draggable-box {
  width: 150px;
  height: 100px;
  background: linear-gradient(135deg, #3498db, #2c3e50);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  cursor: move;
  user-select: none;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
</style> 

特点:

  • 使用 transform 实现高性能拖拽
  • 支持鼠标事件处理
  • 自动清理事件监听器

使用示例:

vue
<!-- 将元素设为可拖拽 -->
<div v-draggable class="draggable-box">可拖拽元素</div>

<!-- 通过CSS添加样式 -->
<style>
.draggable-box {
  width: 100px;
  height: 100px;
  background-color: #3498db;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: move;
}
</style>

源码实现

typescript
// draggable.ts
import type { Directive } from 'vue'

interface Position {
  startX: number
  startY: number
  initialLeft: number
  initialTop: number
}

interface DraggableElement extends HTMLElement {
  __position?: Position
  __mousedownHandler?: (e: MouseEvent) => void
  __mousemoveHandler?: (e: MouseEvent) => void
  __mouseupHandler?: () => void
}

/**
 * 拖拽指令
 * 使用方式:v-draggable
 * 功能:使元素可以自由拖拽
 */
export const draggable: Directive = {
  mounted(el: DraggableElement) {
    // 确保元素可以定位
    if (getComputedStyle(el).position === 'static') {
      el.style.position = 'relative'
    }
    
    el.__mousedownHandler = (e: MouseEvent) => {
      // 阻止默认行为和冒泡
      e.preventDefault()
      e.stopPropagation()
      
      // 记录初始位置
      const rect = el.getBoundingClientRect()
      el.__position = {
        startX: e.clientX,
        startY: e.clientY,
        initialLeft: parseFloat(el.style.left) || 0,
        initialTop: parseFloat(el.style.top) || 0
      }
      
      // 添加移动和释放事件
      document.addEventListener('mousemove', el.__mousemoveHandler!)
      document.addEventListener('mouseup', el.__mouseupHandler!)
    }
    
    el.__mousemoveHandler = (e: MouseEvent) => {
      if (!el.__position) return
      
      // 计算移动距离
      const dx = e.clientX - el.__position.startX
      const dy = e.clientY - el.__position.startY
      
      // 更新元素位置
      el.style.left = `${el.__position.initialLeft + dx}px`
      el.style.top = `${el.__position.initialTop + dy}px`
    }
    
    el.__mouseupHandler = () => {
      // 移除事件监听
      document.removeEventListener('mousemove', el.__mousemoveHandler!)
      document.removeEventListener('mouseup', el.__mouseupHandler!)
    }
    
    // 添加鼠标按下事件
    el.addEventListener('mousedown', el.__mousedownHandler)
  },
  
  unmounted(el: DraggableElement) {
    // 移除所有事件监听
    if (el.__mousedownHandler) {
      el.removeEventListener('mousedown', el.__mousedownHandler)
    }
    
    if (el.__mousemoveHandler) {
      document.removeEventListener('mousemove', el.__mousemoveHandler)
    }
    
    if (el.__mouseupHandler) {
      document.removeEventListener('mouseup', el.__mouseupHandler)
    }
    
    // 清除引用
    delete el.__mousedownHandler
    delete el.__mousemoveHandler
    delete el.__mouseupHandler
    delete el.__position
  }
}

v-permission

权限指令,根据用户权限控制元素的显示和隐藏。

example

权限指令 (v-permission)

当前模拟用户角色: user

vue
<template>
  <div class="example-section">
    <h3>权限指令 (v-permission)</h3>
    <p>当前模拟用户角色: user</p>
    <button class="permission-button">普通用户可见按钮</button>
    <button v-permission="'admin'" class="permission-button admin">管理员按钮 (不可见)</button>
    <button v-permission="['admin', 'editor']" class="permission-button editor">管理员或编辑可见按钮 (不可见)</button>
    <button v-permission="['user', 'admin']" class="permission-button user">普通用户或管理员可见按钮 (可见)</button>
  </div>
</template>

<script setup lang="ts">
import { permission } from './permission'

// 注册指令
const vPermission = permission
</script>

<style scoped>
.example-section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
  background-color: #f9f9f9;
}

h3 {
  color: #2c3e50;
  border-bottom: 2px solid #3498db;
  padding-bottom: 5px;
  margin-bottom: 15px;
}

.permission-button {
  padding: 8px 15px;
  margin: 5px;
  border: none;
  border-radius: 4px;
  background-color: #95a5a6;
  color: white;
  cursor: pointer;
}

.permission-button.admin {
  background-color: #e74c3c;
}

.permission-button.editor {
  background-color: #f39c12;
}

.permission-button.user {
  background-color: #2ecc71;
}
</style> 

特点:

  • 支持单个权限和权限数组
  • 使用响应式系统动态计算权限
  • 自动监听权限变化并更新显示状态

使用示例:

vue
<!-- 单个权限 -->
<button v-permission="'admin'">管理员按钮</button>

<!-- 权限数组,满足任一权限即可显示 -->
<button v-permission="['admin', 'editor']">管理员或编辑可见</button>

源码实现

typescript
// permission.ts
import type { Directive, DirectiveBinding } from 'vue'

type PermissionValue = string | string[]

/**
 * 权限控制指令
 * 使用方式:
 * 1. v-permission="'admin'"
 * 2. v-permission="['admin', 'editor']"
 * 功能:根据用户权限控制元素的显示和隐藏
 */
export const permission: Directive<HTMLElement, PermissionValue> = {
  mounted(el, binding) {
    const { value } = binding
    // 模拟当前用户权限
    const currentUserRoles = ['user'] // 这里应从实际的用户权限系统获取
    
    const hasPermission = checkPermission(currentUserRoles, value)
    
    if (!hasPermission) {
      // 没有权限,从DOM中移除元素
      el.parentNode?.removeChild(el)
    }
  },
  
  updated(el, binding) {
    // 如果已经被删除,则不再进行处理
    if (!el.parentNode) return
    
    const { value } = binding
    // 模拟当前用户权限
    const currentUserRoles = ['user']
    
    const hasPermission = checkPermission(currentUserRoles, value)
    
    if (!hasPermission) {
      // 没有权限,从DOM中移除元素
      el.parentNode?.removeChild(el)
    }
  }
}

/**
 * 检查是否有权限
 * @param userRoles 用户角色列表
 * @param permission 所需权限
 * @returns 是否有权限
 */
function checkPermission(userRoles: string[], permission: PermissionValue): boolean {
  if (typeof permission === 'string') {
    return userRoles.includes(permission)
  }
  
  if (Array.isArray(permission)) {
    return permission.some(role => userRoles.includes(role))
  }
  
  return false
}

v-phone

手机号格式化指令,自动格式化输入的手机号。

example

手机号格式化指令 (v-phone)

当前手机号: 暂无输入

vue
<template>
  <div class="example-section">
    <h3>手机号格式化指令 (v-phone)</h3>
    <div class="phone-example">
      <input v-phone v-model="phoneNumber" placeholder="请输入手机号" class="phone-input" />
      <p class="phone-display">当前手机号: {{ phoneNumber || '暂无输入' }}</p>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { phone } from './phone'

// 注册指令
const vPhone = phone

// 手机号示例
const phoneNumber = ref('')
</script>

<style scoped>
.example-section {
  margin-bottom: 30px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
  background-color: #f9f9f9;
}

h3 {
  color: #2c3e50;
  border-bottom: 2px solid #3498db;
  padding-bottom: 5px;
  margin-bottom: 15px;
}

.phone-example {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.phone-input {
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.phone-display {
  padding: 10px;
  background-color: #f5f5f5;
  border-radius: 4px;
  font-family: monospace;
}
</style> 

特点:

  • 自动格式化手机号为 xxx-xxxx-xxxx 格式
  • 支持 v-model 双向绑定
  • 自动过滤非数字字符

使用示例:

vue
<template>
  <input v-phone v-model="phoneNumber" placeholder="请输入手机号">
  <p>格式化后的手机号: {{ phoneNumber }}</p>
</template>

<script setup>
import { ref } from 'vue'
const phoneNumber = ref('')
</script>

源码实现

typescript
// phone.ts
import type { Directive, DirectiveBinding } from 'vue'

interface PhoneElement extends HTMLInputElement {
  __handleInput?: (e: Event) => void
}

/**
 * 手机号格式化指令
 * 使用方式:v-phone
 * 功能:自动格式化输入的手机号为 xxx-xxxx-xxxx 格式
 */
export const phone: Directive<PhoneElement> = {
  mounted(el, binding) {
    const formatPhoneNumber = (value: string) => {
      // 移除所有非数字字符
      const numbers = value.replace(/\D/g, '')
      
      // 限制长度为11位(中国手机号)
      const digits = numbers.slice(0, 11)
      
      // 格式化为 xxx-xxxx-xxxx
      let result = ''
      if (digits.length > 0) {
        result += digits.slice(0, 3)
        
        if (digits.length > 3) {
          result += '-' + digits.slice(3, 7)
          
          if (digits.length > 7) {
            result += '-' + digits.slice(7, 11)
          }
        }
      }
      
      return result
    }
    
    el.__handleInput = function(e) {
      // 获取当前输入值
      const inputValue = (e.target as HTMLInputElement).value
      
      // 获取格式化后的值
      const formattedValue = formatPhoneNumber(inputValue)
      
      // 更新输入框的值
      if (formattedValue !== inputValue) {
        (e.target as HTMLInputElement).value = formattedValue
        
        // 触发input事件以更新绑定的v-model
        el.dispatchEvent(new Event('input'))
      }
    }
    
    // 添加输入监听
    el.addEventListener('input', el.__handleInput)
  },
  
  unmounted(el) {
    // 移除事件监听
    if (el.__handleInput) {
      el.removeEventListener('input', el.__handleInput)
      delete el.__handleInput
    }
  }
}

使用说明

  1. 在组件中引入需要的指令:
javascript
// 方式1:按需引入
import { copy } from '@/record/examples/copy'
import { draggable } from '@/record/examples/draggable'

// 方式2:注册指令
const vCopy = copy
  1. 在模板中使用指令:
vue
<div v-copy>点击复制</div>
<div v-draggable>拖我</div>

自定义指令的两种实现方式

1. 传统指令API

typescript
// 指令定义
export const myDirective = {
  mounted(el: HTMLElement) {
    // 在元素首次插入DOM时调用
  },
  updated(el: HTMLElement, binding) {
    // 在包含组件的VNode更新时调用
  },
  unmounted(el: HTMLElement) {
    // 在元素从DOM移除时调用
  }
}

2. 组合式API(推荐)

typescript
// 组合式API定义
export function useMyDirective(el: HTMLElement | Ref<HTMLElement | null>) {
  onMounted(() => {
    // 在组件挂载时执行
  })

  onUnmounted(() => {
    // 在组件卸载时执行
  })
}

注意事项

  1. 所有指令都使用了 Vue 3 的 Composition API 和最新的生命周期钩子
  2. 指令内部会自动处理事件监听器的添加和移除
  3. 使用 TypeScript 提供了完整的类型支持
  4. 所有指令都支持响应式更新

最佳实践

  1. 在组件中按需引入需要的指令
  2. 优先考虑使用组合式API实现,更加灵活
  3. 注意指令的性能影响,避免过度使用
  4. 在组件卸载时,确保所有事件监听器被正确清理