- 作者简介:大家好,我是文艺理科生Owen,某车企前端开发,负责AIGC+RAG项目
- 目前在卷的技术方向:工程化系列,主要偏向最佳实践
- 希望可以在评论区交流互动,感谢支持~~~
最近公司接到了一个简易BI报表
的需求,其中有多个页面是表单筛选+表格数据
显示的需求。
在如今AIGC
盛行的时代,这种CRUD的常规需求都可以用低代码平台搭建
或者用AI直接生成
了。类似于官方示例一样:
<template>
<div>
<el-form :inline="true" :model="formInline" class="demo-form-inline">
<el-form-item label="审批人">
<el-input v-model="formInline.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="formInline.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">查询</el-button>
</el-form-item>
</el-form>
</div>
</template>
优点是容易维护,缺点是扩展性差。综合考虑还是封装一个通用的表单筛选组件
,这样会加快后续的业务迭代速度。
最初是这样设计的,如下图,每个页面只需导入对应的配置数据,就可以直接展示对应页面。通用组件目前只有下拉筛选类
和文本输入类
两种(基于element ui),单独定义两种类型即可。使用时只需维护好配置数据,通用组件可以得到最大程度的复用。
其中通用筛选表单组件设计如下:
就这样,按照上面的方案封装完成后,开心地使用组件(摸鱼)。示例项目采用vue-cli
创建,vue2
语法,默认已安装和注册了element ui
组件库。如果仍对安装注册过程不清晰的同学,可跳转至element ui快速上手。具体代码如下:
<template>
<div class="container">
<el-form :inline="true" :model="formData" class="demo-form-inline">
<template v-for="(item, index) in config">
<el-form-item v-if="item.type === 'select'" :label="item.label">
<el-select v-model="formData[item.name]" :placeholder="`请选择${item.label}`">
<el-option v-for="option in options[item.name]" :label="option.label" :value="option.value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-else-if="item.type === 'input'" :label="item.label">
<el-input v-model="formData[item.name]" :placeholder="`请输入${item.label}`"></el-input>
</el-form-item>
</template>
<el-form-item>
<el-button @click="onReset">重置</el-button>
<el-button type="primary" @click="onSubmit">搜索</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'SearchForm',
props: {
config: {
type: Array,
default: () => []
},
options: {
type: Object,
default: () => []
}
},
data() {
return {
formData: {
},
}
},
methods: {
onSubmit() {
this.$emit('submit', this.formData)
},
onReset() {
this.formData = {
}
}
}
}
</script>
<style scoped>
.container {
width: 100%;
}
</style>
<!-- App.vue -->
<template>
<div id="app">
<SearchForm :config="config" :options="options" @submit="getSearchResult" />
</div>
</template>
<script>
import SearchForm from './components/SearchForm.vue'
export default {
name: 'App',
components: {
SearchForm
},
async mounted() {
this.options = await this.getOptions()
},
data() {
return {
config: [
{
type: 'input',
label: '用户名称',
name: 'username',
},
{
type: 'select',
label: '用户状态',
name: 'status'
}
],
options: {
}
}
},
methods: {
getSearchResult(res) {
console.log(res, '筛选的条件为:')
},
getOptions() {
// 筛选项由一个异步请求整体返回,模拟后端接口
return Promise.resolve({
status: [
{
label: '正常',
value: 0
},
{
label: '停用',
value: 1
}
]
})
}
}
}
</script>
原计划筛选项列表为一个接口整体返回,整体传入即可。但不幸的是,接到了需求变更,要求每一个筛选项都需要来源于不同的异步请求(正常options都是直接传入的),而且用户每点击一次,更新一次。顿时深感社会险恶,但不甘心一腔热血付之东流。
我们来梳理一下目前的难点:

- 每个options都需要一个异步请求,应该如何与动态配置config进行关联?
- 用户每点击一次就进行异步请求刷新一次筛选列表
传入不同异步请求下的筛选数据
目前config的成员对象为:
{
type: 'select',
label: '用户状态',
name: 'status'
}
经过调研,有一个初步的想法:尝试在成员对象中增加一个字段,对应值为一个promise对象,将其传入子组件,当el-select的下拉事件触发时,调用promise对象,将返回的筛选项数据存储在一个对象中。
修改后config
的成员对象为:
{
type: 'select',
label: '用户状态',
name: 'status',
loadOption: () => this.getStatusOption
}
其中getStatusOption
函数如下:
export default {
// ...其他代码
methods: {
getStatusOption() {
// 筛选项由一个异步请求整体返回,模拟后端接口
return Promise.resolve([
{
label: '正常',
value: 0
},
{
label: '停用',
value: 1
}
])
}
}
}
然后子组件中统一调用每个loadOption
函数
<script>
export default {
name: 'SearchForm',
data() {
return {
formData: {
},
options: {
},
}
},
methods: {
getOptions() {
this.config.forEach(async item => {
// 仅有loadOption属性的才会调用
if(item.loadOption) {
const option = await item.loadOption()()
this.$set(this.options, item.name, option)
}
})
}
}
}
</script>
这样实现了不同字段对应不同的异步请求。
另外,当异步请求需要传参时,传入的loadOptions
需要用bind
传入(因为仅做传入,不进行调用)
{
type: 'select',
label: '用户状态',
name: 'status',
loadOption: () => this.getStatusOption.bind(null, 'arg1', 'arg2')
}
每点击一次刷新一次筛选列表
接下来解决每点击一次刷新的问题。
这里用到visible-change
事件,仅当下拉框出现时触发(需要屏蔽隐藏时触发的情况)
子组件的getOptions
函数修改如下:
<template>
<!-- 省略其他代码 -->
<el-select v-model="formData[item.name]" :placeholder="`请选择${item.label}`" @visible-change="getOptions">
<el-option v-for="option in options[item.name]" :label="option.label" :value="option.value"></el-option>
</el-select>
<!-- 省略其他代码 -->
</template>
<script>
export default {
name: 'SearchForm',
data() {
return {
formData: {
},
options: {
},
}
},
methods: {
getOptions(visible) {
// 下拉列表隐藏时不需要刷新筛选项
if(!visible) return;
this.config.forEach(async item => {
if(item.loadOption) {
const option = await item.loadOption()()
this.$set(this.options, item.name, option)
}
})
}
}
}
</script>
经历了九九八十一难后,终于封装完成。又可以开心地摸鱼了。
总结:本文通过封装一个通用表单筛选组件,解决了由不同的异步请求获取筛选项、点击触发刷新的问题,增强了组件的复用性。
demo源码:github
日拱一卒,功不唐捐。