vue+element ui 动态实现多元化表格(包括竖型表格,多级表头,单元格合并,可编辑,可折叠以及自定义样式)

5,301 阅读3分钟

一、分析

1. test.json

{
    "keys": {
        "result_label": {
            "label": "商品类别"
        },
        "round1_label": {
            "label": "双十一",
            "children": {
                "round1_remark": "销量(件)",
                "round1": "库存(件)"
            }
        },
        "round2_label": {
            "label": "双十二",
            "children": {
                "round2_remark": "销量(件)",
                "round2": "库存(件)"
            }
        }
    },
    "project_results": [
        {
            "result_label": "上装",
            "round1": "",
            "round1_remark": "",
            "round2": "",
            "round2_remark": "",
            "children": [
                {
                    "result_label": "外套",
                    "round1": "20000",
                    "round1_remark": "5000",
                    "round2": "30000",
                    "round2_remark": "2000"
                },
                {
                    "result_label": "夹克",
                    "round1": "27476",
                    "round1_remark": "1239",
                    "round2": "32760",
                    "round2_remark": "800"
                },
                {
                    "result_label": "卫衣",
                    "round1": "72008",
                    "round1_remark": "2909",
                    "round2": "74579",
                    "round2_remark": "2132"
                },
                {
                    "result_label": "衬衣",
                    "round1": "83790",
                    "round1_remark": "2009",
                    "round2": "71323",
                    "round2_remark": "3323"
                },
                {
                    "result_label": "T恤",
                    "round1": "290890",
                    "round1_remark": "21221",
                    "round2": "878878",
                    "round2_remark": "4343"
                }
            ]
        },
        {
            "result_label": "下装",
            "round1": "",
            "round1_remark": "",
            "round2": "",
            "round2_remark": "",
            "children": [
                {
                    "result_label": "短裤",
                    "round1": "1350706",
                    "round1_remark": "50706",
                    "round2": "1954425",
                    "round2_remark": "32342"
                },
                {
                    "result_label": "休闲裤",
                    "round1": "1575289",
                    "round1_remark": "11223",
                    "round2": "1631917",
                    "round2_remark": "67887"
                },
                {
                    "result_label": "工装裤",
                    "round1": "129797",
                    "round1_remark": "8787",
                    "round2": "734011",
                    "round2_remark": "54454"
                },
                {
                    "result_label": "牛仔裤",
                    "round1": "1801629",
                    "round1_remark": "34242",
                    "round2": "734011",
                    "round2_remark": "4321"
                }
            ]
        },
        {
            "result_label": "配饰",
            "round1": "",
            "round1_remark": "",
            "round2": "",
            "round2_remark": "",
            "children": [
                {
                    "result_label": "帽子",
                    "round1": "2158",
                    "round1_remark": "233",
                    "round2": "6759",
                    "round2_remark": "100"
                },
                {
                    "result_label": "包包",
                    "round1": "1465",
                    "round1_remark": "32",
                    "round2": "497",
                    "round2_remark": "55"
                }
            ]
        }
    ]
}

2. 需求

  • 将“商品类别”,“双十一”,“双十二”作为一级表头。

  • 其中一级表头“双十一”,“双十二”都包含二级表头“库存(件)”和“销量(件)”。

  • 对上装,下装和配饰的子项进行展开及折叠。

  • 对上装,下装和配饰这三行进行单元格合并。

  • 实现对销量单元格的可编辑功能,其他单元格不可编辑。

3. 知识点

  • element ui 的多级表头功能。

  • element ui 的树形数据与懒加载功能。

  • element ui 的合并行或列功能。

  • 自定义可编辑组件 EditableCell。

  • 自定义样式。

4. 效果

二、实现

1. 自定义表格可编辑组件 EditableCell

