LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

一次搞懂前端拖拽方案

freeflydom
2025年4月12日 10:48 本文热度 123

1. 拖拽概述

拖拽交互在前端应用中无处不在:文件上传、列表排序、拖拽布局、可视化编辑器等。

前端实现拖拽的方式主要有两种:

  • 原生 HTML5 拖拽 API :浏览器内置的拖拽能力
  • 自定义拖拽 :基于鼠标/触摸事件实现的拖拽功能

每种方式都有其适用场景和优缺点,本文就来说说前端拖拽中的那些事儿。

2. 原生拖拽 API

浏览器中有些元素默认就是可拖拽的,比如选中的文本、图片、链接;也有些元素默认是可放置的,比如输入框默认也可以作为文本的可放置元素。

HTML5引入了原生拖拽API,为Web应用提供了标准化的拖拽交互支持。核心概念包括:

  • 可拖拽元素:通过设置 draggable="true" 属性使元素可拖拽

  • 拖拽事件:包括 dragstart、drag、dragend、dragenter、dragover、dragleave 和 drop

  • 数据传输对象:DataTransfer 接口用于在拖拽操作中存储和传递数据

2.1 基础用法

要使元素可拖拽,需要设置 draggable 属性:

<div draggable="true">我可以被拖拽</div>

拖放涉及到两种元素,一种是被拖拽元素(drag source,源对象),一种是放置区元素(drop target,目标对象)。如下图所示,鼠标长按拖拽A元素放置到B元素上,A元素即为源对象,B元素即为目标对象

2.2 拖拽事件

不同的对象产生不同的拖放事件。

触发对象事件名称说明
源对象dragstart源对象开始被拖动时触发

drag源对象被拖动过程中反复触发

dragend源对象拖动结束时触发
目标对象dragenter源对象开始进入目标对象范围内触发

dragover源对象在目标对象范围内移动时触发

dragleave源对象离开目标对象范围时触发

drop源对象在目标对象范围内被释放时触发

对于被拖拽元素,事件触发顺序是:

 graph LR
 
dragstart --> drag --> dragend

对于目标元素,事件触发的顺序是

 graph LR
 
dragenter --> dragover --> drop/dropleave

其中dragdragover会分别在源元素和目标元素反复触发。整个流程一定是dragstart第一个触发,dragend最后一个触发。

如果某个元素同时设置了dragoverdrop的监听,那么必须阻止dragover的默认行为,否则drop将不会被触发。

dropzone.addEventListener('dragover', (e) => {
    // 阻止默认行为以允许放置
    e.preventDefault();
    e.dataTransfer.dropEffect = 'move';
});

2.3 DataTansfer

DataTransfer 对象如同它的名字,作用就是在拖放过程中对数据进行传输,实际上它包含了拖拽事件的状态,例如拖拽事件的类型(如拷贝 copy 或者移动 move),拖拽的数据(一个或者多个项)和每个拖拽项的类型(MIME 类型)。

定义拖拽数据

setData用来存放数据,getData用来获取数据,出于安全的考量,数据只能在drop时获取

// 拖动源事件
draggable.addEventListener('dragstart', (e) => {
    // 设置拖动数据
    e.dataTransfer.setData('text/plain', e.target.id);
});
// 目标区域事件
dropzone.addEventListener('drop', (e) => {
    e.preventDefault();
    // 获取拖动的元素ID
    const id = e.dataTransfer.getData('text/plain');
});

定义拖拽过程中的视觉效果

dropEffect 属性会影响到拖拽过程中浏览器显示的鼠标样式

// 设置当前效果
e.dataTransfer.dropEffect = 'copy'; // 'none', 'copy', 'link', 'move'

拖拽过程中,浏览器会在鼠标旁显示一张默认图片,可以通过 setDragImage 方法自定义一张图片

// 设置拖拽图像
e.dataTransfer.setDragImage(imgElement, xOffset, yOffset);

2.4 完整示例

