实现功能:
- 选择、取消选择单个元素、同时选择多个元素。
- 鼠标悬浮变色。
- 框选、滚动框选区域。
- 同时选择多个区域。
最终实现效果
1. HTML代码
<div class="right-bottom">
<div
class="page"
ref="pageRef"
@scroll="handleScroll"
@mouseup="handleMouseUp"
>
<table class="table" @mousemove="handleMouseMove">
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex">
<td
id="td"
v-for="(col, colIndex) in row"
:key="colIndex"
:class="{
'table-item-active': selectedCells.has(rowIndex + '-' + colIndex),
}"
:style="{
backgroundColor: selectedCells.has(rowIndex + '-' + colIndex)
? 'skyblue'
: col.color,
}"
@mousedown.prevent="startSelect(rowIndex, colIndex)"
>
<div
class="table-item"
:class="{
'item-hover': hoverPosition == `${rowIndex}-${colIndex}`,
}"
@click="handleItemClick(rowIndex, colIndex)"
@mouseenter="handleMouseEnter(rowIndex, colIndex)"
@mouseleave="handleMouseLeave(rowIndex, colIndex)"
>
{{ col.hole }}
</div>
</td>
</tr>
</table>
</div>
</div>
2. 定义全局变量
// 已选择的单元格。因为可能出现重复框选单元格的情况,所以这里最好使用Set类型。
const selectedCells = reactive(new Set([]));
// 矩阵起始坐标位置。框选矩阵在页面上的位置,使用时请根据实际情况计算
let tableStartX = 0;
let tableStartY = 0;
// 单元格宽高
let cellWidth = 63;
let cellHeight = 63;
// 是否按住ctrl
let isClickCtrl = false;
// 当前悬浮单元格坐标
let hoverPosition = ref('');
3. 实现单个元素选择、取消选择、按ctrl同时选择多个元素
/**
* @description: 点击单元格事件
* @param row 当前单元格X坐标
* @param column 当前单元格Y坐标
*/
const handleItemClick = (row: number, column: number) => {
// 按下ctrl
document.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = true;
};
// 松开ctrl
document.onkeyup = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = false;
};
// 如果按住ctrl键
if (isClickCtrl) {
// 点击了已经选中的元素,取消选中; 否则选中
if (selectedCells.has(`${row}-${column}`)) {
selectedCells.delete(`${row}-${column}`);
}else {
selectedCells.add(`${row}-${column}`);
}
}
// 如果没有点击ctrl键,则先清空所有选中的元素,然后选中当前元素
else {
selectedCells.clear();
selectedCells.add(`${row}-${column}`);
}
};
4. 鼠标悬浮变色
/**
* @description: 鼠标悬浮单元格变色
* @param row 当前单元格X坐标
* @param column 当前单元格Y坐标
*/
// 鼠标进入单元格,使用防抖降低代码重复执行次数,提高性能
const handleMouseEnter = _.debounce((row: number, column: number) => {
hoverPosition = `${row}-${column}`;
}, 100);
// 鼠标离开单元格
const handleMouseLeave = _.debounce(() => {
hoverPosition = '';
}, 50);
5. 框选、滚动框选元素功能实现
5.1 获取框选区域内的所有元素
实现步骤:
- 拿到起始单元格坐标以及结束单元格坐标。
- 根据起始结束坐标计算出框选区域内的所有单元格。
实现原理:
例1. 如起始坐标为 startCell = [0, 0],结束坐标 endCell = [2, 2]
则当前区域的元素包括:
0,0 | 0,1 | 0,2 |
---|---|---|
1,0 | 1,1 | 1,2 |
2,0 | 2,1 | 2,2 |
例2. 如起始坐标为 startCell = [3, 4],结束坐标 endCell = [1, 2]
则当前区域的元素包括:
1,2 | 1,3 | 1,4 |
---|---|---|
2,2 | 2,3 | 2,4 |
3,2 | 3,3 | 3,4 |
以此类推。。。
结论: 根据以上实例可得,我们只需要拿到起止位置X坐标和Y坐标,然后用双重for循环即可得到当前框选区域内的所有元素
获取框选区域内的所有元素方法:
/**
* @description: 根据起始坐标和结束坐标,设置选中区域
* @param startX 起始坐标x
* @param startY 起始坐标y
* @param endX 结束坐标x
* @param endY 结束坐标y
*/
const selectRange = (
startX: number,
startY: number,
endX: number,
endY: number
) => {
for (
let rowIndex = Math.min(startX, endX);
rowIndex <= Math.max(startX, endX);
rowIndex++
) {
for (
let cellIndex = Math.min(startY, endY);
cellIndex <= Math.max(startY, endY);
cellIndex++
) {
selectedCells.add(`${rowIndex}-${cellIndex}`);
}
}
};
5.2 计算框选结束坐标
实现原理:
首先分别计算出鼠标距离矩阵原点left距离和top距离,然后再用这个距离除单个单元格的宽高就能得到结束坐标。
如果矩阵框选有滚动操作,只要再加上滚动距离即可。
5.3 鼠标移动事件
// 起始单元格坐标
let startCell: [number, number] | null = null;
// 鼠标点击事件,设置起始坐标
function startSelect(rowIndex: number, cellIndex: number) {
startCell = [rowIndex, cellIndex];
}
// 鼠标抬起事件,将起始坐标设置为null
const handleMouseUp = () => {
startCell = null;
};
// 鼠标移动事件
const handleMouseMove = (e: MouseEvent) => {
debounceFun1(e);
};
const debounceFun1 = _.debounce((e: MouseEvent) => {
// 如果没有起始坐标,则不执行
if (!startCell) return;
// 按下ctrl键
document.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = true;
};
// 松开ctrl键
document.onkeyup = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = false;
};
endXY.x = e.clientX;
endXY.y = e.clientY;
// 获取鼠标移动的当前单元格坐标,并设置为框选结束坐标
let endCell: [number, number] | null = null;
endCell = [
Math.floor((e.clientY + scrollXY.y - tableStartY) / cellHeight),
Math.floor((e.clientX + scrollXY.x - tableStartX) / cellWidth),
];
// 如果鼠标位置发生变化,则清空已选择的单元格,并重新设置选中区域
if (!isClickCtrl) {
selectedCells.clear()
}
// 根据起始坐标和结束坐标,设置选中区域
selectRange(...startCell, ...endCell);
}, 50);
5.4 滚轮滚动事件
const scrollXY = reactive({ x: 0, y: 0 });
const handleScroll = (e: any) => {
scrollXY.x = e.target.scrollLeft;
scrollXY.y = e.target.scrollTop;
if (!startCell) return;
const endCell: [number, number] = [
Math.floor((endXY.y + e.target.scrollTop - tableStartY) / cellHeight),
Math.floor((endXY.x + e.target.scrollLeft - tableStartX) / cellWidth),
];
selectRange(...startCell, ...endCell);
};
6. 样式相关代码
.item-hover {
background-color: #bae0ff !important;
}
th,
td {
border: 1px solid black;
border-collapse: collapse;
}
.right-bottom {
margin-left: 200px;
height: 800px;
width: 800px;
.page {
width: 100%;
height: 100%;
overflow: scroll;
margin: 0 auto;
.table {
overflow-x: auto;
overflow-y: auto;
.table-item {
position: relative;
font-size: 13px;
display: flex;
justify-content: center;
align-items: center;
width: 60px;
height: 60px;
.coordinates {
position: absolute;
left: 0;
top: 0;
font-size: 10px;
}
}
.table-item-active {
background-color: skyblue;
}
}
}
.page ::selection {
background-color: transparent;
}
}
7. 总结
实际开发中,矩阵原点坐标可能会因为各种情况发生改变(这里默认是[0,0]),需要根据实际情况计算,否则框选区域会出现错位的情况。最后希望这个组件能帮助到有需要的人,欢迎大家提出建议!