<template>
  <div class="edit-cell" @dblclick="onFieldClick">
    <!-- 文字提示 -->
    <el-tooltip
      v-if="!editMode && !showInput"
      :placement="toolTipPlacement"
      :open-delay="toolTipDelay"
      :content="toolTipContent"
    >
      <div
        tabindex="0"
        class="cell-content"
        :class="{'edit-enabled-cell': canEdit}"
        @keyup.enter="onFieldClick"
      >
        <slot name="content" />
      </div>
    </el-tooltip>
    <!-- Vue框架自定义的标签,它的用途就是可以动态绑定我们的组件 -->
    <!-- 通过属性is的值可以渲染不同的组件 -->
    <component
      :is="editableComponent"
      v-if="editMode || showInput"
      ref="input"
      v-model="model"
      v-bind="$attrs"
      @focus="onFieldClick"
      @keyup.enter.native="onInputExit"
      v-on="listeners"
    >
      <slot name="edit-component-slot" />
    </component>
  </div>
</template>
<script>
export default {
  name: 'EditableCell',
  // 子组件不继承父组件的特性
  inheritAttrs: false,
  // 子组件使用props来声明需要从父组件接受的数据
  props: {
    value: {
      type: String,
      default: ''
    },
    toolTipContent: {
      type: String,
      default: '双击进行编辑'
    },
    toolTipDelay: {
      type: Number,
      default: 500
    },
    toolTipPlacement: {
      type: String,
      default: 'top-start'
    },
    showInput: {
      type: Boolean,
      default: false
    },
    editableComponent: {
      type: String,
      default: 'el-input'
    },
    closeEvent: {
      type: String,
      default: 'blur'
    },
    canEdit: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      editMode: false
    }
  },
  /**
   * 1.computed用来监控自己定义的变量,
   * 该变量不在data里面声明,直接在computed里面定义,
   * 然后就可以在页面上进行双向数据绑定展示出结果或者用作其他处理
   *
   * 2.computed比较适合对多个变量或者对象进行处理后返回一个结果值,
   * 也就是数多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化,
   * 举例:购物车里面的商品列表和总金额之间的关系,只要商品列表里面的商品数量发生变化,或减少或增多或删除商品,
   * 总金额都应该发生变化。这里的这个总金额使用computed属性来进行计算是最好的选择
   */
  computed: {
    model: {
      get() {
        return this.value
      },
      set(val) {
        // 子组件触发父组件
        this.$emit('input', val)
      }
    },
    listeners() {
      return {
        [this.closeEvent]: this.onInputExit,
        ...this.$listeners
      }
    }
  },
  methods: {
    onFieldClick() {
      if (this.canEdit) {
        this.editMode = true
        // this.$nextTick这个方法作用是当数据被修改后使用这个方法会回调获取更新后的dom再渲染出来
        this.$nextTick(() => {
          const inputRef = this.$refs.input
          if (inputRef && inputRef.focus) {
            inputRef.focus()
          }
        })
      }
    },
    onInputExit() {
      this.editMode = false
    },
    onInputChange(val) {
      this.$emit('input', val)
    }
  }
}
</script>
<style>
.cell-content {
 min-height: 30px;
 line-height: 30px;
 /* padding-left: 5px;
 padding-top: 5px; */
 border: 1px solid transparent;
}
.edit-enabled-cell {
 /* border: 1px dashed #409eff; */
 border:0
}
</style>

2. 定义页面渲染所需的参数

data() {
    return {
      disabled: false, // 表格可编辑
      abled: true, // 表格不可编辑

      labelList: {}, // 用于存放一级表头
      tableData_show: {} // 页面表格渲染数据
    }
  },

3. 获取本地 json 数据并进行操作及赋值

import goodsData from '/static/test.json'// 获取本地json数据

created() {
    this.tableData_show = goodsData//赋值,该变量用于表格数据的渲染
    for (var key in goodsData['keys']) {
      this.labelList[key] = goodsData['keys'][key]['label']// 将一级表头单独存放
    }
  },

4. 注册 EditableCell 组件

import EditableCell from '@/views/task/components/EditableCell'