下面是一个简单的拖拽示例,可以将可拖动的方块拖入目标区域。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>基础拖放示例</title>
    <style>
        #draggable {
            width: 100px;
            height: 100px;
            background-color: #3498db;
            color: white;
            text-align: center;
            line-height: 100px;
            cursor: move;
        }
        
        #dropzone {
            width: 300px;
            height: 300px;
            border: 2px dashed #2c3e50;
            margin-top: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        
        .drop-active {
            background-color: #ecf0f1;
        }
    </style>
</head>
<body>
    <div id="draggable" draggable="true">拖动我</div>
    <div id="dropzone">放置区域</div>
    
    <script>
        const draggable = document.getElementById('draggable');
        const dropzone = document.getElementById('dropzone');
        
        // 拖动源事件
        draggable.addEventListener('dragstart', (e) => {
            // 设置拖动数据
            e.dataTransfer.setData('text/plain', e.target.id);
            // 设置拖动效果
            e.dataTransfer.effectAllowed = 'move';
        });
        
        // 目标区域事件
        dropzone.addEventListener('dragenter', (e) => {
            e.preventDefault();
            dropzone.classList.add('drop-active');
        });
        
        dropzone.addEventListener('dragover', (e) => {
            // 阻止默认行为以允许放置
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        });
        
        dropzone.addEventListener('dragleave', () => {
            dropzone.classList.remove('drop-active');
        });
        
        dropzone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropzone.classList.remove('drop-active');
            
            // 获取拖动的元素ID
            const id = e.dataTransfer.getData('text/plain');
            const draggableElement = document.getElementById(id);
            
            // 将拖动元素添加到放置区
            dropzone.appendChild(draggableElement);
        });
    </script>
</body>
</html>

3. 自定义拖拽

虽然原生 API 使用简单,但存在一些局限,比如移动设备支持不佳、拖拽过程中的视觉反馈有限等。为了克服这些的局限,我们可以基于鼠标/触摸事件实现自定义拖拽。

3.1 核心原理

自定义拖拽的核心是捕获以下事件:

  • 鼠标事件: mousedown 、 mousemove 、 mouseup
  • 触摸事件: touchstart 、 touchmove 、 touchend

3.2 实现步骤

  1. 监听元素的 mousedown / touchstart 事件
  2. 记录初始位置和偏移量
  3. 监听 mousemove / touchmove 事件,更新元素位置
  4. 监听 mouseup / touchend 事件,结束拖拽

3.3 完整示例

