博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
书写一个管理平台开发常用的通用table组件
阅读量:6721 次
发布时间:2019-06-25

本文共 9768 字,大约阅读时间需要 32 分钟。

来现在这公司一年了,一年时间里经手做的项目有六七个,不过呢大部分都是一些管理平台的功能,而管理平台做的最多的就是各种表格的展示了,所以在开发过程中,为了提高开发效率,封装一些通用的功能组件是十分有必要的,在这里我就把我在开发过程中所封装的表格组件分享一下,当然肯定是有很多不足的,因为到目前为止我还是有一些想法没有实现的,也希望可以互相交流一下,就当抛砖引玉了,砸到谁也别怨我啊0.0

开发环境

我这边使用的是vue全家桶+ElementUI,毕竟管理平台,没那么高的要求,版本的话随意,毕竟只是说明一种设计方法,通用的

需求分析

管理平台的表格页面一般包含这几部分功能:1.操作按钮(添加,批量删除等);2.表格数据筛选项(常用的有select过滤,时间过滤,搜索过滤等);3.表格主体;4.分页。

1.操作按钮设计

操作按钮的添加我这边想到的有两种方法,第一种:直接使用vue提供的功能,直接在外部定义按钮、样式,以及按钮的操作事件,通过插槽插入组件内部;第二种:组件外部定义一个按钮的对象,通过父子组件通信传递到table组件内部,组件内部对按钮的对象进行解析、渲染,大概格式如下:

