拖放API(Drag and Drop API)提供了一种简单的方式来实现拖放功能,允许用户通过拖拽操作来移动、复制或重新排列元素。这是现代Web应用实现直观用户交互的重要技术。
东巴文(db-w.cn) 认为:拖放API让Web应用能够实现类似桌面应用的拖放功能,提升用户体验。
| 特点 | 说明 |
|---|---|
| 原生支持 | 浏览器原生支持,无需额外库 |
| 事件驱动 | 通过事件监听实现拖放逻辑 |
| 数据传递 | 使用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>
<!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>
// 推荐:为拖放元素添加键盘支持
<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>
// 推荐:提供清晰的视觉反馈
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');
});
// 推荐:避免在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事件正常触发。
问题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> <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) - 让编程学习更有趣、更高效!