🔥 Vue3 + TS 实现长按指令 v-longPress:优雅解决移动端/PC端长按交互需求
在前端开发中,长按(Long Press)是高频交互场景(如移动端删除操作、PC端右键菜单、批量操作触发等)。原生HTML没有直接的长按事件,通常需要通过 mousedown/touchstart 结合定时器实现。本文将教你基于 Vue3 + TypeScript 实现一个功能完善、体验友好、跨端兼容 的 v-longPress 自定义指令,支持自定义长按时长、触发回调、取消触发等特性,开箱即用。

🎯 指令核心特性
- ✅ 跨端兼容:同时支持PC端(鼠标长按)和移动端(触摸长按)
- ✅ 自定义配置:支持自定义长按时长、触发回调、取消回调
- ✅ 防误触优化:按下后移动超过阈值自动取消长按
- ✅ 完整 TypeScript 类型定义,开发提示友好
- ✅ 支持指令参数动态更新,适配动态业务场景
- ✅ 自动清理定时器/事件监听,无内存泄漏
- ✅ 支持阻止默认行为/冒泡,适配复杂交互场景
📁 完整代码实现(v-longPress.ts)
typescript
// directives/v-longPress.ts
import type { ObjectDirective, DirectiveBinding, App } from 'vue'
/**
* 长按指令配置接口
*/
export interface LongPressOptions {
/** 长按触发时长(ms),默认500ms */
duration?: number
/** 长按触发的回调函数 */
handler: (e: MouseEvent | TouchEvent) => void
/** 长按取消的回调函数(可选) */
cancelHandler?: (e: MouseEvent | TouchEvent) => void
/** 移动阈值(px),超过则取消长按,默认5px */
moveThreshold?: number
/** 是否阻止默认行为,默认true */
preventDefault?: boolean
/** 是否阻止事件冒泡,默认false */
stopPropagation?: boolean
}
/**
* 扩展元素属性,存储长按相关状态
*/
interface LongPressElement extends HTMLElement {
_longPress?: {
options: LongPressOptions
timer: number | null // 长按定时器
startX: number // 按下时X坐标
startY: number // 按下时Y坐标
isPressing: boolean // 是否正在长按中
// 事件处理函数(绑定this,便于移除监听)
mouseDownHandler: (e: MouseEvent) => void
touchStartHandler: (e: TouchEvent) => void
mouseUpHandler: (e: MouseEvent) => void
touchEndHandler: (e: TouchEvent) => void
mouseMoveHandler: (e: MouseEvent) => void
touchMoveHandler: (e: TouchEvent) => void
mouseLeaveHandler: (e: MouseEvent) => void
}
}
/**
* 默认配置
*/
const DEFAULT_OPTIONS: Omit<LongPressOptions, 'handler'> = {
duration: 500,
moveThreshold: 5,
preventDefault: true,
stopPropagation: false
}
/**
* 计算两点之间的距离
* @param x1 起始X坐标
* @param y1 起始Y坐标
* @param x2 结束X坐标
* @param y2 结束Y坐标
* @returns 距离(px)
*/
const calculateDistance = (x1: number, y1: number, x2: number, y2: number): number => {
return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2))
}
/**
* 清除长按相关状态和定时器
* @param el 目标元素
* @param e 触发事件(可选,用于取消回调)
*/
const clearLongPressState = (el: LongPressElement, e?: MouseEvent | TouchEvent) => {
const longPressData = el._longPress
if (!longPressData) return
// 清除定时器
if (longPressData.timer) {
clearTimeout(longPressData.timer)
longPressData.timer = null
}
// 触发取消回调
if (longPressData.isPressing && longPressData.options.cancelHandler && e) {
longPressData.options.cancelHandler(e)
}
// 重置状态
longPressData.isPressing = false
}
/**
* 绑定事件监听
* @param el 目标元素
*/
const bindEvents = (el: LongPressElement) => {
const longPressData = el._longPress
if (!longPressData) return
// PC端事件
el.addEventListener('mousedown', longPressData.mouseDownHandler)
el.addEventListener('mouseup', longPressData.mouseUpHandler)
el.addEventListener('mousemove', longPressData.mouseMoveHandler)
el.addEventListener('mouseleave', longPressData.mouseLeaveHandler)
// 移动端事件
el.addEventListener('touchstart', longPressData.touchStartHandler)
el.addEventListener('touchend', longPressData.touchEndHandler)
el.addEventListener('touchmove', longPressData.touchMoveHandler)
}
/**
* 解绑事件监听
* @param el 目标元素
*/
const unbindEvents = (el: LongPressElement) => {
const longPressData = el._longPress
if (!longPressData) return
// PC端事件
el.removeEventListener('mousedown', longPressData.mouseDownHandler)
el.removeEventListener('mouseup', longPressData.mouseUpHandler)
el.removeEventListener('mousemove', longPressData.mouseMoveHandler)
el.removeEventListener('mouseleave', longPressData.mouseLeaveHandler)
// 移动端事件
el.removeEventListener('touchstart', longPressData.touchStartHandler)
el.removeEventListener('touchend', longPressData.touchEndHandler)
el.removeEventListener('touchmove', longPressData.touchMoveHandler)
}
/**
* 初始化长按事件处理函数
* @param el 目标元素
*/
const initHandlers = (el: LongPressElement) => {
const longPressData = el._longPress
if (!longPressData) return
const { options } = longPressData
// PC端鼠标按下
longPressData.mouseDownHandler = (e: MouseEvent) => {
// 阻止右键菜单(仅左键触发)
if (e.button !== 0) return
// 阻止默认行为/冒泡
if (options.preventDefault) e.preventDefault()
if (options.stopPropagation) e.stopPropagation()
// 记录起始坐标
longPressData.startX = e.clientX
longPressData.startY = e.clientY
longPressData.isPressing = true
// 设置长按定时器
longPressData.timer = window.setTimeout(() => {
if (longPressData.isPressing) {
options.handler(e)
longPressData.isPressing = false
}
}, options.duration!) as unknown as number
}
// 移动端触摸开始
longPressData.touchStartHandler = (e: TouchEvent) => {
// 阻止默认行为/冒泡
if (options.preventDefault) e.preventDefault()
if (options.stopPropagation) e.stopPropagation()
// 记录起始坐标(取第一个触摸点)
const touch = e.touches[0]
longPressData.startX = touch.clientX
longPressData.startY = touch.clientY
longPressData.isPressing = true
// 设置长按定时器
longPressData.timer = window.setTimeout(() => {
if (longPressData.isPressing) {
options.handler(e)
longPressData.isPressing = false
}
}, options.duration!) as unknown as number
}
// PC端鼠标抬起
longPressData.mouseUpHandler = (e: MouseEvent) => {
clearLongPressState(el, e)
}
// 移动端触摸结束
longPressData.touchEndHandler = (e: TouchEvent) => {
clearLongPressState(el, e)
}
// PC端鼠标移动
longPressData.mouseMoveHandler = (e: MouseEvent) => {
if (!longPressData.isPressing) return
// 计算移动距离,超过阈值取消长按
const distance = calculateDistance(
longPressData.startX,
longPressData.startY,
e.clientX,
e.clientY
)
if (distance > options.moveThreshold!) {
clearLongPressState(el, e)
}
}
// 移动端触摸移动
longPressData.touchMoveHandler = (e: TouchEvent) => {
if (!longPressData.isPressing) return
// 计算移动距离,超过阈值取消长按
const touch = e.touches[0]
const distance = calculateDistance(
longPressData.startX,
longPressData.startY,
touch.clientX,
touch.clientY
)
if (distance > options.moveThreshold!) {
clearLongPressState(el, e)
}
}
// PC端鼠标离开元素
longPressData.mouseLeaveHandler = (e: MouseEvent) => {
clearLongPressState(el, e)
}
}
/**
* 清理所有长按相关资源
* @param el 目标元素
*/
const cleanup = (el: LongPressElement) => {
// 清除状态和定时器
clearLongPressState(el)
// 解绑事件
unbindEvents(el)
// 删除扩展属性
delete el._longPress
}
/**
* v-longPress 自定义指令实现
*/
export const longPressDirective: ObjectDirective<LongPressElement, LongPressOptions | (() => void)> = {
/**
* 指令挂载时初始化
*/
mounted(el: LongPressElement, binding: DirectiveBinding<LongPressOptions | (() => void)>) {
// 1. 解析指令参数
let options: LongPressOptions = {
...DEFAULT_OPTIONS,
handler: () => {}
}
if (typeof binding.value === 'function') {
// 直接传函数:作为长按触发回调,使用默认配置
options.handler = binding.value
} else if (typeof binding.value === 'object' && binding.value !== null) {
// 传对象:合并配置
options = {
...DEFAULT_OPTIONS,
...binding.value
}
}
// 校验必填项
if (typeof options.handler !== 'function') {
console.warn('[v-longPress] 必须指定有效的长按回调函数')
return
}
// 2. 初始化长按状态
el._longPress = {
options,
timer: null,
startX: 0,
startY: 0,
isPressing: false,
mouseDownHandler: () => {},
touchStartHandler: () => {},
mouseUpHandler: () => {},
touchEndHandler: () => {},
mouseMoveHandler: () => {},
touchMoveHandler: () => {},
mouseLeaveHandler: () => {}
}
// 3. 初始化事件处理函数
initHandlers(el)
// 4. 绑定事件监听
bindEvents(el)
// 5. 添加长按样式提示
el.style.cursor = 'pointer'
},
/**
* 指令更新时处理参数变化
*/
updated(el: LongPressElement, binding: DirectiveBinding<LongPressOptions | (() => void)>) {
// 先清理旧配置
cleanup(el)
// 重新初始化
this.mounted(el, binding)
},
/**
* 指令卸载时清理资源
*/
unmounted(el: LongPressElement) {
cleanup(el)
}
}
/**
* 全局注册长按指令
* @param app Vue应用实例
* @param directiveName 指令名称,默认longPress
*/
export const setupLongPressDirective = (app: App, directiveName: string = 'longPress') => {
app.directive(directiveName, longPressDirective)
}
// TypeScript 类型扩展
declare module 'vue' {
export interface ComponentCustomDirectives {
longPress: typeof longPressDirective
}
}
🚀 快速上手
1. 全局注册指令(main.ts)
在 Vue3 入口文件中注册指令,全局可用:
typescript
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { setupLongPressDirective } from './directives/v-longPress'
const app = createApp(App)
// 注册长按指令(默认名称v-longPress)
setupLongPressDirective(app)
app.mount('#app')
2. 基础使用(直接传回调)
最简单的用法:直接传递长按触发的回调函数,使用默认配置(500ms长按时长):
vue
<template>
<!-- PC端鼠标长按/移动端触摸长按触发 -->
<button v-longPress="handleLongPress">
长按500ms触发
</button>
</template>
<script setup lang="ts">
// 长按触发回调
const handleLongPress = (e: MouseEvent | TouchEvent) => {
console.log('长按触发', e)
alert('长按成功!')
}
</script>
3. 高级使用(自定义配置)
通过对象参数配置完整的长按规则,支持自定义时长、移动阈值、取消回调等:
vue
<template>
<div class="card">
<div
class="delete-btn"
v-longPress="{
duration: 800, // 长按800ms触发
moveThreshold: 10, // 移动超过10px取消
handler: handleDelete, // 长按触发回调
cancelHandler: handleCancel, // 长按取消回调
preventDefault: true, // 阻止默认行为
stopPropagation: true // 阻止事件冒泡
}"
>
长按删除(800ms)
</div>
</div>
</template>
<script setup lang="ts">
// 长按删除回调
const handleDelete = () => {
console.log('执行删除操作')
if (confirm('确定要删除这条数据吗?')) {
// 实际业务逻辑:调用删除接口
alert('删除成功!')
}
}
// 长按取消回调
const handleCancel = () => {
console.log('长按取消')
// 可添加取消提示,如:
// ElMessage.info('已取消删除')
}
</script>
<style scoped>
.card {
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
width: 300px;
margin: 20px;
}
.delete-btn {
color: #ff4d4f;
cursor: pointer;
padding: 8px 16px;
border: 1px solid #ff4d4f;
border-radius: 4px;
display: inline-block;
}
.delete-btn:hover {
background: #fff2f2;
}
</style>
4. 动态配置长按指令
适配动态变化的长按配置(如根据不同状态调整长按时长):
vue
<template>
<div>
<div>
<label>长按时长(ms):</label>
<input
type="number"
v-model.number="pressDuration"
min="100"
max="2000"
step="100"
/>
</div>
<button
v-longPress="{
duration: pressDuration,
handler: handleDynamicLongPress,
cancelHandler: handleDynamicCancel
}"
>
动态配置长按按钮
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 动态长按时长
const pressDuration = ref(500)
// 动态长按回调
const handleDynamicLongPress = () => {
alert(`长按${pressDuration.value}ms触发成功!`)
}
// 动态取消回调
const handleDynamicCancel = () => {
console.log(`长按${pressDuration.value}ms取消`)
}
</script>
5. 结合UI库实现长按菜单
实际业务中常结合下拉菜单实现长按操作(以Element Plus为例):
vue
<template>
<div>
<el-button
ref="btnRef"
v-longPress="{
handler: openContextMenu,
duration: 600
}"
>
长按打开操作菜单
</el-button>
<!-- 自定义上下文菜单 -->
<el-dropdown
ref="dropdownRef"
:visible="menuVisible"
@visible-change="menuVisible = false"
>
<el-dropdown-menu>
<el-dropdown-item @click="handleEdit">编辑</el-dropdown-item>
<el-dropdown-item @click="handleCopy">复制</el-dropdown-item>
<el-dropdown-item @click="handleDelete" divided>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const btnRef = ref<HTMLButtonElement>(null)
const dropdownRef = ref()
const menuVisible = ref(false)
// 打开上下文菜单
const openContextMenu = () => {
if (btnRef.value && dropdownRef.value) {
const rect = btnRef.value.getBoundingClientRect()
// 设置菜单位置
dropdownRef.value.$el.style.position = 'absolute'
dropdownRef.value.$el.style.top = `${rect.bottom + 10}px`
dropdownRef.value.$el.style.left = `${rect.left}px`
menuVisible.value = true
}
}
// 编辑操作
const handleEdit = () => {
ElMessage.info('执行编辑操作')
menuVisible.value = false
}
// 复制操作
const handleCopy = () => {
ElMessage.success('复制成功')
menuVisible.value = false
}
// 删除操作
const handleDelete = () => {
ElMessage.warning('执行删除操作')
menuVisible.value = false
}
</script>
🔧 核心知识点解析
1. 长按实现原理
长按的核心逻辑是**"按下计时 + 抬起/移动取消"**:
- 按下(
mousedown/touchstart):记录起始坐标,启动定时器,达到指定时长触发回调。 - 抬起/离开(
mouseup/touchend/mouseleave):清除定时器,取消长按。 - 移动(
mousemove/touchmove):计算移动距离,超过阈值自动取消长按,防止误触。
2. 跨端兼容处理
- PC端 :监听
mousedown/mouseup/mousemove/mouseleave事件。 - 移动端 :监听
touchstart/touchend/touchmove事件。 - 统一的坐标计算逻辑,兼容鼠标和触摸事件的坐标获取方式。
3. 防误触优化
通过 moveThreshold 移动阈值实现防误触:
- 默认阈值为5px,按下后移动超过该距离自动取消长按。
- 计算两点间距离使用勾股定理:distance=(x2−x1)2+(y2−y1)2distance = \sqrt{(x2-x1)^2 + (y2-y1)^2}distance=(x2−x1)2+(y2−y1)2 。
4. 内存泄漏防护
unmounted钩子中清理定时器、解绑所有事件监听。updated钩子中先清理旧配置,再初始化新配置。- 使用元素扩展属性存储状态,卸载时删除属性释放内存。
5. TypeScript 类型优化
- 定义
LongPressOptions接口,明确配置项类型。 - 扩展
HTMLElement类型,添加长按状态属性。 - 支持两种参数类型(函数/对象),类型推导自动适配。
📋 配置项说明
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| duration | number | 500 | 长按触发时长,单位ms |
| handler | (e: MouseEvent | TouchEvent) => void | - | 长按触发的回调函数(必填) |
| cancelHandler | (e: MouseEvent | TouchEvent) => void | - | 长按取消的回调函数(可选) |
| moveThreshold | number | 5 | 移动阈值,超过则取消长按,单位px |
| preventDefault | boolean | true | 是否阻止默认行为(如移动端长按弹出菜单) |
| stopPropagation | boolean | false | 是否阻止事件冒泡 |
🎯 常见使用场景
场景1:移动端列表项长按操作
vue
<template>
<div class="list">
<div
class="list-item"
v-for="item in list"
:key="item.id"
v-longPress="{
duration: 600,
handler: () => showActionSheet(item),
cancelHandler: () => hideActionSheet()
}"
>
{{ item.name }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref([
{ id: 1, name: '微信' },
{ id: 2, name: '支付宝' },
{ id: 3, name: '抖音' }
])
// 显示操作面板
const showActionSheet = (item: any) => {
console.log('长按触发', item)
// 实际开发中可调用移动端ActionSheet组件
alert(`长按${item.name},显示操作菜单`)
}
// 隐藏操作面板
const hideActionSheet = () => {
console.log('长按取消')
}
</script>
<style scoped>
.list {
width: 300px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.list-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
}
.list-item:last-child {
border-bottom: none;
}
.list-item:hover {
background: #f5f5f5;
}
</style>
场景2:PC端长按批量选择
vue
<template>
<div class="table-container">
<el-table
:data="tableData"
@row-click="handleRowClick"
>
<el-table-column
label="名称"
prop="name"
>
<template #default="scope">
<div
v-longPress="{
handler: () => toggleSelect(scope.row),
duration: 400
}"
>
{{ scope.row.name }}
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="selected-count" v-if="selectedRows.length">
已选择 {{ selectedRows.length }} 条数据
<el-button size="small" @click="selectedRows = []">清空选择</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 模拟表格数据
const tableData = ref([
{ id: 1, name: '数据1' },
{ id: 2, name: '数据2' },
{ id: 3, name: '数据3' }
])
// 选中的行
const selectedRows = ref<any[]>([])
// 长按切换选择状态
const toggleSelect = (row: any) => {
const index = selectedRows.value.findIndex(item => item.id === row.id)
if (index > -1) {
selectedRows.value.splice(index, 1)
} else {
selectedRows.value.push(row)
}
}
// 点击行(取消批量选择)
const handleRowClick = (row: any) => {
selectedRows.value = []
}
// 编辑操作
const handleEdit = (row: any) => {
console.log('编辑', row)
}
</script>
<style scoped>
.table-container {
width: 500px;
margin: 20px;
}
.selected-count {
margin-top: 10px;
padding: 8px;
background: #f5f5f5;
border-radius: 4px;
}
</style>
🚨 注意事项
- 移动端默认行为 :长按会触发浏览器默认菜单(如复制、保存图片),需设置
preventDefault: true阻止。 - 触摸事件穿透 :移动端需注意事件穿透问题,可结合
pointer-events: none处理。 - 性能优化:避免在长按回调中执行复杂计算,建议使用防抖/节流。
- 兼容性 :
- PC端:所有现代浏览器均支持。
- 移动端:iOS Safari/Android Chrome 均支持,低版本安卓需测试。
- 右键菜单冲突 :PC端长按左键触发,避免与右键菜单冲突,可通过
e.button !== 0过滤右键。
📌 总结
本文实现的 v-longPress 指令具备以下核心优势:
- 跨端兼容:同时支持PC端和移动端,一套代码适配多端。
- 体验优秀:内置防误触机制,支持自定义移动阈值,避免误触发。
- 配置灵活:支持自定义长按时长、回调函数、事件行为,适配各种业务场景。
- 类型安全:基于TypeScript开发,类型提示完善,减少开发错误。
- 性能优异:自动清理定时器和事件监听,无内存泄漏问题。
这个指令可以直接集成到你的Vue3项目中,解决各种长按交互需求。如果需要进一步扩展,可以在此基础上增加:
- 支持长按进度条显示
- 支持多段长按(短按/中按/长按触发不同操作)
- 支持长按拖拽
- 支持自定义长按样式(如按压反馈)
希望这篇文章对你有帮助,欢迎点赞、收藏、评论交流!