拖放API(Drag and Drop API)是HTML5提供的原生接口,允许用户通过拖拽操作在页面元素之间移动数据。这个API让Web应用具备了类似桌面应用的拖放交互能力。
东巴文(db-w.cn) 认为:拖放API让Web交互更加直观和自然,是实现富交互应用的重要工具。
| 特点 | 说明 |
|---|---|
| 原生支持 | HTML5原生API,无需插件 |
| 跨元素拖放 | 支持不同元素间拖放 |
| 数据传递 | 通过DataTransfer传递数据 |
| 视觉反馈 | 支持自定义拖放图像和效果 |
// 拖放事件列表
const dragEvents = {
// 拖拽元素事件
drag: '拖拽过程中持续触发',
dragstart: '开始拖拽时触发',
dragend: '拖拽结束时触发',
// 放置目标事件
dragenter: '拖拽元素进入目标时触发',
dragover: '拖拽元素在目标上移动时持续触发',
dragleave: '拖拽元素离开目标时触发',
drop: '放置时触发'
};
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>基本拖放示例</title>
<style>
.draggable {
width: 100px;
height: 100px;
background: #667eea;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: move;
border-radius: 5px;
margin: 10px;
}
.dropzone {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
margin: 20px;
}
.dropzone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
</style>
</head>
<body>
<!-- 可拖拽元素 -->
<div class="draggable" draggable="true" id="drag1">
拖拽我
</div>
<!-- 放置区域 -->
<div class="dropzone" id="dropzone1">
放置区域
</div>
<script>
const draggable = document.getElementById('drag1');
const dropzone = document.getElementById('dropzone1');
// 拖拽开始
draggable.addEventListener('dragstart', function(e) {
console.log('开始拖拽');
e.dataTransfer.setData('text/plain', this.id);
e.dataTransfer.effectAllowed = 'move';
});
// 拖拽结束
draggable.addEventListener('dragend', function(e) {
console.log('拖拽结束');
});
// 拖拽进入目标
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) {
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);
this.appendChild(element);
});
</script>
</body>
</html>
// 拖拽元素
const draggable = document.querySelector('.draggable');
// dragstart: 开始拖拽
draggable.addEventListener('dragstart', function(e) {
// 设置数据
e.dataTransfer.setData('text/plain', '文本数据');
e.dataTransfer.setData('text/html', '<b>HTML数据</b>');
e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }));
// 设置拖拽效果
e.dataTransfer.effectAllowed = 'move'; // move | copy | link | all | none
// 设置拖拽图像
const dragImage = new Image();
dragImage.src = 'drag-image.png';
e.dataTransfer.setDragImage(dragImage, 0, 0);
console.log('拖拽开始');
});
// drag: 拖拽过程中
draggable.addEventListener('drag', function(e) {
// 持续触发,注意性能
console.log('拖拽中...', e.clientX, e.clientY);
});
// dragend: 拖拽结束
draggable.addEventListener('dragend', function(e) {
console.log('拖拽结束');
// 检查是否成功放置
if (e.dataTransfer.dropEffect === 'none') {
console.log('拖拽被取消');
}
});
// 放置目标
const dropzone = document.querySelector('.dropzone');
// dragenter: 进入目标
dropzone.addEventListener('dragenter', function(e) {
e.preventDefault(); // 必须阻止默认行为
this.classList.add('drag-over');
console.log('进入目标');
});
// dragover: 在目标上移动
dropzone.addEventListener('dragover', function(e) {
e.preventDefault(); // 必须阻止默认行为,否则无法drop
e.dataTransfer.dropEffect = 'move';
console.log('在目标上移动');
});
// dragleave: 离开目标
dropzone.addEventListener('dragleave', function(e) {
this.classList.remove('drag-over');
console.log('离开目标');
});
// drop: 放置
dropzone.addEventListener('drop', function(e) {
e.preventDefault(); // 阻止默认行为
this.classList.remove('drag-over');
// 获取数据
const text = e.dataTransfer.getData('text/plain');
const html = e.dataTransfer.getData('text/html');
const json = e.dataTransfer.getData('application/json');
console.log('放置数据:', { text, html, json });
});
// DataTransfer对象属性
const dataTransfer = e.dataTransfer;
// dropEffect: 放置效果
dataTransfer.dropEffect = 'none'; // none | copy | move | link
// effectAllowed: 允许的效果
dataTransfer.effectAllowed = 'all'; // none | copy | copyLink | copyMove | link | linkMove | move | all | uninitialized
// files: 文件列表
const files = dataTransfer.files;
// items: DataTransferItemList
const items = dataTransfer.items;
// types: 数据类型列表
const types = dataTransfer.types;
// 设置数据
e.dataTransfer.setData('text/plain', '文本数据');
e.dataTransfer.setData('text/html', '<b>HTML数据</b>');
// 获取数据
const text = e.dataTransfer.getData('text/plain');
const html = e.dataTransfer.getData('text/html');
// 清除数据
e.dataTransfer.clearData();
e.dataTransfer.clearData('text/plain');
// 设置拖拽图像
const img = new Image();
img.src = 'drag-image.png';
e.dataTransfer.setDragImage(img, 10, 10); // 图像和偏移量
<!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;
}
.dropzone {
width: 100%;
height: 300px;
border: 3px dashed #ccc;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transition: all 0.3s;
background: #f9f9f9;
}
.dropzone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.1);
transform: scale(1.02);
}
.dropzone-icon {
font-size: 64px;
margin-bottom: 20px;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
margin: 10px 0;
}
.file-icon {
font-size: 32px;
margin-right: 15px;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: bold;
margin-bottom: 5px;
}
.file-size {
color: #666;
font-size: 14px;
}
.image-preview {
max-width: 100px;
max-height: 100px;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>文件拖放示例</h1>
<div class="dropzone" id="dropzone">
<div class="dropzone-icon">📁</div>
<p>拖放文件到这里</p>
<p style="color: #999; font-size: 14px;">支持图片、文本、PDF等文件</p>
</div>
<div class="file-list" id="fileList"></div>
<script>
const dropzone = document.getElementById('dropzone');
const fileList = document.getElementById('fileList');
// 阻止默认拖放行为
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// 拖拽进入
dropzone.addEventListener('dragenter', function(e) {
this.classList.add('dragover');
});
// 拖拽离开
dropzone.addEventListener('dragleave', function(e) {
this.classList.remove('dragover');
});
// 放置
dropzone.addEventListener('drop', function(e) {
this.classList.remove('dragover');
const files = e.dataTransfer.files;
handleFiles(files);
});
// 处理文件
function handleFiles(files) {
[...files].forEach(file => {
displayFile(file);
});
}
// 显示文件
function displayFile(file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
// 文件图标
let icon = '📄';
if (file.type.startsWith('image/')) icon = '🖼️';
else if (file.type.startsWith('video/')) icon = '🎬';
else if (file.type.startsWith('audio/')) icon = '🎵';
else if (file.type === 'application/pdf') icon = '📕';
else if (file.type.includes('word')) icon = '📘';
else if (file.type.includes('excel')) icon = '📗';
// 文件大小格式化
const size = formatFileSize(file.size);
fileItem.innerHTML = `
<div class="file-icon">${icon}</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-size">${size} | ${file.type || '未知类型'}</div>
</div>
`;
// 图片预览
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const img = document.createElement('img');
img.src = e.target.result;
img.className = 'image-preview';
fileItem.appendChild(img);
};
reader.readAsDataURL(file);
}
fileList.appendChild(fileItem);
}
// 格式化文件大小
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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + 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>
body {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 50px auto;
padding: 20px;
}
h1 {
text-align: center;
}
.sortable-list {
list-style: none;
padding: 0;
}
.sortable-item {
display: flex;
align-items: center;
padding: 15px;
background: white;
border: 1px solid #ddd;
margin: 5px 0;
border-radius: 5px;
cursor: move;
transition: all 0.3s;
}
.sortable-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.sortable-item.dragging {
opacity: 0.5;
background: #f0f0f0;
}
.sortable-item.drag-over {
border-top: 2px solid #667eea;
}
.item-number {
width: 30px;
height: 30px;
background: #667eea;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15px;
font-weight: bold;
}
.item-content {
flex: 1;
}
.item-handle {
color: #ccc;
font-size: 20px;
}
</style>
</head>
<body>
<h1>拖放排序</h1>
<ul class="sortable-list" id="sortableList">
<li class="sortable-item" draggable="true">
<span class="item-number">1</span>
<span class="item-content">HTML基础教程</span>
<span class="item-handle">⋮⋮</span>
</li>
<li class="sortable-item" draggable="true">
<span class="item-number">2</span>
<span class="item-content">CSS样式教程</span>
<span class="item-handle">⋮⋮</span>
</li>
<li class="sortable-item" draggable="true">
<span class="item-number">3</span>
<span class="item-content">JavaScript教程</span>
<span class="item-handle">⋮⋮</span>
</li>
<li class="sortable-item" draggable="true">
<span class="item-number">4</span>
<span class="item-content">Vue.js教程</span>
<span class="item-handle">⋮⋮</span>
</li>
<li class="sortable-item" draggable="true">
<span class="item-number">5</span>
<span class="item-content">React教程</span>
<span class="item-handle">⋮⋮</span>
</li>
</ul>
<script>
const list = document.getElementById('sortableList');
let draggedItem = null;
// 为每个列表项添加事件
list.querySelectorAll('.sortable-item').forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
});
function handleDragStart(e) {
draggedItem = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/html', this.innerHTML);
}
function handleDragEnd(e) {
this.classList.remove('dragging');
list.querySelectorAll('.sortable-item').forEach(item => {
item.classList.remove('drag-over');
});
updateNumbers();
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleDragEnter(e) {
this.classList.add('drag-over');
}
function handleDragLeave(e) {
this.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
if (draggedItem !== this) {
const allItems = [...list.querySelectorAll('.sortable-item')];
const draggedIndex = allItems.indexOf(draggedItem);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
this.parentNode.insertBefore(draggedItem, this.nextSibling);
} else {
this.parentNode.insertBefore(draggedItem, this);
}
}
this.classList.remove('drag-over');
}
// 更新序号
function updateNumbers() {
list.querySelectorAll('.sortable-item').forEach((item, index) => {
item.querySelector('.item-number').textContent = index + 1;
});
}
</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>拖放API综合示例 - 东巴文</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
text-align: center;
color: #333;
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
.section {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.section h2 {
margin-top: 0;
color: #667eea;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
/* 看板样式 */
.kanban {
display: flex;
gap: 15px;
min-height: 400px;
}
.kanban-column {
flex: 1;
background: #f9f9f9;
border-radius: 8px;
padding: 15px;
}
.kanban-column h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 2px solid #ddd;
}
.kanban-column.todo h3 { border-color: #dc3545; }
.kanban-column.doing h3 { border-color: #ffc107; }
.kanban-column.done h3 { border-color: #28a745; }
.kanban-card {
background: white;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
border-left: 4px solid #667eea;
cursor: move;
transition: all 0.3s;
}
.kanban-card:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.kanban-card.dragging {
opacity: 0.5;
}
.kanban-column.drag-over {
background: rgba(102, 126, 234, 0.1);
}
.card-title {
font-weight: bold;
margin-bottom: 5px;
}
.card-desc {
font-size: 14px;
color: #666;
margin-bottom: 10px;
}
.card-tag {
display: inline-block;
padding: 3px 8px;
background: #667eea;
color: white;
border-radius: 3px;
font-size: 12px;
}
/* 文件上传区 */
.upload-zone {
border: 3px dashed #ddd;
border-radius: 10px;
padding: 40px;
text-align: center;
transition: all 0.3s;
background: #fafafa;
}
.upload-zone.dragover {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.upload-icon {
font-size: 64px;
margin-bottom: 20px;
}
.upload-text {
font-size: 18px;
margin-bottom: 10px;
}
.upload-hint {
color: #999;
font-size: 14px;
}
/* 文件列表 */
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
align-items: center;
padding: 10px;
background: #f9f9f9;
border-radius: 5px;
margin: 5px 0;
}
.file-icon {
font-size: 24px;
margin-right: 10px;
}
.file-name {
flex: 1;
font-weight: bold;
}
.file-size {
color: #666;
font-size: 14px;
}
/* 购物车 */
.products {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.product {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
text-align: center;
cursor: move;
transition: all 0.3s;
}
.product:hover {
background: #f0f0f0;
}
.product.dragging {
opacity: 0.5;
}
.product-icon {
font-size: 48px;
margin-bottom: 10px;
}
.product-name {
font-weight: bold;
margin-bottom: 5px;
}
.product-price {
color: #667eea;
font-weight: bold;
}
.cart {
min-height: 200px;
border: 2px dashed #ddd;
border-radius: 8px;
padding: 15px;
margin-top: 20px;
transition: all 0.3s;
}
.cart.drag-over {
border-color: #667eea;
background: rgba(102, 126, 234, 0.05);
}
.cart-item {
display: flex;
align-items: center;
padding: 10px;
background: white;
border-radius: 5px;
margin: 5px 0;
}
.cart-item-icon {
font-size: 24px;
margin-right: 10px;
}
.cart-item-name {
flex: 1;
}
.cart-item-price {
color: #667eea;
font-weight: bold;
}
.cart-total {
margin-top: 15px;
padding-top: 15px;
border-top: 2px solid #ddd;
text-align: right;
font-size: 18px;
font-weight: bold;
}
/* 统计信息 */
.stats {
display: flex;
gap: 20px;
margin-top: 20px;
}
.stat-item {
flex: 1;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
color: #667eea;
}
.stat-label {
color: #666;
margin-top: 5px;
}
</style>
</head>
<body>
<h1>拖放API综合示例</h1>
<div class="container">
<!-- 看板 -->
<div class="section">
<h2>任务看板</h2>
<div class="kanban">
<div class="kanban-column todo" data-status="todo">
<h3>待办</h3>
<div class="kanban-card" draggable="true" data-id="1">
<div class="card-title">学习HTML基础</div>
<div class="card-desc">完成HTML基础教程的学习</div>
<span class="card-tag">学习</span>
</div>
<div class="kanban-card" draggable="true" data-id="2">
<div class="card-title">练习CSS布局</div>
<div class="card-desc">完成Flexbox和Grid布局练习</div>
<span class="card-tag">练习</span>
</div>
</div>
<div class="kanban-column doing" data-status="doing">
<h3>进行中</h3>
<div class="kanban-card" draggable="true" data-id="3">
<div class="card-title">JavaScript教程</div>
<div class="card-desc">正在学习JavaScript高级特性</div>
<span class="card-tag">学习</span>
</div>
</div>
<div class="kanban-column done" data-status="done">
<h3>已完成</h3>
<div class="kanban-card" draggable="true" data-id="4">
<div class="card-title">环境搭建</div>
<div class="card-desc">开发环境已配置完成</div>
<span class="card-tag">完成</span>
</div>
</div>
</div>
</div>
<!-- 文件上传 -->
<div class="section">
<h2>文件上传</h2>
<div class="upload-zone" id="uploadZone">
<div class="upload-icon">📁</div>
<div class="upload-text">拖放文件到这里上传</div>
<div class="upload-hint">支持图片、文档、压缩包等文件</div>
</div>
<div class="file-list" id="fileList"></div>
</div>
<!-- 购物车 -->
<div class="section">
<h2>购物车</h2>
<div class="products">
<div class="product" draggable="true" data-name="HTML教程" data-price="99">
<div class="product-icon">📚</div>
<div class="product-name">HTML教程</div>
<div class="product-price">¥99</div>
</div>
<div class="product" draggable="true" data-name="CSS教程" data-price="99">
<div class="product-icon">🎨</div>
<div class="product-name">CSS教程</div>
<div class="product-price">¥99</div>
</div>
<div class="product" draggable="true" data-name="JavaScript教程" data-price="149">
<div class="product-icon">⚡</div>
<div class="product-name">JavaScript教程</div>
<div class="product-price">¥149</div>
</div>
<div class="product" draggable="true" data-name="Vue教程" data-price="199">
<div class="product-icon">💚</div>
<div class="product-name">Vue教程</div>
<div class="product-price">¥199</div>
</div>
</div>
<div class="cart" id="cart">
<p style="text-align: center; color: #999;">拖放商品到这里添加到购物车</p>
</div>
</div>
<!-- 统计 -->
<div class="section">
<h2>操作统计</h2>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="dragCount">0</div>
<div class="stat-label">拖拽次数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="dropCount">0</div>
<div class="stat-label">放置次数</div>
</div>
<div class="stat-item">
<div class="stat-value" id="fileCount">0</div>
<div class="stat-label">上传文件数</div>
</div>
</div>
</div>
</div>
<script>
// 统计
let stats = {
dragCount: 0,
dropCount: 0,
fileCount: 0
};
function updateStats() {
document.getElementById('dragCount').textContent = stats.dragCount;
document.getElementById('dropCount').textContent = stats.dropCount;
document.getElementById('fileCount').textContent = stats.fileCount;
}
// ========== 看板 ==========
const kanbanCards = document.querySelectorAll('.kanban-card');
const kanbanColumns = document.querySelectorAll('.kanban-column');
kanbanCards.forEach(card => {
card.addEventListener('dragstart', function(e) {
this.classList.add('dragging');
e.dataTransfer.setData('text/plain', this.dataset.id);
stats.dragCount++;
updateStats();
});
card.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
});
kanbanColumns.forEach(column => {
column.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
column.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
column.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
column.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const id = e.dataTransfer.getData('text/plain');
const card = document.querySelector(`.kanban-card[data-id="${id}"]`);
if (card) {
this.appendChild(card);
stats.dropCount++;
updateStats();
}
});
});
// ========== 文件上传 ==========
const uploadZone = document.getElementById('uploadZone');
const fileList = document.getElementById('fileList');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
uploadZone.addEventListener('dragenter', function() {
this.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', function() {
this.classList.remove('dragover');
});
uploadZone.addEventListener('drop', function(e) {
this.classList.remove('dragover');
const files = e.dataTransfer.files;
handleFiles(files);
});
function handleFiles(files) {
[...files].forEach(file => {
displayFile(file);
stats.fileCount++;
updateStats();
});
}
function displayFile(file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
let icon = '📄';
if (file.type.startsWith('image/')) icon = '🖼️';
else if (file.type.startsWith('video/')) icon = '🎬';
else if (file.type === 'application/pdf') icon = '📕';
const size = formatFileSize(file.size);
fileItem.innerHTML = `
<div class="file-icon">${icon}</div>
<div class="file-name">${file.name}</div>
<div class="file-size">${size}</div>
`;
fileList.appendChild(fileItem);
}
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 Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
// ========== 购物车 ==========
const products = document.querySelectorAll('.product');
const cart = document.getElementById('cart');
let cartItems = [];
products.forEach(product => {
product.addEventListener('dragstart', function(e) {
this.classList.add('dragging');
e.dataTransfer.setData('application/json', JSON.stringify({
name: this.dataset.name,
price: this.dataset.price
}));
stats.dragCount++;
updateStats();
});
product.addEventListener('dragend', function() {
this.classList.remove('dragging');
});
});
cart.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
});
cart.addEventListener('dragenter', function(e) {
e.preventDefault();
this.classList.add('drag-over');
});
cart.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
cart.addEventListener('drop', function(e) {
e.preventDefault();
this.classList.remove('drag-over');
const data = JSON.parse(e.dataTransfer.getData('application/json'));
if (!cartItems.find(item => item.name === data.name)) {
cartItems.push(data);
updateCart();
stats.dropCount++;
updateStats();
}
});
function updateCart() {
if (cartItems.length === 0) {
cart.innerHTML = '<p style="text-align: center; color: #999;">拖放商品到这里添加到购物车</p>';
return;
}
let total = 0;
cart.innerHTML = cartItems.map(item => {
total += parseInt(item.price);
return `
<div class="cart-item">
<div class="cart-item-icon">📦</div>
<div class="cart-item-name">${item.name}</div>
<div class="cart-item-price">¥${item.price}</div>
</div>
`;
}).join('');
cart.innerHTML += `
<div class="cart-total">
总计: ¥${total}
</div>
`;
}
// 初始化统计
updateStats();
</script>
</body>
</html>
// 推荐:提供清晰的视觉反馈
element.addEventListener('dragenter', function() {
this.classList.add('drag-over');
});
element.addEventListener('dragleave', function() {
this.classList.remove('drag-over');
});
// 不推荐:没有视觉反馈
element.addEventListener('dragover', function(e) {
e.preventDefault();
// 用户不知道是否可以放置
});
// 推荐:设置正确的拖拽效果
element.addEventListener('dragstart', function(e) {
e.dataTransfer.effectAllowed = 'move';
});
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
// 不推荐:不设置拖拽效果
element.addEventListener('dragstart', function(e) {
// 没有设置effectAllowed
});
// 推荐:提供触摸设备支持
function addTouchSupport(element) {
let touchStartX, touchStartY;
element.addEventListener('touchstart', function(e) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
});
element.addEventListener('touchmove', function(e) {
e.preventDefault();
const touch = e.touches[0];
const moveX = touch.clientX - touchStartX;
const moveY = touch.clientY - touchStartY;
// 移动元素
this.style.transform = `translate(${moveX}px, ${moveY}px)`;
});
element.addEventListener('touchend', function(e) {
// 处理放置
this.style.transform = '';
});
}
东巴文点评:原生拖放API在移动端支持不佳,需要使用触摸事件模拟或使用第三方库。
问题1:以下哪个事件在拖拽元素进入放置目标时触发?
A. dragstart B. dragenter C. dragover D. drop
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:dragenter事件在拖拽元素进入放置目标时触发。dragstart在开始拖拽时触发,dragover在拖拽元素在目标上移动时持续触发,drop在放置时触发。
问题2:要允许元素被放置,必须在哪个事件中调用preventDefault()?
A. dragenter B. dragover C. dragleave D. dragend
<details> <summary>点击查看答案</summary>答案:B
东巴文解释:必须在dragover事件中调用preventDefault(),否则drop事件不会触发。虽然dragenter中也需要阻止默认行为,但dragover是关键。
任务:创建一个图片排序应用,用户可以通过拖放重新排列图片顺序。
<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>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
}
.gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.gallery-item {
position: relative;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;
cursor: move;
transition: all 0.3s;
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-item:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.gallery-item.dragging {
opacity: 0.5;
}
.gallery-item.drag-over {
border: 3px solid #667eea;
}
.item-number {
position: absolute;
top: 10px;
left: 10px;
width: 30px;
height: 30px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
}
</style>
</head>
<body>
<h1>图片排序</h1>
<p style="text-align: center; color: #666;">拖拽图片重新排列顺序</p>
<div class="gallery" id="gallery">
<div class="gallery-item" draggable="true">
<span class="item-number">1</span>
<img src="https://picsum.photos/300/300?random=1" alt="图片1">
</div>
<div class="gallery-item" draggable="true">
<span class="item-number">2</span>
<img src="https://picsum.photos/300/300?random=2" alt="图片2">
</div>
<div class="gallery-item" draggable="true">
<span class="item-number">3</span>
<img src="https://picsum.photos/300/300?random=3" alt="图片3">
</div>
<div class="gallery-item" draggable="true">
<span class="item-number">4</span>
<img src="https://picsum.photos/300/300?random=4" alt="图片4">
</div>
<div class="gallery-item" draggable="true">
<span class="item-number">5</span>
<img src="https://picsum.photos/300/300?random=5" alt="图片5">
</div>
<div class="gallery-item" draggable="true">
<span class="item-number">6</span>
<img src="https://picsum.photos/300/300?random=6" alt="图片6">
</div>
</div>
<script>
const gallery = document.getElementById('gallery');
let draggedItem = null;
gallery.querySelectorAll('.gallery-item').forEach(item => {
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
item.addEventListener('dragenter', handleDragEnter);
item.addEventListener('dragleave', handleDragLeave);
});
function handleDragStart(e) {
draggedItem = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd() {
this.classList.remove('dragging');
gallery.querySelectorAll('.gallery-item').forEach(item => {
item.classList.remove('drag-over');
});
updateNumbers();
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
function handleDragEnter(e) {
e.preventDefault();
if (this !== draggedItem) {
this.classList.add('drag-over');
}
}
function handleDragLeave() {
this.classList.remove('drag-over');
}
function handleDrop(e) {
e.preventDefault();
if (this !== draggedItem) {
const allItems = [...gallery.querySelectorAll('.gallery-item')];
const draggedIndex = allItems.indexOf(draggedItem);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
gallery.insertBefore(draggedItem, this.nextSibling);
} else {
gallery.insertBefore(draggedItem, this);
}
}
this.classList.remove('drag-over');
}
function updateNumbers() {
gallery.querySelectorAll('.gallery-item').forEach((item, index) => {
item.querySelector('.item-number').textContent = index + 1;
});
}
</script>
</body>
</html>
</details>
东巴文(db-w.cn) - 让编程学习更有趣、更高效!