前言:在写后台管理系统的时候,查询功能是一个十分常见的功能,像文本输入查询,下拉选项框查询,日期选项查询等。但如果查询条件一旦多了起来,那么界面会变臃肿且难看。所以就需要一个复合查询框。
问题
开发中有时候查询条件过多,类似于下图
我们会发现,单单是查询区域就几乎占据了一半的界面,一般这样数据展示界面,我们需要给表格更多的区域,让用户一次查看到更多数据,而且因为查询的组件不同,例如input属性里面的text和textarea所占据的区域又会不一样,导致查询区域的样式变动复杂。
解决办法
- 最简单粗暴就是给查询区域增加控制显隐按钮,在表格上方的操作栏增加多一个按钮(是否展示查询区域)。默认隐藏,这样页面一开始就会给表格足够高的高度,展示更多的数据。不过这种方法意义不大。
- 在侧边展示查询区域。并附带显隐功能,这样无论如何表格数据都是固定的高度,能够展示较多数据,需要查询的时候就显示查询区域。这种是比较常用的方法之一。
3.今天主要说明的就是将查询条件符合在一个输入框。仿某通后台管理系统的查询功能
组件编写
界面编写
首先需要编写一个简单的下拉框组件,这里我就不一一细说,主要就是样式代码。
<template>
<div class="vue-amazing-selector">
<el-row :gutter="20">
<el-col :span="24">
<div
:class="['m-select-wrap', { 'hover focus': !props.disabled, disabled: props.disabled }]"
:style="`width: ${props.width}px;`"
tabindex="0"
@click="openSelect()"
>
<div
:class="['u-select-input', { placeholder: !selectedName }]"
:style="`line-height: ${props.height - 10}px;width: ${props.width - 37}px;min-height:${props.height}px`"
>
<span v-for="(item, index) in selectedName" :key="index">
<el-tag
v-if="item[1]"
type="info"
closable
@close="tagClose(item, index)"
style="padding: 0 10px; margin: 5px 5px"
>
{{ item[1] ? `${item[0]}:` : '' }}<span style="color: black">{{ item[1] ? item[1] : '' }}</span>
</el-tag></span
>
<span v-if="!selectedName.length" class="u-select-input">{{ props.placeholder }}</span>
</div>
<svg
@click="openSelect"
:class="['triangle', { rotate: showOptions }]"
viewBox="64 64 896 896"
data-icon="down"
aria-hidden="true"
focusable="false"
class=""
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
></path>
</svg>
</div>
<transition name="fade">
<div
v-show="showOptions"
class="m-options-panel"
@mouseleave="onLeave"
:style="`line-height: ${props.height - 12}px; max-height: ${props.num * (props.height - 3)}px; width: ${props.width}px;`"
style="min-height: 50px;"
>
</div>
</transition>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue'
const props = defineProps({
search: {
//下拉框配置
type: Array,
default: () => []
},
searchOption: {
//下拉框字典值
type: Object,
default: () => {}
},
placeholder: {
// 下拉框默认文字
type: String,
default: '请选择'
},
disabled: {
// 是否禁用下拉
type: Boolean,
default: false
},
width: {
// 下拉框宽度
type: Number,
default: 400
},
height: {
// 下拉框高度
type: Number,
default: 36
},
num: {
// 下拉面板最多能展示的下拉项数,超过后滚动显示,修改样式之后值减半
type: Number,
default: 6
}
})
const showOptions = ref(false)
const selectedName=ref([])
const openSelect = () => {
showOptions.value = !showOptions.value
}
</script>
<style lang="scss" scoped>
input:focus {
outline: none;
}
input,
p {
margin: 0;
padding: 0;
}
.searchLabel {
// position: relative;
// bottom: 10px;
width: 100px;
display: inline-block;
line-height: 40px;
}
.vue-amazing-selector {
position: relative;
display: inline-block;
font-size: 14px;
font-weight: 400;
color: rgba(0, 0, 0, 0.65);
line-height: 0px;
}
// 渐变过渡效果
.fade-enter-active,
.fade-leave-active {
transition: all 0.5s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
// transform: translateY(-6px); // 滑动变换
}
.m-select-wrap {
position: relative;
display: inline-block;
border: 1px solid #d9d9d9;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
.u-select-input {
display: block;
text-align: left;
margin-left: 11px;
margin-right: 24px;
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
}
.placeholder {
color: #bfbfbf;
}
.triangle {
position: absolute;
top: 50%;
right: 12px;
width: 12px;
height: 12px;
fill: rgba(0, 0, 0, 0.25);
transform: translateY(-50%);
-webkit-transform: translateY(-50%);
transition: all 0.3s ease-in-out;
pointer-events: none;
}
.rotate {
transform: translateY(-50%) rotate(180deg);
-webkit-transform: translateY(-50%) rotate(180deg);
}
}
.hover {
// 悬浮时样式
&:hover {
border-color: #1890ff;
}
}
.focus {
// 激活时样式
&:focus {
// 需设置元素的tabindex属性
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 20%);
}
}
.disabled {
// 下拉禁用样式
color: rgba(0, 0, 0, 0.25);
background: #f5f5f5;
user-select: none;
cursor: not-allowed;
}
.m-options-panel {
position: absolute;
z-index: 999;
overflow: auto;
background: #fff;
padding: 4px 0;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 15%);
width: 1000px;
// margin-left: 100px;
margin: 0;
.u-option {
// 下拉项默认样式
text-align: left;
position: relative;
display: block;
padding: 5px 12px;
font-weight: 400;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
cursor: pointer;
transition: background 0.3s ease;
}
.option-selected {
// 被选中的下拉项样式
font-weight: 600;
background: #fafafa;
}
.option-hover {
// 悬浮时的下拉项样式
background: #e6f7ff;
}
.option-disabled {
// 禁用某个下拉选项时的样式
color: rgba(0, 0, 0, 0.25);
user-select: none;
cursor: not-allowed;
}
}
</style>
大概界面就这样,有了基本下拉框的动画和hover颜色改变。接下来就是对下拉区域里面进行编写。
从上面要实现的图片看出组件是基本是两列来排布,有些是占据一整列,那么我们就可以通过用el-form来进行编写。
数据格式
编写的时候我们要思考,首先,el-form里面可能有select input date等,位置不明,组件类型不明,组件占据列数不明,所以我们需要进行动态配置。那么传递给下拉区域的参数格式可以是这样:
/*
label:展示的标签
value:传递的值
type:组件类型(input,textarea,select,date)
multiple:当type=select时,true为多选,默认为false
props:当type=select时,字典的属性配置,即options的props配置
dateType:当type=date时,该属性决定日期选择框的类型(类型参考饿了么官网)
format:当type=format时,决定日期时间的格式
*/
search: [
{
label: "商家名称",
value: "callingImGroupName",
type: "input",
},
{
label: "运单编号",
value: "orderCode",
type: "textarea",
},
{
label: "转人工原因",
value: "transferReason",
type: "select",
multiple:true
},
{
label: "处置状态",
value: "disposalStatus",
type: "select",
props:{
label:'dictValue',
value:'dictKey'
}
},
],
当我们决定好数据格式之后,其实组件编写就很清晰了。首先通过循环整个数据,根据类型来判断这个区域需要展示的组件是什么,然后根据其他属性配置是否有无,有则按照特定数据配置,无则依据组件默认值配置。
//在transition标签里面增加如下代码
<div
v-show="showOptions"
class="m-options-panel"
@mouseleave="onLeave"
:style="`line-height: ${height - 12}px;width: ${width}px;`"
>
<el-form :model="formInline" inline label-width="120px">
<template v-for="(item, index) in search">
<el-form-item :key="index" :label="item.label">
<el-input
v-if="item.type === 'input'"
style="width: 350px"
size="mini"
clearable
v-model="formInline[item.value]"
:placeholder="`请输入${item.label}`"
//因为没有预先在data定义变量,需要利用$set来进行响应式
@input="inputChange($event, item.value)"
></el-input>
<el-select
v-else-if="item.type === 'select'"
style="width: 350px"
size="mini"
v-model="formInline[item.value]"
:placeholder="`请选择${item.label}`"
filterable
clearable
:multiple="item.multiple"
:collapse-tags="item.multiple"
@change="
handleChange($event, item.value, 'select', item.props)
"
>
<el-option
v-for="op in searchOption[item.value]"
:key="op.key"
:label="item.props ? op[item.props.label] : op.label"
:value="item.props ? op[item.props.value] : op.value"
>
</el-option>
</el-select>
<el-date-picker
v-else-if="item.type === 'date'"
size="mini"
style="width: 350px"
v-model="formInline[item.value]"
:type="item.dateType || 'datetimerange'"
:value-format="item.format || 'yyyy-MM-dd HH:mm:ss'"
:picker-options="pickerOptions"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束结束"
:default-time="['00:00:00', '23:59:59']"
align="right"
@change="handleChange($event, item.value, 'date')"
>
</el-date-picker>
</el-form-item>
</template>
<el-form-item label="" style="width: 600px; padding-left: 120px">
<el-button
type="primary"
size="mini"
icon="el-icon-search"
@click="handleSearch"
>查询</el-button
>
<el-button
icon="el-icon-refresh"
type="primary"
size="mini"
@click="handleRest"
>重置</el-button
>
</el-form-item>
</el-form>
</div>
//父组件
<template>
<searchVue :search="search" :searchOption="searchOption" :width="1000" :height="30"></searchVue>
</template>
<script setup>
import searchVue from './search.vue'
import { ref } from 'vue'
//下拉框字典值
const searchOption = ref({})
const search = ref([
{
label: '商家名称',
value: 'callingImGroupName',
type: 'input'
},
{
label: '运单编号',
value: 'orderCode',
type: 'input'
},
{
label: '转人工原因',
value: 'transferReason',
type: 'select',
multiple: true,
props: {
label: 'dictValue',
value: 'dictKey'
}
},
{
label: '处置状态',
value: 'disposalStatus',
type: 'select',
props: {
label: 'dictValue',
value: 'dictKey'
}
},
{
label: '高价值',
value: 'ifValuable',
type: 'select'
},
{
label: '问题类型',
value: 'processType',
type: 'select',
props: {
label: 'dictValue',
value: 'dictKey'
}
},
{
label: '创建人',
value: 'createUser',
type: 'select',
props: {
label: 'name',
value: 'roleId'
}
},
{
label: '分配',
value: 'assignedUser',
type: 'select'
},
{
label: '受理时间',
value: 'accept',
type: 'date'
},
{
label: '修改时间',
value: 'modification',
type: 'date'
},
{
label: '综合搜索',
value: 'all',
type: 'input'
}
])
</script>
我们可以看到界面基本完成,然后接下来需要做的就是数据的显示,在显示的
input区域里,我们需要展示的类似于标签的样式来进行分组。首先我们把选项的数据模拟出来再进行展示。
//下拉框字典值
const searchOption = ref({
"processType": [
{
"id": "17",
"dictKey": "ChangeAdd",
"dictValue": "改地址",
},
{
"id": "18",
"dictKey": "ChangeNum",
"dictValue": "改电话",
},
{
"id": "19",
"dictKey": "Push",
"dictValue": "催件",
},
{
"id": "20",
"dictKey": "CheckWgt",
"dictValue": "查重量",
},
{
"id": "21",
"dictKey": "ReturnNwk",
"dictValue": "退回网点",
},
{
"id": "22",
"dictKey": "ReturnAdder",
"dictValue": "退回寄件人",
},
{
"id": "23",
"dictKey": "NotRev",
"dictValue": "签收未收到",
},
{
"id": "51",
"dictKey": "receiveException",
"dictValue": "收件异常",
}
],
"disposalStatus": [
{
"id": "305036671949238273",
"dictKey": "undo",
"dictValue": "未处理",
},
{
"id": "1605165908435333122",
"dictKey": "autoFollow",
"dictValue": "自动化跟进中",
},
{
"id": "1585911111626715137",
"dictKey": "autoDone",
"dictValue": "自动化已完成",
},
{
"id": "1585911043695767554",
"dictKey": "toAgent",
"dictValue": "转人工",
},
{
"id": "305036671949238274",
"dictKey": "doing",
"dictValue": "处理中",
},
{
"id": "305036671949238275",
"dictKey": "done",
"dictValue": "处理完成",
},
{
"id": "1585911151329996801",
"dictKey": "toBeVerify",
"dictValue": "待核实",
},
{
"id": "305036671949238276",
"dictKey": "vitiation",
"dictValue": "无效",
}
],
"createUser": [
{
"id": "1493460106072633346",
"name": "admin",
"roleId": "1510142099646943254",
},
{
"id": "1511520904500318209",
"name": "测试管理者",
"roleId": "1510142099646943234",
},
{
"id": "1512041404334268418",
"name": "dizzy",
"roleId": "1123598816738675201",
},
{
"id": "1512324249559080962",
"name": "read",
"roleId": "1493465905587105794",
}
],
"transferReason": [
{
"id": "303903700819144705",
"dictKey": "SystemProcessingFailed",
"dictValue": "系统处理失败",
},
{
"id": "1585904155021209601",
"dictKey": "ManualTransfer",
"dictValue": "座席手动转人工",
},
{
"id": "1585904596824027137",
"dictKey": "QuestionTypeChanged",
"dictValue": "二次咨询类型变更转人工",
},
{
"id": "1585907252586016769",
"dictKey": "LoginFailed",
"dictValue": "账号登录失败转人工",
},
]
})
逻辑处理
在展示之前我们需要知道查询条件的数据格式是如何的,所以现在查询按钮增加一个打印事件。
/**
* 搜索框查询事件
*/
const handleSearch=()=>{
console.log(formInline.value)
}
点击查询之后得到数据格式可知,如果是传递给后端当做查询条件是没有问题的,但是我们需要展示在界面上,key的值需要转换为中文才可。所以我们需要处理以下有关选项框的值。
/**
* 下拉框选项change事件
* @param value 源事件,当前选中的值
* @param key 用于查找该下拉框所用是哪个字典对象
* @param type 下拉框的类型
* @param config 下拉框props的配置,有些不是默认的label和value,在label和value替换时用到
*/
const handleChange = (value, key, type, config) => {
//不可总体拷贝,否则修改完的值会在下一次change事件触发又变为key值
JSON.stringify(showForm.value) === '{}'
? (showForm.value = JSON.parse(JSON.stringify(formInline.value)))
: (showForm.value[key] = JSON.parse(JSON.stringify(formInline.value[key])))
//如果是下拉选择框,将单选和多选都转换为数组形式进行处理
if (type === 'select') {
Array.isArray(showForm.value[key]) ? null : (showForm.value[key] = showForm.value[key].split())
showForm.value[key].map((item, index) => {
props.searchOption[key].map((x) => {
if (x[config ? config.value : 'value'] == item) {
// showForm.value[key][index] = x.dictValue;
showForm.value[key].splice(index, 1, x[config ? config.label : 'label'])
}
})
})
}
//如果是日期时间框,进行处理,有些接口参数需要处理可在下面函数里面进行处理
if (type === 'date') {
}
console.log('showForm',showForm.value)
}
由上图可知,我们将value值转换为了中文,那么剩下就是将对应的key值转换为中文,然后就进行显示即可。因为每次选择或者输入我们都要进行处理,所以我们需要使用watch监听showForm这个对象,这个对象是用于展示,showForm改变时进行key的替换操作。
watch(
() => showForm,
() => {
let objArr = Object.entries(showForm.value)
objArr.map((x) => {
props.search.map((item) => {
if (item.value == x[0]) {
x[0] = item.label
}
})
if (Array.isArray(x[1])) {
// console.log('x[1]',x[1])
x[1] = x[1].join(',')
}
})
selectedName.value = objArr
console.log('selectedName', selectedName.value)
},
// 需要深度监听
{ deep: true }
)
这时候界面展示成功了一半,接下来就是文本输入框的处理,这里就是简单赋值操作。
const inputChange = (e, key) => {
showForm.value[key] = e
}
现在展示效果就完成一大半了,接下来要注意到,在下拉区域里面删除的话,展示区域会跟着不见,但是展示区域的标签关闭却没效果,所以我们需要让二者联动起来。在
el-tag标签的close事件中编写。
/**
* 标签关闭事件
*/
const tagClose = (item, index) => {
selectedName.value.splice(index, 1)
let key = Object.keys(formInline.value)[index]
delete showForm.value[key]
delete formInline.value[key]
}
最后我们需要考虑当鼠标焦点不在查询区域的时候,查询区域应该收起来,这里一开始考虑到了在body添加事件进行监听,然后阻止冒泡,最后发现不太行。后来想着利用ref来通过父组件控制子组件属性来操作,但是感觉过于麻烦。最后在全局添加监听事件通过判断class类名来进行控制显隐。
mounted() {
document.addEventListener("click", (e) => {
let arr = ['u-select-input','el-input__inner','el-form el-form--inline','el-form-item']
if (arr.indexOf(e.target.className)===-1) {
this.showOptions = false;
}
});
},
最终效果如下图,基本这样算满足要求,如果有更多的业务需求可以以此为基础再修改。
最后附带上完整script代码
//search.vue
<script setup>
import { ref, defineProps, watch } from 'vue'
const props = defineProps({
search: {
//下拉框配置
type: Array,
default: () => []
},
searchOption: {
//下拉框字典值
type: Object,
default: () => {}
},
placeholder: {
// 下拉框默认文字
type: String,
default: '请选择'
},
disabled: {
// 是否禁用下拉
type: Boolean,
default: false
},
width: {
// 下拉框宽度
type: Number,
default: 400
},
height: {
// 下拉框高度
type: Number,
default: 36
},
num: {
// 下拉面板最多能展示的下拉项数,超过后滚动显示,修改样式之后值减半
type: Number,
default: 6
}
})
const showOptions = ref(false)
const selectedName = ref([])
const formInline = ref({})
const showForm = ref({})
watch(
() => showForm,
() => {
let objArr = Object.entries(showForm.value)
objArr.map((x) => {
props.search.map((item) => {
if (item.value == x[0]) {
x[0] = item.label
}
})
if (Array.isArray(x[1])) {
// console.log('x[1]',x[1])
x[1] = x[1].join(',')
}
})
selectedName.value = objArr
console.log('selectedName', selectedName.value)
},
// 需要深度监听
{ deep: true }
)
const openSelect = () => {
showOptions.value = !showOptions.value
}
/**
* 搜索框查询事件
*/
const handleSearch = () => {
console.log(formInline.value)
}
/**
* 下拉框选项change事件
* @param value 源事件,当前选中的值
* @param key 用于查找该下拉框所用是哪个字典对象
* @param type 下拉框的类型
* @param config 下拉框props的配置,有些不是默认的label和value,在label和value替换时用到
*/
const handleChange = (value, key, type, config) => {
//不可总体拷贝,否则修改完的值会在下一次change事件触发又变为key值
//vue2需要使用到$set,如果是空对象则深拷贝formInline,如果不是,则判断showForm是否有这个属性值,如果有则重新赋值,如果没有就用$set进行动态赋值
// JSON.stringify(showForm.value) === '{}'
// ? (showForm.value = JSON.parse(JSON.stringify(formInline.value)))
// : showForm.value.hasOwnProperty(key)
// ? (showForm.value[key] = JSON.parse(JSON.stringify(formInline.value[key])))
// : this.$set(showForm.value, key, formInline.value[key])
JSON.stringify(showForm.value) === '{}'
? (showForm.value = JSON.parse(JSON.stringify(formInline.value)))
: (showForm.value[key] = JSON.parse(JSON.stringify(formInline.value[key])))
//如果是下拉选择框,将单选和多选都转换为数组形式进行处理
if (type === 'select') {
Array.isArray(showForm.value[key]) ? null : (showForm.value[key] = showForm.value[key].split())
showForm.value[key].map((item, index) => {
props.searchOption[key].map((x) => {
if (x[config ? config.value : 'value'] == item) {
// showForm.value[key][index] = x.dictValue;
showForm.value[key].splice(index, 1, x[config ? config.label : 'label'])
}
})
})
}
//如果是日期时间框,进行处理,有些接口参数需要处理可在下面函数里面进行处理
if (type === 'date') {
}
console.log('showForm', showForm.value)
}
const inputChange = (e, key) => {
showForm.value[key] = e
}
/**
* 标签关闭事件
*/
const tagClose = (item, index) => {
selectedName.value.splice(index, 1)
let key = Object.keys(formInline.value)[index]
delete showForm.value[key]
delete formInline.value[key]
}
</script>
总结:关于多种条件的查询组件编写就到这了,因为是小白,所以请勿要求我的代码质量,希望各位大佬多多给点意见。如果这篇文章能帮助到你,请给小弟点个赞(* ̄︶ ̄)。