前言说明
这一版是未经过项目验证的,可能会有地方需要自行调整,如需使用,请慎重、慎重、再慎重!!!
前言追溯
前面分享过的搜索栏组件是一个临时产物,经历了一两个项目之后就被淘汰了。后续在我们前端小组内部开始探讨一种新的页面开发模式:配置渲染。理想状态下,仅需要在页面中建立一个配置对象以及配套的各种处理方法,就可以渲染出一个页面。大概内容如下:
<template>
<custom-component ref="customRef" :config="comConfig"/>
</template>
<script>
name: 'XXX',
data(){
return {
comConfig: {
// 标题栏配置
headerConfig: {
title: 'xxx',
buttonList: [...]
}
// 搜索栏配置
searchConfig: [...]
// 表格配置
tableConfig: {
proxyConfig: {...},
columns: [....],
pagination: {...}
},
modalConfig: {
title: 'xxx',
visible: false,
formInfo: {...},
formRules: {...}
}
}
}
}
</script>
我当时第一感觉:这个构想确实牛的一批!但咋实现出来呢?,这哪是封组件,这就是封装了一个公司内部通用的基于element-ui二次开发的插件库啊这是。最开始只是讨论想法,并没有开始进行实现,也就不关我事,全身心的投入项目需求开发中了。
没想到的是,不就之后的一天,领导发给我了一个项目:你没事的时候看看项目代码,梳理下内容,尽快整理出来个内容的文档,后面有一个项目可能会使用这个项目作为组件去开发。
我花费一天时间把代码大概过了一遍,猛的发现:当时提出的构想,这不就是已经实现了一大半了嘛!!!后续也是由我把代码做了一些修改,更加适合项目中使用,后续也确实是在项目中进行了试用,有用的很好很舒服的地方,也有极其不想用的地方。整理出来的帮助文档,后续也作为培训资料进行了内部培训…
打住,就此打住!这是我后面要分享的内容,此处先不多说。
样例展示
默认状态
展开状态
折叠状态
组件示例
<SearchCustom
ref="searchCustom"
:itemList="searchItemList"
@formSearch="handleSearch"
@formReset="handleReset"
/>
searchItemList: [
{
elType: "el-input",
key: "name",
placeholder: "请输入风险词模糊搜索"
},
{
elType: "el-select",
key: "sex",
placeholder: "请选择性别",
options: []
},
{
elType: "el-cascader",
key: "dept",
placeholder: "请选择部门",
options: [
{
label: '研发部',
value: 'yanfa',
children: [
{
label: '研发一组',
value: 'yanfa1',
},
{
label: '研发二组',
value: 'yanfa2',
}
]
},
{
label: '外包部',
value: 'waibao',
children: [
{
label: '外包一组',
value: 'waibao1',
},
{
label: '外包二组',
value: 'waibao2',
}
]
}
]
},
{
elType: "el-date-picker",
key: "createTime",
type: 'date',
valueFormat: 'yyyy-MM-dd',
placeholder: "请选择截止时间"
},
{
elType: "el-date-picker",
key: "timeRange",
type: 'daterange',
span: 6,
valueFormat: 'yyyy-MM-dd',
rangeSeparator: '至',
startPlaceholder: "开始日期",
endPlaceholder: "结束日期"
},
],
// 支持对搜索项下拉数据进行填充
this.$refs['searchCustom'].setFormItemOptions('sex', [
{
label: '男',
value: 1
},
{
label: '女',
value: 2
}]
)
// 搜索、重置方法
handleSearch (val) {
console.log('val===>', val);
}
handleReset (val) {
console.log('val===>', val);
}
组件源码
<template>
<div class="search-wrapper">
<el-form
inline
:model="formInfo"
size="small"
>
<!-- 行列栅格布局 -->
<el-row
v-for="(formItem, index) in formItemList"
:key="index"
:gutter="20"
>
<el-col
v-for="(item, itemIndex) in formItem"
:key="itemIndex"
:span="item.span"
>
<!-- 搜索项渲染 -->
<el-form-item
v-show="item.itemType == 'search'"
:label="item.label"
>
<!-- 选择框单独处理,后续不能通过配置直接渲染的组件都需要单独处理 -->
<el-select
v-if="item.elType == 'el-select'"
v-model="formInfo[item.key]"
v-bind="{ ...item }"
style="width: 100%"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
></el-option>
</el-select>
<!-- 通用组件 -->
<component
v-else
v-model="formInfo[item.key]"
:is="item.elType"
v-bind="{ ...item }"
style="width: 100%"
></component>
</el-form-item>
<!-- 搜索栏按钮渲染 -->
<el-form-item
v-show="item.itemType == 'button'"
style="text-align: right;"
>
<!-- 当搜索项数量超过一行时自动显示 折叠/展开按钮-->
<el-button
v-if="isCanFoldFlag"
type="text"
:icon="`el-icon-${toggleFoldFlag ? 'arrow-up' : 'arrow-down'}`"
@click="handleToggleFold"
>{{ toggleFoldFlag ? '展开' : '折叠' }}</el-button>
<el-button
type="primary"
@click="handleSearch"
icon="el-icon-search"
>查询
</el-button>
<el-button
type="info"
@click="handleReset"
icon="el-icon-refresh-right"
>重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script>
export default {
name: 'SearchCustom',
props: {
itemList: {
type: Array,
required: true,
default: () => []
}
},
data () {
return {
toggleFoldFlag: false, // 按钮状态:折叠/展开
isCanFoldFlag: false, // 是否展示按钮(折叠/展开)
formInfo: {}, // 搜索栏搜索项值--用于表单绑定
formItemList: [] // 搜索栏数据--用于渲染
}
},
mounted () {
// 初始化搜索栏表单默认值和渲染数据
this.handleInitFormData()
},
methods: {
/**
* 设置搜索项Options数据
* @param {*} field 搜索项名称
* @param {*} options 选择数据
*/
setFormItemOptions (field, options) {
let itemIndex = this.itemList.findIndex(item => item.key == field)
if (itemIndex != -1) {
this.itemList[itemIndex].options = options
// 设置完成后重新处理下数据 todo:可否简单化处理
this.handleInitFormData()
}
},
// 根据传入属性itemList对表单数据进行初始化
handleInitFormData () {
this.itemList.forEach(item => {
item.itemType = 'search'
/******************处理默认值***************/
if (item.defaultValue != null) {
this.$set(this.formInfo, item.key, item.defaultValue)
}
})
// 初始化搜索栏数据渲染
this.handleLayoutForm()
},
// 初始化搜索栏数据渲染
handleLayoutForm () {
// 初始化数据变量
this.formItemList = []
let spanTotal = 0
let spanList = []
/******************处理栅格布局渲染***************/
this.itemList.forEach(item => {
if (item.span == null) {
// 默认占4格
item.span = 4
}
/*
* 累加span的值,如果大于24则表明需要填充下一行数据,否则填充当前行
*/
spanTotal += item.span
if (spanTotal > 24) {
// 超过一行,可以展示按钮(折叠/展开)
this.isCanFoldFlag = true
this.formItemList.push(spanList)
// 开启下一行的填充
spanTotal = item.span
spanList = [item]
} else {
spanList.push(item)
}
})
// 当数据不够一行或为最后一行时候,在最后面追加按钮
if (spanTotal <= 24) {
// 剩余空间不足4(即空余一个搜索项)
if (24 - spanTotal >= 4) {
spanList.push({
span: 24 - spanTotal,
itemType: 'button'
})
this.formItemList.push(spanList)
} else {
// 超过一行,可以展示按钮(折叠/展开)
this.isCanFoldFlag = true
this.formItemList.push(spanList)
this.formItemList.push([{ span: 24, itemType: 'button' }])
}
spanTotal = 0
spanList = []
}
},
// 控制按钮折叠/展开
handleToggleFold () {
this.toggleFoldFlag = !this.toggleFoldFlag
// 处理折叠/展开对搜索项的影响
if (this.toggleFoldFlag) {
let spanTotal = 0
let spanList = []
// 获取足够展示一行的搜索项数据,后续拼接按钮展示
for (let i = 0; i < this.itemList.length; i++) {
let item = this.itemList[i]
// 20 是因为按钮最少展示一个搜索项宽度:4
if ((spanTotal + item.span) > 20) {
break
}
spanTotal += item.span
spanList.push(item)
}
spanList.push({ span: 24 - spanTotal, itemType: 'button' })
this.formItemList = [spanList]
console.log('this.formItemList===>', this.formItemList);
} else {
this.handleLayoutForm()
}
},
/**
* 搜索查询
*/
handleSearch () {
this.$emit('formSearch', this.formInfo)
},
/**
* 搜索重置
*/
handleReset () {
this.formInfo = {}
this.handleInitFormData()
this.$emit('formReset', this.formInfo)
}
}
}
</script>
<style lang="scss" scoped>
.search-wrapper ::v-deep .el-form-item--small {
width: 100%;
display: flex;
align-items: center;
}
.search-wrapper ::v-deep .el-form-item--small .el-form-item__content {
flex: 1 0 auto;
}
</style>
组件说明
栅格布局分化
栅格布局是分行和列的,但传入的搜索项数据却是个数组。所以为了方便渲染,需要将其转化为二维数组。
但同时按钮是需要跟在搜索项后面的,在搜索项不满一行或者最后一行不满的情况下,且满足剩余空间大于等于一个搜索项(我这里看是三个按钮(查询、重置和 展开/折叠)占一个可以正常显示 ),按钮追加到搜索项所在行内,发反之,按钮独占一行。
主要代码在handleLayoutForm 和 handleToggleFold 两个方法中
填充搜索项数据
由于要进行栅格布局分化,所以填充完数据之后再次进行栅格布局处理。
我一直在思考这一步是否可以简化?个人有两点方向,但也觉得麻烦。如有更好的方式,欢迎讨论。
- 目前这种方式,先填充,再处理栅格
- 两次查找,准备定位,填充数据
/**
* 设置搜索项Options数据
* @param {*} field 搜索项名称
* @param {*} options 选择数据
*/
setFormItemOptions (field, options) {
let itemIndex = this.itemList.findIndex(item => item.key == field)
if (itemIndex != -1) {
this.itemList[itemIndex].options = options
// 设置完成后重新处理下数据 todo:可否简单化处理
this.handleInitFormData()
}
},
搜索项样式穿透
这里处理的原因是栅格布局下el-form-item内的组件并没有占满空间,所以强行调整了一波。
<style lang="scss" scoped>
.search-wrapper ::v-deep .el-form-item--small {
width: 100%;
display: flex;
align-items: center;
}
.search-wrapper ::v-deep .el-form-item--small .el-form-item__content {
flex: 1 0 auto;
}
</style>
组件渲染
component动态组件的渲染,重点要说的是 v-bind=“{…item}” 。我最开始想到的是通过 a t t r s / attrs/ attrs/listener组合来做的,但是$attrs接收到的是除了props之外的挂载到子组件根标签上的属性,似乎也不太合适。最后无意中写出了这个组合,渲染出来了,但具体什么原理,一时半会儿还说不上来,有明白人可以在评论区说一下嘛
<!-- 选择框单独处理,后续不能通过配置直接渲染的组件都需要单独处理 -->
<el-select
v-if="item.elType == 'el-select'"
v-model="formInfo[item.key]"
v-bind="{ ...item }"
style="width: 100%"
>
<el-option
v-for="option in item.options"
:key="option.value"
:label="option.label"
:value="option.value"
></el-option>
</el-select>
<!-- 通用组件 -->
<component
v-else
v-model="formInfo[item.key]"
:is="item.elType"
v-bind="{ ...item }"
style="width: 100%"
></component>
个人说
这一版的内容也是经过之前提到的项目代码在真实项目中实践之后自己总结出来的,原项目也很好,但是总觉得封装程度有点深(可能是我能力不够哈,原项目很牛逼!!!),所以选择了搜索栏一部分来实现,配合之前的搜索栏容器处理,虽然没有经过真实项目使用实践 ,但是我个人觉得很满意。
容器部分选择使用了栅格布局,这样就更好控制排版统一。
表单组件部分采用了组件动态渲染,一些特殊的组件通过组件名称自行渲染(如,因为下拉数据没法通过配置渲染)。
最开始想做这个组件时候,觉得最重要的点就是如何通过配置属性转换为对应的组件,其他的属性到时再想办法解决。天知道当时脑子怎么了,我首先想到的居然是createElement方法。。。看了半天才转过来脑子:这玩意儿是从0到1创建东西的,不是用来从一到1做转换的。后来想到了component动态组件,但脑子可能还有点糊涂,想着只有Inpu输入框可以这么搞,还是不行,就用笨方法:v-if/v-else-if把对应组件进行了列举。结果吧,搞着搞着发现,除了select,其余除了组件名称不一样其余都一样,还是换回component组件吧,所以就成了现在这样。