下面是一个自定义的拖拽实现,允许用户在限定区域内拖动一个色块,实现的思路如下:

  1. 定义了一个可拖拽的元素 .draggable,并设置了样式和初始状态。
  2. 容器 .container 限制了拖拽范围,超出部分会被隐藏。
  3. 使用 mousedown 和 touchstart 事件监听拖拽开始,计算鼠标或触控点相对于元素的位置偏移量。
  4. 在拖拽过程中,通过 mousemove 或 touchmove 更新元素位置,并进行边界检测以确保元素不会移出容器。
  5. 拖拽结束时移除相关事件监听,并恢复元素样式。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>自定义拖拽示例</title>
    <style>
        .draggable {
            width: 100px;
            height: 100px;
            background-color: #3498db;
            color: white;
            display: flex;
            justify-content: center;
            align-items: center;
            position: absolute;
            cursor: move;
            user-select: none;
            touch-action: none;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        .dragging {
            opacity: 0.8;
            box-shadow: 0 8px 16px rgba(0,0,0,0.2);
            z-index: 1000;
        }
        .container {
            width: 25%;
            height: 400px;
            border: 2px dashed #ccc;
            position: relative;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <h2>自定义拖拽</h2>
    <div class="container">
        <div class="draggable" id="draggable1">拖拽我</div>
        <!-- <div class="draggable" id="draggable2" style="top: 150px; left: 150px; background-color: #e74c3c;">拖拽我</div> -->
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const draggables = document.querySelectorAll('.draggable');
            
            draggables.forEach(draggable => {
                // 初始位置
                let offsetX, offsetY;
                let isDragging = false;
                
                // 鼠标按下事件处理
                function handleStart(e) {
                    const event = e.type === 'touchstart' ? e.touches[0] : e;
                    
                    // 计算鼠标在元素内的偏移量
                    const rect = draggable.getBoundingClientRect();
                    offsetX = event.clientX - rect.left;
                    offsetY = event.clientY - rect.top;
                    
                    isDragging = true;
                    draggable.classList.add('dragging');
                    
                    // 添加移动和结束事件监听
                    if (e.type === 'mousedown') {
                        document.addEventListener('mousemove', handleMove);
                        document.addEventListener('mouseup', handleEnd);
                    }
                }
                
                // 移动事件处理
                function handleMove(e) {
                    if (!isDragging) return;
                    
                    e.preventDefault();
                    const event = e.type === 'touchmove' ? e.touches[0] : e;
                    
                    // 计算新位置
                    const container = document.querySelector('.container');
                    const containerRect = container.getBoundingClientRect();
                    
                    let left = event.clientX - containerRect.left - offsetX;
                    let top = event.clientY - containerRect.top - offsetY;
                    
                    // 边界检查
                    const maxLeft = containerRect.width - draggable.offsetWidth;
                    const maxTop = containerRect.height - draggable.offsetHeight;
                    
                    left = Math.max(0, Math.min(left, maxLeft));
                    top = Math.max(0, Math.min(top, maxTop));
                    
                    // 设置新位置
                    draggable.style.left = `${left}px`;
                    draggable.style.top = `${top}px`;
                }
                
                // 结束拖拽事件处理
                function handleEnd() {
                    if (!isDragging) return;
                    
                    isDragging = false;
                    draggable.classList.remove('dragging');
                    
                    // 移除事件监听
                    document.removeEventListener('mousemove', handleMove);
                    document.removeEventListener('mouseup', handleEnd);
                    document.removeEventListener('touchmove', handleMove);
                    document.removeEventListener('touchend', handleEnd);
                }
                
                // 添加事件监听
                draggable.addEventListener('mousedown', handleStart);
                draggable.addEventListener('touchstart', e => {
                    handleStart(e);
                    document.addEventListener('touchmove', handleMove, { passive: false });
                    document.addEventListener('touchend', handleEnd);
                });
            });
        });
    </script>
</body>
</html>

4. 常见拖拽功能

在实际应用中,我们常需要一些高级拖拽功能,如拖拽排序、拖拽上传等。

4.1 拖拽排序

拖拽排序是最常见的应用场景之一,其实现思路大体如下:

(1)开始拖拽

当鼠标按下时,记录被拖拽元素,计算鼠标偏移量,创建占位符并插入到被拖拽元素后,设置被拖拽元素样式为绝对定位,同时为 document 绑定 mousemove 和 mouseup 事件。

(2)拖拽过程

若鼠标移动,根据鼠标位置更新被拖拽元素位置。

隐藏被拖拽元素以获取鼠标下方元素,再显示被拖拽元素。

判断下方元素是否为可排序项,若是则计算其中点位置,根据鼠标位置与中点位置的关系,决定将占位符插入到可排序项前或后。

(3)结束拖拽

当鼠标释放,移除 mousemove 和 mouseup 事件监听器,恢复被拖拽元素默认样式,将被拖拽元素插入到占位符位置,移除占位符元素,完成拖拽排序。

