基于Vue3+TypeScript實(shí)現(xiàn)鼠標(biāo)框選功能
實(shí)現(xiàn)功能:
- 選擇、取消選擇單個(gè)元素、同時(shí)選擇多個(gè)元素。
- 鼠標(biāo)懸浮變色。
- 框選、滾動(dòng)框選區(qū)域。
- 同時(shí)選擇多個(gè)區(qū)域。
最終實(shí)現(xiàn)效果

1. HTML代碼
<div class="right-bottom">
<<img src="div" alt="" width="70%" />
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. 定義全局變量
// 已選擇的單元格。因?yàn)榭赡艹霈F(xiàn)重復(fù)框選單元格的情況,所以這里最好使用Set類型。
const selectedCells = reactive(new Set([]));
// 矩陣起始坐標(biāo)位置??蜻x矩陣在頁(yè)面上的位置,使用時(shí)請(qǐng)根據(jù)實(shí)際情況計(jì)算
let tableStartX = 0;
let tableStartY = 0;
// 單元格寬高
let cellWidth = 63;
let cellHeight = 63;
// 是否按住ctrl
let isClickCtrl = false;
// 當(dāng)前懸浮單元格坐標(biāo)
let hoverPosition = ref('');
3. 實(shí)現(xiàn)單個(gè)元素選擇、取消選擇、按ctrl同時(shí)選擇多個(gè)元素
/**
* @description: 點(diǎn)擊單元格事件
* @param row 當(dāng)前單元格X坐標(biāo)
* @param column 當(dāng)前單元格Y坐標(biāo)
*/
const handleItemClick = (row: number, column: number) => {
// 按下ctrl
document.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = true;
};
// 松開(kāi)ctrl
document.onkeyup = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = false;
};
// 如果按住ctrl鍵
if (isClickCtrl) {
// 點(diǎn)擊了已經(jīng)選中的元素,取消選中; 否則選中
if (selectedCells.has(`${row}-${column}`)) {
selectedCells.delete(`${row}-${column}`);
}else {
selectedCells.add(`${row}-${column}`);
}
}
// 如果沒(méi)有點(diǎn)擊ctrl鍵,則先清空所有選中的元素,然后選中當(dāng)前元素
else {
selectedCells.clear();
selectedCells.add(`${row}-${column}`);
}
};
4. 鼠標(biāo)懸浮變色
/**
* @description: 鼠標(biāo)懸浮單元格變色
* @param row 當(dāng)前單元格X坐標(biāo)
* @param column 當(dāng)前單元格Y坐標(biāo)
*/
// 鼠標(biāo)進(jìn)入單元格,使用防抖降低代碼重復(fù)執(zhí)行次數(shù),提高性能
const handleMouseEnter = _.debounce((row: number, column: number) => {
hoverPosition = `${row}-${column}`;
}, 100);
// 鼠標(biāo)離開(kāi)單元格
const handleMouseLeave = _.debounce(() => {
hoverPosition = '';
}, 50);
5. 框選、滾動(dòng)框選元素功能實(shí)現(xiàn)
5.1 獲取框選區(qū)域內(nèi)的所有元素
實(shí)現(xiàn)步驟:
- 拿到起始單元格坐標(biāo)以及結(jié)束單元格坐標(biāo)。
- 根據(jù)起始結(jié)束坐標(biāo)計(jì)算出框選區(qū)域內(nèi)的所有單元格。
實(shí)現(xiàn)原理:例1. 如起始坐標(biāo)為 startCell = [0, 0],結(jié)束坐標(biāo) endCell = [2, 2]
則當(dāng)前區(qū)域的元素包括:
| 0,0 | 0,1 | 0,2 |
|---|---|---|
| 1,0 | 1,1 | 1,2 |
| 2,0 | 2,1 | 2,2 |
例2. 如起始坐標(biāo)為 startCell = [3, 4],結(jié)束坐標(biāo) endCell = [1, 2]
則當(dāng)前區(qū)域的元素包括:
| 1,2 | 1,3 | 1,4 |
|---|---|---|
| 2,2 | 2,3 | 2,4 |
| 3,2 | 3,3 | 3,4 |
以此類推。。。
結(jié)論: 根據(jù)以上實(shí)例可得,我們只需要拿到起止位置X坐標(biāo)和Y坐標(biāo),然后用雙重for循環(huán)即可得到當(dāng)前框選區(qū)域內(nèi)的所有元素
獲取框選區(qū)域內(nèi)的所有元素方法:
/**
* @description: 根據(jù)起始坐標(biāo)和結(jié)束坐標(biāo),設(shè)置選中區(qū)域
* @param startX 起始坐標(biāo)x
* @param startY 起始坐標(biāo)y
* @param endX 結(jié)束坐標(biāo)x
* @param endY 結(jié)束坐標(biāo)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 計(jì)算框選結(jié)束坐標(biāo)
實(shí)現(xiàn)原理:首先分別計(jì)算出鼠標(biāo)距離矩陣原點(diǎn)left距離和top距離,然后再用這個(gè)距離除單個(gè)單元格的寬高就能得到結(jié)束坐標(biāo)。

如果矩陣框選有滾動(dòng)操作,只要再加上滾動(dòng)距離即可。
5.3 鼠標(biāo)移動(dòng)事件
// 起始單元格坐標(biāo)
let startCell: [number, number] | null = null;
// 鼠標(biāo)點(diǎn)擊事件,設(shè)置起始坐標(biāo)
function startSelect(rowIndex: number, cellIndex: number) {
startCell = [rowIndex, cellIndex];
}
// 鼠標(biāo)抬起事件,將起始坐標(biāo)設(shè)置為null
const handleMouseUp = () => {
startCell = null;
};
// 鼠標(biāo)移動(dòng)事件
const handleMouseMove = (e: MouseEvent) => {
debounceFun1(e);
};
const debounceFun1 = _.debounce((e: MouseEvent) => {
// 如果沒(méi)有起始坐標(biāo),則不執(zhí)行
if (!startCell) return;
// 按下ctrl鍵
document.onkeydown = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = true;
};
// 松開(kāi)ctrl鍵
document.onkeyup = (e: KeyboardEvent) => {
if (e.key === 'Control') isClickCtrl = false;
};
endXY.x = e.clientX;
endXY.y = e.clientY;
// 獲取鼠標(biāo)移動(dòng)的當(dāng)前單元格坐標(biāo),并設(shè)置為框選結(jié)束坐標(biāo)
let endCell: [number, number] | null = null;
endCell = [
Math.floor((e.clientY + scrollXY.y - tableStartY) / cellHeight),
Math.floor((e.clientX + scrollXY.x - tableStartX) / cellWidth),
];
// 如果鼠標(biāo)位置發(fā)生變化,則清空已選擇的單元格,并重新設(shè)置選中區(qū)域
if (!isClickCtrl) {
selectedCells.clear()
}
// 根據(jù)起始坐標(biāo)和結(jié)束坐標(biāo),設(shè)置選中區(qū)域
selectRange(...startCell, ...endCell);
}, 50);
5.4 滾輪滾動(dòng)事件
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. 樣式相關(guān)代碼
.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. 總結(jié)
實(shí)際開(kāi)發(fā)中,矩陣原點(diǎn)坐標(biāo)可能會(huì)因?yàn)楦鞣N情況發(fā)生改變(這里默認(rèn)是[0,0]),需要根據(jù)實(shí)際情況計(jì)算,否則框選區(qū)域會(huì)出現(xiàn)錯(cuò)位的情況。最后希望這個(gè)組件能幫助到有需要的人,歡迎大家提出建議!
以上就是基于Vue3+TypeScript實(shí)現(xiàn)鼠標(biāo)框選功能的詳細(xì)內(nèi)容,更多關(guān)于Vue3 TypeScript鼠標(biāo)框選的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue組件 keep-alive 和 transition 使用詳解
這篇文章主要介紹了vue組件 keep-alive 和 transition 使用詳解,需要的朋友可以參考下2019-10-10
element-ui?el-upload實(shí)現(xiàn)上傳文件及簡(jiǎn)單的上傳文件格式驗(yàn)證功能
前端上傳文件后,后端接受文件進(jìn)行處理后直接返回處理后的文件,前端直接再將文件下載下來(lái),下面這篇文章主要給大家介紹了關(guān)于element-ui?el-upload實(shí)現(xiàn)上傳文件及簡(jiǎn)單的上傳文件格式驗(yàn)證功能的相關(guān)資料,需要的朋友可以參考下2022-11-11
Vue.js 動(dòng)態(tài)為img的src賦值方法
下面小編就為大家分享一篇Vue.js 動(dòng)態(tài)為img的src賦值方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
Vue.js開(kāi)發(fā)環(huán)境快速搭建教程
這篇文章主要為大家詳細(xì)介紹了Vue.js開(kāi)發(fā)環(huán)境快速搭建教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03

