react实现页面动态表单设计器(自定义推拽表单) 您所在的位置:网站首页 前端表单设计教程 react实现页面动态表单设计器(自定义推拽表单)

react实现页面动态表单设计器(自定义推拽表单)

2024-06-29 22:42| 来源: 网络整理| 查看: 265

react实现页面动态表单设计器(自定义推拽表单) 含完整代码讲解实现效果安装插件使用组件介绍基本设置,可设置控件标签,是否必填,校验规则校验规则有如下几种多选,下拉,单选可动态设置每个选择的label以及值 代码解析left 左侧拖拽组件left-index.jsx 拖动功能html5拖拽和释放功能知识扩展 left-interface.js 需要渲染的组件遍历列表left-index.less样式设置 center 中间表单组件center-index.jsx 表单功能center-index.less right -右侧所选控件设置组件-label、value、表单校验、下拉、单选、复选等选项数据值设置right-index.jsxright-index.lessright-If.jsx 控制下拉、单选、复选等选项数据值设置显影right-validate.js 表单校验规则 大功告成!此时你就得到自定义表单拉!!!gitee代码地址

含完整代码讲解 实现效果

在这里插入图片描述

左侧为拖拽表单,中间为组件,右侧为属性,可设置label,输入限制等

安装插件 cnpm i dynamic-customization-form 使用 组件介绍

左侧 多个表单控件,可自由选择拖拽至中间

中间 对推拽后的空间进行值的输入和选择

右侧

