之前使用vue3,和别人一起写了个小项目,然后参考了社区里别人的一些优秀的组件风格, 包括vue3的 ProTable, 功能强大, 且使用了很多的 hooks, 特别是一些表格的 比如 列设置的功能、全屏功能、表格里面插槽的功能、搜索表单的 方法、插槽、验证等都做得很全。 但最近要使用 vue2的项目,也同样需要用到这里面的一些功能, 在论坛和 git 上找了半天也没找到有人封装的全一点。最后只能自己实现一下。
使用技术 vue2 + element-ui + vuex
功能
首先介绍一下我需要的是一个什么样的功能的。
- 搜索部分的封装: 包含展开收起功能、查询重置功能、搜索部分正常表单组件的封装(input、inputNumber、select、date等等),搜索参数的二次处理
- 表格功能区封装: 包含左侧按钮部分(多选操作,导入导出等),右侧功能部分(列设置,全屏等)
- 表格内容 支持插槽、操作部分的编辑删除回调函数
- api集成,表格查询接口集成到组件里
- 分页功能
看一下效果
上面这个状态, 这个小红点其实如果使用 antd 组件库的话,是有徽标组件的,但是 el 是没有徽标组件的,这里我也是自己封装了一下。
结构
- 架子这里我是直接找了一个,复用了里面的工程结构及目录结构。