下面是一个示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>拖拽排序</title>
    <style>
        .sortable-container {
            width: 300px;
            border: 1px solid #ddd;
            padding: 10px;
        }
        .sortable-item {
            padding: 10px;
            margin: 5px 0;
            background-color: #f9f9f9;
            border: 1px solid #eee;
            cursor: move;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .sortable-item.dragging {
            background-color: #f0f0f0;
            box-shadow: 0 5px 10px rgba(0,0,0,0.1);
            transform: scale(1.02);
            z-index: 1;
        }
        .placeholder {
            height: 40px;
            background-color: #e9e9e9;
            border: 2px dashed #ccc;
        }
    </style>
</head>
<body>
    <h2>拖拽排序列表</h2>
    <div class="sortable-container" id="sortable">
        <div class="sortable-item">项目 1</div>
        <div class="sortable-item">项目 2</div>
        <div class="sortable-item">项目 3</div>
        <div class="sortable-item">项目 4</div>
        <div class="sortable-item">项目 5</div>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const sortable = document.getElementById('sortable');
            const items = sortable.querySelectorAll('.sortable-item');
            
            let draggedItem = null;
            let placeholder = document.createElement('div');
            placeholder.className = 'placeholder';
            
            items.forEach(item => {
                item.addEventListener('mousedown', function(e) {
                    e.preventDefault();
                    draggedItem = this;
                    
                    // 记录初始位置
                    const rect = draggedItem.getBoundingClientRect();
                    const offsetX = e.clientX - rect.left;
                    const offsetY = e.clientY - rect.top;
                    
                    // 创建占位符
                    placeholder.style.height = `${rect.height}px`;
                    draggedItem.parentNode.insertBefore(placeholder, draggedItem.nextSibling);
                    
                    // 设置拖拽样式
                    draggedItem.classList.add('dragging');
                    draggedItem.style.position = 'absolute';
                    draggedItem.style.zIndex = 1000;
                    draggedItem.style.width = `${rect.width}px`;
                    
                    // 更新位置函数
                    function moveAt(clientX, clientY) {
                        draggedItem.style.left = `${clientX - offsetX}px`;
                        draggedItem.style.top = `${clientY - offsetY}px`;
                    }
                    
                    // 初始位置
                    moveAt(e.clientX, e.clientY);
                    
                    // 鼠标移动事件
                    function onMouseMove(e) {
                        moveAt(e.clientX, e.clientY);
                        
                        // 隐藏元素以便获取下方元素
                        draggedItem.style.display = 'none';
                        const elemBelow = document.elementFromPoint(e.clientX, e.clientY);
                        draggedItem.style.display = 'block';
                        
                        if (!elemBelow) return;
                        
                        // 找到最近的可排序项
                        const droppable = elemBelow.closest('.sortable-item');
                        if (droppable && droppable !== draggedItem) {
                            // 确定插入位置
                            const droppableRect = droppable.getBoundingClientRect();
                            const droppableMidY = droppableRect.top + droppableRect.height / 2;
                            
                            if (e.clientY < droppableMidY) {
                                droppable.parentNode.insertBefore(placeholder, droppable);
                            } else {
                                droppable.parentNode.insertBefore(placeholder, droppable.nextSibling);
                            }
                        }
                    }
                    
                    // 添加移动事件监听
                    document.addEventListener('mousemove', onMouseMove);
                    
                    // 鼠标释放事件
                    document.addEventListener('mouseup', function onMouseUp() {
                        document.removeEventListener('mousemove', onMouseMove);
                        document.removeEventListener('mouseup', onMouseUp);
                        
                        // 放置元素到占位符位置
                        draggedItem.style.position = '';
                        draggedItem.style.left = '';
                        draggedItem.style.top = '';
                        draggedItem.style.width = '';
                        draggedItem.style.zIndex = '';
                        draggedItem.classList.remove('dragging');
                        
                        placeholder.parentNode.insertBefore(draggedItem, placeholder);
                        placeholder.remove();
                        
                        draggedItem = null;
                    }, { once: true });
                });
                
                // 阻止默认拖拽行为
                item.addEventListener('dragstart', e => e.preventDefault());
            });
        });
    </script>
</body>
</html>

4.2 拖拽调整大小

拖拽调整元素大小是另一个常见需求,实现的思路大体如下:

(1)开始调整

当鼠标按下手柄时,阻止默认行为,将 isResizing 标记为 true,同时记录鼠标按下时的位置和元素的初始宽度、高度。

(2)调整过程

若鼠标移动,检查 isResizing 是否为 true,如果是则计算新的宽度和高度。

分别判断新宽度和新高度是否大于最小尺寸(如100px),根据判断结果更新元素的宽度和高度样式。

(3)结束调整

当鼠标释放时,将 isResizing 标记为 false

