表单设计器整体思路
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 中文文档
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 图标
修改:修改属性:
- text、
- 位置信息
- 宽高信息
动态文本元素:绑定数据源、字段
修改属性方面 ,要指定数据字段(最好采取下拉框选择?需要实现)
Table来说,
table层上,要做什么:
- 删除行列
- 追加行列
- 插入行列
- 调整列宽、行高度(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后提示违反许可证安全,禁止操作