1. 为什么我们需要跨固定列的鼠标框选?
如果你用过Excel或者WPS表格,肯定对鼠标拖拽选中一片区域的功能不陌生。这个功能在网页表格里,尤其是在处理大量数据时,简直是个“效率神器”。想象一下,你要从一个有几十列、几百行的员工信息表里,批量复制“姓名”和“手机号”这两列固定列的数据,同时还要选中中间“绩效评分”、“出勤天数”这些不固定的列。如果只能一行一行、一列一列点,那估计点完天都黑了。
在Vue3生态里,vxe-table是个功能非常强大的表格组件,它自带了丰富的选择功能,比如单选、多选、行选择、列选择。但是,当表格设置了左右固定列时,原生的区域选择功能就有点“力不从心”了。你会发现,鼠标框选的范围无法跨越固定列和滚动区域之间的“结界”。固定列区域和中间的可滚动区域像是两个独立的表格,鼠标事件被隔开了,这直接导致我们无法实现那种流畅的、从固定列开始一直拖到表格另一端的“大片选择”体验。
我最近在一个后台管理系统的报表模块就遇到了这个痛点。产品经理要求用户能像操作本地Excel一样,用鼠标自由框选表格的任何区域,然后一键导出或批量操作。特别是当左侧固定了“序号”和“关键指标”,右侧固定了“操作”按钮列时,用户的选择操作必须是连贯的。如果实现不了,用户体验就会大打折扣,显得很“割裂”。所以,动手实现一个支持跨固定列区域的鼠标框选功能,就成了一个刚需。这不仅仅是画个框那么简单,它涉及到多个DOM区域的联动、鼠标事件的精准捕获、以及动态范围的计算,是一个挺有意思的前端交互挑战。
2. 核心思路拆解:如何让鼠标事件“穿透”固定列?
在开始写代码之前,我们得先把这件事儿的原理想明白。vxe-table在渲染固定列时,实际上是把表格“拆”成了三个部分:左侧固定列容器、中间主滚动区域容器、右侧固定列容器。每个容器都有自己独立的tbody和table结构。这就带来了第一个问题:鼠标事件是隔离的。你在左边固定列按下鼠标开始拖动,这个mousedown事件只会停留在左边的容器里,鼠标移到中间区域时,左边容器就接收不到mousemove事件了。
所以,我们的核心思路可以概括为 “事件代理,统一计算,分区域渲染”。
第一步,事件监听要覆盖所有区域。 我们不能只给中间的主表格区域绑定鼠标事件,必须同时给左侧固定列和右侧固定列的tbody也绑上同样的事件监听器(mousedown, mousemove, mouseup等)。这样,无论用户在哪个区域按下鼠标,我们都能捕获到起点。
第二步,建立一个全局的选区状态管理。 我们需要用几个响应式变量来记录当前是否正在选择(isSelecting)、选择的起始单元格坐标(selectionStart)和结束单元格坐标(selectionEnd)。这个坐标不是像素坐标,而是行列索引(rowIndex, cellIndex)。无论鼠标在哪个子区域移动,我们都用同一套逻辑去更新这个结束坐标。
第三步,动态计算选区框的尺寸和位置。 这是最核心的计算部分。当我们有了起始和结束的行列索引后,我们需要遍历所有列(包括固定列和滚动列)的宽度,以及所有行的高度,来计算出一个覆盖所有三个区域的、虚拟的“选区矩形”。这个矩形的宽度是选中所有列的宽度总和,高度是选中所有行的高度总和。它的定位(top, left, right)则需要根据选中列是偏左还是偏右来分别计算,因为固定列的定位方式是fixed或absolute,它们的坐标系是独立的。
第四步,在三个区域分别渲染选区框。 计算出总的选区范围后,我们需要在左侧固定列容器、中间主容器、右侧固定列容器里,分别创建并放置一个半透明的遮罩层(即选区框)。让这三个框在视觉上看起来是一个完整的、跨越了不同滚动区域的整体。当鼠标移动时,同步更新这三个框的位置和大小。
听起来有点复杂?别担心,我们接下来就一步步用代码把它实现出来。我会把我在实际项目中踩过的坑和优化技巧都分享给你。
3. 环境搭建与基础表格配置
工欲善其事,必先利其器。首先,我们得把项目和基础表格跑起来。我用的环境是 Vue 3.3.4 和 vxe-table 4.5.7,这也是目前比较稳定的一个组合。如果你用的是更新的版本,核心思路应该是一样的,但可能有些API细节需要微调。
先安装依赖:
npm install vue@3.3.4 vxe-table@4.5.7
# 或者
yarn add vue@3.3.4 vxe-table@4.5.7
接下来,我们创建一个Vue组件,先把一个带有左右固定列的基础表格搭起来。这是我们的“画布”。为了模拟真实场景,我设计了一个员工信息表,左侧固定“ID”和“姓名”,中间是“年龄”、“性别”等长文本信息(可横向滚动),右侧固定“岗位”和“地址”。
<template>
<div>
<!-- 这三个div是我们的选区框容器,先准备好 -->
<div class="vxe-table--cell-area" ref="cellAreaRef">
<span class="vxe-table--cell-main-area"></span>
<span class="vxe-table--cell-active-area"></span>
</div>
<div class="vxe-table--cell-area" ref="leftFixedCellAreaRef">
<span class="vxe-table--cell-main-area"></span>
<span class="vxe-table--cell-active-area"></span>
</div>
<div class="vxe-table--cell-area" ref="rightFixedCellAreaRef">
<span class="vxe-table--cell-main-area"></span>
<span class="vxe-table--cell-active-area"></span>
</div>
<!-- vxe-table 主表格 -->
<vxe-grid
ref="xGridRef"
v-bind="gridOptions"
height="500px"
@toolbar-button-click="handleToolbarClick"
></vxe-grid>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import type { VxeGridProps } from 'vxe-table'
// 表格实例引用
const xGridRef = ref()
// 三个选区框的DOM引用
const cellAreaRef = ref()
const leftFixedCellAreaRef = ref()
const rightFixedCellAreaRef = ref()
// 表格配置项
const gridOptions = reactive<VxeGridProps<any>>({
toolbarConfig: {
perfect: true,
enabled: true,
size: 'mini',
buttons: [
{ code: 'getSelectData', name: '获取选中数据', type: 'text' }
]
},
columnConfig: {
resizable: true, // 允许调整列宽
useKey: true // 列拖拽必需
},
border: 'full', // 完整边框
stripe: true, // 斑马纹
showOverflow: true, // 重要:内容超出时显示,保证布局正确
rowConfig: {
isCurrent: true,
height: 35, // 固定行高,方便计算
isHover: true
},
columns: [
{ width: 100, field: 'id', title: 'ID', fixed: 'left' },
{ width: 100, field: 'name', title: '姓名', fixed: 'left' },
{ width: 400, field: 'age', title: '年龄' },
{ width: 400, field: 'sex', title: '性别' },
{ width: 400, field: 'department', title: '部门' },
{ width: 100, field: 'job', title: '岗位', fixed: 'right' },
{ width: 100, field: 'address', title: '地址', fixed: 'right' }
],
data: [
// ... 这里放你的测试数据,至少20行以上,才能看到滚动效果
{ id: 1, name: '张三', age: 30, sex: '男', department: '技术部', job: '前端工程师', address: '北京' },
{ id: 2, name: '李四', age: 28, sex: '女', department: '市场部', job: '市场专员', address: '上海' },
// ... 更多数据
]
})
// 工具栏按钮点击事件
const handleToolbarClick = ({ code }) => {
if (code === 'getSelectData') {
// 这里先留空,后面我们会实现获取选中数据的逻辑
console.log('点击了获取数据按钮')
}
}
</script>
<style lang="less" scoped>
// 先禁用文本选择,避免拖动时选中文字
.vxe-grid {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;

5423

被折叠的 条评论
为什么被折叠?