下面是一个示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>拖拽调整大小</title>
    <style>
        .resizable {
            width: 200px;
            height: 150px;
            background-color: #3498db;
            color: white;
            position: relative;
            padding: 20px;
            box-sizing: border-box;
            overflow: hidden;
        }
        .resize-handle {
            width: 10px;
            height: 10px;
            background-color: white;
            position: absolute;
            right: 0;
            bottom: 0;
            cursor: nwse-resize;
        }
    </style>
</head>
<body>
    <h2>拖拽调整大小</h2>
    <div class="resizable" id="resizable">
        <div class="content">可调整大小的元素</div>
        <div class="resize-handle" id="resize-handle"></div>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const resizable = document.getElementById('resizable');
            const handle = document.getElementById('resize-handle');
            
            let isResizing = false;
            let startX, startY, startWidth, startHeight;
            
            handle.addEventListener('mousedown', function(e) {
                e.preventDefault();
                isResizing = true;
                
                startX = e.clientX;
                startY = e.clientY;
                startWidth = parseInt(document.defaultView.getComputedStyle(resizable).width, 10);
                startHeight = parseInt(document.defaultView.getComputedStyle(resizable).height, 10);
                
                document.addEventListener('mousemove', resize);
                document.addEventListener('mouseup', stopResize);
            });
            
            function resize(e) {
                if (!isResizing) return;
                
                const width = startWidth + e.clientX - startX;
                const height = startHeight + e.clientY - startY;
                
                // 设置最小尺寸
                if (width > 100) resizable.style.width = `${width}px`;
                if (height > 100) resizable.style.height = `${height}px`;
            }
            
            function stopResize() {
                isResizing = false;
                document.removeEventListener('mousemove', resize);
                document.removeEventListener('mouseup', stopResize);
            }
        });
    </script>
</body>
</html>

4.3 拖拽上传

拖拽上传文件是现代 Web 应用的常见功能,实现思路如下:

(1)交互阶段

用户可以选择将文件拖拽到拖拽区域,或者点击选择按钮选择文件。 当有文件拖放或选择时,获取文件列表。

(2)文件处理阶段

遍历文件列表,为每个文件创建文件项元素,设置文件名和文件大小,并将其添加到文件列表中。

下面是一个示例:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>拖拽上传</title>
    <style>
        .drop-area {
            width: 300px;
            height: 200px;
            border: 3px dashed #ccc;
            border-radius: 8px;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            transition: all 0.3s;
            margin: 20px 0;
        }
        .drop-area.active {
            border-color: #3498db;
            background-color: rgba(52, 152, 219, 0.1);
        }
        .file-list {
            margin-top: 20px;
            width: 300px;
        }
        .file-item {
            display: flex;
            justify-content: space-between;
            padding: 8px;
            border: 1px solid #eee;
            margin-bottom: 5px;
            border-radius: 4px;
        }
        .file-name {
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .file-size {
            margin-left: 10px;
            color: #666;
        }
    </style>
</head>
<body>
    <h2>拖拽上传文件</h2>
    <div class="drop-area" id="drop-area">
        <p>拖拽文件到此处上传</p>
        <p></p>
        <input type="file" id="file-input" multiple style="display: none;">
        <button id="select-button">选择文件</button>
    </div>
    
    <div class="file-list" id="file-list">
        <h3>已选文件:</h3>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const dropArea = document.getElementById('drop-area');
            const fileInput = document.getElementById('file-input');
            const selectButton = document.getElementById('select-button');
            const fileList = document.getElementById('file-list');
            
            // 阻止默认拖放行为
            ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
                dropArea.addEventListener(eventName, preventDefaults, false);
                document.body.addEventListener(eventName, preventDefaults, false);
            });
            
            function preventDefaults(e) {
                e.preventDefault();
                e.stopPropagation();
            }
            
            // 高亮显示拖放区域
            ['dragenter', 'dragover'].forEach(eventName => {
                dropArea.addEventListener(eventName, highlight, false);
            });
            
            ['dragleave', 'drop'].forEach(eventName => {
                dropArea.addEventListener(eventName, unhighlight, false);
            });
            
            function highlight() {
                dropArea.classList.add('active');
            }
            
            function unhighlight() {
                dropArea.classList.remove('active');
            }
            
            // 处理拖放的文件
            dropArea.addEventListener('drop', handleDrop, false);
            
            function handleDrop(e) {
                const dt = e.dataTransfer;
                const files = dt.files;
                handleFiles(files);
            }
            
            // 点击选择文件
            selectButton.addEventListener('click', () => {
                fileInput.click();
            });
            
            fileInput.addEventListener('change', () => {
                handleFiles(fileInput.files);
            });
            
            function handleFiles(files) {
                if (files.length === 0) return;
                
                // 显示文件列表
                Array.from(files).forEach(file => {
                    const fileItem = document.createElement('div');
                    fileItem.className = 'file-item';
                    
                    const fileName = document.createElement('div');
                    fileName.className = 'file-name';
                    fileName.textContent = file.name;
                    
                    const fileSize = document.createElement('div');
                    fileSize.className = 'file-size';
                    fileSize.textContent = formatFileSize(file.size);
                    
                    fileItem.appendChild(fileName);
                    fileItem.appendChild(fileSize);
                    fileList.appendChild(fileItem);
                    
                    // 这里可以添加上传逻辑
                    // uploadFile(file);
                });
            }
            
            function formatFileSize(bytes) {
                if (bytes === 0) return '0 Bytes';
                const k = 1024;
                const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
                const i = Math.floor(Math.log(bytes) / Math.log(k));
                return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
            }
            
            // 上传文件函数(示例)
            function uploadFile(file) {
                const formData = new FormData();
                formData.append('file', file);
                
                // 使用 fetch API 上传
                fetch('your-upload-url', {
                    method: 'POST',
                    body: formData
                })
                .then(response => response.json())
                .then(data => {
                    console.log('上传成功:', data);
                })
                .catch(error => {
                    console.error('上传失败:', error);
                });
            }
        });
    </script>
