diff --git a/packages/client/src/blocks/DesignableSchemaField/index.tsx b/packages/client/src/blocks/DesignableSchemaField/index.tsx new file mode 100644 index 000000000..82951d76b --- /dev/null +++ b/packages/client/src/blocks/DesignableSchemaField/index.tsx @@ -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) =>
; + +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({ + 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 ; + } + return ( + { + refresh(Math.random()); + props.onRefresh && props.onRefresh(schema); + }, + }} + > + + + ); + }; + + 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 ( + + + + ); +}; diff --git a/packages/client/src/blocks/SchemaField.tsx b/packages/client/src/blocks/SchemaField.tsx index 4dc50b74a..950d9b484 100644 --- a/packages/client/src/blocks/SchemaField.tsx +++ b/packages/client/src/blocks/SchemaField.tsx @@ -47,13 +47,13 @@ export const BlockContext = createContext({ dragRef: null }); const Div = (props) =>
; -const scope = { +export const scope = { useLogin, useRegister, useSubmit, }; -const components = { +export const components = { Div, Space, Card, diff --git a/packages/client/src/blocks/action/index.md b/packages/client/src/blocks/action/index.md index d0218de94..2b0b99333 100644 --- a/packages/client/src/blocks/action/index.md +++ b/packages/client/src/blocks/action/index.md @@ -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 + return } ``` @@ -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 + return } ``` @@ -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 + return } ``` @@ -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 + return } ``` @@ -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 + return } ``` @@ -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 + return } ``` ## 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 (
- + + +
目标容器:
-
+
) } @@ -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 + return } ``` - diff --git a/packages/client/src/blocks/action/index.tsx b/packages/client/src/blocks/action/index.tsx index 4d6bce62a..5e18ff0de 100644 --- a/packages/client/src/blocks/action/index.tsx +++ b/packages/client/src/blocks/action/index.tsx @@ -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 & { Link?: React.FC; URL?: React.FC; Page?: React.FC; + Container?: React.FC; + Popover?: React.FC; Drawer?: React.FC; Modal?: React.FC; Dropdown?: React.FC; @@ -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( +
+ +
, + 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={
- +
} > @@ -170,6 +151,7 @@ export const Action: ActionType = observer((props) => { ); } + return ( ; -} - -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 ; -} - -export default () => { - return ( - - - - - - - Drop Zone1 - - Drop Zone2 - - - Drop Zone3 - - Drop Zone1 - - - ); -}; diff --git a/packages/client/src/blocks/grid/demos/demo5.tsx b/packages/client/src/blocks/grid/demos/demo5.tsx deleted file mode 100644 index 2f7f2b385..000000000 --- a/packages/client/src/blocks/grid/demos/demo5.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Row, Col } from '../'; - -export default () => { - return ( -
- { - console.log(e.data); - }}> - {[1, 2, 3].map((index) => ( - -
col {index}
- - ))} -
-
- ); -}; diff --git a/packages/client/src/blocks/grid/demos/demo6.tsx b/packages/client/src/blocks/grid/demos/demo6.tsx deleted file mode 100644 index a704f54c7..000000000 --- a/packages/client/src/blocks/grid/demos/demo6.tsx +++ /dev/null @@ -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': ( -
- block11 -
- ), - }, - }, - }, - 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': ( -
- block21 -
- ), - }, - }, - }, - }, - }, - 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': ( -
- block211 -
- ), - }, - }, - }, - 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': ( -
- block221 -
- ), - }, - }, - }, - }, - }, - 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': ( -
- block311 -
- ), - }, - }, - }, - }, - }, - }, - }, - }, -}; - -export default () => { - return ; -}; diff --git a/packages/client/src/blocks/grid/DND.tsx b/packages/client/src/blocks/grid/hooks.tsx similarity index 80% rename from packages/client/src/blocks/grid/DND.tsx rename to packages/client/src/blocks/grid/hooks.tsx index 50dd164f9..f5ebbacea 100644 --- a/packages/client/src/blocks/grid/DND.tsx +++ b/packages/client/src/blocks/grid/hooks.tsx @@ -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(); + 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(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 }; +} diff --git a/packages/client/src/blocks/grid/index.md b/packages/client/src/blocks/grid/index.md index 6df26a8c1..007a4d9e3 100644 --- a/packages/client/src/blocks/grid/index.md +++ b/packages/client/src/blocks/grid/index.md @@ -73,62 +73,4 @@ group: - 100% -## 代码演示 - -### useDrag & useDrop - - - -### useColResize - - - -### Grid - - - -## 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 + \ No newline at end of file diff --git a/packages/client/src/blocks/grid/index.tsx b/packages/client/src/blocks/grid/index.tsx index 3fec8a012..11f88a2be 100644 --- a/packages/client/src/blocks/grid/index.tsx +++ b/packages/client/src/blocks/grid/index.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 ( +
+ ); }; -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 ( +
+ ); +}; + +export const Grid: any = observer((props) => { + const schema = useFieldSchema(); + const { insertBefore, insertAfter, remove } = useDesignable(); + const ref = useRef(); + return ( + + +
+ { + 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 ( + <> +
+ +
+ { + 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, + }, + }, + }, + }); + }} + /> + + ); + })} +
+
+
+ ); +}); + +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 ( + + {schema.mapProperties((property, key, index) => { + return ( + <> + 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); + }} + /> + + + ); + })} + + + ); +}); - const schema = getSchemaByPath(field.address.segments); +Grid.Col = observer((props) => { + const field = useField(); + const width = field.componentProps['width']; + const size = useContext(ColumnSizeContext); + return ( +
+ {props.children} +
+ ); +}); - 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 ( + +
+ {props.children} +
+
+ ); +}); diff --git a/packages/client/src/blocks/grid/style.less b/packages/client/src/blocks/grid/style.less index 696731796..8b731fc94 100644 --- a/packages/client/src/blocks/grid/style.less +++ b/packages/client/src/blocks/grid/style.less @@ -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; -} \ No newline at end of file +.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; +} diff --git a/packages/client/src/blocks/index.tsx b/packages/client/src/blocks/index.tsx index a9a7d3ea0..cddc4bb1f 100644 --- a/packages/client/src/blocks/index.tsx +++ b/packages/client/src/blocks/index.tsx @@ -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(), []); diff --git a/packages/client/src/blocks/menu/index.md b/packages/client/src/blocks/menu/index.md index c38fde6a4..7ee1ac063 100644 --- a/packages/client/src/blocks/menu/index.md +++ b/packages/client/src/blocks/menu/index.md @@ -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 ( - + ); }; ``` @@ -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 ( -
+
); }; -``` \ No newline at end of file +``` + +### 混合菜单 + +```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 ( +
+ + + + + + + + + + + {activeKey} + + + +
+ ) +} +``` + +### 设计器模式 + +```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 ( +
+ + + + + + + + + + + {activeKey} + + + +
+ ) +} +``` diff --git a/packages/client/src/blocks/menu/index.tsx b/packages/client/src/blocks/menu/index.tsx index 3c8802a53..9198449bc 100644 --- a/packages/client/src/blocks/menu/index.tsx +++ b/packages/client/src/blocks/menu/index.tsx @@ -16,6 +16,8 @@ import { RecursionField, Schema, SchemaOptionsContext, + FormProvider, + useForm, } from '@formily/react'; import { Menu as AntdMenu, @@ -28,14 +30,7 @@ import { Button, } from 'antd'; import get from 'lodash/get'; -import { FormDialog, FormLayout } from '@formily/antd'; import { uid } from '@formily/shared'; -// import { useSchemaQuery } from '../grid'; -import { - DesignableSchemaContext, - RefreshDesignableSchemaContext, - SchemaField, -} from '../SchemaField'; import { MenuOutlined, GroupOutlined, @@ -53,6 +48,12 @@ import './style.less'; import { Icon } from '../icon-picker'; import { useHistory } from 'react-router-dom'; import cls from 'classnames'; +import ReactDOM from 'react-dom'; +import { useMount } from 'ahooks'; +import { useDesignable, SchemaRenderer, SchemaBlock } from '../'; +import { Router } from "react-router"; + +import { useLifecycle } from 'beautiful-react-hooks'; export type MenuType = React.FC & { Item?: React.FC; @@ -64,108 +65,127 @@ export type MenuType = React.FC & { Url?: React.FC; }; +export const MenuContainerContext = createContext({ + sideMenuRef: null, +}); + +export const MenuContext = createContext({ + mode: null, + designableBar: null, +}); + function Blank() { return null; } function useDesignableBar() { - const schema = useFieldSchema(); - - let s = schema; - let DesignableBarName; - while (s.parent) { - if (s.parent['x-component'] === 'Menu') { - DesignableBarName = s.parent['x-designable-bar']; - break; - } - s = s.parent; - } + const { designableBar } = useContext(MenuContext); const options = useContext(SchemaOptionsContext); - const DesignableBar = DesignableBarName ? get(options.components, DesignableBarName) : null; + const DesignableBar = designableBar + ? get(options.components, designableBar) + : null; return { DesignableBar: DesignableBar || Blank, }; } -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); +export const Menu: MenuType = observer((props) => { + const { onSelect, mode, defaultSelectedKeys, ...others } = props; + const { sideMenuRef } = useContext(MenuContainerContext); + const schema = useFieldSchema(); + const { schema: designableSchema, refresh } = useDesignable(); + const designableBar = schema['x-designable-bar']; + const history = useHistory(); + const renderSideMenu = (selectedKey) => { + if (!selectedKey) { + return; } - 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); + if ((mode as any) !== 'mix') { + return; } - }); -} - -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]; - console.log('names', [...names], path, context); - // names.shift(); - while (s && names.length) { - const name = names.shift(); - s = s.properties[name]; + if (!sideMenuRef || !sideMenuRef.current) { + return; } - return s; + const properties = schema.properties[selectedKey].properties; + console.log({ selectedKey, properties }); + sideMenuRef.current.style.display = properties ? 'block' : 'none'; + const newProps = {}; + Object.keys(properties || {}).forEach((name) => { + newProps[name] = properties[name].toJSON(); + }); + ReactDOM.render( + properties ? ( + + { + const selected = designableSchema.properties[selectedKey]; + const diff = subSchema.properties[`${schema.name}.${selectedKey}`]; + Object.keys(selected.properties).forEach((name) => { + selected.properties[name].parent.removeProperty(name); + }); + Object.keys(diff.properties).forEach((name) => { + if (name.endsWith('-add-new')) { + return; + } + const current = diff.properties[name]; + selected.addProperty(current.name, current.toJSON()); + }); + refresh(); + }} + schema={{ + type: 'void', + name: `${schema.name}.${selectedKey}`, + 'x-component': 'Menu', + 'x-designable-bar': designableBar, + 'x-component-props': { + mode: 'inline', + onSelect, + }, + properties: { + ...newProps, + [`${uid()}-add-new`]: { + type: 'void', + 'x-component': 'Menu.AddNew', + }, + }, + }} + /> + ) : null, + sideMenuRef.current, + ); }; - - const schema = getSchemaByPath(segments || field.address.segments); - - 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(); - }, - }; -} + useMount(() => { + const defaultSelectedKey = defaultSelectedKeys + ? defaultSelectedKeys[0] + : null; + renderSideMenu(defaultSelectedKey); + }); + return ( + + { + console.log('info.key', info.key); + renderSideMenu(info.key); + onSelect && onSelect(info); + }} + mode={(mode as any) === 'mix' ? 'horizontal' : mode} + /> + + ); +}); const AddNewAction = () => { - const field = useField(); - const segments = [...field.address.segments]; - // add new 节点是后加的,并不在菜单树上,要通过父节点处理 add 逻辑 - segments.pop(); - const { appendChild } = useSchemaQuery(segments); + const { insertBefore } = useDesignable(); return ( { trigger={['click']} overlay={ - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }); - }); - }} - style={{ minWidth: 150 }} - > + { + insertBefore({ + type: 'void', + title: uid(), + "x-component": 'Menu.Item', + }) + }} style={{ minWidth: 150 }}> 新建菜单 - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }); - }); - }} - > + 新建分组 @@ -271,419 +217,24 @@ const AddNewAction = () => { ); }; -const SettingAction = () => { - const field = useField(); - const [visible, setVisible] = useState(false); - const { schema, refresh, remove, insertBefore, insertAfter, appendChild } = - useSchemaQuery(); - const text = - schema['x-component'] === 'Menu.SubMenu' ? '当前菜单组' : '当前菜单'; - return ( -
-
{ - e.stopPropagation(); - e.preventDefault(); - }} - className={'designable-bar-actions'} - > - { - setVisible(visible); - }} - trigger={['click']} - overlay={ - - { - FormDialog('编辑菜单', () => { - return ( - - - - - - {/* - 扩展文案 - */} - - ); - }) - .open({ - initialValues: { - title: schema.title, - icon: schema['x-component-props']?.['icon'], - }, - }) - .then((data) => { - schema.title = data.title; - const componentProps = schema['x-component-props'] || {}; - componentProps['icon'] = data.icon; - field.setTitle(data.title); - field.setComponentProps(componentProps); - refresh(); - }); - }} - > - 编辑菜单 - - - 移动到 - - - } title={`${text}前`}> - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertBefore({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertBefore({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }); - }); - }} - > - 新建分组 - - - 添加链接 - - - } - title={`${text}后`} - > - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertAfter( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }), - ); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertAfter( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }), - ); - }); - }} - > - 新建分组 - - - 添加链接 - - - {schema['x-component'] === 'Menu.SubMenu' ? ( - } - title={`${text}里`} - > - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }), - ); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }), - ); - }); - }} - > - 新建分组 - - - 添加链接 - - - ) : null} - - { - Modal.confirm({ - title: '删除菜单', - content: '确认删除此菜单项吗?', - onOk: remove, - }); - }} - > - 删除菜单 - - - } - > - - -
-
- ); -}; - -export const Menu: MenuType = observer((props) => { - const { hideSubMenu, ...others } = props; - // const schema = useFieldSchema(); - // console.log({ schema }, 'Menu'); - return ; -}); - Menu.AddNew = observer((props) => { - const field = useField(); - const { schema } = useSchemaQuery(); - console.log('AddNew', field.address.segments); - return ( + const { designableBar } = useContext(MenuContext); + return designableBar ? ( } > - ); + ) : null; }); Menu.Url = observer((props) => { const field = useField(); - const { schema, refresh } = useSchemaQuery(); + const schema = useFieldSchema(); + const { DesignableBar } = useDesignableBar(); return ( { }} icon={props.icon ? : undefined} > - {field.title} + {field.title} + ); }); @@ -700,11 +252,11 @@ Menu.Url = observer((props) => { Menu.Link = observer((props) => { const history = useHistory(); const field = useField(); - const { schema, refresh } = useSchemaQuery(); + const schema = useFieldSchema(); + const { DesignableBar } = useDesignableBar(); return ( { }} icon={props.icon ? : undefined} > - {field.title} + {field.title} + ); }); Menu.Item = observer((props) => { const field = useField(); + const schema = useFieldSchema(); const { DesignableBar } = useDesignableBar(); - const { schema, refresh } = useSchemaQuery(); return ( { - const el = e.domEvent.target as HTMLElement; - console.log( - 'onMouseEnter', - el.offsetTop, - el.offsetLeft, - el.clientWidth, - el.clientHeight, - ); - }} - onMouseLeave={() => { - console.log('onMouseLeave'); - }} schema={schema} // @ts-ignore eventKey={schema.name} @@ -750,408 +290,13 @@ Menu.Item = observer((props) => { ); }); -Menu.DesignableBar = (props) => { - const field = useField(); - const [visible, setVisible] = useState(false); - const { schema, refresh, remove, insertBefore, insertAfter, appendChild } = - useSchemaQuery(); - const text = - schema['x-component'] === 'Menu.SubMenu' ? '当前菜单组' : '当前菜单'; - return ( -
-
{ - e.stopPropagation(); - e.preventDefault(); - }} - className={'designable-bar-actions'} - > - { - setVisible(visible); - }} - trigger={['click']} - overlay={ - - { - FormDialog('编辑菜单', () => { - return ( - - - - - - {/* - 扩展文案 - */} - - ); - }) - .open({ - initialValues: { - title: schema.title, - icon: schema['x-component-props']?.['icon'], - }, - }) - .then((data) => { - schema.title = data.title; - const componentProps = schema['x-component-props'] || {}; - componentProps['icon'] = data.icon; - field.setTitle(data.title); - field.setComponentProps(componentProps); - refresh(); - }); - }} - > - 编辑菜单 - - - 移动到 - - - } title={`${text}前`}> - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertBefore({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertBefore({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }); - }); - }} - > - 新建分组 - - - 添加链接 - - - } - title={`${text}后`} - > - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertAfter( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }), - ); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - insertAfter( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }), - ); - }); - }} - > - 新建分组 - - - 添加链接 - - - {schema['x-component'] === 'Menu.SubMenu' ? ( - } - title={`${text}里`} - > - { - FormDialog(`新建菜单`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.Item', - 'x-component-props': { - icon: data.icon, - }, - }), - ); - }); - }} - style={{ minWidth: 150 }} - > - 新建菜单 - - { - FormDialog(`新建菜单组`, () => { - return ( - - - - - - - ); - }) - .open({}) - .then((data) => { - appendChild( - new Schema({ - name: `m_${uid()}`, - type: 'void', - title: data.title, - 'x-component': 'Menu.SubMenu', - 'x-component-props': { - icon: data.icon, - }, - properties: { - [`m_${uid()}`]: { - type: 'void', - title: `菜单 ${uid()}`, - 'x-component': 'Menu.Item', - }, - }, - }), - ); - }); - }} - > - 新建分组 - - - 添加链接 - - - ) : null} - - { - Modal.confirm({ - title: '删除菜单', - content: '确认删除此菜单项吗?', - onOk: remove, - }); - }} - > - 删除菜单 - - - } - > - - -
-
- ); -}; - Menu.SubMenu = observer((props) => { const { DesignableBar } = useDesignableBar(); const schema = useFieldSchema(); - let s = schema; - let hideSubMenu; - while (s.parent) { - if (s.parent['x-component'] === 'Menu') { - hideSubMenu = s.parent['x-component-props']?.['hideSubMenu']; - break; - } - s = s.parent; - } - if (hideSubMenu) { - return ; - } - return ( + const { mode } = useContext(MenuContext); + return mode === 'mix' ? ( + + ) : ( { Menu.Divider = observer(AntdMenu.Divider); +function useDesigner() { + const field = useField(); +} + +Menu.DesignableBar = (props) => { + const field = useField(); + const fieldSchema = useFieldSchema(); + const [visible, setVisible] = useState(false); + const { schema, remove, refresh } = useDesignable(); + return ( +
+
{ + e.stopPropagation(); + e.preventDefault(); + }} + className={'designable-bar-actions'} + > + { + // setVisible(visible); + // }} + trigger={['click']} + overlay={ + + { + const title = uid(); + field.title = title; + field.componentProps['icon'] = 'DeleteOutlined'; + schema['x-component-props'] = schema['x-component-props'] || {}; + schema['x-component-props']['icon'] = 'DeleteOutlined'; + schema.title = title; + fieldSchema.title = title; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['icon'] = 'DeleteOutlined'; + refresh(); + }} + > + 修改标题 + + { + Modal.confirm({ + title: '删除菜单', + content: '确认删除此菜单项吗?', + onOk: () => { + remove(); + }, + }); + }} + > + 删除菜单 + + + } + > + + +
+
+ ); +}; + export default Menu; diff --git a/packages/client/src/blocks/menu/style.less b/packages/client/src/blocks/menu/style.less index e2e2d4c08..a30b0bbb5 100644 --- a/packages/client/src/blocks/menu/style.less +++ b/packages/client/src/blocks/menu/style.less @@ -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; } diff --git a/packages/client/src/blocks/time-picker/index.md b/packages/client/src/blocks/time-picker/index.md index aafc26cd6..0b2d4a167 100644 --- a/packages/client/src/blocks/time-picker/index.md +++ b/packages/client/src/blocks/time-picker/index.md @@ -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 ( - + ); }; ``` diff --git a/packages/client/src/blocks/upload/index.md b/packages/client/src/blocks/upload/index.md index 3cde8ccb2..3b87560c3 100644 --- a/packages/client/src/blocks/upload/index.md +++ b/packages/client/src/blocks/upload/index.md @@ -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 ( - + ); }; ``` diff --git a/packages/client/src/demos/api/blocks-getSchema/menu.ts b/packages/client/src/demos/api/blocks-getSchema/menu.ts index 7a2ab036f..2ee0d9b47 100644 --- a/packages/client/src/demos/api/blocks-getSchema/menu.ts +++ b/packages/client/src/demos/api/blocks-getSchema/menu.ts @@ -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: { diff --git a/packages/client/src/templates/admin-layout/index.tsx b/packages/client/src/templates/admin-layout/index.tsx index 7f5af2030..6c9ce918e 100644 --- a/packages/client/src/templates/admin-layout/index.tsx +++ b/packages/client/src/templates/admin-layout/index.tsx @@ -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 ( - -
- NocoBase -
- - - -
- - {sideMenuSchema && ( - - + + + + + + + - )} - - {pageTitle && } -
- {/* {history.location.pathname} */} - -
-
+ + {location.pathname} + + +
-
- ); + ) } function Content({ activeKey }) { @@ -414,7 +207,7 @@ function Content({ activeKey }) { return ; } - return ; + return ; } export function AdminLayout({ route, children }: any) { @@ -432,29 +225,8 @@ export function AdminLayout({ route, children }: any) { } return ( - - {(s) => { - // console.log('DesignableProvider', s.properties.item2.title); - return ( - - ); - }} - + ); - // return ; } export default AdminLayout;