在管理系统页面开发时,会遇到一个简单又令人头痛的问题,那就是:搜索页面太多,搜索表单项内容太多。对于过多的内容,往往采取折叠的形式,仅展示部分内容,需要时展开查看全部。
如果在程序设计时不注意,就会在项目中出现大量、重复的展开折叠逻辑,使得代码冗余不说,后期一旦有逻辑改动,维护起来也相当繁琐。
为此需要设计一个组件来实现表单展开折叠的功能,具体需求如下:
- 默认展示表单项和更多表单项由开发者自定;
- 当没有更多表单项时,不出现展开/折叠按钮,且不需要每个表单传参控制;
- 当表单项成一行展示时,操作按钮在最右边;当表单项成多行展示时,表单按钮在右下方;
- 能够控制表单项之间的距离;
- 按下回车按钮时,触发搜索。
具体实现
对于默认展示表单项,以及更多的表单项,使用插槽来控制。默认展示的表单项,直接使用默认插槽;更多的表单项,包裹在一个具名插槽extra
中。如此,在控制更多的表单项时,只需控制具名插槽的显示/隐藏就可以了。
<slot></slot>
<template v-if="isMore">
<slot name="extra"></slot>
</template>
<script setup>
import { ref } from 'vue'
const isMore = ref(false)
</script>
通过判断是否有具名插槽extra
,就可以知道是否有更多表单项。从而实现控制在没有更多表单项时不出现展开/折叠按钮。
<script setup>
import { useSlots } from 'vue'
// 是否有更多表单项
const hasExtraSlot = useSlots().extra
<script/>
为了使操作按钮始终在表单的右下方,这里使用浮动布局来实现。
<div class="search-form-wrapper">
<slot></slot>
<template v-if="isMore">
<slot name="extra"></slot>
</template>
<!--操作按钮容器-->
<div class="search-form-wrapper__op"></div>
</div>
<style lang="less">
.search-form-wrapper {
&__op {
float: right;
}
}
</style>
表单项之间的距离控制。通过外部传入距离数据,组件内部控制样式实现。为了减少不必要的遍历表单和复杂逻辑,采用css样式变量来控制。
<template>
<div
class="search-form-wrapper"
:style="{ '--form-item-padding': gutter / 2 + 'px', margin: `0 -${gutter / 2}px -18px` }"
>
</div>
<template>
<script setup>
defineProps({
// 表单项之间的距离
gutter: {
type: Number,
default: 20
}
})
</script>
<style lang="less">
.search-form-wrapper {
&__op {
float: right;
padding: 0 var(--form-item-padding);
}
.el-form-item {
padding: 0 var(--form-item-padding);
margin-right: 0;
}
}
</style>
在按下回车键时触发搜索,一个方法是为每个表单项添加enter键盘事件,但是这种方法比较繁琐。另一个比较简单的方法是,为<form>
标签绑定enter键盘事件。
<script setup>
import { onMounted, onUnmounted } from 'vue'
function handleEvent(e) {
if (e.keyCode === 13) {
search() // 搜索
e.preventDefault()
}
}
// el-form 不在本组件内封装,作为本组件的父组件
// 监听父级表单的keydown事件,当按下回车键时触发查询操作
onMounted(() => {
const wrapper = document.querySelector('.search-form-wrapper')
try {
if (Array.from(wrapper.parentNode.classList).includes('el-form')) {
wrapper.parentNode.addEventListener('keydown', handleEvent)
onUnmounted(() => {
wrapper.parentNode.removeEventListener('keydown', handleEvent)
})
}
} catch (e) {
console.error(e)
}
})
</script>
完整代码
<!-- SearchFormWrapper.vue -->
<template>
<div
class="search-form-wrapper"
:style="{ '--form-item-padding': gutter / 2 + 'px', margin: `0 -${gutter / 2}px -18px` }"
>
<slot></slot>
<template v-if="isMore">
<slot name="extra"></slot>
</template>
<div class="search-form-wrapper__op">
<el-button type="success" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
<el-link
v-if="hasExtraSlot"
class="ml10"
type="primary"
:underline="false"
@click="toggleMore"
>
{{ moreText }}
<el-icon style="margin-left: 5px"><ArrowUp v-if="isMore" /><ArrowDown v-else /></el-icon>
</el-link>
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, useSlots } from 'vue'
defineProps({
gutter: {
type: Number,
default: 20
}
})
const isMore = ref(false)
const moreText = ref('更多筛选')
const toggleMore = () => {
isMore.value = !isMore.value
moreText.value = isMore.value ? '收起筛选' : '更多筛选'
}
const hasExtraSlot = useSlots().extra
const emit = defineEmits(['search', 'reset'])
function search() {
emit('search')
}
function reset() {
emit('reset')
}
function handleEvent(e) {
if (e.keyCode === 13) {
search()
e.preventDefault()
}
}
// 监听父级表单的keydown事件,当按下回车键时触发查询操作
onMounted(() => {
const wrapper = document.querySelector('.search-form-wrapper')
try {
if (Array.from(wrapper.parentNode.classList).includes('el-form')) {
wrapper.parentNode.addEventListener('keydown', handleEvent)
onUnmounted(() => {
wrapper.parentNode.removeEventListener('keydown', handleEvent)
})
}
} catch (e) {
console.error(e)
}
})
</script>
<style lang="less">
.search-form-wrapper {
&__op {
float: right;
padding: 0 var(--form-item-padding);
}
.el-form-item {
padding: 0 var(--form-item-padding);
margin-right: 0; // 表单inline模式会有margin-rihgt影响,需要重置它
}
}
</style>
使用示例
其中用到的样式类 w_25
是一个原子类,表示 width: 25%
。
<template>
<el-form :model="searchForm" :inline="true">
<SearchFormWrapper @search="handleSearch" @reset="handleReset">
<el-form-item class="w_25" label="表单项1">
<el-input v-model="test1"/>
</el-form-item>
<el-form-item class="w_25" label="表单项2">
<el-input v-model="test2"/>
</el-form-item>
<template slot="extra">
<el-form-item class="w_25" label="表单项3">
<el-input v-model="test2"/>
</el-form-item>
<el-form-item class="w_25" label="表单项4">
<el-input v-model="test2"/>
</el-form-item>
</template>
</SearchFormWrapper>
</el-form>
</template>
<script setup>
import { ref } from 'vue'
const searchForm = ref({
test1: '',
test2: '',
test3: '',
test4: ''
})
// 搜索
function handleSearch() {}
// 重置
function handleReset() {}
</script>