vue3+elementPlus 二次封装表单

3,966 阅读3分钟

功能

  • Input输入框

  • autocomplete自动补齐输入框

  • radio 单选框

  • checkbox 复选框

  • date 日期选择框

  • select 下拉框

  • 如需添加更多功能参考elementPlus或者根据业务需求自行自定义组件

效果图

image.png

目录结构

image.png

input

<template>
  <el-input
    v-bind="$attrs"
    v-model="modelValue"
    w-full
    @blur="props.blur ? props.blur($event) : false"
    @focus="props.focus ? props.focus($event) : false"
    @change="props.change ? props.change($event) : false"
    @input="props.input ? props.input($event) : false"
    @clear="props.clear ? props.clear() : false"
  />
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: String,
    default: () => "",
  },
  blur: {
    type: Function,
    default: () => () => {},
  },
  focus: {
    type: Function,
    default: () => () => {},
  },
  change: {
    type: Function,
    default: () => () => {},
  },
  input: {
    type: Function,
    default: () => () => {},
  },
  clear: {
    type: Function,
    default: () => () => {},
  },
});

const modelValue = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    modelValue.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", modelValue);
</script>
<style lang="scss" scoped></style>

select

<template>
  <el-select
    v-model="modelValue"
    v-bind="$attrs"
    w-full
    @change="props.change ? props.change($event) : false"
    @visible-change="props.visibleChange ? props.visibleChange($event) : false"
    @remove-tag="props.removeTag ? props.removeTag($event) : false"
    @clear="props.clear ? props.clear() : false"
    @blur="props.blur ? props.blur($event) : false"
    @focus="props.focus ? props.focus($event) : false"
  >
    <el-option
      v-for="item in options"
      :key="item[valueFiled]"
      :label="item[labelFiled]"
      :value="item[valueFiled]"
    ></el-option>
  </el-select>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: [String, Array],
    default: () => "",
  },
  options: {
    type: Array as any,
    default: () => [],
  },
  valueFiled: {
    type: String,
    default: "value",
  },
  labelFiled: {
    type: String,
    default: "label",
  },
  change: {
    type: Function,
    default: () => () => {},
  },
  visibleChange: {
    type: Function,
    default: () => () => {},
  },
  removeTag: {
    type: Function,
    default: () => () => {},
  },
  clear: {
    type: Function,
    default: () => () => {},
  },
  blur: {
    type: Function,
    default: () => () => {},
  },
  focus: {
    type: Function,
    default: () => () => {},
  },
});

const modelValue = ref(props.modelValue);

//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    modelValue.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", modelValue);
</script>
<style lang="scss" scoped></style>

autocomplete

<template>
  <el-autocomplete
    v-bind="$attrs"
    v-model="modelValue"
    style="width: 100%"
    @select="props.select ? props.select($event) : false"
    @change="props.change ? props.change($event) : false"
  />
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: String,
    default: () => "",
  },
  select: {
    type: Function,
    default: () => () => {},
  },
  change: {
    type: Function,
    default: () => () => {},
  },
});

const modelValue = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    modelValue.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", modelValue);
</script>

date

<template>
  <el-date-picker
    v-model="Val"
    v-bind="$attrs"
    style="width: 100%"
    @change="props.change ? props.change($event) : false"
    @blur="props.blur ? props.blur($event) : false"
    @focus="props.focus ? props.focus($event) : false"
    @calendar-change="props.calendarChange ? props.calendarChange($event) : false"
    @panel-change="(a, b, c) => (props.panelChange ? props.panelChange(a, b, c) : false)"
    @visible-change="props.visibleChange ? props.visibleChange($event) : false"
  ></el-date-picker>
</template>

<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: [String, Array, Date],
    default: () => "",
  },
  change: {
    type: Function,
    default: () => () => {},
  },
  blur: {
    type: Function,
    default: () => () => {},
  },
  focus: {
    type: Function,
    default: () => () => {},
  },
  calendarChange: {
    type: Function,
    default: () => () => {},
  },
  panelChange: {
    type: Function,
    default: () => () => {},
  },
  visibleChange: {
    type: Function,
    default: () => () => {},
  },
});

const Val = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    Val.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", Val);
</script>

checkbox

<template>
  <el-checkbox-group
    v-model="Val"
    v-bind="$attrs"
    @change="props.change ? props.change($event) : false"
  >
    <template v-if="props.cType === 'button'">
      <el-checkbox-button
        v-for="item in options"
        :key="item[valueFiled]"
        :value="item[valueFiled]"
        >{{ item[labelFiled] }}</el-checkbox-button
      >
    </template>
    <template v-else>
      <el-checkbox
        v-for="item in options"
        :key="item[valueFiled]"
        :value="item[valueFiled]"
        :border="props.cType === 'border'"
        >{{ item[labelFiled] }}</el-checkbox
      >
    </template>
  </el-checkbox-group>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: Array,
    default: () => "",
  },
  options: {
    type: Array as any,
    default: () => [],
  },
  valueFiled: {
    type: String,
    default: "value",
  },
  labelFiled: {
    type: String,
    default: "label",
  },
  cType: {
    type: String,
    default: "",
  },
  change: {
    type: Function,
    default: () => () => {},
  },
});