components: {
   EditableCell
},

5.页面渲染

<template>
  <div class="app-container progStyle">
    <!-- 表格标题 -->
    <h4 style="text-align: center;margin: 0 0 10px 0;font-size:18px;">淘宝店铺库存销量对比表</h4>
    <!-- 表格 -->
    <el-table
      :data="tableData_show['project_results']"
      style="width: 100%;"
      border
      :cell-class-name="addClass"
      row-key="result_label"
      default-expand-all
      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
      :span-method="arraySpanMethod"
    >
      <!-- 一级表头:商品类别、双十一、双十二  -->
      <el-table-column
        v-for="(value1,key1,index1) in tableData_show['keys']"
        :key="index1"
        :label="value1['label']"
        align="center"
      >
        <!-- 注意这里一定要写,获取一级表头下所有单元格数据 -->
        <template slot-scope="scope">
          <span>{{ scope.row[key1] }}</span>
        </template>

        <!-- 二级表头:销量(件)、库存(件) -->
        <div v-if="key1!=='result_label'">
          <el-table-column
            v-for="(value2,key2,index2) in value1['children']"
            :key="index2"
            :label="value2"
            align="center"
          >
            <!-- 二级表头下所有单元格数据 -->
            <editable-cell
              v-model="row[key2]"
              slot-scope="{row}"
              :can-edit="(key2.indexOf('remark')===-1) ? disabled : abled"
            >
              <span slot="content">{{ row[key2] }}</span>
            </editable-cell>
          </el-table-column>
        </div>
      </el-table-column>
    </el-table>
  </div>
</template>

6. 合并单元格方法 arraySpanMethod 及自定义单元格样式的方法 addClass

methods: {
    // 合并单元格
    arraySpanMethod({ row }) {
      var keys = Object.keys(row)
      if (keys.indexOf('children') > -1) {
        return [1, Object.keys(this.labelList).length * 2 + 1]
      }
    },

    // 自定义单元格的样式
    addClass({ row, columnIndex }) {
      if (columnIndex === 0 && Object.keys(row).indexOf('children') > -1) {
        return 'fatherColStyle'// 第一列父项的样式
      } else if (columnIndex === 0 && Object.keys(row).indexOf('children') === -1) {
        return 'sonStyle'// 第一列子项的样式
      }
    }
  }

7. 表格自定义样式

<style lang="scss">
// 表格边框样式
.progStyle .el-table--border, .el-table--group{
  border: 1px solid #979fb1;
}
.progStyle .el-table--border th,
.progStyle .el-table__fixed-right-patch,
.progStyle .el-table td,
.progStyle .el-table th.is-leaf{
  border-bottom: 1px solid #979fb1;
}
.progStyle .el-table--border td,
.progStyle .el-table--border th,
.progStyle .el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{
  border-right: 1px solid #979fb1;
}

// 父项icon样式
.progStyle .el-table [class*=el-table__row--level] .el-table__expand-icon{
  height: 32px;
  line-height: 32px;
  float: left;
}

// 表头样式
.progStyle .el-table th, .el-table tr:nth-child(0) {
  background-color: #cbcfd9;
}
.progStyle .el-table th>.cell{
  color: black;
  font-size: 18px;
}

// 第一列父项样式
.fatherColStyle{
  font-weight: bold;
  font-size: 16px;
  background-color: #f5f7fa;
  color: black;
}
// 文字样式
.fatherColStyle span{
  float: left;
}

//第一列子项的样式,第一行表头样式
.sonStyle{
  font-weight: bold;
  background-color: #f0f9eb;
  font-size: 14px;
  color: #27a2ff;
}
</style>

8. 完整代码

