组件封装的缘由
开发后台管理系统,在业务上接触的最多就是表单(输入)和表格(输出)了。对于使用 Vue 框架进行开发的同学来说,组件库 Element 是肯定会接触的,而其中的 el-table 和 el-form 更是管理系统中的常客。
然而,一旦项目的表格或表单多起来,每个不同的配置,以及多一个字段少一个字段,都要在 template 中重新写一大段组件代码,显得非常麻烦。或许你会考虑将这些代码封装起来,可是又会发现,封装的表格、表单大多数只在一处地方使用,还不如不封装呢。到底要如何封装,可以让每处使用 el-table 或 el-form, 都可以复用相同的组件,减少代码量的同时又具备高度的可定制性?
本文章将会按照从无到有的步骤,按照封装组件常用的思路来封装 el-table,并且实现封装完成的组件支持 el-table 的全配置
最近安排一个模块主要是数据查询展示的列表模块,为了减少开发代码以及提供工作效率(程序猿的偷懒),就想着把相关的业务封装成一个组件,主要功能有
- 数据查询条件的筛选以及筛选条件过多默认展示几个,可通过更过和收起来控制显示的筛选条件的操作
- 表格的多选,自定义列,操作列的按钮自定等操作
- 集成了数据分页的公共
话不多说,直接看图撸代码
效果图
组件代码
<template>
<div>
<div class="formWrap">
<el-form ref="form" :rules="formRules" :model="form" label-width="100px">
<el-row :gutter="10">
<template v-for="(item, index) in headerData">
<template v-if="item.show">
<el-col :span="item.span || 6" :key="index">
<template v-if="item.type !== 'slot'">
<!-- 当类型为普通文本输入框时 -->
<el-form-item
v-if="item.itemType == 'text'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-input
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
clearable
size="small"
></el-input>
</el-form-item>
<!-- 当类型为数字类型输入框时 -->
<el-form-item
v-if="item.itemType == 'number'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-input
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
clearable
size="small"
>
<span slot="suffix">{{ item.unit }}</span>
</el-input>
</el-form-item>
<!-- 当类型为文本域输入框时 -->
<el-form-item
v-if="item.itemType == 'textarea'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-input
type="textarea"
:placeholder="item.placeholder"
v-model.trim="form[item.propName]"
clearable
size="small"
></el-input>
</el-form-item>
<!-- 当类型为下拉框一时,固定下拉选项 -->
<el-form-item
v-if="item.itemType == 'selectOne'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
size="small"
>
<el-option
v-for="(ite, ind) in item.optionsArr"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为下拉框二时,属于枚举值(单选)下拉框,需要根据枚举id发请求获取枚举值 -->
<el-form-item
v-if="item.itemType == 'selectTwo'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
@visible-change="
(flag) => {
getOptionsArr(flag, item)
}
"
:loading="loadingSelect"
size="small"
>
<el-option
v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为下拉框三时,属于枚举值(多选)下拉框,需要根据枚举id发请求获取枚举值 -->
<el-form-item
v-if="item.itemType == 'selectThree'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-select
v-model="form[item.propName]"
:placeholder="item.placeholder"
clearable
@visible-change="
(flag) => {
getOptionsArr(flag, item)
}
"
:loading="loadingSelect"
multiple
collapse-tags
size="small"
>
<el-option
v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
:key="ind"
:label="ite.label"
:value="ite.value"
></el-option>
</el-select>
</el-form-item>
<!-- 当类型为日期范围 -->
<el-form-item
v-if="item.itemType == 'dateRange'"
:key="index"
:label="item.labelName"
:prop="item.propName"
>
<el-date-picker
v-model="form[item.propName]"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
>
</el-date-picker>
</el-form-item>
</template>
<template v-else>
<el-form-item label-width="30px">
<slot></slot>
</el-form-item>
</template>
</el-col>
</template>
</template>
</el-row>
</el-form>
<!-- 提交表单和重置表单部分 -->
</div>
<el-table
:data="tableData"
border
style="width: 100%"
:element-loading-text="loadingText"
:emptyText="emptyText"
@selection-change="handleSelectionChange"
:header-cell-style="{
background: '#f1f1f1',
color: '#606266',
fontWeight: 'normal',
}"
highlight-current-row
>
<el-table-column
v-if="isShowCheckbox"
type="selection"
width="50"
align="center"
>
</el-table-column>
<el-table-column
v-for="(item, index) in tableProps"
:key="index"
:label="item.label"
:width="item.width"
:show-overflow-tooltip="item.tooltip"
:align="item.align || 'center'"
>
<template slot-scope="scope">
<span v-if="!item.slot">{{ scope.row[item.prop] }}</span>
<div
v-else
:style="setCorol(item.color)"
class="slot-box"
@click="clickSlot(item.prop, scope.row)"
>
{{ scope.row[item.prop] }}
</div>
</template>
</el-table-column>
<!-- 操作栏 -->
<el-table-column
v-if="operation.length"
:fixed="operationFixed"
align="center"
label="操作"
:width="operationWidth"
>
<template slot-scope="scope">
<div class="t_c">
<template v-for="(item, index) in operation">
<div
:key="item.name"
:class="{ dis_in_center: direction === 'level' }"
>
<el-button
:key="index"
:style="{ width: item.width }"
:type="item.btnType || 'primary'"
:icon="item.icon"
:circle="item.circle"
:plain="item.plain"
:size="item.size || 'mini'"
@click.stop="
handleOperation(scope.row, item.type, index, scope.$index)
"
>
<span :style="getBtnStyle(item)">{{ item.name }}</span>
</el-button>
</div>
</template>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-if="total > 0"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="pageSizes"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: 'search-table',
props: {
// 表单项数据
formData: {
type: Array,
default: () => [],
},
// 表单校验数组
formRules: {
type: Object,
default: () => {},
},
// 下拉项的展示数据对象
selectTwoOptionsObj: {
type: Object,
default: () => {},
},
// 下拉状态
loadingSelect: {
type: Boolean,
default: false,
},
// 表单 更多/收起按钮的状态
isActive: {
type: Boolean,
default: false,
},
// 初始化表单数据
activeData: {
type: Array,
default: () => [],
},
// 加载文字
loadingText: {
type: String,
default: '拼命加载中',
},
// 表格无数据展示的文字
emptyText: {
type: String,
default: '暂无数据',
},
// 是否多选
isShowCheckbox: {
type: Boolean,
default: false,
},
// 表头数据
tableProps: {
type: Array,
default: () => [],
},
// 操作栏的展示规则
direction: {
type: String,
default: 'level',
},
// 表格数据
tableData: {
type: Array,
default: () => [],
},
// 操作项数据
operation: {
type: Array,
default: () => [],
},
// 操作栏是否固定
operationFixed: {
type: [String, Boolean],
default: false,
},
// 操作栏宽度
operationWidth: {
type: Number,
default: 200,
},
// 分页拉下选择展示条数
pageSizes: {
type: Array,
default: () => [10, 20, 50, 100],
},
// 一页展示多少条
pageSize: {
type: Number,
default: 10,
},
// 当前页
currentPage: {
type: Number,
default: 1,
},
// 总条数
total: {
type: Number,
default: 0,
},
},
data() {
return {
// 绑定的数据
form: {},
// 表单配置项数据
headerData: [],
}
},
watch: {
isActive: {
handler(val) {
this.setFormData(val)
},
immediate: true,
},
},
mounted() {
this.handleFormData()
},
methods: {
/**
* 动态控制表单
*/
setFormData(val) {
if (this.activeData && !this.activeData.length > 0) return
this.headerData = []
if (!val) {
this.formData.forEach((item, index) => {
if (this.activeData.includes(item.propName)) {
this.headerData.push(this.formData[index])
}
})
} else {
this.headerData = this.formData
}
},
handleFormData() {
if (this.activeData && !this.activeData.length > 0) {
this.headerData = this.formData
}
},
// 获取下拉框数据
async getOptionsArr(flag, item) {
// console.log(flag, item);
// 为true时表示展开,这里模拟根据枚举值id发请求,获取下拉框的值的
if (flag) {
this.$emit('update:loadingSelect', true) // 使用了加载中效果,最好加上一个try catch捕获异常
// let result = await this.$api.getEnumList({id:item.enumerationId})
this.$emit('getOptionsArrData', item)
} else {
// 解决多选下拉框失去焦点校验规则仍然存在问题
if (item.itemType == 'selectThree') {
// console.log("关闭时校验多选值", this.form[item.propName]);
if (this.form[item.propName].length > 0) {
// 如果至少选择一个了,说明符合要求,就再校验一次,这样校验规则就去掉了
this.$refs.form.validateField(item.propName)
}
}
}
},
// 保存提交表单
submitForm() {
let flag = null
this.$refs.form.validate((valid) => {
if (valid) {
flag = this.form
} else {
flag = false
}
})
return flag
},
// 重置表单
resetForm() {
this.$refs.form.resetFields()
this.form = {} // 这里重置完了以后,要重新初始化数据,否则会出现输入不上去的问题
},
// 点击插槽事件
clickSlot(prop, row) {
this.$emit('clickCell', { prop, row })
},
// 全选
handleSelectionChange(val) {
this.$emit('handle-selection-change', val)
},
// 编辑
editTable(val) {
this.$emit('edit-table', val)
},
// 操作栏事件
handleOperation(data, operaType, rowIndex, index) {
this.$emit('btnClick', { data, operaType, rowIndex, index })
},
// 每页多少条
handleSizeChange(val) {
this.$emit('changSize', val)
},
// 分页
handleCurrentChange(val) {
this.$emit('changCurrent', val)
},
setCorol(e) {
return `color:${e}`
},
getBtnStyle(e) {
let str = ''
for (let i in e.style) {
str += `${i}:${e.style[i]};`
}
return str
},
},
}
</script>
<style scoped lang="less">
.formWrap {
width: 100%;
/deep/ .el-form {
.el-form-item {
margin-bottom: 12px !important;
.el-form-item__content {
// 给下拉框指定宽度百分比
.el-select {
width: 100% !important;
}
// 时间选择器指定宽度百分比
.el-date-editor {
width: 100% !important;
.el-range-separator {
width: 10% !important;
}
}
.el-form-item__error {
padding-top: 1px !important;
}
}
}
}
.btns {
width: 100%;
text-align: center;
margin-top: 12px;
}
}
.dis_in_center {
display: inline-block;
vertical-align: middle;
}
.slot-box {
cursor: pointer;
}
.t_c {
text-align: center;
}
.pageContainer {
margin: 10px;
display: flex;
justify-content: flex-end;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
::v-deep .el-table__body tr.current-row > td {
background-color: #ffe0cc !important;
color: #ff6600 !important;
}
</style>
使用代码
<template>
<div id="app">
<search-table
ref="myForm"
:formData="formData"
:formRules="formRules"
:isActive="isActive"
:activeData="activeData"
:selectTwoOptionsObj="selectTwoOptionsObj"
:loadingSelect.sync="loadingSelect"
@getOptionsArrData="getOptionsArrData"
@submitForm="submitForm"
@resetForm="resetForm"
:tableData="tableData"
:tableProps="tableProps"
:operation="operation"
:isShowCheckbox="true"
:isShowPagination="true"
@clickCell="clickCell"
@btnClick="handleOperation"
:total="500"
>
<div class="btns">
<el-button type="primary" @click="submitForm" size="small"
>保存</el-button
>
<el-button @click="resetForm" size="small">重置</el-button>
<el-button @click="isActive = !isActive" size="small">{{
!isActive ? '更多' : '收起'
}}</el-button>
</div>
</search-table>
</div>
</template>
<script>
import SearchTable from './lib/search-table/SearchTable.vue'
import formJson from './lib/testForm.js'
export default {
name: 'App',
components: {
SearchTable,
},
data() {
return {
// 表单 更多/收起按钮的状态
isActive: false,
// 初始化展示的表单项
activeData: ['name', 'age', 'salary', 'btnSlot'],
// 表头数组数据
formData: formJson.formData,
// 表单校验数组
formRules: formJson.rules,
// 用于下拉框加载时的效果
loadingSelect: false,
// 此对象用于存储各个下拉框的数组数据值,其实也可以挂在vue的原型上,不过个人认为写在data中好些
selectTwoOptionsObj: {},
// 表格操作栏数组
operation: formJson.operation,
// 表格数据
tableData: formJson.tableData,
// 表格配置数据
tableProps: formJson.tableProps,
}
},
mounted() {
// 数据回显的时候,要先发请求获取枚举值下拉框的值才能够正确的回显,所以
// 就提前发请求获取对应下拉框的值了,这里要注意!注意!注意!
this.formData.forEach((item) => {
if ((item.itemType == 'selectTwo') | (item.itemType == 'selectThree')) {
this.getOptionsArrData(item)
}
})
},
methods: {
// 自定义插槽的点击事件
clickCell(e) {
console.log(e)
},
// 操作栏的点击事件
handleOperation(e) {
console.log(e)
},
// 获取表单填写的数据
submitForm() {
console.log('表单提交喽', this.$refs.myForm.submitForm())
},
// 重置表单数据
resetForm() {
this.$refs.myForm.resetForm()
},
// 表单部分下拉数据的设置 模拟异步请求
getOptionsArrData(item) {
setTimeout(() => {
this.loadingSelect = false
if (item.propName == 'job') {
this.selectTwoOptionsObj[item.propName] = formJson.jobList
}
if (item.propName == 'wish') {
this.selectTwoOptionsObj[item.propName] = formJson.wishList
}
if (item.propName == 'hobby') {
this.selectTwoOptionsObj[item.propName] = formJson.hobbyList
}
if (item.propName == 'wantPhone') {
this.selectTwoOptionsObj[item.propName] = formJson.wantPhoneList
}
this.$forceUpdate() // 这里需要强制更新一下,否则渲染不出来下拉框选项
}, 500)
},
},
}
</script>
<style lang="less"></style>