const Val = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    Val.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", Val);
</script>

radio

<template>
  <el-radio-group
    v-model="modelValue"
    v-bind="$attrs"
    @change="props.change ? props.change($event) : false"
  >
    <template v-if="props.cType === 'button'">
      <el-radio-button v-for="item in options" :key="item[valueFiled]" :value="item[valueFiled]">{{
        item[labelFiled]
      }}</el-radio-button>
    </template>
    <template v-else>
      <el-radio
        v-for="item in options"
        :border="props.cType === 'border'"
        :key="item[valueFiled]"
        :value="item[valueFiled]"
        >{{ item[labelFiled] }}</el-radio
      >
    </template>
  </el-radio-group>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: [Number, String, Boolean],
    default: () => "",
  },
  options: {
    type: Array as any,
    default: () => [],
  },
  valueFiled: {
    type: String,
    default: "value",
  },
  labelFiled: {
    type: String,
    default: "label",
  },
  cType: {
    //radio类型:button/border
    type: String,
    default: "",
  },
  change: {
    type: Function,
    default: () => () => {},
  },
});

const modelValue = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    modelValue.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", modelValue);
</script>

cascader

<template>
  <el-cascader
    v-bind="$attrs"
    v-model="modelValue"
    style="width: 100%"
    @change="props.change ? props.change($event) : false"
    @expand-change="props.expandChange ? props.expandChange($event) : false"
  />
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: {
    type: Array,
    default: () => [],
  },
  change: {
    type: Function,
    default: () => () => {},
  },
  expandChange: {
    type: Function,
    default: () => () => {},
  },
});

const modelValue = ref(props.modelValue);
//监听父组件的值
watch(
  () => props.modelValue,
  () => {
    modelValue.value = props.modelValue;
  },
);

// 通过emit将值传递给父组件
emit("update:modelValue", modelValue);
</script>

types

/*
 * @Author: vhen
 * @Date: 2024-03-24 00:36:03
 * @LastEditTime: 2024-03-24 15:21:30
 * @Description: 现在的努力是为了小时候吹过的牛逼!
 * @FilePath: \vhen-vue3-admin-pro\src\components\SearchForm\types.ts
 * 
 */
export type FormType = "input" | "select" | "radio" | "cascader" | "autocomplete" | "date" | "daterange" | "checkbox";

export interface ItemOption {
  label: string
  value: string | number
  disabled?: boolean
}

export interface FormItemVO {
  type: FormType //输入框类型
  label: string //输入框标题
  disabled?: boolean//表单是否可修改 默认false
  placeholder?: any //输入框默认显示内容
  prop: string //表单校验
  options?: ItemOption[] | (() => ItemOption[]) //选择器的可选子选项 select
  span?: number // 表单栅格数
  offset?: number // 表单栅格偏移
  clearable?: boolean // 是否可清空
  size?: string // 输入框大小
  multiple?: boolean // 是否多选
  collapseTags?: boolean // 是否折叠
  collapseTagsThreshold?: number // 多选时标签的阈值,大于该阈值时折叠
  filterable?: boolean // 是否可搜索
  allowCreate?: boolean // 是否支持创建
  radioType?: string // 单选框类型
}

export interface PropsVO {
  formData: object  // 表单数据
  formItem: FormItemVO[] // 表单配置项
  span?: number // 表单栅格数
  isSeniorSearch?: boolean // 是否高级搜索
  gutter?: number // 表单栅格间隔
  showButton?: boolean // 是否显示查询按钮
}

index.vue

<template>
  <section class="search-form">
    <el-form :model="props.formData" v-bind="$attrs">
      <el-row :gutter="props.gutter">
        <el-col
          v-for="column in useFormItem"
          :key="column.prop"
          :span="column.span"
          :offset="column.offset"
        >
          <el-form-item :label="`${column.label}`" :prop="column.prop">
            <component
              :is="componentType[column.type]"
              v-bind="column"
              v-model="props.formData[column.prop]"
            />
          </el-form-item>
        </el-col>
        <template v-if="$slots.default">
          <slot />
        </template>
        <el-col :span="props.span" style="flex: 1; max-width: 100%" v-if="showButton">
          <div flex justify="end" items-center w-full h-full>
            <div
              v-if="isSeniorSearch"
              flex
              items-center
              mr-2
              class="senior-search"
              @click="isShow = !isShow"
              cursor="pointer"
            >
              <div class="text">{{ isShow ? "收起" : "展开" }}</div>
              <div class="flex m-left-4">
                <el-icon>
                  <ArrowUp v-if="isShow" />
                  <ArrowDown v-else />
                </el-icon>
              </div>
            </div>
            <el-button @click="$emit('reset')" :icon="RefreshLeft">重置</el-button>
            <el-button type="primary" class="m-bottom-12" @click="$emit('search')" :icon="Search"
              >查询</el-button
            >
          </div>
        </el-col>
      </el-row>
    </el-form>
  </section>
