21

封装element-ui表格,我是这样做的

 3 years ago
source link: https://segmentfault.com/a/1190000023382224
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

明晚八点准时走,谁不打卡谁是狗。

使用过 element-ui 的表格的同学应该都有这样的体会,做一个简单的表格还比较容易,但如果这个表格包含了顶部的按钮,还有分页,甚至再包含了行编辑,那开发工作量就成倍的增加,特别是在开发管理系统的时候,表格一个接一个的去开发, 即浪费时间,还对个人没有什么提升。今天小编带来了自己封装的一个表格,让你用 JSON 就可以简单的生成表格。

本文主要集中于使用说明与核心代码说明,完整代码请访问 https://github.com/snowzijun/vue-element-table ,如果觉得有用,麻烦给小编一个 star ,你的每一个 star 都是对小编的支持,当前功能比较简陋,本仓库将持续更新。同时您也可以微信搜索【前端有的玩】公众号,与小编进行沟通。

表格需求

一般管理系统对表格会有以下需求

  1. 可以分页(需要有分页条)
  2. 可以多选(表格带复选框)
  3. 顶部需要加一些操作按钮(新增,删除等等)
  4. 表格每行行尾有操作按钮
  5. 表格行可以编辑

如下图为一个示例表格

emqQ7z7.jpg!web

如果我们直接使用 element-ui 提供的组件的话,那么开发一个这样的表格就需要使用到以下内容

  1. 需要使用表格的插槽功能,开发每一行的按钮
  2. 需要通过样式调整顶部按钮,表格,分页条的布局样式
  3. 需要监听分页的事件然后去刷新表格数据
  4. 顶部按钮或操作按钮如果需要获取表格数据,需要调用表格提供的api
  5. 对于有行编辑的需求,还需要通过插槽去渲染行编辑的内容,同时要控制行编辑的开关

不仅仅开发表格比较麻烦,而且还要考虑团队协作,如果每个人实现表格的方式存在差别,那么可能后期的维护成本也会变得很高。那怎么办呢?

表格配置

为了满足团队快速开发的需要,小编对上面提出来的需求进行了封装,然后使用的时候,开发人员只需要配置一些 JSON 便可以完成以上功能的开发。

基础配置

一个基础的表格包含了数据和列信息,那么如何用封装的表格去配置呢?

<template>
  <zj-table
    :columns="columns"
    :data="data"
    :pagination="false"
  />
</template>
<script>
export default {
  data() {
    return {
      // 表格的列信息, 数组每一项代表一个字段,可以使用element 列属性的所有属性,以下仅为示例
      columns: Object.freeze([
        {
          // 表头显示的文字
          label: '姓名',
          // 对应数据里面的字段
          prop: 'name'
        },
        {
          label: '性别',
          prop: 'sex',
          // 格式化表格,与element-ui 的表格属性相同
          formatter(row, column, cellValue) {
            return cellValue === 1 ? '男' : '女'
          }
        },
        {
          label: '年龄',
          prop: 'age'
        }
      ]),
      data: [
        {
          name: '子君',
          sex: 1,
          age: 18
        }
      ]
    }
  }
}
</script>

通过上面的配置,就可以完成一个基础表格的开发,完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/base.vue ,效果如下图所示

aqIVjaa.jpg!web

表格默认会显示复选框,也可以通过配置 selectable 属性来关闭掉

添加分页

简单的表格用封装之后的或未封装的开发工作量区别并不大,我们继续为表格添加上分页

