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

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


前置条件:

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

我们发现ant-design-vue Input组件和FormItem组件某些属性支持slot插槽,如何使得我们封装的动态表单组件也支持该功能呢(slot透传)?本篇文章主要是解决该问题。
在这里插入图片描述
在这里插入图片描述

动态组件配置文件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"
      >
        <!-- 表单form-item插槽, 注意优先级:组件formItemProps.slots > formItemPropsSlots-->
        <template
          v-for="slot in formItemPropsSlots"
          #[slot.name]="slotProps"
          :key="slot.key"
        >
          <template v-if="slot.field === item.field">
            <slot :name="slot.key" v-bind="slotProps"></slot>
          </template>
        </template>
        <template
          v-for="(slot, name) in item.formItemProps?.slots || {}"
          #[name]="slotProps"
          :key="`${item.field}_${name}`"
        >
          <component :is="slot" v-bind="slotProps"></component>
        </template>

        <template v-if="item.slot">
          <slot :name="item.slot" v-bind="formModel"></slot>
        </template>

        <template v-else>
          <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]"
          >
            <!-- 表单项组件插槽, 注意优先级:组件componentProps.slots > componentPropsSlots-->
            <template
              v-for="slot in componentPropsSlots"
              #[slot.name]="slotProps"
              :key="slot.key"
            >
              <template v-if="slot.field === item.field">
                <slot :name="slot.key" v-bind="slotProps"></slot>
              </template>
            </template>

            <template
              v-for="(slot, name) in item.componentProps?.slots || {}"
              #[name]="slotProps"
              :key="`${item.field}_componentProps_${name}`"
            >
              <!-- 这里是关键, 渲染slot -->
              <component :is="slot" v-bind="slotProps"></component>
            </template>
          </component>
        </template>
      </a-form-item>
    </a-form>
  </div>
</template>

<script setup>
import { ref, watch, onMounted, computed, useSlots } 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 slots = useSlots();

// 表单formItem slots
const formItemPropsSlots = ref([]);

// 表单项组件slots
const componentPropsSlots = ref([]);

// 用于获取componentProps、formItemProps插槽
const createPropsSlots = (type) => {
  // 对象转数组, 这里表单项slots规则为 对应的filed + '-type-' + slot名称,可自行定义规则,对应字段匹配上即可
  const slotsArr = Object.entries(slots);
  return slotsArr
    .filter((x) => x[0].indexOf(type) !== -1)
    .map((x) => {
      const slotParams = x[0].split("-");
      return {
        key: x[0],
        value: x[1],
        name: slotParams[2],
        field: slotParams[0],
      };
    });
};
const createSlots = () => {
  formItemPropsSlots.value = createPropsSlots("formItemProps");
  componentPropsSlots.value = createPropsSlots("componentProps");
};

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;
    }
  }
  return formatValue;
};

