Appearance
Vue 常用自定义指令
Vue 3 提供了强大的自定义指令功能,可以让我们对 DOM 元素进行底层操作。以下是几个常用的自定义指令示例:
v-copy
复制指令,用于一键复制文本内容。
example
复制指令 (v-copy)
这是手动添加图标的示例
这段文本显示的内容和复制的内容不同
使用组合式API实现的复制功能
特点:
- 自动添加复制图标到文本旁边
- 提供复制成功的反馈提示
- 支持自定义复制文本和提示信息
- 可指定要复制内容的目标元素
使用示例:
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>
复制指令选项:
选项 | 类型 | 默认值 | 说明 |
---|---|---|---|
text | string | 元素文本内容 | 自定义要复制的文本内容 |
success | string | '复制成功' | 自定义复制成功的提示消息 |
target | string | - | 目标元素选择器,指定要复制文本的元素 |
showIcon | boolean | true | 是否显示复制图标 |
iconHtml | string | SVG图标 | 自定义图标的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)
拖拽我
特点:
- 使用 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
<!-- 单个权限 -->
<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)
当前手机号: 暂无输入
特点:
- 自动格式化手机号为 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
}
}
}
使用说明
- 在组件中引入需要的指令:
javascript
// 方式1:按需引入
import { copy } from '@/record/examples/copy'
import { draggable } from '@/record/examples/draggable'
// 方式2:注册指令
const vCopy = copy
- 在模板中使用指令:
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(() => {
// 在组件卸载时执行
})
}
注意事项
- 所有指令都使用了 Vue 3 的 Composition API 和最新的生命周期钩子
- 指令内部会自动处理事件监听器的添加和移除
- 使用 TypeScript 提供了完整的类型支持
- 所有指令都支持响应式更新
最佳实践
- 在组件中按需引入需要的指令
- 优先考虑使用组合式API实现,更加灵活
- 注意指令的性能影响,避免过度使用
- 在组件卸载时,确保所有事件监听器被正确清理