前言
=====
在后台管理系统中,经常需要在多个页面中使用搜索功能。搜索的形式可以包括文本搜索、时间范围搜索、选择框搜索、日期搜索和多级联选搜索等。然而,在每个页面中编写重复的搜索功能代码会导致效率低下且代码冗余。为了解决这个问题,我们可以封装一个通用的搜索组件,以节省开发时间和减少重复代码的量。(仅供参考,欢迎评论区jym提出更好的意见)
首先,让我们分析页面布局和功能需求:
-
搜索组件本质上是一个表单。
-
表单中包含了文本框、选择框、级联选择器和日期选择器等不同类型的输入控件。
-
当点击搜索按钮时,应触发搜索事件;当点击重置按钮时,应触发重置事件。
基于上述分析,我们可以逐步实现具体的功能。请按照以下步骤进行操作:
-
创建一个新的 Vue 3 项目:
npm create vue@latest
-
删除项目中自动生成的文件和内容。
-
在项目中安装 Element Plus:
npm install element-plus --save
在components目录中创建 mySearch
组件,包含了一个基于 Element Plus 的表单 (el-form
)。表单的属性包括大小 (size
)、数据模型 (model
) 和标签宽度 (label-width
),这些属性通过组件的 props 传递进来。
在 el-row
元素中,使用了 v-for
指令来遍历 props.option
数组,该数组用于配置不同类型的搜索输入控件。
搜索按钮 (el-button
) 和重置按钮 (el-button
),它们分别绑定了 search
方法和 reset
方法。这些方法会在用户点击按钮时触发相应的事件。
<template>
<div>
<el-form ref="searchRef" :size="props.size" :model="searchVal" :label-width="props.labelWidth">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="props.span" v-for="(item, index) in props.option" :key="index" class="mb20">
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="props.span">
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset(searchRef)">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script lang='ts' setup>
import { onMounted, reactive, ref } from 'vue'
import type { ElForm } from 'element-plus'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/formatTime'
import type { Option } from '@/components/type.d.ts'
type FormInstance = InstanceType<typeof ElForm>
const searchRef = ref<FormInstance>()
// 搜索值
let searchVal: any = reactive({})
// 搜索
const search = () => {
for (let i in searchVal) {
if (searchVal[i] === undefined || searchVal[i] === '') {
delete searchVal[i]
}
}
emit('search', searchVal)
}
// 重置
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
emit('search', searchVal)
}
interface Props {
labelWidth?: number | string
size?: 'small' | 'default' | 'large'
span?: number
option: Option[]
defaultValue?: any
}
const props = withDefaults(defineProps<Props>(), {
size: 'default',
span: 8,
labelWidth: 100
})
</script>
声明了一个类型别名 FormInstance
,用于表示表单的实例。通过 ref
函数创建了一个响应式引用 searchRef
,用于引用表单实例。使用 reactive
函数创建了一个响应式对象 searchVal
,用于存储搜索的值。
定义 search
方法,该方法会在搜索按钮被点击时触发。遍历 searchVal
对象的属性,如果属性值为空或未定义,则从 searchVal
中删除该属性。最后,我们通过 emit
方法触发 search
的自定义事件,并将 searchVal
作为参数传递给父组件。定义了 reset
方法,该方法会在重置按钮被点击时触发。传入的表单实例调用 resetFields
方法,将表单重置为初始状态。最后,再次通过 emit
方法触发search
的自定义事件,并将 searchVal
作为参数传递给父组件。
接下来分析option中的数据,新建type.d.ts
//选择框的option类型
interface SelectOption {
label: string
value: any
}
//选择框和文本输入框的option类型
interface InputOptions {
prop: string
option: SelectOption[]
}
export interface Option {
label: string //标签名称
prop: string //表单项字段名
endProp?: string //日期范围的结束日期字段名
placeholder?: string //输入框占位文本
type?: 'input' | 'select' | 'cascader' | 'date' | 'datetime' | 'daterange' | 'inputSelect' //表单项类型
optionLabel?: string //选择框的label字段
optionValue?: string //选择框的value字段
option?: any[] //选择框option
inputOptions?: InputOptions //选择框和文本输入框的option
max?: number //级联选择器的最大选择数量
children?: string //级联选择器children字段名
multiple?: boolean //级联选择器是否多选
value?: string //级联选择器返回值字段名
itemLabel?: string //级联选择器选项名称
checkStrictly?: boolean //是否可选择父节点和子节点
emitPath?: boolean // 是否返回由该节点所在的各级菜单的值所组成的数组
show?: boolean //是否显示选中值的完整路径
filterable?: boolean //选择框是否可搜索
}
完善表单,根据实际需求,暴露出表单的配置项,完整代码如下
<template>
<div>
<el-form ref="searchRef" :size="props.size" :model="searchVal" :label-width="props.labelWidth">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="12" :lg="props.span" v-for="(item, index) in props.option" :key="index" class="mb20">
<el-form-item v-if="item.type == 'input' || !item.type" @keyup.enter="search" :prop="item.prop" :label="item.label">
<el-input v-model="searchVal[item.prop]" :placeholder="item.placeholder"></el-input>
</el-form-item>
<el-form-item v-if="item.type == 'inputSelect'" label-width="0" :prop="item.prop">
<el-input v-model="searchVal[item.prop]" clearable :placeholder="item.placeholder">
<template #prepend>
<el-select v-model="searchVal[item.inputOptions.prop]" :style="{ width: props.labelWidth + 'px' }">
<el-option v-for="(sItem, sIndex) in item.inputOptions.option" :key="sIndex" :label="sItem.label" :value="sItem.value"></el-option>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item v-else-if="item.type == 'select'" :prop="item.prop" :label="item.label">
<el-select v-model="searchVal[item.prop]" :filterable="item.filterable" :fit-input-width="true" style="width: 100%" :placeholder="item.placeholder">
<el-option v-for="(sItem, sIndex) in item.option" :key="sIndex" :label="sItem[item.optionLabel || 'label']" :value="sItem[item.optionValue || 'value']"></el-option>
</el-select>
</el-form-item>
<el-form-item v-else-if="item.type == 'cascader'" :prop="item.prop" :label="item.label">
<el-cascader
v-model="searchVal[item.prop]"
:options="item.option"
:placeholder="item.placeholder"
clearable
:props="{
value: item.value || 'value',
label: item.itemLabel || 'label',
children: item.children || 'children',
multiple: item.multiple || false,
emitPath: item.emitPath || false,
checkStrictly: item.checkStrictly || false
}"
:show-all-levels="item.show || false"
@change="(val: any) => changeType(val, item)"
/>
</el-form-item>
<el-form-item v-else-if="item.type == 'date'" :prop="item.prop" :label="item.label">
<el-date-picker
v-model="searchVal[item.prop]"
type="date"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
:placeholder="item.placeholder"
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item v-else-if="item.type == 'datetime'" :prop="item.prop" :label="item.label">
<el-date-picker
v-model="searchVal[item.prop]"
type="datetime"
value-format="YYYY-MM-DD HH:mm"
format="YYYY-MM-DD HH:mm"
:placeholder="item.placeholder"
style="width: 100%"
></el-date-picker>
</el-form-item>
<el-form-item v-else-if="item.type == 'daterange'" :prop="item.prop" :label="item.label">
<el-date-picker
v-model="searchVal[item.prop + 'Range']"
style="width: 100%"
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD"
format="YYYY-MM-DD"
@change="changeDateRange(item)"
>
</el-date-picker>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="props.span">
<el-form-item>
<el-button type="primary" @click="search">搜索</el-button>
<el-button @click="reset(searchRef)">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import type { ElForm } from 'element-plus'
import { ElMessage } from 'element-plus'
import { formatDate } from '@/utils/formatTime'
import type { Option } from '@/components/type.d.ts'
type FormInstance = InstanceType<typeof ElForm>
const searchRef = ref<FormInstance>()
// 搜索值
let searchVal: any = reactive({})
// 搜索
const search = () => {
for (let i in searchVal) {
if (searchVal[i] === undefined || searchVal[i] === '') {
delete searchVal[i]
}
}
emit('search', searchVal)
}
// 重置
const reset = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
emit('search', searchVal)
}
// 级联选择
const changeType = (value: any, data: Option) => {
if (!value) value = []
if (value.length > (data.max as number)) {
searchVal[data.prop] = value.slice(0, 4)
ElMessage.error('馆校类型数量限制为4个')
}
}
// 日期范围选择
const changeDateRange = (item: any) => {
let dateArr = searchVal[item.prop + 'Range']
if (dateArr) {
searchVal[item.prop] = formatDate(dateArr[0], 'YYYY-mm-dd')
searchVal[item.endProp] = formatDate(dateArr[1], 'YYYY-mm-dd')
} else {
searchVal[item.prop] = ''
searchVal[item.endProp] = ''
}
}
const emit = defineEmits(['search', 'reset'])
interface Props {
labelWidth?: number | string
size?: 'small' | 'default' | 'large'
span?: number
option: Option[]
defaultValue?: any
}
const props = withDefaults(defineProps<Props>(), {
size: 'default',
span: 8,
labelWidth: 100
})
onMounted(() => {
if (props.defaultValue) {
props.option.forEach((p: any) => {
if (p.type == 'daterange' && props.defaultValue[p.prop]) {
searchVal[p.prop + 'Range'] = []
if (props.defaultValue[p.prop]) {
searchVal[p.prop + 'Range'][0] = props.defaultValue[p.prop]
}
if (props.defaultValue[p.endProp]) {
searchVal[p.prop + 'Range'][1] = props.defaultValue[p.endProp]
}
}
})
searchVal = Object.assign(searchVal, props.defaultValue)
}
})
</script>
<style lang="scss" scoped></style>
实际使用,App.vue中
<template>
<div class="main">
<MySearch :option="searchOptions" :defaultValue="{ daterange: '', endDaterange: '' }" @search="search" />
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import MySearch from './components/mySearch.vue'
import type { Option } from '@/components/type.d.ts'
const cascaderOptions = [
{
value: 'guide',
label: 'Guide',
children: [
{
value: 'disciplines',
label: 'Disciplines',
children: [
{
value: 'consistency',
label: 'Consistency'
},
{
value: 'feedback',
label: 'Feedback'
},
{
value: 'efficiency',
label: 'Efficiency'
},
{
value: 'controllability',
label: 'Controllability'
}
]
},
{
value: 'navigation',
label: 'Navigation',
children: [
{
value: 'side nav',
label: 'Side Navigation'
},
{
value: 'top nav',
label: 'Top Navigation'
}
]
}
]
},
{
value: 'component',
label: 'Component',
children: [
{
value: 'basic',
label: 'Basic',
children: [
{
value: 'layout',
label: 'Layout'
},
{
value: 'color',
label: 'Color'
},
{
value: 'typography',
label: 'Typography'
},
{
value: 'icon',
label: 'Icon'
},
{
value: 'button',
label: 'Button'
}
]
},
{
value: 'form',
label: 'Form',
children: [
{
value: 'radio',
label: 'Radio'
},
{
value: 'checkbox',
label: 'Checkbox'
},
{
value: 'input',
label: 'Input'
},
{
value: 'input-number',
label: 'InputNumber'
},
{
value: 'select',
label: 'Select'
},
{
value: 'cascader',
label: 'Cascader'
},
{
value: 'switch',
label: 'Switch'
},
{
value: 'slider',
label: 'Slider'
},
{
value: 'time-picker',
label: 'TimePicker'
},
{
value: 'date-picker',
label: 'DatePicker'
},
{
value: 'datetime-picker',
label: 'DateTimePicker'
},
{
value: 'upload',
label: 'Upload'
},
{
value: 'rate',
label: 'Rate'
},
{
value: 'form',
label: 'Form'
}
]
},
{
value: 'data',
label: 'Data',
children: [
{
value: 'table',
label: 'Table'
},
{
value: 'tag',
label: 'Tag'
},
{
value: 'progress',
label: 'Progress'
},
{
value: 'tree',
label: 'Tree'
},
{
value: 'pagination',
label: 'Pagination'
},
{
value: 'badge',
label: 'Badge'
}
]
},
{
value: 'notice',
label: 'Notice',
children: [
{
value: 'alert',
label: 'Alert'
},
{
value: 'loading',
label: 'Loading'
},
{
value: 'message',
label: 'Message'
},
{
value: 'message-box',
label: 'MessageBox'
},
{
value: 'notification',
label: 'Notification'
}
]
},
{
value: 'navigation',
label: 'Navigation',
children: [
{
value: 'menu',
label: 'Menu'
},
{
value: 'tabs',
label: 'Tabs'
},
{
value: 'breadcrumb',
label: 'Breadcrumb'
},
{
value: 'dropdown',
label: 'Dropdown'
},
{
value: 'steps',
label: 'Steps'
}
]
},
{
value: 'others',
label: 'Others',
children: [
{
value: 'dialog',
label: 'Dialog'
},
{
value: 'tooltip',
label: 'Tooltip'
},
{
value: 'popover',
label: 'Popover'
},
{
value: 'card',
label: 'Card'
},
{
value: 'carousel',
label: 'Carousel'
},
{
value: 'collapse',
label: 'Collapse'
}
]
}
]
},
{
value: 'resource',
label: 'Resource',
children: [
{
value: 'axure',
label: 'Axure Components'
},
{
value: 'sketch',
label: 'Sketch Templates'
},
{
value: 'docs',
label: 'Design Documentation'
}
]
}
]
const searchOptions: Option[] = reactive([
{
label: '文本类型',
placeholder: '请输入文本',
type: 'input',
prop: 'input'
},
{
label: '选择框',
prop: 'select',
placeholder: '请选择支付方式',
type: 'select',
option: [
{
label: '微信',
value: 0
},
{
label: '支付宝',
value: 1
}
]
},
{
label: '文本选择框',
prop: 'inputSelect1',
placeholder: '请输入文本',
type: 'inputSelect',
inputOptions: {
prop: 'inputSelect2',
option: [
{
label: '微信',
value: 0
},
{
label: '支付宝',
value: 1
}
]
}
},
{
label: '级联选择器',
placeholder: '请选择级联选择器',
type: 'cascader',
prop: 'cascader',
option: cascaderOptions
},
{
label: '日期选择',
type: 'date',
prop: 'date',
placeholder: '请选择日期'
},
{
label: '日期时间',
type: 'datetime',
prop: 'datetime',
placeholder: '请选择日期时间'
},
{
label: '日期范围',
type: 'daterange',
prop: 'daterange',
endProp: 'endDaterange'
}
])
// 搜索
const search = (val: any) => {
tableData.searchForm = val
tableData.page.pageIndex = 1
initTableData()
}
//表格数据
const tableData: any = reactive({
data: [],
searchForm: {},
page: {
pageIndex: 1,
pageSize: 10,
total: 0
}
})
// 初始化表格数据
const initTableData = () => {
console.log(tableData.searchForm)
}
</script>
<style scoped>
.main {
padding: 20px;
}
</style>
总结:
实现了一个搜索组件,根据 props.option
数组动态生成不同类型的表单项,以便用户输入搜索条件。
- 代码中的
el-col
元素表示一个列布局,用于展示生成的表单项。 - 根据
props.option
数组的每一项item
,根据item.type
的不同选择不同的表单项类型进行渲染。 - 支持的表单项类型包括输入框、带下拉选择框的输入框、选择框、级联选择器、日期选择器和日期范围选择器。
- 每个表单项都绑定了相应的数据模型,通过
v-model
实现了数据的双向绑定。 - 表单项的标签、属性和占位符等信息都是根据
item
对象中的属性动态设置的。
这个搜索组件提供了一种灵活的方式来生成不同类型的表单项,以便用户输入搜索条件。通过配置 props.option
数组,可以定制化生成所需的表单项,并且支持不同的表单验证和交互方式。如果需要进一步定制和扩展,可以根据具体的需求修改和补充代码。
原文链接:https://juejin.cn/post/7340472228928012328 作者:优秀稳妥的JiaJi