<template>
  <div class="app-container progStyle">
    <!-- 表格标题 -->
    <h4 style="text-align: center;margin: 0 0 10px 0;font-size:18px;">淘宝店铺库存销量对比表</h4>
    <!-- 表格 -->
    <el-table
      :data="tableData_show['project_results']"
      style="width: 100%;"
      border
      :cell-class-name="addClass"
      row-key="result_label"
      default-expand-all
      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
      :span-method="arraySpanMethod"
    >
      <!-- 一级表头:商品类别、双十一、双十二  -->
      <el-table-column
        v-for="(value1,key1,index1) in tableData_show['keys']"
        :key="index1"
        :label="value1['label']"
        align="center"
      >
        <!-- 注意这里一定要写,获取一级表头下所有单元格数据 -->
        <template slot-scope="scope">
          <span>{{ scope.row[key1] }}</span>
        </template>

        <!-- 二级表头:销量(件)、库存(件) -->
        <div v-if="key1!=='result_label'">
          <el-table-column
            v-for="(value2,key2,index2) in value1['children']"
            :key="index2"
            :label="value2"
            align="center"
          >
            <!-- 二级表头下所有单元格数据 -->
            <editable-cell
              v-model="row[key2]"
              slot-scope="{row}"
              :can-edit="(key2.indexOf('remark')===-1) ? disabled : abled"
            >
              <span slot="content">{{ row[key2] }}</span>
            </editable-cell>
          </el-table-column>
        </div>
      </el-table-column>
    </el-table>
  </div>
</template>
<script>
import goodsData from '/static/test.json'// 本地json
import EditableCell from '@/views/task/components/EditableCell'

export default {
  components: {
    EditableCell
  },
  data() {
    return {
      disabled: false, // 表格可编辑
      abled: true, // 表格不可编辑

      labelList: {}, // 用于存放一级表头
      tableData_show: {} // 页面表格渲染数据
    }
  },

  created() {
    // 获取本地json数据
    this.tableData_show = goodsData
    for (var key in goodsData['keys']) {
      this.labelList[key] = goodsData['keys'][key]['label']// 将一级表头单独存放
    }
  },

  methods: {
    // 合并单元格
    arraySpanMethod({ row }) {
      var keys = Object.keys(row)
      if (keys.indexOf('children') > -1) {
        return [1, Object.keys(this.labelList).length * 2 + 1]
      }
    },

    // 自定义单元格的样式
    addClass({ row, columnIndex }) {
      if (columnIndex === 0 && Object.keys(row).indexOf('children') > -1) {
        return 'fatherColStyle'// 第一列父项的样式
      } else if (columnIndex === 0 && Object.keys(row).indexOf('children') === -1) {
        return 'sonStyle'// 第一列子项的样式
      }
    }
  }
}
</script>

<style lang="scss">
// 表格边框样式
.progStyle .el-table--border, .el-table--group{
  border: 1px solid #979fb1;
}
.progStyle .el-table--border th,
.progStyle .el-table__fixed-right-patch,
.progStyle .el-table td,
.progStyle .el-table th.is-leaf{
  border-bottom: 1px solid #979fb1;
}
.progStyle .el-table--border td,
.progStyle .el-table--border th,
.progStyle .el-table__body-wrapper .el-table--border.is-scrolling-left~.el-table__fixed{
  border-right: 1px solid #979fb1;
}

// 父项icon样式
.progStyle .el-table [class*=el-table__row--level] .el-table__expand-icon{
  height: 32px;
  line-height: 32px;
  float: left;
}

// 表头样式
.progStyle .el-table th, .el-table tr:nth-child(0) {
  background-color: #cbcfd9;
}
.progStyle .el-table th>.cell{
  color: black;
  font-size: 18px;
}

// 第一列父项样式
.fatherColStyle{
  font-weight: bold;
  font-size: 16px;
  background-color: #f5f7fa;
  color: black;
}
// 文字样式
.fatherColStyle span{
  float: left;
}

//第一列子项的样式,第一行表头样式
.sonStyle{
  font-weight: bold;
  background-color: #f0f9eb;
  font-size: 14px;
  color: #27a2ff;
}
</style>