[    {        name: 'addBtn',        text: '新增', // 按钮文案        icon: 'el-icon-plus', // 按钮图标        style: 'primary', // 按钮样式(这里取element的按钮样式)        class: 'addBtn', // 自定义按钮class        func: 'toAdd' // 按钮点击事件    }, {        name: 'multiDelBtn',        text: '批量删除',        icon: 'el-icon-delete',        style: 'danger',        class: 'multiDel',        func: 'toMultiDel'    }]复制代码

2.表格数据筛选项

筛选项的设计没想到有什么好的,就内设几个常用的就上面说的那些,然后通过参数判断是否展示,其他如果有定制需求可以外部定义,然后通过插槽插入

3.表格主体

表格主要包括两部分:表头和表格体,分开分析

表格头我这边设计是在组件外部定义配置项传递组件内部,组件内进行解析,格式如下:

[    {        prop: 'name', // 表格数据对应的字段        label: '用户姓名', // 表格头展示的信息        sortable: false,  // 这一列是否支持排序,true|false|'custom',可以不传(为true是前端排序,不过前端排序没什么意义,一半排序的话还是传‘custom’进行服务端排序)        minWidth: '100', // 这一列的最小宽度(用minWidth是因为在表格宽度不够的时候有个限制不会变形,在宽度过大的时候又能够按照各列的比例进行伸展,perfect!)    }, {        prop: 'address',        label: '住址',        minWidth: '170'    }, {        prop: 'age',        label: '年龄',        sortable: 'custom',        minWidth: '80'    }]复制代码

表格体没什么,就参看element-ui的就行

4.分页

分页也没什么好说的,内置在组件内,少于一页不显示,在element组件库选一个自己需要的功能的分页

组件开发

根据需求对组件进行了封装

1.操作按钮设计

{
{item.text}}
复制代码

2.筛选项设计

复制代码

3.table主体设计

复制代码

4.分页设计

复制代码

5.接受传参与methods

props: {    tableConfig: {      type: Object,      default: () => {        return {}      }    },    tableData: {      type: Object,      default: () => {        return {          thead: [],          tbody: [],          isMulti: false, // 是否展示多选          pageInfo: { page: 1, size: 10, total: 0 } // 默认一页十条数据        }      }    }},  methods: {    toEmitFunc (funName, params) {      this.$emit(funName, params)    },    toSearch () {      this.toEmitFunc('setFilter', { search: this.search, page: 1 })    },    pageChange (val) {      this.toEmitFunc('setFilter', { page: val })    },    handleSelection (val) {      let cluster = {        id: [],        status: [],        grantee: [],        rows: []      }      val.forEach(function (element) {        cluster.id.push(element.id)        cluster.status.push(element.status)        cluster.rows.push(element)        if (element.grantee) cluster.grantee.push(element.grantee)      })      this.toEmitFunc('selectionChange', cluster)    },    handleSort (value) {      this.toEmitFunc('setFilter', {        prop: value.prop,        order: value.order      })    },    handleTimerange () {        if (this.dateRange) {            this.eventBus('setFilter', {              startTime: this.dateRange[0],              endTime: this.dateRange[1]            })        } else {            this.eventBus('setFilter', {              startTime: '',              endTime: ''            })        }    },    handleSelect () {      this.toEmitFunc('setFilter', {        filter: this.filter      })    }  }复制代码

看到这,你肯定说这不就是element的table的使用吗?嗯...你说的很有道理,我竟无法反驳0.0,下面我就加入一些自己的想法设计吧(づ ̄3 ̄)づ

先写demo,在写的过程中才能一步步完善不足

复制代码

demo写完就发现,很多功能都么得啊,功能很是单一,发现问题:

  • 时间那一列,后端不一定传递过来就是可以直接展示的数据,如果传过来的是个时间戳呢?这时候就需要前端来做一下格式化处理
  • 状态那一列,单纯的文字并不显眼,很多时候需要一个状态标签或者其他样式来展示
  • 我还遇到很多次交互设计要求点击列表中姓名(不一定是姓名,就是某一项点击能进入详情页面)进入该用户的详情页
  • 如果列表内有操作项呢?操作项该如何配置传递到组件内部?

有问题了,一个一个来0.0

第一个:时间那一项需要格式化,在表格中这一列都需要按照同样的方法进行格式化处理,那么我们可以把配置信息放在表头中,然后再表格组件中解析处理,定义formatFn:

{  prop: 'createdTime',  label: '添加时间',  minWidth: '128',  formatFn: 'timeFormat'}复制代码

组件内添加formatFn判断

复制代码

添加utils类,编写格式化数据方法,并注册全局

// 格式化方法文件(format.js)(记得要在main.js注册啊)export default {  install (Vue, options) {    Vue.prototype.formatFunc = (fnName = 'default', data = '', row = {}) => {      const fnMap = {        default: data => data,        /**         * 时间戳转换时间         * 接受两个参数需要格式化的数据         * 为防止某些格式化的规则需要表格当前行其他数据信息扩展传参row(可不传)         */        timeFormat: (data, row) => {          return unixToTime(data) // unixToTime是我书写的一个时间戳转时间的方法,如果你项目有引用其他类似方法插件,在这里返回格式化后的数据就可以        }      }      return fnMap[fnName](data, row)    }  }}复制代码

这样如果有其他格式化规则的也可以通过自定义格式化方法,然后再表头中定义需要调用的方法就可以了,同时这个格式化方法不只是可以用在表格中,其他任何你想要进行格式化的地方都可以在这个文件中定义,然后直接使用就可以了,而不用再引入方法再使用,是不是很方便(づ ̄3 ̄)づ

看到这里应该就明白这个table组件的核心其实还是这个格式化方法的使用

继续第二个问题,状态那一列,需要的不只是数据的变化,更是需要将相应的状态数据转换成对应的标签,这就需要扩展一下table组件,添加新的判断逻辑

{    prop: 'status',    label: '账号状态',    minWidth: '100',    formatFn: 'formatAccountStatus',    formatType: 'dom'}复制代码
复制代码

我本意是想着format成element-ui的标签,如

但是在写format方法的时候发现直接返回el-tag标签,然后经过v-html解析为html标签但是后面发现并不能按照预期的那样解析成element的标签,思考一番没有发现什么好的方法,没办法只能自己返回原生标签,定义class然后自己书写样式修改为标签的样子

定义状态关系表,添加format方法

const accountStatusMaps = {  status: {    online: '在线',    offline: '离线'  },  type: {    online: 'success',    offline: 'warning'  }}// 用户账号状态转标签formatAccountStatus: (data, row) => {  return `${accountStatusMaps.status[data]}`}复制代码

这样一般的样式format是可以满足了,但是一些比较复杂的需求自己手写样式就比较不方便了,但是我自己一时也没想到好的解决办法,再加上这样比较难搞的需求比较少,所以就一直放着了(可能这就是我技术成长有限的原因吧0.0),各位看官如果有什么好的建议方法的话欢迎来提啊

继续第三项,这个需求在后台管理平台很容易遇到,点击名称进入详情,本来我还是想使用format,format出一个a标签,然后href是想要跳往的地址,但是功能虽然是实现了,但是使用a标签有个很大问题就是页面跳出感太强,只能放弃这个方法,后来没什么好的办法,就想着在表头上做文章,新定义一种formatType => 'link',可选参数linkUrl定义跳转链接,修改table组件的template

{
{scope.row[item.prop]}}
复制代码
toLink (url, row) {  if (url) {    this.$router.push(`${url}/${row.id}`)  } else {    this.$router.push(this.$route.path + '/detail/' + row.id)  }}复制代码
.to-detail-link {  color: #1c92ff;  cursor: pointer;  &:hover {    color: #66b1ff;  }}复制代码

需求满足了,但是只是个妥协之计,该怎么在format返回的标签字符串上面绑定方法呢,如有想法,不胜感激,解决了这个问题能让这组件功能提高一大步,因为这个详情功能还算通用,可以在组件兼容,但是如果是其他点击方法呢?总不能一点点的都兼容,这样就失去了封装组件的意义,因为兼容是兼容不完的。

继续继续,操作项在后台管理平台是不可缺少的,主要的问题就在于怎么传递、抛出点击方法,我这边是这么实现的

先修改template

    {
{item.text}}
复制代码

设置操作项数据信息

computed: {    operateConfig () {      return {        optType: {          toEdit: {            event: 'toEdit', // 操作按钮调用的方法            text: '编辑', // 操作按钮展示的文案            type: 'primary' // 操作按钮展示的样式          },          toDel: {            event: 'toDel',            text: '删除',            type: 'danger'          }        },        optFunc: function (row) {        // 在线状态用户不能删除          if (row.status === 'offline') {            return ['toEdit', 'toDel']          } else {            return ['toEdit']          }        }      }    }}复制代码

把一些通用的属性、方法抽离出来放在tableMixins里面,减少每次调用的书写

// tableMixins.js// 表格方法的mixinsexport default {  data() {    return {      // 表格数据,具体参考接口数据      tableData: {        thead: [],        tbody: [],        isMulti: false,        pageInfo: { page: 1, size: 10, total: 0 }      },      // 表格是否处于loading状态      loading: true,      // 多选,已选中数据      selection: [],      // 查询条件,包括排序、搜索以及筛选      searchCondition: {}    }  },  mounted: function () { },  methods: {    // 多选事件, 返回选中的行及每行的当前状态    selectionChange(value) {      this.selection = value    },    接口请求到数据后将数据传入这个方法进行thead、tbody、pageInfo等信息的赋值    afterListSet(res) {      let formData = this.setOperation(res)      if (formData.thead) {        this.tableData.thead = JSON.parse(JSON.stringify(formData.thead))      }      this.tableData.tbody = formData.tbody      if (formData.pageInfo) {        this.tableData.pageInfo = JSON.parse(JSON.stringify(formData.pageInfo))      }      formData.isMulti && (this.tableData.isMulti = formData.isMulti)      let query = JSON.parse(JSON.stringify(this.$route.query))      this.$router.replace({        query: Object.assign(query, { page: this.tableData.pageInfo.page })      })      this.loading = false    },    // 遍历原始数据,塞入前端定义好的操作项方法序列,设置操作项    setOperation(res) {      let that = this      let tdata = JSON.parse(JSON.stringify(res))      if (that.operateConfig && that.operateConfig.optFunc) {        for (let i in tdata.tbody) {          let temp = that.operateConfig.optFunc(tdata.tbody[i])          let operation = []          for (let j in temp) {            operation.push(that.operateConfig.optType[temp[j]])          }          that.$set(tdata.tbody[i], 'operation', operation)        }      }      return tdata    }  }}复制代码

这样一套组合拳下来,管理平台常用的表格功能基本都实现了,贴一下成果图(样式没怎么写,这个就根据自己的项目风格进行调整吧)

我把这个组件的源码上传到了我的GitHub上了,同时写的有demo,有兴趣的朋友可以clone到本地琢磨琢磨,同时欢迎提出代码的不足或者bug,不胜感激0.0,贴上

思考

组件写完了,文章也水完了,反思一些不足,一个是怎么渲染出UI组件的标签,另一个是怎么在format出来的标签上面绑定方法,诚然组件开发过程中有很多妥协,这些妥协就是自身水平或者视野没能达到导致的恶果,把组件抛出来就是为了集思广益,这个文章对你有帮助,能帮助你提高那我会很高兴,如果你帮忙解决了我遇到的困难,帮助到我的提高那我会更加高兴,我为的不就是这个嘛,所以欢迎大家帮忙想解决方案

当然组件开发过程中我还是做了一些很人性化的设计,一个是提供了多冲筛选条件同时作用的方法,就是维护一个searchCondition,每次筛选起作用就添加到searchCondition中,然后把searchCondition传递给数据查询方法,这个方法可以在demo中尝试,控制台会有输出,另一个小功能就不得不吐槽一下很多开源组件的一个反人类设计,富文本编辑器很多人都用过吧,会有一个menu的配置项,如果这个配置项你什么也不穿他就默认展示全部的菜单项,但是如果你不想要其中一个菜单项,你在menu的配置中把这一项置为false.......然后所有的菜单全没了,我看过一些源码是直接使用传递进去的menu覆盖默认的menu配置项,那么我只是想取消其中一项,我就得把全部的菜单项全部配置一遍且全部置为true,这...不坑爹呢嘛,所以我在向组件内部传参的时候添加了一个方法,而是使用解构赋值,维护一个computed,代码如下:

computed: {    propConfig () {      // defaultConfig是默认配置项,tableConfig是父组件传递进来的配置项      return { ...this.defaultConfig, ...this.tableConfig }    }}复制代码

这样默认配置项只会在父组件某一项有修改的时候进行变更,其他不变

就这些吧,希望会对大家有所帮助

转载于:https://juejin.im/post/5ce1507551882525d05f7bed

你可能感兴趣的文章
MVC2.0中的HtmlHelper大全
查看>>
《面向模式的软件体系结构3-资源管理模式》读书笔记(1)--- Lookup模式
查看>>
You must configure either the server or JDBC driver (via the serverTimezone configuration property
查看>>
扩展方法判断序列(或集合)是否包含元素
查看>>
Sql Server Profiler跟踪死锁
查看>>
使用反射操作私有(Private)方法和属性
查看>>
第二阶段团队冲刺站立会议10
查看>>
php 的rabbitmq 扩展模块amqp安装
查看>>
APK签名校验绕过
查看>>
[LeetCode] 4Sum
查看>>
让最新官方编译的 ffmpeg 在 XP 上 跑起来
查看>>
庆祝博客开通
查看>>
地址栏中传递中文参数
查看>>
设计模式之结构型模式
查看>>
冒泡,快排
查看>>
git: fatal: Could not read from remote repository
查看>>
PHP移动互联网开发笔记(7)——MySQL数据库基础回顾[1]
查看>>
2.文件内容的命令
查看>>
XNA 中 SoundEffect 与 SoundEffectInstance 的区别
查看>>
day 036 线程 -创建,守护线程
查看>>