</body>
</html>

5. 通用拖拽解决方案

在实际项目中,我们通常会使用成熟的拖拽库来简化开发,这里列举了一些流行的拖拽库。

拖拽实现方式基础拖拽能力表现框架兼容实现原理
React DnD支持元素的拖拽和放置ReactHTML5
react-grid-layout支持网格/自由拖拽和缩放,不支持旋转React鼠标事件
react-draggable支持自由拖拽,不支持缩放和旋转React鼠标事件
react-resizable支持缩放,不支持自由拖拽和旋转React鼠标事件
Vue Draggable支持自由拖拽和缩放Vue2鼠标事件
vue-drag-resize支持自由拖拽和缩放Vue2、Vue3鼠标事件
Vue3 Dnd支持元素的拖拽和放置Vue3HTML5
Vue Grid Layout支持网格/自由拖拽和缩放,不支持旋转Vue2、Vue3鼠标事件
dnd/kit支持元素的拖拽和放置Vue和ReactHTML5
movable支持自由拖拽和缩放Vue和React鼠标事件

下面仅介绍Vue生态下几个常用的拖拽库:

5.1 Vue3 Dnd

Vue3 DnD 是一个专门为 Vue3 设计的拖放解决方案,它基于 React DnD 的核心程序实现,提供了一种数据驱动的方式来实现拖拽功能,侧重于逻辑处理,允许开发者基于数据灵活地进行定制。

以下是一个简单的示例,展示如何使用 Vue3 DnD 实现拖拽和放置功能:

<template>
  <div>
    <div v-draggable="draggableOptions" :class="{ draggable: isDragging }" @dragstart="handleDragStart" @dragend="handleDragEnd">
      Drag me!
    </div>
    <div v-droppable="droppableOptions" @drop="handleDrop" @dragover.prevent>
      Drop here!
    </div>
  </div>
</template>
<script setup>
import { ref } from 'vue';
import { useDraggable, useDroppable } from '@vueuse/dnd';
const isDragging = ref(false);
const draggableOptions = {
  type: 'item',
  data: { id: 1, text: 'Drag me!' },
};
const handleDragStart = () => {
  isDragging.value = true;
};
const handleDragEnd = () => {
  isDragging.value = false;
};
const droppableOptions = {
  types: ['item'],
};
const handleDrop = (event) => {
  const data = event.dataTransfer.getData('text/plain');
  console.log('Dropped item:', data);
};
</script>

