#前端做管理端项目时 会有一个非常明显的问题 就是 表格页面过多 如果每个表格页面 都是自己写一套表格搜索条件 会造成很多的代码冗余 于是我就尝试封装一个适用于大部分表格的 搜索条件组件 用于提高代码复用性、可维护性和用户体验#
文章目录
-
- 引言
- 需求分析
- 设计思路
- 实现细节
- 使用示例
- 总结
引言
在前端开发中,表格搜索是常见的功能需求。特别是在管理端项目中,表格搜索能够帮助用户快速定位到所需的数据。Element Plus作为基于Vue 3的组件库,提供了丰富的组件和功能,非常适合用于构建这样的搜索功能。本文将介绍如何使用Element Plus封装前端表格搜索条件。
需求分析
在封装表格搜索条件之前,我们需要明确用户的需求。通常,用户期望能够通过输入关键字、选择筛选条件等方式来搜索表格数据。因此,我们需要设计一个易于使用的搜索界面,并封装相应的搜索逻辑。
设计思路
- 搜索界面设计:使用Element Plus的表单组件(如el-form)和输入组件(如el-input)来设计搜索界面。可以根据需求添加不同的筛选条件,如文本框、下拉框、复选框等。
- 搜索逻辑封装:将搜索逻辑封装在一个单独的方法或组件中。该方法或组件接收搜索参数,并返回符合搜索条件的表格数据。在封装过程中,我们需要注意处理各种边界情况和异常情况,确保搜索功能的稳定性和可用性。
实现细节
-
搜索界面实现:
- 使用el-form组件创建表单容器,并设置其属性以适应不同的布局需求。
- 在表单中添加所需的输入组件和筛选组件,如el-input、el-select等。
- 为每个输入组件和筛选组件绑定相应的数据字段和事件处理器,以便在用户输入或选择时更新搜索参数。
- 展开/收缩功能 搜索条件默认展示时最多不超过三行 超过就会出现展开/收缩功能
- 根据屏幕的大小实现搜索内容的自适应
- 实现搜索和重置按钮始终处于搜索条件页面的右下角位置
-
特殊情况扩展:
- 可能有时会有一些特殊的定制化的搜索条件 所以要兼容到这种情况做出处理。
使用示例
为了更好地说明如何使用Element Plus封装表格搜索条件,我将提供一个简单的使用示例。在该示例中,我将创建一个包含文本框和下拉框等的搜索界面以及页面图片和代码内容展示
首先展示下各个情况下的页面图片
以上是页面在各种屏幕大小下自适应显示的内容
下面是封装的搜索条件组件代码部分
<template>
<div class="com-search-module" @submit.prevent ref="tableSearchFormRef">
<div class="com-search-module-box">
<el-form ref="searchformref" class="filter-container" :model="searchForm" @keyup.enter="search">
<el-row ref="elRowRef" :gutter="20" :style="{width: `${props.isUseWindowSize ? widthVal : userModuleWidth}px`}">
<template v-for="(item, index) in list" :key="index">
<el-col :sm="24" :md="12" :lg="showThreeOrFour" :xl="showThreeOrFour" v-show="calcIsShowCol(index)">
<div class="com-search-module-list" v-if="item.type == 'input'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-input v-model="item.value" :placeholder="'请输入' + item.placeholder" clearable />
</div>
<div class="com-search-module-list" v-if="item.type == 'select'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<HnSelect :filterable="item.filterable" @seletChange="(val) => seletChange(item, val)" v-model="item.value" :item='item' :clearable="item.clearable === false?false:true" ></HnSelect>
</div>
<div class="com-search-module-list" v-if="item.type == 'datePicker'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-date-picker class="date-picker" :placeholder="'请选择' + item.name" v-model="item.value"
type="daterange" unlink-panels range-separator="至" start-placeholder="开始日期"
end-placeholder="结束日期" :shortcuts="state.shortcuts" />
</div>
<div class="com-search-module-list" v-if="item.type == 'datePickerTime'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-date-picker class="date-picker" :placeholder="'请选择' + item.name" v-model="item.value" value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange" unlink-panels range-separator="至" start-placeholder="开始日期" :default-time="[new Date(new Date().setHours(0, 0, 0)),new Date(new Date().setHours(23, 59, 59))]"
end-placeholder="结束日期"/>
</div>
<div class="com-search-module-list" v-if="item.type == 'onedatePicker'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-date-picker v-model="item.value" type="date" value-format="YYYY-MM-DD" :placeholder="'请选择' + item.name"/>
</div>
<div class="com-search-module-list" v-if="item.type == 'onedatePickerTime'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-date-picker v-model="item.value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" :placeholder="'请选择' + item.name"/>
</div>
<!-- 多选 -->
<div class="com-search-module-list" v-if="item.type == 'moreSelect'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<!-- <HnSelect @seletChange="seletChange" v-model="item.value" :item='item' :moreSelect="true"></HnSelect> -->
<HnMoreSelect :filterable="item.filterable" @moreSeletChange="moreSeletChange" v-model="item.value" :item='item'></HnMoreSelect>
</div>
<!-- 复合输入框 -->
<div class="com-search-module-list" v-if="item.type == 'inputTemplate'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<el-input v-model="item.value" placeholder="请输入">
<template #append><el-button :icon="RefreshLeft" @click="item.value=''"/></template>
</el-input>
<!-- <HnInputTemplate v-model="item.value"></HnInputTemplate> -->
</div>
<!-- 复合单选框 -->
<div class="com-search-module-list" v-if="item.type == 'secondSelect'">
<span class="list-text" :style="{width: props.labelWidth}">{{ item.name }}</span>
<HnSecondSelect @seletChangeOne="seletChangeOne" @seletChangeTwo="seletChangeTwo" v-model="item.value" :item='item'></HnSecondSelect>
</div>
<!-- 添加自定义插槽 -->
<div v-if="item.type === 'mySlot'">
<slot :name="item.key"></slot>
</div>
</el-col>
</template>
<!-- <div class="com-search-module-list" v-if="placeholderEle()" v-for="(item,index) in placeholderEle()" :key="index">
</div> -->
<el-col :sm="24" :md="calcSearchBtnPos" :lg="calcSearchBtnPos" :xl="calcSearchBtnPos">
<div class="com-search-module-button">
<div class="search" @click="search">搜索</div>
<div class="reset" @click="reset(searchformref)">重置</div>
<slot name="com-search-module-button"></slot>
</div>
</el-col>
</el-row>
</el-form>
</div>
<!-- <div class="com-search-module-button" style="display: flex;justify-content: flex-end;align-items: center;padding-right:200px; padding-bottom: 20px;">
<div class="search" @click="search" style="background: #34b46f">搜索</div>
<div class="reset" @click="reset(searchformref)">重置</div>
<slot name="com-search-module-button"></slot>
</div> -->
<div v-if="showButton" class="unfold_fewer_box">
<el-button class="unfold_fewer_box_button" @click="isFormExpand = !isFormExpand" type="info">
<!-- <el-icon v-show="!isFormExpand"><ArrowDownBold /></el-icon>
<el-icon v-show="isFormExpand"><ArrowUpBold /></el-icon> -->
<el-icon v-show="!isFormExpand"><CaretBottom color="#999" /></el-icon>
<el-icon v-show="isFormExpand"><CaretTop color="#999" /></el-icon>
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import { FormInstance } from "element-plus";
import { RefreshLeft } from '@element-plus/icons-vue'
import { useWindowSize } from "@vueuse/core"
import { useSettingsStore } from "@/store/modules/settings"
import { useAppStore } from "@/store/modules/app";
import shortcuts from '@/utils/dateShortCuts';
const emit = defineEmits(["search","reset", "seletChange","moreSeletChange",'seletChangeOne',"seletChangeTwo"]);
const props = defineProps({
list: { type: Array<Search> },//查询框架数据
searchForm: { //返回数据
type: Array,
default: () => {
return []
}
},
isUseWindowSize: {
type: Boolean,
default: true
},
labelWidth: {
type: String,
default: '120px'
}
});
watch(() => props.list, (val:any) => {//监听list
if (val && val.length > 0) {
state.list = val;
// 排序功能
state.list.sort(compare('sort'))
} else {
state.list = []
}
})
const elRowRef = ref()
const tableSearchFormRef = ref()
const { width } = useWindowSize()
const { width: elWidth } = useElementSize(tableSearchFormRef)
const appStore = useAppStore();
const settingsStore = useSettingsStore();
const showSidebar = computed(() => settingsStore.layout !== 'top')
const widthVal = computed(() => {
const widthVal = width.value - 40 - 40
return showSidebar.value ? (!appStore.sidebar.opened ? (width.value >= 992 ? (widthVal - 54) : widthVal) : (widthVal - 210)) : widthVal
})
const showThreeOrFour = computed(() => {
if (width.value >= 1200 && width.value < 1600) return 8
return 6
})
const userModuleWidth = computed(() => {
return elWidth.value
})
const searchformref = ref<FormInstance>()
// 搜索表单 关于展开和收缩功能
const isFormExpand = ref(false);
const showButton = computed(() => {
if(props.list){
// if(spanValue.value === 6 && props.list.length >= 12){
// return true;
// }else if(spanValue.value === 8 && props.list.length >= 9){
// return true;
// }else if(spanValue.value === 12 && props.list.length >= 6){
// return true;
// }else if(spanValue.value === 24 && props.list.length >= 3){
// return true;
// }else{
// return false
// }
if (width.value < 992) {
if (props.list.length >= 3) {
return true
}
} else if (width.value >= 992 && width.value < 1200) {
if (props.list.length >= 6) {
return true
}
} else if (width.value >= 1200 && width.value < 1600) {
if (props.list.length >= 9) {
return true
}
} else if (width.value >= 1600) {
if (props.list.length >= 12) {
return true
}
} else {
return false
}
}else{
return false
}
});
const state = reactive({
list: [],
shortcuts: shortcuts
})
const spanValue = ref(6);
// 站位元素是否显示
const placeholderEle = ()=>{
if( !isFormExpand.value ){// 收起
if( spanValue.value === 6 && props.list && is4PatternNumber1(props.list?.length) && props.list?.length<11){// 一行四个时 lsit 有2/6/10个 补位一个
return 1
}else if( spanValue.value === 6 && props.list && is4PatternNumber2(props.list?.length) && props.list?.length<10){// 一行四个时 lsit 有1/5/9个 补位两个
return 2
}else if( spanValue.value === 6 && props.list && is4PatternNumber3(props.list?.length) && props.list?.length<9){// 一行四个时 lsit 有4/8个 补位三个
return 3
}else if( spanValue.value === 8 && props.list && is3PatternNumber1(props.list?.length) && props.list?.length<8){// 一行三个时 lsit 有1/4/7个 补位一个
return 1
}else if( spanValue.value === 8 && props.list && is3PatternNumber2(props.list?.length) && props.list?.length<7){// 一行三个时 lsit 有3/6个 补位两个
return 2
}else if( spanValue.value === 12 && props.list && is2PatternNumber1(props.list?.length) && props.list?.length<5){// 一行两个时 lsit 有2/4个 补位一个
return 1
}else{
return false
}
}else{// 展开
if( spanValue.value === 6 && props.list && is4PatternNumber1(props.list?.length)){// 一行四个时 lsit 有2/6/10/14/18/22/26/30/34/38个 补位一个
return 1
}else if( spanValue.value === 6 && props.list && is4PatternNumber2(props.list?.length)){// 一行四个时 lsit 有1/5/9/13/17/21/25/29/33/37/41/45个 补位两个
return 2
}else if( spanValue.value === 6 && props.list && is4PatternNumber3(props.list?.length)){// 一行四个时 lsit 有4/8/12/16/20/24/28/32/36/40/44个 补位三个
return 3
}else if( spanValue.value === 8 && props.list && is3PatternNumber1(props.list?.length)){// 一行三个时 lsit 有1/4/7个 补位一个
return 1
}else if( spanValue.value === 8 && props.list && is3PatternNumber2(props.list?.length)){// 一行三个时 lsit 有3/6/9个 补位两个
return 2
}else if( spanValue.value === 12 && props.list && is2PatternNumber1(props.list?.length)){// 一行两个时 lsit 有2/4/6个 补位一个
return 1
}else{
return false
}
}
}
const is4PatternNumber1 = (num:number)=>{// 一行四个时 lsit 有2/6/10/14/18/22/26/30/34/38个 这种规律时 补位一个
let result = (num - 2) % 4 === 0;
return result && Number.isInteger((num - 2) / 4);
}
const is4PatternNumber2 = (num:number)=>{// 一行四个时 lsit 有1/5/9/13/17/21/25/29/33/37/41/45个 这种规律时 补位两个
let result = (num - 1) % 4 === 0;
return result && Number.isInteger((num - 1) / 4);
}
const is4PatternNumber3 = (num:number)=>{// 一行四个时 lsit 有4/8/12/16/20/24/28/32/36/40/44个 这种规律时 补位三个
return num % 4 === 0 && Number.isInteger(num / 4);
}
const is3PatternNumber1 = (num:number)=>{// 一行三个时 lsit 有1/4/7个 这种规律时 补位一个
let result = (num - 1) % 3 === 0;
return result && Number.isInteger((num - 1) / 3);
}
const is3PatternNumber2 = (num:number)=>{// 一行三个时 lsit 有3/6/9个 这种规律时 补位两个
return num % 3 === 0 && Number.isInteger(num / 3);
}
const is2PatternNumber1 = (num:number)=>{// 一行两个时 lsit 有2/4/6个 这种规律时 补位一个
return num % 2 === 0 && Number.isInteger(num / 2);
}
// 计算收起状态下 最多展示几个搜索条件
const calcIsShowCol = (index:number) => {
if( isFormExpand.value ){// 展开
return true
} else {
// if(spanValue.value === 6 && index<11){// 收缩 一行4个 最多显示11个
// return true
// }else if(spanValue.value === 8 && index<8){// 收缩 一行3个 最多显示8个
// return true
// }else if(spanValue.value === 12 && index<5){// 收缩 一行2个 最多显示5个
// return true
// }else if(spanValue.value === 24 && index<2){// 收缩 一行1个 最多显示2个
// return true
// }else{
// return false
// }
if (width.value < 992) {
if (index < 2) {
return true
}
} else if (width.value >= 992 && width.value < 1200) {
if (index < 5) {
return true
}
} else if (width.value >= 1200 && width.value < 1600) {
if (index < 8) {
return true
}
} else if (width.value >= 1600) {
if (index < 11) {
return true
}
} else {
return false
}
}
}
// 计算一行显示几个搜索条件
const calcSpan = () => {
if(tableSearchFormRef.value.offsetWidth<992){
spanValue.value = 24
}else if(tableSearchFormRef.value.offsetWidth>=992 && tableSearchFormRef.value.offsetWidth<1200){
spanValue.value = 12
}else if(tableSearchFormRef.value.offsetWidth>=1200 && tableSearchFormRef.value.offsetWidth<1600){
spanValue.value = 8
}else{
spanValue.value = 6
}
}
// 计算搜索按钮的位置
const calcSearchBtnPos = computed(() => {
if (width.value >= 992 && width.value < 1200) {
if (isFormExpand.value) {
const count = props.list!.length % 2
return 24 - 12 * count
} else {
const len = props.list!.length
if (len > 5) {
return 12
} else {
const count = len % 2
return 24 - 12 * count
}
}
} else if (width.value >= 1200 && width.value < 1600) {
if (isFormExpand.value) {
const count = props.list!.length % 3
return 24 - 8 * count
} else {
const len = props.list!.length
if (len > 8) {
return 8
} else {
const count = len % 3
return 24 - 8 * count
}
}
} else if (width.value >= 1600) {
if (isFormExpand.value) {
const count = props.list!.length % 4
return 24 - count * 6
} else {
const len = props.list!.length
if (len > 11) {
return 6
} else {
const count = props.list!.length % 4
return 24 - count * 6
}
}
} else {
return 24
}
})
onUnmounted(() => {
window.removeEventListener('resize', calcSpan)
})
function search() {
state.list.forEach((item: Search) => {
if (item.type == 'datePicker' || item.type == 'datePickerTime') {
if (item.value && item.value.length > 0) {
if (typeof item.value[0] === 'string') {
item.value[0] = item.value[0].replace(/\//g, '-');
} else {
item.value[0] = item.value[0].toLocaleDateString().replace(/\//g, '-');
}
if (typeof item.value[1] === 'string') {
item.value[1] = item.value[1].replace(/\//g, '-');
} else {
item.value[1] = item.value[1].toLocaleDateString().replace(/\//g, '-');
}
}
}
props.searchForm[item.key] = item.value;
})
emit('search', props.searchForm)
}
function reset(searchformref: FormInstance | undefined) {
emit('reset');
nextTick(() => {
// searchformref.validateOnRuleChange.resetField();
searchformref?.resetFields()
});
state.list.forEach((item) => {
switch (Object.prototype.toString.call(item.value)) {
case '[object Array]':
item.value = []
break;
case '[object String]':
item.value = ""
break;
case '[object Number]':
item.value = null
break;
default:
item.value = null
break;
}
})
}
function compare(attr) {
return function (a, b) {
let val1 = a[attr]
let val2 = b[attr]
return val1 - val2
}
}
function seletChange(item, val) {
emit('seletChange', item, val)
}
function moreSeletChange(item,val){
emit('moreSeletChange',item,val)
}
function seletChangeOne(item,val){
emit('seletChangeOne',item,val)
}
function seletChangeTwo(item,val){
emit('seletChangeTwo',item,val)
}
onMounted(() => {
window.addEventListener('resize', calcSpan);
calcSpan();
if (props.list && props.list.length > 0) {
state.list = props.list
// 排序功能
state.list.sort(compare('sort'))
} else {
state.list = []
}
})
defineExpose({
state,
search,
reset,
compare,
seletChange,
moreSeletChange
});
</script>
<style lang="less" scoped>
.input-with-select .el-input-group__prepend {
background-color: var(--el-fill-color-blank);
}
// 展开 收起区域
.unfold_fewer_box{
position: absolute;
bottom: -20px;
left: 50%;
transform: translateX(-30%);
width: 200px;
.unfold_fewer_box_button{
width: 96px;
height: 20px;
border-radius: 0px 0px 8px 8px;
box-shadow: 0px 1px 10px 1px #E6E6E6;
background-color: #FFF;
border: none
}
}
.com-search-module-box{
transition: max-height 0.6s ease-in-out;
-webkit-transition: max-height 0.6s ease-in-out; /* Safari 和 Chrome */
-moz-transition: max-height 0.6s ease-in-out; /* Firefox */
-ms-transition: max-height 0.6s ease-in-out; /* IE 9 */
overflow: hidden; /* 出现滚动条 */
padding: 0 0 20px;
}
</style>
下面是在页面中使用的示例
<template>
<div class="app-container" v-loading="loading">
<HnQueryTemplate @search="handleQuery" :searchForm='searchForm' :list='list' label-width="110px"></HnQueryTemplate>
</template>
<script setup lang="ts">
const searchForm = reactive<ForecastQuery>({
pageNum: 1,
pageSize: 10,
});
const list = reactive<Array<Search>>([
{
key: 'customerIdList',
name: '客户', //名字
type: 'moreSelect', //类型
placeholder: '客户',
filterable:true,//筛选功能
value: [], //绑定的值
options: [],
sort: 1,
},
{
key: 'serviceTypeIdList',
name: '服务', //名字
type: 'moreSelect', //类型
filterable:true,//筛选功能
placeholder: '服务',
value: '', //绑定的值
options: [],
sort: 2,
},
{
key: 'siteIdList',
name: '拣货站点', //名字
type: 'moreSelect', //类型
filterable:true,//筛选功能
placeholder: '拣货站点',
value: '', //绑定的值
isShow:true,
options: [],
sort: 3,
},
{
key: 'forecastTime',
name: '预报节点', //名字
type: 'onedatePickerTime', //类型
placeholder: '预报节点',
value: '', //绑定的值
sort: 4,
},
{
key: 'createByIdList',
name: '创建人', //名字
type: 'moreSelect', //类型
placeholder: '创建人',
value: '', //绑定的值
options: [],
filterable: true,
sort: 5,
},
{
key: 'createByTime',
name: '创建时间', //名字
type: 'datePickerTime', //类型
placeholder: '创建时间',
value: '', //绑定的值
sort: 6,
},
])
/**
* 查询
*/
function handleQuery() {
loading.value = true;
getPageList(searchForm)
.then(({ data }) => {
tableData.value = data.records;
total.value = data.total;
}).finally(() => {
loading.value = false;
});
}
</script>
总结
本文介绍了如何使用Element Plus封装前端表格搜索条件。通过合理的设计和实现,我们可以构建出一个易于使用、功能强大的搜索功能,提高用户的数据检索效率和使用体验。同时,我们也需要注意性能优化和功能扩展的问题,以满足不同场景下的需求。