拖放API进阶

拖放API概述

拖放API(Drag and Drop API)提供了一种简单的方式来实现拖放功能,允许用户通过拖拽操作来移动、复制或重新排列元素。这是现代Web应用实现直观用户交互的重要技术。

东巴文(db-w.cn) 认为:拖放API让Web应用能够实现类似桌面应用的拖放功能,提升用户体验。

拖放API特点

核心特点

特点 说明
原生支持 浏览器原生支持,无需额外库
事件驱动 通过事件监听实现拖放逻辑
数据传递 使用DataTransfer传递数据
视觉反馈 支持自定义拖放图像和效果
文件支持 支持拖放文件到页面

拖放事件

事件 触发时机 作用对象
dragstart 开始拖动 被拖动元素
drag 拖动过程中 被拖动元素
dragend 拖动结束 被拖动元素
dragenter 拖入目标 目标元素
dragover 在目标上方 目标元素
dragleave 拖出目标 目标元素
drop 放下 目标元素

基本拖放

简单拖放示例

<!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>
        body {
            font-family: Arial, sans-serif;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .container {
            display: flex;
            gap: 20px;
            margin: 20px 0;
        }
        
        .box {
            width: 150px;
            height: 150px;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 48px;
            cursor: move;
            user-select: none;
            transition: transform 0.3s, box-shadow 0.3s;
        }
        
        .box:hover {
            transform: scale(1.05);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
        }
        
        .box.dragging {
            opacity: 0.5;
            transform: scale(0.95);
        }
        
        .box1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
        .box2 { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
        .box3 { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
        
        .drop-zone {
            flex: 1;
            min-height: 300px;
            border: 3px dashed #ddd;
            border-radius: 15px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 24px;
            color: #999;
            transition: all 0.3s;
        }
        
        .drop-zone.dragover {
            border-color: #667eea;
            background: #f0f0ff;
            color: #667eea;
        }
        
        .drop-zone .box {
            margin: 10px;
        }
        
        .instructions {
            background: #f9f9f9;
            padding: 20px;
            border-radius: 10px;
            margin-bottom: 20px;
        }
        
        .instructions h3 {
            margin-bottom: 10px;
            color: #333;
        }
        
        .instructions ul {
            margin-left: 20px;
        }
        
        .instructions li {
            margin: 5px 0;
            color: #666;
        }
    </style>
</head>
<body>
    <h1>基本拖放示例</h1>
    
    <div class="instructions">
        <h3>使用说明</h3>
        <ul>
            <li>拖动左侧的彩色方块</li>
            <li>放到右侧的放置区域</li>
            <li>方块会被移动到放置区域</li>
        </ul>
    </div>
    
    <div class="container">
        <div class="source-area">
            <div class="box box1" draggable="true" id="box1">🍎</div>
            <div class="box box2" draggable="true" id="box2">🍊</div>
            <div class="box box3" draggable="true" id="box3">🍋</div>
        </div>
        
        <div class="drop-zone" id="dropZone">
            拖放到这里
        </div>
    </div>
    
    <script>
        const boxes = document.querySelectorAll('.box');
        const dropZone = document.getElementById('dropZone');
        
        let draggedElement = null;
        
        // 为每个盒子添加拖动事件
        boxes.forEach(box => {
            // 开始拖动
            box.addEventListener('dragstart', function(e) {
                draggedElement = this;
                this.classList.add('dragging');
                
                // 设置拖动数据
                e.dataTransfer.setData('text/plain', this.id);
                e.dataTransfer.effectAllowed = 'move';
            });
            
            // 拖动结束
            box.addEventListener('dragend', function(e) {
                this.classList.remove('dragging');
                draggedElement = null;
            });
        });
        
        // 放置区域事件
        dropZone.addEventListener('dragenter', function(e) {
            e.preventDefault();
            this.classList.add('dragover');
        });
        
        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
        });
        
        dropZone.addEventListener('dragleave', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
        });
        
        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
            
            // 获取拖动的元素
            const id = e.dataTransfer.getData('text/plain');
            const element = document.getElementById(id);
            
            if (element) {
                // 如果放置区域已有内容,清空提示文字
                if (this.textContent === '拖放到这里') {
                    this.textContent = '';
                }
                
                // 移动元素
                this.appendChild(element);
            }
        });
    </script>
</body>
</html>

拖放事件详解

完整事件流程

