This commit is contained in:
chenos 2021-07-09 21:19:53 +08:00
parent e53a1edf7d
commit 708afa059c
12 changed files with 606 additions and 202 deletions

View File

@ -1,7 +1,7 @@
export default { export default {
type: 'object', type: 'object',
properties: { properties: {
input: { email: {
type: 'string', type: 'string',
required: true, required: true,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -13,7 +13,7 @@ export default {
}, },
}, },
}, },
textarea: { password: {
type: 'string', type: 'string',
required: true, required: true,
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
@ -27,7 +27,6 @@ export default {
}, },
actions: { actions: {
type: 'void', type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Div', 'x-component': 'Div',
properties: { properties: {
submit: { submit: {

View File

@ -2,15 +2,18 @@ import { uid } from '@formily/shared';
export default { export default {
type: 'void', type: 'void',
name: `m_${uid()}`, name: `menu_${uid()}`,
'x-component': 'Menu', 'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar', 'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': { 'x-component-props': {
mode: 'mix', mode: 'mix',
theme: 'dark', theme: 'dark',
defaultSelectedKeys: '{{ selectedKeys }}',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
}, },
properties: { properties: {
item2: { [uid()]: {
type: 'void', type: 'void',
title: `菜单2`, title: `菜单2`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
@ -18,7 +21,7 @@ export default {
icon: 'MailOutlined', icon: 'MailOutlined',
}, },
}, },
item22: { [uid()]: {
type: 'void', type: 'void',
title: `菜单22`, title: `菜单22`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
@ -27,15 +30,23 @@ export default {
// url: 'https://www.google.com', // url: 'https://www.google.com',
}, },
}, },
item3: { [uid()]: {
type: 'void', type: 'void',
title: '菜单组', title: '菜单组1',
'x-component': 'Menu.SubMenu',
'x-component-props': {
icon: 'SettingOutlined',
},
},
[uid()]: {
type: 'void',
title: '菜单组2',
'x-component': 'Menu.SubMenu', 'x-component': 'Menu.SubMenu',
'x-component-props': { 'x-component-props': {
icon: 'SettingOutlined', icon: 'SettingOutlined',
}, },
properties: { properties: {
item6: { [uid()]: {
type: 'void', type: 'void',
title: '菜单6', title: '菜单6',
'x-component': 'Menu.SubMenu', 'x-component': 'Menu.SubMenu',
@ -43,55 +54,48 @@ export default {
icon: 'AppstoreOutlined', icon: 'AppstoreOutlined',
}, },
properties: { properties: {
item9: { [uid()]: {
type: 'void', type: 'void',
title: '菜单9', title: '菜单9',
'x-component': 'Menu.SubMenu', 'x-component': 'Menu.SubMenu',
properties: { properties: {
item10: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单10`, title: `子菜单10`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
// properties: {
// action1: {
// type: 'void',
// title: '页面标题2',
// 'x-component': 'Action.Page',
// },
// },
}, },
item11: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单11`, title: `子菜单11`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
}, },
} }
}, },
item7: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单7`, title: `子菜单7`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
}, },
item8: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单8`, title: `子菜单8`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
}, },
} }
}, },
item4: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单1`, title: `子菜单1`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
}, },
item5: { [uid()]: {
type: 'void', type: 'void',
title: `子菜单2`, title: `子菜单2`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',
}, },
} }
}, },
item1: { [uid()]: {
type: 'void', type: 'void',
title: `菜单1`, title: `菜单1`,
'x-component': 'Menu.Link', 'x-component': 'Menu.Link',

View File

@ -15,6 +15,7 @@ import {
FormProvider, FormProvider,
useField, useField,
useFieldSchema, useFieldSchema,
useForm,
} from '@formily/react'; } from '@formily/react';
import { observable } from '@formily/reactive'; import { observable } from '@formily/reactive';
import { uid, clone } from '@formily/shared'; import { uid, clone } from '@formily/shared';
@ -304,8 +305,7 @@ export function useDesignable(path?: any) {
}; };
} }
export function useSchemaPath() { export function getSchemaPath(schema: Schema) {
const schema = useFieldSchema();
const path = [schema.name]; const path = [schema.name];
let parent = schema.parent; let parent = schema.parent;
while (parent) { while (parent) {
@ -315,7 +315,13 @@ export function useSchemaPath() {
path.unshift(parent.name); path.unshift(parent.name);
parent = parent.parent; parent = parent.parent;
} }
console.log('useSchemaPath', path, schema); console.log('getSchemaPath', path, schema);
return [...path];
}
export function useSchemaPath() {
const schema = useFieldSchema();
const path = getSchemaPath(schema);
return [...path]; return [...path];
} }
@ -325,6 +331,7 @@ export const createDesignableSchemaField = (options) => {
const SchemaField = createSchemaField(options); const SchemaField = createSchemaField(options);
const DesignableSchemaField = (props) => { const DesignableSchemaField = (props) => {
const schema = useMemo(() => new Schema(props.schema), [props.schema]); const schema = useMemo(() => new Schema(props.schema), [props.schema]);
const [, refresh] = useState(0); const [, refresh] = useState(0);
if (props.designable === false) { if (props.designable === false) {
@ -340,8 +347,9 @@ export const createDesignableSchemaField = (options) => {
}, },
}} }}
> >
<SchemaField schema={schema} /> <SchemaField scope={props.scope} components={props.components} schema={schema} />
<CodePreview schema={schema} /> <CodePreview schema={schema} />
<FormValues />
</DesignableContext.Provider> </DesignableContext.Provider>
); );
}; };
@ -349,11 +357,34 @@ export const createDesignableSchemaField = (options) => {
return DesignableSchemaField; return DesignableSchemaField;
}; };
const CodePreview = ({ schema }) => { const FormValues = () => {
const form = useForm();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
<> <>
<CodeOutlined onClick={() => setVisible(true)} /> <CodeOutlined onClick={() => setVisible(true)} />
<Modal
width={'50%'}
onOk={() => setVisible(false)}
onCancel={() => setVisible(false)}
visible={visible}
>
<Editor
height="60vh"
defaultLanguage="json"
value={JSON.stringify(form.values, null, 2)}
/>
{/* <pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre> */}
</Modal>
</>
);
};
const CodePreview = ({ schema }) => {
const [visible, setVisible] = useState(false);
return (
<>
<CodeOutlined style={{position: 'relative', zIndex: 100}} onClick={() => setVisible(true)} />
<Modal <Modal
width={'50%'} width={'50%'}
onOk={() => setVisible(false)} onOk={() => setVisible(false)}
@ -387,6 +418,8 @@ export interface SchemaRendererProps {
designable?: boolean; designable?: boolean;
onRefresh?: any; onRefresh?: any;
onlyRenderProperties?: boolean; onlyRenderProperties?: boolean;
scope?: any;
components?: any;
} }
export const SchemaRenderer = (props: SchemaRendererProps) => { export const SchemaRenderer = (props: SchemaRendererProps) => {
@ -415,6 +448,8 @@ export const SchemaRenderer = (props: SchemaRendererProps) => {
return ( return (
<FormProvider form={form}> <FormProvider form={form}>
<DesignableSchemaField <DesignableSchemaField
scope={props.scope}
components={props.components}
onRefresh={props.onRefresh} onRefresh={props.onRefresh}
designable={props.designable} designable={props.designable}
schema={schema} schema={schema}

View File

@ -13,11 +13,13 @@ group:
操作有三类: 操作有三类:
- 常规操作 - 常规操作Action
- 弹出层操作,弹出层有 Drawer、Modal、Popover - 弹出层操作,弹出层可以是 Action.Drawer、Action.Modal、Action.Popover
- 指定容器内操作 Container - 指定容器内打开: Action.Container
- 下拉菜单,用于收纳多种操作 - 下拉菜单Action.Dropdown用于收纳多种操作
- 跳转操作 - 跳转操作Action.Link、Action.URL 和 Action.Route
Action.Drawer、Action.Modal、Action.Popover 和 Action.Container 需要和 Action 搭配使用
## Action - 常规操作 ## Action - 常规操作

View File

@ -106,12 +106,18 @@ const [VisibleProvider, useVisibleContext] = constate((props: any = {}) => {
export { VisibleProvider, useVisibleContext }; export { VisibleProvider, useVisibleContext };
const BaseAction = observer((props: any) => { const BaseAction = observer((props: any) => {
const { useAction = useDefaultAction, ...others } = props; const {
ButtonComponent = Button,
className,
useAction = useDefaultAction,
...others
} = props;
const field = useField(); const field = useField();
const { run } = useAction(); const { run } = useAction();
const { DesignableBar } = useDesignableBar(); const { DesignableBar } = useDesignableBar();
const { setVisible } = useVisibleContext(); const { setVisible } = useVisibleContext();
const schema = useFieldSchema(); // const schema = useFieldSchema();
const { schema } = useDesignable();
useEffect(() => { useEffect(() => {
field.componentProps.setVisible = setVisible; field.componentProps.setVisible = setVisible;
}, []); }, []);
@ -119,17 +125,18 @@ const BaseAction = observer((props: any) => {
console.log('BaseAction', { field, schema }, field.title); console.log('BaseAction', { field, schema }, field.title);
const renderButton = () => ( const renderButton = () => (
<Button <ButtonComponent
{...others} {...others}
className={classNames(className, `name-${schema.name}`)}
onClick={async (e) => { onClick={async (e) => {
e.stopPropagation(); e.stopPropagation && e.stopPropagation();
setVisible && setVisible(true); setVisible && setVisible(true);
await run(); await run();
}} }}
> >
{field.title} {schema.title}
<DesignableBar /> <DesignableBar />
</Button> </ButtonComponent>
); );
const popover = schema.reduceProperties((items, current) => { const popover = schema.reduceProperties((items, current) => {
@ -268,6 +275,7 @@ Action.Dropdown = observer((props) => {
// const { visible, setVisible } = useVisibleContext(); // const { visible, setVisible } = useVisibleContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
return ( return (
<>
<Popover <Popover
// visible={visible} // visible={visible}
// onVisibleChange={(visible) => { // onVisibleChange={(visible) => {
@ -281,13 +289,16 @@ Action.Dropdown = observer((props) => {
> >
<Button>{schema.title}</Button> <Button>{schema.title}</Button>
</Popover> </Popover>
{/* popover 的按钮初始化时并未渲染,暂时先这么处理 */}
<div style={{display: 'none'}}>{props.children}</div>
</>
); );
}); });
Action.DesignableBar = () => { Action.DesignableBar = () => {
const field = useField(); const field = useField();
const schema = useFieldSchema(); // const schema = useFieldSchema();
const { insertAfter } = useDesignable(); const { schema, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
return ( return (
<div className={classNames('designable-bar', { active: visible })}> <div className={classNames('designable-bar', { active: visible })}>

View File

@ -186,10 +186,15 @@ import { MenuContainerContext } from './';
import { Layout } from 'antd'; import { Layout } from 'antd';
export default () => { export default () => {
const ref = useRef(); const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3'); const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = { const schema = {
type: 'object', type: 'object',
properties: { properties: {
@ -197,13 +202,11 @@ export default () => {
type: 'void', type: 'void',
'x-component': 'Menu', 'x-component': 'Menu',
'x-component-props': { 'x-component-props': {
sideMenuRef: '{{ sideMenuRef }}',
defaultSelectedKeys: [activeKey], defaultSelectedKeys: [activeKey],
mode: 'mix', mode: 'mix',
theme: 'dark', theme: 'dark',
onSelect(info) { onSelect: '{{ onSelect }}',
setActiveKey(info.key);
console.log({ info })
},
}, },
properties: { properties: {
item1: { item1: {
@ -271,14 +274,13 @@ export default () => {
<div> <div>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<MenuContainerContext.Provider value={{ <SchemaRenderer
sideMenuRef: ref, schema={schema}
}}> scope={{ onSelect, sideMenuRef }}
<SchemaRenderer schema={schema} /> />
</MenuContainerContext.Provider>
</Layout.Header> </Layout.Header>
<Layout> <Layout>
<Layout.Sider ref={ref} theme={'light'} width={200}> <Layout.Sider ref={sideMenuRef} theme={'light'} width={200}>
</Layout.Sider> </Layout.Sider>
<Layout.Content> <Layout.Content>
{activeKey} {activeKey}
@ -299,10 +301,15 @@ import { MenuContainerContext } from './';
import { Layout } from 'antd'; import { Layout } from 'antd';
export default () => { export default () => {
const ref = useRef(); const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3'); const [activeKey, setActiveKey] = useState('item3');
const onSelect = (info) => {
setActiveKey(info.key);
console.log({ info })
}
const schema = { const schema = {
type: 'object', type: 'object',
properties: { properties: {
@ -314,10 +321,8 @@ export default () => {
defaultSelectedKeys: [activeKey], defaultSelectedKeys: [activeKey],
mode: 'mix', mode: 'mix',
theme: 'dark', theme: 'dark',
onSelect(info) { sideMenuRef: '{{ sideMenuRef }}',
setActiveKey(info.key); onSelect: '{{ onSelect }}',
console.log({ info })
},
}, },
properties: { properties: {
item1: { item1: {
@ -385,14 +390,17 @@ export default () => {
<div> <div>
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<MenuContainerContext.Provider value={{ <SchemaRenderer
sideMenuRef: ref, schema={schema}
}}> scope={{ onSelect, sideMenuRef }}
<SchemaRenderer schema={schema} /> />
</MenuContainerContext.Provider>
</Layout.Header> </Layout.Header>
<Layout> <Layout>
<Layout.Sider ref={ref} theme={'light'} width={200}> <Layout.Sider
ref={sideMenuRef}
theme={'light'}
width={200}
>
</Layout.Sider> </Layout.Sider>
<Layout.Content> <Layout.Content>
{activeKey} {activeKey}
@ -403,3 +411,82 @@ export default () => {
) )
} }
``` ```
### Menu.Action
```tsx
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaRenderer } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'horizontal',
theme: 'dark',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Action',
properties: {
drawer1: {
type: 'void',
title: '抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
action2: {
type: 'void',
title: '打开二级抽屉',
// 'x-decorator': 'FormItem',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '二级抽屉标题',
'x-component': 'Action.Drawer',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
},
},
},
},
}
},
},
},
},
},
},
},
}
export default () => {
return (
<SchemaRenderer schema={schema} />
);
};
```

View File

@ -51,10 +51,10 @@ import cls from 'classnames';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useMount } from 'ahooks'; import { useMount } from 'ahooks';
import { useDesignable, SchemaRenderer } from '..'; import { useDesignable, SchemaRenderer } from '..';
import { Router } from "react-router"; import { Router } from 'react-router';
import { useLifecycle } from 'beautiful-react-hooks'; import { useLifecycle } from 'beautiful-react-hooks';
import { useDefaultAction } from '../action'; import { Action, useDefaultAction } from '../action';
export type MenuType = React.FC<MenuProps & { hideSubMenu?: boolean }> & { export type MenuType = React.FC<MenuProps & { hideSubMenu?: boolean }> & {
Item?: React.FC<MenuItemProps>; Item?: React.FC<MenuItemProps>;
@ -63,6 +63,7 @@ export type MenuType = React.FC<MenuProps & { hideSubMenu?: boolean }> & {
DesignableBar?: React.FC<any>; DesignableBar?: React.FC<any>;
AddNew?: React.FC<any>; AddNew?: React.FC<any>;
Link?: React.FC<MenuItemProps>; Link?: React.FC<MenuItemProps>;
Action?: React.FC<MenuItemProps>;
Url?: React.FC<MenuItemProps & { url: string }>; Url?: React.FC<MenuItemProps & { url: string }>;
}; };
@ -92,9 +93,9 @@ function useDesignableBar() {
}; };
} }
export const Menu: MenuType = observer((props) => { export const Menu: MenuType = observer((props: any) => {
const { onSelect, mode, defaultSelectedKeys, ...others } = props; const { sideMenuRef, onSelect, mode, defaultSelectedKeys, ...others } = props;
const { sideMenuRef } = useContext(MenuContainerContext); let defaultSelectedKey = defaultSelectedKeys ? defaultSelectedKeys[0] : null;
const schema = useFieldSchema(); const schema = useFieldSchema();
const { schema: designableSchema, refresh } = useDesignable(); const { schema: designableSchema, refresh } = useDesignable();
const designableBar = schema['x-designable-bar']; const designableBar = schema['x-designable-bar'];
@ -109,33 +110,41 @@ export const Menu: MenuType = observer((props) => {
if (!sideMenuRef || !sideMenuRef.current) { if (!sideMenuRef || !sideMenuRef.current) {
return; return;
} }
const properties = schema.properties[selectedKey].properties; const subSchema = schema.properties[selectedKey];
console.log({ selectedKey, properties }); if (!subSchema) {
sideMenuRef.current.style.display = properties ? 'block' : 'none'; sideMenuRef.current.style.display = 'none';
ReactDOM.render(null, sideMenuRef.current);
return;
}
if (subSchema['x-component'] !== 'Menu.SubMenu') {
sideMenuRef.current.style.display = 'none';
return;
}
const properties = subSchema.properties || {};
sideMenuRef.current.style.display = 'block';
const newProps = {}; const newProps = {};
Object.keys(properties || {}).forEach((name) => { Object.keys(properties || {}).forEach((name) => {
newProps[name] = properties[name].toJSON(); newProps[name] = properties[name].toJSON();
}); });
ReactDOM.render( ReactDOM.render(
properties ? ( <Router history={history}>
<Router history={history}>
<SchemaRenderer <SchemaRenderer
key={`${Math.random()}`} key={`${Math.random()}`}
onRefresh={(subSchema: Schema) => { onRefresh={(subSchema: Schema) => {
const selected = designableSchema.properties[selectedKey]; const selected = designableSchema.properties[selectedKey];
const diff = subSchema.properties[`${schema.name}.${selectedKey}`]; const diff = subSchema.properties[`${schema.name}.${selectedKey}`];
Object.keys(selected.properties).forEach((name) => { Object.keys(selected.properties || {}).forEach((name) => {
selected.properties[name].parent.removeProperty(name); selected.properties[name].parent.removeProperty(name);
}); });
Object.keys(diff.properties).forEach((name) => { Object.keys(diff.properties).forEach((name) => {
if (name.endsWith('-add-new')) { if (name.endsWith('-add-new')) {
return; return;
} }
console.log('diff', name) console.log('diff', name);
const current = diff.properties[name]; const current = diff.properties[name];
selected.addProperty(current.name, current.toJSON()); selected.addProperty(current.name, current.toJSON());
}); });
console.log({selected }) console.log({ selected });
refresh(); refresh();
}} }}
schema={{ schema={{
@ -155,15 +164,13 @@ export const Menu: MenuType = observer((props) => {
}, },
}, },
}} }}
/></Router> />
) : null, </Router>,
sideMenuRef.current, sideMenuRef.current,
); );
}; };
useMount(() => { useMount(() => {
const defaultSelectedKey = defaultSelectedKeys console.log({ defaultSelectedKey }, schema.properties);
? defaultSelectedKeys[0]
: null;
renderSideMenu(defaultSelectedKey); renderSideMenu(defaultSelectedKey);
}); });
return ( return (
@ -197,13 +204,16 @@ const AddNewAction = () => {
trigger={['click']} trigger={['click']}
overlay={ overlay={
<AntdMenu> <AntdMenu>
<AntdMenu.Item onClick={() => { <AntdMenu.Item
insertBefore({ onClick={() => {
type: 'void', insertBefore({
title: uid(), type: 'void',
"x-component": 'Menu.Item', title: uid(),
}) 'x-component': 'Menu.Item',
}} style={{ minWidth: 150 }}> });
}}
style={{ minWidth: 150 }}
>
<MenuOutlined /> <MenuOutlined />
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item> <AntdMenu.Item>
@ -298,6 +308,25 @@ Menu.Item = observer((props: any) => {
); );
}); });
Menu.Action = observer((props: any) => {
const { icon, ...others } = props;
const schema = useFieldSchema();
const { DesignableBar } = useDesignableBar();
return (
<Action
// @ts-ignore
eventKey={schema.name}
key={schema.name}
icon={icon ? <Icon type={icon as string} /> : undefined}
ButtonComponent={AntdMenu.Item}
{...others}
/>
// <Action {...others}/>
// <DesignableBar />
// </AntdMenu.Item>
);
});
Menu.SubMenu = observer((props) => { Menu.SubMenu = observer((props) => {
const { DesignableBar } = useDesignableBar(); const { DesignableBar } = useDesignableBar();
const schema = useFieldSchema(); const schema = useFieldSchema();
@ -372,11 +401,13 @@ Menu.DesignableBar = (props) => {
const title = uid(); const title = uid();
field.title = title; field.title = title;
field.componentProps['icon'] = 'DeleteOutlined'; field.componentProps['icon'] = 'DeleteOutlined';
schema['x-component-props'] = schema['x-component-props'] || {}; schema['x-component-props'] =
schema['x-component-props'] || {};
schema['x-component-props']['icon'] = 'DeleteOutlined'; schema['x-component-props']['icon'] = 'DeleteOutlined';
schema.title = title; schema.title = title;
fieldSchema.title = title; fieldSchema.title = title;
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props'] =
fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['icon'] = 'DeleteOutlined'; fieldSchema['x-component-props']['icon'] = 'DeleteOutlined';
refresh(); refresh();
}} }}
@ -396,6 +427,9 @@ Menu.DesignableBar = (props) => {
> >
<DeleteOutlined /> <DeleteOutlined />
</AntdMenu.Item> </AntdMenu.Item>
<AntdMenu.Item>
<ModalButton />
</AntdMenu.Item>
</AntdMenu> </AntdMenu>
} }
> >
@ -406,4 +440,27 @@ Menu.DesignableBar = (props) => {
); );
}; };
function ModalButton() {
const [visible, setVisible] = useState(false);
return (
<>
<div
onClick={() => {
setVisible(true);
}}
>
</div>
<Modal
visible={visible}
onCancel={() => {
setVisible(false);
}}
>
aaa
</Modal>
</>
);
}
export default Menu; export default Menu;

View File

@ -35,10 +35,12 @@
left: 0; left: 0;
bottom: 0; bottom: 0;
border: 2px solid #1890ff; border: 2px solid #1890ff;
pointer-events: none;
&.active { &.active {
display: block; display: block;
} }
.designable-bar-actions { .designable-bar-actions {
pointer-events: auto;
position: absolute; position: absolute;
right: 0; right: 0;
line-height: 1rem; line-height: 1rem;
@ -60,4 +62,16 @@
right: -20px; right: -20px;
border: 2px solid #1890ff; border: 2px solid #1890ff;
} }
}
.ant-menu-title-content {
.ant-btn {
background: none;
border: 0;
border-radius: 0;
color: inherit;
padding: 0 20px;
height: 100%;
margin: 0 -20px;
}
} }

View File

@ -14,7 +14,7 @@ function useAction() {
} }
const schema = { const schema = {
name: `t_${uid()}`, name: `table_${uid()}`,
type: 'array', type: 'array',
'x-component': 'Table', 'x-component': 'Table',
// default: [ // default: [
@ -36,7 +36,7 @@ const schema = {
// isRemoteDataSource: true, // isRemoteDataSource: true,
}, },
properties: { properties: {
[`a_${uid()}`]: { [`action_bar_${uid()}`]: {
type: 'void', type: 'void',
'x-component': 'Table.ActionBar', 'x-component': 'Table.ActionBar',
properties: { properties: {
@ -132,22 +132,22 @@ const schema = {
}, },
}, },
}, },
[`a_${uid()}`]: { // [`a_${uid()}`]: {
type: 'void', // type: 'void',
'x-component': 'Table.ActionBar', // 'x-component': 'Table.ActionBar',
'x-component-props': { // 'x-component-props': {
align: 'bottom', // align: 'bottom',
}, // },
properties: { // properties: {
pagination: { // pagination: {
type: 'void', // type: 'void',
'x-component': 'Table.Pagination', // 'x-component': 'Table.Pagination',
'x-component-props': { // 'x-component-props': {
defaultPageSize: 5, // defaultPageSize: 5,
}, // },
}, // },
}, // },
}, // },
[`a_${uid()}`]: { [`a_${uid()}`]: {
type: 'void', type: 'void',
'x-component': 'Table.ActionBar', 'x-component': 'Table.ActionBar',
@ -163,7 +163,7 @@ const schema = {
}, },
}, },
}, },
[`c_${uid()}`]: { [`column_${uid()}`]: {
type: 'void', type: 'void',
title: '排序', title: '排序',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
@ -174,7 +174,7 @@ const schema = {
}, },
}, },
}, },
[`c_${uid()}`]: { [`column_${uid()}`]: {
type: 'void', type: 'void',
title: '序号', title: '序号',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
@ -185,7 +185,7 @@ const schema = {
}, },
}, },
}, },
[`c_${uid()}`]: { [`column_${uid()}`]: {
type: 'void', type: 'void',
title: '字段1', title: '字段1',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
@ -197,7 +197,7 @@ const schema = {
field1: { field1: {
type: 'string', type: 'string',
required: true, required: true,
// 'x-read-pretty': true, 'x-read-pretty': true,
'x-decorator-props': { 'x-decorator-props': {
feedbackLayout: 'popover', feedbackLayout: 'popover',
}, },
@ -206,16 +206,17 @@ const schema = {
}, },
}, },
}, },
[`c_${uid()}`]: { [`column_${uid()}`]: {
type: 'void', type: 'void',
title: '字段2', title: '字段2',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
'x-designable-bar': 'Table.Column.DesignableBar',
properties: { properties: {
field2: { field2: {
type: 'string', type: 'string',
// title: '字段2', // title: '字段2',
required: true, required: true,
// 'x-read-pretty': true, 'x-read-pretty': true,
'x-decorator-props': { 'x-decorator-props': {
feedbackLayout: 'popover', feedbackLayout: 'popover',
}, },
@ -224,18 +225,18 @@ const schema = {
}, },
}, },
}, },
[`col_${uid()}`]: { [`column_${uid()}`]: {
type: 'void', type: 'void',
title: '操作', title: '操作',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
properties: { properties: {
action1: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '查看', title: '查看',
'x-component': 'Action', 'x-component': 'Action',
'x-default-action': true, 'x-designable-bar': 'Table.Action.DesignableBar',
'x-designable-bar': 'Action.DesignableBar',
properties: { properties: {
drawer1: { drawer1: {
type: 'void', type: 'void',
@ -285,13 +286,12 @@ const schema = {
}, },
}, },
}, },
action2: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '修改', title: '修改',
'x-component': 'Action', 'x-component': 'Action',
'x-default-action': true, 'x-designable-bar': 'Table.Action.DesignableBar',
'x-designable-bar': 'Action.DesignableBar',
properties: { properties: {
drawer1: { drawer1: {
type: 'void', type: 'void',
@ -333,7 +333,7 @@ const schema = {
}, },
}, },
}, },
action3: { [uid()]: {
type: 'void', type: 'void',
title: '删除', title: '删除',
'x-component': 'Action', 'x-component': 'Action',
@ -341,12 +341,12 @@ const schema = {
useAction: '{{ useTableDestroyAction }}', useAction: '{{ useTableDestroyAction }}',
}, },
}, },
action4: { [uid()]: {
type: 'void', type: 'void',
title: '...', title: '...',
'x-component': 'Action.Dropdown', 'x-component': 'Action.Dropdown',
properties: { properties: {
action5: { [uid()]: {
type: 'void', type: 'void',
title: '操作 1', title: '操作 1',
'x-component': 'Action', 'x-component': 'Action',
@ -355,21 +355,22 @@ const schema = {
disabled: true, disabled: true,
}, },
}, },
action4: { [uid()]: {
type: 'void', type: 'void',
title: '操作 2', title: '操作 2',
'x-component': 'Action', 'x-component': 'Action',
'x-default-action': true,
'x-component-props': { 'x-component-props': {
useAction, useAction,
}, },
}, },
action1: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '查看', title: '查看',
'x-component': 'Action', 'x-component': 'Action',
// 'x-default-action': true, // 'x-default-action': true,
'x-designable-bar': 'Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
properties: { properties: {
drawer1: { drawer1: {
type: 'void', type: 'void',
@ -419,13 +420,12 @@ const schema = {
}, },
}, },
}, },
action2: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '修改', title: '修改',
'x-component': 'Action', 'x-component': 'Action',
'x-default-action': true, 'x-designable-bar': 'Table.Action.DesignableBar',
'x-designable-bar': 'Action.DesignableBar',
properties: { properties: {
drawer1: { drawer1: {
type: 'void', type: 'void',
@ -467,7 +467,7 @@ const schema = {
}, },
}, },
}, },
action3: { [uid()]: {
type: 'void', type: 'void',
title: '删除', title: '删除',
'x-component': 'Action', 'x-component': 'Action',
@ -482,17 +482,14 @@ const schema = {
}, },
}; };
const form = createForm(); const form = createForm({
// designable: true,
});
export default observer(() => { export default observer(() => {
return ( return (
<div> <div>
<SchemaRenderer form={form} schema={schema} /> <SchemaRenderer form={form} schema={schema} />
{/* <Editor
height="200px"
defaultLanguage="json"
value={JSON.stringify(form.values, null, 2)}
/> */}
</div> </div>
); );
}); });

View File

@ -8,6 +8,7 @@ import {
useForm, useForm,
FormProvider, FormProvider,
createSchemaField, createSchemaField,
SchemaOptionsContext,
} from '@formily/react'; } from '@formily/react';
import { import {
Button, Button,
@ -16,24 +17,32 @@ import {
Space, Space,
Spin, Spin,
Table as AntdTable, Table as AntdTable,
Dropdown,
Menu,
} from 'antd'; } from 'antd';
import { findIndex } from 'lodash'; import { findIndex, get } from 'lodash';
import constate from 'constate'; import constate from 'constate';
import useRequest from '@ahooksjs/use-request'; import useRequest from '@ahooksjs/use-request';
import { BaseResult } from '@ahooksjs/use-request/lib/types'; import { BaseResult } from '@ahooksjs/use-request/lib/types';
import { uid, clone } from '@formily/shared'; import { uid, clone } from '@formily/shared';
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { useVisibleContext } from '../action'; import { useVisibleContext } from '../action';
import { SortableHandle, SortableContainer, SortableElement } from 'react-sortable-hoc' import {
SortableHandle,
SortableContainer,
SortableElement,
} from 'react-sortable-hoc';
import cls from 'classnames'; import cls from 'classnames';
import { getSchemaPath, useDesignable, useSchemaPath } from '../DesignableSchemaField';
import './style.less';
interface TableRowProps { interface TableRowProps {
index: number; index: number;
data: any; data: any;
} }
const SortableRow = SortableElement((props: any) => <tr {...props} />) const SortableRow = SortableElement((props: any) => <tr {...props} />);
const SortableBody = SortableContainer((props: any) => <tbody {...props} />) const SortableBody = SortableContainer((props: any) => <tbody {...props} />);
const TableRowContext = createContext<TableRowProps>(null); const TableRowContext = createContext<TableRowProps>(null);
@ -105,7 +114,8 @@ function useTableActionBars() {
} }
function useTableColumns(props?: any) { function useTableColumns(props?: any) {
const schema = useFieldSchema(); const { schema } = useDesignable();
// const schema = useFieldSchema();
const { dataSource } = props || {}; const { dataSource } = props || {};
function findColumns(schema: Schema): Schema[] { function findColumns(schema: Schema): Schema[] {
@ -120,7 +130,7 @@ function useTableColumns(props?: any) {
return findColumns(schema).map((item) => { return findColumns(schema).map((item) => {
const columnProps = item['x-component-props'] || {}; const columnProps = item['x-component-props'] || {};
return { return {
title: item.title, title: <RecursionField name={item.name} schema={item} onlyRenderSelf />,
dataIndex: item.name, dataIndex: item.name,
...columnProps, ...columnProps,
render(value, record, recordIndex) { render(value, record, recordIndex) {
@ -282,23 +292,22 @@ const TableContainer = observer((props) => {
useTableContext(); useTableContext();
const rowKey = field.componentProps.rowKey || 'id'; const rowKey = field.componentProps.rowKey || 'id';
const defaultAction = useDefaultAction(); const defaultAction = useDefaultAction();
console.log({ defaultAction });
const dataSource = Array.isArray(field.value) ? field.value.slice() : []; const dataSource = Array.isArray(field.value) ? field.value.slice() : [];
const columns = useTableColumns({ dataSource }); const columns = useTableColumns({ dataSource });
const ref = useRef<HTMLDivElement>() const ref = useRef<HTMLDivElement>();
const addTdStyles = (node: HTMLElement) => { const addTdStyles = (node: HTMLElement) => {
const helper = document.body.querySelector(`.nb-table-sort-helper`) const helper = document.body.querySelector(`.nb-table-sort-helper`);
if (helper) { if (helper) {
const tds = node.querySelectorAll('td') const tds = node.querySelectorAll('td');
requestAnimationFrame(() => { requestAnimationFrame(() => {
helper.querySelectorAll('td').forEach((td, index) => { helper.querySelectorAll('td').forEach((td, index) => {
if (tds[index]) { if (tds[index]) {
td.style.width = getComputedStyle(tds[index]).width td.style.width = getComputedStyle(tds[index]).width;
} }
}) });
}) });
} }
} };
return ( return (
<div ref={ref} className={'nb-table'}> <div ref={ref} className={'nb-table'}>
{actionBars.top.map((actionBarSchema) => { {actionBars.top.map((actionBarSchema) => {
@ -339,10 +348,10 @@ const TableContainer = observer((props) => {
// disableAutoscroll // disableAutoscroll
helperClass={`nb-table-sort-helper`} helperClass={`nb-table-sort-helper`}
helperContainer={() => { helperContainer={() => {
return ref.current?.querySelector('tbody') return ref.current?.querySelector('tbody');
}} }}
onSortStart={({ node }) => { onSortStart={({ node }) => {
addTdStyles(node) addTdStyles(node);
}} }}
onSortEnd={({ oldIndex, newIndex }) => { onSortEnd={({ oldIndex, newIndex }) => {
field.move(oldIndex, newIndex); field.move(oldIndex, newIndex);
@ -352,13 +361,11 @@ const TableContainer = observer((props) => {
/> />
), ),
row: (props: any) => { row: (props: any) => {
const index = findIndex(field.value, item => item[rowKey] === props['data-row-key']); const index = findIndex(
return ( field.value,
<SortableRow (item) => item[rowKey] === props['data-row-key'],
index={index} );
{...props} return <SortableRow index={index} {...props} />;
/>
)
}, },
}, },
}} }}
@ -366,20 +373,17 @@ const TableContainer = observer((props) => {
const index = dataSource.indexOf(data); const index = dataSource.indexOf(data);
return { return {
onClick(e) { onClick(e) {
console.log('onRow', (e.target as HTMLElement), (e.target as HTMLElement).classList.contains('ant-table-cell'));
if (!(e.target as HTMLElement).classList.contains('ant-table-cell')) {
return;
}
if (!defaultAction) { if (!defaultAction) {
return; return;
} }
// console.log('defaultAction'); const el = (e.target as HTMLElement);
field if (
.query(`.${schema.name}.${index}.${defaultAction.name}`) !el.classList.contains('ant-table-cell')
.take((f) => { ) {
const setVisible = f.componentProps.setVisible; return;
setVisible && setVisible(true); }
}); const btn = el.parentElement.querySelector<HTMLElement>(`.name-${defaultAction.name}`);
btn && btn.click();
}, },
}; };
}} }}
@ -411,7 +415,99 @@ export const Table: any = observer((props) => {
); );
}); });
Table.Column = () => null; function Blank() {
return null;
}
function useDesignableBar() {
const schema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const DesignableBar = get(options.components, schema['x-designable-bar']);
return {
DesignableBar: DesignableBar || Blank,
};
}
Table.Column = observer((props) => {
const schema = useFieldSchema();
const field = useField();
console.log('Table.Column', schema, field.title);
const { DesignableBar } = useDesignableBar();
return (
<div className={'nb-table-column'}>
{field.title}
<DesignableBar />
</div>
);
});
Table.Column.DesignableBar = () => {
const field = useField();
// const fieldSchema = useFieldSchema();
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
console.log('Table.Column.DesignableBar', { schema });
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item onClick={(e) => {
const title = uid();
field.title = title;
schema.title = title;
setVisible(false);
}}></Menu.Item>
<Menu.Item onClick={() => {
remove();
console.log('Table.Column.DesignableBar', { schema });
}}></Menu.Item>
<Menu.Item onClick={() => {
const name = uid();
insertAfter({
name: `column_${name}`,
type: 'void',
title: `字段 ${name}`,
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
[name]: {
type: 'string',
required: true,
// 'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
})
}}></Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};
Table.ActionBar = observer((props) => { Table.ActionBar = observer((props) => {
return ( return (
@ -447,17 +543,20 @@ const SortHandle = SortableHandle((props: any) => {
className={cls(`nb-table-sort-handle`, props.className)} className={cls(`nb-table-sort-handle`, props.className)}
style={{ ...props.style }} style={{ ...props.style }}
/> />
) );
}) as any }) as any;
Table.SortHandle = observer((props) => { Table.SortHandle = observer((props) => {
const field = useField<Formily.Core.Models.Field>(); const field = useField<Formily.Core.Models.Field>();
console.log('SortHandle', field.value); console.log('SortHandle', field.value);
return <SortHandle {...props}/>; return <SortHandle {...props} />;
}); });
Table.Index = observer((props) => { Table.Index = observer((props) => {
const index = useTableIndex(); const index = useTableIndex();
const schema = useFieldSchema();
const field = useField<Formily.Core.Models.Field>();
const path = useSchemaPath();
return <div>#{index + 1}</div>; return <div>#{index + 1}</div>;
}); });
@ -484,3 +583,41 @@ Table.Addition = observer((props: any) => {
</Button> </Button>
); );
}); });
Table.Action = () => null;
Table.Action.DesignableBar = () => {
const field = useField();
const path = useSchemaPath();
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
console.log('Table.Action.DesignableBar', path, field.address.entire, { schema, field });
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item onClick={(e) => {
schema.title = uid();
refresh();
}}></Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
</div>
);
};

View File

@ -0,0 +1,61 @@
.nb-table-column {
&:hover {
> .designable-bar {
display: block;
}
}
> .designable-bar {
display: none;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
border-radius: 0;
border: 2px solid #1890ff;
&.active {
display: block;
}
.designable-bar-actions {
position: absolute;
right: 0;
line-height: 1rem;
background-color: #1890ff;
color: #fff;
z-index: 10;
padding: 0 3px;
.anticon {
font-size: 10px;
}
}
}
}
.ant-popover.nb-action-group {
z-index: 1000;
padding-top: 0;
.ant-popover-arrow {
display: none;
}
.ant-popover-inner-content {
padding: 4px 0;
.ant-btn {
display: block;
min-width: 100px;
width: 100%;
border: 0;
border-radius: 0;
text-align: left;
padding: 5px 12px;
color: #000;
&:hover {
background-color: #f5f5f5;
}
&[disabled] {
color: #00000040;
background-color:#fff;
cursor:not-allowed;
}
}
}
}

View File

@ -161,33 +161,36 @@ function Database() {
} }
function LayoutWithMenu({ schema }) { function LayoutWithMenu({ schema }) {
const match = useRouteMatch<any>();
const location = useLocation(); const location = useLocation();
const ref = useRef(); const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState('item3'); const [activeKey, setActiveKey] = useState(match.params.name);
schema['x-component-props']['defaultSelectedKeys'] = [activeKey]; const onSelect = (info) => {
schema['x-component-props']['onSelect'] = (info) => { console.log('LayoutWithMenu', schema);
console.log('LayoutWithMenu', schema)
setActiveKey(info.key); setActiveKey(info.key);
} };
console.log({ match });
return ( return (
<Layout> <Layout>
<Layout.Header> <Layout.Header>
<MenuContainerContext.Provider value={{ <SchemaRenderer
sideMenuRef: ref, schema={schema}
}}> scope={{ sideMenuRef, onSelect, selectedKeys: [activeKey].filter(Boolean) }}
<SchemaRenderer schema={schema} /> />
</MenuContainerContext.Provider> </Layout.Header>
</Layout.Header> <Layout>
<Layout> <Layout.Sider
<Layout.Sider ref={ref} theme={'light'} width={200}> ref={sideMenuRef}
</Layout.Sider> theme={'light'}
<Layout.Content> width={200}
{location.pathname} ></Layout.Sider>
<Content activeKey={activeKey}/> <Layout.Content>
</Layout.Content> {location.pathname}
</Layout> <Content activeKey={activeKey} />
</Layout.Content>
</Layout> </Layout>
) </Layout>
);
} }
function Content({ activeKey }) { function Content({ activeKey }) {
@ -207,7 +210,6 @@ function Content({ activeKey }) {
} }
export function AdminLayout({ route, children }: any) { export function AdminLayout({ route, children }: any) {
const match = useRouteMatch<any>();
const { data = {}, loading } = useRequest( const { data = {}, loading } = useRequest(
`/api/blocks:getSchema/${route.blockId}`, `/api/blocks:getSchema/${route.blockId}`,
@ -220,9 +222,7 @@ export function AdminLayout({ route, children }: any) {
return <Spin />; return <Spin />;
} }
return ( return <LayoutWithMenu schema={data} />;
<LayoutWithMenu schema={data}/>
);
} }
export default AdminLayout; export default AdminLayout;