<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <script src="https://unpkg.com/vue@3.2.47"></script>
    <link rel="stylesheet" href="https://unpkg.com/element-plus@1.1.0-beta.12/dist/index.css">
    <script src="https://unpkg.com/element-plus@1.1.0-beta.12"></script>
    <title>Element Plus demo</title>
  </head>
  <body>
    <div id="app">

     <div class="tb-container" ref="tbContainerRef">
      <h2 style="margin:0;text-align:center;">Element Plus 可编辑表格</h2>

      <!-- 表格 -->
      <el-table
        :data="testDatas"
        border
        style="width: 100%;margin-top:10px"
        @header-contextmenu="(column, $event) => rightClick(null, column, $event)"
        @row-contextmenu="rightClick"
        :row-class-name="tableRowClassName"
      >
        <el-table-column v-if="columnList.length > 0" type="index" label="编号" :width="60"></el-table-column>
        <el-table-column v-for="(col, idx) in columnList" :key="col.prop" :index="idx">
          <!-- 自定义表头的内容 -->
          <template #header>
            <p v-show="col.show" @dblclick="$event => handleEdit(col, $event.target)">
              {{col.label}} 
              <i class="el-icon-edit-outline" @click="$event => handleEdit(col, $event.target.parentNode)"></i>
            </p>
            <el-input
              size="mini"
              v-show="!col.show"
              v-model="col.label"
              @blur="col.show=true">
            </el-input>
          </template>
          <!-- 自定义列的内容-->
          <template #default="{ row }">
            <p v-show="row[col.prop].show" @dblclick="$event => handleEdit(row[col.prop], $event.target)">
              {{row[col.prop].content}} 
              <i class="el-icon-edit-outline" @click="$event => handleEdit(row[col.prop], $event.target.parentNode)"></i>
            </p>
            <el-input type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
              v-show="!row[col.prop].show"
              v-model="row[col.prop].content"
              @blur="row[col.prop].show = true">
            </el-input>
          </template>
        </el-table-column>
      </el-table>
      <p style="text-align:left;color:#ccc;">右键菜单,双击编辑</p>

      <div>
        <h3 style="text-align:center;">实时数据展示</h3>
        <label>当前目标:</label>
        <p>{{JSON.stringify(curTarget)}}</p>
        <label>表头:</label>
        <p v-for="col in columnList" :key="col.prop">{{JSON.stringify(col)}}</p>
        <label>数据:</label>
        <ul>
          <li v-for="(data,idx) in testDatas" :key="idx">
            <p v-for="(key,idx1) in Object.keys(data)" :key="idx1">{{key + ': ' + JSON.stringify(data[key])}}</p>
          </li>
        </ul>
      </div>

      <!-- 表头右键菜单 -->
      <div v-show="showMenu" id="contextmenu">
        <i class="el-icon-circle-close hideContextMenu" @click="showMenu=false"></i>
        <el-button size="mini" type="primary" @click="addColumn()">前方插入一列</el-button>
        <el-button size="mini" type="primary" @click="addColumn(true)">后方插入一列</el-button>
        <el-popconfirm title="确定删除该列吗?" @confirm="delColumn">
          <template #reference>
            <el-button type="primary" size="mini">删除当前列</el-button>
          </template>
        </el-popconfirm>
        <div v-show="!curTarget.isHead">
          <hr/>
          <el-button size="mini" type="primary" @click="addRow()">上方插入一行</el-button>
          <el-button size="mini" type="primary" @click="addRow(true)">下方插入一行</el-button>
          <el-popconfirm title="确定删除该行吗?" @confirm="delRow">
            <template #reference>
              <el-button type="primary" size="mini">删除当前行</el-button>
            </template>
          </el-popconfirm>
        </div>
      </div>

     </div>
     <!-- 相关链接 -->
     <div style="text-align:left;border-top:1px solid #666">
      <p>相关链接: </p>
      <a href="https://blog.csdn.net/ymzhaobth/article/details/104716431" target="_blank">博文</a><br/>
      <a href="https://github.com/zymbth/editable-table" target="_blank">代码仓库</a><br/>
      <a href="https://zymbth.github.io/editable-table/" target="_blank">Github Page</a><br/>
