用el-table实现的可编辑的动态表格组件

用el-table实现的可编辑的动态表格组件

需求

  1. 点击单元格可编辑内容
  2. 右键单元格可选择"向下合并"或"拆分"
  3. 点击"新增行"按钮添加新行
  4. 点击"删除"按钮删除行(不能删除被合并的行)

说明

  1. 仅选择了具有特殊属性的列做合并与拆分操作
  2. span-method没有生效,【暂不清楚】,当前是使用的操作dom的方式来改变合并状态
  3. 删除时,如果是合并项的第一项会自动拆分并删除,其他则提示不能删除

实现效果





代码

使用数据

js 复制代码
cs_columns: any = [
    { label: '项目', prop: 'proName' },
    { label: '位置', prop: 'projectLocation' },
    { label: '有无地铁', prop: 'subway' },
    { label: '通勤时间', prop: 'time' },
    { label: '结论', prop: 'checkResult' },
  ]
cs_checkItemsList: any = [
    {
      proName: '土壤重金属检测',
      projectLocation: '农田北区',
      subway: '无',
      time: '30分钟',
      checkResult: '合格'
    },
    {
      proName: '水质农药残留',
      projectLocation: '灌溉水渠',
      subway: '有',
      time: '45分钟',
      checkResult: '不合格'
    },
    {
      proName: '空气污染物检测',
      projectLocation: '加工厂周边',
      subway: '无',
      time: '60分钟',
      checkResult: '合格'
    },
    {
      proName: '农产品营养成分',
      projectLocation: '果蔬大棚',
      subway: '有',
      time: '25分钟',
      checkResult: '合格'
    },
    {
      proName: '饲料添加剂检测',
      projectLocation: '养殖场',
      subway: '无',
      time: '40分钟',
      checkResult: '不合格'
    }
  ]

使用EditTable组件

html 复制代码
<edit-table ref="editTable" :columns="columns" :initial-data="checkItemsList"></edit-table>

EditTable组件具体实现

