This commit is contained in:
chenos 2021-07-20 23:19:54 +08:00
parent 97ca17bc10
commit e8399e167e
17 changed files with 1020 additions and 140 deletions

View File

@ -17,7 +17,7 @@ import {
useHistory, useHistory,
Redirect, Redirect,
} from 'react-router-dom'; } from 'react-router-dom';
import { SchemaRenderer } from '../../schemas'; import { CollectionContextProvider, PageTitleContextProvider, SchemaRenderer, usePageTitleContext } from '../../schemas';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import './style.less'; import './style.less';
@ -30,6 +30,7 @@ function LayoutWithMenu({ schema }) {
const location = useLocation(); const location = useLocation();
const sideMenuRef = useRef(); const sideMenuRef = useRef();
const [activeKey, setActiveKey] = useState(match.params.name); const [activeKey, setActiveKey] = useState(match.params.name);
const [,setPageTitle] = usePageTitleContext();
const onSelect = (info) => { const onSelect = (info) => {
console.log('LayoutWithMenu', info); console.log('LayoutWithMenu', info);
if (!info.schema) { if (!info.schema) {
@ -39,6 +40,9 @@ function LayoutWithMenu({ schema }) {
setActiveKey(null); setActiveKey(null);
} else { } else {
setActiveKey(info.schema.key); setActiveKey(info.schema.key);
if (info.schema.title) {
setPageTitle(info.schema.title);
}
} }
}; };
console.log({ match }); console.log({ match });
@ -98,7 +102,13 @@ export function AdminLayout({ route }: any) {
return <Spin />; return <Spin />;
} }
return <LayoutWithMenu schema={data} />; return (
<CollectionContextProvider>
<PageTitleContextProvider>
<LayoutWithMenu schema={data} />
</PageTitleContextProvider>
</CollectionContextProvider>
);
} }
export default AdminLayout; export default AdminLayout;

View File

