vue3 + antd 封装动态表单组件(二)

传送带:
vue3 + antd 封装动态表单组件(一)


前置条件:

vue版本 v3.3.11
ant-design-vue版本 v4.1.1

vue3 + antd 封装动态表单组件(一)是基础版本,但是并不好用, 因为需要配置很多表单项的schem组件属性componentProps,如果很多地方用到这些表单项,就需要大量的重复工作去配置这些相同的组件属性。

因此,本篇文章新增了默认组件属性和表单项配置功能,大大简化了动态表单schema配置,以及新增了各表单项空值配置,请根据实际的业务场景进行配置;

动态组件配置文件config.js

import {
    Input, Textarea, InputNumber, Select, RadioGroup, CheckboxGroup, DatePicker } from 'ant-design-vue';

// 表单域组件类型
export const componentsMap = {
   
    Text: Input,
    Textarea,
    Number: InputNumber,
    Select,
    Radio: RadioGroup,
    Checkbox: CheckboxGroup,
    DatePicker,
}

// 配置各组件属性默认值,相关配置项请查看ant-design官网各组件api属性配置
export const defaultComponentProps = {
   
    Text: {
   
        allowClear: true,
        bordered: true,
        disabled: false,
        showCount: true,
        maxlength: 20,
    },
    Textarea: {
   
        allowClear: true,
        autoSize: {
    minRows: 4, maxRows: 4 },
        showCount: true,
        maxlength: 200,
        style: {
   
            width: '100%'
        }
    },
    Select: {
   
        allowClear: true,
        bordered: true,
        disabled: false,
        showArrow: true,
        optionFilterProp: 'label',
        optionLabelProp: 'label',
        showSearch: true,
    },
    DatePicker: {
   
        allowClear: true,
        bordered: true,
        disabled: false,
        format: 'YYYY-MM-DD',
        picker: 'date',
        style: {
   
            width: '100%'
        }
    },
}

dynamic-form.vue组件

<template>
  <div>
    <a-form ref="formRef" :model="formModel" v-bind="$attrs">
      <a-form-item
        :name="item.field"
        :label="item.label"
        v-for="item in formSchema"
        :key="item.field"
        v-bind="item.formItemProps"
      >
        <span v-if="item.loading"
          ><LoadingOutlined style="margin-right: 4px" />数据加载中...</span
        >
        <component
          v-else
          :is="componentsMap[item.component]"
          v-bind="item.componentProps"
          v-model:value="formModel[item.field]"
        />
      </a-form-item>
    </a-form>
  </div>
</template>

<script setup>
import {
      ref, watch, onMounted, computed } from "vue";
import {
      componentsMap, defaultComponentProps } from "./config.js";
import {
      LoadingOutlined } from "@ant-design/icons-vue";
import dayjs from "dayjs";
const props = defineProps({
     
  // 表单项配置
  schema: {
     
    type: Array,
    default: () => [],
  },
  // 表单model配置,一般用于默认值、回显数据
  model: {
     
    type: Object,
    default: () => ({
     }),
  },
  // 组件属性配置
  componentProps: {
     
    type: Object,
    default: () => ({
     }),
  },
});

const formRef = ref(null);

const formSchema = ref([]);
const formModel = ref({
     });

// 组件placeholder
const getPlaceholder = (x) => {
     
  let placeholder = "";
  switch (x.component) {
     
    case "Text":
    case "Textarea":
      placeholder = `请输入${ x.label}`;
      break;
    case "RangePicker":
      placeholder = ["开始时间", "结束时间"];
      break;
    default:
      placeholder = `请选择${ x.label}`;
      break;
  }
  return placeholder;
};