我这个组件封装在了 ProTable 里面, 下面会拆解代码进行介绍,最后会放一下这个demo的 git 大家可以自行查看, 我这里只是基于我现有的业务需求进行了一些封装,大家有新的想法或者意见可以留言, 俺一定会回复的
table
上面是封装的
table
组件的代码,我没有把 table
和 form
单独再拆分出去, 之前使用vue3的时候, 是拆分了的, 这里满足自己业务功能就没有再拆分了。 这里有几点注意事项
- 列设置这里是可以有一个拖拽改变顺序的功能, 在注释的那段
el-tree
中,api
是:allow-drop="allowDrop"
table
里面的column
后面有更改
js
<el-table
ref="table"
:key="tableKey"
v-loading="tableLoading"
:data="tableData"
:row-style="{ height: '40px' }"
:cell-style="{ }"
:header-cell-style="{ height: '40px', padding: 0, background: '#f6f8fa', color: '#333' }"
size="mini"
tooltip-effect="dark"
v-bind="tableProps"
@row-click="handelTableClick"
@selection-change="handleSelectionChange"
@header-dragend="surverWidth"
>
<template
v-for="item in columnsHanlder(columns)"
>
<template v-if="item.show && item.prop === 'selection'">
<el-table-column :key="item.prop" align="center" :resizable="false" type="selection" />
</template>
<template v-else-if="item.show && item.prop === 'actions'">
<!-- 固定列 -->
<el-table-column :key="item.prop" fixed="right" align="center" :label="item.label ? 'item.label' : '操作'" :width="item.width" :resizable="false">
<div slot-scope="scope" class="col-slot">
<slot name="actions" :scope="{...scope}" />
</div>
</el-table-column>
</template>
<template v-else>
<el-table-column
v-if="item.show"
:key="item.prop"
align="center"
show-overflow-tooltip
:prop="item.prop"
:sortable="item.sortable"
:label="item.label"
:width="item.width"
:resizable="false"
>
<template slot-scope="scope">
<span v-if="item.slot">
<slot :name="item.slot" :scope="scope" />
</span>
<span v-else-if="item.type === 'time' && scope.row[item.prop]">{{ dayjs(scope.row[item.prop]).format('YYYY年MM月DD日') }}</span>
<span v-else>{{ scope.row[item.prop] || '-' }}</span>
</template>
</el-table-column>
</template>
</template>
</el-table>
这里使用了 columnsHanlder
方法,来对传入的 columns
进行一次过滤, 给默认参数进行赋值, 同时给 多选 和 操作部分进行了分离, 操作部分需要传入插槽, 然后会把对应这一行的 scope
作为插槽的 slot-scope
传回给父组件。 这里我们拿到的就是上面这个东西啦, 包含这一行的
index, column, row
等信息, 其中 row 就是我们这一行的数据, 注意使用的时候不要修改原数据哦, 编辑成功以后的正常逻辑是刷新~
- 在下面的
el-table-column
中,我们首先判定是否有插槽,如果有插槽则直接渲染插槽, 如果没有插槽这里就可以做一些通用的处理,比如时间格式的转换,没有数据时候的默认显示等等。
这里需要注意一点的就是 在我们使用列设置的时候, 因为 el 的固定列的特性, 当列发生改变的时候, 固定列的高度会掉下去一小部分
js
// 解决 自定义列 + 固定列 导致显示问题 参考 https://blog.csdn.net/qq_36126031/article/details/121970398
updated() {
this.$refs.table.doLayout()
},
SearchFormItem
这里我们在
ProTable
组件中就是这样喽, 这里还是一个正常的 form
表单来处理, 这里我没有封装验证规则, 大家有需求的话可以提一下子,因为搜索的验证我觉得没啥意义,输入错了搜不出来数据就完事了,又不是提交表单~, 这里每一个 item
使用 SearchFormItem
进行了封装, 展开收起使用了的就是 一个 防抖函数来触发动态侦听 form
高度的改变
js
// 页面初始化时候调用,以及窗口size调整 以及侧边菜单变化时调用
toShowMore: debounce(function() {
this.$nextTick(() => {
if (!this.$refs.FormRef) return
const formHeight = window.getComputedStyle(this.$refs.FormRef.$el)
.getPropertyValue('height')
.replace('px', '')
if (formHeight < 50) {
this.showMoreButton = false
} else {
this.showMoreButton = true
}
})
}, 300),
以下是封装的每一个 form
,使用动态组件的方式进行, 针对日期时间组件根据自己的业务进行了通用配置
js
<template>
<component
:is="`el-${column.search.el}`"
v-if="column.search && column.search.el"
v-model="searchParam[column.search.key || handleProp(column.prop)]"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
:data="column.search.el === 'tree-select' ? columnEnum : []"
:options="['cascader', 'select-v2'].includes(column.search.el) ? columnEnum : []"
:clearable="clearable"
v-bind="handleSearchProps"
>
<template v-if="column.search.el === 'cascader'" v-slot="data">
<span>{{ data[fieldNames.label] }}</span>
</template>
<template v-if="column.search.el === 'select'">
<component
:is="`el-option`"
v-for="(col, index) in columnEnum"
:key="index"
:label="col[fieldNames.label]"
:value="col[fieldNames.value]"
/>
</template>
<slot v-else />
</component>
</template>
<script>
export default {
props: {
column: Object,
searchParam: Object
},
computed: {
fieldNames() {
return {
label: this.column.fieldNames && this.column.fieldNames.label ? this.column.fieldNames.label : 'label',
value: this.column.fieldNames && this.column.fieldNames.value ? this.column.fieldNames.value : 'value'
}
},
columnEnum() {
return this.column.enum || []
},
handleSearchProps() {
const label = this.fieldNames.label
const value = this.fieldNames.value
const searchEl = this.column.search && this.column.search.el
const searchProps = this.column.search && this.column.search.props ? this.column.search.props : {}
let handleProps = searchProps
if (searchEl === 'tree-select') {
handleProps = {
...searchProps,
props: { label, ...searchProps.props },
nodeKey: value
}
}
if (searchEl === 'cascader') {
handleProps = {
...searchProps,
props: { label, value, ...searchProps.props }
}
}
return handleProps
},
placeholder() {
const search = this.column.search
return search && search.props && search.props.placeholder
? search.props.placeholder
: search && search.el === 'input'
? '请输入'
: '请选择'
},
clearable() {
const search = this.column.search
return (
search && search.props && search.props.clearable
? search.props.clearable
: search && (search.defaultValue == null || search.defaultValue === undefined)
)
}
},
methods: {
handleProp(prop) {
const propArr = prop.split('.')
if (propArr.length === 1) return prop
return propArr[propArr.length - 1]
}
}
}
</script>
Pagination
- 针对分页,我们可以使用了固定的一些配置, 同时也可以自己插入一个分页的插槽,插槽中
scope
返回的是分页信息, 父组件来改变这个分页信息来控制, 同时可以使用 子组件实例ref
上的getTableList
方法进行表格数据更新。
js
<slot :scope="pageable" name="pagination">
<Pagination
v-if="pagination"
:pageable="pageable"
:handle-size-change="handleSizeChange"
:handle-current-change="handleCurrentChange"
/>
</slot>
上面是 ProTable 中使用, 下面是封装的
js
<template>
<!-- 分页组件 -->
<el-pagination
class="pagination"
:current-page="pageable.pageNum"
:page-sizes="[10, 25, 50, 100]"
:page-size.sync="pageable.pageSize"
:total="pageable.total"
layout="total, sizes, prev, pager, next, jumper"
:hide-on-single-page="true"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</template>
<!-- eslint-disable vue/require-default-prop -->
<script>
export default {
props: {
pageable: Object,
handleSizeChange: Function,
handleCurrentChange: Function
}
}
</script>
<style lang="scss" scoped>
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
</style>
地址
- 本项目地址 gitee.com/li-haibo-19...