updates
This commit is contained in:
		
							parent
							
								
									46e4089140
								
							
						
					
					
						commit
						8b34b84a5e
					
				
							
								
								
									
										329
									
								
								packages/client/src/blocks/DesignableSchemaField/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								packages/client/src/blocks/DesignableSchemaField/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,329 @@ | ||||
| import React, { | ||||
|   createContext, | ||||
|   useCallback, | ||||
|   useContext, | ||||
|   useMemo, | ||||
|   useState, | ||||
| } from 'react'; | ||||
| import { createForm } from '@formily/core'; | ||||
| import { | ||||
|   Field, | ||||
|   ISchema, | ||||
|   observer, | ||||
|   Schema, | ||||
|   createSchemaField, | ||||
|   FormProvider, | ||||
|   useField, | ||||
|   useFieldSchema, | ||||
| } from '@formily/react'; | ||||
| import { observable } from '@formily/reactive'; | ||||
| import { uid, clone } from '@formily/shared'; | ||||
| import { ArrayCollapse, ArrayTable, FormLayout } from '@formily/antd'; | ||||
| 
 | ||||
| import { Space, Card } from 'antd'; | ||||
| import { Action, useLogin, useRegister, useSubmit } from '../action'; | ||||
| import { AddNew } from '../add-new'; | ||||
| import { Cascader } from '../cascader'; | ||||
| import { Checkbox } from '../checkbox'; | ||||
| import { ColorSelect } from '../color-select'; | ||||
| import { DatabaseField } from '../database-field'; | ||||
| import { DatePicker } from '../date-picker'; | ||||
| import { DrawerSelect } from '../drawer-select'; | ||||
| import { Filter } from '../filter'; | ||||
| import { Form } from '../form'; | ||||
| import { Grid } from '../grid'; | ||||
| import { IconPicker } from '../icon-picker'; | ||||
| import { Input } from '../input'; | ||||
| import { InputNumber } from '../input-number'; | ||||
| import { Markdown } from '../markdown'; | ||||
| import { Menu } from '../menu'; | ||||
| import { Password } from '../password'; | ||||
| import { Radio } from '../radio'; | ||||
| import { Select } from '../select'; | ||||
| import { Table } from '../table'; | ||||
| import { Tabs } from '../tabs'; | ||||
| import { TimePicker } from '../time-picker'; | ||||
| import { Upload } from '../upload'; | ||||
| import { FormItem } from '../form-item'; | ||||
| 
 | ||||
| export const BlockContext = createContext({ dragRef: null }); | ||||
| 
 | ||||
| const Div = (props) => <div {...props} />; | ||||
| 
 | ||||
| export const scope = { | ||||
|   useLogin, | ||||
|   useRegister, | ||||
|   useSubmit, | ||||
| }; | ||||
| 
 | ||||
| export const components = { | ||||
|   Div, | ||||
|   Space, | ||||
|   Card, | ||||
| 
 | ||||
|   ArrayCollapse, | ||||
|   ArrayTable, | ||||
|   FormLayout, | ||||
|   FormItem, | ||||
| 
 | ||||
|   Action, | ||||
|   AddNew, | ||||
|   Cascader, | ||||
|   Checkbox, | ||||
|   ColorSelect, | ||||
|   DatabaseField, | ||||
|   DatePicker, | ||||
|   DrawerSelect, | ||||
|   Filter, | ||||
|   Form, | ||||
|   Grid, | ||||
|   IconPicker, | ||||
|   Input, | ||||
|   InputNumber, | ||||
|   Markdown, | ||||
|   Menu, | ||||
|   Password, | ||||
|   Radio, | ||||
|   Select, | ||||
|   Table, | ||||
|   Tabs, | ||||
|   TimePicker, | ||||
|   Upload, | ||||
| }; | ||||
| 
 | ||||