基本设置,可设置控件标签,是否必填,校验规则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jEeWzU2S-1690273324713)(https://gitee.com/jumping-little-stars/dynamic-forms/raw/master/assets/right1.png)]

校验规则有如下几种

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j0Z3RBci-1690273324713)(https://gitee.com/jumping-little-stars/dynamic-forms/raw/master/assets/right2.png)]

多选,下拉,单选可动态设置每个选择的label以及值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7a1eLAW-1690273324713)(https://gitee.com/jumping-little-stars/dynamic-forms/raw/master/assets/right3.png)]

最后可点击表单保存,也可以表单重置

代码解析

可以通过代码自己理解去封装组件! 此为发包代码讲解,自己写,可以在项目里封装,导出引入即可!

在这里插入图片描述

left 左侧拖拽组件

在这里插入图片描述

left-index.jsx 拖动功能 import React, { useState } from "react"; import "./index.less"; import { example } from "./interface"; const Config = (props) => { const [list, setList] = useState(example); // fn 拖动开始 const dragStartFn=(item)=> { // 自动识别配置必要参数 let param = { ...item, required: false, //是否必填 }; props.setNewParams(param); props.setDragEnd(false); } // fn 拖动结束 const dragEndFn=()=> { props.setDragEnd(true); } const renderList=()=> { return list.map((item, index) => { return ( dragStartFn(item)} className="catalogue-item" draggable="true" onDragEnd={dragEndFn} key={index} > {item.label} ); }); } return ( { if (!props.valid) return; e.preventDefault(); props.setValid(false); }} className="catalogue" > {renderList()} ); }; export default Config; html5拖拽和释放功能知识扩展

默认情况下,图片、链接和文本是可拖动的。HTML5 在所有 HTML 元素上规定了一个 draggable 属性, 表示元素是否可以拖动。图片和链接的 draggable 属性自动被设置为 true,而其他所有元素此属性的默认值为 false。

拖拽:Drag 释放:Drop

某个元素被拖动时,会依次触发以下事件:

ondragstart:拖动开始,当鼠标按下并且开始移动鼠标时,触发此事件;整个周期只触发一次; ondrag:只要元素仍被拖拽,就会持续触发此事件; ondragend:拖拽结束,当鼠标松开后,会触发此事件;整个周期只触发一次。

当把拖拽元素移动到一个有效的放置目标时,目标对象会触发以下事件:

ondragenter:只要一把拖拽元素移动到目标时,就会触发此事件; ondragover:拖拽元素在目标中拖动时,会持续触发此事件; ondragleave:拖拽元素离开目标时(没有在目标上放下),会触发ondragleave ondrop: 当拖拽元素在目标放下(松开鼠标),则触发ondrop事件。

left-interface.js 需要渲染的组件遍历列表 你可以自己增加修改删除哦! export const example = [ { label: "单行文本", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "Input", // 控件类型 value: "", allowClear: true, //是否允许清除-默认true }, { label: "多行文本", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "InputTextera", // 控件类型 value: "", allowClear: true, //是否允许清除-默认true }, { label: "数字框", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "InputNumber", // 控件类型 value: null, }, { label: "下拉框", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "Select", // 控件类型 options: [{ label: "选项1", value: 1 }], // 控件选项参数名称 value: "", }, { label: "单选", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "Radio", // 控件类型 options: [{ label: "选项1", value: 1}], // 控件选项参数名称 value: "", }, { label: "复选框", // 标签名称 tips: "请输入", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "Checkbox", // 控件类型 options: [{ label: "选项1", value: 1 }], // 控件选项参数名称 value: "", }, { label: "开关", // 标签名称 tips: "开启", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "Switch", // 控件类型 value: false, }, { label: "日期选择", // 标签名称 tips: "请选择", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "DatePicker", // 控件类型 value: "", }, { label: "日期区间", // 标签名称 tips: "请选择", // 提示语 rule: "validateNone", // 需要限制规则名称 type: "RangePicker", // 控件类型 value: [], }, ]; left-index.less样式设置 .catalogue { width: 15%; border: 1px solid #f1e8e8; padding: 15px; height: 100%; &-item { display: flex; align-items: center; justify-content: center; margin-bottom: 5px; cursor: move; color: #000; user-select: none; padding: 8px 10px; background: #f6f7ff; font-size: 12px; cursor: move; border: 1px dashed #f6f7ff; border-radius: 3px; } > div:last-child { margin-bottom: 0; } } center 中间表单组件

在这里插入图片描述

center-index.jsx 表单功能 import React, { useEffect, useState } from 'react'; import moment from 'moment'; import { Form, Checkbox, Switch, Input, Select, Radio, InputNumber, DatePicker, } from 'antd'; import * as validateParams from '../left/validate'; import './index.less'; let draging = null; const Option = Select.Option; const CheckboxGroup = Checkbox.Group; const { RangePicker } = DatePicker; let targetName = 'drag-move'; //需要换位目标 let targetIndex = -1; //当前换位位置 let prevIndex = -1; //原坐标 const UiForm = (props) => { const { formList, newParams, setList, dragEnd, valid, setValid, setActive, active, } = props; useEffect(() => { renderForm(props.formList); }, [props.formList]); // render 表单 function renderForm(arr) { return arr.map((item, index) => { if (!item || !item.label) return; return ( {renderSwatch(item, index)} ); }); } // render 表单类型判定 function renderSwatch(item, index) { switch (item.type) { case 'Input': return ( ); case 'InputTextera': return ( ); case 'InputNumber': return ( ); case 'Select': return ( (option?.children).includes(input)} > {item.options.map((its) => { return ( {' '} {its.label}{' '} ); })}{' '} ); case 'Radio': return ( {item.options.map((its) => { return ( {' '} {its.label}{' '} ); })} ); case 'Checkbox': return ( ); case 'Switch': return ( ); case 'DatePicker': return ( ); case 'RangePicker': return ( ); default: return ''; } } // fn 拖动开始 function dragStartFn(index, e) { e.dataTransfer.setData('te', e.target.innerText); //不能使用text,firefox会打开新tab draging = e.target; prevIndex = index; } // fn 拖动中 function dragOverFn(index, e) { e.preventDefault(); let target = getParentNode(e.target); if (!target || target.className.includes(targetName)) return; if (target !== draging) { targetIndex = index; } } // fn 拖动结束 function dragEndFn() { if (prevIndex === -1 || targetIndex === -1) return; let newList = [...formList]; let prev = newList[prevIndex]; let target = newList[targetIndex]; newList.splice(prevIndex, 1, target); newList.splice(targetIndex, 1, prev); setList(newList); } // 数组转对象 function arrToObj1(arr, idx) { return arr.reduce((obj, item, index) => { if (index == idx) { obj[index] = ''; } else { obj[index] = item; } return obj; }, {}); } // fn 新生成元素 function dargAddFn() { let newForm = Object.values(props.form.getFieldValue()); let setform = arrToObj1(newForm, formList.length); props.form.setFieldsValue({ ...setform, }); // 直接新增 if (!dragEnd || !newParams.label) return; var code = '' + (parseInt(Math.random() * 1000000) + 1000000); code = code.substring(1, 7); newParams.key = newParams.type + code; setList([...formList, newParams]); } // 阶梯查询父级元素 function getParentNode(el) { try { if (el.className === '') return getParentNode(el.parentNode); if (el.className.includes(targetName)) return el; let parentName = el.parentNode.className; if (parentName.includes(targetName)) return getParentNode(el.parentNode); if (!parentName.includes(targetName)) return el.parentNode; } catch (error) { console.log(error); console.log(el.className); return undefined; } } // 时间选择传值 function checkSetTime(index, val) { let newList = [...formList]; newList[index].value = moment(val._d).format('YYYY-MM-DD'); setList(newList); } // 时间区间选择传值 function checkSetArrTime(index, val) { let param; if (val === null) param = val; else if (val[1] != null) param = moment(val[1]._d).format('YYYY-MM-DD'); else param = moment(val[0]._d).format('YYYY-MM-DD'); let newList = [...formList]; newList[index].value = param; setList(newList); } // 表单传值 function checkNoSpace(index, e) { let param = e && e.target ? e.target.value : e; let newList = [...formList]; newList[index].value = param; setList(newList); } useEffect(() => { if (valid) dargAddFn(); if (dragEnd) { draging = null; targetIndex = -1; } }, [dragEnd]); return ( { if (valid) return; e.preventDefault(); setValid(true); }} id="uiform" className="uiform" > {renderForm(formList)} ); }; export default UiForm; center-index.less @color: 'red'; @hover: '#333'; @disabled: '#000'; @error: 'red'; .drag-item { transition: all 0.3s; margin: 5px 0; cursor: move; &-active { box-shadow: 0 0 2px @color; } } .uiform { .ant-input-textarea-affix-wrapper { .ant-input-clear-icon { position: absolute; right: 30px !important; } .ant-input-textarea-suffix { .ant-form-item-feedback-icon { position: absolute; top: 6px; right: 0px; font-size: 14px; } } } } right -右侧所选控件设置组件-label、value、表单校验、下拉、单选、复选等选项数据值设置

在这里插入图片描述

right-index.jsx import React, { useState } from 'react'; import { Switch, Input, Select, Button, Tag } from 'antd'; import './index.less'; import If from './If'; import { OptionsValidate } from './validate'; import { ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined, } from '@ant-design/icons'; let draging = null; let targetName = 'config-item-option'; //需要换位目标 let targetIndex = -1; //当前换位位置 let prevIndex = -1; //原坐标 const Option = Select.Option; const Config = (props) => { const { active, formList, setList, setActive } = props; // fn 拖动开始 function dragStartFn(index, e) { e.dataTransfer.setData('te', e.target.innerText); //不能使用text,firefox会打开新tab draging = e.target; prevIndex = index; } // fn 拖动中 function dragOverFn(index, e) { e.preventDefault(); let target = getParentNode(e.target); if (!target || target.className !== targetName) return; if (target !== draging && draging) { //getBoundingClientRect()用于获取某个元素相对于视窗的位置集合 let targetRect = target.getBoundingClientRect(); let dragingRect = draging.getBoundingClientRect(); if (target && target.animated) return; targetIndex = index; } } // fn 拖动结束 function dragEndFn() { let newList = [...formList]; let param = newList[active].options; let prev = param[prevIndex]; let target = param[targetIndex]; param.splice(prevIndex, 1, target); param.splice(targetIndex, 1, prev); newList[active].options = [...param]; setList([...newList]); } // 阶梯查询父级元素 function getParentNode(el) { if (el.className === '') return getParentNode(el.parentNode); if (el.className === targetName) return el; let parentName = el.parentNode.className; if (parentName !== targetName) return getParentNode(el.parentNode); if (parentName === targetName) return el.parentNode; } //获取元素在父元素中的index function dragIndex(el) { let index = 0; if (!el || !el.parentNode) return -1; //previousElementSibling属性返回指定元素的前一个兄弟元素(相同节点树层中的前一个元素节点)。 while (el && (el = el.previousElementSibling)) { index++; } return index; } function dragAnimate(prevRect, target) { let ms = 300; let currentRect = target.getBoundingClientRect(); //nodeType 属性返回以数字值返回指定节点的节点类型。1=元素节点 2=属性节点 if (prevRect.nodeType === 1) { prevRect = prevRect.getBoundingClientRect(); } dragStyle(target, 'transition', 'none'); dragStyle( target, 'transform', 'translate3d(' + (prevRect.left - currentRect.left) + 'px,' + (prevRect.top - currentRect.top) + 'px,0)', ); target.offsetWidth; // 触发重绘 dragStyle(target, 'transition', 'all ' + ms + 'ms'); dragStyle(target, 'transform', 'translate3d(0,0,0)'); clearTimeout(target.animated); target.animated = setTimeout(() => { dragStyle(target, 'transition', ''); dragStyle(target, 'transform', ''); target.animated = false; }, ms); } //给元素添加style function dragStyle(el, prop, val) { let style = el && el.style; if (!style) return false; if (val === void 0) { //使用DefaultView属性可以指定打开窗体时所用的视图 if (document.defaultView && document.defaultView.getComputedStyle) { val = document.defaultView.getComputedStyle(el, ''); } else if (el.currentStyle) { val = el.currentStyle; } return prop === void 0 ? val : val[prop]; } else { if (!(prop in style)) prop = '-webkit-' + prop; style[prop] = val + (typeof val === 'string' ? '' : 'px'); } } // 表单传值 function changeValue(name, e) { let param = e && e.target ? e.target.value : e; let arr = [...formList]; arr[active][name] = param; setList(arr); } // 表单传值-option function changeOptionValue(index, e) { let value = e && e.target ? e.target.value : e; let arr = [...formList]; let param = arr[active].options; param[index].value = value; setList(arr); } function changeOptionLabel(index, e) { let value = e && e.target ? e.target.value : e; let arr = [...formList]; let param = arr[active].options; param[index].label = value; setList(arr); } // 数组转对象 function arrToObj1(arr) { return arr.reduce((obj, item, index) => { obj[index] = item; return obj; }, {}); } // fn 删除控件 function delItemFn() { let newList = [...formList]; newList.splice(active, 1); let newForm = newList.map((o) => o.value); let setform = {}; if (newForm.length > 0) { setform = arrToObj1(newForm); } props.form.setFieldsValue({ ...setform, }); setList(newList); setActive(-1); } // fn option 新增 function addOptionFn() { let index = formList[active]?.options?.length || 0; let param = { label: `选项${index}`, value: index, }; let newList = [...formList]; let options = newList[active].options; newList[active].options = [...options, param]; setList(newList); } // fn 删除 options function delOptionFn(index) { let newList = [...formList]; newList[active]?.options?.splice(index, 1); setList(newList); } // fn 排序 options function orderOptionFn(index, desc, disabled) { if (disabled) return; //排除无法换位的情况 let newList = [...formList]; let param = newList[active]?.options; let prev = param ? param[index] : ''; let newIndex; switch (desc) { case true: newIndex = param ? param[index + 1] : ''; newList[active]?.options?.splice(index + 1, 1, prev); break; default: newIndex = param ? param[index - 1] : ''; newList[active]?.options?.splice(index - 1, 1, prev); } newList[active]?.options?.splice(index, 1, newIndex); setList(newList); } // render options 选项卡 function renderOption() { return formList[active]?.options?.map((item, index) => { let max = (formList[active]?.options?.length || 1) - 1; return ( ); }); } return ( {/* 标签 */} 标签 {/* 是否必填 */} 是否必填 {/* 提示语 */} 提示语 {/* rule */} 限制规则 (option?.children).includes(input) } > {OptionsValidate.map((its) => { return ( {its.label} ); })} {/* options */} 选项 新增选项 你可以对选项进行新增修改删除操作 {renderOption()} 删除 ); }; export default Config; right-index.less @color: '#000'; @hover: '#0958d9'; @disabled: '#ccc'; @error: '#000'; .config { width: 30%; padding: 15px; border: 1px solid #f1e8e8; overflow: auto; &-item { margin-bottom: 10px; padding-bottom: 15px; border-bottom: 1px solid rgba(0, 0, 0, 0.07); &-title { color: #666; } &-option { display: flex; align-items: center; justify-content: space-between; margin-bottom: 5px; padding: 5px; user-select: none; cursor: move; input { border: none; outline: none; padding: 2px 5px; width: 30%; background-color: #fff; border: 1px solid #d9d9d9; border-radius: 5px; } &-edit { display: flex; align-items: center; > span { margin-left: 5px; } } &-disa { cursor: pointer; color: @color; &:hover { color: @hover; } } &-disabled { cursor: not-allowed; color: @disabled; } &-del { color: @error; cursor: pointer; } } } } right-If.jsx 控制下拉、单选、复选等选项数据值设置显影 const conditionContainer = (props) => { return props.show ? props.children : ''; }; export default conditionContainer; right-validate.js 表单校验规则 // 密码校验 export const validatePass = (rule, value) => { if (value === '') { return Promise.resolve(); } else { if (!/^(?:\d+|[a-zA-Z]+|[.!@#$%^&*]+){6,12}$/.test(value)) { return Promise.reject('请输入6-12位密码'); } else { return Promise.resolve(); } } }; // 数字 export const validateNumber = (rule, value) => { if (value != '') { if (!/^[1-9]\d*$/.test(value)) { return Promise.reject('只能输入数字'); } else { return Promise.resolve(); } } else { return Promise.resolve(); } }; // 金额 export const validatePrice = (rule, value) => { if (value != '' && value != undefined && value != null) { let num = parseFloat(value); if (isNaN(num)) { return Promise.reject('请输入有效金额'); } else { return Promise.resolve(); } } else { return Promise.resolve(); } }; // 不作为 export const validateNone = () => { return Promise.resolve(); }; // 真实姓名 export const validateName = (rule, value) => { if (value === '' || !value) { return Promise.resolve(); } else { if (!/^([\u4e00-\u9fa5]{1,20}|[a-zA-Z\.\s]{2,5})$/.test(value)) { return Promise.reject('请输入真实姓名'); } else { return Promise.resolve(); } } }; // 身份证 export const validateIdCard = (rule, value) => { if (value != '' && value != undefined) { if ( !/^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$|^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$/.test( value, ) ) { return Promise.reject('请输入有效身份证'); } else { return Promise.resolve(); } } else { return Promise.resolve(); } }; //手机号 export const validatePhone = (rule, value) => { if (value != '' && value != undefined) { if ( !/^1(3[0-9]|4[5,7]|5[0,1,2,3,5,6,7,8,9]|6[2,5,6,7]|7[0,1,7,8]|8[0-9]|9[1,8,9])\d{8}$/.test( value, ) ) { return Promise.reject('请输入有效手机号'); } else { return Promise.resolve(); } } else { return Promise.resolve(); } }; // 检测支付宝账号 export const validateZFB = (rule, value) => { if ( /^([a-zA-Z\d])(\w|\-)+@[a-zA-Z\d]+\.[a-zA-Z]{2,4}$/.test(value) || /^1[3456789]\d{9}$/.test(value) ) { return Promise.resolve(); } else { return Promise.reject('请输入有效支付宝账号'); } }; // 邮箱 export const validateEmail = (rule, value) => { let email = /^([a-zA-Z\d])(\w|\-)+@[a-zA-Z\d]+\.[a-zA-Z]{2,4}$/.test(value); if (email) { return Promise.resolve(); } else { return Promise.reject('请输入有效邮箱'); } }; export const OptionsValidate = [ { label: '密码校验', value: 'validatePass' }, { label: '数字', value: 'validateNumber' }, { label: '金额', value: 'validatePrice' }, { label: '不限制', value: 'validateNone' }, { label: '真实姓名', value: 'validateName' }, { label: '身份证', value: 'validateIdCard' }, { label: '手机号', value: 'validatePhone' }, { label: '支付宝账号', value: 'validateZFB' }, { label: '邮箱', value: 'validateEmail' }, ]; 大功告成!此时你就得到自定义表单拉!!! gitee代码地址

gitee代码地址 在这里插入图片描述



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有