小知识,大挑战!本文正在参与“程序员必备小知识”创作活动
本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。
前言
在我们做后台管理系统的时候,其实我们接触最多的组件就是表格组件
了,所以表格组件的好用与否,直接关系到我们做后台管理系统的效率
了. 那么今天我们讲一下,如何对element-ui
的表格组件做一层封装,集成更多的功能进去,让我们可以写更少的代码
,实现更多并且更稳定的功能
。 废话不多说,下面我们就开始吧.
功能
首先我们先整理一下我们需要实现哪一些功能.
- 分页功能 结合
Pagination
组件来实现表格的分页 - 表格添加插槽,可以自定义样式、数据到表格里边
- 实现多表头
- 实现跨页勾选数据
- 查询功能 使用
[输入框、多选框、单选框、日期框]
等组件实现查询交互 - 序号累加排序
- 更好操作的升降序功能
- 实现查询条件展示, 可能表格查询可以根据很多的条件查询,有了查询条件展示,可以一目了然,看到列表是根据哪一些条件查询的结果.
- 动态表头实现, 有可能表格有很多的表头,用户一眼很难找到想看的某一些表头. 所以可以需要一个可以实现表头过滤的功能.
基础实现
在实现我们上述讲的这些feture的时候,我们肯定得先把element-ui本身自有的功能给加上。 那么我们就先把这些基础功能给放到我们自己的组件上.
<template>
<el-table v-bind="$attrs" v-on="$listeners">
<template v-for="(item) in columns">
<el-table-column
:key="item.prop"
v-bind="item"
show-overflow-tooltip>
</el-table-column>
</template>
</el-table>
</template>
<script>
export default {
name: 'miniTable',
props: {
columns: {
type: Array,
default: () => [],
},
}
}
</script>
<mini-table border :columns="columns"></mini-table>
columns: [
{ label: '姓名', prop: 'name', align: 'center' },
{ label: '年龄', prop: 'age', align: 'center' },
{ label: '爱好', prop: 'hobby', align: 'center' },
{ label: '学历', prop: 'education', align: 'center' },
{ label: '籍贯', prop: 'nativePlace', align: 'center' },
{ label: '备注', prop: 'remark', align: 'left', width: 200 }
],
复制代码
以上就实现了我们在element-ui里边的基础功能了. 以上主要依靠
vue
提供的$attr
和$listeners
来实现的功能.
listeners"
传入内部组件——在创建更高层次的组件时非常有用 $attrs 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (
class和
style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (
class和
style除外),并且可以通过
v-bind="$attrs"` 传入内部组件——在创建高级别的组件时非常有用。
以上是vue
官方文档对这两个属性的介绍. 大致意思就是可以依靠这两个属性,对已有的组件做一层更高级的封装。 让用户可以在用我们的组件的时候,透传属性或者方法到element-ui
的组件去. 当然如果有些功能,我们需要在element-ui
的事件里边再做一些处理的话,我们可以再对代码做一下改造。这个后边我们会说到。 然后还有一点,由于需要把属性透传给element-ui
的组件,为了避免后边会造成一些不必要的冲突,所以后边在我们的组件里边用到的属性,我们会在命名前加上__
,来表示是我们自己的组件私有的属性.
集成分页
先上代码
<el-pagination
class="pagination"
background
v-if="hasPagination"
@size-change="sizeChange"
@current-change="currentChange"
:total="pagination.totalRow"
:current-page="pagination.pageIndex"
:page-size="pagination.pageSize"
:page-sizes="pageSizes"
:layout="layout">
</el-pagination>
// methods
/**
* 切换分页数量
* @param { Number } pageSize 页数
*/
sizeChange (pageSize) {
this.pagination.pageIndex = 1
this.pagination.pageSize = pageSize
this.queryData()
},
/**
* 切换页码
* @param { Number } pageIndex 页码
*/
currentChange (pageIndex) {
this.pagination.pageIndex = pageIndex
this.queryData(true)
},
复制代码
分页的话,没有什么特别好说的,分页逻辑基本上是和表格耦合在一起的功能。 所以pagination
对象直接在组件的data
里边定义好,pageSizes
和layout
接受外部传入。 如果不传递的话,给一个默认的值. 也有情况是不需要页码的。 所以外部还可以传入一个hasPagination
,不需要页码的话,传入一个false
. 默认true
表格自定义内容
表格里边不一定是纯文本的数据。 有可能需要渲染一个按钮
,或者一个图片
也可能是一个百分比进度条
. 然后到底是什么,在我们这个组件里边是不得而知的。 所以我们需要在组件里,给用户提供一个插槽,让用户可以自定义内容。 保证组件的灵活性. 那么需要怎么做呢? 我们往下看代码实现吧:
// 添加slot
<el-table-column
:key="item.prop"
v-bind="item"
show-overflow-tooltip>
<template v-if="item.__slotName" v-slot="scope">
<slot :name="item.__slotName" :data="scope"></slot>
</template>
</el-table-column>
// data
columns: [{ label: '头像', prop: 'avatar', align: 'center', __slotName: 'avatar' }]
// 使用miniTable组件
<mini-table size="small" border :columns="columns">
<template slot="avatar" slot-scope="scope">
<img slot="avatar" width="40" :src="scope.data.row.avatar" />
</template>
</mini-table>
复制代码
实现其实也不难,我们添加一个__slotName
属性传递给组件,然后组件里边添加判断,如果传递了__slotName
的话,则使用插槽,并传递scope
给插槽,插槽的名字就使用传递过来的__slotName
. 这样的话,我们在使用组件的时候,就可以定义一个slot="avatar"
的插槽,并且拿到组件给的行数据。
然后,我们就可以写入一条有头像数据的行数据到表格了.
多表头
实际项目中,我们还会遇到多表头的问题. 可能是二级表头,可能有三级表头,可能更多。 为了满足所有情况,我们实现的方式采用组件递归的方式,实现多表头.
<template>
<el-table-column
v-bind="item"
:key="item.prop"
show-overflow-tooltip>
<template v-for="obj in item.__children">
<my-table-column v-if="obj.__children" :item="obj" v-bind="obj" :key="obj.prop"></my-table-column>
<el-table-column
v-else
:key="obj.prop"
v-bind="obj"
show-overflow-tooltip>
<template v-if="obj.__slotName" v-slot="scope">
<slot :name="obj.__slotName" :data="scope"></slot>
</template>
</el-table-column>
</template>
</el-table-column>
</template>
<script>
import MyTableColumn from './tableColumn'
export default {
name: 'MyTableColumn',
components: {
MyTableColumn
},
props: {
item: {
type: Object,
default: () => {}
}
}
}
</script>
复制代码
上面创建了一个tableColumn
文件,如果表头存在__children
则递归组件,如果不存在,则是一个二级表头
. 我们在使用的时候,添加一个这样的数据测试
{ label: '地址信息',
prop: 'address',
align: 'center',
__children: [
{
label: '省份',
prop: 'province',
align: 'center'
},
{
label: '城市',
prop: 'city',
align: 'center',
__children: [
{
label: '区',
prop: 'area',
align: 'center',
},
{
label: '县',
prop: 'county',
align: 'center',
}
]
}
]
}
复制代码
最后我们根据我们的
json
数据描述,得到我们想要的样子.
跨页勾选数据
跨页勾选,其实也是我们比较常见的一个需求了。 用户想要勾选的数据,在第一页的页尾和第二页的页头. 一般情况下,如果勾选了第一页的数据,再点击第二页,由于数据刷新了。 勾选也就没有了. 需要做跨页勾选的话,其实也很简单,element-ui其实提供了这个功能. 主要用到了两个属性.
reserve-selection: 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定
row-key
)
row-key: 行数据的 Key,用来优化 Table 的渲染;在使用 reserve-selection 功能的情况下,该属性是必填的。类型为 String 时,支持多层访问:
user.info.id
,但不支持user.info[0].id
,此种情况请使用Function
// 对组件外部暴露一个 isCheckMemory 属性,默认false, 需要跨页,设置成true
props: {
/**
* 是否需要跨页勾选
*/
isCheckMemory: {
type: Boolean,
default: false
},
/**
* 表格行数据的唯一键
*/
idKey: {
type: String,
default: 'id'
}
}
// 给table设置row-key,给type="selection"的tableColumn设置reserve-selection
<template>
<el-table
ref="__table"
:data="tableData"
v-bind="$attrs"
:row-key="idKey"
v-on="listeners">
<el-table-column
v-if="isCheck"
align="center"
width="70"
type="selection"
:reserve-selection="isCheckMemory"
>
</el-table-column>
...
</el-table>
</template>
复制代码
以上就把跨页勾选给做好了。 然后我们再和之前一样. 去selection-change
就能拿到勾选的所有数据了. 这里值得注意的一点是,v-on="listeners"
,如果组件内部需要对勾选的数据做一些操作的话,我们可以这样写:
computed: {
listeners: function () {
var vm = this
return Object.assign(
{},
this.$listeners, {
'selection-change': function (val) {
// dosomething
vm.selectData = val
vm.$emit('selection-change', val)
}
}
)
}
},
复制代码
列排序
列排序的话,其实也算是比较常用的功能,因为表格一般都是分页的数据,前端拿不到所有的数据,所以排序大多数情况都是后台进行排序,前端负责给后台一些字段,告诉后台按照什么字段排序,是降序还是升序。
//props 外部传递一个sortArr,告诉组件哪一些字段需要排序
/**
* 排序
*/
sortArr: {
type: Array,
default: () => { return [] }
}
// el-table-column中加入sortable
<el-table-column
v-else
:key="item.prop"
v-bind="item"
:sortable="sortFun(item.prop) ? 'custom' : false "
show-overflow-tooltip>
<template v-if="item.__slotName" v-slot="scope">
<slot :name="item.__slotName" :data="scope"></slot>
</template>
</el-table-column>
// methods
sortFun (prop) {
if (this.sortArr && this.sortArr.length > 0) {
return this.sortArr.indexOf(prop) > -1
} else {
return false
}
}
// 监听事件 sort-change
listeners: function () {
var vm = this
return Object.assign(
{},
this.$listeners, {
'sort-change': function (column) {
vm.order.sortName = column.prop
switch (column.order) {
case 'ascending':
vm.order.sortOrder = 'asc'
break
case 'descending':
vm.order.sortOrder = 'desc'
break
default:
vm.order.sortOrder = ''
}
vm.$emit('sortChange', vm.order)
}
}
)
}
// 用法
// 加入 sortArr 去设置哪一些需要排序
<mini-table size="small" border :sortArr="['age', 'area', 'county']" :columns="columns"></mini-table>
复制代码
然后我们就能知道需要根据什么字段去做排序了. 直接把sortName,sortOrder传递给后台重新进行一次查询就ok了.
序号累加
在列表里边我们的需求上是需要对序号进行累加的。 比如第一页显示1-20的序号,如果不做处理的话,且换到第二页,显示的序号还是1-20,我们想要它展示的序号是21-40的话,就需要稍微做一点处理了。
// template 加入 :index="typeIndex"
<el-table-column
v-if="isIndex"
show-overflow-tooltip
align="center"
:index="typeIndex"
type="index"
:fixed="fixed">
<template slot="header">
<span>序号</span>
</template>
</el-table-column>
// methods
typeIndex (index) {
const tabIndex = index + (this.pagination.pageIndex - 1) * this.pagination.pageSize + 1
return tabIndex
}
复制代码
这里我们根据页码去做一下处理就可以达到我们想要的预期了.
查询数据
由于有可能表格是没有查询条件的,单就一个表格,所以我们把查询数据的功能放在表格组件里边去做。 之后还会独立做一个查询的组件,查询组件把用户输入的查询条件收集起来,然后传递到表格组件里边来. 查询的话,首先引入axios
,我们这边由于没有线上的api做支撑,所以我这边用了mockjs
. 首先我们先看一下axios
import axios from 'axios'
const service = axios.create({
baseURL: '/',
timeout: 10000
})
export const get = function (url, params) {
return service.get(url, { params })
}
export const post = function (url, data) {
return service.post(url, { data })
}
复制代码
这里根据自己公司后台的情况修改,添加拦截器。 我这边就简单的导出一下get
和post
方法. mockjs
的话, 大家可以去网上找一下资料看看, 我这边就直接贴一下代码,简单的讲一下
import Mock from 'mockjs'
const data = Mock.mock({
"list|60-400": [
{
"id": '@increment(1)', // 生成累加的id
"name": "@cname()", // 生成名称
"age|1-50": 1, // 生成1-50的数字
"avatar": "@image('40x40', '#50B347', '#FFF', 'Mock.js')", // 生成 40*40的头像
"hobby": "@ctitle(6)", // 生成6字废文
"education": "@ctitle(6)", // 生成6字废文
"nativePlace": "@ctitle(6)", // 生成6字废文
"province": "@ctitle(6)", // 生成6字废文
"area": "@ctitle(6)", // 生成6字废文
"county": "@ctitle(6)", // 生成6字废文
"remark": "@csentence(20)", // 生成20字废文
}
]
})
// 拦截axios发出的getList接口, 返回上边定义的data数据
Mock.mock(/\/getList/, 'get', (options) => {
// 获取传递的参数pageindex
const pagenum = getQuery(options.url,'pageOffset')
// 获取传递的参数pagesize
const pagesize = getQuery(options.url,'pageSize')
// 截取数据的起始位置
const start = (pagenum-1)*pagesize
// 截取数据的终点位置
const end = pagenum*pagesize
// 计算总页数
const totalPage = Math.ceil(data.list.length/pagesize)
// 数据的起始位置:(pageindex-1)*pagesize 数据的结束位置:pageindex*pagesize
const list = pagenum>totalPage?[]:data.list.slice(start,end)
return {
status: 200,
success: true,
message: '获取新闻列表成功',
list: list,
total: data.list.length
}
})
// 拿到query接口?和&符号后边的参数
const getQuery = (url,name)=>{
const index = url.indexOf('?')
if(index !== -1) {
const queryStrArr = url.substr(index+1).split('&')
for(var i=0;i<queryStrArr.length;i++) {
const itemArr = queryStrArr[i].split('=')
if(itemArr[0] === name) {
return itemArr[1]
}
}
}
return null
}
export default Mock
复制代码
mockJS的拦截和定义数据我在上边代码的注释简单说明了一下。 那么查询方法的话,如下
async queryData (isReset) {
if (this.query && this.query.url) {
this.loading = true
this.pagination.pageIndex = isReset ? this.pagination.pageIndex : 1
let param = Object.assign(
{},
this.query.queryParam, {
[this.pageParam.pageOffset]: this.pagination.pageIndex,
[this.pageParam.pageSize]: this.pagination.pageSize
})
if (this.order.sortName && this.order.sortOrder) {
param.sortName = this.order.sortName
param.sortOrder = this.order.sortOrder
}
let result = null
try {
switch(this.query.method) {
case 'get':
result = await get(this.query.url, param)
break
case 'post':
result = await post(this.query.url, param)
break
default:
result = await get(this.query.url, param)
break
}
} catch(e) {
console.warn(e)
} finally {
this.loading = false
const { data } = result
if (data && data.success) {
this.pagination.totalRow = data.total
this.tableData = data.list
this.$emit('fetchData', data)
} else {
this.$message({
type: 'warning',
message: data ? data.message : '查询失败!'
})
this.tableData = []
this.pagination.pageIndex = 1
this.pagination.totalRow = 0
}
}
}
}
复制代码
其中有几处说明一下, 请求接口,为了良好的体验。添加loading
,在请求结束后loading
设置成false. 还有一点pageIndex
和pageSize
,大家的后台都不一样,所以大家的页码的字段可能也叫的不一样。 我们可以在props
里边传入自己后台的页码的字段名称. 然后这边就会对应修改成你要的字段.
/**
* 分页需要传入后台的字段
*/
pageParam: {
type: Object,
default: () => {
return {
pageOffset: 'pageOffset',
pageSize: 'pageSize'
}
}
},
复制代码
然后method支持post
和get
两种请求方式.只需要在用组件的时候,传入即可
<mini-table :isAutoQuery="true" :query="query" size="small" border :sortArr="['age', 'area', 'county']" :columns="columns">...</mini-table>
// data
query: {
url: '/getList',
method: 'get',
queryParam: {}
},
复制代码
ok, 那么我们看一下最后实现的效果:
写在最后
ok, 那么常用的一个表格需要有的一些功能,我们基本上都把它给集成到我们自己写的组件里边了。 使用如果出现一些小问题,我们也只需要在组件内部做一些修改,在项目中使用一段时间后,组件会越来越稳定,并且在稳定的同时,也兼具灵活性。项目做到后边,越能体现我们做这么一个组件的价值。 后边我们会在这个基础上,再加上表单搜索功能和动态表头功能. 大家如果觉得文章最自己有一些用的话,不妨点赞关注一波。另外后边在完整我提到的所有的功能后。我会把代码提交到gitee上给有需要的人克隆下来查看. 那么下次见咯!