基于 VxeTable 的高级表格选择组件
概述
VxeTableSelect 是一个基于 VxeTable 的高级表格选择组件,支持单元格级别的选择、框选、多选等功能。该组件提供了类似 Excel 的表格交互体验,包括鼠标拖拽选择、键盘快捷键操作等。

组件架构
1. 核心依赖和类型定义
typescript
import { ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from "vue";
import { useVModel, onClickOutside } from "@vueuse/core";
import type { VxeTableInstance } from "vxe-table";
解读: 组件使用了 Vue 3 的 Composition API,结合 VueUse 工具库来处理响应式数据和外部点击检测。VxeTable 作为底层表格组件提供基础的表格功能。
2. 接口定义
typescript
interface TableRow {
id: number | string;
[key: string]: any;
}
interface CellPosition {
rowIndex: number;
colIndex: number;
}
interface SelectionData {
rowIndex: number;
colIndex: number;
rowId: number | string;
value: any;
rowData: any;
}
interface SelectionBox {
visible: boolean;
startX: number;
startY: number;
endX: number;
endY: number;
}
解读: 这些接口定义了组件的核心数据结构。TableRow
定义表格行数据,CellPosition
表示单元格位置,SelectionData
包含选中单元格的完整信息,SelectionBox
用于框选功能的坐标管理。
关键功能解析
1. 响应式状态管理
typescript
// 组件引用
const tableRef = ref<VxeTableInstance>();
const tableWrapper = ref<HTMLElement>();
// 选中状态管理
const selectedCells = ref<Set<string>>(new Set());
// 使用 useVModel 处理 v-model
const modelValue = useVModel(props, "modelValue", emit);
// 鼠标交互状态
const isMouseDown = ref(false);
const isModifierPressed = ref(false);
const selectionStart = ref<CellPosition | null>(null);
const selectionEnd = ref<CellPosition | null>(null);
// 选择框状态
const selectionBox = reactive<SelectionBox>({
visible: false,
startX: 0,
startY: 0,
endX: 0,
endY: 0,
});
解读: 组件使用了多个响应式变量来管理不同的状态:
selectedCells
使用 Set 数据结构存储选中的单元格,提高查找和操作效率useVModel
实现了双向数据绑定,支持 v-model 语法selectionBox
使用 reactive 包装,用于实时更新选择框的位置和大小
2. 性能优化策略
typescript
// 性能优化:节流函数
const throttle = <T extends (...args: any[]) => any>(func: T, delay: number): T => {
let lastCall = 0;
return ((...args: any[]) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
return func(...args);
}
}) as T;
};
// 缓存DOM元素以避免重复查询
let cachedTableBodyElement: HTMLElement | null = null;
let cachedFirstRow: HTMLElement | null = null;
let cachedCells: NodeListOf<Element> | null = null;
let cacheTimestamp = 0;
const CACHE_DURATION = 1000; // 缓存1秒
解读: 组件采用了多种性能优化策略:
- 节流函数:限制鼠标移动事件的触发频率,避免过度渲染
- DOM 缓存:缓存频繁访问的 DOM 元素,减少重复查询的开销
- 时间戳控制:通过时间戳控制缓存的有效期,平衡性能和数据准确性
3. 单元格位置计算
typescript
const calculateCellPosition = (clientX: number, clientY: number) => {
const cached = getCachedTableElements();
if (!cached) return null;
const { tableBodyElement, firstRow, cells } = cached;
const bodyRect = tableBodyElement.getBoundingClientRect();
// 计算相对于表格内容的坐标(考虑滚动)
const contentX = clientX - bodyRect.left + tableBodyElement.scrollLeft;
const contentY = clientY - bodyRect.top + tableBodyElement.scrollTop;
let colIndex = -1;
let rowIndex = -1;
// 计算列索引
if (cells) {
let currentX = 0;
for (let i = 0; i < cells.length; i++) {
const cellWidth = (cells[i] as HTMLElement).offsetWidth;
if (contentX >= currentX && contentX < currentX + cellWidth) {
colIndex = i;
break;
}
currentX += cellWidth;
}
}
// 计算行索引
if (firstRow) {
const cellHeight = firstRow.offsetHeight;
rowIndex = Math.floor(contentY / cellHeight);
}
return { rowIndex, colIndex, contentX, contentY };
};
解读: 这是组件的核心算法之一,用于将鼠标坐标转换为表格单元格位置:
- 坐标转换:将屏幕坐标转换为相对于表格内容的坐标
- 滚动补偿:考虑表格的滚动偏移量,确保计算准确性
- 边界处理:确保计算出的索引在有效范围内
4. 鼠标事件处理
typescript
const onMouseDown = (event: MouseEvent) => {
// 只处理左键和在修饰键模式下的右键
if (event.button !== 0 && !(event.button === 2 && isModifierPressed.value)) {
return;
}
// 点击表格时设置焦点状态
isTableFocusedState.value = true;
if (event.target && (event.target as HTMLElement).closest(".cell-content")) {
const cellElement = (event.target as HTMLElement).closest(".cell-content");
if (!cellElement) return;
const rowIndex = parseInt(cellElement.getAttribute("data-row-index") || "0");
const colIndex = parseInt(cellElement.getAttribute("data-col-index") || "0");
const cellKey = getCellKey(rowIndex, colIndex);
// 修饰键+右键:切换选择状态
if (props.enableMultiSelect && isModifierPressed.value && event.button === 2) {
if (selectedCells.value.has(cellKey)) {
selectedCells.value.delete(cellKey);
} else {
selectedCells.value.add(cellKey);
}
updateSelection();
event.preventDefault();
event.stopPropagation();
return;
}
// 正常的左键框选逻辑
if (event.button === 0) {
isMouseDown.value = true;
selectionStart.value = { rowIndex, colIndex };
selectionEnd.value = { rowIndex, colIndex };
// 如果没有按修饰键或者未开启多选,清空之前的选择
if (!props.enableMultiSelect || !isModifierPressed.value) {
selectedCells.value.clear();
}
// 开始框选
const wrapperRect = tableWrapper.value?.getBoundingClientRect();
if (wrapperRect) {
selectionBox.startX = event.clientX - wrapperRect.left;
selectionBox.startY = event.clientY - wrapperRect.top;
selectionBox.endX = selectionBox.startX;
selectionBox.endY = selectionBox.startY;
selectionBox.visible = true;
}
event.preventDefault();
}
}
};
解读: 鼠标按下事件处理了多种交互模式:
- 按键区分:区分左键和右键,支持不同的操作模式
- 修饰键支持:支持 Ctrl/Cmd 键进行多选操作
- 焦点管理:点击时设置表格焦点状态
- 数据属性读取:通过 data 属性获取单元格的行列索引
5. 选择区域可视化
typescript
// 计算已选中单元格的覆盖层
const selectionOverlays = computed<SelectionOverlay[]>(() => {
if (selectedCells.value.size === 0) return [];
// 计算所有选中单元格的边界
let minRow = Infinity;
let maxRow = -Infinity;
let minCol = Infinity;
let maxCol = -Infinity;
for (const cellKey of selectedCells.value) {
const [rowIndex, colIndex] = cellKey.split("-").map(Number);
minRow = Math.min(minRow, rowIndex);
maxRow = Math.max(maxRow, rowIndex);
minCol = Math.min(minCol, colIndex);
maxCol = Math.max(maxCol, colIndex);
}
// 计算选择区域的边界位置
const topLeftRect = getCellRect(minRow, minCol);
const bottomRightRect = getCellRect(maxRow, maxCol);
if (!topLeftRect || !bottomRightRect) return [];
const left = topLeftRect.left;
const top = topLeftRect.top;
const width = bottomRightRect.left + bottomRightRect.width - topLeftRect.left;
const height = bottomRightRect.top + bottomRightRect.height - topLeftRect.top;
return [
{
key: "selection-area",
style: {
position: "absolute",
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
transform: "",
},
},
];
});
解读: 这个计算属性负责生成选择区域的可视化覆盖层:
- 边界计算:遍历所有选中单元格,计算最小和最大的行列索引
- 位置计算:根据边界单元格的位置计算整个选择区域的位置和尺寸
- 响应式更新:作为计算属性,会在选中状态变化时自动更新
6. 覆盖层DOM管理
typescript
// 创建并插入覆盖层容器到表格主体中
const createOverlayContainer = () => {
const cached = getCachedTableElements();
if (!cached?.tableBodyElement || overlayContainer) return;
overlayContainer = document.createElement("div");
overlayContainer.className = "selection-overlay-container";
overlayContainer.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
background: transparent;
`;
cached.tableBodyElement.style.position = "relative";
cached.tableBodyElement.appendChild(overlayContainer);
};
// 更新覆盖层DOM
const updateOverlayDOM = () => {
if (!overlayContainer) return;
// 清空现有覆盖层
overlayContainer.innerHTML = "";
// 添加已选中单元格覆盖层
for (const overlay of selectionOverlays.value) {
const overlayDiv = document.createElement("div");
overlayDiv.className = "selection-overlay selected-area";
overlayDiv.style.cssText = `
position: absolute;
left: ${overlay.style.left};
top: ${overlay.style.top};
width: ${overlay.style.width};
height: ${overlay.style.height};
background-color: rgba(24, 144, 255, 0.1);
border: 1px solid #1890ff;
pointer-events: none;
`;
overlayContainer.appendChild(overlayDiv);
}
// 添加正在选择区域覆盖层
if (selectingOverlay.value) {
const overlayDiv = document.createElement("div");
overlayDiv.className = "selection-overlay selecting";
overlayDiv.style.cssText = `
position: absolute;
left: ${selectingOverlay.value.style.left}px;
top: ${selectingOverlay.value.style.top}px;
width: ${selectingOverlay.value.style.width}px;
height: ${selectingOverlay.value.style.height}px;
background-color: rgba(82, 196, 26, 0.1);
border: 1px solid #52c41a;
pointer-events: none;
`;
overlayContainer.appendChild(overlayDiv);
}
};
解读: 覆盖层管理是组件的重要特性:
- 动态创建:在表格主体中动态创建覆盖层容器
- 样式控制:通过 CSS 样式控制覆盖层的外观和层级
- 事件穿透 :设置
pointer-events: none
确保覆盖层不影响表格的正常交互
关键技术点
1. 事件监听优化
typescript
// 防止默认的 passive 事件监听
(function () {
if (typeof EventTarget !== "undefined") {
const func = EventTarget.prototype.addEventListener;
EventTarget.prototype.addEventListener = function (type, fn, capture) {
(this as any).func = func;
if (typeof capture !== "boolean") {
capture = capture || {};
capture.passive = false;
}
(this as any).func(type, fn, capture);
};
}
})();
解读: 这段代码重写了 addEventListener
方法,确保事件监听器不是被动的,这样可以调用 preventDefault()
来阻止默认行为。
2. 键盘修饰键检测
typescript
// 检查修饰键是否按下(支持 Ctrl 和 Command)
const isModifierKeyPressed = (event: KeyboardEvent | MouseEvent) => {
return event.ctrlKey || event.metaKey;
};
解读: 跨平台支持,在 Windows/Linux 上检测 Ctrl 键,在 macOS 上检测 Command 键。
3. 自动滚动功能
typescript
// 自动滚动检测
const cached = getCachedTableElements();
if (cached?.tableBodyElement) {
const tableBodyElement = cached.tableBodyElement;
const bodyRect = tableBodyElement.getBoundingClientRect();
const scrollMargin = 50;
let scrollX = 0;
let scrollY = 0;
// 横向滚动
if (event.clientX < bodyRect.left + scrollMargin) {
scrollX = -10;
} else if (event.clientX > bodyRect.right - scrollMargin) {
scrollX = 10;
}
// 纵向滚动
if (event.clientY < bodyRect.top + scrollMargin) {
scrollY = -10;
} else if (event.clientY > bodyRect.bottom - scrollMargin) {
scrollY = 10;
}
if (scrollX !== 0 || scrollY !== 0) {
tableBodyElement.scrollBy(scrollX, scrollY);
}
}
解读: 当鼠标接近表格边缘时自动滚动,提供流畅的选择体验。通过检测鼠标位置与表格边界的距离来触发滚动。
4. 焦点管理
typescript
// 使用 onClickOutside 来管理表格焦点状态
onClickOutside(tableWrapper, () => {
isTableFocusedState.value = false;
});
解读: 使用 VueUse 的 onClickOutside
来检测外部点击,自动管理表格的焦点状态。
使用示例
vue
<template>
<VxeTableSelect
v-model="selectedData"
:table-data="tableData"
:columns="columns"
:height="500"
:show-selection-box="true"
:enable-multi-select="true"
@selection-change="onSelectionChange"
/>
</template>
<script setup>
import { ref } from 'vue';
const selectedData = ref([]);
const tableData = ref([
{ id: 1, name: '张三', age: 25, city: '北京' },
{ id: 2, name: '李四', age: 30, city: '上海' },
// ...
]);
const columns = ref([
{ field: 'name', title: '姓名', width: 120 },
{ field: 'age', title: '年龄', width: 80 },
{ field: 'city', title: '城市', width: 100 },
]);
const onSelectionChange = (data) => {
console.log('选择变化:', data);
};
</script>
总结
VxeTableSelect 组件是一个功能丰富的表格选择组件,主要特点包括:
- 高性能:通过节流、缓存等技术优化性能
- 交互友好:支持鼠标拖拽、键盘快捷键等多种交互方式
- 可视化反馈:提供选择框和覆盖层的可视化反馈
- 跨平台兼容:支持不同操作系统的修饰键
- 灵活配置:支持单选、多选等多种选择模式
该组件适用于需要复杂表格交互的场景,如数据分析、报表系统等。通过学习其实现原理,可以深入理解 Vue 3 的响应式系统、DOM 操作优化、事件处理等核心概念。