el-table实现可编辑表格的开发历程

206 阅读6分钟

写在前面的话

  想直接看代码的朋友可以省略下面的历程直接翻到最底下,我把完整示例代码放在最下面

引子

  笔者最近在做项目中遇到了一件事,某个迭代我们需要对项目进行UI改造,特别是把当前正在使用的一个可编辑表格换一下UI。说是换UI,其实是换表格,因为当前在用的表格组件是项目组花钱买的,但老板应该是对这个组件的UI有别的想法(其实就是觉得丑),然后经过老大的决定,我们需要换成Element-UI的组件(Element打钱~~ )。
  虽说组件要换,但是我们要尽可能的保留原先的功能,原来的组件,在使用上面非常贴近于Excel表格。然后,笔者开始库库干了。。。

初步实现

  为了快速实现功能,我们首先选择的是把这个可编辑表格的所有编辑项全部展示出来,这样用户就可以直接进行表格的编辑,就像这样:

PixPin_2025-09-23_01-08-19.gif   但很快,我们就发现了第一个问题。
  我们的表格中,有两列下拉框使用了远端搜索功能,同时使用了一个封装的下拉选择组件。这就使得当下拉框有值的时候,它会尝试用value在下拉选项中去匹配对应的label,而下拉选项需要通过远端搜索即调接口获取。这两列调的是同一个接口,哪怕这里做了分页并且默认一页10条,仍然默认会调同一个接口20次,这是一个很影响性能的问题,如果切换成一页20条、30条、50条的话,后果不堪设想。。。
  考虑到这种情况,我们首先采取的方法是只调一次接口,把选项数据全部拉回来本地,然后让使用这些选项的下拉框直接引用。但在这里,我们又发现,这些下拉框是这样的:

PixPin_2025-09-23_01-47-28.gif   是的,label和value同时展示出来,而且在远程搜索中,可以搜索label或value来找对应项。
  那这里我们就得使用filter-method自定义搜索方法咯,但这里有个问题,那就是搜索结果得要是独立的才行,即:第一次搜索的选项结果,不能出现在第二次的搜索选项里,意思就是每次搜索完,需要把选项还原到默认状态。这好办,visible-change事件可以实现。
  当我们把实现的功能交付出去后,产品给我们带来了一个噩耗:用户非得要跟Excel一样的,也就是说为了满足用户的使用习惯,我们需要尽可能还原出原来的表格组件来

image.png

解决之道

第一步

  事已至此,先吃饭吧,啊,不是,先百度吧
  在某次冲浪中,我发现了一篇文章,里面提到使用el-table组件的cell-dblclick事件来实现双击进入编辑状态的做法,也就是下面这样:
  通过列的prop和行的id一起来定位到双击选中的单元格的位置,然后通过v-if使得输入框渲染出来

PixPin_2025-09-23_02-48-59.gif

<template>
  <el-table
    :data="tableData"
    style="width: 100%"
    @cell-dblclick="cellDblclick"
  >
    <el-table-column prop="name" label="姓名" width="180">
      <template slot-scope="scope">
        <el-input v-if="formViewMethod(scope)" v-model="scope.row.name"></el-input>
        <span v-else>{{ scope.row.name }}</span>
      </template>
    </el-table-column>
  </el-table>
</template>
<script>
export default {
  data() {
    return {
      editColumnProp: null,
      editRowId: null
    }
  },
  methods: {
    cellDblclick(row, column, cell) {
      cell.style.background = 'pink'
      this.editColumnProp = column.property
      this.editRowId = row.id
    },
    formViewMethod(scope) {
      const { row, column } = scope
      return (
        row.id === this.editRowId &&
        this.editColumnProp === column.property
      )
    }
  }
}
</script>

  这方法确实可行!在默认是text的情况下,也就不会去调接口,这样,哪怕是用回远端搜索功能,也能保证对性能没有那种压力。
  第一步走出了,另一个问题就摆在眼前了:当我点击编辑框以外的地方,该怎么让它恢复默认那种文本状态呢?文章的作者并没有给出答案,那就得自己去寻找了

新的曙光

  最近在冲浪中,我了解到有一个名为ClickOutside的指令,这是一个vue3中的自定义指令,顾名思义,就是点击外面的意思。这下子灵感就来了:在cell-dblclick事件中,我们可以获取到当前单元格的dom,那如果我们在获取dom的时候,给它加上一个点击事件,当点击到外面的时候,就清空当前单元格的选中状态,那是不是就可以实现了呢?说干就干,上代码:

cellDblclick(row, column, cell) {
  cell.style.background = 'pink'
  cell.__clickOutside__ = (e) => {
    if (cell.contains(e.target)) {
      return console.log('点击了自己')
    }
    console.log('点击了外面')
    cell.__clickOutside__ && document.removeEventListener('click', cell.__clickOutside__)
  }
  document.addEventListener('click', cell.__clickOutside__)
  this.editColumnProp = column.property
  this.editRowId = row.id
}