// 组件属性componentProps, 注意优先级:组件自己配置的componentProps > props.componentProps > config.js中的componentProps
const getComponentProps = (x) => {
     
  if (!x?.componentProps) x.componentProps = {
     };
  // 使得外层可以直接配置options
  if (x.hasOwnProperty("options") && x.options) {
     
    x.componentProps.options = [];
    const isFunction = typeof x.options === "function";
    const isArray = Array.isArray(x.options);
    if (isFunction || isArray) {
     
      // 函数时先赋值空数组
      x.componentProps.options = isFunction ? [] : x.options;
    }
  }

  return {
     
    placeholder: x?.componentProps?.placeholder ?? getPlaceholder(x),
    ...(defaultComponentProps[x.component] || {
     }), // config.js带过来的基础componentProps默认配置
    ...(props.componentProps[x.component] || {
     }), // props传进来的组件componentProps配置
    ...x.componentProps, // 组件自身的componentProps
  };
};

// 表单属性formItemProps
const getFormItemProps = (x) => {
     
  let result = {
      ...(x.formItemProps || {
     }) };
  // 使得外层可以直接配置required必填项
  if (x.hasOwnProperty("required") && x.required) {
     
    result.rules = [
      ...(x?.formItemProps?.rules || []),
      {
     
        required: true,
        message: getPlaceholder(x),
        trigger: "blur",
      },
    ];
  }
  return result;
};

// 各组件为空时的默认值
const getDefaultEmptyValue = (x) => {
     
  let defaultEmptyValue = "";
  switch (x.component) {
     
    case "Text":
    case "Textarea":
      defaultEmptyValue = "";
      break;
    case "Select":
      defaultEmptyValue = ["tag", "multiple"].includes(x?.componentProps?.mode)
        ? []
        : undefined;
    case "Cascader":
      defaultEmptyValue = x?.value?.length ? x.value : [];
    default:
      defaultEmptyValue = undefined;
      break;
  }
  return defaultEmptyValue;
};

// 格式化各组件值
const getValue = (x) => {
     
  let formatValue = x.value;
  if (!!x.value) {
     
    switch (x.component) {
     
      case "DatePicker":
        formatValue = dayjs(x.value, "YYYY-MM-DD");
        break;
      default:
      	formatValue = x.value;
      	break;   
    }
  }
  return formatValue;
};

const getSchemaConfig = (x) => {
     
  return {
     
    ...x,
    componentProps: getComponentProps(x),
    formItemProps: getFormItemProps(x),
    value: x.value ?? getDefaultEmptyValue(x),
  };
};

const setFormModel = () => {
     
  formModel.value = formSchema.value.reduce((pre, cur) => {
     
    if (!pre[cur.field]) {
     
      // 表单初始数据(默认值)
      pre[cur.field] = getValue(cur);
      return pre;
    }
  }, {
     });
};

// 表单初始化
const initForm = () => {
     
  formSchema.value = props.schema.map((x) => getSchemaConfig(x));
  // model初始数据
  setFormModel();
  // options-获取异步数据
  formSchema.value.forEach(async (x) => {
     
    if (x.options && typeof x.options === "function") {
     
      x.loading = true;
      x.componentProps.options = await x.options(formModel.value);
      x.loading = false;
    }
  });
};

onMounted(() => {
     
  initForm();
  watch(
    () => props.model,
    (newVal) => {
     
      // model重新赋值给formSchema,注意:model会覆盖schema配置的value值
      formSchema.value.forEach((x) => {
     
        for (const key in newVal) {
     
          if (x.field === key) {
     
            x.value = newVal[key];
          }
        }
      });
      setFormModel();
    },
    {
     
      immediate: true,
      deep: true,
    }
  );
});

const hasLoadingSchema = computed(() =>
  formSchema.value.some((x) => x.loading)
);

// 表单验证
const validateFields = () => {
     
  if (hasLoadingSchema.value) {
     
    console.log("正在加载表单项数据...");
    return;
  }
  return new Promise((resolve, reject) => {
     
    formRef.value
      .validateFields()
      .then((formData) => {
     
        resolve(formData);
      })
      .catch((err) => reject(err));
  });
};

// 表单重置
const resetFields = (isInit = true) => {
     
  // 是否清空默认值
  if (isInit) {
     
    formModel.value = {
     };
  }
  formRef.value.resetFields();
};

