一、介绍:
本文介绍了一个基于ElementUI实现的表格框选复制功能,支持单元格和行两种选择模式。主要功能包括:1)通过点击或拖动框选单元格;2)支持全选、复制选中内容到剪贴板;3)可切换单元格/行选择模式;4)复制内容可直接粘贴到Excel/WPS等办公软件。该方案采用Vue.js框架,结合ElementUI组件库,实现了高效的大数据量表格操作功能,并提供了用户友好的交互反馈。
由于数据量增大,需要考虑性能问题。但Element UI的表格是虚拟滚动吗?不,Element UI的表格默认不提供虚拟滚动,所以大量数据可能会影响性能。但用户要求10000条,所以只能尽量优化。
在现有代码中,表格是通过el-table
组件渲染的,对于大量数据,Element UI建议使用虚拟滚动,但虚拟滚动需要额外配置。然而,用户没有指定使用虚拟滚动,所以先直接增加数据量。
另外,需要增加列数。现有列有:选择列、日期、姓名、地址、年龄、职业、状态。可以再添加一些列,比如电话、邮箱、部门等。
目前已经实现的功能有:
1.框选多行多列单元格进行复制粘贴到excel
2.切换按行模式时,通过勾选复选框实现按照行复制粘贴到excel
3.横向滚动时可以框选复制
4.当表格数据较大时候,固定了表头,通过elementUI中table的height属性控制
5.目前随机生成10000条数据,当数据量超大时,需要使用虚拟滚动进行优化,要不然表格一定会卡顿
二、效果:
1.框选中+复制

2.粘贴wps