在这里我们使用了dom的contains()方法,这个方法用于检测一个元素是否包含另一个元素,返回的是一个布尔值。也就是当点击的时候,判断被点击元素B是否在双击的时候绑定点击事件的元素A之内,如果返回true的话,就是点击自己了,否则就是点击外面,这样就能实现清空选中状态的方法了。就像下面这样子:

PixPin_2025-09-23_03-54-37.gif 到这里,可编辑表格的功能就算实现了,谢谢大家观看,下面会贴上完整的示例代码,大伙儿可以直接复制粘贴来看看效果。

完整代码

<template>
  <div class="irregular-table-container">
    <div class="custom-table">
      <!-- 表格区域 -->
      <el-table
        :data="tableData"
        style="width: 100%"
        @cell-dblclick="cellDblclick"
      >
        <el-table-column
          prop="date"
          label="日期"
          width="180">
        </el-table-column>
        <el-table-column prop="name" label="姓名" width="180">
          <template slot-scope="scope">
            <el-input v-if="formViewMethod(scope)" v-model="scope.row.name"></el-input>
            <span v-else>{{ scope.row.name }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="gender"
          label="性别"
          width="120"
          :formatter="formatGender"
        >
        </el-table-column>
        <el-table-column
          prop="city"
          label="城市"
          width="200"
          :formatter="formatCity"
        >
        </el-table-column>
        <el-table-column prop="address" label="地址"/>
      </el-table>
    </div>
  </div>
</template>

<script>
export default {
  name: "EditableTable",
  data() {
    return {
      tableData: [
        {
          id: 1,
          date: '2016-05-02',
          name: '王小虎',
          gender: '男',
          city: 'Beijing',
          address: '上海市普陀区金沙江路 1518 弄'
        }, {
          id: 2,
          date: '2016-05-04',
          name: '王小虎',
          gender: '男',
          city: 'Nanjing',
          address: '上海市普陀区金沙江路 1517 弄'
        }, {
          id: 3,
          date: '2016-05-01',
          name: '王小虎',
          gender: '男',
          city: 'Guangzhou',
          address: '上海市普陀区金沙江路 1519 弄'
        }, {
          id: 4,
          date: '2016-05-03',
          name: '王小虎',
          gender: '男',
          city: 'Shanghai',
          address: '上海市普陀区金沙江路 1516 弄'
        }
      ],
      options: [
        { label: '男', value: 1 },
        { label: '女', value: 0 }
      ],
      cities: [
        {
          value: 'Beijing',
          label: '北京'
        }, {
          value: 'Shanghai',
          label: '上海'
        }, {
          value: 'Nanjing',
          label: '南京'
        }, {
          value: 'Chengdu',
          label: '成都'
        }, {
          value: 'Shenzhen',
          label: '深圳'
        }, {
          value: 'Guangzhou',
          label: '广州'
        }
      ],
      editColumnProp: null,
      editRowId: null
    }
  },
  computed: {},
  created() {},
  methods: {
    formatName(row) {
      const input = (
        <el-input v-model={row.name} clearable />
      )
      return input
    },
    formatGender(row) {
      const select = (
        <el-select v-model={row.gender}>
          {this.options.map(item => {
            return (
              <el-option
                key={item.value}
                label={item.label}
                value={item.value}
              />
            )
          })}
        </el-select>
      )
      return select
    },
    formatCity(row) {
      const select = (
        <el-select v-model={row.city}>
          {this.cities.map(item => {
            return (
              <el-option
                key={item.value}
                label={item.label}
                value={item.value}
              >
              <span style="float: left">{ item.label }</span>
              <span style="float: right; color: #8492a6; font-size: 13px">{ item.value }</span>
              </el-option>
            )
          })}
        </el-select>
      )
      return select
    },
    cellDblclick(row, column, cell) {
      cell.style.background = 'pink'
      cell.__clickOutside__ = (e) => {
        if (cell.contains(e.target)) {
          return console.log('点击了自己')
        }
        // console.log('点击了外面')
        this.editColumnProp = null
        this.editRowId = null
        cell.__clickOutside__ && document.removeEventListener('click', cell.__clickOutside__)
      }
      document.addEventListener('click', cell.__clickOutside__)
      this.editColumnProp = column.property
      this.editRowId = row.id
    },
    formViewMethod(scope) {
      const { row, column } = scope
      return (
        row.id === this.editRowId &&
        this.editColumnProp === column.property
      )
    }
  }
}
</script>

感谢名单

  写完一看时间,嚯,好家伙,凌晨4点了,赶紧碎觉,狗命要紧~~
  最后的最后,这里要感谢两位给我提供灵感和思路的大大,我把他们的文章链接放到下面了,感兴趣的小伙伴可以过去学习下。
vue对el-table的二次封装,双击单元格编辑,避免表格输入框过多卡顿
vue自定义指令(v-clickoutside)-点击当前区域之外的位置