ts 复制代码
<template>
  <div>
    <el-button class="m-y-16" @click="addRow" type="primary" size="small">新增行</el-button>
    <el-table
      ref="table"
      :data="tableData"
      border
      :span-method="objectSpanMethod"
      style="width: 100%">

      <el-table-column type="index" label="序号" width="50px"></el-table-column>
      <el-table-column
        v-for="col in columns"
        :key="col.prop"
        :prop="col.prop"
        :label="col.label"
        :width="col.width">
        <template #default="scope">
          <div v-if="editingCell.rowIndex === scope.$index && editingCell.colKey === col.prop">
            <el-input
              v-model="scope.row[col.prop]"
              @blur="saveEdit"
              size="small"
              autofocus />
          </div>
          <div
            v-else
            :id="col.prop + '-' + scope.$index"
            @click="handleCellClick(scope.$index, col.prop)"
            @contextmenu="handleContextMenu($event, scope.$index, col.prop)"
          >
            {{ scope.row[col.prop] || '--' }}
          </div>
        </template>
      </el-table-column>

      <el-table-column label="操作" width="100">
        <template #default="scope">
          <el-button
            size="small"
            @click="deleteRow(scope.$index)"
            type="danger">
            删除
          </el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 右键菜单 -->
    <div v-show="contextMenu.visible"
         :style="{left: contextMenu.left+'px', top: contextMenu.top+'px'}"
         class="context-menu">
      <div class="menu-item" @click="mergeCells">向下合并</div>
      <div class="menu-item" @click="splitCells">拆分</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'EditTable',
  props: {
    initialData: {
      type: Array,
      default: () => []
    },
    columns: {
      type: Array,
      default: () => []
    },
  },
  data() {
    return {
      tableData: this.initialData.length > 0 ? this.initialData : [],
      spanArr: [],
      editingCell: {
        rowIndex: -1,
        colKey: ''
      },
      contextMenu: {
        visible: false,
        left: 0,
        top: 0,
        rowIndex: -1
      },
      colIndex: -1,
      checkItemsList: []
    }
  },
  created() {
    this.colIndex = this.columns.findIndex(item => item.prop === 'checkResult');
    this.initSpanArr();
    // 点击其他地方关闭右键菜单
    document.addEventListener('click', () => {
      this.contextMenu.visible = false
    })
  },
  mounted() {
    // 初始调用保持不变
    this.$nextTick(() => {
      this.initEditTable();
    });
  },
  methods: {
    // 初始化合并规则
    initSpanArr() {
      this.spanArr = this.tableData.map(item => {
        return item.rowspan || 1
      })
    },

    initEditTable() {
      // 添加更多的检查条件确保元素已渲染
      if (this.$refs.table && this.tableData.length > 0 && this.spanArr.length > 0) {
        this.tableData.forEach((item, index) => {
          // 确保元素存在且 rowspan 大于 1
          if (item.rowspan > 1 && index < this.spanArr.length) {
            const elementById = document.getElementById('checkResult-' + index);
            if (elementById) {
              const parentNode = elementById.parentNode.parentNode;
              parentNode.rowSpan = item.rowspan;

              // 隐藏被合并的行
              for (let i = 1; i < item.rowspan && (index + i) < this.tableData.length; i++) {
                const nextElementById = document.getElementById('checkResult-' + (index + i));
                if (nextElementById) {
                  const nextParentNode = nextElementById.parentNode.parentNode;
                  nextParentNode.style.display = 'none';
                }
              }
            }
          }
        });
      }
    },

    // 合并单元格方法
    objectSpanMethod({ column, rowIndex }) {
      if (column.prop === 'checkResult') {
        if (this.spanArr[rowIndex]) {
          return {
            rowspan: this.spanArr[rowIndex],
            colspan: 1
          }
        }
      }
    },

    // 单元格点击编辑
    handleCellClick(rowIndex, colKey) {
      this.editingCell = { rowIndex, colKey }
    },

    // 保存编辑
    saveEdit() {
      this.editingCell = { rowIndex: -1, colKey: '' }
    },

    // 处理右键菜单
    handleContextMenu(event, rowIndex, colKey) {
      if (colKey === 'checkResult') {
        event.preventDefault();
        this.contextMenu = {
          visible: true,
          left: event.clientX,
          top: event.clientY,
          rowIndex
        };
      }
    },

    // 合并单元格
    mergeCells() {
      const { rowIndex } = this.contextMenu;
      if (rowIndex === -1) return;

      // 获取当前行在checkResult列的rowspan
      const currentRowspan = this.spanArr[rowIndex];

      // 检查是否可以合并下一行(防止越界)
      console.log("当前行的index:",rowIndex,"当前行的rowSpan:", currentRowspan, "表数据行数", this.tableData.length);
      if (rowIndex + currentRowspan >= this.tableData.length) {
        this.$message.warning('无法向下合并,已到达表格底部');
        this.contextMenu.visible = false;
        return;
      }

      // 检查目标行是否已被其他单元格合并
      const targetRowIndex = rowIndex + currentRowspan;
      console.log("目标行的index", targetRowIndex);
      if (this.spanArr[targetRowIndex] === 0) {
        this.$message.warning('无法合并,目标行已被其他单元格合并');
        this.contextMenu.visible = false;
        return;
      }

      // 检查下一行的值是否相同
      console.log("当前行的值", this.tableData[rowIndex][this.columns[this.colIndex].prop]);
      console.log("下一行的值", this.tableData[targetRowIndex][this.columns[this.colIndex].prop]);
      if (this.tableData[targetRowIndex][this.columns[this.colIndex].prop] !== this.tableData[rowIndex][this.columns[this.colIndex].prop]) {
        this.$message.warning('无法合并,目标行单元格的值不相同');
        this.contextMenu.visible = false;
        return;
      }

      // 获取当前行的元素
      const elementById = document.getElementById('checkResult-' + rowIndex)
      // 获取当前行的父元素的父元素,设置它的rowspan
      const parentNode = elementById.parentNode.parentNode
      // 获取目标行的rowspan
      const targetRowspan = this.spanArr[targetRowIndex];
      console.log("目标行的rowspan", targetRowspan);

      // 更新spanArr数据:将当前单元格的rowspan增加目标单元格的rowspan
      this.spanArr[rowIndex] += targetRowspan;
      // 获取当前行的父元素的父元素,设置它的rowspan
      parentNode.rowSpan = this.spanArr[rowIndex]

      // 将被合并的行标记为rowspan=0,表示被合并
      // 如果目标行还合并了其他行,需要将这些行也标记为被当前行合并
      for (let i = 0; i < targetRowspan; i++) {
        if (targetRowIndex + i < this.spanArr.length) {
          let nextElementById = document.getElementById('checkResult-' + (targetRowIndex + i))
          let nextParentNode = nextElementById.parentNode.parentNode
          nextParentNode.style.display = 'none'
          this.spanArr[targetRowIndex + i] = 0;
        }
      }
      console.log(this.spanArr);

      this.contextMenu.visible = false;
    },

    // 拆分单元格
    splitCells() {
      const { rowIndex } = this.contextMenu;
      if (rowIndex === -1) return;

      const currentRowspan = this.spanArr[rowIndex];
      console.log("当前行行数", rowIndex,"当前行的所占行数",currentRowspan);

      // 如果当前单元格没有合并其他行,则无需拆分
      if (currentRowspan <= 1) {
        this.$message.warning('当前单元格未合并其他行');
        this.contextMenu.visible = false;
        return;
      }

      // 获取当前行的元素并设置rowSpan为1
      const elementById = document.getElementById('checkResult-' + rowIndex);
      if (elementById) {
        const parentNode = elementById.parentNode.parentNode;
        parentNode.rowSpan = 1;
      }

      // 将当前行的rowspan重置为1
      this.spanArr[rowIndex] = 1;

      // 恢复被合并行的rowspan为1
      for (let i = 1; i < currentRowspan; i++) {
        const targetIndex = rowIndex + i;

        // 显示被隐藏的单元格
        const nextElementById = document.getElementById('checkResult-' + targetIndex);
        if (nextElementById) {
          const nextParentNode = nextElementById.parentNode.parentNode;
          nextParentNode.style.display = 'table-cell';
          nextParentNode.rowSpan = 1;
        }

        // 恢复被合并行的span值为1
        this.spanArr[targetIndex] = 1;
        this.tableData[targetIndex][this.columns[this.colIndex].prop] = this.tableData[rowIndex][this.columns[this.colIndex].prop];
      }

      console.log(this.spanArr);

      this.contextMenu.visible = false;
    },

    // 新增行
    addRow() {
      const newRow = {};
      this.columns.forEach(col => {
        newRow[col.prop] = '';
      });

      this.tableData.push(newRow);
      this.spanArr.push(1);
    },

    // 删除行
    deleteRow(index) {
      // 如果该行是被合并的行,则不允许删除
      if (this.spanArr[index] === 0) {
        this.$message.warning('不能删除被合并的行,请先拆分单元格');
        return;
      }

      // 如果是合并的起始行,需要先拆分
      if (this.spanArr[index] > 1) {
        // 设置当前行索引用于拆分
        this.contextMenu.rowIndex = index;
        // 执行拆分
        this.splitCells();
      }
      this.tableData.splice(index, 1)
      this.spanArr.splice(index, 1)
    }
  }
}
</script>

