使用 HTML + JavaScript 实现滑动框选功能(附完整代码)
效果演示

页面结构
头部操作栏
<header><h3>我的文件</h3><span id="selectedInfo">已选 0 项</span><button class="act" onclick="clearAll()">取消选择</button><button class="act primary" id="delBtn" disabled onclick="batch('delete')">删除</button><button class="act primary" id="downBtn" disabled onclick="batch('download')">下载</button></header>
主体内容区
<div id="grid"></div>
辅助选择框
<div id="selector"></div>
核心功能实现
数据准备与初始化
fake 和图标映射表 iconMap,然后遍历生成 DOM 结构并填充初始内容。var fake = [{name:'项目文档',type:'folder'},{name:'照片.zip',type:'zip'},// ...];var iconMap = {folder: '<svg viewBox="0 0 24 24" width="42" height="42"><path fill="#FFA000" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z"/></svg>',zip: '<svg viewBox="0 0 24 24" width="42" height="42"><path fill="#616161" d="M7 3H4v18h16V7l-3-3H7zm0 2v10h10V5H7zm2 2h6v2H9V7zm0 4h6v2H9v-2zm0 4h6v2H9v-2z"/></svg>',// ...};
var grid = document.getElementById('grid');var selector = document.getElementById('selector');var cards = [];fake.forEach((f,i)=>{var card = document.createElement('div');card.className = 'card';card.innerHTML = `<div class="thumb"></div><div class="name" title="${f.name}">${f.name}</div><input type="checkbox" class="check">`;card.querySelector('.thumb').innerHTML = iconMap[f.type] || iconMap.default;card.dataset.idx = i;grid.appendChild(card);cards.push(card);});
选择状态管理
Set 来维护当前被选中的索引集合,保证唯一性和高性能查找。每当选择发生变化都会调用 refreshUI() 方法同步更新 UI 层面的展示效果。var startEl = null;var selectedSet = new Set(); // 保存已选索引function refreshUI(){cards.forEach(c=>{c.classList.toggle('selected',selectedSet.has(+c.dataset.idx));c.querySelector('.check').checked = selectedSet.has(+c.dataset.idx);});var n = selectedSet.size;document.getElementById('selectedInfo').textContent = `已选 ${n} 项`;document.getElementById('delBtn').disabled = !n;document.getElementById('downBtn').disabled = !n;}
滑动框选机制
var selecting = 0, sx=0, sy=0, ex=0, ey=0;grid.addEventListener('mousedown',e=>{if(e.target.closest('.card') && e.target.tagName !== 'INPUT') return; // 点在卡片上由下面逻辑处理selecting = 1;var rect = grid.getBoundingClientRect();sx = e.clientX; sy = e.clientY;selector.style.left = sx + 'px';selector.style.top = sy + 'px';selector.style.width = selector.style.height = '0px';selector.style.display = 'block';});document.addEventListener('mousemove',e=>{if(!selecting) return;ex = e.clientX; ey = e.clientY;var x = Math.min(sx,ex), y = Math.min(sy,ey), w = Math.abs(ex-sx), h = Math.abs(ey-sy);selector.style.left = x + 'px';selector.style.top = y + 'px';selector.style.width = w + 'px';selector.style.height = h + 'px';// 实时碰撞检测var r1 = selector.getBoundingClientRect();cards.forEach(c=>{var r2 = c.getBoundingClientRect();var collide = !(r1.right<r2.left||r1.left>r2.right||r1.bottom<r2.top||r1.top>r2.bottom);var idx = +c.dataset.idx;if(collide) selectedSet.add(idx);else selectedSet.delete(idx);});refreshUI();});document.addEventListener('mouseup',e=>{if(selecting){selecting = 0;selector.style.display = 'none';}});
键盘辅助选择
grid.addEventListener('click',e=>{var card = e.target.closest('.card');if(!card) return;var idx = +card.dataset.idx;if(e.target.tagName === 'INPUT') {// 点击 checkboxtoggle(idx);startEl = card;} else if(e.ctrlKey||e.metaKey){// 点选toggle(idx);startEl = card;} else if(e.shiftKey && startEl){// 连续选var a = +startEl.dataset.idx, b = idx;var min = Math.min(a,b), max = Math.max(a,b);selectedSet.clear();for(var i=min;i<=max;i++) selectedSet.add(i);refreshUI();} else{// 普通单击selectedSet.clear(); selectedSet.add(idx);startEl = card;refreshUI();}});
扩展建议
- 搜索过滤:添加搜索框帮助用户更快找到目标
- 右键菜单:添加右键菜单提供更多操作选项
- 模式切换:增加列表模式,允许用户根据喜好切换
- 拖拽排序:支持通过拖拽来重新排列文件顺序
完整代码
<html lang="zh-CN"><head><meta charset="utf-8" /><title>滑动框选</title><meta name="viewport" content="width=device-width, initial-scale=1"/><style>* {margin: 0;padding: 0;box-sizing: border-box;-webkit-user-select: none;-moz-user-select: none;-ms-user-select: none;user-select: none;}html, body {height: 100%;}body {display: flex;flex-direction: column;background: #f6f7f9;color: #333;}header {height: 52px;background: #fff;border-bottom: 1px solid #e5e5e5;display: flex;align-items: center;padding: 0 24px;position: sticky;top: 0;z-index: 9;flex-shrink: 0;}header h3 {font-size: 18px;font-weight: 500;margin-right: auto;}header .act {margin-left: 12px;padding: 6px 14px;border: 1px solid #d0d0d0;border-radius: 4px;background: #fff;font-size: 14px;cursor: pointer;}header .act.primary {background: #06a7ff;color: #fff;border-color: #06a7ff;}header .act:disabled {opacity: .5;cursor: not-allowed;}#selectedInfo {margin-left: 12px;font-size: 14px;color: #666;}#grid {flex: 1;display: grid;grid-template-columns: repeat(auto-fill, 118px);grid-auto-rows: 148px;gap: 14px;padding: 24px;overflow-y: auto;}.card {position: relative;width: 118px;height: 148px;background: #fff;border: 1px solid #e5e5e5;border-radius: 8px;cursor: pointer;transition: transform .15s, box-shadow .15s;overflow: hidden;}.card:hover {box-shadow: 0 4px 12px rgba(0, 0, 0, .08);}.card .thumb {height: 88px;display: flex;align-items: center;justify-content: center;font-size: 42px;color: #999;}.card .name {font-size: 13px;padding: 0 8px;text-align: center;white-space: nowrap;text-overflow: ellipsis;overflow: hidden;line-height: 20px;}.card .check {position: absolute;top: 6px;left: 6px;width: 18px;height: 18px;cursor: pointer;z-index: 1;}.card.selected {border-color: #06a7ff ;}#selector {position: absolute;border: 1px dashed #06a7ff;background: rgba(6, 167, 255, .08);display: none;pointer-events: none;z-index: 8;}</style></head><body><header><h3>我的文件</h3><span id="selectedInfo">已选 0 项</span><button class="act" onclick="clearAll()">取消选择</button><button class="act primary" id="delBtn" disabled onclick="batch('delete')">删除</button><button class="act primary" id="downBtn" disabled onclick="batch('download')">下载</button></header><div id="grid"></div><div id="selector"></div><script>var fake = [{name:'项目文档',type:'folder'},{name:'照片.zip',type:'zip'},// ...];var iconMap = {folder: '<svg viewBox="0 0 24 24" width="42" height="42"><path fill="#FFA000" d="M10 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2h-8l-2-2z"/></svg>',zip: '<svg viewBox="0 0 24 24" width="42" height="42"><path fill="#616161" d="M7 3H4v18h16V7l-3-3H7zm0 2v10h10V5H7zm2 2h6v2H9V7zm0 4h6v2H9v-2zm0 4h6v2H9v-2z"/></svg>',// ...};// 渲染数据var grid = document.getElementById('grid');var selector = document.getElementById('selector');var cards = [];fake.forEach((f,i)=>{var card = document.createElement('div');card.className = 'card';card.innerHTML = `<div class="thumb"></div><div class="name" title="${f.name}">${f.name}</div><input type="checkbox" class="check">`;card.querySelector('.thumb').innerHTML = iconMap[f.type] || iconMap.default;card.dataset.idx = i;grid.appendChild(card);cards.push(card);});// 选择逻辑var startEl = null;var selectedSet = new Set(); // 保存已选索引function refreshUI(){cards.forEach(c=>{c.classList.toggle('selected',selectedSet.has(+c.dataset.idx));c.querySelector('.check').checked = selectedSet.has(+c.dataset.idx);});var n = selectedSet.size;document.getElementById('selectedInfo').textContent = `已选 ${n} 项`;document.getElementById('delBtn').disabled = !n;document.getElementById('downBtn').disabled = !n;}function toggle(idx){if(selectedSet.has(idx)) selectedSet.delete(idx);else selectedSet.add(idx);refreshUI();}// 鼠标框选var selecting = 0, sx=0, sy=0, ex=0, ey=0;grid.addEventListener('mousedown',e=>{if(e.target.closest('.card') && e.target.tagName !== 'INPUT') return; // 点在卡片上由下面逻辑处理selecting = 1;var rect = grid.getBoundingClientRect();sx = e.clientX; sy = e.clientY;selector.style.left = sx + 'px';selector.style.top = sy + 'px';selector.style.width = selector.style.height = '0px';selector.style.display = 'block';});document.addEventListener('mousemove',e=>{if(!selecting) return;ex = e.clientX; ey = e.clientY;var x = Math.min(sx,ex), y = Math.min(sy,ey), w = Math.abs(ex-sx), h = Math.abs(ey-sy);selector.style.left = x + 'px';selector.style.top = y + 'px';selector.style.width = w + 'px';selector.style.height = h + 'px';// 实时碰撞检测var r1 = selector.getBoundingClientRect();cards.forEach(c=>{var r2 = c.getBoundingClientRect();var collide = !(r1.right<r2.left||r1.left>r2.right||r1.bottom<r2.top||r1.top>r2.bottom);var idx = +c.dataset.idx;if(collide) selectedSet.add(idx);else selectedSet.delete(idx);});refreshUI();});document.addEventListener('mouseup',e=>{if(selecting){selecting = 0;selector.style.display = 'none';}});// 支持 Ctrl / Shift 多选grid.addEventListener('click',e=>{var card = e.target.closest('.card');if(!card) return;var idx = +card.dataset.idx;if(e.target.tagName === 'INPUT') {// 点击 checkboxtoggle(idx);startEl = card;} else if(e.ctrlKey||e.metaKey){// 点选toggle(idx);startEl = card;} else if(e.shiftKey && startEl){// 连续选var a = +startEl.dataset.idx, b = idx;var min = Math.min(a,b), max = Math.max(a,b);selectedSet.clear();for(var i=min;i<=max;i++) selectedSet.add(i);refreshUI();} else{// 普通单击selectedSet.clear(); selectedSet.add(idx);startEl = card;refreshUI();}});// 顶部按钮function clearAll(){ selectedSet.clear(); refreshUI(); }function batch(action){var arr = Array.from(selectedSet);if(!arr.length) return;if(action==='delete') alert('删除: ' + arr.map(i=>fake[i].name).join(', '));if(action==='download') alert('下载: ' + arr.map(i=>fake[i].name).join(', '));}</script></body></html>
THE END





