基于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…

一个vue3的待办列表组件

一个vue3的待办列表组件, 仿企业微信的待办列表 TodoList.vue <template><div><el-input v-model"todoInput" placeholder"写下你的待办事项..." class"el-input" keyup.enter"addTodo"input-style"background-c…

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

一、建设背景 随着物联网技术的迅猛发展和广泛应用&#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;也可以根据自身内容自动计…

使用frp0.61.0透传局域网的https服务到自有域名

本文成因&#xff1a;我之前已经写过多个frphttps的文章&#xff0c;但因为frp版本升级后&#xff0c;更换了配置文件&#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;均为正整…

安全见闻(3)

脚本语言 lua php go(也算吧) python JavaScript nodejs这种主流脚本就很多了&#xff0c;这些编程语言都可以写一些脚本性的病毒&#xff0c;python可以编写木马&#xff0c;js也可以编写木马&#xff0c;比如beefxss&#xff0c;了解本质的人都知道那个就是相当于js写的木马…

TensorFlow面试整理-TensorFlow 结构与组件

TensorFlow 的结构和组件是其功能强大、灵活性高的重要原因。掌握这些结构和组件有助于更好地理解和使用 TensorFlow 构建、训练和部署模型。以下是 TensorFlow 关键的结构与组件介绍: 1. Tensor(张量) 定义:张量是 TensorFlow 中的数据载体,类似于多维数组或矩阵。张量的…

深入了解Vue Router:基本用法、重定向、动态路由与路由守卫的性能优化

文章目录 1. 引言2. Vue Router的基本用法2.1 基本配置 3. 重定向和命名路由的使用3.1 重定向3.2 命名路由 4. 在Vue Router中如何处理动态路由4.1 动态路由的概念4.2 如何处理动态路由4.3 动态路由的懒加载 5. 路由守卫的实现与性能影响5.1 什么是路由守卫&#xff1f;5.2 路由…

Redis 持久化 总结

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

通信协议——UART

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

WPF:Binding数据绑定

WPF&#xff08;Windows Presentation Foundation&#xff09;是微软推出的一种用于构建Windows客户端应用程序的UI框架。数据绑定是WPF中一个强大的功能&#xff0c;它允许UI元素与数据源之间建立连接&#xff0c;使得UI能够自动显示数据源中的数据&#xff0c;并且当数据源中…

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…

EasyExcel文件导入与导出

1.文件导入 导入校验 public class BoyListener extends AnalysisEventListener {List<String> names new ArrayList<>();/*** 每解析一行&#xff0c;回调该方法**/Overridepublic void invoke(Object data, AnalysisContext analysisContext) {//校验名称Stri…

上传Gitee仓库流程图

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