main.js:
import directive from './directive'
Vue.use(directive)
/directive/index.js:
import batchselect from './batchselect/batchselect'
const install = function (Vue) {
Vue.directive('batchselect', batchselect)
}
if (window.Vue) {
window['batchselect'] = batchselect
Vue.use(install); // eslint-disable-line
}
export default install
/directive/batchselect/batchselect.js:
//框选时 实时选中、可带动滚动条
export default {
inserted: (el, binding) => {
// console.log(el)
// console.log(binding)
// 设置元素为相对定位,确保遮罩层定位正确
el.style.position = 'relative';
// 创建一个div作为area区域,用于显示框选效果
const area = document.createElement('div');
area.style = 'position: absolute; border: 1px solid #1362b4; background: rgba(139 191 249 / 50%); z-index: 10; visibility: hidden;';
area.className = 'area';
el.appendChild(area);
// 保存相关数据的对象
let state = {
elPos: null, // 元素在视口中的位置
options: [], // 可选元素列表
optionsXYWH: [], // 可选元素的位置和尺寸
scrollOffset: { x: 0, y: 0 }, // 滚动偏移量
startX: 0, // 鼠标按下的X坐标
startY: 0, // 鼠标按下的Y坐标
currentX: 0, // 当前鼠标的X坐标
currentY: 0, // 当前鼠标的Y坐标
hasMove: false, // 是否有鼠标移动
isSelecting: false, // 是否正在框选
scrollContainer: null, // 滚动容器
scrollTimer: null, // 滚动定时器
scrollSpeed: 10, // 滚动速度
edgeThreshold: 50 // 边缘检测阈值
};
// 更新可选元素的位置信息
const updateOptionsPosition = () => {
// 获取元素在视口中的位置
const { x, y } = el.getBoundingClientRect();
state.elPos = { x, y };
// 获取可选元素
const optionClassName = binding.value.className;
state.options = [].slice.call(el.querySelectorAll(optionClassName));
// 更新可选元素的位置和尺寸
state.optionsXYWH = state.options.map(v => {
const obj = v.getBoundingClientRect();
return {
x: obj.x - state.elPos.x + state.scrollOffset.x,
y: obj.y - state.elPos.y + state.scrollOffset.y,
w: obj.width,
h: obj.height
};
});
};
// 查找最近的滚动容器
const findScrollContainer = (element) => {
let parent = element.parentElement;
console.log(parent)
while (parent && parent !== document.body) {
const overflow = window.getComputedStyle(parent).overflow;
if (overflow === 'auto' || overflow === 'scroll') {
return parent;
}
parent = parent.parentElement;
}
return null;
};
// 更新框选区域
const updateSelectionArea = () => {
if (!state.isSelecting) return;
// 计算框选区域的尺寸和位置
const width = Math.abs(state.currentX - state.startX);
const height = Math.abs(state.currentY - state.startY);
const left = Math.min(state.startX, state.currentX);
const top = Math.min(state.startY, state.currentY);
// 更新框选区域的样式
area.style.left = `${left}px`;
area.style.top = `${top}px`;
area.style.width = `${width}px`;
area.style.height = `${height}px`;
// 显示框选区域
area.style.visibility = 'visible';
};
// 检测并更新选中项
const updateSelectedItems = () => {
if (!state.isSelecting || !state.hasMove) return;
// 获取框选区域的位置和尺寸
const { left, top, width, height } = area.style;
const areaTop = parseInt(top);
const areaRight = parseInt(left) + parseInt(width);
const areaBottom = parseInt(top) + parseInt(height);
const areaLeft = parseInt(left);
// 清空之前的选择
binding.value.selectIdxs.length = 0;
// 执行碰撞检测,找出被框选的元素
state.optionsXYWH.forEach((v, i) => {
const optionTop = v.y;
const optionRight = v.x + v.w;
const optionBottom = v.y + v.h;
const optionLeft = v.x;
// 判断是否相交
if (!(areaTop > optionBottom || areaRight < optionLeft || areaBottom < optionTop || areaLeft > optionRight)) {
// 记录被选中的元素索引
binding.value.selectIdxs.push(i);
}
});
};
// 处理滚动
const handleScroll = () => {
if (!state.scrollContainer || !state.isSelecting) return;
const rect = state.scrollContainer.getBoundingClientRect();
const scrollHeight = state.scrollContainer.scrollHeight;
const clientHeight = state.scrollContainer.clientHeight;
// 计算鼠标位置与容器边缘的距离
const distanceFromTop = state.currentY - state.startY + state.elPos.y - rect.top;
const distanceFromBottom = rect.bottom - (state.currentY - state.startY + state.elPos.y);
// 顶部边缘检测
if (distanceFromTop < state.edgeThreshold && state.scrollContainer.scrollTop > 0) {
state.scrollContainer.scrollTop -= state.scrollSpeed;
}
// 底部边缘检测
else if (distanceFromBottom < state.edgeThreshold &&
state.scrollContainer.scrollTop < scrollHeight - clientHeight) {
state.scrollContainer.scrollTop += state.scrollSpeed;
}
// 重新计算滚动偏移量
state.scrollOffset.y = state.scrollContainer.scrollTop;
// 更新元素位置
updateOptionsPosition();
// 更新框选区域
updateSelectionArea();
// 更新选中项
updateSelectedItems();
};
// 开始滚动定时器
const startScrollTimer = () => {
console.log('---开始滚动定时器---')
if (state.scrollTimer) return;
state.scrollTimer = setInterval(handleScroll, 50);
};
// 停止滚动定时器
const stopScrollTimer = () => {
console.log('---停止滚动定时器---')
if (state.scrollTimer) {
clearInterval(state.scrollTimer);
state.scrollTimer = null;
}
};
// 处理滚动事件
const handleContainerScroll = () => {
if (!state.isSelecting) return;
// 更新滚动偏移量
state.scrollOffset.y = state.scrollContainer.scrollTop;
// 更新元素位置
updateOptionsPosition();
// 更新框选区域
updateSelectionArea();
// 更新选中项
updateSelectedItems();
};
// 初始化滚动事件监听
const initScrollListener = () => {
// state.scrollContainer = findScrollContainer(el);
state.scrollContainer = el.querySelector(binding.value.scrollelm);
console.log(state.scrollContainer)//滚动容器
if (state.scrollContainer) {
state.scrollContainer.addEventListener('scroll', handleContainerScroll);
// 保存清理函数
state.cleanupScroll = () => {
state.scrollContainer.removeEventListener('scroll', handleContainerScroll);
};
}
};
// 初始化框选功能
const initSelect = () => {
el.onmousedown = (e) => {
e.preventDefault();
// 更新元素位置信息
updateOptionsPosition();
// 获取鼠标按下时相对元素的坐标
state.startX = e.clientX - state.elPos.x + state.scrollOffset.x;
state.startY = e.clientY - state.elPos.y + state.scrollOffset.y;
state.currentX = state.startX;
state.currentY = state.startY;
state.hasMove = false;
state.isSelecting = true;
// 显示框选区域
area.style.visibility = 'visible';
// 鼠标移动事件
document.onmousemove = (e) => {
e.preventDefault();
state.hasMove = true;
// 更新当前鼠标位置
state.currentX = e.clientX - state.elPos.x + state.scrollOffset.x;
state.currentY = e.clientY - state.elPos.y + state.scrollOffset.y;
// 更新框选区域
updateSelectionArea();
// 更新选中项
updateSelectedItems();
// 开始滚动检测
startScrollTimer();
};
// 鼠标抬起事件
document.onmouseup = (e) => {
document.onmousemove = document.onmouseup = null;
state.isSelecting = false;
// 停止滚动检测
stopScrollTimer();
// 如果有移动,处理选中项
// if (state.hasMove) {
// // 滚动到选中的元素位置
// if (state.scrollContainer && binding.value.selectIdxs.length > 0) {
// const firstSelectedIndex = binding.value.selectIdxs[0];
// const selectedElement = state.options[firstSelectedIndex];
// if (selectedElement) {
// selectedElement.scrollIntoView({
// behavior: 'smooth',
// block: 'nearest'
// });
// }
// }
// }
// 重置框选区域
area.style.visibility = 'hidden';
area.style.left = '0';
area.style.top = '0';
area.style.width = '0';
area.style.height = '0';
return false;
};
};
};
// 初始化
updateOptionsPosition();
initScrollListener();
initSelect();
// 保存清理函数
el.__batchselect_cleanup = () => {
el.onmousedown = null;
document.onmousemove = null;
document.onmouseup = null;
stopScrollTimer();
if (state.cleanupScroll) {
state.cleanupScroll();
}
};
},
unbind: (el) => {
// 清理事件监听
if (el.__batchselect_cleanup) {
el.__batchselect_cleanup();
delete el.__batchselect_cleanup;
}
}
}
demo:
<template>
<div class="dragcontainer">
<div class="cardbox" v-batchselect="{ className: '.tlpItem',selectIdxs,scrollelm:'.cardList'}">
<el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="checkCardAllChange">全选</el-checkbox>
<div class="cardList">
<el-tooltip class="tlpItem" effect="dark" placement="bottom" v-for="(item,index) in tableData" :key="index">
<div slot="content">123<br />456</div>
<div class="cardItem">
<el-checkbox class="checked" v-model="item.checked" @change="checkCardChange(item)"></el-checkbox>
<p class="name">{{ index }}</p>
<p class="date">{{ item.date }}</p>
</div>
</el-tooltip>
</div>
</div>
</div>
</template>
<script>
export default {
name: "drag",
data() {
return {
isIndeterminate: false,
checkAll: false,
tableData: [
{
id: 0,
name: '111',
date: '2025-06-12',
checked: false,
}, {
id: 1,
name: '222',
date: '2025-06-12',
checked: false,
}, {
id: 2,
name: '333',
date: '2025-06-12',
checked: false,
}, {
id: 3,
name: '444',
date: '2025-06-12',
checked: false,
}, {
id: 4,
name: '555',
date: '2025-06-12',
checked: false,
}, {
id: 5,
name: '666',
date: '2025-06-12',
checked: false,
}, {
id: 6,
name: '777',
date: '2025-06-12',
checked: false,
}, {
id: 7,
name: '888',
date: '2025-06-12',
checked: false,
}, {
id: 8,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 9,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 10,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 11,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 12,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 13,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 14,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 15,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 16,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 17,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 18,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 19,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 20,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 21,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 22,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 23,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 24,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 25,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 26,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 27,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 28,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 29,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 30,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 31,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 32,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 33,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 34,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 35,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 36,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 37,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 38,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 39,
name: '999',
date: '2025-06-12',
checked: false,
}, {
id: 40,
name: '999',
date: '2025-06-12',
checked: false,
},
],
SelectRows: [],
selectIdxs: [],
};
},
watch: {
// 监听自定义指令v-batchselect返回的selectIdxs
selectIdxs(idxs) {
// console.log(idxs)//[0,1,2,...]
this.tableData.forEach((v, i) => {
v.checked = idxs.indexOf(i) === -1 ? false : true;
});
this.$nextTick(() => {
this.checkCardChange();
});
},
},
created() {
},
methods: {
// 全选
checkCardAllChange(val) {
this.SelectRows = val ? this.tableData : [];
this.isIndeterminate = false;
for (var i = 0; i < this.tableData.length; i++) {
this.tableData[i].checked = val
}
},
// 单选
checkCardChange() {
this.$nextTick(() => {
this.SelectRows = [];
let checknum = 0;
for (var i = 0; i < this.tableData.length; i++) {
if (this.tableData[i].checked) {
checknum++;
this.SelectRows.push(this.tableData[i])
}
}
this.checkAll = checknum === this.tableData.length;
this.isIndeterminate = checknum > 0 && checknum < this.tableData.length;
})
},
}
};
</script>
<style>
html,
body {
overflow: hidden;
}
</style>
<style scoped>
.dragcontainer {
width: 100%;
height: 100%;
padding: 20px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
overflow: hidden;
overflow: auto;
}
.cardbox {
width: 100%;
height: 80%;
overflow: hidden;
}
.cardList {
display: flex;
flex-wrap: wrap;
height: 100%;
border: 1px solid #f00;
overflow: auto;
}
.cardList .cardItem {
width: 150px;
height: 150px;
border: 1px solid #bedfff;
margin: 10px;
padding: 10px;
text-align: center;
}
.cardList .cardItem:hover {
background: #bedfff;
}
</style>
参考:vue鼠标批量框选复选框