lk-use-table-td-select
v1.1.7
Published
实现table下的td选取功能。
Readme
安装
# 首先确保安装了Vue 3
npm install vue@^3.0.0
# 然后安装本库
npm install lk-use-table-td-select使用
import { useTableTdSelect } from 'lk-use-table-td-select'具体使用环境与方法
<style lang="scss" scoped>
.lk-table {
border-collapse: collapse;
td,
th {
border: 1px solid #e2eaf6;
min-width: 200px;
min-height: 40px;
font-size: 20px;
}
td {
cursor: cell;
user-select: none;
}
}
.out-box {
width: 800px;
height: 800px;
overflow: auto;
margin: 50px;
border: #e2eaf6 1px solid;
padding: 20px;
.single_selection {
position: absolute;
background: #ffffff00;
width: 100px;
height: 100px;
border-color: rgba(16, 153, 104, 0.3);
border-width: 0px 100px 100px 0px;
box-shadow: rgb(16, 153, 104) 0px 0px 0px 2px;
top: 0;
left: 0;
pointer-events: none;
cursor: cell;
//box-sizing: content-box;
// 不可以这么使用,这么使用的话,box-shadow作用就失去了
.single-dot {
width: 6px;
height: 6px;
position: absolute;
transform: translate(-3px, -3px);
background: rgb(153, 71, 16);
top: 0;
left: 0;
pointer-events: none;
}
}
.single_selection_corner {
width: 6px;
height: 6px;
position: absolute;
transform: translate(-3px, -3px);
background: rgb(16, 153, 104);
top: 0;
left: 0;
pointer-events: none;
}
h1 {
all: unset;
}
}
</style>
<template>
<div>
<h1>USE</h1>
<div class="out-box" ref="outBoxRef">
<div style="position: relative">
<table class="lk-table" ref="tableRef">
<thead>
<tr>
<th v-for="(header, index) in table_header" :key="index">{{ header }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, y) in table_data" :key="y">
<template v-for="(cell, x) in row">
<td
:key="x"
align="center"
:colspan="cell.colspan || 1"
:rowspan="cell.rowspan || 1"
v-if="!cell.merged || cell.isMaster"
>
<p>{{ cell.content || cell }}</p>
</td>
</template>
</tr>
</tbody>
</table>
<div class="single_selection" id="et_select_grid"></div>
<div class="single_selection_corner"></div>
</div>
</div>
<el-button type="primary" @click="mergeCells">合并单元格</el-button>
<el-button type="primary" @click="splitCells">拆分单元格</el-button>
<el-button type="primary" @click="clearRect">清除选中区域1</el-button>
</div>
</template>
<script setup>
const table_header = [
'培训日期',
'培训时间',
'培训岗位',
'培训主题',
'培训内容',
'培训方式',
'备注'
];
const table_data = ref(Array.from({ length: 77 }, () => Array.from({ length: 7 }, () => '数据')));
// 禁用table元素上面的右键
const table_ref = useTemplateRef('tableRef');
import { useTableTdSelect } from 'lk-use-table-td-select';
const { is_selecting, mousedown_dom_info, rect_select_elements, track_rect, clearRect } =
useTableTdSelect(table_ref, {
select_table_tbody: 'tbody',
et_select_grid: '#et_select_grid',
single_selection_corner: '.single_selection_corner'
});
function mergeCells() {
// 检查是否有选中的单元格
if (!rect_select_elements.value || rect_select_elements.value.length === 0) {
console.warn('没有选中的单元格');
return;
}
// 获取选中区域的边界
const positions = rect_select_elements.value.map((item) => item.position);
const minX = Math.min(...positions.map((pos) => pos.x));
const minY = Math.min(...positions.map((pos) => pos.y));
const maxX = Math.max(...positions.map((pos) => pos.x + pos.width));
const maxY = Math.max(...positions.map((pos) => pos.y + pos.height));
// 获取选中区域的行列范围
const topLeftElement = rect_select_elements.value.find(
(item) => item.position.x === minX && item.position.y === minY
);
const bottomRightElement = rect_select_elements.value.find(
(item) =>
item.position.x + item.position.width === maxX &&
item.position.y + item.position.height === maxY
);
if (!topLeftElement || !bottomRightElement) {
console.error('无法确定选中区域的边界');
return;
}
// 获取左上角和右下角单元格的行列索引
const topLeftPosition = getCellPosition(topLeftElement.element);
const bottomRightPosition = getCellPosition(bottomRightElement.element);
// 检查是否已经有合并的单元格在这个区域内
let hasMergedCells = false;
for (let i = topLeftPosition.row; i <= bottomRightPosition.row; i++) {
for (let j = topLeftPosition.col; j <= bottomRightPosition.col; j++) {
const cell = table_data.value[i][j];
if (cell.merged && cell.isMaster) {
hasMergedCells = true;
break;
}
}
if (hasMergedCells) break;
}
if (hasMergedCells) {
console.warn('选中区域内已有合并单元格,无法再次合并');
return;
}
// 计算合并的行列数
const rowSpan = bottomRightPosition.row - topLeftPosition.row + 1;
const colSpan = bottomRightPosition.col - topLeftPosition.col + 1;
// 更新table_data数据结构以支持合并
// 将table_data转换为支持合并的结构
if (typeof table_data.value[0][0] === 'string') {
// 如果是原始数据结构,转换为支持合并的结构
for (let i = 0; i < table_data.value.length; i++) {
for (let j = 0; j < table_data.value[i].length; j++) {
table_data.value[i][j] = {
content: table_data.value[i][j],
colspan: 1,
rowspan: 1,
merged: false,
isMaster: false
};
}
}
}
// 设置左上角单元格为合并的主单元格
const masterCell = table_data.value[topLeftPosition.row][topLeftPosition.col];
masterCell.colspan = colSpan;
masterCell.rowspan = rowSpan;
masterCell.merged = true;
masterCell.isMaster = true;
// 隐藏被合并的其他单元格
for (let i = topLeftPosition.row; i <= bottomRightPosition.row; i++) {
for (let j = topLeftPosition.col; j <= bottomRightPosition.col; j++) {
if (i !== topLeftPosition.row || j !== topLeftPosition.col) {
// 标记为被合并的单元格
table_data.value[i][j].merged = true;
table_data.value[i][j].isMaster = false;
}
}
}
console.log('单元格合并完成', {
topLeft: topLeftPosition,
bottomRight: bottomRightPosition,
rowSpan,
colSpan
});
}
function splitCells() {
// 检查是否有选中的单元格
if (!rect_select_elements.value || rect_select_elements.value.length === 0) {
console.warn('没有选中的单元格');
return;
}
// 用于跟踪已处理的主单元格,避免重复处理
const processedMasterCells = new Set();
// 遍历所有选中的单元格
rect_select_elements.value.forEach((item) => {
const cellElement = item.element;
const position = getCellPosition(cellElement);
console.log('🚀 ~ splitCells ~ position:', position);
// 检查位置有效性
if (
position.row < 0 ||
position.col < 0 ||
position.row >= table_data.value.length ||
position.col >= table_data.value[position.row].length
) {
console.warn('无效的单元格位置:', position);
return;
}
// 获取对应的单元格数据
const cellData = table_data.value[position.row][position.col];
// 如果是合并的主单元格,则拆分它
if (cellData.isMaster && cellData.merged) {
// 创建唯一标识符以避免重复处理
const cellKey = `${position.row}-${position.col}`;
if (processedMasterCells.has(cellKey)) {
return; // 已经处理过这个主单元格
}
processedMasterCells.add(cellKey);
// 保存原始的colspan和rowspan值用于计算影响范围
const originalColspan = cellData.colspan || 1;
const originalRowspan = cellData.rowspan || 1;
// 重置主单元格属性
cellData.colspan = 1;
cellData.rowspan = 1;
cellData.merged = false;
cellData.isMaster = false;
// 计算需要恢复显示的单元格范围
const startRow = position.row;
const endRow = Math.min(position.row + originalRowspan, table_data.value.length);
const startCol = position.col;
const endCol = Math.min(
position.col + originalColspan,
table_data.value[position.row].length
);
// 恢复被合并的单元格的显示
for (let i = startRow; i < endRow; i++) {
for (let j = startCol; j < endCol; j++) {
// 跳过主单元格本身
if (i === startRow && j === startCol) {
continue;
}
// 检查数组边界
if (i < table_data.value.length && j < table_data.value[i].length) {
const targetCell = table_data.value[i][j];
// 只有被标记为合并的单元格才需要恢复
if (targetCell.merged) {
targetCell.merged = false;
targetCell.isMaster = false;
// 保持原有的colspan和rowspan值(通常应该为1)
targetCell.colspan = targetCell.colspan || 1;
targetCell.rowspan = targetCell.rowspan || 1;
}
}
}
}
console.log(`拆分单元格完成: 行${position.row}, 列${position.col}`, {
originalColspan,
originalRowspan,
affectedRange: {
startRow,
endRow,
startCol,
endCol
}
});
}
// 如果是被合并的单元格(不是主单元格),我们需要找到它的主单元格并拆分
else if (cellData.merged && !cellData.isMaster) {
// 查找包含此单元格的主单元格
const masterCellInfo = findMasterCellForPosition(position.row, position.col);
if (masterCellInfo) {
const { masterRow, masterCol } = masterCellInfo;
const cellKey = `${masterRow}-${masterCol}`;
// 避免重复处理
if (processedMasterCells.has(cellKey)) {
return;
}
processedMasterCells.add(cellKey);
const masterCell = table_data.value[masterRow][masterCol];
// 保存原始的colspan和rowspan值用于计算影响范围
const originalColspan = masterCell.colspan || 1;
const originalRowspan = masterCell.rowspan || 1;
// 重置主单元格属性
masterCell.colspan = 1;
masterCell.rowspan = 1;
masterCell.merged = false;
masterCell.isMaster = false;
// 计算需要恢复显示的单元格范围
const startRow = masterRow;
const endRow = Math.min(masterRow + originalRowspan, table_data.value.length);
const startCol = masterCol;
const endCol = Math.min(masterCol + originalColspan, table_data.value[masterRow].length);
// 恢复被合并的单元格的显示
for (let i = startRow; i < endRow; i++) {
for (let j = startCol; j < endCol; j++) {
// 跳过主单元格本身
if (i === startRow && j === startCol) {
continue;
}
// 检查数组边界
if (i < table_data.value.length && j < table_data.value[i].length) {
const targetCell = table_data.value[i][j];
// 只有被标记为合并的单元格才需要恢复
if (targetCell.merged) {
targetCell.merged = false;
targetCell.isMaster = false;
// 保持原有的colspan和rowspan值(通常应该为1)
targetCell.colspan = targetCell.colspan || 1;
targetCell.rowspan = targetCell.rowspan || 1;
}
}
}
}
console.log(`拆分单元格完成: 行${masterRow}, 列${masterCol}`, {
originalColspan,
originalRowspan,
affectedRange: {
startRow,
endRow,
startCol,
endCol
}
});
}
}
});
console.log('所有选中单元格的拆分操作完成');
}
/**
* 查找包含指定位置的主单元格
* @param {number} row - 目标行索引
* @param {number} col - 目标列索引
* @returns {Object|null} 主单元格的位置信息 {masterRow, masterCol} 或 null
*/
function findMasterCellForPosition(row, col) {
// 遍历所有单元格查找主单元格
for (let i = 0; i < table_data.value.length; i++) {
for (let j = 0; j < table_data.value[i].length; j++) {
const cell = table_data.value[i][j];
// 检查是否为主单元格且处于合并状态
if (cell.isMaster && cell.merged) {
const rowspan = cell.rowspan || 1;
const colspan = cell.colspan || 1;
// 检查目标位置是否在此主单元格的范围内
if (row >= i && row < i + rowspan && col >= j && col < j + colspan) {
return { masterRow: i, masterCol: j };
}
}
}
}
return null; // 未找到主单元格
}
function getCellPosition(tdElement) {
// 通过table_data数据来确认单元格位置
// 获取DOM元素的行列信息
const rowElement = tdElement.parentElement;
const rowIndex = Array.from(rowElement.parentElement.children).indexOf(rowElement);
const domColIndex = Array.from(rowElement.children).indexOf(tdElement);
// 检查table_data的有效性
if (rowIndex >= 0 && rowIndex < table_data.value.length) {
const rowData = table_data.value[rowIndex];
// 需要考虑到前面单元格的colspan影响
// 计算实际的数据列索引
let visibleCellCount = 0; // 已经遇到的可见单元格数量
for (let colIndex = 0; colIndex < rowData.length; colIndex++) {
const cellData = rowData[colIndex];
// 如果当前单元格是可见的(不是被合并的单元格)
if (!cellData.merged || cellData.isMaster) {
// 如果我们已经到达目标DOM列索引
if (visibleCellCount === domColIndex) {
return { row: rowIndex, col: colIndex };
}
visibleCellCount++;
}
}
}
// 回退方案:如果无法通过table_data确定,则使用原有的DOM方式
return { row: rowIndex, col: domColIndex };
}
// 渲染右键选择逻辑
import VNodeContext from './vNodeContext.vue';
import { render } from 'vue';
/**
* 判断选中的单元格是否可以合并
* @returns {boolean} 是否可以合并
*/
const isCanMergeCells = computed(() => {
// 检查是否有选中的单元格
if (!rect_select_elements.value || rect_select_elements.value.length === 0) {
return false;
}
// 至少需要选中2个单元格才能合并
if (rect_select_elements.value.length < 2) {
return false;
}
try {
// 获取选中区域的边界
const positions = rect_select_elements.value.map((item) => item.position);
const minX = Math.min(...positions.map((pos) => pos.x));
const minY = Math.min(...positions.map((pos) => pos.y));
const maxX = Math.max(...positions.map((pos) => pos.x + pos.width));
const maxY = Math.max(...positions.map((pos) => pos.y + pos.height));
// 获取选中区域的行列范围
const topLeftElement = rect_select_elements.value.find(
(item) => item.position.x === minX && item.position.y === minY
);
const bottomRightElement = rect_select_elements.value.find(
(item) =>
item.position.x + item.position.width === maxX &&
item.position.y + item.position.height === maxY
);
if (!topLeftElement || !bottomRightElement) {
return false;
}
// 获取左上角和右下角单元格的行列索引
const topLeftPosition = getCellPosition(topLeftElement.element);
const bottomRightPosition = getCellPosition(bottomRightElement.element);
// 检查是否已经有合并的单元格在这个区域内
for (let i = topLeftPosition.row; i <= bottomRightPosition.row; i++) {
for (let j = topLeftPosition.col; j <= bottomRightPosition.col; j++) {
// 检查数组边界
if (i < table_data.value.length && j < table_data.value[i].length) {
const cell = table_data.value[i][j];
if (cell.merged && cell.isMaster) {
return false; // 区域内已有合并单元格,无法再次合并
}
}
}
}
// 检查选中的单元格是否构成一个连续的矩形区域
const selectedCellPositions = rect_select_elements.value.map((item) =>
getCellPosition(item.element)
);
// 检查是否所有选中的单元格都在矩形区域内
const allSelectedInRect = selectedCellPositions.every(
(pos) =>
pos.row >= topLeftPosition.row &&
pos.row <= bottomRightPosition.row &&
pos.col >= topLeftPosition.col &&
pos.col <= bottomRightPosition.col
);
if (!allSelectedInRect) {
return false;
}
// 检查矩形区域内是否包含所有应该选中的单元格
const expectedCellCount =
(bottomRightPosition.row - topLeftPosition.row + 1) *
(bottomRightPosition.col - topLeftPosition.col + 1);
if (rect_select_elements.value.length !== expectedCellCount) {
return false; // 选中的单元格不构成完整的矩形
}
return true;
} catch (error) {
console.error('判断合并条件时出错:', error);
return false;
}
});
/**
* 判断选中的单元格是否可以拆分
* @returns {boolean} 是否可以拆分
*/
const isCanSplitCells = computed(() => {
// 检查是否有选中的单元格
if (!rect_select_elements.value || rect_select_elements.value.length === 0) {
return false;
}
try {
// 检查选中的单元格中是否包含已合并的单元格
const hasMergedCells = rect_select_elements.value.some((item) => {
const position = getCellPosition(item.element);
// 检查位置有效性
if (
position.row < 0 ||
position.col < 0 ||
position.row >= table_data.value.length ||
position.col >= table_data.value[position.row].length
) {
return false;
}
const cellData = table_data.value[position.row][position.col];
return cellData.merged;
});
if (!hasMergedCells) {
return false; // 没有合并的单元格,无法拆分
}
// 检查选中的合并单元格是否可以被拆分
const processedMasterCells = new Set();
for (const item of rect_select_elements.value) {
const position = getCellPosition(item.element);
// 检查位置有效性
if (
position.row < 0 ||
position.col < 0 ||
position.row >= table_data.value.length ||
position.col >= table_data.value[position.row].length
) {
continue;
}
const cellData = table_data.value[position.row][position.col];
// 如果是合并的主单元格
if (cellData.isMaster && cellData.merged) {
const cellKey = `${position.row}-${position.col}`;
if (!processedMasterCells.has(cellKey)) {
processedMasterCells.add(cellKey);
// 主单元格可以直接拆分
return true;
}
}
// 如果是被合并的单元格(不是主单元格)
else if (cellData.merged && !cellData.isMaster) {
// 查找包含此单元格的主单元格
const masterCellInfo = findMasterCellForPosition(position.row, position.col);
if (masterCellInfo) {
const { masterRow, masterCol } = masterCellInfo;
const cellKey = `${masterRow}-${masterCol}`;
if (!processedMasterCells.has(cellKey)) {
processedMasterCells.add(cellKey);
// 找到对应的主单元格,可以拆分
return true;
}
}
}
}
return processedMasterCells.size > 0;
} catch (error) {
console.error('判断拆分条件时出错:', error);
return false;
}
});
const outBoxRef = useTemplateRef('outBoxRef');
onMounted(() => {
document.addEventListener('contextmenu', (e) => {
// 检查是否在表格区域内
if (!table_ref.value.contains(e.target)) return;
e.preventDefault();
// 获取outBoxRef的滚动数据
const scrollLeft = outBoxRef.value.scrollLeft;
const scrollTop = outBoxRef.value.scrollTop;
// 使用outBoxRef的getBoundingClientRect()数据进行位置校验
const outBoxRect = outBoxRef.value.getBoundingClientRect();
// 获取选择框的边界数据
const selectionBoxRect = track_rect.value;
// 检查是否在选择框内 - 基于outBoxRef的位置数据进行校验
let isInSelectionBox = false;
if (selectionBoxRect && selectionBoxRect.length > 0) {
// 获取选择框的四个角点
const topLeft = selectionBoxRect.find((item) => item.corner === 'top-left');
const topRight = selectionBoxRect.find((item) => item.corner === 'top-right');
const bottomLeft = selectionBoxRect.find((item) => item.corner === 'bottom-left');
const bottomRight = selectionBoxRect.find((item) => item.corner === 'bottom-right');
if (topLeft && topRight && bottomLeft && bottomRight) {
// 基于outBoxRef的位置计算选择框的实际位置
const selectionBoxX = outBoxRect.left + topLeft.x - scrollLeft;
const selectionBoxY = outBoxRect.top + topLeft.y - scrollTop;
const selectionBoxWidth = topRight.x - topLeft.x;
const selectionBoxHeight = bottomLeft.y - topLeft.y;
// 检查鼠标是否在选择框内(基于outBoxRef的绝对位置)
isInSelectionBox =
e.clientX >= selectionBoxX &&
e.clientX <= selectionBoxX + selectionBoxWidth &&
e.clientY >= selectionBoxY &&
e.clientY <= selectionBoxY + selectionBoxHeight;
if (isInSelectionBox) {
const vNode = h(VNodeContext, {
isShow: true,
style: {
top: `${e.clientY}px`,
left: `${e.clientX}px`
},
onMergeCells: () => {
if (isCanMergeCells.value) {
mergeCells();
render(h(VNodeContext, { isShow: false }), document.body);
}
},
onSplitCells: () => {
if (isCanSplitCells.value) {
splitCells();
render(h(VNodeContext, { isShow: false }), document.body);
}
},
isCanMergeCells: isCanMergeCells.value,
isCanSplitCells: isCanSplitCells.value
});
render(vNode, document.body);
}
}
}
});
document.addEventListener('click', (e) => {
if (table_ref.value.contains(e.target)) {
const vNode = h(VNodeContext);
render(vNode, document.body);
}
});
const tbody_target = table_ref.value.querySelector('tbody');
tbody_target.addEventListener('mousemove', (event) => {
if (!is_selecting.value) return;
// 自动滚动功能 - 当鼠标靠近容器边界时
handleAutoScroll(event, '.out-box', 100, 60);
});
});
/**
* 处理容器自动滚动功能 - 当鼠标靠近容器边界时自动滚动
* @param {MouseEvent} event - 鼠标移动事件对象
* @param {string|HTMLElement} rectScrollDom - 滚动容器的选择器或DOM元素
* @param {number} scrollThreshold - 触发滚动的边界距离(像素)
* @param {number} scrollSpeed - 滚动速度(像素)
*/
function handleAutoScroll(
event,
rectScrollDom = '.out-box',
scrollThreshold = 100,
scrollSpeed = 100
) {
// 自动滚动功能 - 当鼠标靠近容器边界时
const outBox =
typeof rectScrollDom === 'string' ? document.querySelector(rectScrollDom) : rectScrollDom;
if (!outBox) return;
const rect = outBox.getBoundingClientRect();
const mouseX = event.clientX;
const mouseY = event.clientY;
let scrollX = 0;
let scrollY = 0;
// 检测左右边界
if (mouseX < rect.left + scrollThreshold) {
scrollX = -scrollSpeed; // 向左滚动
} else if (mouseX > rect.right - scrollThreshold) {
scrollX = scrollSpeed; // 向右滚动
}
// 检测上下边界
if (mouseY < rect.top + scrollThreshold) {
scrollY = -scrollSpeed; // 向上滚动
} else if (mouseY > rect.bottom - scrollThreshold) {
scrollY = scrollSpeed; // 向下滚动
}
// 执行滚动
if (scrollX !== 0 || scrollY !== 0) {
outBox.scrollBy({
left: scrollX,
top: scrollY,
behavior: 'smooth'
});
}
}
</script>
更新信息
1.0.0
- 版本发布
1.0.1
- 模块环境中使用问题
1.1.0
- 选择框选中数组问题解决
- 重新绘制选择框数据
1.1.1
- 重写calculateUnionRectangle工具函数
- 新增generateCornerPoints函数
- 解决选择框选中的节点尾部不包含问题:

1.1.2
- 忘记构建。。。。。
1.1.3
- 添加mouseMove事件监听,使其更加美观。并使用requestAnimationFrame减少事件触发。
1.1.4
- 限制只能左键点击选中功能
1.1.5
- 优化选择框滑到选择区域外逻辑,使用debounce校验,1s钟后回来可以继续操作。
- 限制鼠标移动逻辑,只有左键按住移动才可以绘制矩阵。
1.1.6
- handleMouseUp方法优化
1.1.7
- 暴露出来clearRect清空选择框,为了合并单元格和拆分单元格时候使用。