const getSchemaConfig = (x) => {
  return {
    ...x,
    componentProps: getComponentProps(x),
    formItemProps: getFormItemProps(x),
    value: x.value ?? getDefaultEmptyValue(x),
    label:
      x.formItemProps?.slots?.label ||
      formItemPropsSlots.value.find((y) => y.field === x.field)?.field
        ? undefined
        : x.label,
  };
};

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(() => {
  createSlots();
  initForm();
  watch(
    () => props.model,
    (newVal) => {
      // 重新赋值给formSchema
      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>

使用动态表单组件

<template>
  <div style="padding: 200px">
    <DynamicForm
      ref="formRef"
      :schema="schema"
      :model="model"
      :labelCol="{ span: 4 }"
      :wrapperCol="{ span: 20 }"
    >
      <template #country-formItemProps-label>
        <span style="color: green">国家</span>
      </template>

      <!-- 表单项field为name的slot,componentProps配置的slot优先级高于此处 -->
      <template #name-componentProps-addonAfter>
        <span>我是slot</span>
      </template>

      <template #country-componentProps-suffixIcon>
        <span>我也是slot</span>
      </template>

      <template #someComponentX="formModel">
        <div><BellFilled style="color: red" />我是特殊的某某组件</div>
        <div>表单信息:{{ formModel }}</div>
      </template>
    </DynamicForm>
    <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 lang="jsx" setup>
import DynamicForm from "@/components/form/dynamic-form.vue";
import { ref, reactive } from "vue";
import dayjs from "dayjs";
import { getRemoteData } from "@/common/utils";
import { UserOutlined, BellFilled } from "@ant-design/icons-vue";
const formRef = ref(null);

const schema = ref([
  {
    label: "姓名",
    field: "name",
    component: "Text",
    required: true,
    componentProps: {
      slots: {
        addonAfter: () => <UserOutlined />,
      },
    },
  },
  {
    label: '性别',
    field: "sex",
    component: "Radio",
    options: [
      { value: 1, label: "男" },
      { value: 2, label: "女" },
      { value: 3, label: "保密" },
    ],
    value: 1,
    required: true,
    formItemProps: {
      slots: {
        label: () => <div style="color: blue">性别</div>
      }
    }
  },
  {
    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",
  },
  {
    label: "插槽组件X",
    field: "someComponentX",
    slot: "someComponentX",
  },
]);
const model = reactive({ name: "百里守约", someComponentB: 'ok' });
// 提交
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>

效果图

在这里插入图片描述

注意这里使用了jsx,需要安装相关插件(本人用的前端构建工具是vite)

在这里插入图片描述

安装插件

npm install @vitejs/plugin-vue-jsx --save

vite.config.js配置该插件

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/357900.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【RT-DETR有效改进】2024.1最新MFDS-DETR的HS-FPN改进特征融合层(降低100W参数,全网独家首发)

👑欢迎大家订阅本专栏,一起学习RT-DETR👑 一、本文介绍 本文给大家带来的改进机制是最近这几天最新发布的改进机制MFDS-DETR提出的一种HS-FPN结构,其是一种为白细胞检测设计的网络结构,主要用于解决白细胞数据集中的多尺度挑战。它的基本原理包括两个关键部分:特征…

[Linux]:软硬连接(什么是软硬链接,怎么创建软硬链接,以及对应的例子)

目录 软连接&#xff1a; 什么是软连接&#xff1a; 怎么创建软连接&#xff1a; 例子&#xff1a; 硬链接&#xff1a; 什么是硬链接&#xff1a; 怎么创建硬链接&#xff1a; 例子&#xff1a; 软连接&#xff1a; 什么是软连接&#xff1a; 软连接文件是一个独立的…

如何在群晖NAS部署office服务实现多人远程协同办公编辑文档

文章目录 本教程解决的问题是&#xff1a;1. 本地环境配置2. 制作本地分享链接3. 制作公网访问链接4. 公网ip地址访问您的分享相册5. 制作固定公网访问链接 本教程解决的问题是&#xff1a; 1.Word&#xff0c;PPT&#xff0c;Excel等重要文件存在本地环境&#xff0c;如何在编…

Adobe Photoshop 2024 v25.4.0 - 专业的图片设计软件

Adobe Photoshop 2024 v25.4.0更新了&#xff0c;从照片编辑和合成到数字绘画、动画和图形设计&#xff0c;任何您能想象到的内容都能通过PS2024轻松实现。 利用人工智能技术进行快速编辑。学习新技能并与社区分享您的工作。借助我们的最新版本&#xff0c;做令人惊叹的事情从未…

2023年全国职业院校技能大赛(高职组)“云计算应用”赛项赛卷9

某企业根据自身业务需求&#xff0c;实施数字化转型&#xff0c;规划和建设数字化平台&#xff0c;平台聚焦“DevOps开发运维一体化”和“数据驱动产品开发”&#xff0c;拟采用开源OpenStack搭建企业内部私有云平台&#xff0c;开源Kubernetes搭建云原生服务平台&#xff0c;选…

生物信息学高质量刊物

个人觉得生信期刊的水平排名&#xff08;只谈计算方法&#xff09; 1&#xff0c;Nature Biotechnology (Article) 生信人心中的梦之刊&#xff0c;很少有纯计算能上的&#xff0c;一般都需要一定的湿实验验证&#xff0c;在计算领域某些场合认可度甚至高于正刊。 2&#xf…

【pdf密码】怎么打印加密的PDF文件?

PDF文件是可以打开查看的&#xff0c;但是现在不能编辑、不能打印&#xff0c;功能栏中的功能都是灰色的&#xff0c;这种设置了加密的PDF文件该如何加密&#xff1f; 如果PDF中的大多数功能按钮以及打印按钮都是灰色的状态&#xff0c;那就证明是文件的问题导致不能打印的。 …

FC-135 / FC-135 TYPE 贴片晶振

描述 FC135是一种被广泛采用的32.768 kHz晶体单元&#xff0c;自2002年发布以来已在全球范围内使用。 理想的单片机子时钟和模块&#xff0c;从消费设备到工业设备的应用。如果温度范围至105.C&#xff0c;请与我们联系。 爱普生是千赫波段晶体单元的领先供应商&#xff0c;…

力扣hot100 柱状图中最大的矩形 单调栈

Problem: 84. 柱状图中最大的矩形 文章目录 思路复杂度Code 思路 &#x1f468;‍&#x1f3eb; 参考地址 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) Code class Solution {public static int largestRectangleArea(int[] height){Stack&l…

Python入门到精通(六)——Python函数进阶

Python函数进阶 一、函数的多返回值 二、函数多种传参方式 1、位置参数 2、关键字参数 3、缺省参数 4、不定长参数 &#xff08;1&#xff09;位置传递 &#xff08;2&#xff09;关键字传递 三、匿名函数 &#xff08;1&#xff09;函数作为参数传递 &#xff08;2&…

Java面试架构篇【一览众山小】

文章目录 &#x1f6a1; 简介☀️ Spring&#x1f425; 体系结构&#x1f420; 生命周期 &#x1f341; SpringMVC&#x1f330; 执行流程 &#x1f31c; SpringBoot&#x1f30d; 核心组件&#x1f38d; 自动装配&#x1f391; 3.0升级 &#x1f505; spring Cloud Alibaba&am…

docker+jekins+maven+ssh 持续集成交付部署 jar包

一. docker环境搭建&#xff0c;此处略过。 二. docker部署jekins 2.1 拉取镜像&#xff0c;挂载工作目录,xxxx为宿主机指定工作目录 docker pull jenkins/jenkins docker run -d -p 8080:8080 -p 50000:50000 --name jenkins --privilegedtrue -v xxxxxxxxxx:/var/jenkins…

Prompt Learning 的几个重点paper

Prefix Tuning: Prefix-Tuning: Optimizing Continuous Prompts for Generation 在输入token之前构造一段任务相关的virtual tokens作为Prefix&#xff0c;然后训练的时候只更新Prefix部分的参数&#xff0c;PLM中的其他参数固定。针对自回归架构模型&#xff1a;在句子前面添…

RabbitMQ之三种队列之间的区别及如何选型

目录 不同队列之间的区别 Classic经典队列 Quorum仲裁队列 Stream流式队列 如何使用不同类型的队列​ Quorum队列 Stream队列 不同队列之间的区别 Classic经典队列 这是RabbitMQ最为经典的队列类型。在单机环境中&#xff0c;拥有比较高的消息可靠性。 经典队列可以选…

基于java新闻发布及管理系统

技术架构&#xff1a; Servlet JSP MySQL 功能介绍&#xff1a; Java新闻发布系统新闻发布及管理系统就是一个能够在网上实现新闻的发布及管理&#xff0c;让人们更好的获取更新的新闻资讯。 &#xff08;1&#xff09;用户管理&#xff1a; 用户注册&#xff1a;新用…

亚马逊、速卖通、虾皮等平台测评自养号,虚拟信用卡的使用

在上一篇文章中&#xff0c;我提到了一个完善的自养号测评系统需要具备的几个要求。在这篇文章中&#xff0c;我将重点探讨支付方式虚拟信用卡的问题&#xff0c;以及如何避免信用卡关联。 首先&#xff0c;需要了解信用卡关联风控机制的存在意义。在跨境电商亚马逊平台上&…

Orion-14B-Chat-Plugin本地部署的解决方案

大家好,我是herosunly。985院校硕士毕业,现担任算法研究员一职,热衷于机器学习算法研究与应用。曾获得阿里云天池比赛第一名,CCF比赛第二名,科大讯飞比赛第三名。拥有多项发明专利。对机器学习和深度学习拥有自己独到的见解。曾经辅导过若干个非计算机专业的学生进入到算法…

C# 二分搜索(Binary Search)

二分搜索概念 二分查找也称折半查找&#xff08;Binary Search&#xff09;它是一种效率较高的查找方法。但是&#xff0c;折半查找要求线性表必须采用顺序存储结构&#xff0c;而且表中元素按关键字有序排列。 二分搜索的背景 二分搜索法的概念和思想可以追溯到古代的中国和…

服务器遭遇CC攻击怎么办,使用高防SCDN能防御吗

随着互联网的普及&#xff0c;网络安全问题日益凸显&#xff0c;其中CC攻击&#xff08;也称为Challenge Collapsar攻击&#xff09;已成为一种常见的网络攻击手段。CC攻击主要针对服务器的验证码系统进行攻击&#xff0c;通过大量请求来耗尽服务器资源&#xff0c;导致服务器无…

14个国产AI大模型备案获批,众多科技巨头进入AIGC赛道

北京商报官网消息&#xff0c;第四范式、什么值得买、新壹科技、衔远科技、小米、智联招聘、Boss直聘、脉脉等13家企业的&#xff0c;14个国产AI大模型通过《生成式人工智能服务管理暂行办法》备案&#xff0c;可实现商业化应用。 自2023年8月&#xff0c;文心一言、讯飞星火、…