用react实现页面动态表单设计器

本文介绍了一款采用flex布局的表单设计器实现思路,着重探讨了如何通过拖拽元素完成表单设计,并介绍了表单元素与数据绑定的方法。文章还讨论了布局树的构建与更新、表单模板的保存方式以及与元数据的交互。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 表单设计器整体思路


1、从数据局出发

2、从表单出发

3、拖动布局

grid布局还是flex布局实现呢?grid布局整体上更明快,flex方便一步步推进,2种方式也许都实现一遍,进行比对后再下结论。

前端形成模板后 怎么保存

保存整个模板?还是把结构分解描述为一个个字段保存到数据库?

从表单出发的页面设计器

数据可以和表单元素绑定:

 (元数据来源:)

既然需要和数据元素绑定,必须有一套元数据描述体系作为支撑。

元数据描述怎么来:

   1、手工维护(不动态生成数据库table)

   2、反射取得数据库表信息(能维护吗)

拖动到右边画布的动作?

通过行、列解决整体布局的问题


可能涉及如下操作dom方法?

1、html5 antd

2、react 操作dom
3、js  操作dom

4、es6 操作dom

5、react  jquery 混用(最好不这样)

用flex布局,Row的主轴设成列 Col的主轴设置成行?

行列交叉进行

整个布局为,左边设置工具栏,中间是画布,右边是属性栏目

整个动态布局背后是个树形结构,render方法,即使根据这个结构形成

全页面采取flex布局,层层嵌套(行、列交互嵌套)

每一个元素都是flex:  行元素必然放在列元素当中

列元素 必然放到行元素当中

每个元素都是flex元素  flex display

row 元素 

  • flex-direction:   column

column 元素:

flex-direction:   row

初始化的时候,只能放row,画布director 方向为column

 (整个布局为,左边设置工具栏,中间是画布,右边是属性栏目)

初始化的时候,只留一个div,这个div不可删除

                                        <Button type="primary" shape="circle"
                                                style={{display: node.id=="1"? "none": "" }}
                                                size="smaill"  onClick={()=>removeNode(_id)}>
                                            D
                                        </Button>

工具栏:


     工具栏布局后期可能需要改进,按grid 或者flex进行修正?

                <div style={ tool_area }>
                    <PageHeader
                        className="site-page-header"
                         title="工具"
                    />
                    <div style={textAlign}>


                    <IconFont id="rowIcon" type="icon-rows"  onDragStart={onDrop}   draggable="true"></IconFont>
                    </div>

                    <div style={textAlign}>
                        <IconFont id="colIcon" type="icon-column" onDragStart={onDrop}   draggable="true"></IconFont>
                    </div>


                    <div style={textAlign}>
                        <IconFont id="input" type="icon-input"></IconFont>
                    </div>


                    <div style={textAlign}>
                        <IconFont id="input1" type="icon-input1"></IconFont>
                    </div>
                </div>
拖放工具栏的图标,放在目标元素上的时候:
    const drop=(event)=>
    {
        let _id=event.currentTarget.id
        // 根据id找到树种的节点
        //给这个节点赋予新的子节点
        let newLayoutTree = deepCopy(layoutTree);
        let n=queryNode(newLayoutTree,_id);
        if(n==null  || n==undefined){
            return;
        }
        if(n.layoutType==null  || n.layoutType==undefined){
            return;
        }
        if(!(n.layoutType==1  || n.layoutType==2)){
            return;
        }
        let newNode=null;
        let _nodeId = newGuid();
        if(n.layoutType==1){
             newNode = new LayoutTreeNode(_nodeId,_nodeId,n.id,_nodeId,2,1,[]);  //布局树
        }else if(n.layoutType==2){
            newNode = new LayoutTreeNode(_nodeId,_nodeId,n.id,_nodeId,1,1,[]);  //布局树
        }
        n.children.push({...newNode});
        //设置
        console.log(newLayoutTree)
        setLayoutTree(newLayoutTree);
        stopBubbling(event)
    }

      这里需要注意的是,必须对原来的存放在state中的数据。进行deep copy,否则,render的时候,感知不到数据的变化。删除元素的时候,也面临似的问题,同样需要deep copy。

