基于ElementPlus的Form组件封装

server/2024/10/25 7:11:28/
前言

我们在项目开发过程中遇到最多就是表单页面的开发,那么使用频率比较高的就是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.ppmy.cn/server/134640.html

相关文章

自动驾驶系列—图像数据在自动驾驶中的关键角色及其实际应用场景探讨

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

微信小程序美团点餐

引言&#xff1a;外卖已经成为了都市人的必备&#xff0c;在无数个来不及&#xff08;懒得&#xff09;做饭的时刻拯救孤单寂寞的胃。美团外卖无疑是外卖届的领头羊&#xff0c;它的很多功能与设计都值得我们学习。本文将从五个方面&#xff0c;对美团外卖展开产品分析&#xf…

Spring Boot + Vue项目开发学习笔记2

这个笔记是在看B站视频的时候做的&#xff0c;所以肯定是很多直接把课程的ppt的文字直接写下来了或者把老师口述的内容写下来&#xff0c;目的是为了让自己遗忘某些知识点的时候能直接看笔记&#xff0c;应该不至于构成侵权吧&#xff0c;如有不妥望告知&#xff0c;我会删除并…

动态规划之打家劫舍

大纲 题目思路第一步&#xff1a;确定下标含义第二步&#xff1a;确定递推公式第二步&#xff1a;dp数组如何初始化第三步&#xff1a;确定遍历顺序第四步&#xff1a;举例推导dp数组 总结 最近有人询问我 LeetCode 「打家劫舍」系列问题&#xff08;英文版叫 House Robber&…

CentOS 8在Linux虚拟机修改IP地址,出现:错误:“ens160“ 不是活动的连接。错误:未提供活动连接。

问题&#xff1a;错误&#xff1a;"ens160" 不是活动的连接。错误&#xff1a;未提供活动连接。 1.查看网络服务运行状态&#xff1a; 1)CentOS 7执行命令&#xff1a;systemctl status network 2)CentOS 8执行命令&#xff1a;systemctl status NetworkManager&a…

医院信息化与智能化系统(7)

医院信息化与智能化系统(7) 这里只描述对应过程&#xff0c;和可能遇到的问题及解决办法以及对应的参考链接&#xff0c;并不会直接每一步详细配置 如果你想通过文字描述或代码画流程图&#xff0c;可以试试PlantUML&#xff0c;告诉GPT你的文件结构&#xff0c;让他给你对应的…

又是一年 1024

今天是 1024 程序员节&#xff0c;现在是一名大数据讲师&#xff0c;我和往常一样&#xff0c;依旧在讲课中度过。对于很多程序员来说&#xff0c;这一天也许是属于代码、调试和无数行 SQL 查询的&#xff0c;而对于我来说&#xff0c;虽然工作内容不同&#xff0c;却也和数据、…

AMD XILINX 20nm器件价格上调25%

随着市场回暖&#xff0c;台积电也在调整价格策略&#xff0c;近期台积电上调了20nm的出厂价格。 据相关消息显示&#xff0c;AMD为了保障持续的供货和服务&#xff0c;也计划将20nm器件的价格统一上调25%&#xff0c;预计将于11月发布正式的涨价通知&#xff0c;并于2025年Q1开…