<template>
    <!--
    current-page.sync 表示页码, 添加上 .sync 在页码发生变化时自动同步页码
    page-size.sync 每页条数
    total  总条数
    height="auto" 配置height:auto, 表格高度会根据内容自动调整,如果不指定,表格将保持充满父容器,同时表头会固定,不跟随滚动条滚动
    @page-change 无论pageSize currentPage 哪一个变化,都会触发这个事件
  -->
  <zj-table
    v-loading="loading"
    :columns="columns"
    :data="data"
    :current-page.sync="currentPage"
    :page-size.sync="pageSize"
    :total="total"
    height="auto"
    @page-change="$_handlePageChange"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        // 列字段与上例一样,此处省略
      ]),
      data: [],
      // 当前页码
      currentPage: 1,
      // 每页条数
      pageSize: 10,
      // 总条数
      total: 0,
      // 是否显示loading
      loading: false
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    // 加载表格数据
    loadData() {
      this.loading = true
      setTimeout(() => {
        // 假设总条数是40条
        this.total = 40
        const { currentPage, pageSize } = this
        // 模拟数据请求获取数据
        this.data = new Array(pageSize).fill({}).map((item, index) => {
          return {
            name: `子君${currentPage + (index + 1) * 10}`,
            sex: Math.random() > 0.5 ? 1 : 0,
            age: Math.floor(Math.random() * 100)
          }
        })
        this.loading = false
      }, 1000)
    },
    $_handlePageChange() {
      // 因为上面设置属性指定了.sync,所以这两个属性会自动变化
      console.log(this.pageSize, this.currentPage)
      // 分页发生变化,重新请求数据
      this.loadData()
    }
  }
}
</script>

完整代码请参考 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/pagination.vue

通过封装,表格将自带分页功能,通过上面代码,实现效果如下所示,是不是变得简单了一些。接下来我们继续给表格添加按钮

qyeYRnJ.jpg!web

添加顶部按钮

表格上面可能会有新增,删除等等按钮,怎么办呢,接下来我们继续通过配置去添加按钮

<template>
  <zj-table
    :buttons="buttons"
  />
</template>
<script>
export default {
  data() {
    return {
      buttons: Object.freeze([
        {
          // id 必须有而且是在当前按钮数组里面是唯一的
          id: 'add',
          text: '新增',
          type: 'primary',
          icon: 'el-icon-circle-plus',
          click: this.$_handleAdd
        },
        {
          id: 'delete',
          text: '删除',
          // rows 是表格选中的行,如果没有选中行,则禁用删除按钮, disabled可以是一个boolean值或者函数
          disabled: rows => !rows.length,
          click: this.$_handleRemove
        },
        {
          id: 'auth',
          text: '这个按钮根据权限显示',
          // 可以通过返回 true/false来控制按钮是否显示
          before: (/** rows */) => {
            return true
          }
        },
        // 可以配置下拉按钮哦
        {
          id: 'dropdown',
          text: '下拉按钮',
          children: [
            {
              id: 'moveUp',
              text: '上移',
              icon: 'el-icon-arrow-up',
              click: () => {
                console.log('上移')
              }
            },
            {
              id: 'moveDown',
              text: '下移',
              icon: 'el-icon-arrow-down',
              disabled: rows => !rows.length,
              click: () => {
                console.log('下移')
              }
            }
          ]
        }
      ])
    }
  },
  created() {},
  methods: {
    // 新增
    $_handleAdd() {
      this.$alert('点击了新增按钮')
    },
    // 顶部按钮会自动将表格所选的行传出来
    $_handleRemove(rows) {
      const ids = rows.map(({ id }) => id)
      this.$alert(`要删除的行id为${ids.join(',')}`)
    },
    // 关注作者公众号
    $_handleFollowAuthor() {}
  }
}
</script>

表格顶部可以添加普通的按钮,也可以添加下拉按钮,同时还可以通过 before 来配置按钮是否显示, disabled 来配置按钮是否禁用,上面完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue

通过上面的代码就可以配置出下面的表格,是不是很简单呢?

AzqEBrr.jpg!web

表格顶部可以有按钮,行尾也是可以添加按钮的,一起来看看

行操作按钮

一般我们会将一些单行操作的按钮放在行尾,比如编辑,下载等按钮,那如何给行尾配置按钮呢?