<!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>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .container {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
            margin: 20px 0;
        }
        
        .draggable {
            padding: 20px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            cursor: move;
            text-align: center;
            font-size: 18px;
            user-select: none;
            transition: all 0.3s;
        }
        
        .draggable:active {
            transform: scale(0.95);
        }
        
        .drop-zone {
            min-height: 200px;
            border: 3px dashed #ddd;
            border-radius: 10px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 18px;
            color: #999;
            transition: all 0.3s;
        }
        
        .drop-zone.active {
            border-color: #667eea;
            background: #f0f0ff;
        }
        
        .event-log {
            background: #1e1e1e;
            color: #d4d4d4;
            padding: 20px;
            border-radius: 10px;
            max-height: 400px;
            overflow-y: auto;
            font-family: 'Courier New', monospace;
            font-size: 14px;
        }
        
        .event-item {
            padding: 8px;
            margin: 5px 0;
            border-radius: 5px;
            background: #2d2d2d;
        }
        
        .event-item.drag { border-left: 3px solid #667eea; }
        .event-item.drop { border-left: 3px solid #27ae60; }
        .event-item.enter { border-left: 3px solid #f39c12; }
        .event-item.leave { border-left: 3px solid #e74c3c; }
        
        .event-time {
            color: #999;
            font-size: 12px;
        }
        
        .event-name {
            font-weight: bold;
            color: #fff;
        }
        
        .clear-btn {
            margin-top: 10px;
            padding: 10px 20px;
            background: #e74c3c;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .info-card {
            background: #f9f9f9;
            padding: 20px;
            border-radius: 10px;
            margin: 20px 0;
        }
        
        .info-card h3 {
            margin-bottom: 15px;
            color: #333;
        }
        
        .event-table {
            width: 100%;
            border-collapse: collapse;
        }
        
        .event-table th,
        .event-table td {
            padding: 10px;
            text-align: left;
            border-bottom: 1px solid #ddd;
        }
        
        .event-table th {
            background: #667eea;
            color: white;
        }
    </style>
</head>
<body>
    <h1>拖放事件详解</h1>
    
    <div class="info-card">
        <h3>拖放事件说明</h3>
        <table class="event-table">
            <thead>
                <tr>
                    <th>事件</th>
                    <th>触发时机</th>
                    <th>事件目标</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><code>dragstart</code></td>
                    <td>用户开始拖动元素</td>
                    <td>被拖动元素</td>
                </tr>
                <tr>
                    <td><code>drag</code></td>
                    <td>拖动过程中持续触发</td>
                    <td>被拖动元素</td>
                </tr>
                <tr>
                    <td><code>dragend</code></td>
                    <td>拖动操作结束</td>
                    <td>被拖动元素</td>
                </tr>
                <tr>
                    <td><code>dragenter</code></td>
                    <td>拖动元素进入目标</td>
                    <td>放置目标</td>
                </tr>
                <tr>
                    <td><code>dragover</code></td>
                    <td>拖动元素在目标上方</td>
                    <td>放置目标</td>
                </tr>
                <tr>
                    <td><code>dragleave</code></td>
                    <td>拖动元素离开放置目标</td>
                    <td>放置目标</td>
                </tr>
                <tr>
                    <td><code>drop</code></td>
                    <td>拖动元素被放置</td>
                    <td>放置目标</td>
                </tr>
            </tbody>
        </table>
    </div>
    
    <div class="container">
        <div>
            <div class="draggable" draggable="true" id="draggable">
                拖动我
            </div>
            
            <div class="drop-zone" id="dropZone">
                放置区域
            </div>
        </div>
        
        <div>
            <h3>事件日志</h3>
            <div class="event-log" id="eventLog"></div>
            <button class="clear-btn" onclick="clearLog()">清空日志</button>
        </div>
    </div>
    
    <script>
        const draggable = document.getElementById('draggable');
        const dropZone = document.getElementById('dropZone');
        const eventLog = document.getElementById('eventLog');
        
        // 日志函数
        function logEvent(eventName, type) {
            const time = new Date().toLocaleTimeString();
            const item = document.createElement('div');
            item.className = `event-item ${type}`;
            item.innerHTML = `
                <span class="event-time">[${time}]</span>
                <span class="event-name">${eventName}</span>
            `;
            eventLog.insertBefore(item, eventLog.firstChild);
        }
        
        // 被拖动元素事件
        draggable.addEventListener('dragstart', function(e) {
            logEvent('dragstart', 'drag');
            e.dataTransfer.setData('text/plain', '拖动的数据');
            e.dataTransfer.effectAllowed = 'move';
            this.style.opacity = '0.5';
        });
        
        draggable.addEventListener('drag', function(e) {
            // drag事件触发频率很高,不记录日志
        });
        
        draggable.addEventListener('dragend', function(e) {
            logEvent('dragend', 'drag');
            this.style.opacity = '1';
        });
        
        // 放置目标事件
        dropZone.addEventListener('dragenter', function(e) {
            e.preventDefault();
            logEvent('dragenter', 'enter');
            this.classList.add('active');
        });
        
        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault();
            // dragover事件触发频率很高,不记录日志
        });
        
        dropZone.addEventListener('dragleave', function(e) {
            e.preventDefault();
            logEvent('dragleave', 'leave');
            this.classList.remove('active');
        });
        
        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            logEvent('drop', 'drop');
            this.classList.remove('active');
            
            const data = e.dataTransfer.getData('text/plain');
            this.textContent = `收到数据: ${data}`;
        });
        
        // 清空日志
        function clearLog() {
            eventLog.innerHTML = '';
        }
    </script>
</body>
</html>

DataTransfer对象