@ -46,8 +46,10 @@ import { TimePicker } from '../../schemas/time-picker';
import { Upload } from '../../schemas/upload'; import { Upload } from '../../schemas/upload';
import { FormItem } from '../../schemas/form-item'; import { FormItem } from '../../schemas/form-item';
import { BlockItem } from '../../schemas/block-item'; import { BlockItem } from '../../schemas/block-item';
import { CardItem } from '../../schemas/card-item';
import { DragAndDrop } from '../../schemas/drag-and-drop'; import { DragAndDrop } from '../../schemas/drag-and-drop';
import { TreeSelect } from '../../schemas/tree-select'; import { TreeSelect } from '../../schemas/tree-select';
import { Page } from '../../schemas/page';
export const BlockContext = createContext({ dragRef: null }); export const BlockContext = createContext({ dragRef: null });
@ -59,11 +61,14 @@ interface DesignableContextProps {
} }
export const SchemaField = createSchemaField({ export const SchemaField = createSchemaField({
scope: {}, scope: {
Table,
},
components: { components: {
Card, Card,
Div, Div,
Space, Space,
Page,
ArrayCollapse, ArrayCollapse,
ArrayTable, ArrayTable,
@ -73,6 +78,7 @@ export const SchemaField = createSchemaField({
DragAndDrop, DragAndDrop,
BlockItem, BlockItem,
CardItem,
FormItem, FormItem,
Action, Action,
@ -173,6 +179,14 @@ function setKeys(schema: ISchema, parentKey = null) {
}); });
} }
export function useSchemaComponent(component: string) {
if (!component) {
return null;
}
const options = useContext(SchemaOptionsContext);
return get(options.components, component);
}
export function useDesignable(path?: any) { export function useDesignable(path?: any) {
const { schema = new Schema({}), refresh } = useContext(DesignableContext); const { schema = new Schema({}), refresh } = useContext(DesignableContext);
const schemaPath = path || useSchemaPath(); const schemaPath = path || useSchemaPath();

View File

@ -46,9 +46,17 @@ import {
DownOutlined, DownOutlined,
DatabaseOutlined, DatabaseOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { createSchema, useDesignable, useSchemaPath } from '../'; import {
createOrUpdateCollection,
createSchema,
request,
useCollectionContext,
useDesignable,
useSchemaPath,
} from '../';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { request } from '../../demos/api'; import { useRequest } from 'ahooks';
import { SchemaField } from '../../components/schema-renderer';
const generateGridBlock = (schema: ISchema) => { const generateGridBlock = (schema: ISchema) => {
const name = schema.name || uid(); const name = schema.name || uid();
@ -68,6 +76,10 @@ const generateGridBlock = (schema: ISchema) => {
}; };
}; };
const isGrid = (schema: Schema) => {
return schema['x-component'] === 'Grid';
};
const isGridBlock = (schema: Schema) => { const isGridBlock = (schema: Schema) => {
if (schema.parent['x-component'] !== 'Grid.Col') { if (schema.parent['x-component'] !== 'Grid.Col') {
return false; return false;
@ -83,14 +95,519 @@ const isGridBlock = (schema: Schema) => {
return true; return true;
}; };
function generateCardItemSchema(component) {
const defaults = {
Markdown: {
type: 'string',
default: '这是一段演示文字',
'x-designable-bar': 'Markdown.DesignableBar',
'x-decorator': 'CardItem',
'x-read-pretty': true,
'x-component': 'Markdown',
'x-component-props': {
savedInSchema: true,
},
},
Table: {
type: 'array',
'x-designable-bar': 'Table.DesignableBar',
'x-decorator': 'CardItem',
'x-component': 'Table',
default: [
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
{ key: uid(), field1: uid(), field2: uid() },
],
'x-component-props': {
rowKey: 'key',
},
properties: {
[`action_bar_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
action1: {
type: 'void',
name: 'action1',
title: '筛选',
'x-component': 'Action',
properties: {
popover1: {
type: 'void',
title: '弹窗标题',
'x-decorator': 'Form',
'x-component': 'Action.Popover',
'x-component-props': {},
properties: {
filter: {
name: 'filter',
type: 'object',
'x-component': 'Filter',
properties: {
column1: {
type: 'void',
title: '字段1',
'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
],
},
properties: {
field1: {
type: 'string',
'x-component': 'Input',
},
},
},
column2: {
type: 'void',
title: '字段2',
'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '大于', value: 'gt' },
{ label: '小于', value: 'lt' },
{
label: '非空',
value: 'notNull',
noValue: true,
},
],
},
properties: {
field1: {
type: 'number',
'x-component': 'InputNumber',
},
},
},
},
},
},
},
},
},
action2: {
type: 'void',
name: 'action1',
title: '新增',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
properties: {
drawer1: {
type: 'void',
title: '抽屉标题',
'x-decorator': 'Form',
'x-component': 'Action.Modal',
'x-component-props': {
useOkAction: '{{ Table.useTableCreateAction }}',
},
properties: {
field1: {
type: 'string',
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
field2: {
type: 'string',
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
action3: {
type: 'void',
name: 'action1',
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
},
[`a_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
'x-component-props': {
align: 'bottom',
},
properties: {
pagination: {
type: 'void',
'x-component': 'Table.Pagination',
'x-component-props': {},
},
},
},
[`column_${uid()}`]: {
type: 'void',
title: '排序',
'x-component': 'Table.Column',
properties: {
sort: {
type: 'void',
'x-component': 'Table.SortHandle',
},
},
},
[`column_${uid()}`]: {
type: 'void',
title: '序号',
'x-component': 'Table.Column',
properties: {
index: {
type: 'void',
'x-component': 'Table.Index',
},
},
},
[`column_${uid()}`]: {
type: 'void',
title: '字段1',
'x-component': 'Table.Column',
'x-component-props': {
// title: 'z1',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field1: {
type: 'string',
required: true,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
[`column_${uid()}`]: {
type: 'void',
title: '字段2',
'x-component': 'Table.Column',
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field2: {
type: 'string',
// title: '字段2',
required: true,
'x-read-pretty': true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
[`column_${uid()}`]: {
type: 'void',
title: '操作',
'x-component': 'Table.Column',
properties: {
[uid()]: {
type: 'void',
name: 'action1',
title: '查看',
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
properties: {
drawer1: {
type: 'void',
title: '查看',
'x-component': 'Action.Modal',
'x-component-props': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Tabs',
properties: {
tab1: {
type: 'void',
'x-component': 'Tabs.TabPane',
'x-component-props': {
tab: 'Tab1',
},
properties: {
aaa: {
type: 'string',
title: 'AAA',
'x-decorator': 'FormItem',
required: true,
'x-component': 'Input',
},
},
},
tab2: {
type: 'void',
'x-component': 'Tabs.TabPane',
'x-component-props': {
tab: 'Tab2',
},
properties: {
bbb: {
type: 'string',
title: 'BBB',
'x-decorator': 'FormItem',
required: true,
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
[uid()]: {
type: 'void',
name: 'action1',
title: '修改',
'x-component': 'Action',
'x-designable-bar': 'Table.Action.DesignableBar',
properties: {
drawer1: {
type: 'void',
title: '编辑表单',
'x-decorator': 'Form',
'x-decorator-props': {
useValues: '{{ Table.useTableRow }}',
},
'x-component': 'Action.Modal',
'x-component-props': {
useOkAction: '{{ Table.useTableUpdateAction }}',
},
properties: {
field1: {
type: 'string',
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
field2: {
type: 'string',
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
[uid()]: {
type: 'void',
title: '删除',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ Table.useTableDestroyAction }}',
},
},
},
},
},
},
Form: {
type: 'void',
name: uid(),
'x-decorator': 'CardItem',
'x-component': 'Form',
'x-designable-bar': 'Form.DesignableBar',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
};
return defaults[component];
}
function generateFormItemSchema(component) {
const defaults = {
Markdown: {
type: 'string',
title: uid(),
'x-designable-bar': 'Markdown.DesignableBar',
'x-decorator': 'FormItem',
'x-component': 'Markdown',
'x-component-props': {},
},
};
return defaults[component];
}
const dbSchema = {
type: 'object',
properties: {
title: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
name: {
type: 'string',
title: '数据表标识',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-read-pretty': true,
},
fields: {
type: 'array',
title: '数据表字段',
'x-decorator': 'FormItem',
'x-component': 'DatabaseField',
default: [],
},
},
};
export const AddNew = () => null; export const AddNew = () => null;
AddNew.CardItem = observer((props: any) => {
const { ghost, defaultAction } = props;
const { schema, insertBefore, insertAfter, appendChild } = useDesignable();
const path = useSchemaPath();
const { data: collections = [], loading, run } = useRequest('collections:findAll', {
formatResult: (result) => result?.data,
});
console.log({ collections });
return (
<Dropdown
overlayStyle={{
minWidth: 200,
}}
onVisibleChange={(visible) => {
if (visible) {
run();
}
console.log('onVisibleChange', visible);
}}
placement={'bottomCenter'}
overlay={
<Menu
onClick={async (info) => {
let data: ISchema;
if (['addNewTable', 'addNewForm'].includes(info.key)) {
const values = await FormDialog(`新建数据表`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField schema={dbSchema} />
</FormLayout>
);
}).open({
initialValues: {
name: `t_${uid()}`,
fields: [],
},
});
await createOrUpdateCollection(values);
data = generateCardItemSchema(
info.key === 'addNewTable' ? 'Table' : 'Form',
);
} else if (info.key !== 'Markdown') {
const keys = info.key.split('.');
const component = keys.shift();
const tableName = keys.join('.');
data = generateCardItemSchema(component);
console.log('info.keyPath', component, tableName);
} else {
data = generateCardItemSchema(info.key);
}
if (schema['key']) {
data['key'] = uid();
}
if (isGridBlock(schema)) {
path.pop();
path.pop();
data = generateGridBlock(data);
} else if (isGrid(schema)) {
data = generateGridBlock(data);
}
if (data) {
let s;
if (isGrid(schema)) {
s = appendChild(data, [...path]);
} else if (defaultAction === 'insertAfter') {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
}}
>
<Menu.SubMenu key={'Table'} title={'新建表格'}>
<Menu.ItemGroup key={'table-select'} title={'选择数据表'}>
{collections.map((item) => (
<Menu.Item key={`Table.${item.name}`}>{item.title}</Menu.Item>
))}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item key={'addNewTable'}></Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu key={'Form'} title={'新建表单'}>
<Menu.ItemGroup key={'form-select'} title={'选择数据表'}>
{collections.map((item) => (
<Menu.Item key={`Form.${item.name}`}>{item.title}</Menu.Item>
))}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item key={'addNewForm'}></Menu.Item>
</Menu.SubMenu>
<Menu.Item key={'Markdown'}> Markdown</Menu.Item>
<Menu.SubMenu key={'Ref'} title={'引用模板'}>
<Menu.ItemGroup key={'form-select'} title={'选择模板'}>
<Menu.Item key={'Ref.name1'}>1</Menu.Item>
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item key={'addNewRef'}></Menu.Item>
</Menu.SubMenu>
</Menu>
}
>
{ghost ? (
<PlusOutlined />
) : (
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
)}
</Dropdown>
);
});
AddNew.BlockItem = observer((props: any) => { AddNew.BlockItem = observer((props: any) => {
const { ghost, defaultAction } = props; const { ghost, defaultAction } = props;
const { schema, insertBefore, insertAfter } = useDesignable(); const { schema, insertBefore, insertAfter } = useDesignable();
const path = useSchemaPath(); const path = useSchemaPath();
return ( return (
<Dropdown <Dropdown
overlayStyle={{
minWidth: 200,
}}
placement={'bottomCenter'}
overlay={ overlay={
<Menu> <Menu>
<Menu.SubMenu title={'新建表格'}> <Menu.SubMenu title={'新建表格'}>
@ -113,7 +630,7 @@ AddNew.BlockItem = observer((props: any) => {
type: 'void', type: 'void',
title: uid(), title: uid(),
'x-designable-bar': 'BlockItem.DesignableBar', 'x-designable-bar': 'BlockItem.DesignableBar',
'x-decorator': 'BlockItem', 'x-decorator': 'CardItem',
'x-component': 'Markdown', 'x-component': 'Markdown',
}; };
if (schema['key']) { if (schema['key']) {
@ -141,7 +658,11 @@ AddNew.BlockItem = observer((props: any) => {
</Menu> </Menu>
} }
> >
{ghost ? <PlusOutlined /> : <Button icon={<PlusOutlined />}></Button>} {ghost ? (
<PlusOutlined />
) : (
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
)}
</Dropdown> </Dropdown>
); );
}); });
@ -152,6 +673,10 @@ AddNew.FormItem = observer((props: any) => {
const path = useSchemaPath(); const path = useSchemaPath();
return ( return (
<Dropdown <Dropdown
overlayStyle={{
minWidth: 200,
}}
placement={'bottomCenter'}
overlay={ overlay={
<Menu> <Menu>
<Menu.ItemGroup title={'选择已有字段'}> <Menu.ItemGroup title={'选择已有字段'}>
@ -159,25 +684,24 @@ AddNew.FormItem = observer((props: any) => {
style={{ style={{
minWidth: 150, minWidth: 150,
}} }}
onClick={() => { onClick={async () => {
let data: ISchema = { let data: ISchema = generateFormItemSchema('Markdown');
type: 'void', if (schema['key']) {
title: uid(), data['key'] = uid();
'x-designable-bar': 'FormItem.DesignableBar', }
'x-decorator': 'FormItem',
'x-component': 'Markdown',
};
if (isGridBlock(schema)) { if (isGridBlock(schema)) {
path.pop(); path.pop();
path.pop(); path.pop();
data = generateGridBlock(data); data = generateGridBlock(data);
} }
if (data) { if (data) {
let s;
if (defaultAction === 'insertAfter') { if (defaultAction === 'insertAfter') {
insertAfter(data, [...path]); s = insertAfter(data, [...path]);
} else { } else {
insertBefore(data, [...path]); s = insertBefore(data, [...path]);
} }
await createSchema(s);
} }
}} }}
> >
@ -219,7 +743,11 @@ AddNew.FormItem = observer((props: any) => {
</Menu> </Menu>
} }
> >
{ghost ? <PlusOutlined /> : <Button icon={<PlusOutlined />}></Button>} {ghost ? (
<PlusOutlined />
) : (
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
)}
</Dropdown> </Dropdown>
); );
}); });

View File

@ -113,17 +113,6 @@ export const BlockItem: any = observer((props) => {
return React.createElement(uid ? DraggableBlock : Block, props); return React.createElement(uid ? DraggableBlock : Block, props);
}); });
// export const BlockItem: any = connect((props) => {
// const { children, ...others } = props;
// const { DesignableBar } = useDesignableBar();
// return (
// <div {...others} style={{marginBottom: 24}} className={'designable-form-item'}>
// {children}
// <DesignableBar />
// </div>
// );
// });
BlockItem.DesignableBar = () => { BlockItem.DesignableBar = () => {
const field = useField(); const field = useField();
const { schema, refresh } = useDesignable(); const { schema, refresh } = useDesignable();

View File

@ -0,0 +1,32 @@
import React, { useContext, useState } from 'react';
import {
connect,
mapProps,
mapReadPretty,
observer,
SchemaOptionsContext,
useField,
useFieldSchema,
} from '@formily/react';
import { FormItem as FormilyFormItem } from '@formily/antd';
import { Card, Dropdown, Menu, Space } from 'antd';
import classNames from 'classnames';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
// import './style.less';
import get from 'lodash/get';
import { GridBlockContext } from '../grid';
import { uid } from '@formily/shared';
import { useDesignable } from '../';
import { AddNew } from '../add-new';
import { BlockItem } from '../block-item';
import { DraggableBlockContext } from '../../components/drag-and-drop';
export const CardItem: any = connect((props) => {
return (
<BlockItem>
<Card bordered={false} {...props}>
{props.children}
</Card>
</BlockItem>
);
});

View File

@ -39,7 +39,7 @@ import Modal from 'antd/lib/modal/Modal';
import { clone, cloneDeep, get } from 'lodash'; import { clone, cloneDeep, get } from 'lodash';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { createOrUpdateCollection, deleteCollection } from '..'; import { createOrUpdateCollection, deleteCollection, useCollectionContext } from '..';
export const DatabaseCollection = observer((props) => { export const DatabaseCollection = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>(); const field = useField<Formily.Core.Models.ArrayField>();
@ -49,6 +49,7 @@ export const DatabaseCollection = observer((props) => {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const form = useForm(); const form = useForm();
const [newValue, setNewValue] = useState(''); const [newValue, setNewValue] = useState('');
const { refresh } = useCollectionContext();
const { run, loading } = useRequest('collections:findAll', { const { run, loading } = useRequest('collections:findAll', {
formatResult: (result) => result?.data, formatResult: (result) => result?.data,
@ -125,6 +126,7 @@ export const DatabaseCollection = observer((props) => {
setOpen(false); setOpen(false);
setNewValue(''); setNewValue('');
await createOrUpdateCollection(data); await createOrUpdateCollection(data);
await refresh();
}} }}
/> />
</div> </div>
@ -166,6 +168,7 @@ export const DatabaseCollection = observer((props) => {
} }
if (item.name) { if (item.name) {
await deleteCollection(item.name); await deleteCollection(item.name);
await refresh();
} }
}} }}
/> />
@ -188,6 +191,7 @@ export const DatabaseCollection = observer((props) => {
await form.validate(`${field.address.entire}.${activeIndex}.*`); await form.validate(`${field.address.entire}.${activeIndex}.*`);
delete field.value[activeIndex]['unsaved']; delete field.value[activeIndex]['unsaved'];
await createOrUpdateCollection(field.value[activeIndex]); await createOrUpdateCollection(field.value[activeIndex]);
await refresh();
message.success('保存成功'); message.success('保存成功');
} catch (error) {} } catch (error) {}
}} }}

View File

@ -8,14 +8,18 @@ import {
SchemaExpressionScopeContext, SchemaExpressionScopeContext,
FormProvider, FormProvider,
ISchema, ISchema,
useField,
} from '@formily/react'; } from '@formily/react';
import { useSchemaPath, SchemaField, useDesignable } from '../'; import { useSchemaPath, SchemaField, useDesignable, removeSchema } from '../';
import get from 'lodash/get'; import get from 'lodash/get';
import { Dropdown, Menu } from 'antd'; import { Dropdown, Menu, Space } from 'antd';
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import cls from 'classnames'; import cls from 'classnames';
import { FormLayout } from '@formily/antd'; import { FormLayout } from '@formily/antd';
import './style.less'; import './style.less';
import AddNew from '../add-new';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import { isGridRowOrCol } from '../grid';
export const Form: any = observer((props: any) => { export const Form: any = observer((props: any) => {
const { useValues = () => ({}), ...others } = props; const { useValues = () => ({}), ...others } = props;
@ -61,21 +65,59 @@ export const Form: any = observer((props: any) => {
Form.__Decorator = ({ children }) => children; Form.__Decorator = ({ children }) => children;
Form.DesignableBar = (props) => { Form.DesignableBar = observer((props) => {
const { active } = props; const field = useField();
const { schema, refresh, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
return ( return (
<div className={cls('designable-bar', { active })}> <div className={cls('designable-bar', { active: visible })}>
<div className={'designable-bar-actions'}> <span
<Dropdown onClick={(e) => {
overlay={ e.stopPropagation();
<Menu> }}
<Menu.Item></Menu.Item> className={cls('designable-bar-actions', { active: visible })}
</Menu> >
} <Space size={'small'}>
> <AddNew.CardItem defaultAction={'insertAfter'} ghost />
<MenuOutlined /> {dragRef && <DragOutlined ref={dragRef} />}
</Dropdown> <Dropdown
</div> trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
key={'update'}
onClick={() => {
field.readPretty = false;
}}
>
Markdown
</Menu.Item>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={async () => {
const removed = deepRemove();
// console.log({ removed })
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div> </div>
); );
}; });

View File

@ -14,6 +14,9 @@ const schema: ISchema = {
type: 'void', type: 'void',
name: uid(), name: uid(),
'x-component': 'Grid', 'x-component': 'Grid',
"x-component-props": {
addNewComponent: 'AddNew.CardItem',
},
properties: { properties: {
[`row_${uid()}`]: { [`row_${uid()}`]: {
type: 'void', type: 'void',

View File

@ -30,6 +30,7 @@ import {
useDragDropUID, useDragDropUID,
DragDropManagerProvider, DragDropManagerProvider,
} from '../../components/drag-and-drop'; } from '../../components/drag-and-drop';
import { useSchemaComponent } from '../../components/schema-renderer';
const ColumnSizeContext = createContext(null); const ColumnSizeContext = createContext(null);
@ -37,7 +38,7 @@ export const GridBlockContext = createContext({
dragRef: null, dragRef: null,
}); });
function isGridRowOrCol(schema: ISchema) { export function isGridRowOrCol(schema: ISchema) {
return ['Grid.Row', 'Grid.Col'].includes(schema['x-component']); return ['Grid.Row', 'Grid.Col'].includes(schema['x-component']);
} }
@ -81,11 +82,14 @@ const ColDivider = (props: any) => {
); );
}; };
export const Grid: any = observer((props) => { export const Grid: any = observer((props: any) => {
const { addNewComponent } = props;
const AddNewComponent = useSchemaComponent(addNewComponent);
const ref = useRef(); const ref = useRef();
const schema = useFieldSchema(); // const schema = useFieldSchema();
const gridPath = useSchemaPath(); const gridPath = useSchemaPath();
const { const {
schema,
schema: designableSchema, schema: designableSchema,
insertAfter, insertAfter,
prepend, prepend,
@ -162,6 +166,7 @@ export const Grid: any = observer((props) => {
</> </>
); );
})} })}
{AddNewComponent && <AddNewComponent />}
</div> </div>
</DragDropManagerProvider> </DragDropManagerProvider>
); );
@ -169,9 +174,10 @@ export const Grid: any = observer((props) => {
Grid.Row = observer((props) => { Grid.Row = observer((props) => {
const field = useField(); const field = useField();
const schema = useFieldSchema(); // const schema = useFieldSchema();
const rowPath = useSchemaPath(); const rowPath = useSchemaPath();
const { const {
schema,
schema: designableSchema, schema: designableSchema,
refresh, refresh,
insertAfter, insertAfter,

View File

@ -1,18 +1,50 @@
import { ISchema } from '@formily/react'; import React from 'react';
import { ISchema, observer, Schema, useField } from '@formily/react';
import constate from 'constate';
import { useState } from 'react';
import { createContext } from 'react'; import { createContext } from 'react';
import { extend } from 'umi-request';
import { isVoidField } from '@formily/core';
import { useRequest } from 'ahooks';
export * from '../components/schema-renderer'; export * from '../components/schema-renderer';
export function mapReadPretty(component, readPrettyProps) {
return function (target) {
return observer(
(props) => {
const field = useField()
if (!isVoidField(field) && field?.pattern === 'readPretty') {
return React.createElement(component, {
...readPrettyProps,
...props,
})
}
return React.createElement(target, props)
},
{
forwardRef: true,
}
)
};
}
export const VisibleContext = createContext(null); export const VisibleContext = createContext(null);
export const DesignableBarContext = createContext(null); export const DesignableBarContext = createContext(null);
const [PageTitleContextProvider, usePageTitleContext] = constate(() => {
return useState(null);
});
export { PageTitleContextProvider, usePageTitleContext };
export function useDefaultAction() { export function useDefaultAction() {
return { return {
async run () {} async run() {},
} };
} }
import { extend } from 'umi-request';
export const request = extend({ export const request = extend({
prefix: 'http://localhost:23003/api/', prefix: 'http://localhost:23003/api/',
timeout: 1000, timeout: 1000,
@ -31,7 +63,7 @@ export async function deleteCollection(name) {
params: { params: {
filter: { filter: {
name, name,
} },
}, },
}); });
} }
@ -44,7 +76,7 @@ export async function createSchema(schema: ISchema) {
method: 'post', method: 'post',
data: schema.toJSON(), data: schema.toJSON(),
}); });
}; }
export async function updateSchema(schema: ISchema) { export async function updateSchema(schema: ISchema) {
if (!schema['key']) { if (!schema['key']) {
@ -52,9 +84,9 @@ export async function updateSchema(schema: ISchema) {
} }
return await request(`ui_schemas:update/${schema.key}`, { return await request(`ui_schemas:update/${schema.key}`, {
method: 'post', method: 'post',
data: schema.toJSON(), data: Schema.isSchemaInstance(schema) ? schema.toJSON() : schema,
}); });
}; }
export async function removeSchema(schema: ISchema) { export async function removeSchema(schema: ISchema) {
if (!schema['key']) { if (!schema['key']) {
@ -65,7 +97,15 @@ export async function removeSchema(schema: ISchema) {
params: { params: {
filter: { filter: {
key: schema['key'], key: schema['key'],
} },
}, },
}); });
} }
const [CollectionContextProvider, useCollectionContext] = constate(() => {
return useRequest('collections:findAll', {
formatResult: (result) => result?.data,
});
});
export { CollectionContextProvider, useCollectionContext };

View File

@ -33,7 +33,6 @@ const schema = {
type: 'object', type: 'object',
properties: { properties: {
input: { input: {
interface: 'string',
type: 'string', type: 'string',
title: `编辑模式`, title: `编辑模式`,
name: 'name1', name: 'name1',
@ -76,3 +75,58 @@ export default () => {
); );
}; };
``` ```
### Markdown.DesignableBar
```tsx
import React from 'react';
import { SchemaRenderer } from '../';
const schema1 = {
type: 'object',
properties: {
markdown1: {
type: 'string',
title: `简易Markdown`,
default: '这是一段示例文案',
'x-read-pretty': true,
'x-decorator': 'CardItem',
'x-component': 'Markdown',
'x-designable-bar': 'Markdown.DesignableBar',
'x-component-props': {
savedInSchema: true,
// ellipsis: true,
},
},
},
};
const schema2 = {
type: 'object',
properties: {
markdown1: {
type: 'string',
title: `简易Markdown`,
default: '这是一段示例文案',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Markdown',
'x-designable-bar': 'Markdown.DesignableBar',
'x-component-props': {
},
},
},
};
export default () => {
return (
<div>
<h3>区块项</h3>
<SchemaRenderer schema={schema1} />
<h3>表单项</h3>
<SchemaRenderer schema={schema2} />
</div>
);
};
```

View File

@ -1,15 +1,53 @@
import React from 'react'; import React from 'react';
import { connect, mapProps, mapReadPretty, useFieldSchema } from '@formily/react'; import {
import { Input as AntdInput } from 'antd'; connect,
mapProps,
mapReadPretty,
observer,
useField,
useFieldSchema,
} from '@formily/react';
import { Button, Dropdown, Input as AntdInput, Menu, Space } from 'antd';
import { InputProps, TextAreaProps } from 'antd/lib/input'; import { InputProps, TextAreaProps } from 'antd/lib/input';
import { Display } from '../display'; import { Display } from '../display';
import { LoadingOutlined } from '@ant-design/icons'; import { LoadingOutlined, MenuOutlined, DragOutlined } from '@ant-design/icons';
import micromark from 'micromark'; import micromark from 'micromark';
import { useDesignable } from '../../components/schema-renderer';
import { useContext, useState } from 'react';
import AddNew from '../add-new';
import cls from 'classnames';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import { uid } from '@formily/shared';
import { removeSchema, updateSchema } from '..';
import { isGridRowOrCol } from '../grid';
export const Markdown = connect( export const Markdown: any = connect(
(props) => { (props) => {
const schema = useFieldSchema(); const { schema } = useDesignable();
return <AntdInput.TextArea defaultValue={schema.name} {...props}/> const field = useField<any>();
const { savedInSchema, ...others } = props;
console.log('Markdown', props);
return savedInSchema ? (
<div className={'mb-markdown'} style={{ position: 'relative' }}>
<AntdInput.TextArea autoSize={{ minRows: 3 }} {...others} />
<Button
style={{ position: 'absolute', bottom: 5, right: 5 }}
type={'primary'}
onClick={async () => {
field.readPretty = true;
schema['default'] = field.value;
await updateSchema({
key: schema['key'],
default: field.value,
});
}}
>
</Button>
</div>
) : (
<AntdInput.TextArea autoSize={{ minRows: 1 }} {...others} />
);
}, },
mapProps((props: any, field) => { mapProps((props: any, field) => {
return { return {
@ -26,6 +64,7 @@ export const Markdown = connect(
}; };
}), }),
mapReadPretty((props) => { mapReadPretty((props) => {
console.log('Markdown', props);
let text = props.value; let text = props.value;
let value = ( let value = (
<div dangerouslySetInnerHTML={{ __html: micromark(text || '') }} /> <div dangerouslySetInnerHTML={{ __html: micromark(text || '') }} />
@ -34,4 +73,61 @@ export const Markdown = connect(
}), }),
); );
Markdown.DesignableBar = observer((props) => {
const field = useField();
const { schema, refresh, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={'small'}>
<AddNew.CardItem defaultAction={'insertAfter'} ghost />
{dragRef && <DragOutlined ref={dragRef} />}
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
key={'update'}
onClick={() => {
field.readPretty = false;
}}
>
Markdown
</Menu.Item>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={async () => {
const removed = deepRemove();
// console.log({ removed })
const last = removed.pop();
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
});
export default Markdown; export default Markdown;

View File

@ -1,71 +1,59 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
export const defaultSchemas = { export function generateDefaultSchema(component) {
MixMenu: { const defaultSchemas: { [key: string]: ISchema } = {
type: 'void', MixMenu: {
'x-component': 'Menu', type: 'void',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu',
'x-component-props': { 'x-designable-bar': 'Menu.DesignableBar',
sideMenuRef: '{{ sideMenuRef }}', 'x-component-props': {
mode: 'mix', sideMenuRef: '{{ sideMenuRef }}',
theme: 'dark', mode: 'mix',
// onSelect: '{{ onSelect }}', theme: 'dark',
// onSelect: '{{ onSelect }}',
},
}, },
}, 'Menu.Item': {
'Menu.Item': { type: 'void',
type: 'void', 'x-designable-bar': 'Menu.DesignableBar',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu.Item',
'x-component': 'Menu.Item', },
}, 'Menu.Link': {
'Menu.Link': { type: 'void',
type: 'void', 'x-designable-bar': 'Menu.DesignableBar',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu.Link',
'x-component': 'Menu.Link', properties: {
properties: { [uid()]: {
[uid()]: { type: 'void',
type: 'void', 'x-component': 'Page',
name: uid(), properties: {
'x-component': 'Grid', [uid()]: {
properties: { type: 'void',
[`row_${uid()}`]: { 'x-component': 'Grid',
type: 'void', 'x-component-props': {
'x-component': 'Grid.Row', addNewComponent: 'AddNew.CardItem',
'x-component-props': {
locked: true,
},
properties: {
[`col_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
properties: {
[uid()]: {
type: 'void',
'x-component': 'AddNew.BlockItem',
},
},
}, },
}, },
}, },
}, },
} },
} },
}, 'Menu.URL': {
'Menu.URL': { type: 'void',
type: 'void', 'x-designable-bar': 'Menu.DesignableBar',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu.URL',
'x-component': 'Menu.URL', },
}, 'Menu.SubMenu': {
'Menu.SubMenu': { type: 'void',
type: 'void', 'x-designable-bar': 'Menu.DesignableBar',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu.SubMenu',
'x-component': 'Menu.SubMenu', },
}, 'Menu.Action': {
'Menu.Action': { type: 'void',
type: 'void', 'x-designable-bar': 'Menu.DesignableBar',
'x-designable-bar': 'Menu.DesignableBar', 'x-component': 'Menu.Action',
'x-component': 'Menu.Action', },
}, };
}; return defaultSchemas[component];
}
export default defaultSchemas;

View File

@ -62,7 +62,7 @@ import './style.less';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { findPropertyByPath, getSchemaPath, useSchemaPath } from '../../'; import { findPropertyByPath, getSchemaPath, useSchemaPath } from '../../';
import { request } from '../'; import { request } from '../';
import defaultSchemas from './defaultSchemas'; import { generateDefaultSchema } from './defaultSchemas';
import _, { cloneDeep, get, isNull } from 'lodash'; import _, { cloneDeep, get, isNull } from 'lodash';
import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd'; import { FormDialog, FormItem, FormLayout, Input } from '@formily/antd';
import deepmerge from 'deepmerge'; import deepmerge from 'deepmerge';
@ -369,7 +369,7 @@ Menu.AddNew = observer((props: any) => {
</FormLayout> </FormLayout>
); );
}).open(); }).open();
const defaults = cloneDeep(defaultSchemas[info.key]); const defaults = generateDefaultSchema(info.key);
const data = appendChild(deepmerge(defaults, values)); const data = appendChild(deepmerge(defaults, values));
await createSchema(data); await createSchema(data);
}} }}
@ -568,7 +568,7 @@ Menu.DesignableBar = (props) => {
insertBefore, insertBefore,
appendChild, appendChild,
}; };
const defaults = cloneDeep(defaultSchemas[type]); const defaults = generateDefaultSchema(type);
const data = methods[method](deepmerge(defaults, values)); const data = methods[method](deepmerge(defaults, values));
await createSchema(data); await createSchema(data);
}} }}
@ -711,11 +711,12 @@ Menu.DesignableBar = (props) => {
insertBefore, insertBefore,
appendChild, appendChild,
}; };
const data = schema.toJSON();
remove();
const source = methods[values.method]( const source = methods[values.method](
schema.toJSON(), data,
values.path, values.path,
); );
remove();
await updateSchema(source); await updateSchema(source);
}} }}
> >

View File

@ -0,0 +1,18 @@
import React from 'react';
import { PageHeader as AntdPageHeader } from 'antd';
import { observer } from '@formily/react';
import { usePageTitleContext } from '..';
export const Page = observer((props) => {
const { children, ...others } = props;
const [pageTitle] = usePageTitleContext();
return (
<>
<AntdPageHeader ghost={false} title={pageTitle} {...others} />
<div style={{margin: 24}}>
{children}
</div>
</>
);
});

View File

@ -25,7 +25,7 @@ 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, DragOutlined } from '@ant-design/icons';
import { import {
SortableHandle, SortableHandle,
SortableContainer, SortableContainer,
@ -34,6 +34,8 @@ import {
import cls from 'classnames'; import cls from 'classnames';
import { getSchemaPath, useDesignable, useSchemaPath, VisibleContext } from '../'; import { getSchemaPath, useDesignable, useSchemaPath, VisibleContext } from '../';
import './style.less'; import './style.less';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import AddNew from '../add-new';
interface TableRowProps { interface TableRowProps {
index: number; index: number;
@ -632,4 +634,56 @@ Table.Action.DesignableBar = () => {
</span> </span>
</div> </div>
); );
}; };
Table.DesignableBar = observer((props) => {
const field = useField();
const { schema, refresh, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={'small'}>
<AddNew.CardItem defaultAction={'insertAfter'} ghost />
{dragRef && <DragOutlined ref={dragRef} />}
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
key={'update'}
onClick={() => {
field.readPretty = false;
}}
>
Markdown
</Menu.Item>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={() => {
deepRemove();
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
});

View File

@ -1,5 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import { Model } from '@nocobase/database'; import { Model } from '@nocobase/database';
import deepmerge from 'deepmerge';
export class UISchema extends Model { export class UISchema extends Model {
static async create(value?: any, options?: any): Promise<any> { static async create(value?: any, options?: any): Promise<any> {
@ -33,7 +34,7 @@ export class UISchema extends Model {
]; ];
const attrs = _.pick(data, keys); const attrs = _.pick(data, keys);
const options = _.omit(data, keys); const options = _.omit(data, keys);
return { ...attrs, options: {...options, ...opts} }; return { ...attrs, options: deepmerge(opts, options) };
} }
static properties2children(properties = []) { static properties2children(properties = []) {