</template>
<script lang="ts" setup>
import { RefreshLeft, Search } from "@element-plus/icons-vue";
import { FormInstance } from "element-plus";
import { computed, markRaw, ref } from "vue";
import VhenAutocomplete from "./src/VhenAutocomplete.vue";
import VhenCascader from "./src/VhenCascader.vue";
import VhenCheckbox from "./src/VhenCheckbox.vue";
import VhenDatePicker from "./src/VhenDatePicker.vue";
import VhenInput from "./src/VhenInput.vue";
import VhenRadio from "./src/VhenRadio.vue";
import VhenSelect from "./src/VhenSelect.vue";
import { PropsVO } from "./types";

const emit = defineEmits<{
  (e: "reset"): void;
  (e: "search"): void;
}>();

const props = withDefaults(defineProps<PropsVO>(), {
  isSeniorSearch: false,
  gutter: 12,
  span: 8,
  showButton: true,
});
const isShow = ref(false);
const useFormItem = computed(() => {
  const isShowRow = props.isSeniorSearch && !isShow.value && props.showButton;
  if (isShowRow) {
    const num = Math.floor(24 / props.span) - 1;
    return props.formItem.slice(0, num);
  } else {
    return props.formItem;
  }
});

const componentType = markRaw({
  input: VhenInput,
  select: VhenSelect,
  radio: VhenRadio,
  cascader: VhenCascader,
  autocomplete: VhenAutocomplete,
  date: VhenDatePicker,
  daterange: VhenDatePicker,
  checkbox: VhenCheckbox,
});

const formRef = ref<FormInstance>();
defineExpose({ formRef });
</script>
<style lang="scss" scoped>
.senior-search {
  color: var(--el-text-color-regular);
  .text {
    font-size: 14px;
  }
}
</style>

组件案例

<template>
  <div>
    <SearchForm
      :formData="formData"
      :form-item="formItemList"
      :rules="formRules"
      :span="6"
      label-position="top"
      label-width="100px"
      isSeniorSearch
      @reset="resetData"
      @search="handleSearch"
    >
    </SearchForm>
    <pre>
    {{ formData }}
    </pre>
  </div>
</template>
<script lang="ts" setup>
import SearchForm from "@/components/SearchForm/index.vue";
import { FormRules } from "element-plus";
import { reactive } from "vue";
const formData = reactive({
  userName: "",
  email: "843348394@qq.com",
  sex: "1",
  area: [],
  city: "",
});
const formItemList = reactive([
  {
    type: "input",
    label: "姓名",
    prop: "userName",
    clearable: true,
    span: 6,
    placeholder: "请输入姓名",
    // disabled: true,
    input: handleInput,
  },
  {
    type: "autocomplete",
    label: "邮箱",
    prop: "email",
    span: 6,
    "fetch-suggestions": querySearch,
  },
  {
    type: "daterange",
    label: "出生日期",
    prop: "birthday",
    span: 6,
  },
  {
    type: "radio",
    label: "-",
    prop: "sex",
    cType: "button",
    span: 6,
    options: [
      {
        value: "0",
        label: "男",
      },
      {
        value: "1",
        label: "女",
      },
    ],
  },
  {
    type: "checkbox",
    label: "工作地点",
    prop: "area",
    span: 6,
    options: [
      {
        label: "北京",
        value: "beijing",
      },
      {
        label: "上海",
        value: "shanghai",
      },
      {
        label: "深圳",
        value: "shenzhen",
      },
    ],
  },
  {
    type: "select",
    prop: "city",
    label: "城市",
    span: 6,
    options: [
      {
        label: "北京",
        value: "beijing",
      },
      {
        label: "上海",
        value: "shanghai",
      },
      {
        label: "深圳",
        value: "shenzhen",
      },
    ],
  },
]);

const resetData = () => {
  console.log(formData);
};
const handleSearch = () => {
  console.log(formData);
};

const formRules = reactive<FormRules>({
  userName: [{ required: true, message: "请输入姓名", trigger: "blur" }],
  email: [{ required: true, message: "请输入邮箱", trigger: "blur" }],
});
function handleInput(val: string | number) {
  console.log(val);
}
function querySearch(query: string, cb: any) {
  console.log(query);
  cb([query]);
}
</script>
<style lang="scss" scoped></style>

结束语

简单二次封装form 表单组件,如大家有更好的方案,欢迎大家评论区讨论,一起学习一起成长....