当前位置: 首页 > news >正文

vue2+elementUi实现自定义表格框选复制粘贴

一、介绍:

        本文介绍了一个基于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.完整代码

<!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+Cif (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) {// 跳过选择列,所以+1const 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>

http://www.dtcms.com/a/486960.html

相关文章:

  • Home Assistant-IOT模块
  • R Excel 文件:高效数据处理与可视化分析利器
  • 有做敦煌网站的吗创建网站 英文
  • Vue2项目搭建指南(基于Vue CLI和Webpack)
  • Python基础入门:语法、执行、配置与部署指南
  • 网站建设上传和下载柳州网站虚拟主机销售价格
  • 浙江天奥建设集团网站信息技术网站建设教案
  • 01_机器学习初步
  • C++---向上取整
  • 多字节串口收发IP设计(五)串口模块增加数据位停止位动态配置功能(含源码)
  • 上海网站建设专业公司排名百度搜索app
  • Java 设计模式—— 责任链模式:从原理到 SpringBoot 最优实现
  • Linux中快速部署Minio(基础TLS配置)
  • 大型小说网站开发语言望野博物馆
  • 做早餐烧菜有什么网站零基础搭建网站
  • SAP PCE生产订单组件不能更改物料编码和工厂
  • Aosp14系统壁纸的启动和加载流程
  • 电压源和电流源学习理解
  • 刘洋洋《魔法派对Magic Party》童话重启,温柔守护每颗童心
  • 东莞长安网站设计搞网站开发的程序员属于哪一类
  • 运维领域核心概念的专有名词解释-详解
  • 【AIGC】语音识别ASR:火山引擎大模型技术实践
  • 如何在AutoCAD中加载大型影像文件?
  • 爬虫调试技巧:如何用浏览器开发者工具找接口?
  • Linux 页缓存(Page Cache)与回写(Writeback)机制详解
  • 【NI测试方案】基于ARM+FPGA的整车仿真与电池标定
  • JavaScript将url转为blob和file,三种方法
  • 电商营销型网站建设中国菲律宾关系现状
  • 英文网站建设 飞沐wordpress公众号文章分类
  • 怎么做qq靓号网站岳阳网站设计公司