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、嵌入式:分两部分,第一是嵌入式软件开发,主要与嵌入式操作系统、应用软件等有关。第二是嵌入式硬件开发,需要掌握硬件…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
涂鸦T5AI手搓语音、emoji、otto机器人从入门到实战
“🤖手搓TuyaAI语音指令 😍秒变表情包大师,让萌系Otto机器人🔥玩出智能新花样!开整!” 🤖 Otto机器人 → 直接点明主体 手搓TuyaAI语音 → 强调 自主编程/自定义 语音控制(TuyaAI…...
自然语言处理——循环神经网络
自然语言处理——循环神经网络 循环神经网络应用到基于机器学习的自然语言处理任务序列到类别同步的序列到序列模式异步的序列到序列模式 参数学习和长程依赖问题基于门控的循环神经网络门控循环单元(GRU)长短期记忆神经网络(LSTM)…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
用鸿蒙HarmonyOS5实现中国象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的中国象棋小游戏的实现代码。这个实现使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chinesechess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├──…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
热烈祝贺埃文科技正式加入可信数据空间发展联盟
2025年4月29日,在福州举办的第八届数字中国建设峰会“可信数据空间分论坛”上,可信数据空间发展联盟正式宣告成立。国家数据局党组书记、局长刘烈宏出席并致辞,强调该联盟是推进全国一体化数据市场建设的关键抓手。 郑州埃文科技有限公司&am…...
