Uncle_MrLee

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

58c984098a03a94c9169a191fa94e2df

 

<template>
   <div class="table-container">
      <h3>类Excel表格复制粘贴示例 (修复版)</h3>
      <el-table ref="tableRef"
         :data="tableData"
         border
         style="width: 100%"
         @mousedown="handleTableMouseDown"
         @mousemove="handleTableMouseMove"
         @mouseup="handleTableMouseUp"
         @mouseleave="handleTableMouseLeave"
      >
         <el-table-column prop="id"
            label="ID"
            width="100"
         />
         <el-table-column prop="name"
            label="姓名"
            width="120"
         />
         <el-table-column prop="age"
            label="年龄"
            width="100"
         />
         <el-table-column prop="gender"
            label="性别"
            width="100"
         />
         <el-table-column prop="email"
            label="邮箱"
         />
         <el-table-column prop="test1" label="测试1">
            <template #default="{ row }">
               <el-input v-model="row.test1" />
            </template>
         </el-table-column>
         <el-table-column prop="test2" label="测试2">
            <template #default="{ row }">
               <el-input v-model="row.test2" />
            </template>
         </el-table-column>
      </el-table>

      <div v-if="selectionRange" class="selection-info">
         选中区域: {{ selectionRange.start.row + 1 }}行{{ getColumnLetter(selectionRange.start.col) }}列
         至 {{ selectionRange.end.row + 1 }}行{{ getColumnLetter(selectionRange.end.col) }}列
      </div>

      <div v-if="statusMessage" class="status-message">
         {{ statusMessage }}
      </div>
   </div>
</template>

<script setup>
import { ref, reactive, computed, nextTick,onUnmounted } from 'vue';

// 表格数据
const tableData = reactive([
   { id: 1, name: '张三', age: 28, gender: '男', email: 'zhangsan@example.com' },
   { id: 2, name: '李四', age: 32, gender: '男', email: 'lisi@example.com' },
   { id: 3, name: '王五', age: 45, gender: '女', email: 'wangwu@example.com' },
   { id: 4, name: '赵六', age: 22, gender: '男', email: 'zhaoliu@example.com' },
   { id: 5, name: '钱七', age: 36, gender: '女', email: 'qianqi@example.com' },
]);

const tableRef = ref(null);
const selectionRange = ref(null);
const isSelecting = ref(false);
const columns = computed(() => {
   return tableRef.value?.columns.filter(col => !col.hidden) || [];
});
const statusMessage = ref('');

// 监听全局快捷键,确保能捕获Ctrl+C和Ctrl+V
const setupGlobalKeyListeners = () => {
   const handleKeyDown = (event) => {
      // 处理复制 (Ctrl+C)
      if(event.ctrlKey && event.key === 'c') {
         event.preventDefault();
         event.stopPropagation();
         handleCopy();
      }

      // 处理粘贴 (Ctrl+V)
      if(event.ctrlKey && event.key === 'v') {
         event.preventDefault();
         event.stopPropagation();
         handlePaste();
      }
   };

   window.addEventListener('keydown', handleKeyDown);

   // 组件卸载时移除事件监听
   return () => {
      window.removeEventListener('keydown', handleKeyDown);
   };
};

// 设置全局事件监听
const removeKeyListener = setupGlobalKeyListeners();

// 处理表格鼠标按下事件
const handleTableMouseDown = (event) => {
   console.log('鼠标按下事件 ', event);
   if(event.button !== 0) return; // 只处理左键点击

   event.preventDefault();
   const cellInfo = getCellInfoFromEvent(event);
   if(!cellInfo) return;

   selectionRange.value = {
      start: { ...cellInfo },
      end: { ...cellInfo },
   };

   isSelecting.value = true;
   highlightSelection();
};

// 处理表格鼠标移动事件
const handleTableMouseMove = (event) => {
   if(!isSelecting.value) return;
   const cellInfo = getCellInfoFromEvent(event);
   if(!cellInfo) return;

   selectionRange.value.end = { ...cellInfo };
   highlightSelection();
};

// 处理鼠标释放事件
const handleTableMouseUp = () => {
   isSelecting.value = false;
};

// 处理鼠标离开表格事件
const handleTableMouseLeave = () => {
   isSelecting.value = false;
};

// 获取单元格信息
const getCellInfoFromEvent = (event) => {
   const cellEl = event.target.closest('.el-table__cell');

   if(!cellEl) return null;

   const rowEl = cellEl.closest('.el-table__row');

   if(!rowEl) return null;

   const rows = Array.from(document.querySelectorAll('.el-table__row'));
   console.log('rows: ', rows);

   const rowIndex = rows.indexOf(rowEl);

   const cells = Array.from(rowEl.querySelectorAll('.el-table__cell'));
   const colIndex = cells.indexOf(cellEl);

   if(rowIndex === -1 || colIndex === -1) return null;

   return { row: rowIndex, col: colIndex };
};

// 高亮选中区域
const highlightSelection = () => {
   // 清除之前的高亮
   document.querySelectorAll('.el-table__cell').forEach(cell => {
      cell.style.backgroundColor = '';
      cell.style.border = '';
   });

   if(!selectionRange.value) return;

   const { start, end } = selectionRange.value;
   const minRow = Math.min(start.row, end.row);
   const maxRow = Math.max(start.row, end.row);
   const minCol = Math.min(start.col, end.col);
   const maxCol = Math.max(start.col, end.col);

   const rows = document.querySelectorAll('.el-table__row');

   for(let i = minRow; i <= maxRow; i++) {
      const row = rows[i];
      if(!row) continue;

      const cells = row.querySelectorAll('.el-table__cell');

      for(let j = minCol; j <= maxCol; j++) {
         const cell = cells[j];

         if(cell) {
            cell.style.backgroundColor = 'rgba(144, 202, 249, 0.5)';
            cell.style.border = '1px solid #409EFF';
         }
      }
   }
};

// 处理复制操作
const handleCopy = () => {
   if(!selectionRange.value) {
      showStatusMessage('请先选择要复制的单元格区域', 'warning');
      return;
   }

   const { start, end } = selectionRange.value;
   const minRow = Math.min(start.row, end.row);
   const maxRow = Math.max(start.row, end.row);
   const minCol = Math.min(start.col, end.col);
   const maxCol = Math.max(start.col, end.col);

   // 构建复制内容
   let copyText = '';

   for(let i = minRow; i <= maxRow; i++) {
      const rowData = [];

      for(let j = minCol; j <= maxCol; j++) {
         const prop = columns.value[j]?.property;
         rowData.push(prop ? (tableData[i][prop] || '') : '');
      }

      copyText += rowData.join('\t') + '\n';
   }

   // 复制到剪贴板 - 兼容旧浏览器
   if(navigator.clipboard) {
      navigator.clipboard.writeText(copyText).then(() => {
         showStatusMessage(`已复制 ${maxRow - minRow + 1}行${maxCol - minCol + 1}列数据`, 'success');
      }).catch(() => {
         fallbackCopyTextToClipboard(copyText);
      });
   }
   else {
      fallbackCopyTextToClipboard(copyText);
   }
};

// 处理粘贴操作
const handlePaste = () => {
   if(!selectionRange.value) {
      showStatusMessage('请先选择粘贴目标区域的起始单元格', 'warning');
      return;
   }

   if(navigator.clipboard) {
      navigator.clipboard.readText().then(text => {
         processPasteData(text);
      }).catch(err => {
         showStatusMessage('粘贴失败,尝试使用快捷键Ctrl+V', 'error');
         console.error('粘贴失败:', err);
      });
   }
   else {
      showStatusMessage('您的浏览器不支持剪贴板API', 'error');
   }
};

// 处理粘贴的数据
const processPasteData = (text) => {
   if(!text) {
      showStatusMessage('剪贴板为空', 'warning');
      return;
   }

   const rows = text.split('\n').filter(row => row.trim() !== '');
   const startRow = selectionRange.value.start.row;
   const startCol = selectionRange.value.start.col;

   rows.forEach((row, rowIdx) => {
      const cellValues = row.split('\t');
      cellValues.forEach((value, colIdx) => {
         const targetRowIdx = startRow + rowIdx;
         const targetColIdx = startCol + colIdx;

         if(
            targetRowIdx < tableData.length &&
        targetColIdx < columns.value.length &&
        columns.value[targetColIdx]?.property !== 'id'
         ) {
            tableData[targetRowIdx][columns.value[targetColIdx].property] = value;
         }
      });
   });

   showStatusMessage(`已粘贴 ${rows.length} 行数据`, 'success');
};

// 显示状态消息
const showStatusMessage = (msg ) => {
   statusMessage.value = msg;

   // 3秒后自动清除消息
   setTimeout(() => {
      statusMessage.value = '';
   }, 3000);
};

// 复制文本到剪贴板的 fallback 方法(兼容不支持Clipboard API的浏览器)
const fallbackCopyTextToClipboard = (text) => {
   const textArea = document.createElement('textarea');
   textArea.value = text;

   // 确保文本区域不在可视区域内
   textArea.style.position = 'fixed';
   textArea.style.top = '-999px';
   textArea.style.left = '-999px';

   document.body.appendChild(textArea);
   textArea.focus();
   textArea.select();

   try {
      const successful = document.execCommand('copy');
      const msg = successful ? '已复制数据' : '复制失败,请手动复制';
      showStatusMessage(msg, successful ? 'success' : 'error');
   }
   catch(err) {
      showStatusMessage('复制失败,请手动复制', 'error');
      console.error('Fallback: 无法复制文本: ', err);
   }

   document.body.removeChild(textArea);
};

// 列索引转字母
const getColumnLetter = (index) => {
   let letter = '';
   let remaining = index;

   while(true) {
      const code = remaining % 26;
      letter = String.fromCharCode(65 + code) + letter;
      remaining = Math.floor(remaining / 26) - 1;

      if(remaining < 0) break;
   }

   return letter;
};

// 组件卸载时清理
const cleanup = () => {
   if(removeKeyListener) {
      removeKeyListener();
   }
};

// 注册清理函数
onUnmounted(cleanup);

// 初始化
nextTick(() => {
   console.log('表格组件初始化完成');
});
</script>

<style scoped>
.table-container {
  max-width: 1200px;
  margin: 20px auto;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}

h3 {
  margin-bottom: 20px;
  color: #333;
}

.selection-info {
  margin-top: 10px;
  color: #666;
  font-size: 14px;
  padding: 5px 10px;
  background-color: #f5f7fa;
  border-radius: 4px;
}

.status-message {
  margin-top: 10px;
  padding: 5px 10px;
  border-radius: 4px;
  color: #fff;
}

.status-message.success {
  background-color: #52c41a;
}

.status-message.warning {
  background-color: #faad14;
}

.status-message.error {
  background-color: #f5222d;
}

/* 阻止默认文本选择 */
::v-deep .el-table,
::v-deep .el-table__cell {
  user-select: none !important;
  -webkit-user-select: none !important;
}

/* 移除表格默认样式干扰 */
::v-deep .el-table__cell:focus-within {
  outline: none !important;
}
</style>
posted on 2025-10-11 18:14  Uncle_MrLee  阅读(4)  评论(0)    收藏  举报