5.2 Vue Draggable

vue-draggable 是一个基于 Sortable.js 的 Vue 组件库,用于实现列表项的拖拽排序功能。它支持拖拽、交换位置、复制、移动到其他列表等多种操作,非常适合用于任务管理、列表排序等场景。其实现原理如下:

  • 拖拽逻辑:内部使用 Sortable.js 来处理拖拽逻辑,该库封装了 HTML5 的 Drag and Drop API
  • 事件监听:支持 Sortable.js 的所有事件,触发时会以相同的参数调用
  • 状态管理:使用 Vue 的响应式数据绑定机制来跟踪列表项的状态;在拖拽过程中,实时更新数据模型并重新渲染组件
  • DOM 操作:通过修改元素的 CSS 样式来实现拖拽效果,使用 Sortable.js 提供的 API 来处理拖拽过程中的 DOM 操作
  • 动画效果:通过设置 animation 属性,可以为拖拽操作添加动画效果,与 Vue 的过渡动画兼容

Vue Draggable 提供了许多配置选项,以下是一些常用的配置项:

配置项说明
list要拖拽的数组
tag指定 draggable 组件的根元素类型,默认为 'div'
clone用于克隆被拖拽的元素
move用于控制元素的移动逻辑
componentData用于向通过 tag 属性声明的子组件传递额外信息

以下是一个简单的示例,展示如何使用 vue-draggable 创建一个可拖动的列表:

<template>
  <draggable v-model="list" @end="onEnd">
    <div v-for="item in list" :key="list.id">{{ list.name }}</div>
  </draggable>
</template>
<script>
import draggable from 'vuedraggable';
export default {
  components: {
    draggable,
  },
  data() {
    return {
      list: [
        { id: 1, name: 'Item 1' },
        { id: 2, name: 'Item 2' },
        { id: 3, name: 'Item 3' },
      ],
    };
  },
  methods: {
    onEnd(event) {
      console.log('Drag ended', event);
    },
  },
};
</script>

5.3 Vue Draggable Resizable

vue-draggable-resizable 允许用户通过拖拽和调整大小来移动和改变元素的尺寸,非常适合需要在页面上自由拖拽和调整元素大小的场景。其主要特性如下:

  • 灵活的拖拽和调整大小功能:允许用户自由拖拽元素,并在四个角和边上进行调整大小操作。
  • 丰富的配置选项:提供了多种配置项,满足不同场景下的需求。
  • 事件驱动:通过事件监听,可以在拖拽和调整大小的过程中执行自定义逻辑。
  • 良好的兼容性:与 Vue.js 框架无缝集成,适用于 Vue 2 和 Vue 3

下面是一个使用 vue-draggable-resizable 的示例,展示如何创建一个可拖拽且可调整大小的元素:

<template>
  <div id="box">
    <vue-draggable-resizable
      :w="width"
      :h="height"
      :x="x"
      :y="y"
      @dragging="onDrag"
      @resizing="onResize"
      @resized="onResized"
    >
      Drag me!
    </vue-draggable-resizable>
  </div>
</template>
<script>
import VueDraggableResizable from 'vue-draggable-resizable'
import 'vue-draggable-resizable/dist/VueDraggableResizable.css'
export default {
  components: {
    VueDraggableResizable
  },
  data() {
    return {
      x: 0,
      y: 0,
      width: 100,
      height: 100
    }
  },
  methods: {
    // 拖拽过程中更新组件位置
    onDrag(x, y) {
      this.x = x
      this.y = y
    },
    // 调整大小过程中更新组件尺寸
    onResize(x, y, width, height) {
      this.x = x
      this.y = y
      this.width = width
      this.height = height
    },
    // 调整大小结束时的回调
    onResized() {
      console.log('Resized')
    }
  }
}
</script>

6. 可视化大屏编辑器中的拖拽

可视化大屏编辑器允许用户通过拖拽添加到画布,对一个组件来说,一个完整的拖拽流程由两部分组成:

  1. 拖拽组件放置到画布;
  2. 在画布中拖拽组件调整位置、大小等信息。