// 暴露方法
defineExpose({
     
  validateFields,
  resetFields,
});
</script>

使用dynamic-form.vue组件,注意比较vue3 + antd 封装动态表单组件(一)中的表单项的schema配置

<template>
  <div style="padding: 200px">
    <DynamicForm ref="formRef" :schema="schema" :model="model" :labelCol="{ span: 4 }" :wrapperCol="{ span: 20 }"/>
    <div style="display: flex; justify-content: center">
      <a-button @click="handleReset(true)">重置(全部清空)</a-button>
      <a-button style="margin-left: 50px" @click="handleReset(false)"
        >重置</a-button
      >
      <a-button type="primary" style="margin-left: 50px" @click="handleSubmit"
        >提交</a-button
      >
    </div>
  </div>
</template>

<script setup>
import DynamicForm from "@/components/form/dynamic-form.vue";
import {
      ref } from "vue";
import dayjs from "dayjs";
import {
      getRemoteData } from "@/common/utils";
const formRef = ref(null);

const schema = ref([
  {
     
    label: "姓名",
    field: "name",
    component: "Text",
    required: true,
  },
  {
     
    label: "性别",
    field: "sex",
    component: "Radio",
    options: [
      {
      value: 1, label: "男" },
      {
      value: 2, label: "女" },
      {
      value: 3, label: "保密" },
    ],
    value: 1,
    required: true,
  },
  {
     
    label: "生日",
    field: "birthday",
    component: "DatePicker",
    required: true,
  },
  {
     
    label: "兴趣",
    field: "hobby",
    component: "Checkbox",
    options: async () => {
     
      // 后台返回的数据list
      const list = [
        {
      value: 1, label: "足球" },
        {
      value: 2, label: "篮球" },
        {
      value: 3, label: "排球" },
      ];
      return await getRemoteData(list);
    },
  },
  {
     
    label: "国家",
    field: "country",
    component: "Select",
    options: [
      {
      value: 1, label: "中国" },
      {
      value: 2, label: "美国" },
      {
      value: 3, label: "俄罗斯" },
    ],
  },
  {
     
    label: "简介",
    field: "desc",
    component: "Textarea",
  },
]);
const model = ref({
      name: "百里守约" });
// 提交
const handleSubmit = async () => {
     
  const formData = await formRef.value.validateFields();
  if (formData.birthday) {
     
    formData.birthday = dayjs(formData.birthday).format("YYYY-MM-DD");
  }
  console.log("提交信息:", formData);
};

// 重置
const handleReset = (isInit) => {
     
  formRef.value.resetFields(isInit);
};
</script>

效果图

在这里插入图片描述

相关推荐

  1. vue2 elementui 封装一个动态复杂组件

    2024-01-27 05:00:01       21 阅读
  2. vue2-组件封装

    2024-01-27 05:00:01       10 阅读
  3. 如何封装一个Vue3组件库?

    2024-01-27 05:00:01       36 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-27 05:00:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-27 05:00:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-27 05:00:01       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-27 05:00:01       20 阅读

热门阅读

  1. LeetCode //C - 547. Number of Provinces

    2024-01-27 05:00:01       31 阅读
  2. XJTU大学计算机I C语言编程题 第七周

    2024-01-27 05:00:01       35 阅读
  3. 20240126 大模型快讯

    2024-01-27 05:00:01       32 阅读
  4. RISC-V架构的了解

    2024-01-27 05:00:01       30 阅读
  5. Docker:docker run的 --rm 选项

    2024-01-27 05:00:01       37 阅读
  6. 力扣2859-计算k置位下标对应元素的和

    2024-01-27 05:00:01       35 阅读
  7. 最近在对接电商供应链,说说开放平台API接口

    2024-01-27 05:00:01       36 阅读
  8. MySQL数据库的一些缩写含义

    2024-01-27 05:00:01       30 阅读
  9. 跨语言编程:在C#应用程序中调用Python

    2024-01-27 05:00:01       26 阅读
  10. laspy + open3d 实现.las点云可视化

    2024-01-27 05:00:01       35 阅读