来现在这公司一年了,一年时间里经手做的项目有六七个,不过呢大部分都是一些管理平台的功能,而管理平台做的最多的就是各种表格的展示了,所以在开发过程中,为了提高开发效率,封装一些通用的功能组件是十分有必要的,在这里我就把我在开发过程中所封装的表格组件分享一下,当然肯定是有很多不足的,因为到目前为止我还是有一些想法没有实现的,也希望可以互相交流一下,就当抛砖引玉了,砸到谁也别怨我啊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主体设计
复制代码 { {scope.row[item.prop]}}
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判断
{ {scope.row[item.prop]}} { {formatFunc(item.formatFn, scope.row[item.prop], scope.row)}} 复制代码
添加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'}复制代码
{ {scope.row[item.prop]}} { {formatFunc(item.formatFn, scope.row[item.prop], scope.row)}} 复制代码
我本意是想着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 } }}复制代码
这样默认配置项只会在父组件某一项有修改的时候进行变更,其他不变
就这些吧,希望会对大家有所帮助