<style scoped>
.context-menu {
  position: fixed;
  z-index: 9999;
  background: #fff;
  border: 1px solid #ebeef5;
  border-radius: 4px;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  padding: 10px;
  display: flex;
  flex-direction: column;
}

.menu-item {
  padding: 8px 20px;
  cursor: pointer;
  color: #606266;
}

.menu-item:hover {
  background: #f5f7fa;
  color: #409eff;
}
</style>
相关推荐
燕山石头13 分钟前
解决 IntelliJ IDEA Build时 Lombok 不生效问题
java·前端·intellij-idea
chancygcx_19 分钟前
前端核心技术Node.js(二)——path模块、HTTP与模块化
前端·http·node.js
YGY_Webgis糕手之路21 分钟前
Cesium 快速入门(三)Viewer:三维场景的“外壳”
前端·gis·cesium
丘色果31 分钟前
NPM打包时,报reason: getaddrinfo ENOTFOUND registry.nlark.com
前端·npm·node.js
姜太小白38 分钟前
【前端】CSS Flexbox布局示例介绍
前端·css
我命由我123451 小时前
Spring Boot 项目问题:Web server failed to start. Port 5566 was already in use.
java·前端·jvm·spring boot·后端·spring·java-ee
南囝coding1 小时前
最近Vibe Coding的经验总结
前端·后端·程序员
前端小咸鱼一条2 小时前
React组件化的封装
前端·javascript·react.js
随便起的名字也被占用2 小时前
leaflet中绘制轨迹线的大量轨迹点,解决大量 marker 绑定 tooltip 同时显示导致的性能问题
前端·javascript·vue.js·leaflet
南方kenny2 小时前
TypeScript + React:让前端开发更可靠的黄金组合
前端·react.js·typescript