<template>
  <zj-table
    :columns="columns"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        {
          // 可以指定列的宽度,与element-ui原生用法一致
          width: 220,
          label: '姓名',
          prop: 'name'
        },
        // 行编辑按钮,在表格末尾出现,自动锁定右侧
        {
          width: 180,
          label: '操作',
          // 通过 actions 指定行尾按钮
          actions: [
            {
              id: 'follow',
              text: '关注作者',
              click: this.$_handleFollowAuthor
            },
            {
              id: 'edit',
              text: '编辑',
              // 可以通过before控制按钮是否显示,比如下面年龄四十岁的才会显示编辑按钮
              before(row) {
                return row.age < 40
              },
              click: this.$_handleEdit
            },
            {
              id: 'delete',
              text: '删除',
              icon: 'el-icon-delete',
              disabled(row) {
                return row.sex === 0
              },
              // 为了拿到this,这里需要用箭头函数
              click: () => {
                this.$alert('女生被禁止删除了')
              }
            }
          ]
        }
      ])
    }
  },
  methods: {
    // 关注作者公众号
    $_handleFollowAuthor() {
            console.log('微信搜索【前端有的玩】,这是对小编最大的支持')
    },
    /**
     * row 这一行的数据
     */
    $_handleEdit(row, column) {
      this.$alert(`点击了姓名为【${row.name}】的行上的按钮`)
    }
  }
}
</script>

行操作按钮会被冻结到表格最右侧,不会跟随滚动条滚动而滚动,上面完整代码见, https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue

通过上面的代码就可以完成以下效果

7JZr2yR.jpg!web

最后再来一起看看行编辑

行编辑

比如上例,我希望点击行尾的编辑按钮的时候,可以直接在行上面编辑用户的姓名与性别,如何配置呢?

<template>
  <zj-table
    ref="table"
    :columns="columns"
    :data="data"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        {
          label: '姓名',
          prop: 'name',
          editable: true,
          field: {
            componentType: 'input',
            rules: [
              {
                required: true,
                message: '请输入姓名'
              }
            ]
          }
        },
        {
          label: '性别',
          prop: 'sex',
          // 格式化表格,与element-ui 的表格属性相同
          formatter(row, column, cellValue) {
            return cellValue === '1' ? '男' : '女'
          },
          editable: true,
          field: {
            componentType: 'select',
            options: [
              {
                label: '男',
                value: '1'
              },
              {
                label: '女',
                value: '0'
              }
            ]
          }
        },
        {
          label: '年龄',
          prop: 'age',
          editable: true,
          field: {
            componentType: 'number'
          }
        },
        {
          label: '操作',
          actions: [
            {
              id: 'edit',
              text: '编辑',
              // 如果当前行启用了编辑,则不显示编辑按钮
              before: row => {
                return !this.editIds.includes(row.id)
              },
              click: this.$_handleEdit
            },
            {
              id: 'save',
              text: '保存',
              // 如果当前行启用了编辑,则显示保存按钮
              before: row => {
                return this.editIds.includes(row.id)
              },
              click: this.$_handleSave
            }
          ]
        }
      ]),
      data: [
        {
          // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"
          id: '0',
          name: '子君',
          sex: '1',
          age: 18
        },
        {
          // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"
          id: '1',
          name: '子君1',
          sex: '0',
          age: 18
        }
      ],
      editIds: []
    }
  },
  methods: {
    $_handleEdit(row) {
      // 通过调用 startEditRow 可以开启行编辑
      this.$refs.table.startEditRow(row.id)
      // 记录开启了行编辑的id
      this.editIds.push(row.id)
    },
    $_handleSave(row) {
      // 点击保存的时候,通过endEditRow 结束行编辑
      this.$refs.table.endEditRow(row.id, (valid, result, oldRow) => {
        // 如果有表单验证,则valid会返回是否验证成功
        if (valid) {
          console.log('修改之后的数据', result)
          console.log('原始数据', oldRow)
          const index = this.editIds.findIndex(item => item === row.id)
          this.editIds.splice(index, 1)
        } else {
          // 如果校验失败,则返回校验的第一个输入框的异常信息
          console.log(result)
          this.$message.error(result.message)
        }
      })
    }
  }
}
</script>

不需要使用插槽就可以完成行编辑,是不是很开心。上述完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/row-edit.vue

效果如下图所示:

aE7bayR.jpg!web

其他功能

除了上面的功能之外,表格还可以配置其他许多功能,比如

link

表格开发说明

通过上面的代码示例,我们已经知道了封装之后的表格可以完成哪些事情,接下来一起来看看表格是如何实现的。完整代码见 https://github.com/snowzijun/vue-element-table/tree/master/src/components/zj-table

表格布局

