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--><templatev-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><templatev-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><componentv-else:is="componentsMap[item.component]"v-bind="item.componentProps"v-model:value="formModel[item.field]"><!-- 表单项组件插槽, 注意优先级:组件componentProps.slots > componentPropsSlots--><templatev-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><templatev-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 = {};// 使得外层可以直接配置optionsif (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) => {// 重新赋值给formSchemaformSchema.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"><DynamicFormref="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 () => {// 后台返回的数据listconst 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配置该插件

相关文章:
vue3 + antd 封装动态表单组件(三)
传送带: vue3 antd 封装动态表单组件(一) vue3 antd 封装动态表单组件(二) 前置条件: vue版本 v3.3.11 ant-design-vue版本 v4.1.1 我们发现ant-design-vue Input组件和FormItem组件某些属性支持slot插…...
【算法专题】贪心算法
贪心算法 贪心算法介绍1. 柠檬水找零2. 将数组和减半的最少操作次数3. 最大数4. 摆动序列(贪心思路)5. 最长递增子序列(贪心算法)6. 递增的三元子序列7. 最长连续递增序列8. 买卖股票的最佳时机9. 买卖股票的最佳时机Ⅱ(贪心算法)10. K 次取反后最大化的数组和11. 按身高排序12…...
x-cmd pkg | sqlite3 - 轻量级的嵌入式关系型数据库
目录 简介首次用户 技术特点竞品和相关产品sqlite 与 x-cmd进一步阅读 简介 sqlite3 是一个轻量级的文件数据库,体积非常小,提供简单优雅而功能强大的 sql 化的数据查询。 通常情况下,sqlite 指的是 SQLite 2.x 版本,而 sqlite3 …...
LeetCode —— 43. 字符串相乘
😶🌫️😶🌫️😶🌫️😶🌫️Take your time ! 😶🌫️😶🌫️😶🌫️😶🌫️…...
PalWorld/幻兽帕鲁Ubuntu 22.04 LTS 一键部署脚本
上去就是干! 创建install.sh文件 #!/bin/bashsteam_usersteam log_path/tmp/pal_server.logif getent passwd "$steam_user" >/dev/null 2>&1; thenecho "User $steam_user exists." elseecho "User $steam_user does not exi…...
【Vue】Vue3.0样式隔离
在这里记录一下Vue3.0里面的样式隔离特性,在项目开发过程当中,有时候将样式单独提到了一个文件当中再引入到单组件文件当中,会导致没有样式隔离。 这里阅读Vue官方文档找到了解决办法。 一、scoped 我们了解到的最常见就是scopedÿ…...
Git初识
📙 作者简介 :RO-BERRY 📗 学习方向:致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 📒 日后方向 : 偏向于CPP开发以及大数据方向,欢迎各位关注,谢谢各位的支持 在学习Git之前我们先引入一…...
OpenHarmony隐藏应用(应用不在桌面显示,隐藏应用图标)
注意:此种方式是在OpenHarmony系统中生效 目录 一.找到UnsgnedReleasedProfileTemplate.json文件 二.修改 UnsgnedReleasedProfileTemplate.json文件 三.重新签名 一.找到UnsgnedReleasedProfileTemplate.json文件 什么是U...
2024年新提出的算法:(凤头豪猪优化器)冠豪猪优化算法Crested Porcupine Optimizer(附Matlab代码)
本次介绍一种新的自然启发式元启发式算法——凤头豪猪优化器(Crested Porcupine Optimizer,CPO)。该成果于2024年1月发表在中科院1区SCI top期刊Knowledge-Based Systems(IF 8.8)上。 1、简介 受到凤头豪猪(CP)各种…...
vue3 el-pagination 将组件中英文‘goto’ 修改 为 中文到‘第几’
效果如图: 要求:将英文中Go to 改为到第几 操作如下: <template><div class"paging"><el-config-provider :locale"zhCn"> // 注意:这是重要部分<el-pagination //分页组件根据官…...
【蓝桥杯日记】复盘篇二:分支结构
前言 本篇笔记主要进行复盘的内容是分支结构,通过学习分支结构从而更好巩固之前所学的内容。 目录 前言 目录 🍊1.数的性质 分析: 知识点: 🍅2.闰年判断 说明/提示 分析: 知识点: &am…...
Vulnhub靶机:hackme1
一、介绍 运行环境:Virtualbox(攻击机)和VMware(靶机) 攻击机:kali(192.168.56.106) 靶机:hackme1(192.168.56.107) 目标:获取靶机root权限和flag 靶机下载地址:htt…...
【C/C++ 06】基数排序
基数排序是桶排序的一种,算法思路为: 利用队列进行数据收发创建一个队列数组,数组大小为10,每个元素都是一个队列,存储取模为1~9的数从低位到高位进行数据收发,完成排序适用于数据位不高的情况(…...
Flume1.9基础学习
文章目录 一、Flume 入门概述1、概述2、Flume 基础架构2.1 Agent2.2 Source2.3 Sink2.4 Channel2.5 Event 3、Flume 安装部署3.1 安装地址3.2 安装部署 二、Flume 入门案例1、监控端口数据官方案例1.1 概述1.2 实现步骤 2、实时监控单个追加文件2.1 概述2.2 实现步骤 3、实时监…...
ThinkPHP6的助手函数汇总
原文地址 abort(): 抛出 HTTP 异常 1. /** 2. * 抛出 HTTP 异常 3. * param integer|Response $code 状态码 或者 Response 对象实例 4. * param string $message 错误信息 5. * param array $header 参数 6. */ 7. abort($code, string…...
·备忘录模式
备忘录模式 备忘录模式 备忘录模式 介绍:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先的状态。 实现:备忘录类,有一个私有状态属性…...
docker-学习-2
docker学习第二天 docker学习第二天1.docker和虚拟机的区别2.docker的底层隔离机制2.1 Namespaces(命名空间)2.1.1 什么是命名空间 2.2 Cgroups2.3 Union file systems2.4 Container format2.5 docker在底层如何做隔离的,如何进行资源限制的? 3. docker命…...
树--二叉树(C语言纯手凹)
目录 目录 1.什么是树?(不深入,仅做了解) 2.树的表示方式 2.1孩子兄弟表示法(左孩子右兄弟) 2.2孩子表示法 2.3双亲表示法 3.什么是二叉树 4.二叉树分类 4.1满二叉树 4.2完全二叉树 4.3二叉搜索树…...
TypeScript(七) 函数
1. TypeScript 函数 1.1. 函数的定义 函数就是包裹在花括号中的代码块,前面使用关键字function。 语法: // An highlighted block function function_name() {// 执行代码 }实例: function test() { // 函数定义console.log("我就是…...
学fpga和还是嵌入式?
具体要选哪个,更多还是看个人喜好还有基础知识结构。 我们先来明白下两者区别在哪? 1、嵌入式:分两部分,第一是嵌入式软件开发,主要与嵌入式操作系统、应用软件等有关。第二是嵌入式硬件开发,需要掌握硬件…...
网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...
如何在网页里填写 PDF 表格?
有时候,你可能希望用户能在你的网站上填写 PDF 表单。然而,这件事并不简单,因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件,但原生并不支持编辑或填写它们。更糟的是,如果你想收集表单数据ÿ…...
CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...