3.完整代码
javascript
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Element UI表格框选复制功能</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
body {
background-color: #f5f7fa;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
header {
background: #2c3e50;
color: white;
padding: 20px;
text-align: center;
}
h1 {
font-size: 24px;
margin-bottom: 10px;
}
.subtitle {
font-size: 14px;
opacity: 0.8;
}
.content {
padding: 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
.el-button {
transition: all 0.3s;
}
.el-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.table-container {
position: relative;
margin-bottom: 20px;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
}
.status-bar {
display: flex;
justify-content: space-between;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 14px;
color: #606266;
}
.instructions {
background-color: #ecf5ff;
padding: 15px;
border-radius: 4px;
margin-top: 20px;
border-left: 5px solid #409eff;
}
.instructions h3 {
color: #2c3e50;
margin-bottom: 10px;
}
.instructions ul {
padding-left: 20px;
}
.instructions li {
margin-bottom: 8px;
line-height: 1.5;
}
.highlight {
background-color: #fff9c4;
padding: 2px 5px;
border-radius: 3px;
font-weight: 500;
}
.selection-info {
display: flex;
gap: 15px;
}
.selecting-rect {
position: absolute;
border: 2px solid #409eff;
background-color: rgba(64, 158, 255, 0.1);
pointer-events: none;
z-index: 100;
}
.selected-cell {
background-color: rgba(64, 158, 255, 0.2) !important;
border: 1px solid #409eff !important;
}
.el-table .current-row td {
background-color: #f0f9ff !important;
}
.el-table .el-table__body tr:hover>td {
background-color: #f5f7fa !important;
}
.copy-status {
position: fixed;
top: 20px;
right: 20px;
background: #67c23a;
color: white;
padding: 10px 15px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
z-index: 2000;
opacity: 0;
transition: opacity 0.3s;
}
.copy-status.show {
opacity: 1;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<div class="content">
<div class="controls">
<el-button type="primary" @click="selectAll">全选</el-button>
<el-button type="success" @click="copySelected">复制选中内容</el-button>
<el-button type="warning" @click="clearSelection">清除选择</el-button>
<el-button type="info" @click="toggleSelectionMode">
{{ selectionMode === 'cell' ? '切换至行选择模式' : '切换至单元格选择模式' }}
</el-button>
</div>
<div class="table-container" id="table-container">
<el-table ref="multipleTable" :data="tableData" border style="width: 100%;height: 700px;overflow-y: auto;"
height="700"
:row-class-name="tableRowClassName" @cell-click="handleCellClick"
@row-click="handleRowClick" @selection-change="handleSelectionChange"
@mousedown.native="handleMouseDown" @mousemove.native="handleMouseMove"
@mouseup.native="handleMouseUp">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column prop="date" label="日期" width="150"></el-table-column>
<el-table-column prop="name" label="姓名" width="120"></el-table-column>
<el-table-column prop="address" label="地址"></el-table-column>
<el-table-column prop="age" label="年龄" width="80"></el-table-column>
<el-table-column prop="job" label="职业" width="120"></el-table-column>
<el-table-column prop="status" label="状态" width="100"></el-table-column>
<el-table-column prop="phone" label="电话" width="150"></el-table-column>
<el-table-column prop="email" label="邮箱" width="200"></el-table-column>
<el-table-column prop="department" label="部门" width="150"></el-table-column>
<el-table-column prop="salary" label="薪资" width="120"></el-table-column>
<el-table-column prop="education" label="学历" width="100"></el-table-column>
<el-table-column prop="experience" label="工作经验" width="120"></el-table-column>
<el-table-column prop="city" label="城市" width="120"></el-table-column>
<el-table-column prop="country" label="国家" width="120"></el-table-column>
<el-table-column prop="company" label="公司" width="200"></el-table-column>
<el-table-column prop="position" label="职位" width="150"></el-table-column>
<el-table-column prop="project" label="项目" width="200"></el-table-column>
<el-table-column prop="skill" label="技能" width="150"></el-table-column>
</el-table>
<!-- 选择框 -->
<div v-if="selecting" class="selecting-rect" :style="rectStyle"></div>
</div>
<div class="status-bar">
<div>总记录数: {{ tableData.length }}</div>
<div class="selection-info">
<span>选中: {{ selectedCount }} 个单元格</span>
<span>选择模式: {{ selectionMode === 'cell' ? '单元格' : '行' }}</span>
</div>
<div>最后操作: {{ lastOperation }}</div>
</div>
<!-- <div class="instructions">
<h3>使用说明</h3>
<ul>
<li><span class="highlight">点选</span>: 点击单元格可选择单个单元格</li>
<li><span class="highlight">框选</span>: 按住鼠标左键拖动可以选择多个单元格</li>
<li><span class="highlight">行选择</span>: 点击行可以选择整行,或使用左侧复选框</li>
<li><span class="highlight">全选</span>: 点击"全选"按钮选择所有行</li>
<li><span class="highlight">复制</span>: 选择内容后点击"复制选中内容"按钮或按Ctrl+C</li>
<li><span class="highlight">模式切换</span>: 可以切换单元格选择模式或行选择模式</li>
<li><span class="highlight">粘贴到Excel/WPS</span>: 复制后可直接粘贴到Excel或WPS表格中</li>
</ul>
</div> -->
</div>
</div>
<!-- 复制状态提示 -->
<div class="copy-status" :class="{show: showCopyStatus}">
已复制 {{ selectedCount }} 个单元格到剪贴板
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
// tableData: [
// { date: '2023-10-01', name: '张三', address: '北京市海淀区', age: 30, job: '工程师', status: '在职' },
// { date: '2023-10-02', name: '李四', address: '上海市浦东新区', age: 28, job: '设计师', status: '在职' },
// { date: '2023-10-03', name: '王五', address: '广州市天河区', age: 35, job: '产品经理', status: '离职' },
// { date: '2023-10-04', name: '赵六', address: '深圳市南山区', age: 32, job: '开发工程师', status: '在职' },
// { date: '2023-10-05', name: '钱七', address: '杭州市西湖区', age: 29, job: 'UI设计师', status: '休假' },
// { date: '2023-10-06', name: '孙八', address: '南京市鼓楼区', age: 31, job: '测试工程师', status: '在职' },
// { date: '2023-10-07', name: '周九', address: '武汉市江汉区', age: 27, job: '前端开发', status: '在职' },
// { date: '2023-10-08', name: '吴十', address: '成都市锦江区', age: 33, job: '后端开发', status: '休假' },
// { date: '2023-10-09', name: '郑十一', address: '西安市雁塔区', age: 26, job: '运维工程师', status: '在职' },
// { date: '2023-10-10', name: '王十二', address: '苏州市工业园区', age: 34, job: '架构师', status: '在职' }
// ],
tableData: this.generateTableData(10000), //模拟数据
selectedCount: 0,
lastOperation: '暂无',
selectionMode: 'cell', // 'cell' 或 'row'
selecting: false,
startX: 0,
startY: 0,
endX: 0,
endY: 0,
selectedCells: [],
selectedRows: [],
showCopyStatus: false,
isDragging: false, // 标记是否正在拖拽框选
tableRect: null // 存储表格位置信息
};
},
computed: {
rectStyle() {
if (!this.selecting) return {};
const left = Math.min(this.startX, this.endX);
const top = Math.min(this.startY, this.endY);
const width = Math.abs(this.endX - this.startX);
const height = Math.abs(this.endY - this.startY);
return {
left: left + 'px',
top: top + 'px',
width: width + 'px',
height: height + 'px'
};
}
},
mounted() {
// 初始化表格
this.$nextTick(() => {
this.updateSelectedCount();
this.updateTableRect();
// 监听窗口大小变化,更新表格位置信息
window.addEventListener('resize', this.updateTableRect);
});
// 添加键盘事件监听
document.addEventListener('keydown', this.handleKeyDown);
},
beforeDestroy() {
window.removeEventListener('resize', this.updateTableRect);
// 移除键盘事件监听
document.removeEventListener('keydown', this.handleKeyDown);
},
methods: {
// 生成表格数据
generateTableData(count) {
const data = [];
const departments = ['技术部', '市场部', '销售部', '财务部', '人力资源部', '研发部', '产品部', '运营部'];
const statuses = ['进行中', '已完成', '待审核', '已取消', '待开始'];
const jobs = ['工程师', '设计师', '产品经理', '开发工程师', 'UI设计师', '测试工程师', '前端开发', '后端开发', '运维工程师', '架构师'];
const cities = ['北京', '上海', '广州', '深圳', '杭州', '南京', '武汉', '成都', '西安', '苏州'];
const countries = ['中国', '美国', '英国', '日本', '德国', '法国', '澳大利亚', '加拿大'];
const companies = ['腾讯科技', '阿里巴巴', '百度', '华为', '小米', '字节跳动', '美团', '滴滴'];
const positions = ['初级工程师', '中级工程师', '高级工程师', '技术专家', '架构师', '项目经理', '部门经理'];
const projects = ['项目A', '项目B', '项目C', '项目D', '项目E', '项目F', '项目G'];
const skills = ['Java', 'Python', 'JavaScript', 'C++', 'Go', 'React', 'Vue', 'Angular'];
const educations = ['本科', '硕士', '博士', '大专', '高中'];
for (let i = 0; i < count; i++) {
data.push({
date: '2023-' + this.padZero(Math.floor(Math.random() * 12) + 1) + '-' + this
.padZero(Math.floor(Math.random() * 28) + 1),
name: '用户' + (i + 1),
address: cities[Math.floor(Math.random() * cities.length)] + '市某区某街道' + (Math
.floor(Math.random() * 100) + 1) + '号',
age: Math.floor(Math.random() * 40) + 20,
job: jobs[Math.floor(Math.random() * jobs.length)],
status: statuses[Math.floor(Math.random() * statuses.length)],
phone: '1' + Math.floor(Math.random() * 10) + this.padZero(Math.floor(Math
.random() * 1000000000), 9),
email: 'user' + (i + 1) + '@example.com',
department: departments[Math.floor(Math.random() * departments.length)],
salary: (Math.floor(Math.random() * 50000) + 10000).toLocaleString(),
education: educations[Math.floor(Math.random() * educations.length)],
experience: Math.floor(Math.random() * 20) + '年',
city: cities[Math.floor(Math.random() * cities.length)],
country: countries[Math.floor(Math.random() * countries.length)],
company: companies[Math.floor(Math.random() * companies.length)],
position: positions[Math.floor(Math.random() * positions.length)],
project: projects[Math.floor(Math.random() * projects.length)],
skill: skills[Math.floor(Math.random() * skills.length)]
});
}
return data;
},
// 数字补零
padZero(num, length = 2) {
return num.toString().padStart(length, '0');
},
// 处理键盘事件
handleKeyDown(event) {
// 检查是否Ctrl+C
if (event.ctrlKey && event.key === 'c') {
event.preventDefault(); // 防止默认行为
this.copySelected(); // 调用复制方法
}
},
// 更新表格位置信息
updateTableRect() {
if (this.$refs.multipleTable && this.$refs.multipleTable.$el) {
this.tableRect = this.$refs.multipleTable.$el.getBoundingClientRect();
}
},
// 处理单元格点击
handleCellClick(row, column, cell, event) {
// 如果正在框选,不触发单元格点击事件
if (this.isDragging) {
this.isDragging = false;
return;
}
if (this.selectionMode === 'cell') {
this.toggleCellSelection(cell);
this.lastOperation = '点选了单元格: ' + column.label;
}
},
// 处理行点击
handleRowClick(row, event, column) {
// 如果正在框选,不触发行点击事件
if (this.isDragging) {
this.isDragging = false;
return;
}
if (this.selectionMode === 'row') {
this.toggleRowSelection(row);
this.lastOperation = '点击了行: ' + row.name;
}
},
// 处理选择变化
handleSelectionChange(selection) {
this.selectedRows = selection;
this.updateSelectedCount();
},
// 处理鼠标按下
handleMouseDown(event) {
if (this.selectionMode !== 'cell') return;
// 更新表格位置信息
this.updateTableRect();
this.selecting = true;
this.isDragging = false;
// 计算相对于表格的坐标
this.startX = event.clientX - this.tableRect.left;
this.startY = event.clientY - this.tableRect.top;
this.endX = this.startX;
this.endY = this.startY;
// 清除之前的选择(如果不按Ctrl键)
if (!event.ctrlKey) {
this.clearSelection();
}
// 阻止默认行为,避免选中文本
event.preventDefault();
},
// 处理鼠标移动
handleMouseMove(event) {
if (!this.selecting) return;
// 标记为拖拽状态
this.isDragging = true;
// 计算相对于表格的坐标
this.endX = event.clientX - this.tableRect.left;
this.endY = event.clientY - this.tableRect.top;
// 限制选择框在表格范围内
this.endX = Math.max(0, Math.min(this.endX, this.tableRect.width));
this.endY = Math.max(0, Math.min(this.endY, this.tableRect.height));
},
// 处理鼠标释放
handleMouseUp() {
if (!this.selecting) return;
this.selecting = false;
// 只有在拖拽距离足够大时才认为是框选
const dragDistance = Math.sqrt(
Math.pow(this.endX - this.startX, 2) +
Math.pow(this.endY - this.startY, 2)
);
if (dragDistance > 5) {
this.selectCellsInRect();
this.lastOperation = '框选了多个单元格';
}
},
// 选择矩形区域内的单元格
selectCellsInRect() {
const tableBody = this.$refs.multipleTable.$el.querySelector('.el-table__body tbody');
if (!tableBody) return;
const minX = Math.min(this.startX, this.endX);
const maxX = Math.max(this.startX, this.endX);
const minY = Math.min(this.startY, this.endY);
const maxY = Math.max(this.startY, this.endY);
// 获取所有行
const rows = tableBody.querySelectorAll('tr');
// 获取表头信息
const tableHeader = this.$refs.multipleTable.$el.querySelector('.el-table__header thead');
const headers = tableHeader.querySelectorAll('th');
const headerWidths = Array.from(headers).map(th => th.offsetWidth);
// 遍历每一行
rows.forEach((row, rowIndex) => {
const rowRect = row.getBoundingClientRect();
const rowTop = rowRect.top - this.tableRect.top;
const rowBottom = rowTop + rowRect.height;
// 检查行是否在选择矩形内
if (rowBottom > minY && rowTop < maxY) {
// 获取行中的所有单元格
const cells = row.querySelectorAll('td');
// 遍历每个单元格
cells.forEach((cell, cellIndex) => {
// 跳过选择列(第一列)
if (cellIndex === 0) return;
const cellRect = cell.getBoundingClientRect();
const cellLeft = cellRect.left - this.tableRect.left;
const cellRight = cellLeft + cellRect.width;
const cellTop = cellRect.top - this.tableRect.top;
const cellBottom = cellTop + cellRect.height;
// 检查单元格是否在选择矩形内
if (cellRight > minX && cellLeft < maxX &&
cellBottom > minY && cellTop < maxY) {
this.toggleCellSelection(cell, true);
}
});
}
});
this.updateSelectedCount();
},
// 切换单元格选择状态
toggleCellSelection(cell, forceSelect = false) {
if (cell.classList.contains('selected-cell')) {
if (!forceSelect) {
cell.classList.remove('selected-cell');
this.removeCellFromSelection(cell);
}
} else {
cell.classList.add('selected-cell');
this.addCellToSelection(cell);
}
},
// 添加单元格到选择集合
addCellToSelection(cell) {
const row = cell.parentNode;
const rowIndex = Array.from(row.parentNode.children).indexOf(row);
const cellIndex = Array.from(row.children).indexOf(cell);
if (rowIndex >= 0 && rowIndex < this.tableData.length) {
const key = `${rowIndex}-${cellIndex}`;
if (!this.selectedCells.includes(key)) {
this.selectedCells.push(key);
}
}
},
// 从选择集合移除单元格
removeCellFromSelection(cell) {
const row = cell.parentNode;
const rowIndex = Array.from(row.parentNode.children).indexOf(row);
const cellIndex = Array.from(row.children).indexOf(cell);
const key = `${rowIndex}-${cellIndex}`;
this.selectedCells = this.selectedCells.filter(k => k !== key);
},
// 切换行选择状态
toggleRowSelection(row) {
this.$refs.multipleTable.toggleRowSelection(row);
},
// 全选
selectAll() {
this.$refs.multipleTable.toggleAllSelection();
this.lastOperation = '选择了所有行';
},
// 清除选择
clearSelection() {
this.$refs.multipleTable.clearSelection();
// 清除单元格选择
const table = this.$refs.multipleTable.$el;
const cells = table.querySelectorAll('.selected-cell');
cells.forEach(cell => {
cell.classList.remove('selected-cell');
});
this.selectedCells = [];
this.updateSelectedCount();
this.lastOperation = '清除了所有选择';
},
// 复制选中内容
copySelected() {
let textToCopy = '';
if (this.selectionMode === 'row' && this.selectedRows.length > 0) {
// 复制选中的行
const columns = this.$refs.multipleTable.columns;
// 跳过选择列
const dataColumns = columns.filter((col, index) => index > 0);
// 表头
textToCopy = dataColumns.map(col => col.label).join('\t') + '\n';
// 数据行
textToCopy += this.selectedRows.map(row => {
return dataColumns.map(col => {
return col.property ? row[col.property] || '' : '';
}).join('\t');
}).join('\n');
} else if (this.selectedCells.length > 0) {
// 复制选中的单元格
const tableBody = this.$refs.multipleTable.$el.querySelector('.el-table__body tbody');
if (!tableBody) return;
// 获取表头信息
const tableHeader = this.$refs.multipleTable.$el.querySelector('.el-table__header thead');
const headers = tableHeader.querySelectorAll('th');
const headerLabels = Array.from(headers).map(th => th.textContent.trim()).slice(1); // 跳过选择列
// 将选中的单元格按行和列分组
const selectedMap = {};
this.selectedCells.forEach(key => {
const [rowIndex, cellIndex] = key.split('-').map(Number);
if (!selectedMap[rowIndex]) selectedMap[rowIndex] = [];
// 跳过选择列,调整列索引
selectedMap[rowIndex].push(cellIndex - 1);
});
// 确定最小和最大行索引
const rowIndexes = Object.keys(selectedMap).map(Number).sort((a, b) => a - b);
if (rowIndexes.length === 0) return;
const minRow = Math.min(...rowIndexes);
const maxRow = Math.max(...rowIndexes);
// 确定最小和最大列索引
let minCol = Infinity,
maxCol = -Infinity;
Object.values(selectedMap).forEach(colIndexes => {
minCol = Math.min(minCol, ...colIndexes);
maxCol = Math.max(maxCol, ...colIndexes);
});
// 生成TSV格式文本
for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {
const rowData = [];
for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {
if (selectedMap[rowIndex] && selectedMap[rowIndex].includes(colIndex)) {
// 获取单元格数据
const row = tableBody.children[rowIndex];
if (row) {
// 跳过选择列,所以+1
const cell = row.children[colIndex + 1];
if (cell) {
rowData.push(cell.textContent.trim());
} else {
rowData.push('');
}
} else {
rowData.push('');
}
} else {
rowData.push('');
}
}
textToCopy += rowData.join('\t') + '\n';
}
}
if (textToCopy) {
// 使用Clipboard API复制文本
navigator.clipboard.writeText(textToCopy).then(() => {
this.showCopyStatus = true;
setTimeout(() => {
this.showCopyStatus = false;
}, 2000);
this.lastOperation = '复制了选中内容';
}).catch(err => {
// 如果Clipboard API不可用,使用传统方法
this.fallbackCopyTextToClipboard(textToCopy);
});
} else {
this.$message.warning('没有选中任何内容');
}
},
// 传统复制方法
fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = 0;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
this.showCopyStatus = true;
setTimeout(() => {
this.showCopyStatus = false;
}, 2000);
this.lastOperation = '复制了选中内容';
} else {
this.$message.error('复制失败');
}
} catch (err) {
this.$message.error('复制失败: ' + err);
}
document.body.removeChild(textArea);
},
// 切换选择模式
toggleSelectionMode() {
this.selectionMode = this.selectionMode === 'cell' ? 'row' : 'cell';
this.clearSelection();
this.lastOperation = `切换到${this.selectionMode === 'cell' ? '单元格' : '行'}选择模式`;
},
// 更新选中计数
updateSelectedCount() {
if (this.selectionMode === 'row') {
this.selectedCount = this.selectedRows.length;
} else {
this.selectedCount = this.selectedCells.length;
}
},
// 表格行类名
tableRowClassName({
row,
rowIndex
}) {
if (this.selectedRows.includes(row)) {
return 'selected-row';
}
return '';
}
}
});
</script>
</body>
</html>