| export function registerScope(scopes) { | ||||
|   Object.keys(scopes).forEach((key) => { | ||||
|     scope[key] = scopes[key]; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function registerComponents(values) { | ||||
|   Object.keys(values).forEach((key) => { | ||||
|     components[key] = values[key]; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export interface DesignableContextProps { | ||||
|   schema: Schema; | ||||
|   refresh: () => void; | ||||
| } | ||||
| 
 | ||||
| export const DesignableContext = createContext<DesignableContextProps>({ | ||||
|   schema: null, | ||||
|   refresh: null, | ||||
| }); | ||||
| 
 | ||||
| export function pathToArray(path): string[] { | ||||
|   if (Array.isArray(path)) { | ||||
|     return [...path]; | ||||
|   } | ||||
| 
 | ||||
|   if (typeof path === 'string') { | ||||
|     return path.split('.'); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function findPropertyByPath(schema: Schema, path?: any): Schema { | ||||
|   if (!path) { | ||||
|     return schema; | ||||
|   } | ||||
|   const arr = pathToArray(path); | ||||
|   let property = schema; | ||||
|   while (arr.length) { | ||||
|     const name = arr.shift(); | ||||
|     property = property.properties[name]; | ||||
|     if (!property) { | ||||
|       console.error('property does not exist.'); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   return property; | ||||
| } | ||||
| 
 | ||||
| export function addPropertyBefore(target: Schema, data: ISchema) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(data.name, data); | ||||
|     } | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function addPropertyAfter(target: Schema, data: ISchema) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(data.name, data); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function useDesignable(path?: any) { | ||||
|   const { schema, refresh } = useContext(DesignableContext); | ||||
|   const schemaPath = path || useSchemaPath(); | ||||
|   const currentSchema = findPropertyByPath(schema, schemaPath); | ||||
|   console.log('useDesignable', { schema, schemaPath, currentSchema }); | ||||
|   return { | ||||
|     schema: currentSchema, | ||||
|     refresh, | ||||
|     appendChild: (property: ISchema, targetPath?: any): Schema => { | ||||
|       let target = currentSchema; | ||||
|       if (targetPath) { | ||||
|         target = findPropertyByPath(schema, targetPath); | ||||
|       } | ||||
|       if (!target) { | ||||
|         console.error('target schema does not exist.'); | ||||
|         return; | ||||
|       } | ||||
|       if (!property.name) { | ||||
|         property.name = uid(); | ||||
|       } | ||||
|       target.addProperty(property.name, property); | ||||
|       // BUG: 空 properties 时,addProperty 无反应。
 | ||||
|       const tmp = { name: uid() }; | ||||
|       addPropertyAfter(target, tmp); | ||||
|       target.parent.removeProperty(tmp.name); | ||||
|       refresh(); | ||||
|       return target.properties[property.name]; | ||||
|     }, | ||||
|     insertAfter: (property: ISchema, targetPath?: any): Schema => { | ||||
|       let target = currentSchema; | ||||
|       if (targetPath) { | ||||
|         target = findPropertyByPath(schema, targetPath); | ||||
|       } | ||||
|       if (!target) { | ||||
|         console.error('target schema does not exist.'); | ||||
|         return; | ||||
|       } | ||||
|       if (!property.name) { | ||||
|         property.name = uid(); | ||||
|       } | ||||
|       addPropertyAfter(target, property); | ||||
|       refresh(); | ||||
|       return target.parent.properties[property.name]; | ||||
|     }, | ||||
|     insertBefore(property: ISchema, targetPath?: any): Schema { | ||||
|       let target = currentSchema; | ||||
|       if (targetPath) { | ||||
|         target = findPropertyByPath(schema, targetPath); | ||||
|       } | ||||
|       if (!target) { | ||||
|         console.error('target schema does not exist.'); | ||||
|         return; | ||||
|       } | ||||
|       if (!property.name) { | ||||
|         property.name = uid(); | ||||
|       } | ||||
|       addPropertyBefore(target, property); | ||||
|       refresh(); | ||||
|       return target.parent.properties[property.name]; | ||||
|     }, | ||||
|     remove(targetPath?: any) { | ||||
|       let target = currentSchema; | ||||
|       if (targetPath) { | ||||
|         target = findPropertyByPath(schema, targetPath); | ||||
|       } | ||||
|       if (!target) { | ||||
|         console.error('target schema does not exist.'); | ||||
|         return; | ||||
|       } | ||||
|       target.parent.removeProperty(target.name); | ||||
|       refresh(); | ||||
|       return target; | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function useSchemaPath() { | ||||
|   const schema = useFieldSchema(); | ||||
|   const path = [schema.name]; | ||||
|   let parent = schema.parent; | ||||
|   while (parent) { | ||||
|     if (!parent.name) { | ||||
|       break; | ||||
|     } | ||||
|     path.unshift(parent.name); | ||||
|     parent = parent.parent; | ||||
|   } | ||||
|   return [...path]; | ||||
| } | ||||
| 
 | ||||
| console.log({ scope, components }); | ||||
| 
 | ||||
| export const createDesignableSchemaField = (options) => { | ||||
|   const SchemaField = createSchemaField(options); | ||||
| 
 | ||||
|   const DesignableSchemaField = (props) => { | ||||
|     const schema = useMemo(() => new Schema(props.schema), [props.schema]); | ||||
|     const [, refresh] = useState(0); | ||||
|     if (props.designable === false) { | ||||
|       return <SchemaField schema={schema} />; | ||||
|     } | ||||
|     return ( | ||||
|       <DesignableContext.Provider | ||||
|         value={{ | ||||
|           schema, | ||||
|           refresh: () => { | ||||
|             refresh(Math.random()); | ||||
|             props.onRefresh && props.onRefresh(schema); | ||||
|           }, | ||||
|         }} | ||||
|       > | ||||
|         <SchemaField schema={schema} /> | ||||
|       </DesignableContext.Provider> | ||||
|     ); | ||||
|   }; | ||||
| 
 | ||||
|   return DesignableSchemaField; | ||||
| }; | ||||
| 
 | ||||
| export const DesignableSchemaField = createDesignableSchemaField({ | ||||
|   scope, | ||||
|   components, | ||||
| }); | ||||
| 
 | ||||
| export interface SchemaRendererProps { | ||||
|   schema: ISchema; | ||||
|   form?: any; | ||||
|   designable?: boolean; | ||||
|   onRefresh?: any; | ||||
|   onlyRenderProperties?: boolean; | ||||
| } | ||||
| 
 | ||||
| export const SchemaRenderer = (props: SchemaRendererProps) => { | ||||
|   const form = useMemo(() => props.form || createForm({}), []); | ||||
| 
 | ||||
|   const schema = useMemo(() => { | ||||
|     let s = props.schema; | ||||
|     if (props.onlyRenderProperties) { | ||||
|       s = { | ||||
|         type: 'object', | ||||
|         properties: s.properties, | ||||
|       }; | ||||
|     } else if (s.name) { | ||||
|       s = { | ||||
|         type: 'object', | ||||
|         properties: { | ||||
|           [s.name]: s, | ||||
|         }, | ||||
|       }; | ||||
|     } | ||||
|     return s; | ||||
|   }, []); | ||||
| 
 | ||||
|   console.log('SchemaRenderer', schema, props.schema); | ||||
| 
 | ||||
|   return ( | ||||
|     <FormProvider form={form}> | ||||
|       <DesignableSchemaField | ||||
|         onRefresh={props.onRefresh} | ||||
|         designable={props.designable} | ||||
|         schema={schema} | ||||
|       /> | ||||
|     </FormProvider> | ||||
|   ); | ||||
| }; | ||||
| @ -47,13 +47,13 @@ export const BlockContext = createContext({ dragRef: null }); | ||||
| 
 | ||||
| const Div = (props) => <div {...props} />; | ||||
| 
 | ||||
| const scope = { | ||||
| export const scope = { | ||||
|   useLogin, | ||||
|   useRegister, | ||||
|   useSubmit, | ||||
| }; | ||||
| 
 | ||||
| const components = { | ||||
| export const components = { | ||||
|   Div, | ||||
|   Space, | ||||
|   Card, | ||||
|  | ||||
| @ -27,7 +27,7 @@ group: | ||||
|  * desc: 可以通过配置 `useAction` 来处理操作逻辑 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock, registerScope } from '../'; | ||||
| import { SchemaRenderer, registerScope } from '../'; | ||||
| 
 | ||||
| function useCustomAction() { | ||||
|   return { | ||||
| @ -52,7 +52,7 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| @ -60,7 +60,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -73,7 +73,7 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| @ -81,7 +81,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -94,7 +94,7 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| @ -102,7 +102,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -110,7 +110,7 @@ const schema = { | ||||
|   title: '按钮', | ||||
|   'x-component': 'Action', | ||||
|   properties: { | ||||
|     drawer1: { | ||||
|     popover1: { | ||||
|       type: 'void', | ||||
|       title: '弹窗标题', | ||||
|       'x-component': 'Action.Popover', | ||||
| @ -126,7 +126,7 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| @ -134,7 +134,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -179,7 +179,7 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| @ -187,7 +187,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -207,15 +207,16 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Action.Container - 指定容器内打开 | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import React, { useRef } from 'react'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| import { ActionContext } from './'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -223,17 +224,15 @@ const schema = { | ||||
|   title: '按钮', | ||||
|   'x-component': 'Action', | ||||
|   properties: { | ||||
|     drawer1: { | ||||
|     container1: { | ||||
|       type: 'void', | ||||
|       title: '页面标题', | ||||
|       'x-component': 'Action.Container', | ||||
|       'x-component-props': { | ||||
|         container: '#container' | ||||
|       }, | ||||
|       properties: { | ||||
|         input: { | ||||
|           type: 'string', | ||||
|           title: '字段', | ||||
|           'x-designable-bar': 'FormItem.DesignableBar', | ||||
|           'x-decorator': 'FormItem', | ||||
|           'x-component': 'Input', | ||||
|         } | ||||
| @ -243,11 +242,17 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   const ref = useRef(); | ||||
|   console.log('containerRef2222', ref) | ||||
|   return ( | ||||
|     <div> | ||||
|       <SchemaBlock schema={schema} /> | ||||
|       <ActionContext.Provider value={{ | ||||
|         containerRef: ref, | ||||
|       }}> | ||||
|         <SchemaRenderer schema={schema} /> | ||||
|       </ActionContext.Provider> | ||||
|       <div style={{padding: '8px 0'}}>目标容器:</div> | ||||
|       <div id={'container'} style={{ border: '1px dashed #ebedf1', background: '#fafafa', padding: 24 }}/> | ||||
|       <div ref={ref} id={'container'} style={{ border: '1px dashed #ebedf1', background: '#fafafa', padding: 24 }}/> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| @ -257,7 +262,7 @@ export default () => { | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'void', | ||||
| @ -303,7 +308,6 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
|  | ||||
| @ -1,12 +1,4 @@ | ||||
| import React, { useContext, useState } from 'react'; | ||||
| import { | ||||
|   Input, | ||||
|   FormItem, | ||||
|   FormButtonGroup, | ||||
|   Submit, | ||||
|   Password, | ||||
| } from '@formily/antd'; | ||||
| import { createForm } from '@formily/core'; | ||||
| import React, { createContext, useContext, useState } from 'react'; | ||||
| import { | ||||
|   useForm, | ||||
|   FormProvider, | ||||
| @ -21,56 +13,16 @@ import { | ||||
| import { Button, Dropdown, Menu, Popover, Space } from 'antd'; | ||||
| import { Link, useHistory, LinkProps } from 'react-router-dom'; | ||||
| import Drawer from '../../components/Drawer'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer, useDesignable } from '../'; | ||||
| import ReactDOM from 'react-dom'; | ||||
| import get from 'lodash/get'; | ||||
| import { | ||||
|   DesignableSchemaContext, | ||||
|   RefreshDesignableSchemaContext, | ||||
| } from '../SchemaField'; | ||||
| import { | ||||
|   MenuOutlined, | ||||
|   GroupOutlined, | ||||
|   PlusOutlined, | ||||
|   LinkOutlined, | ||||
|   AppstoreAddOutlined, | ||||
|   EditOutlined, | ||||
|   DeleteOutlined, | ||||
|   ArrowRightOutlined, | ||||
|   SettingOutlined, | ||||
|   ArrowUpOutlined, | ||||
|   ArrowDownOutlined, | ||||
|   LoadingOutlined, | ||||
| } from '@ant-design/icons'; | ||||
| import { MenuOutlined } from '@ant-design/icons'; | ||||
| import classNames from 'classnames'; | ||||
| import { useMount } from 'ahooks'; | ||||
| import { uid } from '@formily/shared'; | ||||
| 
 | ||||
| import './style.less'; | ||||
| 
 | ||||
| export function useSchemaQuery(segments?: any[]) { | ||||
|   const context = useContext(DesignableSchemaContext); | ||||
|   const refresh = useContext(RefreshDesignableSchemaContext); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const field = useField(); | ||||
| 
 | ||||
|   const getSchemaByPath = (path) => { | ||||
|     let s: Schema = context; | ||||
|     const names = [...path]; | ||||
|     // names.shift();
 | ||||
|     while (s && names.length) { | ||||
|       const name = names.shift(); | ||||
|       s = s.properties[name]; | ||||
|     } | ||||
|     return s; | ||||
|   }; | ||||
| 
 | ||||
|   const schema = getSchemaByPath(segments || field.address.segments); | ||||
| 
 | ||||
|   return { | ||||
|     refresh, | ||||
|     schema, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function useDefaultAction() { | ||||
|   return { | ||||
|     run() {}, | ||||
| @ -121,6 +73,8 @@ export type ActionType = React.FC<ActionProps> & { | ||||
|   Link?: React.FC<LinkProps>; | ||||
|   URL?: React.FC<any>; | ||||
|   Page?: React.FC<any>; | ||||
|   Container?: React.FC<any>; | ||||
|   Popover?: React.FC<any>; | ||||
|   Drawer?: React.FC<any>; | ||||
|   Modal?: React.FC<any>; | ||||
|   Dropdown?: React.FC<any>; | ||||
| @ -141,13 +95,40 @@ function useDesignableBar() { | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const ActionContext = createContext({ containerRef: null }); | ||||
| 
 | ||||
| export const Action: ActionType = observer((props) => { | ||||
|   const { useAction = useDefaultAction, ...others } = props; | ||||
|   const { containerRef } = useContext(ActionContext); | ||||
|   const field = useField(); | ||||
|   const { schema } = useSchemaQuery(); | ||||
|   const schema = useFieldSchema(); | ||||
|   const { run } = useAction(); | ||||
|   const { DesignableBar } = useDesignableBar(); | ||||
| 
 | ||||
|   const renderContainer = () => { | ||||
|     let childSchema = null; | ||||
|     if (schema.properties) { | ||||
|       const key = Object.keys(schema.properties).shift(); | ||||
|       const current = schema.properties[key]; | ||||
|       childSchema = current; | ||||
|     } | ||||
|     if (childSchema && childSchema['x-component'] === 'Action.Container') { | ||||
|       containerRef && | ||||
|         ReactDOM.render( | ||||
|           <div> | ||||
|             <SchemaRenderer schema={childSchema} onlyRenderProperties /> | ||||
|           </div>, | ||||
|           containerRef.current, | ||||
|         ); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   useMount(() => { | ||||
|     renderContainer(); | ||||
|   }); | ||||
| 
 | ||||
|   let childSchema = null; | ||||
| 
 | ||||
|   if (schema.properties) { | ||||
|     const key = Object.keys(schema.properties).shift(); | ||||
|     const current = schema.properties[key]; | ||||
| @ -162,7 +143,7 @@ export const Action: ActionType = observer((props) => { | ||||
|         {...childSchema['x-component-props']} | ||||
|         content={ | ||||
|           <div> | ||||
|             <SchemaBlock schema={childSchema} onlyRenderProperties /> | ||||
|             <SchemaRenderer schema={childSchema} onlyRenderProperties /> | ||||
|           </div> | ||||
|         } | ||||
|       > | ||||
| @ -170,6 +151,7 @@ export const Action: ActionType = observer((props) => { | ||||
|       </Popover> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <Button | ||||
|       {...others} | ||||
| @ -184,25 +166,14 @@ export const Action: ActionType = observer((props) => { | ||||
|             content: () => { | ||||
|               return ( | ||||
|                 <div> | ||||
|                   <SchemaBlock schema={childSchema} onlyRenderProperties /> | ||||
|                   <SchemaRenderer schema={childSchema} onlyRenderProperties /> | ||||
|                 </div> | ||||
|               ); | ||||
|             }, | ||||
|           }); | ||||
|         } | ||||
|         if (childSchema['x-component'] === 'Action.Container') { | ||||
|           const el = document.createElement('div'); | ||||
|           const target = document.querySelector( | ||||
|             childSchema['x-component-props']?.['container'], | ||||
|           ); | ||||
|           target.childNodes.forEach((child) => child.remove()); | ||||
|           target.appendChild(el); | ||||
|           ReactDOM.render( | ||||
|             <div> | ||||
|               <SchemaBlock schema={childSchema} onlyRenderProperties /> | ||||
|             </div>, | ||||
|             el, | ||||
|           ); | ||||
|           renderContainer(); | ||||
|         } | ||||
|       }} | ||||
|     > | ||||
| @ -227,9 +198,26 @@ Action.URL = observer((props) => { | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| Action.Container = ({ children }) => { | ||||
|   return children; | ||||
| }; | ||||
| 
 | ||||
| Action.Popover = ({ children }) => { | ||||
|   return children; | ||||
| }; | ||||
| 
 | ||||
| Action.Drawer = ({ children }) => { | ||||
|   return children; | ||||
| }; | ||||
| 
 | ||||
| Action.Modal = ({ children }) => { | ||||
|   return children; | ||||
| }; | ||||
| 
 | ||||
| Action.DesignableBar = () => { | ||||
|   const field = useField(); | ||||
|   const { schema, refresh } = useSchemaQuery(); | ||||
|   const schema = useFieldSchema(); | ||||
|   const { insertAfter } = useDesignable(); | ||||
|   const [visible, setVisible] = useState(false); | ||||
|   return ( | ||||
|     <div className={classNames('designable-bar', { active: visible })}> | ||||
| @ -253,12 +241,21 @@ Action.DesignableBar = () => { | ||||
|                   schema.title = '按钮文案被修改了'; | ||||
|                   field.setTitle('按钮文案被修改了'); | ||||
|                   schema.properties.drawer1.title = '抽屉标题文案被修改了'; | ||||
|                   refresh(); | ||||
|                   setVisible(false); | ||||
|                 }} | ||||
|               > | ||||
|                 点击修改按钮文案 | ||||
|               </Menu.Item> | ||||
|               <Menu.Item | ||||
|                 onClick={() => { | ||||
|                   insertAfter({ | ||||
|                     name: uid(), | ||||
|                     'x-component': 'Input', | ||||
|                   }); | ||||
|                 }} | ||||
|               > | ||||
|                 insertAfter | ||||
|               </Menu.Item> | ||||
|             </Menu> | ||||
|           } | ||||
|         > | ||||
|  | ||||
| @ -44,327 +44,12 @@ import { | ||||
|   DatabaseOutlined, | ||||
| } from '@ant-design/icons'; | ||||
| 
 | ||||
| import { uid } from '@formily/shared'; | ||||
| import { | ||||
|   DesignableSchemaContext, | ||||
|   RefreshDesignableSchemaContext, | ||||
|   SchemaField, | ||||
| } from '../SchemaField'; | ||||
| 
 | ||||
| import { SchemaBlock } from '../'; | ||||
| 
 | ||||
| import table from './table'; | ||||
| import markdown from './markdown'; | ||||
| import form from './form'; | ||||
| import { useRequest } from 'ahooks'; | ||||
| 
 | ||||
| const row = (schema) => { | ||||
|   const component = schema['x-component']; | ||||
|   return { | ||||
|     type: 'void', | ||||
|     name: `gr_${uid()}`, | ||||
|     'x-component': 'Grid.Row', | ||||
|     properties: { | ||||
|       [`gc_${uid()}`]: { | ||||
|         type: 'void', | ||||
|         'x-component': 'Grid.Col', | ||||
|         'x-component-props': { | ||||
|           size: 1, | ||||
|         }, | ||||
|         properties: { | ||||
|           [`gb_${uid()}`]: { | ||||
|             type: 'void', | ||||
|             'x-component': 'Grid.Block', | ||||
|             properties: { | ||||
|               [`gbn_${uid()}`]: schema, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function removeProperty(property: Schema) { | ||||
|   property.parent.removeProperty(property.name); | ||||
| } | ||||
| 
 | ||||
| export function addPropertyBefore(target, prop) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(prop.name, prop); | ||||
|     } | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function addPropertyAfter(target, prop) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(prop.name, prop); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function useSchemaQuery(segments?: any[]) { | ||||
|   const context = useContext(DesignableSchemaContext); | ||||
|   const refresh = useContext(RefreshDesignableSchemaContext); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
|   const field = useField(); | ||||
| 
 | ||||
|   const getSchemaByPath = (path) => { | ||||
|     let s: Schema = context; | ||||
|     const names = [...path]; | ||||
|     // names.shift();
 | ||||
|     while (s && names.length) { | ||||
|       const name = names.shift(); | ||||
|       s = s.properties[name]; | ||||
|     } | ||||
|     return s; | ||||
|   }; | ||||
| 
 | ||||
|   const schema = getSchemaByPath(segments || field.address.segments); | ||||
| 
 | ||||
|   console.log({ context, schema }); | ||||
| 
 | ||||
|   return { | ||||
|     refresh, | ||||
|     schema, | ||||
|     appendChild(data) { | ||||
|       schema.addProperty(data.name, data); | ||||
|       refresh(); | ||||
|     }, | ||||
|     insertAfter(data) { | ||||
|       addPropertyAfter(schema, data); | ||||
|       refresh(); | ||||
|     }, | ||||
|     insertBefore(data) { | ||||
|       addPropertyBefore(schema, data); | ||||
|       refresh(); | ||||
|     }, | ||||
|     push(data) { | ||||
|       addPropertyBefore(schema, data); | ||||
|     }, | ||||
|     remove() { | ||||
|       removeProperty(schema); | ||||
|       refresh(); | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export const AddNew: any = observer((props) => { | ||||
|   const field = useField(); | ||||
| 
 | ||||
|   const segments = [...field.address.segments]; | ||||
|   segments.pop(); | ||||
|   segments.pop(); | ||||
|   segments.pop(); | ||||
|   // segments.pop();
 | ||||
| 
 | ||||
|   const { insertBefore, refresh, schema } = useSchemaQuery(segments); | ||||
|   console.log('field.address.segments', segments, schema); | ||||
| 
 | ||||
|   const { | ||||
|     data = [], | ||||
|     loading, | ||||
|     mutate, | ||||
|   } = useRequest(() => { | ||||
|     return Promise.resolve([ | ||||
|       { title: '数据表1' }, | ||||
|       { title: '数据表2' }, | ||||
|       { title: '数据表3' }, | ||||
|     ]); | ||||
|   }); | ||||
| 
 | ||||
|   const [visible, setVisible] = useState(false); | ||||
| 
 | ||||
|   const dbschema = { | ||||
|     type: 'object', | ||||
|     properties: { | ||||
|       layout: { | ||||
|         type: 'void', | ||||
|         'x-component': 'FormLayout', | ||||
|         'x-component-props': { | ||||
|           layout: 'vertical', | ||||
|         }, | ||||
|         properties: { | ||||
|           input: { | ||||
|             type: 'string', | ||||
|             title: '数据表名称', | ||||
|             required: true, | ||||
|             'x-decorator': 'FormItem', | ||||
|             'x-component': 'Input', | ||||
|           }, | ||||
|           array: { | ||||
|             type: 'array', | ||||
|             title: '数据表字段', | ||||
|             'x-component': 'ArrayCollapse', | ||||
|             'x-component-props': { | ||||
|               accordion: true, | ||||
|             }, | ||||
|             // maxItems: 3,
 | ||||
|             'x-decorator': 'FormItem', | ||||
|             items: { | ||||
|               type: 'object', | ||||
|               'x-component': 'ArrayCollapse.CollapsePanel', | ||||
|               'x-component-props': { | ||||
|                 header: '字段', | ||||
|               }, | ||||
|               properties: { | ||||
|                 index: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'ArrayCollapse.Index', | ||||
|                 }, | ||||
|                 input: { | ||||
|                   type: 'string', | ||||
|                   'x-decorator': 'FormItem', | ||||
|                   title: 'Input', | ||||
|                   required: true, | ||||
|                   'x-component': 'Input', | ||||
|                 }, | ||||
|                 remove: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'ArrayCollapse.Remove', | ||||
|                 }, | ||||
|                 moveUp: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'ArrayCollapse.MoveUp', | ||||
|                 }, | ||||
|                 moveDown: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'ArrayCollapse.MoveDown', | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|             properties: { | ||||
|               addition: { | ||||
|                 type: 'void', | ||||
|                 title: '添加字段', | ||||
|                 'x-component': 'ArrayCollapse.Addition', | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }; | ||||
| 
 | ||||
|   if (loading) { | ||||
|     return <Spin />; | ||||
|   } | ||||
| 
 | ||||
|   console.log({ data }); | ||||
| 
 | ||||
|   return ( | ||||
|     <Dropdown | ||||
|       overlay={ | ||||
|         <Menu> | ||||
|           <Menu.SubMenu | ||||
|             key={`table-tables`} | ||||
|             style={{ minWidth: 150 }} | ||||
|             title={'新建表单'} | ||||
|           > | ||||
|             <Menu.ItemGroup key={`table-tables-itemgroup`} title={'所属数据表'}> | ||||
|               {data.map((item, index) => { | ||||
|                 return ( | ||||
|                   <Menu.Item | ||||
|                     key={`table-${index}`} | ||||
|                     style={{ minWidth: 150 }} | ||||
|                     onClick={() => { | ||||
|                       const rowData = row(form()); | ||||
|                       insertBefore(rowData); | ||||
|                     }} | ||||
|                   > | ||||
|                     {item.title} | ||||
|                   </Menu.Item> | ||||
|                 ); | ||||
|               })} | ||||
|             </Menu.ItemGroup> | ||||
|             <Menu.Divider></Menu.Divider> | ||||
|             <Menu.Item | ||||
|               style={{ minWidth: 150 }} | ||||
|               onClick={() => { | ||||
|                 FormDialog('新建数据表', () => { | ||||
|                   return <SchemaField schema={dbschema} />; | ||||
|                 }) | ||||
|                   .open({ | ||||
|                     initialValues: { | ||||
|                       // aaa: '123',
 | ||||
|                     }, | ||||
|                   }) | ||||
|                   .then(() => { | ||||
|                     const items = [...data]; | ||||
|                     items.push({ title: '数据表5' }); | ||||
|                     mutate(items); | ||||
|                     const rowData = row(form()); | ||||
|                     insertBefore(rowData); | ||||
|                   }); | ||||
|                 // const rowData = row(table());
 | ||||
|                 // insertBefore(rowData);
 | ||||
|               }} | ||||
|             > | ||||
|               新增数据表 | ||||
|             </Menu.Item> | ||||
|           </Menu.SubMenu> | ||||
|           <Menu.SubMenu | ||||
|             key={`form-tables`} | ||||
|             style={{ minWidth: 150 }} | ||||
|             title={'新建表格'} | ||||
|           > | ||||
|             <Menu.ItemGroup key={`form-tables-itemgroup`} title={'所属数据表'}> | ||||
|               {data.map((item, index) => { | ||||
|                 return ( | ||||
|                   <Menu.Item | ||||
|                     key={`form-${index}`} | ||||
|                     style={{ minWidth: 150 }} | ||||
|                     onClick={() => { | ||||
|                       const rowData = row(table()); | ||||
|                       console.log({ rowData }); | ||||
|                       insertBefore(rowData); | ||||
|                     }} | ||||
|                   > | ||||
|                     {item.title} | ||||
|                   </Menu.Item> | ||||
|                 ); | ||||
|               })} | ||||
|             </Menu.ItemGroup> | ||||
|             <Menu.Divider></Menu.Divider> | ||||
|             <Menu.Item | ||||
|               style={{ minWidth: 150 }} | ||||
|               onClick={() => { | ||||
|                 FormDialog('新建数据表', () => { | ||||
|                   return <SchemaField schema={dbschema} />; | ||||
|                 }) | ||||
|                   .open({ | ||||
|                     initialValues: { | ||||
|                       // aaa: '123',
 | ||||
|                     }, | ||||
|                   }) | ||||
|                   .then(() => { | ||||
|                     const items = [...data]; | ||||
|                     items.push({ title: '数据表4' }); | ||||
|                     mutate(items); | ||||
|                     const rowData = row(table()); | ||||
|                     insertBefore(rowData); | ||||
|                   }); | ||||
|               }} | ||||
|             > | ||||
|               新建数据表 | ||||
|             </Menu.Item> | ||||
|           </Menu.SubMenu> | ||||
|           <Menu.Item | ||||
|             onClick={() => { | ||||
|               const rowData = row(markdown()); | ||||
|               insertBefore(rowData); | ||||
|             }} | ||||
|           > | ||||
|             新建 Markdown | ||||
|           </Menu.Item> | ||||
|           <Menu.Item>新建 Markdown</Menu.Item> | ||||
|         </Menu> | ||||
|       } | ||||
|     > | ||||
| @ -374,47 +59,11 @@ export const AddNew: any = observer((props) => { | ||||
| }); | ||||
| 
 | ||||
| AddNew.FormItem = observer((props) => { | ||||
|   const field = useField(); | ||||
| 
 | ||||
|   const segments = [...field.address.segments]; | ||||
|   segments.pop(); | ||||
|   segments.pop(); | ||||
|   segments.pop(); | ||||
| 
 | ||||
|   const { insertBefore, refresh, schema } = useSchemaQuery(segments); | ||||
| 
 | ||||
|   const insertBeforeHandle = () => { | ||||
|     const rowData = row({ | ||||
|       type: 'string', | ||||
|       // required: true,
 | ||||
|       name: `f_${uid()}`, | ||||
|       title: `字段${uid()}`, | ||||
|       'x-decorator': 'FormItem', | ||||
|       'x-designable-bar': 'FormItem.DesignableBar', | ||||
|       'x-component': 'Input', | ||||
|     }); | ||||
|     console.log({ rowData }); | ||||
|     insertBefore(rowData); | ||||
|   }; | ||||
|   return ( | ||||
|     <Dropdown | ||||
|       overlay={ | ||||
|         <Menu> | ||||
|           <Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}> | ||||
|             字段1 | ||||
|           </Menu.Item> | ||||
|           <Menu.Item onClick={insertBeforeHandle}>字段2</Menu.Item> | ||||
|           <Menu.Item onClick={insertBeforeHandle}>字段3</Menu.Item> | ||||
|           <Menu.Divider /> | ||||
|           <Menu.SubMenu title={'新增字段'}> | ||||
|             <Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}> | ||||
|               单行文本 | ||||
|             </Menu.Item> | ||||
|             <Menu.Item onClick={insertBeforeHandle}>多行文本</Menu.Item> | ||||
|             <Menu.Item onClick={insertBeforeHandle}>电子邮箱</Menu.Item> | ||||
|             <Menu.Item onClick={insertBeforeHandle}>手机号</Menu.Item> | ||||
|             <Menu.Item onClick={insertBeforeHandle}>数字</Menu.Item> | ||||
|           </Menu.SubMenu> | ||||
|           <Menu.Item>字段 1</Menu.Item> | ||||
|         </Menu> | ||||
|       } | ||||
|     > | ||||
|  | ||||
| @ -18,7 +18,7 @@ group: | ||||
|  * title: 省市区级联 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const options = [ | ||||
|   { | ||||
| @ -94,7 +94,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -20,7 +20,7 @@ group: | ||||
|  * title: 勾选 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -53,7 +53,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
| @ -65,7 +65,7 @@ export default () => { | ||||
|  * title: 组 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const options = [ | ||||
|   { | ||||
| @ -111,7 +111,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -16,7 +16,7 @@ group: | ||||
|  * title: 颜色选择器 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -59,7 +59,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -13,7 +13,7 @@ group: | ||||
| 
 | ||||
| ```tsx | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -71,6 +71,6 @@ const schema = { | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} /> | ||||
|   return <SchemaRenderer schema={schema} /> | ||||
| } | ||||
| ``` | ||||
|  | ||||
| @ -20,7 +20,7 @@ group: | ||||
|  * title: 日期选择器 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -53,7 +53,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
| @ -65,7 +65,7 @@ export default () => { | ||||
|  * title: 日期时间选择 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -104,7 +104,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -1,12 +1,13 @@ | ||||
| import React, { useContext, useState } from 'react'; | ||||
| import { connect, mapProps, mapReadPretty, SchemaOptionsContext, useField, useFieldSchema } from '@formily/react'; | ||||
| import { FormItem as FormilyFormItem } from '@formily/antd'; | ||||
| import { Dropdown, Menu } from 'antd'; | ||||
| import { Dropdown, Menu, Space } from 'antd'; | ||||
| import classNames from 'classnames'; | ||||
| import { MenuOutlined } from '@ant-design/icons'; | ||||
| import { MenuOutlined, DragOutlined } from '@ant-design/icons'; | ||||
| import './style.less'; | ||||
| import get from 'lodash/get'; | ||||
| import { BlockContext } from '../SchemaField'; | ||||
| import { GridBlockContext } from '../grid'; | ||||
| import { uid } from '@formily/shared'; | ||||
| 
 | ||||
| function Blank() { | ||||
|   return null; | ||||
| @ -24,11 +25,10 @@ function useDesignableBar() { | ||||
| 
 | ||||
| export const FormItem: any = connect((props) => { | ||||
|   const { DesignableBar } = useDesignableBar(); | ||||
|   const { dragRef } = useContext(BlockContext); | ||||
|   return ( | ||||
|     <div className={'designable-form-item'}> | ||||
|       <FormilyFormItem {...props} /> | ||||
|       {dragRef && <div ref={dragRef}>Drag</div>} | ||||
|       {/* {dragRef && <div ref={dragRef}>Drag</div>} */} | ||||
|       <DesignableBar/> | ||||
|     </div> | ||||
|   ); | ||||
| @ -37,6 +37,7 @@ export const FormItem: any = connect((props) => { | ||||
| FormItem.DesignableBar = () => { | ||||
|   const field = useField(); | ||||
|   const [visible, setVisible] = useState(false); | ||||
|   const { dragRef } = useContext(GridBlockContext); | ||||
|   return ( | ||||
|     <div className={classNames('designable-bar', { active: visible })}> | ||||
|       <span | ||||
| @ -45,6 +46,8 @@ FormItem.DesignableBar = () => { | ||||
|         }} | ||||
|         className={classNames('designable-bar-actions', { active: visible })} | ||||
|       > | ||||
|         <Space size={'small'}> | ||||
|         <DragOutlined ref={dragRef} /> | ||||
|         <Dropdown | ||||
|           trigger={['click']} | ||||
|           visible={visible} | ||||
| @ -55,6 +58,7 @@ FormItem.DesignableBar = () => { | ||||
|             <Menu> | ||||
|               <Menu.Item | ||||
|                 onClick={(e) => { | ||||
|                   field.title = uid(); | ||||
|                   setVisible(false); | ||||
|                 }} | ||||
|               > | ||||
| @ -63,8 +67,9 @@ FormItem.DesignableBar = () => { | ||||
|             </Menu> | ||||
|           } | ||||
|         > | ||||
|           <MenuOutlined /> | ||||
|           <MenuOutlined/> | ||||
|         </Dropdown> | ||||
|         </Space> | ||||
|       </span> | ||||
|     </div> | ||||
|   ); | ||||
|  | ||||
| @ -24,12 +24,19 @@ | ||||
|       position: absolute; | ||||
|       right: 0; | ||||
|       line-height: 1rem; | ||||
|       background-color: #1890ff; | ||||
|       color: #fff; | ||||
|       z-index: 10; | ||||
|       padding: 0 3px; | ||||
|       .ant-space { | ||||
|         gap: 1px !important; | ||||
|       } | ||||
|       .anticon { | ||||
|         width: 16px; | ||||
|         height: 16px; | ||||
|         vertical-align: top; | ||||
|         line-height: 16px; | ||||
|         font-size: 10px; | ||||
|         background-color: #1890ff; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -2,7 +2,6 @@ import React, { useState } from 'react'; | ||||
| import classNames from 'classnames'; | ||||
| import { useField, observer, RecursionField, Schema } from '@formily/react'; | ||||
| import { Dropdown, Menu } from 'antd'; | ||||
| import { useSchemaQuery } from '../grid'; | ||||
| import { | ||||
|   MenuOutlined, | ||||
|   ArrowUpOutlined, | ||||
| @ -11,18 +10,11 @@ import { | ||||
| } from '@ant-design/icons'; | ||||
| 
 | ||||
| export const DesignableBar = (props) => { | ||||
|   const { addBlock, removeBlock } = useSchemaQuery(); | ||||
|   const [active, setActive] = useState(false); | ||||
|   return ( | ||||
|     <> | ||||
|       <Menu.Item | ||||
|         onClick={() => { | ||||
|           addBlock( | ||||
|             {}, | ||||
|             { | ||||
|               insertBefore: true, | ||||
|             }, | ||||
|           ); | ||||
|           setActive(false); | ||||
|         }} | ||||
|         icon={<ArrowUpOutlined />} | ||||
| @ -31,7 +23,6 @@ export const DesignableBar = (props) => { | ||||
|       </Menu.Item> | ||||
|       <Menu.Item | ||||
|         onClick={() => { | ||||
|           addBlock(); | ||||
|           setActive(false); | ||||
|         }} | ||||
|         icon={<ArrowDownOutlined />} | ||||
| @ -41,7 +32,6 @@ export const DesignableBar = (props) => { | ||||
|       <Menu.Divider /> | ||||
|       <Menu.Item | ||||
|         onClick={() => { | ||||
|           removeBlock(); | ||||
|           setActive(false); | ||||
|         }} | ||||
|         icon={<DeleteOutlined />} | ||||
|  | ||||
| @ -1,141 +0,0 @@ | ||||
| import React, { useState, useContext, createContext } from 'react'; | ||||
| import { useDrag, mergeRefs, useDrop } from './DND'; | ||||
| import classNames from 'classnames'; | ||||
| import { | ||||
|   useField, | ||||
|   observer, | ||||
|   RecursionField, | ||||
|   Schema, | ||||
|   SchemaOptionsContext, | ||||
| } from '@formily/react'; | ||||
| import { Dropdown, Menu } from 'antd'; | ||||
| import { useSchemaQuery } from '.'; | ||||
| import { | ||||
|   MenuOutlined, | ||||
|   ArrowUpOutlined, | ||||
|   ArrowDownOutlined, | ||||
|   DeleteOutlined, | ||||
| } from '@ant-design/icons'; | ||||
| import get from 'lodash/get'; | ||||
| import { BlockContext } from '../SchemaField'; | ||||
| 
 | ||||
| export const Block = observer((props: any) => { | ||||
|   const { children, title, DesignableBar } = props; | ||||
|   const field = useField<Formily.Core.Models.Field>(); | ||||
|   const { isDragging, dragRef, previewRef } = useDrag({ | ||||
|     type: 'grid', | ||||
|     onDragStart() { | ||||
|       // console.log('onDragStart');
 | ||||
|     }, | ||||
|     onDragEnd(event) { | ||||
|       console.log('onDragEnd', event); | ||||
|     }, | ||||
|     onDrag(event) { | ||||
|       // console.log('onDrag');
 | ||||
|     }, | ||||
|     item: { | ||||
|       path: field.address.segments, | ||||
|     }, | ||||
|   }); | ||||
|   console.log('ActionBar', DesignableBar); | ||||
|   const { isOver, onTopHalf, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: {}, | ||||
|     canDrop: !isDragging, | ||||
|   }); | ||||
| 
 | ||||
|   const options = useContext(SchemaOptionsContext); | ||||
| 
 | ||||
|   const DesignableBarComponent = get(options.components, DesignableBar); | ||||
| 
 | ||||
|   console.log({ DesignableBarComponent, DesignableBar }); | ||||
| 
 | ||||
|   const { addBlock } = useSchemaQuery(); | ||||
| 
 | ||||
|   console.log({ children }); | ||||
|   return ( | ||||
|     <div | ||||
|       data-type={'block'} | ||||
|       ref={mergeRefs([dropRef, previewRef])} | ||||
|       className={classNames('block', { 'top-half': onTopHalf, hover: isOver })} | ||||
|       // style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
 | ||||
|     > | ||||
|       <BlockContext.Provider value={{ dragRef }}> | ||||
|         {children} | ||||
|       </BlockContext.Provider> | ||||
|       {/* {DesignableBarComponent && ( | ||||
|         <DefaultActionBar DesignableBarComponent={DesignableBarComponent} dragRef={dragRef} /> | ||||
|       )} */} | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| // function overlay() {
 | ||||
| //   return <Menu>
 | ||||
| //   <Menu.Item
 | ||||
| //     onClick={() => {
 | ||||
| //       addBlock(
 | ||||
| //         {},
 | ||||
| //         {
 | ||||
| //           insertBefore: true,
 | ||||
| //         },
 | ||||
| //       );
 | ||||
| //       setActive(false);
 | ||||
| //     }}
 | ||||
| //     icon={<ArrowUpOutlined />}
 | ||||
| //   >
 | ||||
| //     在上方插入区块
 | ||||
| //   </Menu.Item>
 | ||||
| //   <Menu.Item
 | ||||
| //     onClick={() => {
 | ||||
| //       addBlock();
 | ||||
| //       setActive(false);
 | ||||
| //     }}
 | ||||
| //     icon={<ArrowDownOutlined />}
 | ||||
| //   >
 | ||||
| //     在下方插入区块
 | ||||
| //   </Menu.Item>
 | ||||
| //   <Menu.Divider />
 | ||||
| //   <Menu.Item
 | ||||
| //     onClick={() => {
 | ||||
| //       removeBlock();
 | ||||
| //       setActive(false);
 | ||||
| //     }}
 | ||||
| //     icon={<DeleteOutlined />}
 | ||||
| //   >
 | ||||
| //     删除区块
 | ||||
| //   </Menu.Item>
 | ||||
| // </Menu>
 | ||||
| // }
 | ||||
| 
 | ||||
| function Overlay(props) { | ||||
|   return ( | ||||
|     <> | ||||
|       <Menu.Item>在上方插入区块1</Menu.Item> | ||||
|       <Menu.Item>在下方插入区块2</Menu.Item> | ||||
|       <Menu.Divider /> | ||||
|       <Menu.Item>删除区块</Menu.Item> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| const DefaultActionBar = ({ DesignableBarComponent, dragRef }) => { | ||||
|   const { addBlock, removeBlock } = useSchemaQuery(); | ||||
|   const [active, setActive] = useState(false); | ||||
|   // return <MenuOutlined className={'draggable'} ref={dragRef} />;
 | ||||
|   return ( | ||||
|     <div className={classNames('action-bar', { active })}> | ||||
|       <Dropdown | ||||
|         overlayStyle={{ minWidth: 200 }} | ||||
|         trigger={['click']} | ||||
|         visible={active} | ||||
|         onVisibleChange={setActive} | ||||
|         overlay={<Menu>{DesignableBarComponent ? <DesignableBarComponent /> : <Overlay />}</Menu>} | ||||
|       > | ||||
|         <MenuOutlined className={'draggable'} ref={dragRef} /> | ||||
|       </Dropdown> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Block; | ||||
| @ -1,100 +0,0 @@ | ||||
| import React, { cloneElement, useRef, useState } from 'react'; | ||||
| import { useMouseEvents } from 'beautiful-react-hooks'; | ||||
| import { mergeRefs, useDrop } from './DND'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
| export function useColResizer(options?: any) { | ||||
|   const { onDragStart, onDrag, onDragEnd } = options || {}; | ||||
|   const dragRef = useRef<HTMLDivElement>(); | ||||
|   const [dragOffset, setDragOffset] = useState({ left: 0, top: 0 }); | ||||
|   const { onMouseDown } = useMouseEvents(dragRef); | ||||
|   const { onMouseMove, onMouseUp } = useMouseEvents(); | ||||
|   const [isDragging, setIsDragging] = useState(false); | ||||
|   const [columns, setColumns] = useState(options.columns || []); | ||||
|   const [initial, setInitial] = useState<any>(null); | ||||
| 
 | ||||
|   onMouseDown((event: React.MouseEvent) => { | ||||
|     if (event.button !== 0) { | ||||
|       return; | ||||
|     } | ||||
|     const prev = dragRef.current.previousElementSibling as HTMLDivElement; | ||||
|     const next = dragRef.current.nextElementSibling as HTMLDivElement; | ||||
|     if (!prev || !next) { | ||||
|       return; | ||||
|     } | ||||
|     setIsDragging(true); | ||||
|     if (!initial) { | ||||
|       setInitial({ | ||||
|         offset: event.clientX, | ||||
|         prevWidth: prev.style.width, | ||||
|         nextWidth: next.style.width, | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   onMouseUp((event: React.MouseEvent) => { | ||||
|     if (!isDragging) { | ||||
|       return; | ||||
|     } | ||||
|     const parent = dragRef.current.parentElement; | ||||
|     const els = parent.querySelectorAll('.col'); | ||||
|     const size = []; | ||||
|     els.forEach((el: HTMLDivElement) => { | ||||
|       const w = el.clientWidth / parent.clientWidth; | ||||
|       size.push(w); | ||||
|       el.style.width = `${100 * w}%`; | ||||
|     }); | ||||
|     console.log(size); | ||||
|     setIsDragging(false); | ||||
|     setInitial(null); | ||||
|     // @ts-ignore
 | ||||
|     event.data = { size }; | ||||
|     onDragEnd && onDragEnd(event); | ||||
|   }); | ||||
| 
 | ||||
|   onMouseMove((event: React.MouseEvent) => { | ||||
|     if (!isDragging) { | ||||
|       return; | ||||
|     } | ||||
|     const offset = event.clientX - initial.offset; | ||||
|     // dragRef.current.style.transform = `translateX(${event.clientX - initialOffset}px)`;
 | ||||
|     const prev = dragRef.current.previousElementSibling as HTMLDivElement; | ||||
|     const next = dragRef.current.nextElementSibling as HTMLDivElement; | ||||
|     prev.style.width = `calc(${initial.prevWidth} + ${offset}px)`; | ||||
|     next.style.width = `calc(${initial.nextWidth} - ${offset}px)`; | ||||
|     // console.log('dragRef.current.nextSibling', prev.style.width);
 | ||||
|   }); | ||||
| 
 | ||||
|   return { isDragging, dragOffset, dragRef, columns }; | ||||
| } | ||||
| 
 | ||||
| export const Col: any = (props) => { | ||||
|   const { size, children, position = {}, isLast } = props; | ||||
|   return ( | ||||
|     <> | ||||
|       <div data-type={'col'} className={'col'} style={{ width: `${size * 100}%` }}> | ||||
|         {children} | ||||
|       </div> | ||||
|       <Col.Divider /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| Col.Divider = (props) => { | ||||
|   const { onDragEnd, resizable = true } = props; | ||||
|   const { isDragging, dragRef } = useColResizer({ onDragEnd }); | ||||
|   const { isOver, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: { }, | ||||
|   }); | ||||
|   return ( | ||||
|     <div | ||||
|       data-type={'col-divider'} | ||||
|       className={classNames('col-divider', { hover: isOver, resizable })} | ||||
|       style={{ width: '24px' }} | ||||
|       ref={mergeRefs(resizable ? [dropRef, dragRef] : [dropRef])} | ||||
|     ></div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Col; | ||||
| @ -1,88 +0,0 @@ | ||||
| import React, { cloneElement, useRef } from 'react'; | ||||
| import { Col } from './Col'; | ||||
| import { Row } from './Row'; | ||||
| import { Block } from './Block'; | ||||
| import { DragDropProvider } from './DND'; | ||||
| import { useSchemaQuery } from './'; | ||||
| 
 | ||||
| type Event = React.MouseEvent & { | ||||
|   dropElement: HTMLElement; | ||||
|   onTopHalf?: boolean; | ||||
|   dragItem?: any; | ||||
| }; | ||||
| 
 | ||||
| export const Grid = (props) => { | ||||
|   const { children, onDrop } = props; | ||||
|   const ref = useRef(); | ||||
|   const { schema, moveTo, refresh } = useSchemaQuery(); | ||||
|   return ( | ||||
|     <div className={'grid'}> | ||||
|       <DragDropProvider | ||||
|         gridRef={ref} | ||||
|         onDrop={(event: Event) => { | ||||
|           const el = event.dropElement; | ||||
|           const type = el.getAttribute('data-type'); | ||||
|           const getIndex = (el) => { | ||||
|             const type = el.getAttribute('data-type'); | ||||
|             return Array.prototype.indexOf.call( | ||||
|               el.parentNode.querySelectorAll(`.${type}`), | ||||
|               el, | ||||
|             ); | ||||
|           }; | ||||
|           let position: any = { type }; | ||||
|           if (type === 'row') { | ||||
|             // position.rowIndex = getIndex(el);
 | ||||
|             position = { | ||||
|               type: 'row-divider', | ||||
|               rowDividerIndex: getIndex( | ||||
|                 event.onTopHalf ? el.previousSibling : el.nextSibling, | ||||
|               ), | ||||
|             }; | ||||
|           } | ||||
|           if (type === 'row-divider') { | ||||
|             position.rowDividerIndex = getIndex(el); | ||||
|           } | ||||
|           if (type === 'col-divider') { | ||||
|             position.colDividerIndex = getIndex(el); | ||||
|             position.rowIndex = getIndex(el.parentNode); | ||||
|           } | ||||
|           if (type === 'block') { | ||||
|             const rowNode = el.parentNode.parentNode; | ||||
|             position.blockIndex = getIndex(el); | ||||
|             position.colIndex = getIndex(el.parentNode); | ||||
|             position.rowIndex = getIndex(rowNode); | ||||
|             const colsize = rowNode.querySelectorAll('.col').length; | ||||
|             if (colsize === 1) { | ||||
|               position = { | ||||
|                 type: 'row-divider', | ||||
|                 rowDividerIndex: getIndex( | ||||
|                   event.onTopHalf | ||||
|                     ? rowNode.previousSibling | ||||
|                     : rowNode.nextSibling, | ||||
|                 ), | ||||
|               }; | ||||
|             } else { | ||||
|               position.type = 'block-divider'; | ||||
|               position.blockDividerIndex = getIndex(el); | ||||
|               if (!event.onTopHalf) { | ||||
|                 position.blockDividerIndex += 1 | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|           onDrop && onDrop(event); | ||||
|           moveTo(event.dragItem.path, position); | ||||
|           console.log('onDrop', position, event.dragItem); | ||||
|         }} | ||||
|       > | ||||
|         <Row.Divider style={{ marginTop: -24 }} /> | ||||
|         {children} | ||||
|       </DragDropProvider> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| Grid.Row = Row; | ||||
| Grid.Col = Col; | ||||
| Grid.Block = Block; | ||||
| 
 | ||||
| export default Grid; | ||||
| @ -1,46 +0,0 @@ | ||||
| import React, { cloneElement, useRef } from 'react'; | ||||
| import classNames from 'classnames'; | ||||
| import { Col } from './Col'; | ||||
| import { DragDropProvider, useDrop } from './DND'; | ||||
| import { useField } from '@formily/react'; | ||||
| 
 | ||||
| export const Row = (props) => { | ||||
|   const { children, onColResize, position = {}, isLast } = props; | ||||
|   const { isOver, onTopHalf, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: {}, | ||||
|     shallow: true, | ||||
|   }); | ||||
|   return ( | ||||
|     <> | ||||
|       <div | ||||
|         data-type={'row'} | ||||
|         ref={dropRef} | ||||
|         className={classNames('row', { hover: isOver, 'top-half': onTopHalf })} | ||||
|         style={{ margin: '0 -24px', display: 'flex' }} | ||||
|       > | ||||
|         <Col.Divider resizable={false} /> | ||||
|         {children} | ||||
|       </div> | ||||
|       <Row.Divider /> | ||||
|     </> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| Row.Divider = (props) => { | ||||
|   const { style = {}, position } = props; | ||||
|   const { isOver, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: { position }, | ||||
|   }); | ||||
|   return ( | ||||
|     <div | ||||
|       data-type={'row-divider'} | ||||
|       ref={dropRef} | ||||
|       className={classNames('row-divider', { hover: isOver })} | ||||
|       style={{ ...style, height: '24px' }} | ||||
|     ></div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default Row; | ||||
							
								
								
									
										120
									
								
								packages/client/src/blocks/grid/demos/demo1.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								packages/client/src/blocks/grid/demos/demo1.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| import React from 'react'; | ||||
| import { uid } from '@formily/shared'; | ||||
| import { | ||||
|   observer, | ||||
|   ISchema, | ||||
|   FormProvider, | ||||
|   useFieldSchema, | ||||
|   RecursionField, | ||||
|   useField, | ||||
| } from '@formily/react'; | ||||
| import { Input } from '../../input'; | ||||
| import { FormItem } from '../../form-item'; | ||||
| import { createDesignableSchemaField } from '../../DesignableSchemaField'; | ||||
| import { createForm } from '@formily/core'; | ||||
| import { Grid } from '../'; | ||||
| 
 | ||||
| const schema: ISchema = { | ||||
|   type: 'object', | ||||
|   properties: { | ||||
|     [uid()]: { | ||||
|       type: 'void', | ||||
|       'x-component': 'Grid', | ||||
|       properties: { | ||||
|         [`row_${uid()}`]: { | ||||
|           type: 'void', | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             [`col_${uid()}`]: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               "x-component-props": { | ||||
|                 width: 30, | ||||
|               }, | ||||
|               properties: { | ||||
|                 [`block_${uid()}`]: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   properties: { | ||||
|                     [uid()]: { | ||||
|                       type: 'string', | ||||
|                       title: uid(), | ||||
|                       'x-designable-bar': 'FormItem.DesignableBar', | ||||
|                       'x-decorator': 'FormItem', | ||||
|                       'x-component': 'Input', | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|             [`col_${uid()}`]: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               "x-component-props": { | ||||
|                 width: 70, | ||||
|               }, | ||||
|               properties: { | ||||
|                 [`block_${uid()}`]: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   properties: { | ||||
|                     [uid()]: { | ||||
|                       type: 'string', | ||||
|                       title: uid(), | ||||
|                       'x-designable-bar': 'FormItem.DesignableBar', | ||||
|                       'x-decorator': 'FormItem', | ||||
|                       'x-component': 'Input', | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         [`row_${uid()}`]: { | ||||
|           type: 'void', | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             [`col_${uid()}`]: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               properties: { | ||||
|                 [`block_${uid()}`]: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   properties: { | ||||
|                     [uid()]: { | ||||
|                       type: 'string', | ||||
|                       title: uid(), | ||||
|                       'x-designable-bar': 'FormItem.DesignableBar', | ||||
|                       'x-decorator': 'FormItem', | ||||
|                       'x-component': 'Input', | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| const DesignableSchemaField = createDesignableSchemaField({ | ||||
|   components: { | ||||
|     Grid, | ||||
|     Input, | ||||
|     FormItem, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const form = createForm(); | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <FormProvider form={form}> | ||||
|       <DesignableSchemaField schema={schema} /> | ||||
|     </FormProvider> | ||||
|   ); | ||||
| }; | ||||
| @ -1,91 +0,0 @@ | ||||
| import React, { createContext, useContext, useEffect, useRef } from 'react'; | ||||
| import { useDrag, useDrop, DragDropProvider, mergeRefs } from '../'; | ||||
| import { Button, Space } from 'antd'; | ||||
| 
 | ||||
| function DropZone({ options, children }) { | ||||
|   const { isOver, dropRef } = useDrop(options); | ||||
|   return ( | ||||
|     <div | ||||
|       ref={dropRef as any} | ||||
|       style={{ | ||||
|         textAlign: 'center', | ||||
|         lineHeight: '100px', | ||||
|         margin: 24, | ||||
|         border: isOver ? '1px solid red' : '1px solid #ddd', | ||||
|       }} | ||||
|     > | ||||
|       {children} | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function Dragable() { | ||||
|   const { isDragging, dragRef, previewRef } = useDrag({ | ||||
|     type: 'box', | ||||
|     onDragStart() { | ||||
|       console.log('onDragStart'); | ||||
|     }, | ||||
|     onDragEnd(event) { | ||||
|       console.log('onDragEnd', event.data); | ||||
|     }, | ||||
|     onDrag(event) { | ||||
|       // console.log('onDrag');
 | ||||
|     }, | ||||
|   }); | ||||
|   return <Button ref={mergeRefs<any>([dragRef, previewRef])}>拖拽1</Button>; | ||||
| } | ||||
| 
 | ||||
| function Dragable2() { | ||||
|   const { isDragging, dragRef, previewRef } = useDrag({ | ||||
|     type: 'box2', | ||||
|     onDragStart() { | ||||
|       console.log('onDragStart'); | ||||
|     }, | ||||
|     onDragEnd(event) { | ||||
|       console.log('onDragEnd', event.data); | ||||
|     }, | ||||
|     onDrag(event) { | ||||
|       // console.log('onDrag');
 | ||||
|     }, | ||||
|   }); | ||||
|   return <Button ref={mergeRefs<any>([dragRef, previewRef])}>拖拽2</Button>; | ||||
| } | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <DragDropProvider> | ||||
|       <Space style={{ marginBottom: 12 }}> | ||||
|         <Dragable /> | ||||
|         <Dragable2 /> | ||||
|       </Space> | ||||
|       <DropZone | ||||
|         options={{ | ||||
|           accept: 'box', | ||||
|           data: { a: 'a' }, | ||||
|           shallow: true, | ||||
|         }} | ||||
|       > | ||||
|         Drop Zone1 | ||||
|         <DropZone | ||||
|           options={{ | ||||
|             accept: 'box', | ||||
|             data: { b: 'b' }, | ||||
|             // shallow: true,
 | ||||
|           }} | ||||
|         > | ||||
|           Drop Zone2 | ||||
|         </DropZone> | ||||
|         <DropZone | ||||
|           options={{ | ||||
|             accept: 'box2', | ||||
|             data: { c: 'c' }, | ||||
|             // shallow: true,
 | ||||
|           }} | ||||
|         > | ||||
|           Drop Zone3 | ||||
|         </DropZone> | ||||
|         Drop Zone1 | ||||
|       </DropZone> | ||||
|     </DragDropProvider> | ||||
|   ); | ||||
| }; | ||||
| @ -1,18 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { Row, Col } from '../'; | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <div> | ||||
|       <Row onColResize={(e) => { | ||||
|         console.log(e.data); | ||||
|       }}> | ||||
|         {[1, 2, 3].map((index) => ( | ||||
|           <Col size={1 / 3}> | ||||
|             <div style={{textAlign: 'center', lineHeight: '60px', background: '#f1f1f1'}}>col {index}</div> | ||||
|           </Col> | ||||
|         ))} | ||||
|       </Row> | ||||
|     </div> | ||||
|   ); | ||||
| }; | ||||
| @ -1,167 +0,0 @@ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../../'; | ||||
| import { ISchema } from '@formily/json-schema'; | ||||
| 
 | ||||
| const schema: ISchema = { | ||||
|   type: 'object', | ||||
|   properties: { | ||||
|     grid: { | ||||
|       type: 'void', | ||||
|       title: 'aa', | ||||
|       'x-component': 'Grid', | ||||
|       properties: { | ||||
|         row1: { | ||||
|           type: 'void', | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             col1: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1 / 2, | ||||
|               }, | ||||
|               properties: { | ||||
|                 block11: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   'x-content': ( | ||||
|                     <div | ||||
|                       style={{ | ||||
|                         padding: 24, | ||||
|                         textAlign: 'center', | ||||
|                         background: 'rgb(241, 241, 241)', | ||||
|                       }} | ||||
|                     > | ||||
|                       block11 | ||||
|                     </div> | ||||
|                   ), | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|             col2: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1 / 2, | ||||
|                 isLast: true, | ||||
|               }, | ||||
|               properties: { | ||||
|                 block21: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   'x-component-props': { | ||||
|                     title: 'block21', | ||||
|                   }, | ||||
|                   'x-content': ( | ||||
|                     <div | ||||
|                       style={{ | ||||
|                         padding: 24, | ||||
|                         textAlign: 'center', | ||||
|                         background: 'rgb(241, 241, 241)', | ||||
|                       }} | ||||
|                     > | ||||
|                       block21 | ||||
|                     </div> | ||||
|                   ), | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         row2: { | ||||
|           type: 'void', | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             col21: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1 / 3, | ||||
|               }, | ||||
|               properties: { | ||||
|                 block211: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   'x-content': ( | ||||
|                     <div | ||||
|                       style={{ | ||||
|                         padding: 24, | ||||
|                         textAlign: 'center', | ||||
|                         background: 'rgb(241, 241, 241)', | ||||
|                       }} | ||||
|                     > | ||||
|                       block211 | ||||
|                     </div> | ||||
|                   ), | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|             col22: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 2 / 3, | ||||
|                 isLast: true, | ||||
|               }, | ||||
|               properties: { | ||||
|                 block221: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   'x-content': ( | ||||
|                     <div | ||||
|                       style={{ | ||||
|                         padding: 24, | ||||
|                         textAlign: 'center', | ||||
|                         background: 'rgb(241, 241, 241)', | ||||
|                       }} | ||||
|                     > | ||||
|                       block221 | ||||
|                     </div> | ||||
|                   ), | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         row3: { | ||||
|           type: 'void', | ||||
|           'x-component': 'Grid.Row', | ||||
|           'x-component-props': { | ||||
|             isLast: true, | ||||
|           }, | ||||
|           properties: { | ||||
|             col31: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1, | ||||
|                 isLast: true, | ||||
|               }, | ||||
|               properties: { | ||||
|                 block311: { | ||||
|                   type: 'void', | ||||
|                   'x-component': 'Grid.Block', | ||||
|                   'x-content': ( | ||||
|                     <div | ||||
|                       style={{ | ||||
|                         padding: 24, | ||||
|                         textAlign: 'center', | ||||
|                         background: 'rgb(241, 241, 241)', | ||||
|                       }} | ||||
|                     > | ||||
|                       block311 | ||||
|                     </div> | ||||
|                   ), | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
| export default () => { | ||||
|   return <SchemaBlock schema={schema} />; | ||||
| }; | ||||
| @ -99,6 +99,7 @@ export function useDrag(options?: any) { | ||||
|     }; | ||||
| 
 | ||||
|     const wrap = document.createElement('div'); | ||||
|     wrap.className = 'drag-container'; | ||||
|     wrap.style.position = 'absolute'; | ||||
|     wrap.style.pointerEvents = 'none'; | ||||
|     wrap.style.opacity = '0.7'; | ||||
| @ -325,3 +326,70 @@ export function useDrop(options) { | ||||
|     dropRef, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function useColResizer(options?: any) { | ||||
|   const { onDragStart, onDrag, onDragEnd } = options || {}; | ||||
|   const dragRef = useRef<HTMLDivElement>(); | ||||
|   const [dragOffset, setDragOffset] = useState({ left: 0, top: 0 }); | ||||
|   const { onMouseDown } = useMouseEvents(dragRef); | ||||
|   const { onMouseMove, onMouseUp } = useMouseEvents(); | ||||
|   const [isDragging, setIsDragging] = useState(false); | ||||
|   const [columns, setColumns] = useState(options.columns || []); | ||||
|   const [initial, setInitial] = useState<any>(null); | ||||
| 
 | ||||
|   onMouseDown((event: React.MouseEvent) => { | ||||
|     if (event.button !== 0) { | ||||
|       return; | ||||
|     } | ||||
|     const prev = dragRef.current.previousElementSibling as HTMLDivElement; | ||||
|     const next = dragRef.current.nextElementSibling as HTMLDivElement; | ||||
|     if (!prev || !next) { | ||||
|       return; | ||||
|     } | ||||
|     setIsDragging(true); | ||||
|     if (!initial) { | ||||
|       setInitial({ | ||||
|         offset: event.clientX, | ||||
|         prevWidth: prev.style.width, | ||||
|         nextWidth: next.style.width, | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   onMouseUp((event: React.MouseEvent) => { | ||||
|     if (!isDragging) { | ||||
|       return; | ||||
|     } | ||||
|     const parent = dragRef.current.parentElement; | ||||
|     const els = parent.querySelectorAll(':scope > .nb-grid-col'); | ||||
|     const size = []; | ||||
|     els.forEach((el: HTMLDivElement) => { | ||||
|       const w = (100 * el.clientWidth) / parent.clientWidth; | ||||
|       const w2 = | ||||
|         (100 * (el.clientWidth + 24 + 24 / els.length)) / parent.clientWidth; | ||||
|       size.push(w2); | ||||
|       el.style.width = `${w}%`; | ||||
|     }); | ||||
|     console.log({ size }); | ||||
|     setIsDragging(false); | ||||
|     setInitial(null); | ||||
|     // @ts-ignore
 | ||||
|     event.data = { size }; | ||||
|     onDragEnd && onDragEnd(event); | ||||
|   }); | ||||
| 
 | ||||
|   onMouseMove((event: React.MouseEvent) => { | ||||
|     if (!isDragging) { | ||||
|       return; | ||||
|     } | ||||
|     const offset = event.clientX - initial.offset; | ||||
|     // dragRef.current.style.transform = `translateX(${event.clientX - initialOffset}px)`;
 | ||||
|     const prev = dragRef.current.previousElementSibling as HTMLDivElement; | ||||
|     const next = dragRef.current.nextElementSibling as HTMLDivElement; | ||||
|     prev.style.width = `calc(${initial.prevWidth} + ${offset}px)`; | ||||
|     next.style.width = `calc(${initial.nextWidth} - ${offset}px)`; | ||||
|     // console.log('dragRef.current.nextSibling', prev.style.width);
 | ||||
|   }); | ||||
| 
 | ||||
|   return { isDragging, dragOffset, dragRef, columns }; | ||||
| } | ||||
| @ -73,62 +73,4 @@ group: | ||||
| 
 | ||||
| - 100% | ||||
| 
 | ||||
| ## 代码演示 | ||||
| 
 | ||||
| ### useDrag & useDrop | ||||
| 
 | ||||
| <code src="./demos/demo4.tsx"/> | ||||
| 
 | ||||
| ### useColResize | ||||
| 
 | ||||
| <code src="./demos/demo5.tsx"/> | ||||
| 
 | ||||
| ### Grid | ||||
| 
 | ||||
| <code src="./demos/demo6.tsx"/> | ||||
| 
 | ||||
| ## API 说明 | ||||
| 
 | ||||
| ### Grid | ||||
| 
 | ||||
| 只能在同一个 Grid 里拖拽布局 | ||||
| 
 | ||||
| ### Grid.Row | ||||
| 
 | ||||
| 行 | ||||
| 
 | ||||
| ### Grid.Column | ||||
| 
 | ||||
| 列 | ||||
| 
 | ||||
| ### Grid.Block | ||||
| 
 | ||||
| 区块 | ||||
| 
 | ||||
| ### BlockOptions | ||||
| 
 | ||||
| ```ts | ||||
| interface BlockOptions { | ||||
|   rowOrder: number; | ||||
|   columnOrder: number; | ||||
|   blockOrder: number; | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| - rowOrder:第几行 | ||||
| - columnOrder:第几列 | ||||
| - blockOrder:某单元格内部区块排序 | ||||
| 
 | ||||
| ### blocks2properties | ||||
| 
 | ||||
| 原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。 | ||||
| 
 | ||||
| ### useDrag & useDrop | ||||
| 
 | ||||
| 拖拽 hooks | ||||
| 
 | ||||
| 原生态的  | ||||
| 
 | ||||
| ### useDrop | ||||
| 
 | ||||
| ### useColResize | ||||
| <code src="./demos/demo1.tsx"/> | ||||
| @ -1,252 +1,237 @@ | ||||
| import React, { useContext, createContext, useState } from 'react'; | ||||
| import React, { | ||||
|   FC, | ||||
|   CSSProperties, | ||||
|   useRef, | ||||
|   createContext, | ||||
|   useContext, | ||||
|   useEffect, | ||||
| } from 'react'; | ||||
| // import { DndProvider, useDrag, useDragDropManager } from 'react-dnd';
 | ||||
| // import { HTML5Backend } from 'react-dnd-html5-backend';
 | ||||
| import { uid } from '@formily/shared'; | ||||
| import { | ||||
|   Schema, | ||||
|   observer, | ||||
|   ISchema, | ||||
|   FormProvider, | ||||
|   useFieldSchema, | ||||
|   useForm, | ||||
|   RecursionField, | ||||
|   useField, | ||||
| } from '@formily/react'; | ||||
| import { uid } from '@formily/shared'; | ||||
| import './style.less'; | ||||
| import cls from 'classnames'; | ||||
| 
 | ||||
| import { | ||||
|   DesignableSchemaContext, | ||||
|   RefreshDesignableSchemaContext, | ||||
| } from '../SchemaField'; | ||||
| import { useDesignable, useSchemaPath } from '../DesignableSchemaField'; | ||||
| import { useColResizer } from './hooks'; | ||||
| import { useDrag, useDrop, DragDropProvider, mergeRefs } from './hooks'; | ||||
| 
 | ||||
| export function removeProperty(property: Schema) { | ||||
|   property.parent.removeProperty(property.name); | ||||
| } | ||||
| export const GridContext = createContext({ | ||||
|   ref: null, | ||||
| }); | ||||
| 
 | ||||
| export function addPropertyBefore(target, prop) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(prop.name, prop); | ||||
|     } | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
| const ColumnSizeContext = createContext(null); | ||||
| 
 | ||||
| export const GridBlockContext = createContext({ | ||||
|   dragRef: null, | ||||
| }); | ||||
| 
 | ||||
| const RowDivider = ({ onDrop }) => { | ||||
|   const { isOver, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     onDrop, | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export function addPropertyAfter(target, prop) { | ||||
|   Object.keys(target.parent.properties).forEach((name) => { | ||||
|     const property = target.parent.properties[name]; | ||||
|     property.parent.removeProperty(property.name); | ||||
|     target.parent.addProperty(property.name, property.toJSON()); | ||||
|     if (name === target.name) { | ||||
|       target.parent.addProperty(prop.name, prop); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| export const getSchemaAddressSegments = (schema: Schema) => { | ||||
|   if (!schema) { | ||||
|     return []; | ||||
|   } | ||||
|   const segments = [schema.name]; | ||||
|   if (schema.parent && schema.parent.name) { | ||||
|     segments.unshift(...getSchemaAddressSegments(schema.parent)); | ||||
|   } | ||||
|   return segments; | ||||
|   return ( | ||||
|     <div | ||||
|       ref={dropRef} | ||||
|       className={cls('nb-grid-row-divider', { hover: isOver })} | ||||
|     /> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export function useSchemaQuery() { | ||||
|   const context = useContext(DesignableSchemaContext); | ||||
|   const refresh = useContext(RefreshDesignableSchemaContext); | ||||
|   const fieldSchema = useFieldSchema(); | ||||
| const ColDivider = (props: any) => { | ||||
|   const { onDragEnd, resizable } = props; | ||||
|   const { isDragging, dragRef } = useColResizer({ onDragEnd }); | ||||
|   const { isOver, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: {}, | ||||
|   }); | ||||
|   return ( | ||||
|     <div | ||||
|       data-type={'col-divider'} | ||||
|       ref={mergeRefs([dragRef, dropRef])} | ||||
|       style={{ width: 24 }} | ||||
|       className={cls('nb-grid-col-divider', { | ||||
|         resizable, | ||||
|         hover: isOver, | ||||
|         dragging: isDragging, | ||||
|       })} | ||||
|     ></div> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const Grid: any = observer((props) => { | ||||
|   const schema = useFieldSchema(); | ||||
|   const { insertBefore, insertAfter, remove } = useDesignable(); | ||||
|   const ref = useRef(); | ||||
|   return ( | ||||
|     <DragDropProvider> | ||||
|       <GridContext.Provider value={{ ref }}> | ||||
|         <div ref={ref} className={'nb-grid'}> | ||||
|           <RowDivider | ||||
|             onDrop={(e) => { | ||||
|               const blockSchema = e.dragItem.schema; | ||||
|               const path = [...e.dragItem.path]; | ||||
|                     path.pop(); | ||||
|                     remove(path); | ||||
|               insertBefore({ | ||||
|                 type: 'void', | ||||
|                 "x-component": 'Grid.Row', | ||||
|                 properties: { | ||||
|                   [uid()]: { | ||||
|                     type: 'void', | ||||
|                     "x-component": 'Grid.Col', | ||||
|                     properties: { | ||||
|                       [blockSchema.name]: blockSchema, | ||||
|                     }, | ||||
|                   }, | ||||
|                 }, | ||||
|               }); | ||||
|             }} | ||||
|           /> | ||||
|           {schema.mapProperties((property) => { | ||||
|             return ( | ||||
|               <> | ||||
|                 <div style={{ display: 'flex' }} className={'nb-grid-row'}> | ||||
|                   <RecursionField name={property.name} schema={property} /> | ||||
|                 </div> | ||||
|                 <RowDivider | ||||
|                   onDrop={(e) => { | ||||
|                     const blockSchema = e.dragItem.schema; | ||||
|                     const path = [...e.dragItem.path]; | ||||
|                     path.pop(); | ||||
|                     remove(path); | ||||
|                     insertAfter({ | ||||
|                       type: 'void', | ||||
|                       "x-component": 'Grid.Row', | ||||
|                       properties: { | ||||
|                         [uid()]: { | ||||
|                           type: 'void', | ||||
|                           "x-component": 'Grid.Col', | ||||
|                           properties: { | ||||
|                             [blockSchema.name]: blockSchema, | ||||
|                           }, | ||||
|                         }, | ||||
|                       }, | ||||
|                     }); | ||||
|                   }} | ||||
|                 /> | ||||
|               </> | ||||
|             ); | ||||
|           })} | ||||
|         </div> | ||||
|       </GridContext.Provider> | ||||
|     </DragDropProvider> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
| Grid.Row = observer((props) => { | ||||
|   const field = useField(); | ||||
|   const form = useForm(); | ||||
|   const schema = useFieldSchema(); | ||||
|   const { schema: designableSchema, refresh } = useDesignable(); | ||||
|   const len = Object.keys(schema.properties || {}).length; | ||||
|   return ( | ||||
|     <ColumnSizeContext.Provider value={len}> | ||||
|       {schema.mapProperties((property, key, index) => { | ||||
|         return ( | ||||
|           <> | ||||
|             <ColDivider | ||||
|               resizable={index > 0} | ||||
|               onDragEnd={(e) => { | ||||
|                 schema.mapProperties((s, key, index) => { | ||||
|                   field.query(`.${schema.name}.${key}`).take((f) => { | ||||
|                     f.componentProps['width'] = e.data.size[index]; | ||||
|                   }); | ||||
|                   s['x-component-props'] = s['x-component-props'] || {}; | ||||
|                   s['x-component-props']['width'] = e.data.size[index]; | ||||
|                   return s; | ||||
|                 }); | ||||
|                 designableSchema.mapProperties((s, key, index) => { | ||||
|                   s['x-component-props'] = s['x-component-props'] || {}; | ||||
|                   s['x-component-props']['width'] = e.data.size[index]; | ||||
|                   return s; | ||||
|                 }); | ||||
| 
 | ||||
|   const getSchemaByPath = (path) => { | ||||
|     let s: Schema = context; | ||||
|     const names = [...path]; | ||||
|     while (names.length) { | ||||
|       s = s.properties[names.shift()]; | ||||
|     } | ||||
|     return s; | ||||
|   }; | ||||
|                 refresh(); | ||||
|                 console.log('e.data', designableSchema); | ||||
|               }} | ||||
|             /> | ||||
|             <RecursionField name={property.name} schema={property} /> | ||||
|           </> | ||||
|         ); | ||||
|       })} | ||||
|       <ColDivider /> | ||||
|     </ColumnSizeContext.Provider> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
|   const schema = getSchemaByPath(field.address.segments); | ||||
| Grid.Col = observer((props) => { | ||||
|   const field = useField(); | ||||
|   const width = field.componentProps['width']; | ||||
|   const size = useContext(ColumnSizeContext); | ||||
|   return ( | ||||
|     <div | ||||
|       style={{ width: `calc(${width || 100 / size}% - 24px / ${size})` }} | ||||
|       className={'nb-grid-col'} | ||||
|     > | ||||
|       {props.children} | ||||
|     </div> | ||||
|   ); | ||||
| }); | ||||
| 
 | ||||
|   const getPropertyByPosition = (position) => { | ||||
|     if (position.type === 'row-divider') { | ||||
|       const names = Object.keys(schema.properties); | ||||
|       const isOver = position.rowDividerIndex > names.length - 1; | ||||
|       const index = isOver ? names.length - 1 : position.rowDividerIndex; | ||||
|       const name = names[index]; | ||||
|       const property = schema.properties[name]; | ||||
|       const addProperty = isOver ? addPropertyAfter : addPropertyBefore; | ||||
|       return (data) => { | ||||
|         return addProperty(property, { | ||||
|           type: 'void', | ||||
|           name: `r_${uid()}`, | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             [`c_${uid()}`]: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1, | ||||
|               }, | ||||
|               properties: { | ||||
|                 [data.name]: data, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       }; | ||||
|     } | ||||
|     const rowNames = Object.keys(schema.properties); | ||||
|     const rowName = rowNames[position.rowIndex]; | ||||
|     const row = schema.properties[rowName]; | ||||
|     if (position.type === 'col-divider') { | ||||
|       const names = Object.keys(row.properties); | ||||
|       const isOver = position.colDividerIndex > names.length - 1; | ||||
|       const index = isOver ? names.length - 1 : position.colDividerIndex; | ||||
|       const name = names[index]; | ||||
|       const property = row.properties[name]; | ||||
|       const addProperty = isOver ? addPropertyAfter : addPropertyBefore; | ||||
|       const count = Object.keys(row.properties).length + 1; | ||||
|       return (data) => { | ||||
|         const other = 1 - 1 / count; | ||||
|         Object.keys(row.properties).forEach((name) => { | ||||
|           const prop = row.properties[name]; | ||||
|           const segments = getSchemaAddressSegments(prop); | ||||
|           form.setFieldState(segments.join('.'), (state) => { | ||||
|             state.componentProps.size = other * state.componentProps.size; | ||||
|             console.log({ state }, other * state.componentProps.size); | ||||
|           }); | ||||
|         }); | ||||
|         addProperty(property, { | ||||
|           type: 'void', | ||||
|           name: `c_${uid()}`, | ||||
|           'x-component': 'Grid.Col', | ||||
|           'x-component-props': { | ||||
|             size: 1 / count, | ||||
|           }, | ||||
|           properties: { | ||||
|             [data.name]: data, | ||||
|           }, | ||||
|         }); | ||||
|       }; | ||||
|     } | ||||
|     const colNames = Object.keys(row.properties); | ||||
|     const colName = colNames[position.colIndex]; | ||||
|     const col = row.properties[colName]; | ||||
|     if (position.type === 'block-divider') { | ||||
|       const names = Object.keys(col.properties); | ||||
|       const isOver = position.blockDividerIndex > names.length - 1; | ||||
|       const index = isOver ? names.length - 1 : position.blockDividerIndex; | ||||
|       const name = names[index]; | ||||
|       const property = col.properties[name]; | ||||
|       const addProperty = isOver ? addPropertyAfter : addPropertyBefore; | ||||
|       return (data) => { | ||||
|         return addProperty(property, data); | ||||
|       }; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   return { | ||||
|     schema, | ||||
|     fieldSchema, | ||||
|     refresh, | ||||
|     removeBlock: () => { | ||||
|       if (Object.keys(schema.parent.parent.properties).length === 1) { | ||||
|         removeProperty(schema.parent.parent); | ||||
|       } else if (Object.keys(schema.parent.properties).length === 1) { | ||||
|         removeProperty(schema.parent); | ||||
|         const cols = []; | ||||
|         let allSize = 0; | ||||
|         Object.keys(schema.parent.parent.properties).forEach((name) => { | ||||
|           const prop = schema.parent.parent.properties[name]; | ||||
|           const segments = getSchemaAddressSegments(prop); | ||||
|           cols.push(segments); | ||||
|           form.setFieldState(segments.join('.'), (state) => { | ||||
|             allSize += state.componentProps.size; | ||||
|           }); | ||||
|           return; | ||||
|         }); | ||||
|         for (const segments of cols) { | ||||
|           form.setFieldState(segments.join('.'), (state) => { | ||||
|             state.componentProps.size = state.componentProps.size / allSize; | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|       refresh(); | ||||
| Grid.Block = observer((props) => { | ||||
|   const schema = useFieldSchema(); | ||||
|   const ctx = useContext(GridContext); | ||||
|   const path = useSchemaPath(); | ||||
|   const { isDragging, dragRef, previewRef } = useDrag({ | ||||
|     type: 'grid', | ||||
|     onDragStart() { | ||||
|       console.log('onDragStart'); | ||||
|     }, | ||||
|     addBlock: (data?: any, options?: any) => { | ||||
|       const { insertBefore = false } = options || {}; | ||||
|       data = { | ||||
|         type: 'void', | ||||
|         name: `b_${uid()}`, | ||||
|         'x-component': 'Grid.Block', | ||||
|       }; | ||||
|       const addProperty = insertBefore ? addPropertyBefore : addPropertyAfter; | ||||
|       if (Object.keys(schema.parent.parent.properties).length === 1) { | ||||
|         addProperty(schema.parent.parent, { | ||||
|           type: 'void', | ||||
|           name: `r_${uid()}`, | ||||
|           'x-component': 'Grid.Row', | ||||
|           properties: { | ||||
|             [`c_${uid()}`]: { | ||||
|               type: 'void', | ||||
|               'x-component': 'Grid.Col', | ||||
|               'x-component-props': { | ||||
|                 size: 1, | ||||
|               }, | ||||
|               properties: { | ||||
|                 [data.name]: data, | ||||
|               }, | ||||
|             }, | ||||
|           }, | ||||
|         }); | ||||
|       } else { | ||||
|         addProperty(schema, data); | ||||
|       } | ||||
|       refresh(); | ||||
|     onDragEnd(event) { | ||||
|       console.log('onDragEnd', event.data); | ||||
|     }, | ||||
|     moveTo: (path, position) => { | ||||
|       const source = getSchemaByPath(path); | ||||
|       const insert = getPropertyByPosition(position); | ||||
|       if (!insert) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       // 只有一列时,删除当前行
 | ||||
|       if (Object.keys(source.parent.parent.properties).length === 1) { | ||||
|         source.parent.parent.parent.removeProperty(source.parent.parent.name); | ||||
|       } | ||||
|       // 某列只有一个区块时删除当前列
 | ||||
|       else if (Object.keys(source.parent.properties).length === 1) { | ||||
|         source.parent.parent.removeProperty(source.parent.name); | ||||
|         const cols = []; | ||||
|         let allSize = 0; | ||||
|         Object.keys(source.parent.parent.properties).forEach((name) => { | ||||
|           const prop = source.parent.parent.properties[name]; | ||||
|           const segments = getSchemaAddressSegments(prop); | ||||
|           cols.push(segments); | ||||
|           form.setFieldState(segments.join('.'), (state) => { | ||||
|             allSize += state.componentProps.size; | ||||
|           }); | ||||
|           return; | ||||
|         }); | ||||
|         for (const segments of cols) { | ||||
|           form.setFieldState(segments.join('.'), (state) => { | ||||
|             state.componentProps.size = state.componentProps.size / allSize; | ||||
|           }); | ||||
|         } | ||||
|       } else { | ||||
|         source.parent.removeProperty(source.name); | ||||
|       } | ||||
|       insert(source.toJSON()); | ||||
|       refresh(); | ||||
|     onDrag(event) { | ||||
|       // console.log('onDrag');
 | ||||
|     }, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export * from './DND'; | ||||
| export * from './Row'; | ||||
| export * from './Col'; | ||||
| export * from './Grid'; | ||||
| export * from './Block'; | ||||
|     item: { | ||||
|       path, | ||||
|       schema: schema.toJSON(), | ||||
|     }, | ||||
|   }); | ||||
|   const { isOver, onTopHalf, dropRef } = useDrop({ | ||||
|     accept: 'grid', | ||||
|     data: {}, | ||||
|     canDrop: !isDragging, | ||||
|   }); | ||||
|   useEffect(() => { | ||||
|     if (ctx.ref && ctx.ref.current) { | ||||
|       (ctx.ref.current as HTMLElement).className = isDragging | ||||
|         ? 'nb-grid dragging' | ||||
|         : 'nb-grid'; | ||||
|     } | ||||
|     console.log('ctx.ref.current'); | ||||
|   }, [isDragging]); | ||||
|   return ( | ||||
|     <GridBlockContext.Provider value={{ dragRef }}> | ||||
|       <div | ||||
|         ref={mergeRefs([previewRef, dropRef])} | ||||
|         className={cls('nb-grid-block', { | ||||
|           'top-half': onTopHalf, | ||||
|           hover: isOver, | ||||
|           dragging: isDragging, | ||||
|         })} | ||||
|       > | ||||
|         {props.children} | ||||
|       </div> | ||||
|     </GridBlockContext.Provider> | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| @ -1,114 +1,81 @@ | ||||
| .col-divider { | ||||
|   position: relative; | ||||
|   &:last-child { | ||||
|     &.resizable:hover { | ||||
|       cursor: auto; | ||||
|       &::after { | ||||
|         display: none; | ||||
|       } | ||||
|     } | ||||
|     &.resizable.hover { | ||||
|       &::after { | ||||
|         display: block !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   &.resizable { | ||||
|     cursor: col-resize; | ||||
|   } | ||||
|   &.hover { | ||||
|     cursor: grab !important; | ||||
|   } | ||||
|   &.resizable:hover, | ||||
|   &.hover { | ||||
|     &::after { | ||||
|       content: ''; | ||||
|       display: block; | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 50%; | ||||
|       transform: translateX(-50%); | ||||
|       height: 100%; | ||||
|       width: 12px; | ||||
|       background: #e6f7ff; | ||||
| .nb-grid-col-divider { | ||||
|   width: 24px; | ||||
| } | ||||
| .nb-grid-row { | ||||
|   margin: 0px -24px; | ||||
| } | ||||
| 
 | ||||
| .nb-grid { | ||||
|   .nb-grid-col-divider { | ||||
|     &.resizable { | ||||
|       cursor: col-resize; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .row-divider { | ||||
| .nb-grid.dragging { | ||||
|   position: relative; | ||||
|   &.hover { | ||||
|     &::after { | ||||
|       content: ''; | ||||
|       display: block; | ||||
|       position: absolute; | ||||
|       left: 0; | ||||
|       top: 50%; | ||||
|       transform: translateY(-50%); | ||||
|       height: 12px; | ||||
|       width: 100%; | ||||
|   .nb-grid-col-divider { | ||||
|     // background-color: #ddd; | ||||
|     // opacity: 0.5; | ||||
|     &.resizable { | ||||
|       cursor: col-resize; | ||||
|     } | ||||
|     &.hover { | ||||
|       background: #e6f7ff; | ||||
|     } | ||||
|   } | ||||
|   &:last-child { | ||||
|     margin-bottom: -24px; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .row { | ||||
|   position: relative; | ||||
|   &.hover { | ||||
|     &::after { | ||||
|       content: ''; | ||||
|       display: block; | ||||
|       position: absolute; | ||||
|       left: 24px; | ||||
|       bottom: -18px; | ||||
|       height: 12px; | ||||
|       right: 24px; | ||||
|       background: #e6f7ff; | ||||
|     } | ||||
|     &.top-half { | ||||
|       &::after { | ||||
|         top: -18px; | ||||
|         bottom: auto; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .block { | ||||
|   position: relative; | ||||
|   margin-bottom: 24px; | ||||
|   &:last-child { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
|   &.hover { | ||||
|     &::after { | ||||
|       content: ''; | ||||
|       display: block; | ||||
|       position: absolute; | ||||
|       left: 0; | ||||
|       bottom: -18px; | ||||
|       height: 12px; | ||||
|       width: 100%; | ||||
|       background: #e6f7ff; | ||||
|     } | ||||
|     &.top-half { | ||||
|       &::after { | ||||
|         top: -18px; | ||||
|         bottom: auto; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   > .action-bar { | ||||
|   .nb-grid-row-divider { | ||||
|     // background-color: #ddd; | ||||
|     height: 24px; | ||||
|     width: 100%; | ||||
|     position: absolute; | ||||
|     top: 5px; | ||||
|     right: 8px; | ||||
|     z-index: 222; | ||||
|     z-index: 20; | ||||
|     // opacity: 0.5; | ||||
|     transform: translateY(-100%); | ||||
|     &.hover { | ||||
|       background: #e6f7ff; | ||||
|     } | ||||
|   } | ||||
|   .designable-bar { | ||||
|     display: none; | ||||
|   } | ||||
|   .nb-grid-block { | ||||
|     position: relative; | ||||
|     &.hover { | ||||
|       &::after { | ||||
|         background: #e6f7ff; | ||||
|         content: ''; | ||||
|         display: block; | ||||
|         position: absolute; | ||||
|         bottom: 0; | ||||
|         left: 0; | ||||
|         right: 0; | ||||
|         height: 12px; | ||||
|         z-index: 30; | ||||
|       } | ||||
|       &.top-half { | ||||
|         &::after { | ||||
|           top: 0; | ||||
|           bottom: auto; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .draggable { | ||||
|   cursor: grab !important; | ||||
| } | ||||
| .nb-grid-block.dragging { | ||||
|   > div > .designable-bar { | ||||
|     display: block !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .drag-container { | ||||
|   .nb-grid-block.dragging .designable-bar { | ||||
|     display: none !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .anticon-drag { | ||||
|   cursor: grab; | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,8 @@ import { DesignableSchemaField, SchemaField } from './SchemaField'; | ||||
| 
 | ||||
| export * from './SchemaField'; | ||||
| 
 | ||||
| export { SchemaRenderer, useDesignable, registerComponents, registerScope } from './DesignableSchemaField'; | ||||
| 
 | ||||
| export const SchemaBlock = ({ schema, onlyRenderProperties =  false, designable = true }) => { | ||||
|   const form = useMemo(() => createForm(), []); | ||||
| 
 | ||||
|  | ||||
| @ -22,7 +22,7 @@ group: | ||||
|  * title: 横向菜单 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -33,6 +33,7 @@ const schema = { | ||||
|       'x-designable-bar': 'Menu.DesignableBar', | ||||
|       'x-component-props': { | ||||
|         mode: 'horizontal', | ||||
|         theme: 'dark', | ||||
|       }, | ||||
|       properties: { | ||||
|         item1: { | ||||
| @ -47,24 +48,53 @@ const schema = { | ||||
|         }, | ||||
|         item3: { | ||||
|           type: 'void', | ||||
|           title: '菜单组', | ||||
|           title: '菜单组3', | ||||
|           'x-component': 'Menu.SubMenu', | ||||
|           properties: { | ||||
|             item4: { | ||||
|             item5: { | ||||
|               type: 'void', | ||||
|               title: `子菜单1`, | ||||
|               title: `子菜单5`, | ||||
|               'x-component': 'Menu.SubMenu', | ||||
|               properties: { | ||||
|                 item8: { | ||||
|                   type: 'void', | ||||
|                   title: `子菜单8`, | ||||
|                   'x-component': 'Menu.Item', | ||||
|                 }, | ||||
|                 item9: { | ||||
|                   type: 'void', | ||||
|                   title: `子菜单9`, | ||||
|                   'x-component': 'Menu.Item', | ||||
|                 }, | ||||
|               }, | ||||
|             }, | ||||
|           } | ||||
|         }, | ||||
|         item4: { | ||||
|           type: 'void', | ||||
|           title: '菜单组4', | ||||
|           'x-component': 'Menu.SubMenu', | ||||
|           properties: { | ||||
|             item6: { | ||||
|               type: 'void', | ||||
|               title: `子菜单6`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|             item7: { | ||||
|               type: 'void', | ||||
|               title: `子菜单7`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
| @ -76,7 +106,7 @@ export default () => { | ||||
|  * title: 竖向菜单 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -90,23 +120,7 @@ const schema = { | ||||
|         mode: 'inline', | ||||
|       }, | ||||
|       properties: { | ||||
|         item3: { | ||||
|           type: 'void', | ||||
|           title: '菜单组', | ||||
|           'x-component': 'Menu.SubMenu', | ||||
|           properties: { | ||||
|             item4: { | ||||
|               type: 'void', | ||||
|               title: `子菜单1`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|             item5: { | ||||
|               type: 'void', | ||||
|               title: `子菜单2`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|           } | ||||
|         }, | ||||
|          | ||||
|         item1: { | ||||
|           type: 'void', | ||||
|           title: `菜单1`, | ||||
| @ -117,6 +131,40 @@ const schema = { | ||||
|           title: `菜单2`, | ||||
|           'x-component': 'Menu.Item', | ||||
|         }, | ||||
|         item3: { | ||||
|           type: 'void', | ||||
|           title: '菜单组3', | ||||
|           'x-component': 'Menu.SubMenu', | ||||
|           properties: { | ||||
|             item4: { | ||||
|               type: 'void', | ||||
|               title: `子菜单4`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|             item5: { | ||||
|               type: 'void', | ||||
|               title: `子菜单5`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|           } | ||||
|         }, | ||||
|         item4: { | ||||
|           type: 'void', | ||||
|           title: '菜单组4', | ||||
|           'x-component': 'Menu.SubMenu', | ||||
|           properties: { | ||||
|             item6: { | ||||
|               type: 'void', | ||||
|               title: `子菜单6`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|             item7: { | ||||
|               type: 'void', | ||||
|               title: `子菜单7`, | ||||
|               'x-component': 'Menu.Item', | ||||
|             }, | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| @ -124,7 +172,234 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <div style={{width: 200}}><SchemaBlock schema={schema} /></div> | ||||
|     <div style={{width: 200}}><SchemaRenderer schema={schema} /></div> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
| ``` | ||||
| 
 | ||||
| ### 混合菜单 | ||||
| 
 | ||||
| ```tsx | ||||
| import React, { useRef, useState } from 'react'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| import { MenuContainerContext } from './'; | ||||
| import { Layout } from 'antd'; | ||||
| 
 | ||||
| export default () => { | ||||
|   const ref = useRef(); | ||||
| 
 | ||||
|   const [activeKey, setActiveKey] = useState('item3'); | ||||
| 
 | ||||
|   const schema = { | ||||
|     type: 'object', | ||||
|     properties: { | ||||
|       menu1: { | ||||
|         type: 'void', | ||||
|         'x-component': 'Menu', | ||||
|         'x-component-props': { | ||||
|           defaultSelectedKeys: [activeKey], | ||||
|           mode: 'mix', | ||||
|           theme: 'dark', | ||||
|           onSelect(info) { | ||||
|             setActiveKey(info.key); | ||||
|             console.log({ info }) | ||||
|           }, | ||||
|         }, | ||||
|         properties: { | ||||
|           item1: { | ||||
|             type: 'void', | ||||
|             title: `菜单1`, | ||||
|             'x-component': 'Menu.Item', | ||||
|           }, | ||||
|           item2: { | ||||
|             type: 'void', | ||||
|             title: `菜单2`, | ||||
|             'x-component': 'Menu.Item', | ||||
|           }, | ||||
|           item3: { | ||||
|             type: 'void', | ||||
|             title: '菜单组3', | ||||
|             'x-component': 'Menu.SubMenu', | ||||
|             properties: { | ||||
|               item4: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单4`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|               item5: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单5`, | ||||
|                 'x-component': 'Menu.SubMenu', | ||||
|                 properties: { | ||||
|                   item8: { | ||||
|                     type: 'void', | ||||
|                     title: `子菜单8`, | ||||
|                     'x-component': 'Menu.Item', | ||||
|                   }, | ||||
|                   item9: { | ||||
|                     type: 'void', | ||||
|                     title: `子菜单9`, | ||||
|                     'x-component': 'Menu.Item', | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             } | ||||
|           }, | ||||
|           item4: { | ||||
|             type: 'void', | ||||
|             title: '菜单组4', | ||||
|             'x-component': 'Menu.SubMenu', | ||||
|             properties: { | ||||
|               item6: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单6`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|               item7: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单7`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|             } | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <Layout> | ||||
|         <Layout.Header> | ||||
|           <MenuContainerContext.Provider value={{ | ||||
|             sideMenuRef: ref, | ||||
|           }}> | ||||
|             <SchemaRenderer schema={schema} /> | ||||
|           </MenuContainerContext.Provider> | ||||
|         </Layout.Header> | ||||
|         <Layout> | ||||
|           <Layout.Sider ref={ref} theme={'light'} width={200}> | ||||
|           </Layout.Sider> | ||||
|           <Layout.Content> | ||||
|             {activeKey} | ||||
|           </Layout.Content> | ||||
|         </Layout> | ||||
|       </Layout> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### 设计器模式 | ||||
| 
 | ||||
| ```tsx | ||||
| import React, { useRef, useState } from 'react'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| import { MenuContainerContext } from './'; | ||||
| import { Layout } from 'antd'; | ||||
| 
 | ||||
| export default () => { | ||||
|   const ref = useRef(); | ||||
| 
 | ||||
|   const [activeKey, setActiveKey] = useState('item3'); | ||||
| 
 | ||||
|   const schema = { | ||||
|     type: 'object', | ||||
|     properties: { | ||||
|       menu1: { | ||||
|         type: 'void', | ||||
|         'x-component': 'Menu', | ||||
|         'x-designable-bar': 'Menu.DesignableBar', | ||||
|         'x-component-props': { | ||||
|           defaultSelectedKeys: [activeKey], | ||||
|           mode: 'mix', | ||||
|           theme: 'dark', | ||||
|           onSelect(info) { | ||||
|             setActiveKey(info.key); | ||||
|             console.log({ info }) | ||||
|           }, | ||||
|         }, | ||||
|         properties: { | ||||
|           item1: { | ||||
|             type: 'void', | ||||
|             title: `菜单1`, | ||||
|             'x-component': 'Menu.Item', | ||||
|           }, | ||||
|           item2: { | ||||
|             type: 'void', | ||||
|             title: `菜单2`, | ||||
|             'x-component': 'Menu.Item', | ||||
|           }, | ||||
|           item3: { | ||||
|             type: 'void', | ||||
|             title: '菜单组3', | ||||
|             'x-component': 'Menu.SubMenu', | ||||
|             properties: { | ||||
|               item34: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单4`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|               item5: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单5`, | ||||
|                 'x-component': 'Menu.SubMenu', | ||||
|                 properties: { | ||||
|                   item8: { | ||||
|                     type: 'void', | ||||
|                     title: `子菜单8`, | ||||
|                     'x-component': 'Menu.Item', | ||||
|                   }, | ||||
|                   item9: { | ||||
|                     type: 'void', | ||||
|                     title: `子菜单9`, | ||||
|                     'x-component': 'Menu.Item', | ||||
|                   }, | ||||
|                 }, | ||||
|               }, | ||||
|             } | ||||
|           }, | ||||
|           item4: { | ||||
|             type: 'void', | ||||
|             title: '菜单组4', | ||||
|             'x-component': 'Menu.SubMenu', | ||||
|             properties: { | ||||
|               item6: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单6`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|               item7: { | ||||
|                 type: 'void', | ||||
|                 title: `子菜单7`, | ||||
|                 'x-component': 'Menu.Item', | ||||
|               }, | ||||
|             } | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <div> | ||||
|       <Layout> | ||||
|         <Layout.Header> | ||||
|           <MenuContainerContext.Provider value={{ | ||||
|             sideMenuRef: ref, | ||||
|           }}> | ||||
|             <SchemaRenderer schema={schema} /> | ||||
|           </MenuContainerContext.Provider> | ||||
|         </Layout.Header> | ||||
|         <Layout> | ||||
|           <Layout.Sider ref={ref} theme={'light'} width={200}> | ||||
|           </Layout.Sider> | ||||
|           <Layout.Content> | ||||
|             {activeKey} | ||||
|           </Layout.Content> | ||||
|         </Layout> | ||||
|       </Layout> | ||||
|     </div> | ||||
|   ) | ||||
| } | ||||
| ``` | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,18 +1,19 @@ | ||||
| 
 | ||||
| .ant-menu-dark.ant-menu-horizontal { | ||||
| .ant-menu-horizontal { | ||||
|   width: 100%; | ||||
| } | ||||
| .ant-menu-dark .ant-menu-item-active .designable-bar { | ||||
| 
 | ||||
| .ant-menu-item-active .designable-bar { | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .ant-menu-dark .ant-menu-item-active:hover .designable-bar, | ||||
| .ant-menu-dark .ant-menu-item-active.ant-menu-item-selected .designable-bar { | ||||
| .ant-menu-item-active:hover .designable-bar, | ||||
| .ant-menu-item-active.ant-menu-item-selected .designable-bar { | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
| .ant-menu-light .ant-menu-submenu-active > .ant-menu-submenu-title .designable-bar, | ||||
| .ant-menu-light .ant-menu-item-active .designable-bar { | ||||
| .ant-menu-submenu-active > .ant-menu-submenu-title .designable-bar, | ||||
| .ant-menu-item-active .designable-bar { | ||||
|   display: inline-block; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -20,7 +20,7 @@ group: | ||||
|  * title: 日期选择器 | ||||
|  */ | ||||
| import React from 'react'; | ||||
| import { SchemaBlock } from '../'; | ||||
| import { SchemaRenderer } from '../'; | ||||
| 
 | ||||
| const schema = { | ||||
|   type: 'object', | ||||
| @ -51,7 +51,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -20,7 +20,7 @@ import React from 'react'; | ||||
| import { Button } from 'antd' | ||||
| import { UploadOutlined, InboxOutlined } from '@ant-design/icons' | ||||
| import Upload from './'; | ||||
| import { SchemaBlock, registerComponents } from '../'; | ||||
| import { SchemaRenderer, registerComponents } from '../'; | ||||
| 
 | ||||
| const NormalUpload = (props) => { | ||||
|   return ( | ||||
| @ -69,7 +69,7 @@ const schema = { | ||||
| 
 | ||||
| export default () => { | ||||
|   return ( | ||||
|     <SchemaBlock schema={schema} /> | ||||
|     <SchemaRenderer schema={schema} /> | ||||
|   ); | ||||
| }; | ||||
| ``` | ||||
|  | ||||
| @ -4,8 +4,10 @@ export default { | ||||
|   type: 'void', | ||||
|   name: `m_${uid()}`, | ||||
|   'x-component': 'Menu', | ||||
|   'x-decorator': 'Menu.Designable', | ||||
|   'x-designable-bar': 'Menu.DesignableBar', | ||||
|   'x-component-props': { | ||||
|     mode: 'mix', | ||||
|     theme: 'dark', | ||||
|   }, | ||||
|   properties: { | ||||
|     item2: { | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import React, { useContext, useEffect, useState } from 'react'; | ||||
| import React, { useContext, useEffect, useRef, useState } from 'react'; | ||||
| import { | ||||
|   Button, | ||||
|   Spin, | ||||
| @ -22,11 +22,11 @@ import { | ||||
|   refreshGlobalAction, | ||||
|   RouteComponentContext, | ||||
| } from '../../'; | ||||
| import { SchemaBlock } from '../../blocks'; | ||||
| import { SchemaBlock, SchemaRenderer } from '../../blocks'; | ||||
| import { useRequest } from 'ahooks'; | ||||
| import cloneDeep from 'lodash/cloneDeep'; | ||||
| import { Schema } from '@formily/react'; | ||||
| import { DesignableProvider } from '../../blocks/SchemaField'; | ||||
| import { DesignableContext } from '../../blocks/SchemaField'; | ||||
| import { uid } from '@formily/shared'; | ||||
| import { | ||||
|   DatabaseOutlined, | ||||
| @ -36,6 +36,7 @@ import { | ||||
| import { Tabs } from 'antd'; | ||||
| import '@formily/antd/esm/array-collapse/style'; | ||||
| import './style.less'; | ||||
| import { MenuContainerContext } from '../../blocks/menu'; | ||||
| 
 | ||||
| function LogoutButton() { | ||||
|   const history = useHistory(); | ||||
| @ -163,242 +164,34 @@ function Database() { | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| function useMenuSchema({ schema, selectedKey }) { | ||||
|   const [activeTopKey, setActiveTopKey] = useState(selectedKey); | ||||
|   let topMenuSchema = new Schema(cloneDeep(schema.toJSON())); | ||||
|   topMenuSchema = | ||||
|     topMenuSchema.properties[Object.keys(topMenuSchema.properties)[0]]; | ||||
|   const [activeKey, setActiveKey] = useState(selectedKey); | ||||
|   console.log({ activeKey, topMenuSchema }); | ||||
|   topMenuSchema['x-component-props']['hideSubMenu'] = true; | ||||
|   topMenuSchema['x-component-props']['mode'] = 'horizontal'; | ||||
|   topMenuSchema['x-component-props']['theme'] = 'dark'; | ||||
| 
 | ||||
|   function findLastSelected(activeKey) { | ||||
|     function find(schema: Schema) { | ||||
|       return schema.reduceProperties((selected, current) => { | ||||
|         if (current.name === activeKey) { | ||||
|           return [...selected, current]; | ||||
|         } | ||||
|         if (current.properties) { | ||||
|           return [...selected, ...find(current)]; | ||||
|         } | ||||
|         return [...selected]; | ||||
|       }, []); | ||||
|     } | ||||
| 
 | ||||
|     // const topMenuSchema = new Schema(cloneDeep(schema.toJSON()));
 | ||||
| 
 | ||||
|     let selected = find(topMenuSchema).shift() as Schema; | ||||
| 
 | ||||
|     console.log({ topMenuSchema, selected, schema }); | ||||
| 
 | ||||
|     if (selected && selected.properties) { | ||||
|       const findChild = (properties) => { | ||||
|         const keys = Object.keys(properties || {}); | ||||
|         const firstKey = keys.shift(); | ||||
|         if (firstKey) { | ||||
|           selected = properties[firstKey]; | ||||
|           findChild(properties[firstKey].properties); | ||||
|         } | ||||
|       }; | ||||
|       findChild(selected.properties); | ||||
|     } | ||||
| 
 | ||||
|     return selected; | ||||
| function LayoutWithMenu({ schema }) { | ||||
|   const location = useLocation(); | ||||
|   const ref = useRef(); | ||||
|   const [activeKey, setActiveKey] = useState('item3'); | ||||
|   schema['x-component-props']['defaultSelectedKeys'] = [activeKey]; | ||||
|   schema['x-component-props']['onSelect'] = (info) => { | ||||
|     console.log('LayoutWithMenu', schema) | ||||
|     setActiveKey(info.key); | ||||
|   } | ||||
| 
 | ||||
|   function find(schema: Schema) { | ||||
|     return schema.reduceProperties((selected, current) => { | ||||
|       if (current.name === activeKey) { | ||||
|         return [...selected, current]; | ||||
|       } | ||||
|       if (current.properties) { | ||||
|         return [...selected, ...find(current)]; | ||||
|       } | ||||
|       return [...selected]; | ||||
|     }, []); | ||||
|   } | ||||
| 
 | ||||
|   let selected = (find(topMenuSchema).shift() as Schema) || new Schema({}); | ||||
| 
 | ||||
|   const [pageTitle, setPageTitle] = useState(selected.title); | ||||
|   console.log({ selected, pageTitle }, selected.title); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setPageTitle(selected.title); | ||||
|   }, [selected]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     setActiveKey(selectedKey); | ||||
|   }, [selectedKey]); | ||||
| 
 | ||||
|   let s = selected; | ||||
| 
 | ||||
|   let properties = null; | ||||
| 
 | ||||
|   const selectedKeys = [s.name]; | ||||
| 
 | ||||
|   let sideMenuKey = null; | ||||
| 
 | ||||
|   function getAddress(schema: Schema) { | ||||
|     const segments = []; | ||||
| 
 | ||||
|     segments.unshift(schema.name); | ||||
| 
 | ||||
|     while (schema.parent) { | ||||
|       segments.unshift(schema.parent.name); | ||||
|       schema = schema.parent; | ||||
|     } | ||||
| 
 | ||||
|     return segments.join('.'); | ||||
|   } | ||||
| 
 | ||||
|   while (s.parent) { | ||||
|     if (s.parent['x-component'] === 'Menu') { | ||||
|       sideMenuKey = getAddress(s); | ||||
|       if (s['x-component'] === 'Menu.SubMenu') { | ||||
|         properties = s.properties; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     selectedKeys.push(s.parent.name); | ||||
|     s = s.parent; | ||||
|   } | ||||
| 
 | ||||
|   console.log({ selectedKeys }); | ||||
| 
 | ||||
|   if (properties && selectedKeys.length === 1) { | ||||
|     const findChild = (properties) => { | ||||
|       const keys = Object.keys(properties || {}); | ||||
|       const firstKey = keys.shift(); | ||||
|       if (firstKey) { | ||||
|         selectedKeys.push(firstKey); | ||||
|         findChild(properties[firstKey].properties); | ||||
|       } | ||||
|     }; | ||||
|     findChild(properties); | ||||
|     selectedKey = selectedKeys[selectedKeys.length - 1]; | ||||
|   } | ||||
| 
 | ||||
|   topMenuSchema['x-component-props']['onSelect'] = (info) => { | ||||
|     console.log('onSelect', info.item.props.schema); | ||||
|     // setActiveSchema(info.item.props.schema || {});
 | ||||
|     // setPageTitle(info.item.props.schema.title);
 | ||||
|     setActiveTopKey(info.key); | ||||
|     // setActiveKey(info.key);
 | ||||
|     const selected = findLastSelected(info.key); | ||||
|     console.log('selected', selected.name); | ||||
|     setActiveKey(selected.name); | ||||
|     setPageTitle(selected.title); | ||||
|   }; | ||||
| 
 | ||||
|   let sideMenuSchema = null; | ||||
|   if (properties) { | ||||
|     properties['add_new'] = new Schema({ | ||||
|       type: 'void', | ||||
|       name: `m_${uid()}`, | ||||
|       'x-component': 'Menu.AddNew', | ||||
|     }); | ||||
|     sideMenuSchema = new Schema({ | ||||
|       type: 'void', | ||||
|       name: sideMenuKey, | ||||
|       'x-component': 'Menu', | ||||
|       'x-component-props': { | ||||
|         mode: 'inline', | ||||
|         // selectedKeys,
 | ||||
|         defaultSelectedKeys: selectedKeys, | ||||
|         defaultOpenKeys: selectedKeys, | ||||
|         onSelect(info) { | ||||
|           console.log('onSelect', info.item.props.schema); | ||||
|           setPageTitle(info.item.props.schema.title); | ||||
|           setActiveKey(info.key); | ||||
|         }, | ||||
|       }, | ||||
|       properties, | ||||
|     }).toJSON(); | ||||
|   } | ||||
| 
 | ||||
|   // const sideMenuSchema = properties
 | ||||
|   //   ? new Schema({
 | ||||
|   //       type: 'void',
 | ||||
|   //       name: sideMenuKey,
 | ||||
|   //       'x-component': 'Menu',
 | ||||
|   //       'x-component-props': {
 | ||||
|   //         mode: 'inline',
 | ||||
|   //         // selectedKeys,
 | ||||
|   //         defaultSelectedKeys: selectedKeys,
 | ||||
|   //         defaultOpenKeys: selectedKeys,
 | ||||
|   //         onSelect(info) {
 | ||||
|   //           console.log('onSelect', info.item.props.schema);
 | ||||
|   //           setPageTitle(info.item.props.schema.title);
 | ||||
|   //           setActiveKey(info.key);
 | ||||
|   //         },
 | ||||
|   //       },
 | ||||
|   //       properties,
 | ||||
|   //     }).toJSON()
 | ||||
|   //   : null;
 | ||||
| 
 | ||||
|   topMenuSchema['x-component-props']['defaultSelectedKeys'] = selectedKeys; | ||||
|   topMenuSchema['x-component-props']['defaultOpenKeys'] = selectedKeys; | ||||
| 
 | ||||
|   return { | ||||
|     pageTitle, | ||||
|     topMenuSchema, | ||||
|     sideMenuSchema, | ||||
|     selectedKeys, | ||||
|     activeKey, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function LayoutWithMenu({ schema, activeMenuItemKey }) { | ||||
|   const { activeKey, pageTitle, topMenuSchema, sideMenuSchema } = useMenuSchema( | ||||
|     { schema, selectedKey: activeMenuItemKey }, | ||||
|   ); | ||||
|   const history = useHistory(); | ||||
| 
 | ||||
|   return ( | ||||
|     <Layout> | ||||
|       <Layout.Header | ||||
|         style={{ | ||||
|           height: '45px', | ||||
|           lineHeight: '45px', | ||||
|           padding: 0, | ||||
|           display: 'flex', | ||||
|         }} | ||||
|       > | ||||
|         <div | ||||
|           style={{ | ||||
|             width: 200, | ||||
|             fontSize: 24, | ||||
|             fontWeight: 200, | ||||
|             letterSpacing: 3, | ||||
|             textAlign: 'center', | ||||
|             color: '#fff', | ||||
|           }} | ||||
|         > | ||||
|           NocoBase | ||||
|         </div> | ||||
|         <SchemaBlock designable={false} schema={topMenuSchema} /> | ||||
| 
 | ||||
|         <Database /> | ||||
|       </Layout.Header> | ||||
|       <Layout> | ||||
|         {sideMenuSchema && ( | ||||
|           <Layout.Sider theme={'light'} width={200}> | ||||
|             <SchemaBlock designable={false} schema={sideMenuSchema} /> | ||||
|         <Layout.Header> | ||||
|           <MenuContainerContext.Provider value={{ | ||||
|             sideMenuRef: ref, | ||||
|           }}> | ||||
|             <SchemaRenderer schema={schema} /> | ||||
|           </MenuContainerContext.Provider> | ||||
|         </Layout.Header> | ||||
|         <Layout> | ||||
|           <Layout.Sider ref={ref} theme={'light'} width={200}> | ||||
|           </Layout.Sider> | ||||
|         )} | ||||
|         <Layout.Content> | ||||
|           {pageTitle && <PageHeader title={pageTitle} ghost={false} />} | ||||
|           <div style={{ margin: 24 }}> | ||||
|             {/* {history.location.pathname} */} | ||||
|             <Content activeKey={activeKey} /> | ||||
|           </div> | ||||
|         </Layout.Content> | ||||
|           <Layout.Content> | ||||
|             {location.pathname} | ||||
|             <Content activeKey={activeKey}/> | ||||
|           </Layout.Content> | ||||
|         </Layout> | ||||
|       </Layout> | ||||
|     </Layout> | ||||
|   ); | ||||
|   ) | ||||
| } | ||||
| 
 | ||||
| function Content({ activeKey }) { | ||||
| @ -414,7 +207,7 @@ function Content({ activeKey }) { | ||||
|     return <Spin />; | ||||
|   } | ||||
| 
 | ||||
|   return <SchemaBlock schema={data} />; | ||||
|   return <SchemaRenderer schema={data} />; | ||||
| } | ||||
| 
 | ||||
| export function AdminLayout({ route, children }: any) { | ||||
| @ -432,29 +225,8 @@ export function AdminLayout({ route, children }: any) { | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <DesignableProvider | ||||
|       schema={ | ||||
|         new Schema( | ||||
|           data.name | ||||
|             ? { | ||||
|                 type: 'object', | ||||
|                 properties: { | ||||
|                   [data.name]: data, | ||||
|                 }, | ||||
|               } | ||||
|             : data, | ||||
|         ) | ||||
|       } | ||||
|     > | ||||
|       {(s) => { | ||||
|         // console.log('DesignableProvider', s.properties.item2.title);
 | ||||
|         return ( | ||||
|           <LayoutWithMenu activeMenuItemKey={match.params.name} schema={s} /> | ||||
|         ); | ||||
|       }} | ||||
|     </DesignableProvider> | ||||
|     <LayoutWithMenu schema={data}/> | ||||
|   ); | ||||
|   // return <LayoutWithMenu activeMenuItemKey={match.params.name} schema={data} />;
 | ||||
| } | ||||
| 
 | ||||
| export default AdminLayout; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user