基于ElementPlus的Form组件封装

前言

我们在项目开发过程中遇到最多就是表单页面的开发,那么使用频率比较高的就是Form组件,无论是vue亦或者是react,我们在项目中使用到UI库都会有Form组件。多数情况下都是用到了Form组件,我们先根据UI库或者其他类似的页面直接进行ctrl+c、v操作,然后再进行修改,最终按照交互稿和视觉稿完成页面开发。这样做的弊端是,每次用到都需要重新写一遍,是不符合前端组件化的开发思想的。那么我们需要对Form组件进行二次封装,避免重复的代码书写,提高开发效率,增强代码的可读性和可扩展性。

介绍

本篇文章分享的是基于Element PlusForm组件的二次封装,为什么是Element Plus呢?因为正好我的项目中用到的是此UI库,项目中我对Form组件及逆行了二次封装,因为将这块的内容整理出来,输出一篇技术文档,希望对大家有帮助!UI库都是类似的,大家可以参照我的做法对其他UI库的Form组件进行封装。

技术栈

Vue3+Ts+Element Plus

效果图

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

源码
// components/CForm/index.vue<!--@author: duanfc@time: 2024-08-16 10:00:00@description: 描述@path: /demo@lastChange: duanfc
--><template><el-form v-bind="$attrs" ref="elFormRef" class="el-form-style"><el-row :gutter="16"><slot name="formItem"></slot></el-row></el-form>
</template><script lang="ts">
import { ref } from "vue";
import type { ElForm } from "element-plus";export default {name: "CForm",props: {},components: {},setup(props, context) {const elFormRef = ref<InstanceType<typeof ElForm> | null>(null);return {elFormRef,};},
};
</script><style lang="scss" scoped>
.el-form-style {width: calc(100% - 32px);margin-left: 16px;border-bottom: 1px solid #f0f0f0;.search-item:last-child {padding-right: 0 !important;}.search-item:first-child {padding-left: 0 !important;}
}
// 屏幕宽度小于992px
@media only screen and (max-width: 992px) {.search-item {padding: 0 !important;}
}
// 屏幕宽度大于等于992px但小于1200px
@media screen and (min-width: 992px) and (max-width: 1200px) {.search-item:nth-child(3n) {padding-right: 0 !important;}.search-item:nth-child(3n + 1) {padding-left: 0 !important;}
}
// 屏幕宽度大于等于1200px
@media screen and (min-width: 1200px) {.search-item:nth-child(4n) {padding-right: 0 !important;}.search-item:nth-child(4n + 1) {padding-left: 0 !important;}
}
</style>
// components/CFormItem/index.vue<!--@author: duanfc@time: 2024-08-16 10:00:00@description: 描述@path: /demo@lastChange: duanfc
--><template><el-col:md="baseSpan * span":lg="baseSpan * span":xl="baseSpan * span":class="[isBtn ? 'btn-style' : '', 'search-item']":offset="offset"v-show="formItemStatus"><el-form-item v-bind="$attrs"><slot></slot></el-form-item></el-col>
</template><script lang="ts">
import { computed, ref } from "vue";
import { useResize } from "@/layout/hooks/useResize";export default {name: "cFormItem",components: {},props: {span: {type: Number,default: 1,},isBtn: {type: Boolean,default: false,},isExpand: {type: Boolean,required: true,},colTotal: {type: Number,default: 1,},itemIndex: {type: Number,required: true,},},setup(props, context) {const baseSpan = ref(0);// 计算按钮的offset值const calculateOffset = (val: number,colTotalValue: number,status: boolean): number => {if (val) {// 每一个筛选项的份数// 每行可放表单项的数量const itemSpan = 24 / val;// 最后一行筛选项的个数const remainder = colTotalValue % itemSpan;if (status) {// 展开状态if (screenWidth.value >= 992) {if (colTotalValue == 1) return 0;return (itemSpan - remainder - 1) * val;} else if (screenWidth.value < 992) {return 0;}// if (screenWidth.value >= 1200) {//     if (colTotalValue == 1) return 0;//     return (itemSpan - remainder - 1) * val;// } else if (screenWidth.value >= 992) {//     if (colTotalValue == 1) return 0;//     return (itemSpan - remainder - 1) * val;// } else if (screenWidth.value < 992) {//     return 0;// }} else {// 收起状态const itemSpan = 24 / val;const overNum = colTotalValue - itemSpan;if (screenWidth.value >= 1200) {if (colTotalValue < 4) {return (itemSpan - colTotalValue - 1) * val;}return overNum >= 3? 0: (itemSpan - overNum - 1) * val;} else if (screenWidth.value >= 992) {return overNum >= 2 || colTotalValue < itemSpan? 0: (itemSpan - overNum - 1) * val;} else if (screenWidth.value < 992) {return 0;}}}};const screenWidth = ref(0);// 表单项的显示与隐藏const formItemStatus = computed(() => {let bool = true;if (!props.isExpand) {if (screenWidth.value >= 1200) {bool = !(props.itemIndex >= 8);} else if (screenWidth.value >= 992) {bool = !(props.itemIndex >= 6);} else if (screenWidth.value < 992) {if (props.itemIndex <= 5) {bool = true;} else {bool = props.isExpand;}}}if (props.isBtn) bool = true;return bool;});useResize(document.body,(width, height) => {if (width >= 1200) {baseSpan.value = 6;} else if (width >= 992) {baseSpan.value = 8;} else if (width < 992) {baseSpan.value = 24;}screenWidth.value = width;},300);const offset = computed(() => {if (props.isBtn) {return calculateOffset(baseSpan.value,props.colTotal,props.isExpand);} else {return 0;}});return {offset,baseSpan,formItemStatus,};},
};
</script><style lang="scss" scoped>
.btn-style {::v-deep .el-form-item__content {justify-content: flex-end;margin-left: 0 !important;}
}
</style>
// useResize.tsimport { onMounted, onUnmounted, Ref } from 'vue';export function useResize(element: Ref<HTMLElement | null> | HTMLElement, callback: (width: number, height: number) => void, delay: number = 300) {function debounce(fn: Function, delay: number) {let timer: ReturnType<typeof setTimeout>;return (...args: any[]) => {clearTimeout(timer);timer = setTimeout(() => fn(...args), delay);};}const debouncedCallback = debounce((width: number, height: number) => {callback(width, height);}, delay);let resizeObserver: ResizeObserver | null = null;onMounted(() => {// 判断element的类型并获取targetElementconst targetElement = (element as Ref<HTMLElement | null>).value || (element as HTMLElement);if (targetElement) {resizeObserver = new ResizeObserver(entries => {for (const entry of entries) {if (entry.contentRect) {debouncedCallback(entry.contentRect.width, entry.contentRect.height);}}});resizeObserver.observe(targetElement);}})onUnmounted(() => {if (resizeObserver) {resizeObserver.disconnect();resizeObserver = null;}});
}
// useFormBtnStatus.tsimport { ref, Ref } from 'vue';
import { useResize } from "./useResize";export function useFormBtnStatus(total: number): Ref<boolean> {const bool = ref(false);useResize(document.body, (width, height) => {if (total > 5) {if (width >= 1200) {bool.value = total > 7;} else if (width >= 992) {bool.value = total > 5;} else if (width < 992) {bool.value = true;}} else {bool.value = false;}}, 300);return bool;
}
// 使用 index.vue<!--@author: duanfc@time: 2024-08-16 10:00:00@description: 描述@path: /demo@lastChange: duanfc
--><template><div class="fifth"><c-formref="formRef"label-width="90px":model="formInfo"label-position="right":rules="rules"><template #formItem><c-form-itemlabel="姓名":is-expand="btnStatus":item-index="1"prop="name"><el-input v-model="formInfo.name" placeholder="请输入" /></c-form-item><c-form-item:span="2"label="日期":is-expand="btnStatus":item-index="3"prop="time"><el-date-pickerv-model="formInfo.time"type="datetimerange"range-separator="至"start-placeholder="开始时间"end-placeholder="结束时间"/></c-form-item><c-form-itemlabel="地址":is-expand="btnStatus":item-index="4"><el-input v-model="formInfo.address" placeholder="请输入" /></c-form-item><!-- <c-form-itemlabel="编号":is-expand="btnStatus":item-index="5"><el-input v-model="formInfo.code" placeholder="请输入" /></c-form-item><c-form-itemlabel="车牌号":is-expand="btnStatus":item-index="6"><el-input v-model="formInfo.car" placeholder="请输入" /></c-form-item><c-form-itemlabel="手机号":is-expand="btnStatus":item-index="7"><el-input v-model="formInfo.phone" placeholder="请输入" /></c-form-item><c-form-itemlabel="手机号1":is-expand="btnStatus":item-index="8"><el-input v-model="formInfo.phone" placeholder="请输入" /></c-form-item><c-form-itemlabel="手机号2":is-expand="btnStatus":item-index="9"><el-input v-model="formInfo.phone" placeholder="请输入" /></c-form-item> --><c-form-item:is-btn="true":col-total="4":item-index="10":is-expand="btnStatus"><el-button type="primary" @click="onSubmit">查询</el-button><el-button>取消</el-button><divclass="open-retract-btn"@click.prevent="btnStatus = !btnStatus"v-if="formBtnStatus"><span class="text">{{btnStatus ? "收起" : "展开"}}</span><el-icon><ArrowUp v-if="btnStatus" /><ArrowDown v-else /></el-icon></div></c-form-item></template></c-form></div>
</template><script lang="ts">
import { ref, reactive, onMounted } from "vue";
import cForm from "./components/cForm/index.vue";
import cFormItem from "./components/cFormItem/index.vue";
import { useFormBtnStatus } from "@/layout/hooks/useFormBtnStatus";export default {name: "fifth",props: {},components: {cForm,cFormItem,},setup(props, context) {const formInfo = reactive({name: "",time: [],address: "",code: "",car: "",phone: "",idNum: "",});const formRef = ref(null);const onSubmit = async () => {if (!formRef.value.elFormRef) return;await formRef.value.elFormRef.validate((valid: boolean, fields: Record<string, any>) => {if (valid) {console.log("submit!");} else {console.log("error submit!", fields);}});};const btnStatus = ref(false); // true: 展开(收起状态);false: 收起(收起状态)// 控制“展开”和“收起”按钮的显示与隐藏const formBtnStatus = useFormBtnStatus(4);const rules = reactive({name: [{required: true,message: "Please input Activity name",trigger: "blur",},{min: 3,max: 5,message: "Length should be 3 to 5",trigger: "blur",},],time: [{type: "array",required: true,message: "Please pick a date",trigger: ["change", "blur"],},],});onMounted(() => {});return {formInfo,onSubmit,btnStatus,formBtnStatus,formRef,rules,};},
};
</script><style lang="scss" scoped>
.fifth {height: 100%;width: 100%;.open-retract-btn {height: 32px;display: flex;align-items: center;padding: 0 8px;color: #296dff;cursor: pointer;.text {margin-right: 4px;}}
}
</style>

[!CAUTION]

Form的自适应能够从代码里看出,只做了屏幕宽度 >= 1200px屏幕宽度 >= 992px屏幕宽度 < 992px三种情况的区分,有兴趣的小伙伴可以在我的基础上对其他的屏宽进行自适应设置

Form API

属性属性值备注
refformRef通过formRef.value.elFormRef来访问UI库的Form组件暴露出的方法,例如:formRef.value.elFormRef.validate()调用表单验证方法
其他属性-参照https://element-plus.org/zh-CN/component/form.html中的FormAPI

FormItem API

属性类型是否必填备注
spannumber不传默认为1
isBtnboolean按钮项是必传,其他表单项非必传判断是否是按钮
isExpandboolean
colTotalnumber按钮项是必传,其他表单项非必传除按钮项外其他表单项的数量
itemIndexnumber从1开始递增,与span的值有关。如果某个表单项的span值为2,那么此表单项的itemIndex要递增2;如果span值为1,则递增1
其他属性-参照https://element-plus.org/zh-CN/component/form.html中的FormItem API

对于Table组件封装可以参考https://blog.csdn.net/dfc_dfc/article/details/143134143?spm=1001.2014.3001.5501

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

h5页面与小程序页面互相跳转

小程序跳转h5页面 一个home页 /pages/home/home 一个含有点击事件的元素&#xff1a;<button type"primary" bind:tap"toWebView">点击跳转h5页面</button>toWebView(){ wx.navigateTo({ url: /pages/webview/webview }) } 一个webView页 /pa…

物联网行业应用实训室建设方案

一、建设背景 随着物联网技术的迅猛发展和广泛应用&#xff0c;物联网产业已跃升为新时代的经济增长引擎&#xff0c;对于产业升级和社会信息化水平的提升具有举足轻重的地位。因此&#xff0c;为了满足这一领域的迫切需求&#xff0c;培养具备物联网技术应用能力的优秀人才成…

自动发现-实现运维管理自动化

nVisual-Discovery是一款自动化工具软件&#xff0c;通过多种自动发现技术&#xff0c;协助运维管理人员快速建立可视化的网络文档&#xff0c;提升网络管理的效率与准确性。 01 IP扫描发现 当我们新接手一个网络运维项目&#xff0c;通常缺乏精准的网络文档数据&#xff0c;…

4.2-6 使用Hadoop WebUI

文章目录 1. 查看HDFS集群状态1.1 端口号说明1.2 用主机名访问1.3 主节点状态1.4 用IP地址访问1.5 查看数据节点 2. 操作HDFS文件系统2.1 查看HDFS文件系统2.2 在HDFS上创建目录2.3 上传文件到HDFS2.4 删除HDFS文件和目录 3. 查看YARN集群状态4. 实战总结 1. 查看HDFS集群状态 …

Docker部署MySQL主从复制

1. 主从复制概念及优势 1.1 概念 MySQL主从复制是一种数据库复制技术&#xff0c;它允许将一个数据库服务器&#xff08;主服务器&#xff09;上的数据更改复制到一个或多个数据库服务器&#xff08;从服务器&#xff09;。这种技术在数据库管理和维护中扮演着重要的角色&…

CSS 网格布局

网格布局是一个二维布局系统&#xff0c;允许开发者以行和列的形式创建灵活的网络&#xff0c;并将内容放置在网络的单元格中。有些元素可能只占据网络的一个单元&#xff0c;另一些元素则可能占据多行或多列。 网格的大小既可以精确定义&#xff0c;也可以根据自身内容自动计…

C/C++(六)多态

本文将介绍C的另一个基于继承的重要且复杂的机制&#xff0c;多态。 一、多态的概念 多态&#xff0c;就是多种形态&#xff0c;通俗来说就是不同的对象去完成某个行为&#xff0c;会产生不同的状态。 多态严格意义上分为静态多态与动态多态&#xff0c;我们平常说的多态一般…

实战应用WPS WebOffice开放平台服务

概述 根据公司的业务需要&#xff0c;主要功能是在线编辑文档&#xff0c;前端的小伙伴进行的技术调研&#xff0c;接入的是WPS WebOffice&#xff0c;这里只阐述技术介入的步骤、流程和遇到的坑进行的一些总结。 实践 WPS WebOffice 开放平台进行认证 在开始之前&#xff…

【NOIP提高组】加分二叉树

【NOIP提高组】加分二叉树 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 设一个n个节点的二叉树tree的中序遍历为&#xff08;l,2,3,…,n&#xff09;&#xff0c;其中数字1,2,3,…,n为节点编号。每个节点都有一个分数&#xff08;均为正整…

Redis 持久化 总结

前言 相关系列 《Redis & 目录》&#xff08;持续更新&#xff09;《Redis & 持久化 & 源码》&#xff08;学习过程/多有漏误/仅作参考/不再更新&#xff09;《Redis & 持久化 & 总结》&#xff08;学习总结/最新最准/持续更新&#xff09;《Redis & …

通信协议——UART

目录 基础概念串行&并行串行的优缺点 单工&双工 UART基本概念时序图思考&#xff1a;接收方如何确定01和0011 基础概念 串行&并行 串行为8车道&#xff0c;并行为1车道 串行的优缺点 通行速度快浪费资源布线复杂线与线之间存在干扰 单工&双工 单工&#xf…

NewStarCTF 2023 公开赛道 Web week1-week2

目录 week1 泄漏的秘密 Begin of Upload Begin of HTTP ErrorFlask ​Begin of PHP R!C!E! EasyLogin ​week2 游戏高手 include 0。0 ez_sql ​Unserialize&#xff1f; Upload again! R!!C!!E!! week1 泄漏的秘密 使用ctf-scan.py&#xff08;https://gith…

上传Gitee仓库流程图

推荐一个流程图工具 登录 | ProcessOnProcessOn是一个在线协作绘图平台&#xff0c;为用户提供强大、易用的作图工具&#xff01;支持在线创作流程图、思维导图、组织结构图、网络拓扑图、BPMN、UML图、UI界面原型设计、iOS界面原型设计等。同时依托于互联网实现了人与人之间的…

Qt中使用线程之QConcurrent

QConcurrent可以实现并发&#xff0c;好处是我们可以不用单独写一个类了&#xff0c;直接在类里面定义任务函数&#xff0c;然后使用QtConcurrent::run在单独的线程里执行一个任务 1、定义一个任务函数 2、定义1个QFutureWatcher的对象&#xff0c;使用QFutureWatcher来监测任…

用Python将Office文档(Word、Excel、PowerPoint)批量转换为PDF

在处理不同格式的Office文档&#xff08;如Word、Excel和PowerPoint&#xff09;时&#xff0c;将其转换为PDF格式是常见的需求。这种转换不仅确保了文件在不同设备和操作系统间的一致性显示&#xff0c;而且有助于保护原始内容不被轻易修改&#xff0c;非常适合于正式报告、提…

Redisson(三)应用场景及demo

一、基本的存储与查询 分布式环境下&#xff0c;为了方便多个进程之间的数据共享&#xff0c;可以使用RedissonClient的分布式集合类型&#xff0c;如List、Set、SortedSet等。 1、demo <parent><groupId>org.springframework.boot</groupId><artifact…

【主机漏洞扫描常见修复方案】:Tomcat安全(机房对外Web服务扫描)

文章目录 引言I SSL/TLS Not ImplementedTomcat 服务器 SSL 证书安装部署(JKS 格式)Tomcat 服务器 SSL 证书安装部署(PFX 格式)HTTP 自动跳转 HTTPS 的安全配置(可选)修复SSL证书版本低II 主机漏洞扫描常见修复方案Apache JServ protocol serviceSlow HTTP DEnial of Ser…

多楼层智能穿梭:转运机器人助力制造业转型升级

针对当前喷砂产品人工转运存在的劳动强度大、效率低、安全隐患多等问题&#xff0c;本方案提出设计一套高效、安全、多楼层自动转运系统&#xff0c;采用潜伏式转运机器人结合电梯与升降平台技术&#xff0c;实现平面类、立柱类及小工件类喷砂产品的自动化、智能化转运。 项目需…

Docker 与 Yocto

Yocto项目为什么需要Docker Yocto 项目并不直接依赖 Docker&#xff0c;但在某些情况下使用 Docker 可以为 Yocto 项目提供以下具体且实际的好处&#xff1a; 1. 环境一致性&#xff1a; Yocto 构建需要一个稳定且一致的开发环境。不同的 Linux 发行版可能会有不同的库版本、…

深入探索电能消耗数据:基于机器学习的分析与洞察

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…