数据传递

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DataTransfer对象示例</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 1000px;
            margin: 50px auto;
            padding: 20px;
        }
        
        .container {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            margin: 20px 0;
        }
        
        .item {
            padding: 20px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            cursor: move;
            transition: transform 0.3s, box-shadow 0.3s;
        }
        
        .item:hover {
            transform: translateY(-5px);
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
        }
        
        .item.dragging {
            opacity: 0.5;
        }
        
        .item-icon {
            font-size: 48px;
            text-align: center;
            margin-bottom: 10px;
        }
        
        .item-name {
            font-weight: bold;
            text-align: center;
            margin-bottom: 5px;
        }
        
        .item-price {
            text-align: center;
            color: #e74c3c;
            font-weight: bold;
        }
        
        .cart {
            background: #f9f9f9;
            border-radius: 15px;
            padding: 30px;
            margin-top: 20px;
        }
        
        .cart-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
        }
        
        .cart-title {
            font-size: 24px;
            color: #333;
        }
        
        .cart-count {
            background: #667eea;
            color: white;
            padding: 5px 15px;
            border-radius: 15px;
        }
        
        .cart-items {
            min-height: 100px;
            border: 3px dashed #ddd;
            border-radius: 10px;
            padding: 20px;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            transition: all 0.3s;
        }
        
        .cart-items.dragover {
            border-color: #667eea;
            background: #f0f0ff;
        }
        
        .cart-item {
            background: white;
            padding: 10px 15px;
            border-radius: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
        }
        
        .cart-item-icon {
            font-size: 24px;
        }
        
        .cart-item-remove {
            color: #e74c3c;
            cursor: pointer;
            font-size: 18px;
        }
        
        .cart-total {
            margin-top: 20px;
            padding: 20px;
            background: white;
            border-radius: 10px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 18px;
        }
        
        .total-price {
            color: #e74c3c;
            font-weight: bold;
            font-size: 24px;
        }
        
        .info-panel {
            background: #e7f3ff;
            padding: 20px;
            border-radius: 10px;
            margin-top: 20px;
        }
        
        .info-panel h3 {
            margin-bottom: 10px;
            color: #333;
        }
        
        .info-panel code {
            background: white;
            padding: 2px 8px;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h1>DataTransfer对象示例</h1>
    
    <div class="info-panel">
        <h3>DataTransfer常用方法</h3>
        <p><code>setData(format, data)</code> - 设置拖动数据</p>
        <p><code>getData(format)</code> - 获取拖动数据</p>
        <p><code>clearData()</code> - 清除所有数据</p>
        <p><code>setDragImage(element, x, y)</code> - 设置拖动图像</p>
    </div>
    
    <div class="container" id="productList">
        <div class="item" draggable="true" data-id="1" data-name="苹果" data-price="5.99" data-icon="🍎">
            <div class="item-icon">🍎</div>
            <div class="item-name">苹果</div>
            <div class="item-price">¥5.99</div>
        </div>
        
        <div class="item" draggable="true" data-id="2" data-name="香蕉" data-price="3.99" data-icon="🍌">
            <div class="item-icon">🍌</div>
            <div class="item-name">香蕉</div>
            <div class="item-price">¥3.99</div>
        </div>
        
        <div class="item" draggable="true" data-id="3" data-name="橙子" data-price="4.99" data-icon="🍊">
            <div class="item-icon">🍊</div>
            <div class="item-name">橙子</div>
            <div class="item-price">¥4.99</div>
        </div>
        
        <div class="item" draggable="true" data-id="4" data-name="葡萄" data-price="8.99" data-icon="🍇">
            <div class="item-icon">🍇</div>
            <div class="item-name">葡萄</div>
            <div class="item-price">¥8.99</div>
        </div>
        
        <div class="item" draggable="true" data-id="5" data-name="西瓜" data-price="12.99" data-icon="🍉">
            <div class="item-icon">🍉</div>
            <div class="item-name">西瓜</div>
            <div class="item-price">¥12.99</div>
        </div>
        
        <div class="item" draggable="true" data-id="6" data-name="草莓" data-price="15.99" data-icon="🍓">
            <div class="item-icon">🍓</div>
            <div class="item-name">草莓</div>
            <div class="item-price">¥15.99</div>
        </div>
    </div>
    
    <div class="cart">
        <div class="cart-header">
            <div class="cart-title">🛒 购物车</div>
            <div class="cart-count">商品数量: <span id="cartCount">0</span></div>
        </div>
        
        <div class="cart-items" id="cartItems">
            <p style="color: #999; width: 100%; text-align: center;">拖放商品到这里</p>
        </div>
        
        <div class="cart-total">
            <span>总计:</span>
            <span class="total-price">¥<span id="totalPrice">0.00</span></span>
        </div>
    </div>
    
    <script>
        const items = document.querySelectorAll('.item');
        const cartItems = document.getElementById('cartItems');
        const cartCount = document.getElementById('cartCount');
        const totalPrice = document.getElementById('totalPrice');
        
        let cart = [];
        
        // 商品拖动事件
        items.forEach(item => {
            item.addEventListener('dragstart', function(e) {
                this.classList.add('dragging');
                
                // 使用JSON传递复杂数据
                const data = {
                    id: this.dataset.id,
                    name: this.dataset.name,
                    price: parseFloat(this.dataset.price),
                    icon: this.dataset.icon
                };
                
                e.dataTransfer.setData('application/json', JSON.stringify(data));
                e.dataTransfer.effectAllowed = 'copy';
            });
            
            item.addEventListener('dragend', function(e) {
                this.classList.remove('dragging');
            });
        });
        
        // 购物车事件
        cartItems.addEventListener('dragover', function(e) {
            e.preventDefault();
            e.dataTransfer.dropEffect = 'copy';
            this.classList.add('dragover');
        });
        
        cartItems.addEventListener('dragleave', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
        });
        
        cartItems.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
            
            // 获取JSON数据
            const json = e.dataTransfer.getData('application/json');
            const data = JSON.parse(json);
            
            // 添加到购物车
            addToCart(data);
        });
        
        // 添加到购物车
        function addToCart(item) {
            // 检查是否已存在
            const existing = cart.find(i => i.id === item.id);
            
            if (existing) {
                existing.quantity++;
            } else {
                cart.push({
                    ...item,
                    quantity: 1
                });
            }
            
            renderCart();
        }
        
        // 从购物车移除
        function removeFromCart(id) {
            const index = cart.findIndex(i => i.id === id);
            if (index > -1) {
                if (cart[index].quantity > 1) {
                    cart[index].quantity--;
                } else {
                    cart.splice(index, 1);
                }
            }
            renderCart();
        }
        
        // 渲染购物车
        function renderCart() {
            if (cart.length === 0) {
                cartItems.innerHTML = '<p style="color: #999; width: 100%; text-align: center;">拖放商品到这里</p>';
            } else {
                cartItems.innerHTML = cart.map(item => `
                    <div class="cart-item">
                        <span class="cart-item-icon">${item.icon}</span>
                        <span>${item.name} × ${item.quantity}</span>
                        <span>¥${(item.price * item.quantity).toFixed(2)}</span>
                        <span class="cart-item-remove" onclick="removeFromCart('${item.id}')">×</span>
                    </div>
                `).join('');
            }
            
            // 更新统计
            const count = cart.reduce((sum, item) => sum + item.quantity, 0);
            const total = cart.reduce((sum, item) => sum + item.price * item.quantity, 0);
            
            cartCount.textContent = count;
            totalPrice.textContent = total.toFixed(2);
        }
    </script>