完整代码如下:

//动态表单设计器

import React, {useState} from 'react';
import {Button, Form, Input, message, PageHeader} from 'antd';
import {createFromIconfontCN} from '@ant-design/icons';

const ROW_TYPE=1;   //行类型
const COL_TYPE=2;   //列类型



//export default class Dynamic_Form_Designer2 extends Component {
export default function Dynamic_Form_Designer2(prop) {
    const  [form] = Form.useForm();


    /**flexGrow :flex grow 值**/
    const getRowLayoutStyle=(flexGrow)=>{
        return {flex: flexGrow+" 1 auto",
            // border: "1px solid #cccccc",
            paddingTop:"1em",paddingLeft:"0.3em",paddingRight:"1em",paddingBottom:"1em",
            margin:"0.5em",backgroundColor:"RGB(172,216,230)",boxSizing: "border-box",
            display:"flex",
            flexDirection:"row"
        }
     }

    const getColLayoutStyle=(flexGrow)=>{
        return {
            // border: "1px solid #cccccc",
            flex: flexGrow+" 1 auto",
            paddingTop:"1em",paddingLeft:"0.3em",paddingRight:"1em",paddingBottom:"1.5em",
            margin:"0.3em",backgroundColor:"RGB(135,206,235)",
            display: "flex",
            flexDirection:"column"
        }
    }

    const flexContentStyle={
        display: "flex",
        flexFlow:"column nowrap",
        border: "1px solid #cccccc",
        padding:"1em"
    }

    const titleStyle={
         height:"30px",
         width:"100%",
        background: "#999999"
    }

    const rowTool={
        display: "flex",
        flexDirection: "column",
        boxSizing: "border-box",
        border: "2px solid #999999"
    }


    const colTool={
        display: "flex",
        flexDirection: "row",
        boxSizing: "border-box",
        border: "2px solid #999999",
        justifyContent:"flex-end"
    }

    let _children=[];
    //id,key,parentId,name,layoutType,flexGrow,children,colSpan,showVale
    let layoutTreeNood1 = new LayoutTreeNode("1",1,"0","row1",1,3,[]);  //布局树
    let layoutTreeNood2 = new LayoutTreeNode("2",2,"0","row2",1,1,[]);  //布局树
    let layoutTreeNood3 = new LayoutTreeNode("3",3,"0","row3",1,2,[]);  //布局树
    let layoutTreeRoot = new LayoutTreeNode("0",0,"-1","root",0,1,_children);  //布局树
    _children.push(layoutTreeNood1);
    _children.push(layoutTreeNood2);
    _children.push(layoutTreeNood3);
    // id,key,parentId,name,layoutType,flexGrow,children
    let layoutTreeNood1_col1 = new LayoutTreeNode("11",11,"1","col11",2,1,[],6);  //布局树
    let layoutTreeNood1_col2 = new LayoutTreeNode("12",12,"1","col12",2,5,[],6);  //布局树
     layoutTreeNood1.getChildren().push(layoutTreeNood1_col1);
     layoutTreeNood1.getChildren().push(layoutTreeNood1_col2);
    //
    let layoutTreeNood1col2row1 = new LayoutTreeNode("111",111,"11","ttt",1,1,[]);
    let layoutTreeNood1col2row2 = new LayoutTreeNode("112",112,"11","ttt2",1,1,[]);
    layoutTreeNood1_col1.getChildren().push(layoutTreeNood1col2row1);
    layoutTreeNood1_col1.getChildren().push(layoutTreeNood1col2row2);

    const [layoutTree, setLayoutTree] = useState(layoutTreeRoot);
    const [flexGrowVal, setFlexGrowVal] = useState(1);
    const [flexShrinkVal, setFlexShrinkVal] = useState(1);
    const [flexBasisVal, setFlexBasisVal] = useState("auto");
    const [idVal, setIdVal] = useState("-1");


// 编辑flex
    const modifyFlex = (id) => {
        //根据info 设置state相关值
        //let _id=info.node.id
        let _id=id
        // 根据id查找节点
        let n=queryNode(layoutTree,_id);
        console.log("n:"+n)
        if(n!=null){
            //setItemId(_id);
            let _flexGrow=n.flexGrow;
            let _flexShrink=n.flexGrow;
            let _flexBasis=n.flexGrow;
            setFlexGrowVal(_flexGrow);
            setFlexShrinkVal(_flexShrink);
            setFlexBasisVal(_flexBasis);
            setIdVal(_id)
            form.setFieldsValue({
                "flexGrowItem":_flexGrow,
                "flexShrinkItem":_flexShrink,
                "flexBasisItem":_flexBasis,
                "idItem": _id
            })
        }else{
            message.error("未查询到对应的节点")
        }
    }

    const renderLayout=(nodeObj)=>{
        console.log(nodeObj);
        let ns=nodeObj.children
        let _cells = [];
        let  i=0
        if(ns==null) return '' ;
        for(let node of ns){
            console.log(node);
            if(node.layoutType==1){  //ROW

                        let _flexGrow= node.flexGrow;
                        let _id= node.id;
                        console.log("_flexGrow:"+_flexGrow);
                        _cells.push(<div  id={node.id} style={getRowLayoutStyle(node.flexGrow)} key={i}    onDrop={drop}   onDragOver={allowDrop}>
                                    <div style={rowTool}>
                                        <Button type="primary" shape="circle"  size="smaill"
                                                onClick={()=>modifyFlex(_id)}>
                                            E
                                        </Button>
                                        <Button type="primary" shape="circle"
                                                style={{display: node.id=="1"? "none": "" }}
                                                size="smaill"  onClick={()=>removeNode(_id)}>
                                            D
                                        </Button>
                                    </div>
                                     {renderLayout(node)}
                                   </div>)

                } else if(node.layoutType==2) {  //COL
                    let _flexGrowForCol= node.flexGrow;
                    let _idForCol= node.id;

                    _cells.push(<div id={node.id}  key={node.id}  style={getColLayoutStyle(node.flexGrow)}   onDrop={drop} onDragOver={allowDrop} >
                        <div style={colTool}>
                            <Button type="primary" shape="circle"  size="smaill"
                                    onClick={()=>modifyFlex(_idForCol)}>
                            E
                            </Button>
                            <Button type="primary" shape="circle"  size="smaill"    onClick={()=>removeNode(_idForCol)}>
                                D
                            </Button>
                        </div>

                        {/*<div style={this.titleStyle}>col</div>*/}
                        {renderLayout(node)}
                    </div>)
                };
                i++;
                }
            return  _cells;
        }

    //设置树形节点属性
    const setNodeProperty=(rootNode,currentNode)=>{

        let ns=rootNode.children
        let _cells = [];
        let  i=0
        if(ns==null) return '' ;
        for(let node of ns){
            if(currentNode.id==node.id){  //当前节点
                //进行相关处理
                //node.flexGrow=currentNode.flexGrow
                let _g=node.flexGrow=currentNode.flexGrow
                node.flexGrow=+_g;   //通过+转换为数字
                return  ;
            }else{  //判断是否有子节点?  有则进行递归调用
                //判断是否有子节点  如果有子节点 则递归 否则返回
               if(node.children==null ||  node.children.length==0){
                    continue;
               }else{
                   setNodeProperty(node,currentNode)
               }
            }
        }
        return;
    }

    const allowDrop=(event)=>{
        event.preventDefault();
    };
    //根据id查找node
    const  queryNode=(root,nodeId)=>{
        let nodeObj=null;
        // 根节点的情况
        if (nodeId=='0'){
            return  root;
        }
        for(var currentNode of root.children){
            if(nodeId==currentNode.id){  //当前节点
                nodeObj= currentNode;
                break;
            }else{  //判断是否有子节点?  有则进行递归调用
                //判断是否有子节点  如果有子节点 则递归 否则返回
                if(currentNode.children==null ||  currentNode.children.length==0){
                    continue;
                }else{
                    nodeObj=queryNode(currentNode,nodeId)
                    if(nodeObj!=null)  break;
                }
            }
        }
        return nodeObj;
    }

    function deepCopy(obj1) {
        let _obj = JSON.stringify(obj1);
        let obj2 = JSON.parse(_obj);
        return obj2;
    }

    const IconFont =createFromIconfontCN({
        scriptUrl: '/iconfont.js'
    })

    const myLayout={
        flex: "1 1 0%",
        boxSizing: "border-box",
        display: 'flex',
        flexDirection: 'row',
    }

    const myLayout_1={
        backgroundColor: "#cc99sc"
    }


    // 列布局
    const flex_Col={
        display: 'flex',
        flexDirection: 'row'
    }

    const tool_area={
        flex: "0 0 80px",
        boxSizing: "border-box",
        border:"2px solid #999999",
        margin:"5px",
        padding: "2px",
        display:"flex",
        flexDirection: "column"
    }

    const content_area={
        flex: "1 1 0%",
        boxSizing: "border-box",
        border:"2px solid #999999",
        margin:"5px",
        display:"flex",
        flexDirection:"column"
    }

    const property_area={
        flex: "0 0 150px",

        boxSizing: "border-box"
    }

    const textAlign={
        textAlign:"center"
    }
    // //删除节点
    // 查找本节点 找到parent Node
    // 从父节点child集合当中删除子节点
    const removeNode=(nodeId)=>{
        let rootNodeCopy = deepCopy(layoutTree);

        let n=queryNode(rootNodeCopy,nodeId);
        let _parentId=n.parentId;
        let parentNode=queryNode(rootNodeCopy,_parentId);
        parentNode.children.map((item,index)=>{
            if(item.id==n.id){
                parentNode.children.splice(index,1)
            }
        })
        setLayoutTree(rootNodeCopy)
    }

    const onDrop = (event) => {
        console.log("begin drop:"+event)
        // event.event.dataTransfer.setData('memberName', event.node.text);
        // event.event.dataTransfer.setData('memberId', event.node.id);
    }


    const drop=(event)=>
    {
        let _id=event.currentTarget.id
        // 根据id找到树种的节点
        //给这个节点赋予新的子节点
        let newLayoutTree = deepCopy(layoutTree);
        let n=queryNode(newLayoutTree,_id);
        if(n==null  || n==undefined){
            return;
        }
        if(n.layoutType==null  || n.layoutType==undefined){
            return;
        }
        if(!(n.layoutType==1  || n.layoutType==2)){
            return;
        }
        let newNode=null;
        let _nodeId = newGuid();
        if(n.layoutType==1){
             newNode = new LayoutTreeNode(_nodeId,_nodeId,n.id,_nodeId,2,1,[]);  //布局树
        }else if(n.layoutType==2){
            newNode = new LayoutTreeNode(_nodeId,_nodeId,n.id,_nodeId,1,1,[]);  //布局树
        }
        n.children.push({...newNode});
        //设置
        console.log(newLayoutTree)
        setLayoutTree(newLayoutTree);
        stopBubbling(event)
    }




    //stop Bubbling
    const  stopBubbling =(e)=>
    {
        if (e && e.stopPropagation){
            e.stopPropagation()
        }else{
            window.event.cancelBubble=true;
        }
    }


    function newGuid(){
        var guid = "";
        for (var i=1; i<=32; i++){
            var n = Math.floor(Math.random()*16.0).toString(16);
            guid +=   n;
            if((i==8)||(i==12)||(i==16)||(i==20))
                guid += "-";
        }
        return guid+"";
    }

    const FlexItemProperty = () => {

        const onFinish = (values) => {
            console.log('Success:', values);
            let _Itemid=values.idItem;
            let _flexGrow=values.flexGrowItem;
            let _flexShrink=values.flexShrinkItem;
            let _flexBasis=values.flexBasisItem;
            //找到对应节点 进行设置  根据id 找到所有的已恶点   遍历树 进行设置
            if(values.idItem==='-1'){
                message.info("请选中相应节点")
                return
            }
            let  currentNode=new LayoutTreeNode(_Itemid,-1,-1,"",-1,_flexGrow,[]);
            console.log("test0:"+layoutTree)
            let rootNodeCopy = deepCopy(layoutTree);
            setNodeProperty(rootNodeCopy,currentNode);
            console.log("===================================================="+layoutTreeRoot)
            //setLayoutTree(layoutTreeRoot);
            setLayoutTree(rootNodeCopy);
            console.log("test1:"+layoutTree)
        };

        const onFinishFailed = (errorInfo) => {
            console.log('Failed:', errorInfo);
        };



        const onReset = () => {
            form.resetFields();
        };

        return (
            <Form
                name="basic"
                form={form}
                labelCol={{
                    span: 12,
                }}
                wrapperCol={{
                    span: 12,
                }}
                initialValues={{
                    idItem:idVal,
                    flexGrowItem:flexGrowVal,
                    flexShrinkItem:flexShrinkVal,
                    flexBasisItem:flexBasisVal
                }}
                onFinish={onFinish}
                onFinishFailed={onFinishFailed}
            >

                <Form.Item
                    label="id"
                    name="idItem"
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    label="grow"
                    name="flexGrowItem"
                    rules={[
                        {
                            required: true,
                            message: 'Please input the flex-grow!',
                        },
                    ]}
                >
                    <Input />
                </Form.Item>


                <Form.Item
                    label="Shrink"
                    name="flexShrinkItem"
                    rules={[
                        {
                            required: true,
                            message: 'Please input the flex-shrink!',
                        },
                    ]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    label="basis"
                    name="flexBasisItem"
                    rules={[
                        {
                            required: true,
                            message: 'Please input flex-basis!',
                        },
                    ]}
                >
                    <Input />
                </Form.Item>

                <Form.Item
                    wrapperCol={{
                        offset: 8,
                        span: 16,
                    }}
                >
                    <Button type="primary" htmlType="submit">
                        Submit
                    </Button>

                    {/*<Button htmlType="button" onClick={onReset}>*/}
                    {/*Reset*/}
                    {/*</Button>*/}

                </Form.Item>
            </Form>
        );
    }


        return (
            <div style={myLayout}>
                <div style={ tool_area }>
                    <PageHeader
                        className="site-page-header"
                         title="工具"
                    />
                    <div style={textAlign}>


                    <IconFont id="rowIcon" type="icon-rows"  onDragStart={onDrop}   draggable="true"></IconFont>
                    </div>

                    <div style={textAlign}>
                        <IconFont id="colIcon" type="icon-column" onDragStart={onDrop}   draggable="true"></IconFont>
                    </div>


                    <div style={textAlign}>
                        <IconFont id="rowIcon" type="icon-rows"></IconFont>
                    </div>


                    <div style={textAlign}>
                        <IconFont id="rowIcon" type="icon-rows"></IconFont>
                    </div>
                </div>
                <div id = 'rootPanel'   style={ content_area }    onDrop={drop}   onDragOver={allowDrop}>
                    {renderLayout(layoutTree)}
                </div>
                <div style={ property_area }>

                    {FlexItemProperty()}

                </div>
            </div>
        )

}

//type  0 是根  1 是row  2 是col
class LayoutTreeNode{
    constructor(id,key,parentId,name,layoutType,flexGrow,children,colSpan,showVale,position)
    {
        this.id=id;  // 数值
        this.key=key;  // 数值
        this.parentId=parentId;  // 数值
        this.name=name;
        this.layoutType=layoutType;
        this.children=children;  // 用数组的方式保存子节点,适合更多业务场景
        //this.flexValue="1 1 auto";
        this.flexGrow=flexGrow;
        this.colSpan=colSpan;
        this.position=position;
    }

    setId(id){
        this.id=id;
    }
    getId(){
        return this.id
    }
    getKey(){
        return this.key
    }
    getName(){
        return this.name
    }
    getParentId(){
        return this.parentId
    }
    getLayoutType(){
        return this.layoutType
    }
    getChildren(){
        return this.children
    }
    getFlexGrow(){
        return this.flexGrow
    }
}

属性面板:


点击画布中元素工具图标的时候,右边出现相应的属性面板,我通过路由实现这个功能:

路由的使用:A Complete Beginner's Guide to React Router (Including Router Hooks)

通过路由link获取面板。同时传递点击元素的id

首先传递元素的id及layoutTree,  属性 页面根据元素的属性,初始化页面。

layoutTree如何传递呢,是vuex还是直接通过prop传递呢,考虑到多个页面共用这个,用一下React Redux,是否完全合理,没有深入思考。

ps:React Redux 中文文档 | React Redux 中文文档

Redux工具包

在这里插入图片描述

 React Components:在组件中要“做什么”,把需要做的事情告诉Action Creators(“Action的创建者们”,)行为创建器。
Action Creators:将需要做的事情包装成一个动作对象,可以省去,自己直接创建一个动作对象。action在redux中被称为动作对象(js中的Object一般对象),action包括type:动作类型、data:动作值。
dispatch(分发)为一个函数,生成action动作对象后,需要一个函数将动作对象继续传递下去,否则这个动作就此终止,即deispatch将Action继续“分发”下去,交给Store
Store:为redux的核心,可以理解为一个调用者,负责对全局的掌控。Store本身并不执行操作,也不加工状态,只是一个调用者,Action对象交给Store后不停留,会被Store交给加工者Reducers
Reducers:为redux的实际加工者,可以有多个。Store将previousState和Action对象交给Reducers,previousState表示之前的状态,要在之前的状态上进行action操作。Reducers操作完毕后要向Store返回一个新的状态即newState
getState():组件通过getState()方法拿到操作后的最新的结果,state变化重新渲染组件
注意:状态初始化时没有previousState参数,只会在此位置传递undefined,只有在第二次及之后的加工中才会有

ps:React -- redux详解

ps:调试react,有个浏览器插件,react-devTools,根据github上构建这个插件,安装在chromel浏览器报错:Manifest version 2 is deprecated, and support will be removed in 2023. See https://developer.chrome.com/blog/mv2-transition/ for more details.

好像是新的chromel浏览器不支持react-devTools的某种协议了?

那就用firefox调试react!

前端:(排除字体 对齐 颜色等CSS属性)

第一期出现动态布局三个元素:

  • label元素:
  • 动态文本元素:(针对主表字段)和后端数据绑定
  • table

label: 增加 删除 修改

增加:在pallet中单击label tag,添加label

删除:点击remove 图标

修改:修改属性:

  1. text、
  2. 位置信息 
  3. 宽高信息

动态文本元素:绑定数据源、字段

修改属性方面 ,要指定数据字段(最好采取下拉框选择?需要实现)

Table来说,

table层上,要做什么:

  1. 删除行列
  2.  追加行列
  3. 插入行列
  4.  调整列宽、行高度(ok)

table加标尺:调整列宽、行高度

追加列的实现:

遍历header、detail、footer三个区域,追加最后一列的元素

追加行:

分区域 ,坐标参考系:row插入,

行坐标系+append;

插入一个整行  就是行数

列只要遍历行既可

如果是行头,行头追加 则把后面的明细区 表脚区单元格的startrow+1, endrow+1

如果是明细区,行头追加 则把后面的叶脚区 表脚区单元格 的startrow+1, endrow+1

如果是表脚区,则追 startrow+1, endrow+1

准备数据:

追加:

保存:

追加行:

// 表头有几行 表明细有几行  表脚有几行
              let _headerRowHeightArray= theTable.rowHeights.slice(0,theTable.headerRowNum-1);
              let _detailRowHeightArray= theTable.rowHeights.slice(theTable.headerRowNum,
                theTable.headerRowNum+theTable.detailRowNum-1);
              let _footerRowHeightArray= theTable.rowHeights.slice(theTable.headerRowNum+theTable.detailRowNum,
                theTable.rowHeights.length-1);

测试:

ps:Loadrunner11--输入license后提示违反许可证安全,禁止操作


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值