日韩欧美国产精品免费一二-日韩欧美国产精品亚洲二区-日韩欧美国产精品专区-日韩欧美国产另-日韩欧美国产免费看-日韩欧美国产免费看清风阁

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

一次搞懂前端拖拽方案

freeflydom
2025年4月12日 10:48 本文熱度 294

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

主站蜘蛛池模板: 亚洲最大 | 亚洲成aⅴ人片久青草影院 国产91精品系列在线观看 | 欧美综合自拍亚洲综合百度 | 日本中文字幕第 | 亚洲v高清一区二区三区尤物 | 99re这里只有精品国产精品 | 中文日产无乱码v在线观 | 国产午夜福利精品在线观看不 | 欧美一级大 | 国语精品91自产拍在线观看二区 | 亚洲欧美日韩在线资源观看 | 永久精品免费影院在线观看网 | 国产精品福利一区二区 | 欧美一区二区成人午夜在线观看 | 2025年最新国产精品正在播放 | 91情侣在线精品国产 | 欧美日韩一区视频导航 | 无人影院手机版在线观看免费 | 私人影院 | 亚洲第一精品电影网 | 国产在线视频欧美一区二区三区 | 亚洲日韩国产成网在线观看 | 色一情一伦一区二区三 | 野花免费观 | 在线免费观看国 | 思思伊人 | 免费人成年激情视频在线观看 | 亚洲精品永久一区 | 国产午夜亚洲精品不卡福利 | 国产视频中文字幕手机版 | 亚州视频一区 | 亚洲精品1区2区3区4区 | 欧美特黄一免在线观看 | 中字幕一区二区三区乱 | 国产香港日本三级在线观看 | 夜鲁鲁鲁夜夜综合视频 | 日本中文字幕在线观看 | 欧美一级a人与 | 521a成v视频网站在线入口 | 亚洲不卡精品在线观看 | 国产精选免|