</body>
</html>

文件拖放

拖放上传文件

<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
        }
        
        .container {
            max-width: 800px;
            margin: 0 auto;
        }
        
        .header {
            text-align: center;
            color: white;
            padding: 30px 0;
        }
        
        .header h1 {
            font-size: 36px;
            margin-bottom: 10px;
        }
        
        .upload-card {
            background: white;
            border-radius: 15px;
            padding: 30px;
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
        }
        
        .drop-zone {
            border: 3px dashed #ddd;
            border-radius: 15px;
            padding: 60px 20px;
            text-align: center;
            transition: all 0.3s;
            cursor: pointer;
        }
        
        .drop-zone:hover,
        .drop-zone.dragover {
            border-color: #667eea;
            background: #f0f0ff;
        }
        
        .drop-zone-icon {
            font-size: 64px;
            margin-bottom: 20px;
        }
        
        .drop-zone-title {
            font-size: 24px;
            margin-bottom: 10px;
            color: #333;
        }
        
        .drop-zone-subtitle {
            color: #999;
            margin-bottom: 20px;
        }
        
        .file-input {
            display: none;
        }
        
        .browse-btn {
            padding: 12px 30px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 25px;
            cursor: pointer;
            font-size: 16px;
        }
        
        .file-list {
            margin-top: 30px;
        }
        
        .file-item {
            display: flex;
            align-items: center;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 10px;
            margin-bottom: 10px;
        }
        
        .file-icon {
            font-size: 32px;
            margin-right: 15px;
        }
        
        .file-info {
            flex: 1;
        }
        
        .file-name {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .file-meta {
            font-size: 14px;
            color: #666;
        }
        
        .progress-bar {
            width: 100%;
            height: 6px;
            background: #e0e0e0;
            border-radius: 3px;
            margin-top: 10px;
            overflow: hidden;
        }
        
        .progress-fill {
            height: 100%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            width: 0%;
            transition: width 0.3s;
        }
        
        .file-status {
            padding: 5px 15px;
            border-radius: 15px;
            font-size: 14px;
        }
        
        .file-status.success {
            background: #d4edda;
            color: #155724;
        }
        
        .file-status.error {
            background: #f8d7da;
            color: #721c24;
        }
        
        .upload-actions {
            display: flex;
            gap: 10px;
            margin-top: 20px;
        }
        
        .upload-btn {
            flex: 1;
            padding: 15px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            font-size: 16px;
        }
        
        .upload-btn:hover {
            opacity: 0.9;
        }
        
        .upload-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }
        
        .clear-btn {
            padding: 15px 30px;
            background: #f5f5f5;
            border: none;
            border-radius: 10px;
            cursor: pointer;
            font-size: 16px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📁 文件拖放上传</h1>
            <p>拖放文件或点击选择文件</p>
        </div>
        
        <div class="upload-card">
            <div class="drop-zone" id="dropZone">
                <div class="drop-zone-icon">📂</div>
                <div class="drop-zone-title">拖放文件到这里</div>
                <div class="drop-zone-subtitle"></div>
                <button class="browse-btn" onclick="document.getElementById('fileInput').click()">
                    浏览文件
                </button>
                <input type="file" id="fileInput" class="file-input" multiple>
            </div>
            
            <div class="file-list" id="fileList"></div>
            
            <div class="upload-actions" id="uploadActions" style="display: none;">
                <button class="upload-btn" onclick="uploadFiles()">开始上传</button>
                <button class="clear-btn" onclick="clearFiles()">清空列表</button>
            </div>
        </div>
    </div>
    
    <script>
        const dropZone = document.getElementById('dropZone');
        const fileInput = document.getElementById('fileInput');
        const fileList = document.getElementById('fileList');
        const uploadActions = document.getElementById('uploadActions');
        
        let selectedFiles = [];
        
        // 拖放处理
        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault();
            dropZone.classList.add('dragover');
        });
        
        dropZone.addEventListener('dragleave', function(e) {
            e.preventDefault();
            dropZone.classList.remove('dragover');
        });
        
        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            dropZone.classList.remove('dragover');
            
            // 处理文件
            const files = e.dataTransfer.files;
            handleFiles(files);
        });
        
        // 文件选择
        fileInput.addEventListener('change', function(e) {
            handleFiles(e.target.files);
        });
        
        // 处理文件
        function handleFiles(files) {
            selectedFiles = Array.from(files);
            renderFileList();
        }
        
        // 渲染文件列表
        function renderFileList() {
            if (selectedFiles.length === 0) {
                fileList.innerHTML = '';
                uploadActions.style.display = 'none';
                return;
            }
            
            uploadActions.style.display = 'flex';
            
            fileList.innerHTML = selectedFiles.map((file, index) => `
                <div class="file-item">
                    <div class="file-icon">${getFileIcon(file.type)}</div>
                    <div class="file-info">
                        <div class="file-name">${file.name}</div>
                        <div class="file-meta">
                            ${formatFileSize(file.size)} | ${file.type || '未知类型'}
                        </div>
                        <div class="progress-bar">
                            <div class="progress-fill" id="progress-${index}"></div>
                        </div>
                    </div>
                    <div class="file-status" id="status-${index}">等待上传</div>
                </div>
            `).join('');
        }
        
        // 上传文件
        async function uploadFiles() {
            for (let i = 0; i < selectedFiles.length; i++) {
                await uploadFile(selectedFiles[i], i);
            }
        }
        
        // 上传单个文件
        async function uploadFile(file, index) {
            const progressFill = document.getElementById(`progress-${index}`);
            const status = document.getElementById(`status-${index}`);
            
            status.className = 'file-status';
            status.textContent = '上传中...';
            
            // 模拟上传
            return new Promise(resolve => {
                let progress = 0;
                const interval = setInterval(() => {
                    progress += Math.random() * 30;
                    
                    if (progress >= 100) {
                        progress = 100;
                        clearInterval(interval);
                        
                        progressFill.style.width = '100%';
                        status.className = 'file-status success';
                        status.textContent = '上传成功';
                        
                        resolve();
                    } else {
                        progressFill.style.width = progress + '%';
                    }
                }, 200);
            });
        }
        
        // 清空文件
        function clearFiles() {
            selectedFiles = [];
            renderFileList();
            fileInput.value = '';
        }
        
        // 获取文件图标
        function getFileIcon(type) {
            if (type.startsWith('image/')) return '🖼️';
            if (type.startsWith('video/')) return '🎬';
            if (type.startsWith('audio/')) return '🎵';
            if (type === 'application/pdf') return '📄';
            if (type.includes('word')) return '📝';
            if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
            return '📁';
        }
        
        // 格式化文件大小
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
    </script>