<!--         <a href="https://codepen.io/zymbth/pen/BaJpvoO" target="_blank">Editable Table V1 (vue3 & Element Plus)</a><br/> -->
      <span>Editable Table V1 (vue3 & Element Plus)</span><br/>
      <a href="https://codepen.io/zymbth/pen/gOogZMK" target="_blank">Editable Table V2 (vue3 & Element Plus)</a><br/>
      <a diabled href="https://codepen.io/zymbth/pen/eYWYrmz" target="_blank">Editable Table V2 (vue2 & Element UI)</a>
     </div>

    </div>

    <script type="module">
      const App = {
        data() {
          return {
            columnList: [
              { prop: "name", label: '姓名', show: true },
              { prop: "age", label: '年龄', show: true },
              { prop: "city", label: '城市', show: true },
              { prop: "tel", label: '电话', show: true }
            ],
            testDatas: [{
              name: { content: '张三', show: true },
              age: { content: 24, show: true },
              city: { content: '广州', show: true },
              tel: { content: '13312345678', show: true }
            },{
              name: { content: '李四', show: true },
              age: { content: 25, show: true },
              city: { content: '九江', show: true },
              tel: { content: '18899998888', show: true }
            }],
            count_col: 0,
            showMenu: false,
            curTarget: {                 // 当前目标信息
              rowIdx: null,              // 行下标
              colIdx: null,              // 列下标
              isHead: undefined          // 当前目标是表头?
            },
          }
        },
        methods: {
          /**
           * 表头/单元格编辑处理
           *
           * @param {Object} cell - The cell object to edit.
           * @param {HTMLElement} pEl - The parent element of the cell.
           */
          handleEdit(cell, pEl) {
            const editIputEl = Array.from(pEl.nextSibling.childNodes).find(n => ['INPUT','TEXTAREA'].includes(n.tagName))
            cell.show = false
            editIputEl && this.$nextTick(() => {
              editIputEl.focus()
            })
          },
          rightClick(row, column, $event) {
            // 阻止浏览器自带的右键菜单弹出
            $event.preventDefault()
            if(column.index == null) return
            // 表格容器的位置
            const { x: tbX, y: tbY } = this.$refs.tbContainerRef.getBoundingClientRect()
            // 当前鼠标位置
            const { x: pX, y: pY } = $event
            // 定位菜单
            const ele = document.getElementById('contextmenu')
            ele.style.top = pY - tbY - 6 + 'px'
            ele.style.left = pX - tbX - 6 + 'px'
            // 边界调整
            if(window.innerWidth - 140 < pX - tbX) {
              ele.style.left = 'unset'
              ele.style.right = 0
            }
            this.showMenu = true
            // 当前目标
            this.curTarget = {
              rowIdx: row ? row.row_index : null,
              colIdx: column.index,
              isHead: !row
            }
          },

          // 新增行
          addRow(later) {
            this.showMenu = false
            if(this.curTarget.rowIdx === null) return
            const idx = later ? this.curTarget.rowIdx + 1 : this.curTarget.rowIdx
            let obj = {}
            this.columnList.forEach(p => {
              obj[p.prop] = { content: '', show: true }
            })
            this.testDatas.splice(idx, 0, obj)
          },
          // 删除行
          delRow() {
            this.showMenu = false
            this.curTarget.rowIdx !== null && this.testDatas.splice(this.curTarget.rowIdx, 1)
          },
          // 新增列
          addColumn(later) {
            this.showMenu = false
            const idx = later ? this.curTarget.colIdx + 1 : this.curTarget.colIdx
            let obj = { prop: 'col_' + ++this.count_col, label: '', show: true }
            this.columnList.splice(idx, 0, obj)
            this.testDatas.forEach(p => {
              // vue3无需 this.$set(p, obj.col, { content: '', show: true }) // vue2中, 新增的对象无法被监听到
              p[obj.prop] = { content: '', show: true }
            })
          },
          // 删除列
          delColumn() {
            this.showMenu = false
            let colKey = this.columnList[this.curTarget.colIdx].prop
            this.columnList.splice(this.curTarget.colIdx, 1)
            this.testDatas.forEach(p => delete p[colKey] )
          },
          // 添加表格行下标
          tableRowClassName({row, rowIndex}) {
            row.row_index = rowIndex
          },
        }
      };
      const app = Vue.createApp(App);
      app.use(ElementPlus);
      app.mount("#app");
    </script>
  </body>

  <style>
    html,body {margin:0;padding:0;}
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      color: #2c3e50;
      padding: 20px;
    }
    label {font-weight:bold;}
    #tb-container {position: relative;}
    #contextmenu {
      position:absolute;
      top: 0;
      left: 0;
      height:auto;
      width:120px;
      border-radius: 3px;
      border: 1px solid #999999;
      background-color: #f4f4f4;
      padding: 10px;
      z-index: 12;
    }
    #contextmenu button {display:block;margin:0 0 5px;}
    .hideContextMenu {position:absolute;top:5px;right:5px;}
    th .el-input__inner {padding:0 6px;}
    .el-textarea__inner {padding:3px 6px;}
  </style>
</html>
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.