表体只有一列表格

  • 示例图

da801813fa9242ec87323c182ce12b80

  • 表体只有一列
  • 组件代码

      <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>
    
posted @ 2025-12-01 16:54  不完美的完美  阅读(0)  评论(0)    收藏  举报