</body>
</html>

拖放排序

列表排序

<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        
        .container {
            max-width: 600px;
            margin: 50px auto;
        }
        
        h1 {
            text-align: center;
            margin-bottom: 30px;
            color: #333;
        }
        
        .sortable-list {
            background: white;
            border-radius: 15px;
            padding: 20px;
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
        }
        
        .list-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 2px solid #f0f0f0;
        }
        
        .list-title {
            font-size: 18px;
            color: #333;
        }
        
        .list-count {
            background: #667eea;
            color: white;
            padding: 5px 15px;
            border-radius: 15px;
            font-size: 14px;
        }
        
        .list-item {
            display: flex;
            align-items: center;
            padding: 15px;
            background: #f9f9f9;
            border-radius: 10px;
            margin-bottom: 10px;
            cursor: move;
            transition: all 0.3s;
            user-select: none;
        }
        
        .list-item:hover {
            background: #f0f0f0;
            transform: translateX(5px);
        }
        
        .list-item.dragging {
            opacity: 0.5;
            background: #e0e0e0;
        }
        
        .list-item.drag-over {
            border-top: 3px solid #667eea;
        }
        
        .item-handle {
            font-size: 20px;
            margin-right: 15px;
            color: #999;
        }
        
        .item-content {
            flex: 1;
        }
        
        .item-title {
            font-weight: bold;
            margin-bottom: 5px;
        }
        
        .item-description {
            font-size: 14px;
            color: #666;
        }
        
        .item-number {
            width: 30px;
            height: 30px;
            background: #667eea;
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            margin-left: 15px;
        }
        
        .instructions {
            background: #e7f3ff;
            padding: 15px;
            border-radius: 10px;
            margin-top: 20px;
            font-size: 14px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>📋 拖放排序</h1>
        
        <div class="sortable-list">
            <div class="list-header">
                <div class="list-title">任务列表</div>
                <div class="list-count"><span id="itemCount">0</span></div>
            </div>
            
            <div id="listContainer"></div>
            
            <div class="instructions">
                💡 提示:拖动任务项可以重新排序
            </div>
        </div>
    </div>
    
    <script>
        const listContainer = document.getElementById('listContainer');
        const itemCount = document.getElementById('itemCount');
        
        let items = [
            { id: 1, title: '学习HTML基础', description: '了解HTML标签和结构' },
            { id: 2, title: '学习CSS样式', description: '掌握CSS布局和样式' },
            { id: 3, title: '学习JavaScript', description: '掌握JavaScript基础语法' },
            { id: 4, title: '学习DOM操作', description: '了解如何操作DOM元素' },
            { id: 5, title: '学习事件处理', description: '掌握事件监听和处理' },
            { id: 6, title: '实践项目', description: '完成一个实际项目' }
        ];
        
        let draggedItem = null;
        
        // 渲染列表
        function renderList() {
            listContainer.innerHTML = items.map((item, index) => `
                <div class="list-item" draggable="true" data-id="${item.id}">
                    <div class="item-handle">⋮⋮</div>
                    <div class="item-content">
                        <div class="item-title">${item.title}</div>
                        <div class="item-description">${item.description}</div>
                    </div>
                    <div class="item-number">${index + 1}</div>
                </div>
            `).join('');
            
            itemCount.textContent = items.length;
            
            // 添加拖放事件
            addDragEvents();
        }
        
        // 添加拖放事件
        function addDragEvents() {
            const listItems = document.querySelectorAll('.list-item');
            
            listItems.forEach(item => {
                item.addEventListener('dragstart', function(e) {
                    draggedItem = this;
                    this.classList.add('dragging');
                    
                    e.dataTransfer.effectAllowed = 'move';
                });
                
                item.addEventListener('dragend', function(e) {
                    this.classList.remove('dragging');
                    draggedItem = null;
                    
                    // 移除所有drag-over类
                    document.querySelectorAll('.list-item').forEach(i => {
                        i.classList.remove('drag-over');
                    });
                });
                
                item.addEventListener('dragover', function(e) {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    
                    if (draggedItem && draggedItem !== this) {
                        this.classList.add('drag-over');
                    }
                });
                
                item.addEventListener('dragleave', function(e) {
                    this.classList.remove('drag-over');
                });
                
                item.addEventListener('drop', function(e) {
                    e.preventDefault();
                    this.classList.remove('drag-over');
                    
                    if (draggedItem && draggedItem !== this) {
                        // 获取索引
                        const fromIndex = Array.from(listContainer.children).indexOf(draggedItem);
                        const toIndex = Array.from(listContainer.children).indexOf(this);
                        
                        // 重新排序
                        const [movedItem] = items.splice(fromIndex, 1);
                        items.splice(toIndex, 0, movedItem);
                        
                        // 重新渲染
                        renderList();
                    }
                });
            });
        }
        
        // 初始化
        renderList();
    </script>
</body>
</html>

综合示例

<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f5f5f5;
            min-height: 100vh;
        }
        
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        
        .header-content {
            max-width: 1200px;
            margin: 0 auto;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        
        .header h1 {
            font-size: 24px;
        }
        
        .add-btn {
            padding: 10px 20px;
            background: white;
            color: #667eea;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-weight: bold;
        }
        
        .container {
            max-width: 1200px;
            margin: 30px auto;
            padding: 0 20px;
        }
        
        .board {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
        }
        
        .column {
            background: white;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
        }
        
        .column-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 20px;
            padding-bottom: 15px;
            border-bottom: 2px solid #f0f0f0;
        }
        
        .column-title {
            font-size: 18px;
            font-weight: bold;
            color: #333;
        }
        
        .column-count {
            background: #667eea;
            color: white;
            padding: 3px 10px;
            border-radius: 10px;
            font-size: 14px;
        }
        
        .column-content {
            min-height: 400px;
            padding: 10px;
            border-radius: 10px;
            transition: background 0.3s;
        }
        
        .column-content.dragover {
            background: #f0f0ff;
        }
        
        .card {
            background: #f9f9f9;
            border-radius: 10px;
            padding: 15px;
            margin-bottom: 10px;
            cursor: move;
            transition: all 0.3s;
            border-left: 4px solid #667eea;
        }
        
        .card:hover {
            transform: translateY(-3px);
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
        }
        
        .card.dragging {
            opacity: 0.5;
            transform: scale(0.95);
        }
        
        .card-title {
            font-weight: bold;
            margin-bottom: 10px;
            color: #333;
        }
        
        .card-description {
            font-size: 14px;
            color: #666;
            margin-bottom: 15px;
        }
        
        .card-footer {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 12px;
            color: #999;
        }
        
        .card-priority {
            padding: 3px 8px;
            border-radius: 3px;
            font-size: 12px;
            font-weight: bold;
        }
        
        .priority-high { background: #f8d7da; color: #721c24; }
        .priority-medium { background: #fff3cd; color: #856404; }
        .priority-low { background: #d4edda; color: #155724; }
        
        .modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 1000;
        }
        
        .modal.show {
            display: flex;
            align-items: center;
            justify-content: center;
        }
        
        .modal-content {
            background: white;
            border-radius: 15px;
            padding: 30px;
            width: 90%;
            max-width: 500px;
        }
        
        .modal-header {
            margin-bottom: 20px;
        }
        
        .modal-header h3 {
            color: #333;
        }
        
        .form-group {
            margin-bottom: 20px;
        }
        
        .form-group label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #333;
        }
        
        .form-group input,
        .form-group textarea,
        .form-group select {
            width: 100%;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 5px;
            font-size: 14px;
        }
        
        .form-group textarea {
            min-height: 100px;
            resize: vertical;
        }
        
        .modal-actions {
            display: flex;
            gap: 10px;
        }
        
        .modal-actions button {
            flex: 1;
            padding: 12px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        
        .btn-primary {
            background: #667eea;
            color: white;
        }
        
        .btn-secondary {
            background: #f5f5f5;
            color: #333;
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="header-content">
            <h1>📋 任务看板</h1>
            <button class="add-btn" onclick="showModal()">+ 添加任务</button>
        </div>
    </div>
    
    <div class="container">
        <div class="board">
            <div class="column" data-status="todo">
                <div class="column-header">
                    <div class="column-title">📝 待办</div>
                    <div class="column-count" id="todoCount">0</div>
                </div>
                <div class="column-content" id="todo"></div>
            </div>
            
            <div class="column" data-status="inProgress">
                <div class="column-header">
                    <div class="column-title">🔄 进行中</div>
                    <div class="column-count" id="inProgressCount">0</div>
                </div>
                <div class="column-content" id="inProgress"></div>
            </div>
            
            <div class="column" data-status="done">
                <div class="column-header">
                    <div class="column-title">✅ 已完成</div>
                    <div class="column-count" id="doneCount">0</div>
                </div>
                <div class="column-content" id="done"></div>
            </div>
        </div>
    </div>
    
    <div class="modal" id="modal">
        <div class="modal-content">
            <div class="modal-header">
                <h3>添加新任务</h3>
            </div>
            
            <div class="form-group">
                <label>任务标题</label>
                <input type="text" id="taskTitle" placeholder="输入任务标题">
            </div>
            
            <div class="form-group">
                <label>任务描述</label>
                <textarea id="taskDescription" placeholder="输入任务描述"></textarea>
            </div>
            
            <div class="form-group">
                <label>优先级</label>
                <select id="taskPriority">
                    <option value="low"></option>
                    <option value="medium"></option>
                    <option value="high"></option>
                </select>
            </div>
            
            <div class="modal-actions">
                <button class="btn-primary" onclick="addTask()">添加</button>
                <button class="btn-secondary" onclick="hideModal()">取消</button>
            </div>
        </div>
    </div>
    
    <script>
        let tasks = [
            { id: 1, title: '学习HTML', description: '掌握HTML基础标签', status: 'todo', priority: 'high' },
            { id: 2, title: '学习CSS', description: '掌握CSS样式和布局', status: 'todo', priority: 'medium' },
            { id: 3, title: '学习JavaScript', description: '掌握JavaScript基础', status: 'inProgress', priority: 'high' },
            { id: 4, title: '完成项目', description: '完成一个实际项目', status: 'done', priority: 'low' }
        ];
        
        let draggedCard = null;
        
        // 渲染看板
        function renderBoard() {
            const todoContainer = document.getElementById('todo');
            const inProgressContainer = document.getElementById('inProgress');
            const doneContainer = document.getElementById('done');
            
            const todoTasks = tasks.filter(t => t.status === 'todo');
            const inProgressTasks = tasks.filter(t => t.status === 'inProgress');
            const doneTasks = tasks.filter(t => t.status === 'done');
            
            todoContainer.innerHTML = todoTasks.map(renderCard).join('');
            inProgressContainer.innerHTML = inProgressTasks.map(renderCard).join('');
            doneContainer.innerHTML = doneTasks.map(renderCard).join('');
            
            document.getElementById('todoCount').textContent = todoTasks.length;
            document.getElementById('inProgressCount').textContent = inProgressTasks.length;
            document.getElementById('doneCount').textContent = doneTasks.length;
            
            addCardDragEvents();
            addColumnDropEvents();
        }
        
        // 渲染卡片
        function renderCard(task) {
            const priorityClass = `priority-${task.priority}`;
            const priorityText = { high: '高', medium: '中', low: '低' }[task.priority];
            
            return `
                <div class="card" draggable="true" data-id="${task.id}">
                    <div class="card-title">${task.title}</div>
                    <div class="card-description">${task.description}</div>
                    <div class="card-footer">
                        <span class="card-priority ${priorityClass}">${priorityText}</span>
                        <span>任务 #${task.id}</span>
                    </div>
                </div>
            `;
        }
        
        // 添加卡片拖动事件
        function addCardDragEvents() {
            const cards = document.querySelectorAll('.card');
            
            cards.forEach(card => {
                card.addEventListener('dragstart', function(e) {
                    draggedCard = this;
                    this.classList.add('dragging');
                    e.dataTransfer.effectAllowed = 'move';
                });
                
                card.addEventListener('dragend', function(e) {
                    this.classList.remove('dragging');
                    draggedCard = null;
                });
            });
        }
        
        // 添加列放置事件
        function addColumnDropEvents() {
            const columns = document.querySelectorAll('.column-content');
            
            columns.forEach(column => {
                column.addEventListener('dragover', function(e) {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    this.classList.add('dragover');
                });
                
                column.addEventListener('dragleave', function(e) {
                    this.classList.remove('dragover');
                });
                
                column.addEventListener('drop', function(e) {
                    e.preventDefault();
                    this.classList.remove('dragover');
                    
                    if (draggedCard) {
                        const taskId = parseInt(draggedCard.dataset.id);
                        const newStatus = this.parentElement.dataset.status;
                        
                        // 更新任务状态
                        const task = tasks.find(t => t.id === taskId);
                        if (task) {
                            task.status = newStatus;
                            renderBoard();
                        }
                    }
                });
            });
        }
        
        // 显示模态框
        function showModal() {
            document.getElementById('modal').classList.add('show');
        }
        
        // 隐藏模态框
        function hideModal() {
            document.getElementById('modal').classList.remove('show');
            document.getElementById('taskTitle').value = '';
            document.getElementById('taskDescription').value = '';
            document.getElementById('taskPriority').value = 'low';
        }
        
        // 添加任务
        function addTask() {
            const title = document.getElementById('taskTitle').value.trim();
            const description = document.getElementById('taskDescription').value.trim();
            const priority = document.getElementById('taskPriority').value;
            
            if (!title) {
                alert('请输入任务标题');
                return;
            }
            
            tasks.push({
                id: Date.now(),
                title,
                description,
                status: 'todo',
                priority
            });
            
            hideModal();
            renderBoard();
        }
        
        // 初始化
        renderBoard();
    </script>
</body>
</html>

最佳实践

1. 可访问性

// 推荐:为拖放元素添加键盘支持
<div draggable="true" 
     tabindex="0"
     role="button"
     aria-label="可拖动项:按空格键开始拖动">
    拖动我
</div>

<script>
element.addEventListener('keydown', function(e) {
    if (e.key === 'Space') {
        // 开始拖动
        e.preventDefault();
        startDrag(this);
    } else if (e.key === 'Escape') {
        // 取消拖动
        cancelDrag();
    }
});
</script>

2. 视觉反馈

// 推荐:提供清晰的视觉反馈
element.addEventListener('dragstart', function(e) {
    // 设置拖动图像
    const dragImage = this.cloneNode(true);
    dragImage.style.opacity = '0.8';
    document.body.appendChild(dragImage);
    e.dataTransfer.setDragImage(dragImage, 0, 0);
    
    // 延迟移除
    setTimeout(() => {
        document.body.removeChild(dragImage);
    }, 0);
});

dropZone.addEventListener('dragenter', function(e) {
    e.preventDefault();
    this.classList.add('drag-over');
});

dropZone.addEventListener('dragleave', function(e) {
    this.classList.remove('drag-over');
});

3. 性能优化

// 推荐:避免在drag事件中执行复杂操作
let lastDragTime = 0;

element.addEventListener('drag', function(e) {
    const now = Date.now();
    
    // 节流:每100ms执行一次
    if (now - lastDragTime < 100) return;
    lastDragTime = now;
    
    // 执行操作
    updatePosition(e.clientX, e.clientY);
});

东巴文点评:拖放API使用要注重可访问性和视觉反馈,提供良好的用户体验。

学习检验

知识点测试

问题1:哪个事件在拖动元素进入放置目标时触发?

A. dragstart B. dragenter C. dragover D. drop

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释dragenter事件在拖动元素进入有效的放置目标时触发。注意:必须在dragover事件中调用e.preventDefault()才能使drop事件正常触发。

</details>

问题2:如何使用DataTransfer传递JSON数据?

A. setData('json', data) B. setData('application/json', JSON.stringify(data)) C. setData('text/json', data) D. 直接传递对象

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:使用setData('application/json', JSON.stringify(data))传递JSON数据,接收时使用JSON.parse(getData('application/json'))解析。

</details>

实践任务

任务:创建一个图片排序工具,支持拖放重新排序图片。

<details> <summary>点击查看参考答案</summary>
<!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>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: #f5f5f5;
            padding: 20px;
        }
        
        .container {
            max-width: 1000px;
            margin: 0 auto;
        }
        
        h1 {
            text-align: center;
            margin-bottom: 30px;
            color: #333;
        }
        
        .toolbar {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-bottom: 30px;
        }
        
        button {
            padding: 10px 20px;
            background: #667eea;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
        }
        
        button:hover {
            background: #5568d3;
        }
        
        .image-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
            gap: 20px;
        }
        
        .image-item {
            background: white;
            border-radius: 10px;
            overflow: hidden;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
            cursor: move;
            transition: all 0.3s;
        }
        
        .image-item:hover {
            transform: translateY(-5px);
            box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
        }
        
        .image-item.dragging {
            opacity: 0.5;
            transform: scale(0.95);
        }
        
        .image-item.drag-over {
            border: 3px solid #667eea;
        }
        
        .image-item img {
            width: 100%;
            height: 200px;
            object-fit: cover;
        }
        
        .image-info {
            padding: 15px;
        }
        
        .image-number {
            display: inline-block;
            width: 30px;
            height: 30px;
            background: #667eea;
            color: white;
            border-radius: 50%;
            text-align: center;
            line-height: 30px;
            font-weight: bold;
        }
        
        .drop-zone {
            border: 3px dashed #ddd;
            border-radius: 15px;
            padding: 60px 20px;
            text-align: center;
            margin-bottom: 30px;
            transition: all 0.3s;
        }
        
        .drop-zone.dragover {
            border-color: #667eea;
            background: #f0f0ff;
        }
        
        .drop-zone-icon {
            font-size: 48px;
            margin-bottom: 10px;
        }
        
        input[type="file"] {
            display: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🖼️ 图片排序工具</h1>
        
        <div class="toolbar">
            <button onclick="document.getElementById('fileInput').click()">
                📁 添加图片
            </button>
            <button onclick="clearImages()">🗑️ 清空</button>
            <button onclick="exportOrder()">💾 导出顺序</button>
        </div>
        
        <input type="file" id="fileInput" accept="image/*" multiple>
        
        <div class="drop-zone" id="dropZone">
            <div class="drop-zone-icon">📂</div>
            <p>拖放图片到这里添加</p>
        </div>
        
        <div class="image-grid" id="imageGrid"></div>
    </div>
    
    <script>
        const fileInput = document.getElementById('fileInput');
        const dropZone = document.getElementById('dropZone');
        const imageGrid = document.getElementById('imageGrid');
        
        let images = [];
        let draggedItem = null;
        
        // 文件选择
        fileInput.addEventListener('change', function(e) {
            addImages(e.target.files);
        });
        
        // 拖放处理
        dropZone.addEventListener('dragover', function(e) {
            e.preventDefault();
            this.classList.add('dragover');
        });
        
        dropZone.addEventListener('dragleave', function(e) {
            this.classList.remove('dragover');
        });
        
        dropZone.addEventListener('drop', function(e) {
            e.preventDefault();
            this.classList.remove('dragover');
            
            const files = e.dataTransfer.files;
            addImages(files);
        });
        
        // 添加图片
        function addImages(files) {
            Array.from(files).forEach(file => {
                if (file.type.startsWith('image/')) {
                    const reader = new FileReader();
                    reader.onload = function(e) {
                        images.push({
                            id: Date.now() + Math.random(),
                            src: e.target.result,
                            name: file.name
                        });
                        renderImages();
                    };
                    reader.readAsDataURL(file);
                }
            });
        }
        
        // 渲染图片
        function renderImages() {
            imageGrid.innerHTML = images.map((img, index) => `
                <div class="image-item" draggable="true" data-id="${img.id}">
                    <img src="${img.src}" alt="${img.name}">
                    <div class="image-info">
                        <span class="image-number">${index + 1}</span>
                    </div>
                </div>
            `).join('');
            
            addDragEvents();
        }
        
        // 添加拖动事件
        function addDragEvents() {
            const items = document.querySelectorAll('.image-item');
            
            items.forEach(item => {
                item.addEventListener('dragstart', function(e) {
                    draggedItem = this;
                    this.classList.add('dragging');
                    e.dataTransfer.effectAllowed = 'move';
                });
                
                item.addEventListener('dragend', function(e) {
                    this.classList.remove('dragging');
                    draggedItem = null;
                    
                    document.querySelectorAll('.image-item').forEach(i => {
                        i.classList.remove('drag-over');
                    });
                });
                
                item.addEventListener('dragover', function(e) {
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    
                    if (draggedItem && draggedItem !== this) {
                        this.classList.add('drag-over');
                    }
                });
                
                item.addEventListener('dragleave', function(e) {
                    this.classList.remove('drag-over');
                });
                
                item.addEventListener('drop', function(e) {
                    e.preventDefault();
                    this.classList.remove('drag-over');
                    
                    if (draggedItem && draggedItem !== this) {
                        const fromId = parseFloat(draggedItem.dataset.id);
                        const toId = parseFloat(this.dataset.id);
                        
                        const fromIndex = images.findIndex(i => i.id === fromId);
                        const toIndex = images.findIndex(i => i.id === toId);
                        
                        const [movedItem] = images.splice(fromIndex, 1);
                        images.splice(toIndex, 0, movedItem);
                        
                        renderImages();
                    }
                });
            });
        }
        
        // 清空图片
        function clearImages() {
            images = [];
            renderImages();
        }
        
        // 导出顺序
        function exportOrder() {
            const order = images.map((img, index) => ({
                index: index + 1,
                name: img.name
            }));
            
            console.log('图片顺序:', order);
            alert('顺序已导出到控制台');
        }
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!