整个表格是通过 JSX 来封装的,因为 JSX 使用起来更加灵活。对于我们封装的表格,我们从竖向可以分为三部分,分别是顶部按钮区,中间表格区,底部分页区,如何去实现三个区域的布局呢,核心代码如下

render(h) {
    // 按钮区域
    const toolbar = this.$_renderToolbar(h)
    // 表格区域
    const table = this.$_renderTable(h)
    // 分页区域
    const page = this.$_renderPage(h)

    return (
      <div class="zj-table" style={{ height: this.tableContainerHeight }}>
        {toolbar}
        {table}
        {page}
      </div>
    )
  }

通过三个 render 函数分别渲染对应区域,然后将三个区域组合在一起。

渲染表格列

通过前文的讲解,我们可以将表格的列分为以下几种

  1. 常规列
  2. 行编辑列
  3. 操作按钮列
  4. 插槽列
  5. 链接列(文档后续完善)
  6. 嵌套列(文档后续完善)
$_renderColumns(h, columns) {
      // 整体是否排序
      let sortable = this.sortable ? 'custom' : false
      return columns
        .filter(column => {
          const { hidden } = column
          if (hidden !== undefined) {
            if (typeof hidden === 'function') {
              return hidden({
                columns,
                column
              })
            }
            return hidden
          }
          return true
        })
        .map(column => {
          const {
            useSlot = false,
            // 如果存在操作按钮,则actions为非空数组
            actions = [],
            // 是否可编辑列, 对于可编辑列需要动态启用编辑
            editable = false,
            // 是否有嵌套列
            nests,
            // 是否可点击
            link = false
          } = column
          let newSortable = sortable
          if (column.sortable !== undefined) {
            newSortable = column.sortable ? 'custom' : false
          }
          column = {
            ...column,
            sortable: newSortable
          }
          if (nests && nests.length) {
            // 使用嵌套列
            return this.$_renderNestColumn(h, column)
          } else if (editable) {
            // 使用编辑列
            return this.$_renderEditColumn(h, column)
          } else if (useSlot) {
            // 使用插槽列
            return this.$_renderSlotColumn(h, column)
          } else if (actions && actions.length > 0) {
            // 使用操作列
            column.sortable = false
            return this.$_renderActionColumn(h, column)
          } else if (link) {
            // 使用链接列
            return this.$_renderLinkColumn(h, column)
          } else {
            // 使用默认列
            return this.$_renderDefaultColumn(h, column)
          }
        })
    },

行编辑列

当前表格行编辑支持 input , select , datepicker , TimeSelect , InputNumber 等组件,具体渲染代码如下所示

// 编辑单元格
    $_renderEditCell(h, field) {
      const components = {
        input: Input,
        select: ZjSelect,
        date: DatePicker,
        time: TimeSelect,
        number: InputNumber
      }
      const componentType = field.componentType
      const component = components[componentType]
      if (component) {
        return this.$_renderField(h, field, component)
      } else if (componentType === 'custom') {
        // 如果自定义,可以通过component指定组件
        return this.$_renderField(h, field, field.component)
      }
      return this.$_renderField(h, field, Input)
    },
    $_renderField(h, field, Component) {
      // 编辑行的id字段
      const { rowId, events = {}, nativeEvents = {} } = field

      const getEvents = events => {
        const newEvents = {}
        Object.keys(events).forEach(key => {
          const event = events[key]
          newEvents[key] = (...rest) => {
            const args = [
              ...rest,
              {
                rowId,
                row: this.editRowsData[rowId],
                value: this.editRowsData[rowId][field.prop]
              }
            ]
            return event(...args)
          }
        })
        return newEvents
      }
      // 事件改写
      const newEvents = getEvents(events)
      const newNativeEvents = getEvents(nativeEvents)
      return (
        <Component
          size="small"
          on={newEvents}
          nativeOn={newNativeEvents}
          v-model={this.editRowsData[rowId][field.prop]}
          {...{
            attrs: field,
            props: field
          }}
        />
      )
    }

总结

这个表格包含了许多功能,文章长度优先,如果觉得有用,可以通过访问 https://github.com/snowzijun/vue-element-table 查看完整代码,本仓库代码及文档小编将持续完善,欢迎star。缘始积累,希望你可以关注一下下方公众号,这是对小编最大的支持

fUzm6jM.gif

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK