表体只有一列表格
- 示例图

- 表体只有一列
-
组件代码
<template> <div class="coordinate-table h-100" @paste="(e) => handleTablePaste(e)" @mousedown="handleDragStart"> <!-- 拖拽开始事件 --> <table v-if="tableData.length > 0" ref="dataTable"> <tr v-for="(row, rowIdx) in tableData" :key="rowIdx"> <!-- Y轴单元格 --> <td class="y-axis-cell" :style="CellStyle" :class="{'yellow-actived': activeRowIdx === rowIdx, }"> {{ yAxis[rowIdx] }} </td> <!-- 数据单元格:添加选中高亮类 --> <td class="td-box" :style="CellStyle" @click="setActiveRow(rowIdx)" :class="{'selected': isRowSelected(rowIdx)}"> <input type="number" v-limit-decimal class="content-input" :disabled="!row.bUpdate" @input="onChangeItemValue" @click.stop="setActiveRow(rowIdx)" v-model="row.value" :data-row="rowIdx" @keydown="handleKeydown($event, rowIdx)" /> </td> </tr> </table> </div> </template> <script lang="ts"> import { Component, Vue, Prop, Model, Watch } from "vue-property-decorator"; import _ from "lodash"; @Component({ name: "SingleColumnTable", components: {} }) export default class extends Vue { @Prop({ type: Array, default: [] }) yAxis: string[]; // 纵坐标 @Model("change") value; tableData = null; // 记录当前激活的行(粘贴起始行/键盘移动) activeRowIdx = -1; // 复制相关状态:记录选中范围(单列仅需行范围) copyState = { startRow: -1, // 选中起始行 endRow: -1, // 选中结束行 isDragging: false, // 是否正在拖拽选中 hasValidSelection: false, // 是否有有效选中区域(多行) }; // 表格DOM引用(用于滚动) $refs: { dataTable: HTMLTableElement; }; @Watch("value", { immediate: true }) onChangeValue(value) { this.tableData = value; if (this.tableData.length > 0) { this.onChangeItemValue(); } } mounted() { // 绑定Ctrl+C快捷键和删除键监听 window.addEventListener("keydown", this.handleKeyboardShortcuts); // 绑定拖拽结束、取消事件 document.addEventListener("mousemove", this.handleDragMove); document.addEventListener("mouseup", this.handleDragEnd); document.addEventListener("mouseleave", this.handleDragEnd); } beforeDestroy() { // 解绑全局事件 window.removeEventListener("keydown", this.handleKeyboardShortcuts); document.removeEventListener("mousemove", this.handleDragMove); document.removeEventListener("mouseup", this.handleDragEnd); document.removeEventListener("mouseleave", this.handleDragEnd); } // 记录当前激活的行(点击单元格时触发) setActiveRow(rowIdx: number) { // 仅激活可编辑行 if (this.tableData[rowIdx]?.bUpdate) { this.activeRowIdx = rowIdx; // 点击时选中当前单行(不算有效选中区域) this.copyState.startRow = rowIdx; this.copyState.endRow = rowIdx; this.copyState.hasValidSelection = false; // 滚动到当前行(避免被滚动条遮挡) this.scrollToRow(rowIdx); // 激活后自动聚焦输入框 this.focusCellInput(rowIdx); } } // 判断当前行是否在选中范围(用于高亮) isRowSelected(rowIdx: number): boolean { const { startRow, endRow, hasValidSelection } = this.copyState; // 无有效选中范围或行禁用时返回false if (!hasValidSelection || startRow === -1 || endRow === -1) { return false; } // 检查行是否可编辑 if (!this.tableData[rowIdx]?.bUpdate) { return false; } // 兼容正向/反向拖拽 const minRow = Math.min(startRow, endRow); const maxRow = Math.max(startRow, endRow); return rowIdx >= minRow && rowIdx <= maxRow; } // 拖拽开始:记录起始行 handleDragStart(e: MouseEvent) { // 只处理左键拖拽 if (e.button !== 0) { return; } // 仅响应数据列(.td-box)的拖拽 const cell = (e.target as HTMLElement).closest(".td-box"); if (!cell) { // 点击空白处清除选中 this.clearSelection(); return; } const input = cell.querySelector(".content-input") as HTMLInputElement; if (!input || input.disabled) { // 点击禁用行清除选中 this.clearSelection(); return; } const startRow = Number(input.dataset.row); // 检查起始行是否可编辑 if (!this.tableData[startRow]?.bUpdate) { this.clearSelection(); return; } // 初始化选中范围 this.copyState = { startRow, endRow: startRow, isDragging: true, hasValidSelection: false, }; this.activeRowIdx = startRow; } // 拖拽中:更新结束行(实时高亮) handleDragMove(e: MouseEvent) { if (!this.copyState.isDragging) { return; } const cell = (e.target as HTMLElement).closest(".td-box"); if (!cell) { return; } const input = cell.querySelector(".content-input") as HTMLInputElement; if (!input || input.disabled) { return; } const endRow = Number(input.dataset.row); // 检查结束行是否可编辑 if (!this.tableData[endRow]?.bUpdate) { return; } // 更新结束行,触发视图刷新(高亮选中区域) this.copyState.endRow = endRow; // 标记为有效选中区域(多行) this.copyState.hasValidSelection = this.copyState.startRow !== endRow; } // 拖拽结束:停止拖拽状态 handleDragEnd() { this.copyState.isDragging = false; } // 清除选中状态 clearSelection() { this.copyState = { startRow: -1, endRow: -1, isDragging: false, hasValidSelection: false, }; } // 删除选中区域内容 deleteSelectedRows() { const { startRow, endRow, hasValidSelection } = this.copyState; if (!hasValidSelection || startRow === -1 || endRow === -1) { return; } // 计算实际选中范围 const minRow = Math.min(startRow, endRow); const maxRow = Math.max(startRow, endRow); // 遍历选中区域,只删除可编辑行内容 for (let row = minRow; row <= maxRow; row++) { const currentRow = this.tableData[row]; if (currentRow?.bUpdate) { this.$set(currentRow, "value", null); } } // 更新小计 this.onChangeItemValue(); // 清除选中状态 this.clearSelection(); } // 处理键盘快捷键(复制+删除) handleKeyboardShortcuts(e: KeyboardEvent) { const target = e.target as HTMLElement; // 排除输入框聚焦时的全局删除(保留输入框自身删除功能) const isInputFocused = target.tagName === "INPUT" || target.tagName === "TEXTAREA"; // Ctrl+C 或 Cmd+C(Mac) if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "c") { e.preventDefault(); this.copySelectedContent(); } // Delete 或 Backspace 删除选中区域(非输入框聚焦时) if ( [8, 46].includes(e.keyCode) && this.copyState.hasValidSelection && !isInputFocused ) { e.preventDefault(); this.deleteSelectedRows(); } } // 复制选中内容(核心逻辑) copySelectedContent() { const { startRow, endRow, hasValidSelection } = this.copyState; const targetTable = this.tableData; // 无选中范围时,默认复制激活行 let sRow = startRow; let eRow = endRow; if (!hasValidSelection || startRow === -1 || endRow === -1) { if (this.activeRowIdx === -1) { this.$message.error("请先选中要复制的单元格"); return; } sRow = eRow = this.activeRowIdx; } // 规范选中范围(确保start <= end) const minRow = Math.min(sRow, eRow); const maxRow = Math.max(sRow, eRow); // 校验表格数据有效性 if (!targetTable || !targetTable.length) { this.$message.error("表格无数据可复制"); return; } if (minRow >= targetTable.length || maxRow >= targetTable.length) { this.$message.error("复制范围超出表格行数"); return; } // 构建复制内容(单列:每行一个值,换行分隔,Excel可直接识别) const copyContent = []; for (let i = minRow; i <= maxRow; i++) { const row = targetTable[i]; // 兼容禁用行、空值 const value = row?.value ?? ""; copyContent.push(value.toString()); } // 写入剪贴板 const copyText = copyContent.join("\n"); if (!copyText.trim()) { this.$message.error("无有效内容可复制"); return false; } this.setClipboard(copyText); if (minRow === maxRow) { // this.$message.success("单个单元格复制成功"); } else { this.$message.success(`选中${maxRow - minRow + 1}行数据复制成功`); } } // 写入剪贴板工具方法(兼容现代/旧浏览器) setClipboard(content) { // 现代浏览器 if (navigator?.clipboard) { navigator.clipboard.writeText(content).catch(err => { const error = err as Error; // 处理权限拒绝错误 if (error?.name === "NotAllowedError") { this.$message.error( "剪贴板权限被拒绝,请在浏览器设置中允许剪贴板访问" ); } else { this.$message.error(error?.message || "复制失败,请手动选中内容复制"); } }); } else { // 兼容旧浏览器(IE11等) const textarea = document.createElement("textarea"); textarea.value = content; textarea.style.position = "fixed"; textarea.style.top = "-999px"; textarea.style.left = "-999px"; document.body.appendChild(textarea); textarea.select(); try { const success = document.execCommand("copy"); if (!success) { this.$message.error("execCommand 复制失败"); } } catch (err) { this.$message.error("复制失败,请手动选中内容复制"); } finally { document.body.removeChild(textarea); } } } // 辅助函数:判断是否为数字(含小数、负数) isNumber(str: string): boolean { return /^-?\d+(\.\d+)?$/.test(str.trim()); } // 处理表格粘贴事件(保留原有逻辑) handleTablePaste(e: ClipboardEvent) { e.preventDefault(); // 阻止默认粘贴行为 const clipboardData = e.clipboardData || (window as any).clipboardData; if (!clipboardData) { return; } const pastedText = clipboardData.getData("text").trim(); if (!pastedText) { return; } const pastedRows = pastedText .split(/\r?\n/) .filter(line => line.trim() !== "") .map(line => { const columns = line.split(/\t/).map(col => col.trim()); return columns[0] || ""; }) .filter(val => val !== ""); const targetTable = this.tableData; if (!targetTable || targetTable.length === 0) { return; } const startRow = this.activeRowIdx; // 格式化数值为正整数或x.5的方法 const formatToValidValue = (value: string): number | null => { if (!value) { return null; } // 清除所有非数字和小数点的字符(过滤负号) let cleaned = value.replace(/[^0-9.]/g, ""); if (!cleaned) { return null; } // 处理小数点 const dotIndex = cleaned.indexOf("."); if (dotIndex !== -1) { // 只保留一个小数点 cleaned = cleaned.slice(0, dotIndex + 1) + cleaned.slice(dotIndex + 1).replace(/\./g, ""); const integerPart = cleaned.slice(0, dotIndex) || "0"; const decimalPart = cleaned.slice(dotIndex + 1); // 小数部分只能是5且长度为1 const validDecimal = decimalPart.charAt(0) === "5" ? "5" : ""; cleaned = `${integerPart}${validDecimal ? "." + validDecimal : ""}`; // 去除末尾的小数点 if (cleaned.endsWith(".")) { cleaned = cleaned.slice(0, -1); } } // 处理纯整数(去除前导零) if (cleaned.indexOf(".") === -1) { cleaned = cleaned.replace(/^0+(?=\d)/, "") || "0"; } // 转换为数字(确保非空) return cleaned ? Number(cleaned) : null; }; pastedRows.forEach((pastedValue, offset) => { const currentRowIdx = startRow + offset; if (currentRowIdx >= targetTable.length) { return; } const targetRow = targetTable[currentRowIdx]; if (!targetRow || !targetRow.bUpdate) { return; } // 格式化粘贴值(正整数或x.5) const finalValue = formatToValidValue(pastedValue); this.$set(targetRow, "value", finalValue); }); this.onChangeItemValue(); this.$emit("table-pasted", { startRow, pastedCount: pastedRows.length, updatedData: _.cloneDeep(this.tableData), }); } // 原有:输入值变化时计算小计 onChangeItemValue() { const tmpIndex = this.yAxis.indexOf("小计"); if (tmpIndex === -1) { return; } const list = this.tableData.filter((_, index) => index !== tmpIndex); const total = list.reduce((sum, item) => sum + Number(item.value || 0), 0); this.tableData[tmpIndex].value = total || ""; } // 键盘事件(新增单个单元格删除处理) handleKeydown(e: KeyboardEvent, currentRowIdx: number) { // 处理删除键(单个单元格) if ([8, 46].includes(e.keyCode)) { e.preventDefault(); // 如果有选中区域,优先删除选中区域 if (this.copyState.hasValidSelection) { this.deleteSelectedRows(); } else { // 否则删除当前单元格内容 if (this.tableData[currentRowIdx]?.bUpdate) { this.$set(this.tableData[currentRowIdx], "value", null); this.onChangeItemValue(); } } return; } if ([37, 38, 39, 40, 13].includes(e.keyCode)) { e.preventDefault(); e.stopPropagation(); } let newRowIdx = currentRowIdx; switch (e.keyCode) { case 38: // 上箭头 newRowIdx = currentRowIdx > 0 ? currentRowIdx - 1 : currentRowIdx; break; case 40: // 下箭头 newRowIdx = currentRowIdx < this.tableData.length - 1 ? currentRowIdx + 1 : currentRowIdx; break; case 13: // 回车 newRowIdx = currentRowIdx < this.tableData.length - 1 ? currentRowIdx + 1 : currentRowIdx; break; default: return; } if (newRowIdx !== currentRowIdx) { if (this.tableData[newRowIdx]?.bUpdate) { this.setActiveRow(newRowIdx); } } } // 聚焦到指定行的输入框(保留原有逻辑) focusCellInput(rowIdx: number) { this.$nextTick(() => { const input = document.querySelector( `.content-input[data-row="${rowIdx}"]` ) as HTMLInputElement; if (input && !input.disabled) { input.focus(); input.select(); } }); } // 滚动到指定行(保留原有逻辑) scrollToRow(rowIdx: number) { const table = this.$refs.dataTable; if (!table) { return; } const input = document.querySelector( `.content-input[data-row="${rowIdx}"]` ); if (!input) { return; } const cell = input.closest("td"); if (cell) { cell.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest", }); } } get CellStyle() { return { width: `80px`, minWidth: `65px`, height: `30px`, }; } } </script> <style lang="scss" scoped> .coordinate-table { overflow-x: auto; table { border-collapse: collapse; position: relative; td { border: 1px solid #d7d7d7; text-align: center; box-sizing: border-box; margin: 0; padding: 0; } .axis-cell { background-color: #fff2cc; font-weight: bold; } .sticky-box { position: sticky; right: 0; } .y-axis-cell { background-color: #ffe0e0; font-weight: bold; position: sticky; left: 0; z-index: 10; // 避免被选中区域遮挡 &.yellow-actived { background-color: #ffc107; } } .td-box { transition: background-color 0.1s; // 选中状态样式(Excel风格) &.selected { background-color: #0078d7; border-color: #1890ff; // 选中时输入框背景同步 .content-input { background-color: #0078d7; } } } .content-input { width: 100%; height: 100%; padding: 0; border: none; box-sizing: border-box; text-align: center; background-color: #fff; font-size: 14px; color: #000; } .content-input:disabled { background-color: #ffe0e0; cursor: not-allowed; } .content-input:focus { outline: none; background-color: #0078d7; // box-shadow: inset 0 0 0 2px #1890ff; } } } </style>

浙公网安备 33010602011771号