6.1 组件拖拽到画布

拖拽组件放置到画布本质是将拖动源携带的数据传到画布制定区域,目标源监听事件拿到携带的数据动态渲染出实际的组件。过程如下:

实现方面可以直接使用HTML5原生的拖放API

组件列表的伪代码:

<div v-for="item in componentList" :key="item" class="list"
     draggable="true" :data-type="item" @dragstart="handleDragStart">
    <div>
        <span class="iconfont" :class="'icon-' + componentIconMap[item]"></span>
    </div>
</div>
handleDragStart(e) {
    e.dataTransfer.setData('type', e.target.dataset.type)
},

给列表中的每一个组件都设置了 draggable 属性。另外,在触发 dragstart 事件时,使用 dataTransfer.setData() 传输数据。

接收数据的代码如下:

<div id="main-container" @drop="handleDrop" @dragover="handleDragover">
</div>
handleDrop(e) {
    e.preventDefault()
    e.stopPropagation()
    // 渲染的组件类型
    const componentName = e.dataTransfer.getData('type');
    // 位置信息
    const rectInfo = $('#canvas-container')
    .getBoundingClientRect();
    const left = e.x - rectInfo.left;
    const top = e.y - rectInfo.top;
    // 组件渲染数据
    const componentInfo = {
        id: generateID(),
        component: componentName,
        style: {
            left,
            top
        }
    }
    this.addComponent(componentInfo)
},

触发 drop 事件时,使用 dataTransfer.getData() 接收传输过来的数据,然后根据找到对应的组件数据,再添加到画布,从而渲染组件。

6.2 组件在画布中移动

实现拖拽和调整元素大小的功能可以通过监听鼠标事件来控制元素的位置和尺寸变化。伪代码如下:

element.onmousedown = (event) => {
// 记录起始位置
}
element.onmousemove = (event) => {
// 计算移动位置,移动目标元素
}
element.onmouseup = (event) => {
// 松开鼠标,结束移动
}

这种方式相对灵活,可以根据具体需求进行定制,不局限于拖拽和放置这种固定操作模式,比如直接拖动改变位置、双击改变大小等。但需要更多的逻辑处理来确保拖拽的准确性和流畅性。实际开发中,通常使用第三方库实现,如vue-draggable-resizablemovable等,适用于Vue的主流拖拽库对比如下:

实现方式基础拖拽能力使用场景
原生鼠标事件移动、缩放、旋转简单的拖拽功能
vue draggable拖放、分组拖拽对列表或网格中的元素进行排序
vue-drag-resize移动、缩放、旋转实现可拖拽且可调整大小的组件
movable移动、缩放、旋转简单的拖拽移动

以下是 vue-drag-resize 的使用示例:

<template>
  <div id="box">
    <vue-draggable-resizable :w="width" :h="height" :x="x" :y="y" @dragging="onDrag" @resizing="onResize">
      Drag me!
    </vue-draggable-resizable>
  </div>
</template>
<script>
import VueDraggableResizable from 'vue-draggable-resizable';
import 'vue-draggable-resizable/dist/VueDraggableResizable.css';
export default {
  components: {
    VueDraggableResizable
  },
  data() {
    return {
      x: 0,
      y: 0,
      width: 100,
      height: 100
    };
  },
  methods: {
    // 更新组件位置
    onDrag(x, y) {
      this.x = x;
      this.y = y;
    },
      
    // 更新组件尺寸
    onResize(x, y, width, height) {
      this.x = x;
      this.y = y;
      this.width = width;
      this.height = height;
    }
  }
};
</script>

7. 总结

前端拖拽技术作为一种强大的交互模式,已经成为现代 Web 应用的标配。无论是实现简单的元素拖放,还是构建复杂的可视化编辑器,掌握拖拽技术有助于我们构建更加直观、高效的用户界面,为用户提供更优质的体验。希望本文能够帮助读者全面了解前端拖拽技术,并在实际项目中灵活应用。

转自https://juejin.cn/post/7491164546045624356


该文章在 2025/4/12 10:48:20 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved