创建条件搜索组件

190 阅读5分钟

简述

在后台管理中最常用的是表格,而表格的数据一般是需要一定的条件,因此封装这个条件搜索,条件搜索可以是输入框、选择框、时间选择、日期选择等等。

既然包含输入框、选择框、时间选择、日期选择组件,那么我们就需要简单封装一下输入框、选择框、时间选择、日期选择组件,需要对组件添加label

recording

输入框

开发一个兼容element plus所有的属性并添加label的功能

简单布局

首先显编写输入框的布局

<script setup name="eInp" lang="ts">
import { ref } from "vue";
​
let input = ref("");
</script><template>
  <div class="eInp">
    <div class="label">label</div>
    <el-input v-model="input" style="width: 240px" placeholder="Please input" />
  </div>
</template>
<style lang="scss" scoped>
.eInp {
  display: flex;
  align-items: center;
  width: 100%;
  .inpRow {
    flex: 1;
  }
}
</style>

image-20240717162537215

设置传递的参数

需要自定义label并且需要设置是否显示:label的宽度以及输入框中的占位符,并且需要再父组件传入,因此在组件中需要通过props接受

<script setup name="eInp" lang="ts">
import { ref } from "vue";
defineProps({
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
});
​
let input = ref("");
</script><template>
  <div class="eInp">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-input v-model="input" :placeholder />
  </div>
</template>
<style lang="scss" scoped>
.eInp {
  display: flex;
  align-items: center;
  width: 100%;
  .inpRow {
    flex: 1;
  }
}
</style>

设置双向绑定

接下来对组件添加v-model

<script setup name="eInp" lang="ts">
import { ref } from "vue";
defineProps({
  modelValue: {
    type: String,
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
});
​
let emit = defineEmits(["update:modelValue"]);
​
const changeData = val => {
  emit("update:modelValue", val);
};
</script><template>
  <div class="eInp">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-input :model-value="modelValue" :placeholder @input="changeData" />
  </div>
</template>
<style lang="scss" scoped>
.eInp {
  display: flex;
  align-items: center;
  width: 100%;
  .inpRow {
    flex: 1;
  }
}
</style>

添加失焦、获焦、清除事件

通过emit将事件传递给父组件,通过v-bind="$attrs"接受props中没有接受的参数

<script setup name="eInp" lang="ts">
import { ref } from "vue";
defineProps({
  modelValue: {
    type: String,
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
});
​
let emit = defineEmits(["update:modelValue", "blur", "focus", "clear"]);
// 绑定的数据
let inpText = ref<any>(null);
​
const changeData = val => {
  emit("update:modelValue", val);
};
const blurFn = () => {
  emit("blur", inpText.value);
};
const focusFn = () => {
  emit("focus", inpText.value);
};
const clearFn = () => {
  emit("clear", inpText.value);
};
</script><template>
  <div class="eInp">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-input
      v-bind="$attrs"
      :model-value="modelValue"
      :placeholder
      @input="changeData"
      @blur="blurFn"
      @focus="focusFn"
      @clear="clearFn"
    />
  </div>
</template>
<style lang="scss" scoped>
.eInp {
  display: flex;
  align-items: center;
  width: 100%;
  .inpRow {
    flex: 1;
  }
}
</style>

暴漏方法到父组件

有时候需要再父组件调用子组件的方法

<script setup name="eInp" lang="ts">
import { ref } from "vue";
defineProps({
  modelValue: {
    type: [String, Number],
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
});
​
let emit = defineEmits(["update:modelValue", "blur", "focus", "clear"]);
// 绑定的数据
let inpText = ref<any>(null);
​
const changeData = val => {
  emit("update:modelValue", val);
};
const blurFn = () => {
  emit("blur", inpText.value);
};
const focusFn = () => {
  emit("focus", inpText.value);
};
const clearFn = () => {
  emit("clear", inpText.value);
};
​
// 绑定的ref
let inpRef = ref();
// 组件获取失焦
const blur = () => {
  inpRef.value.blur();
};
// 组件清除数据
const clear = () => {
  inpRef.value.clear();
};
// 组件获取焦点
const focus = () => {
  inpRef.value.focus();
};
// 选择组件的文本
const select = () => {
  inpRef.value.select();
};
// 暴漏方法
defineExpose({ blur, clear, focus, select });
</script>
​
<template>
  <div class="eInp">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-input
      ref="inpRef"
      v-bind="$attrs"
      :model-value="modelValue"
      :placeholder
      @input="changeData"
      @blur="blurFn"
      @focus="focusFn"
      @clear="clearFn"
    />
  </div>
</template>
<style lang="scss" scoped>
.eInp {
  display: flex;
  align-items: center;
  width: 100%;
  .inpRow {
    flex: 1;
  }
}
</style>
​

recording

选择器

接下来开发选择器组件,同样需要设置labellabel的宽度等等,同时还需要传入下拉列表以及下拉列表的配置

<script setup name="eSelect" lang="ts">
import { ref } from "vue";
defineProps({
  modelValue: {
    type: [String, Number, Array, Object],
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
  // 列表
  options: {
    type: Array as () => any[],
    default: () => [],
  },
  // 选项对象
  optionObject: {
    type: Object,
    default: () => ({
      label: "label",
      value: "value",
      key: "key",
    }),
  },
});
​
let emit = defineEmits(["update:modelValue", "blur", "focus", "clear"]);
​
// 绑定的数据
let selectData = ref<any>(null);
​
const changeData = val => {
  selectData.value = val;
  emit("update:modelValue", val);
};
​
const blurFn = () => {
  emit("blur", selectData.value);
};
​
const focusFn = () => {
  emit("focus", selectData.value);
};
​
const clearFn = () => {
  emit("clear", selectData.value);
};
​
let eSelectRef = ref();
​
const focus = () => {
  eSelectRef.value.focus();
};
const blur = () => {
  eSelectRef.value.blur();
};
​
defineExpose({ blur, focus });
</script>
​
<template>
  <div class="eSelect">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-select
      :model-value="modelValue"
      :placeholder
      v-bind="$attrs"
      @focus="focusFn"
      @change="changeData"
      @blur="blurFn"
      @clear="clearFn"
      ref="eSelectRef"
      class="selectRow"
    >
      <el-option
        v-for="item in options"
        :key="item[optionObject.key]"
        :label="item[optionObject.label]"
        :value="item[optionObject.value]"
      />
    </el-select>
  </div>
</template>
<style lang="scss" scoped>
.eSelect {
  display: flex;
  align-items: center;
  width: 100%;
  .selectRow {
    flex: 1;
  }
}
</style>
​

时间选择

开发时间选择组件,配置和上面的基本上一样

<script setup name="timeSelect" lang="ts">
import { ref } from "vue";
defineProps({
  modelValue: {
    type: [String, Number, Array, Object],
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
});
​
let timeData = ref<any>(null);
​
let emit = defineEmits(["change", "blur", "focus", "clear"]);
​
const change = val => {
  timeData.value = val;
  emit("change", val);
};
​
const blurFn = () => {
  emit("blur", timeData.value);
};
const focusFn = () => {
  emit("focus", timeData.value);
};
const clearFn = () => {
  emit("clear", timeData.value);
};
​
let timeSelect = ref<any>(null);
​
const blur = () => {
  timeSelect.value.blur();
};
const focus = () => {
  timeSelect.value.focus();
};
const handleOpen = () => {
  timeSelect.value.handleOpen();
};
const handleClose = () => {
  timeSelect.value.handleClose();
};
defineExpose({
  blur,
  focus,
  handleOpen,
  handleClose,
});
</script>
​
<template>
  <div class="timeSelect">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-time-picker
      :model-value="modelValue"
      v-bind="$attrs"
      :placeholder
      @change="change"
      @blur="blurFn"
      @focus="focusFn"
      @clear="clearFn"
      ref="timeSelect"
      class="timeSelectRow"
    />
  </div>
</template>
<style lang="scss" scoped>
.timeSelect {
  display: flex;
  align-items: center;
  // justify-content: space-between;
  width: 100%;
  .timeSelectRow {
    flex: 1;
  }
}
</style>
​

日期选择

<script setup name="dateSelect" lang="ts">
import { ref, watch } from "vue";
let props = defineProps({
  modelValue: {
    type: [String, Number, Array, Object],
    default: "",
  },
  // label
  label: {
    type: String,
    default: "label",
  },
  // 是否显示冒号
  isColon: {
    type: Boolean,
    default: true,
  },
  // label width
  labelWidth: {
    type: Number,
    default: 60,
  },
  placeholder: {
    type: String,
    default: "请输入",
  },
  type: {
    type: String,
    default: "date",
  },
});
​
let dateSelect = ref<any>();
​
watch(
  () => props.modelValue,
  () => {
    dateSelect.value = props.modelValue;
  },
);
​
let emit = defineEmits(["update:modelValue", "blur", "focus", "clear"]);
​
const changeData = () => {
  emit("update:modelValue", dateSelect.value);
};
​
const blurFn = () => {
  emit("blur", dateSelect.value);
};
​
const focusFn = () => {
  emit("focus", dateSelect.value);
};
​
const clearFn = () => {
  emit("clear", dateSelect.value);
};
​
let dateSelectRef = ref();
​
const focus = () => {
  dateSelectRef.value.focus();
};
const handleOpen = () => {
  dateSelectRef.value.handleOpen();
};
const handleClose = () => {
  dateSelectRef.value.handleClose();
};
​
defineExpose({ focus, handleOpen, handleClose });
</script>
​
<template>
  <div class="dateSelect">
    <div class="label" :style="{ width: labelWidth + 'px !important' }">
      {{ label }}{{ isColon ? ":" : "" }}
    </div>
    <el-date-picker
      v-model="dateSelect"
      :placeholder
      :type
      @change="changeData"
      @focus="focusFn"
      @blur="blurFn"
      @clear="clearFn"
      ref="dateSelectRef"
      class="dateSelectRow"
    />
  </div>
</template>
<style lang="scss" scoped>
.dateSelect {
  display: flex;
  align-items: center;
  width: 100%;
  .dateSelectRow {
    flex: 1;
  }
}
</style>
​

条件搜索组件

组件配置

在这个组件中我们需要通过一个配置项来生成条件搜索组件,在这个配置中通过某个字段来决定渲染那个组件

配置代码如下:

let dispositionList = ref([
  {
    type: "eInp", // 组件名称  组件类型
    field: "name", // 需要设置的字段
    // 其他参数  子组件的参数
    attribute: {
      placeholder: "请输入账号",
      label: "账号",
    },
  },
  {
    type: "eSelect",
    field: "sex",
    label: "性别",
    attribute: {
      placeholder: "请选择性别",
      label: "性别",
    },
    // 下拉选项
    options: [
      {
        label: "男",
        value: "1",
      },
      {
        label: "女",
        value: "2",
      },
    ],
    // 下拉选项配置
    optionObject: {
      label: "label",
      value: "value",
      key: "value",
    },
  },
  {
    type: "timeSelect",
    field: "time",
    attribute: {
      label: "时间",
      placeholder: "请选择时间",
    },
  },
  {
    type: "dateSelect",
    field: "dateSelect",
    attribute: {
      label: "日期",
      placeholder: "请选择日期",
    },
  },
]);

动态渲染组件

使用动态组件渲染

<template v-for="(item, index) in dispositionList" :key="index">
      <component
        class="item"
        :is="item.type"
        v-model="formData[item.field]"
        :options="item.options"
        :optionObject="item.optionObject"
        v-bind="item.attribute"
        :style="{ width: itemWidth + 'px !important', height: itemHeight + 'px !important' }"
      ></component>
    </template>

这样基本上就完成了 需要注意的是配置项要和组件进行匹配

插槽

既然是搜索组件,那么就需要显示搜索取消按钮,但是也有可能是需要在使用的时候传递,因此接下来编写插槽部分

如果没有传递内容,就使用插槽

<div class="operateRow">
      <!-- 使用插槽 -->
      <slot>
        <div class="operateRowDefault" v-if="isModelValue">
          <el-button v-if="searchBtn" v-bind="searchOpt" @click="search">{{
            searchText
          }}</el-button>
          <el-button v-if="cancelBtn" v-bind="cancelOpt" @click="cancel">{{
            cancelText
          }}</el-button>
        </div>
      </slot>
    </div>

事件

接下来开始编写事件,点击按钮的时候需要将数据返回到父组件或清除数据,还有一种形式是不需要按钮触发,在数据发生变化的时候直接触发,那么就需要监听绑定的数据,当数据发生变化的时候就返回到父组件

let emit = defineEmits(["update:modelValue", "search", "cancel"]);

let formData = ref({});

watch(
  () => formData.value,
  val => {
    // console.log("双向绑定", val);
    emit("update:modelValue", val);
  },
  {
    deep: true,
  },
);

// 搜索按钮
const search = () => {
  // console.log("查询", formData.value);
  emit("search", JSON.parse(JSON.stringify(formData.value)));
};
// 取消按钮
const cancel = () => {
  formData.value = {};
  // console.log("取消", formData.value);
  emit("cancel", {});
};

// 清除数据
const clear = () => {
  formData.value = {};
  emit("cancel", {});
};

总结

经过以上条件搜索组件就开发完成了,如果需要筛选项添加对应的组件就好了

搜索组件代码

<script setup name="conditionalSearch">
import { ref, watch } from "vue";

defineProps({
  itemWidth: {
    type: Number,
    default: 200,
  },
  itemHeight: {
    type: Number,
    default: 32,
  },
  // 搜索按钮
  searchBtn: {
    type: Boolean,
    default: true,
  },
  searchText: {
    type: String,
    default: "搜索",
  },
  // 搜索按钮配置
  searchOpt: {
    type: Object,
    default: () => ({
      type: "primary",
    }),
  },
  // 取消按钮
  cancelBtn: {
    type: Boolean,
    default: true,
  },
  cancelText: {
    type: String,
    default: "取消",
  },
  // 取消按钮配置
  cancelOpt: {
    type: Object,
    default: () => ({}),
  },
  // 组件配置
  dispositionList: {
    type: Array,
    default: () => [],
  },
  // 是否双向绑定
  isModelValue: {
    type: Boolean,
    default: true,
  },
});

let emit = defineEmits(["update:modelValue", "search", "cancel"]);

let formData = ref({});

watch(
  () => formData.value,
  val => {
    // console.log("双向绑定", val);
    emit("update:modelValue", val);
  },
  {
    deep: true,
  },
);

// 搜索按钮
const search = () => {
  // console.log("查询", formData.value);
  emit("search", JSON.parse(JSON.stringify(formData.value)));
};
// 取消按钮
const cancel = () => {
  formData.value = {};
  // console.log("取消", formData.value);
  emit("cancel", {});
};

// 清除数据
const clear = () => {
  formData.value = {};
  emit("cancel", {});
};

// 暴漏方法
defineExpose({ clear });
</script>

<template>
  <div class="conditionalSearch">
    <template v-for="(item, index) in dispositionList" :key="index">
      <component
        class="item"
        :is="item.type"
        v-model="formData[item.field]"
        :options="item.options"
        :optionObject="item.optionObject"
        v-bind="item.attribute"
        :style="{ width: itemWidth + 'px !important', height: itemHeight + 'px !important' }"
      ></component>
    </template>
    <div class="operateRow">
      <!-- 使用插槽 -->
      <slot>
        <div class="operateRowDefault" v-if="isModelValue">
          <el-button v-if="searchBtn" v-bind="searchOpt" @click="search">{{
            searchText
          }}</el-button>
          <el-button v-if="cancelBtn" v-bind="cancelOpt" @click="cancel">{{
            cancelText
          }}</el-button>
        </div>
      </slot>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.conditionalSearch {
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
  flex-wrap: wrap;
  position: relative;
  .item {
    margin: 5px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    text-align: center;
  }
  .operateRow {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 5px;
    margin: 5px;
    box-sizing: border-box;
    position: absolute;
    right: 0;
    bottom: 0;
    margin-bottom: 0;
  }
}
</style>

recording