This commit is contained in:
chenos 2021-06-27 15:41:40 +08:00
parent 7d6dd556f1
commit 75cd158a27
137 changed files with 8760 additions and 6287 deletions

View File

@ -10,7 +10,7 @@ title: '@nocobase/plugin-notifications'
暂时只实现了核心三步骤:
- 通知模板:包括主题、内容、接收人配置等等
- 通知模板:包括主题、内容、接收人配置、发送服务等等
- 通知服务:可以是短信、邮件等等
- 通知日志:记录通知状态

View File

@ -30,7 +30,7 @@
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.8.2",
"@umijs/plugin-antd": "^0.9.1",
"antd": "^4.15.4",
"antd": "^4.16.3",
"dotenv": "^8.2.0",
"dumi": "^1.1.16",
"eslint": "^7.4.0",

View File

@ -18,9 +18,9 @@
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e",
"dependencies": {
"@formily/antd": "^2.0.0-beta.54",
"@formily/core": "^2.0.0-beta.54",
"@formily/react": "^2.0.0-beta.54",
"@formily/antd": "^2.0.0-beta.72",
"@formily/core": "^2.0.0-beta.72",
"@formily/react": "^2.0.0-beta.72",
"ahooks": "^2.10.2",
"axios": "^0.21.1",
"beautiful-react-hooks": "^0.35.0",

View File

@ -0,0 +1,132 @@
import React, { createContext, useState } from 'react';
import {
FormItem,
FormLayout,
FormButtonGroup,
Submit,
ArrayTable,
ArrayCollapse,
} from '@formily/antd';
import { createForm } from '@formily/core';
import {
Schema,
ISchema,
useForm,
FormProvider,
createSchemaField,
observer,
useFieldSchema,
} from '@formily/react';
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';
const Div = (props) => <div {...props} />;
const scope = {
useLogin,
useRegister,
useSubmit,
};
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 const SchemaField = createSchemaField({
scope,
components,
});
export const DesignableSchemaContext = createContext<Schema>(new Schema({}));
export const RefreshDesignableSchemaContext = createContext(null);
export function DesignableProvider(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<RefreshDesignableSchemaContext.Provider
value={() => {
refresh(Math.random());
}}
>
<DesignableSchemaContext.Provider value={schema}>
{props.children(schema)}
</DesignableSchemaContext.Provider>
{/* <pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre> */}
</RefreshDesignableSchemaContext.Provider>
);
}
export function DesignableSchemaField(props: { schema?: ISchema }) {
return (
<DesignableProvider schema={new Schema(props.schema)}>
{(s) => <SchemaField schema={s} />}
</DesignableProvider>
);
}

View File

@ -0,0 +1,309 @@
---
title: Action - 操作
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Action - 操作
操作有三类:
- 常规操作
- 弹出层操作,弹出层有 Drawer、Modal、Popover
- 指定容器内操作 Container
- 下拉菜单,用于收纳多种操作
- 跳转操作
## Action - 常规操作
```tsx
/**
* title: 按钮操作
* desc: 可以通过配置 `useAction` 来处理操作逻辑
*/
import React from 'react';
import { SchemaBlock, registerScope } from '../';
function useCustomAction() {
return {
run() {
alert('这是自定义的操作逻辑');
}
}
}
registerScope({
useCustomAction,
});
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCustomAction }}'
},
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## Action.Link - 内链跳转
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action.Link',
'x-component-props': {
to: 'abc',
},
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## Action.URL - 外链跳转
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action.URL',
'x-component-props': {
url: 'https://www.nocobase.com/',
},
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## Action.Popover - 打开气泡
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Popover',
'x-component-props': {},
properties: {
input: {
type: 'string',
'x-component': 'Input',
}
},
},
},
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## Action.Drawer - 打开抽屉
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': '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 <SchemaBlock schema={schema} />
}
```
## Action.Modal - 打开对话框
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '弹窗标题',
'x-component': 'Action.Modal',
'x-component-props': {},
properties: {
},
},
},
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## Action.Container - 指定容器内打开
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
properties: {
drawer1: {
type: 'void',
title: '页面标题',
'x-component': 'Action.Container',
'x-component-props': {
container: '#container'
},
properties: {
input: {
type: 'string',
title: '字段',
'x-decorator': 'FormItem',
'x-component': 'Input',
}
},
},
},
};
export default () => {
return (
<div>
<SchemaBlock schema={schema} />
<div style={{padding: '8px 0'}}>目标容器:</div>
<div id={'container'} style={{ border: '1px dashed #ebedf1', background: '#fafafa', padding: 24 }}/>
</div>
)
}
```
## Action.DesignableBar - 按钮配置操作栏
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'action1',
title: '按钮',
'x-component': 'Action',
'x-designable-bar': 'Action.DesignableBar',
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 <SchemaBlock schema={schema} />
}
```

View File

@ -0,0 +1,272 @@
import React, { useContext, useState } from 'react';
import {
Input,
FormItem,
FormButtonGroup,
Submit,
Password,
} from '@formily/antd';
import { createForm } from '@formily/core';
import {
useForm,
FormProvider,
createSchemaField,
observer,
useFieldSchema,
RecursionField,
SchemaOptionsContext,
Schema,
useField,
} from '@formily/react';
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 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 classNames from 'classnames';
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() {},
};
}
export function useSubmit() {
const form = useForm();
const run = async () => {
await form.submit();
console.log(form.values);
};
return {
run,
};
}
export function useLogin() {
const form = useForm();
const history = useHistory();
const run = async () => {
await form.submit();
history.push('/admin');
console.log(form.values);
};
return {
run,
};
}
export function useRegister() {
const form = useForm();
const run = async () => {
await form.submit();
console.log(form.values);
};
return {
run,
};
}
export interface ActionProps {
useAction?: any;
[key: string]: any;
}
export type ActionType = React.FC<ActionProps> & {
Link?: React.FC<LinkProps>;
URL?: React.FC<any>;
Page?: React.FC<any>;
Drawer?: React.FC<any>;
Modal?: React.FC<any>;
Dropdown?: React.FC<any>;
DesignableBar?: React.FC<any>;
};
function Blank({ children }) {
return children;
}
function useDesignableBar() {
const schema = useFieldSchema();
const options = useContext(SchemaOptionsContext);
const DesignableBar = get(options.components, schema['x-designable-bar']);
return {
DesignableBar: DesignableBar || Blank,
};
}
export const Action: ActionType = observer((props) => {
const { useAction = useDefaultAction, ...others } = props;
const field = useField();
const { schema } = useSchemaQuery();
const { run } = useAction();
const { DesignableBar } = useDesignableBar();
let childSchema = null;
if (schema.properties) {
const key = Object.keys(schema.properties).shift();
const current = schema.properties[key];
childSchema = current;
}
console.log({ schema });
if (childSchema && childSchema['x-component'] === 'Action.Popover') {
return (
<Popover
trigger={['click']}
title={childSchema.title}
{...childSchema['x-component-props']}
content={
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
</div>
}
>
<Button {...others}>{field.title}</Button>
</Popover>
);
}
return (
<DesignableBar>
<Button
{...others}
onClick={async () => {
if (!childSchema) {
return await run();
}
if (childSchema['x-component'] === 'Action.Drawer') {
Drawer.open({
title: childSchema.title,
...(childSchema['x-component-props'] || {}),
content: () => {
return (
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
</div>
);
},
});
}
if (childSchema['x-component'] === 'Action.Container') {
const el = document.createElement('div');
const target = document.querySelector(
childSchema['x-component-props']?.['container'],
);
target.childNodes.forEach((child) => child.remove());
target.appendChild(el);
ReactDOM.render(
<div>
<SchemaBlock schema={childSchema} onlyRenderProperties />
</div>,
el,
);
}
}}
>
{field.title}
</Button>
</DesignableBar>
);
});
Action.Link = observer((props) => {
const schema = useFieldSchema();
return <Link {...props}>{schema.title}</Link>;
});
Action.URL = observer((props) => {
const { url, ...others } = props;
const schema = useFieldSchema();
return (
<a target={'_blank'} href={url} {...others}>
{schema.title}
</a>
);
});
Action.DesignableBar = (props) => {
const field = useField();
const { schema, refresh } = useSchemaQuery();
const [visible, setVisible] = useState(false);
return (
<div className={'designable'}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={classNames('designable-bar', { active: visible })}
>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
onClick={(e) => {
console.log({ field, schema });
schema.title = '按钮文案被修改了';
field.setTitle('按钮文案被修改了');
schema.properties.drawer1.title = '抽屉标题文案被修改了';
refresh();
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</span>
{props.children}
</div>
);
};

View File

@ -0,0 +1,21 @@
.designable {
display: inline-block;
position: relative;
&:hover {
.designable-bar {
display: block;
}
}
.designable-bar {
display: none;
background: #fff;
z-index: 2;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
&.active {
display: block;
}
}
}

View File

@ -0,0 +1,73 @@
import { uid } from '@formily/shared';
export default () => ({
type: 'void',
name: `form_${uid()}`,
'x-decorator': 'Card',
'x-component': 'Form',
properties: {
grid: {
type: 'void',
'x-component': 'Grid',
properties: {
[`gr_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[`gc_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[`gb_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[`gbn_${uid()}`]: {
type: 'void',
'x-component': 'AddNew.FormItem',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
})

View File

@ -0,0 +1,46 @@
---
title: AddNew - 新增区块
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# AddNew - 新增区块
## 新增普通区块
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'addnew',
'x-component': 'AddNew',
};
export default () => {
return <SchemaBlock schema={schema} />
}
```
## 新增表单区块
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'void',
name: 'addnew',
'x-component': 'AddNew.FormItem',
};
export default () => {
return <SchemaBlock schema={schema} />
}
```

View File

@ -0,0 +1,429 @@
import React, {
Children,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import { FormDialog, FormLayout } from '@formily/antd';
import {
connect,
observer,
mapProps,
mapReadPretty,
useField,
useFieldSchema,
RecursionField,
Schema,
} from '@formily/react';
import {
Menu,
MenuProps,
MenuItemProps,
SubMenuProps,
DividerProps,
Dropdown,
Modal,
Button,
Spin,
} from 'antd';
import {
MenuOutlined,
GroupOutlined,
PlusOutlined,
LinkOutlined,
AppstoreAddOutlined,
EditOutlined,
DeleteOutlined,
ArrowRightOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
LoadingOutlined,
DownOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
import { uid } from '@formily/shared';
import {
DesignableSchemaContext,
RefreshDesignableSchemaContext,
SchemaField,
} from '../SchemaField';
import { SchemaBlock } from '../';
import table from './table';
import markdown from './markdown';
import form from './form';
import { useRequest } from 'ahooks';
const row = (schema) => {
const component = schema['x-component'];
return {
type: 'void',
name: `gr_${uid()}`,
'x-component': 'Grid.Row',
properties: {
[`gc_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[`gb_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
DesignableBar: `${component}.DesignableBar`,
},
'x-designable-bar': `${component}.DesignableBar`,
properties: {
[`gbn_${uid()}`]: schema,
},
},
},
},
},
};
};
export function removeProperty(property: Schema) {
property.parent.removeProperty(property.name);
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export function useSchemaQuery(segments?: any[]) {
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const field = useField();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
// names.shift();
while (s && names.length) {
const name = names.shift();
s = s.properties[name];
}
return s;
};
const schema = getSchemaByPath(segments || field.address.segments);
console.log({ context, schema });
return {
refresh,
schema,
appendChild(data) {
schema.addProperty(data.name, data);
refresh();
},
insertAfter(data) {
addPropertyAfter(schema, data);
refresh();
},
insertBefore(data) {
addPropertyBefore(schema, data);
refresh();
},
push(data) {
addPropertyBefore(schema, data);
},
remove() {
removeProperty(schema);
refresh();
},
};
}
export const AddNew: any = observer((props) => {
const field = useField();
const segments = [...field.address.segments];
segments.pop();
segments.pop();
segments.pop();
// segments.pop();
const { insertBefore, refresh, schema } = useSchemaQuery(segments);
console.log('field.address.segments', segments, schema);
const {
data = [],
loading,
mutate,
} = useRequest(() => {
return Promise.resolve([
{ title: '数据表1' },
{ title: '数据表2' },
{ title: '数据表3' },
]);
});
const [visible, setVisible] = useState(false);
const dbschema = {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
input: {
type: 'string',
title: '数据表名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
array: {
type: 'array',
title: '数据表字段',
'x-component': 'ArrayCollapse',
'x-component-props': {
accordion: true,
},
// maxItems: 3,
'x-decorator': 'FormItem',
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '字段',
},
properties: {
index: {
type: 'void',
'x-component': 'ArrayCollapse.Index',
},
input: {
type: 'string',
'x-decorator': 'FormItem',
title: 'Input',
required: true,
'x-component': 'Input',
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'ArrayCollapse.MoveDown',
},
},
},
properties: {
addition: {
type: 'void',
title: '添加字段',
'x-component': 'ArrayCollapse.Addition',
},
},
},
},
},
},
};
if (loading) {
return <Spin />;
}
console.log({ data });
return (
<Dropdown
overlay={
<Menu>
<Menu.SubMenu
key={`table-tables`}
style={{ minWidth: 150 }}
title={'新建表单'}
>
<Menu.ItemGroup key={`table-tables-itemgroup`} title={'所属数据表'}>
{data.map((item, index) => {
return (
<Menu.Item
key={`table-${index}`}
style={{ minWidth: 150 }}
onClick={() => {
const rowData = row(form());
insertBefore(rowData);
}}
>
{item.title}
</Menu.Item>
);
})}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item
style={{ minWidth: 150 }}
onClick={() => {
FormDialog('新建数据表', () => {
return <SchemaField schema={dbschema} />;
})
.open({
initialValues: {
// aaa: '123',
},
})
.then(() => {
const items = [...data];
items.push({ title: '数据表5' });
mutate(items);
const rowData = row(form());
insertBefore(rowData);
});
// const rowData = row(table());
// insertBefore(rowData);
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.SubMenu
key={`form-tables`}
style={{ minWidth: 150 }}
title={'新建表格'}
>
<Menu.ItemGroup key={`form-tables-itemgroup`} title={'所属数据表'}>
{data.map((item, index) => {
return (
<Menu.Item
key={`form-${index}`}
style={{ minWidth: 150 }}
onClick={() => {
const rowData = row(table());
console.log({ rowData });
insertBefore(rowData);
}}
>
{item.title}
</Menu.Item>
);
})}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
<Menu.Item
style={{ minWidth: 150 }}
onClick={() => {
FormDialog('新建数据表', () => {
return <SchemaField schema={dbschema} />;
})
.open({
initialValues: {
// aaa: '123',
},
})
.then(() => {
const items = [...data];
items.push({ title: '数据表4' });
mutate(items);
const rowData = row(table());
insertBefore(rowData);
});
}}
>
</Menu.Item>
</Menu.SubMenu>
<Menu.Item
onClick={() => {
const rowData = row(markdown());
insertBefore(rowData);
}}
>
Markdown
</Menu.Item>
</Menu>
}
>
<Button icon={<PlusOutlined />}></Button>
</Dropdown>
);
});
AddNew.FormItem = observer((props) => {
const field = useField();
const segments = [...field.address.segments];
segments.pop();
segments.pop();
segments.pop();
const { insertBefore, refresh, schema } = useSchemaQuery(segments);
const insertBeforeHandle = () => {
const rowData = row({
type: 'string',
// required: true,
name: `f_${uid()}`,
title: `字段${uid()}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
});
console.log({ rowData });
insertBefore(rowData);
};
return (
<Dropdown
overlay={
<Menu>
<Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}>
1
</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}>2</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}>3</Menu.Item>
<Menu.Divider />
<Menu.SubMenu title={'新增字段'}>
<Menu.Item onClick={insertBeforeHandle} style={{ minWidth: 150 }}>
</Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
<Menu.Item onClick={insertBeforeHandle}></Menu.Item>
</Menu.SubMenu>
</Menu>
}
>
<Button icon={<PlusOutlined />}></Button>
</Dropdown>
);
});
export default AddNew;

View File

@ -0,0 +1,8 @@
import { uid } from '@formily/shared';
export default () => ({
name: `markdown_${uid()}`,
type: 'string',
'x-component': 'Markdown',
'x-component-props': {},
});

View File

@ -0,0 +1,86 @@
import { uid } from '@formily/shared';
export default () => ({
name: `t_${uid()}`,
type: 'array',
'x-component': 'Table',
default: [{ field1: 'a', field2: 'b' }, { field1: 'a', field2: 'b' }],
'x-component-props': {},
properties: {
[`a_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
action1: {
type: 'object',
title: '按钮1',
'x-component': 'Table.BulkAction.Popover',
'x-component-props': {},
properties: {},
},
action2: {
type: 'void',
title: '按钮2',
'x-component': 'Table.BulkAction',
'x-component-props': {},
},
},
},
[`a_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
'x-component-props': {
align: 'bottom',
},
properties: {
pagination: {
type: 'void',
'x-component': 'Table.Pagination',
'x-component-props': {
defaultCurrent: 1,
total: 50,
},
},
},
},
[`c_${uid()}`]: {
type: 'void',
title: '字段1',
'x-component': 'Table.Column',
'x-component-props': {
title: 'z1',
width: 100,
// DesignableBar: 'Table.Column.DesignableBar',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field1: {
type: 'string',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
[`c_${uid()}`]: {
type: 'void',
title: '字段2',
'x-component': 'Table.Column',
properties: {
field2: {
type: 'string',
title: '字段2',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
});

View File

@ -1,19 +0,0 @@
---
title: Calendar - 日历
nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
path: /client/blocks
---
# Calendar - 日历
## 代码演示
### 基本使用
### 设计器模式

View File

@ -0,0 +1,100 @@
---
title: Cascader - 级联选择
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Cascader - 级联选择
## 省市区级联
```tsx
/**
* title: 省市区级联
*/
import React from 'react';
import { SchemaBlock } from '../';
const options = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
},
],
},
],
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
name: 'name1',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Cascader',
'x-component-props': {
changeOnSelect: true,
},
},
},
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,11 +1,12 @@
import React from 'react'
import { connect, mapReadPretty, mapProps } from '@formily/react'
import { Cascader as AntdCascader } from 'antd'
import { Descriptions } from '../descriptions'
import { Display } from '../display'
import { LoadingOutlined } from '@ant-design/icons'
export const Cascader = connect(
(props) => {
console.log('props.title', props.title);
return <AntdCascader {...props}/>
},
mapProps(
@ -24,7 +25,7 @@ export const Cascader = connect(
}
}
),
mapReadPretty(Descriptions.Cascader)
mapReadPretty(Display.Cascader)
)
export default Cascader

View File

@ -1,19 +0,0 @@
---
title: Chart - 图表
nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
path: /client/blocks
---
# Chart - 图表
## 代码演示
### 基本使用
### 设计器模式

View File

@ -0,0 +1,117 @@
---
title: Checkbox - 多选框
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Checkbox - 多选框
## 代码演示
### 勾选
```tsx
/**
* title: 勾选
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 组
```tsx
/**
* title: 组
*/
import React from 'react';
import { SchemaBlock } from '../';
const options = [
{
label: '选项1',
value: 1,
color: 'red',
},
{
label: '选项2',
value: 2,
color: 'blue',
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Checkbox.Group',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Checkbox.Group',
}
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -2,7 +2,6 @@ import React from 'react';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { Checkbox as AntdCheckbox, Tag } from 'antd';
import { CheckboxProps, CheckboxGroupProps } from 'antd/lib/checkbox';
import { Descriptions } from '../descriptions';
import uniq from 'lodash/uniq';
import { CheckOutlined, CloseOutlined } from '@ant-design/icons';

View File

@ -0,0 +1,57 @@
---
title: ColorSelect - 颜色选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# ColorSelect - 颜色选择器
```tsx
/**
* title: 颜色选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'icon',
type: 'string',
title: `编辑模式`,
name: 'name1',
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'icon',
type: 'string',
title: `阅读模式`,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'ColorSelect',
}
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,7 +1,6 @@
import React from 'react'
import { connect, mapProps, mapReadPretty } from '@formily/react'
import { Tag, Select } from 'antd'
import { Descriptions } from '../descriptions'
import { LoadingOutlined } from '@ant-design/icons'
const colors = {

View File

@ -0,0 +1,76 @@
---
title: DatabaseField - 数据表字段
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# DatabaseField - 数据表字段
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
array: {
type: 'array',
// title: '数据表字段',
'x-component': 'DatabaseField.ArrayCollapse',
'x-component-props': {
accordion: true,
},
// maxItems: 3,
'x-decorator': 'FormItem',
items: {
type: 'object',
'x-component': 'DatabaseField.ArrayCollapse.CollapsePanel',
'x-component-props': {
header: '字段',
},
properties: {
index: {
type: 'void',
'x-component': 'DatabaseField.ArrayCollapse.Index',
},
input: {
type: 'string',
'x-decorator': 'FormItem',
title: 'Input',
required: true,
'x-component': 'Input',
},
remove: {
type: 'void',
'x-component': 'DatabaseField.ArrayCollapse.Remove',
},
moveUp: {
type: 'void',
'x-component': 'DatabaseField.ArrayCollapse.MoveUp',
},
moveDown: {
type: 'void',
'x-component': 'DatabaseField.ArrayCollapse.MoveDown',
},
},
},
properties: {
addition: {
type: 'void',
title: '添加字段',
'x-component': 'DatabaseField.ArrayCollapse.Addition',
},
},
},
}
};
export default () => {
return <SchemaBlock schema={schema} />
}
```

View File

@ -0,0 +1,9 @@
import { observer, connect } from '@formily/react';
import React from 'react';
import { ArrayCollapse } from '@formily/antd';
import '@formily/antd/lib/form-tab/style';
export const DatabaseField: any = () => null;
DatabaseField.ArrayCollapse = ArrayCollapse;

View File

@ -0,0 +1,110 @@
---
title: DatePicker - 日期选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# DatePicker - 日期选择器
## 代码演示
### 日期选择器
```tsx
/**
* title: 日期选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'icon',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'icon',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
},
},
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 日期时间选择
```tsx
/**
* title: 日期时间选择
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'icon',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'icon',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component-props': {
showTime: true,
},
},
},
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -5,7 +5,7 @@ import {
DatePickerProps as AntdDatePickerProps,
RangePickerProps,
} from 'antd/lib/date-picker'
import { Descriptions } from '../descriptions'
import { Display } from '../display'
import { formatMomentValue, momentable } from '@formily/antd/esm/__builtins__'
type DatePickerProps<PickerProps> = Exclude<
@ -52,13 +52,13 @@ const mapDateFormat = function () {
export const DatePicker: ComposedDatePicker = connect(
AntdDatePicker,
mapProps(mapDateFormat()),
mapReadPretty(Descriptions.DatePicker)
mapReadPretty(Display.DatePicker)
)
DatePicker.RangePicker = connect(
AntdDatePicker.RangePicker,
mapProps(mapDateFormat()),
mapReadPretty(Descriptions.DateRangePicker)
mapReadPretty(Display.DateRangePicker)
)
export default DatePicker

View File

@ -1,8 +0,0 @@
import React from 'react';
import { FormBlock } from '../form';
export function DescriptionsBlock(props: any) {
return <FormBlock readPretty={true} {...props} />
};
export default DescriptionsBlock;

View File

@ -339,7 +339,7 @@ const TimeRangePicker: React.FC<TimeRangePickerProps> = (props) => {
)
}
export const Descriptions = {
export const Display = {
Input,
URL,
TextArea,
@ -356,5 +356,4 @@ export const Descriptions = {
usePlaceholder,
}
export default Descriptions;
export default Display;

View File

@ -0,0 +1,12 @@
---
title: DrawerSelect - 抽屉选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# DrawerSelect - 抽屉选择器

View File

@ -0,0 +1,10 @@
---
title: Filter - 筛选器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
export const ActionBar = (props) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</>
);
};

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
export const DesignableBar = (props) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</>
);
};

View File

@ -1,62 +0,0 @@
import request from 'umi-request';
export interface ResourceOptions {
resourceName: string;
associatedKey?: any;
associatedName?: string;
resourceKey?: any;
}
export interface GetOptions {
resourceKey?: any;
appends?: string[];
}
export interface SaveOptions {
resourceKey?: any;
}
export class Resource {
protected options: ResourceOptions;
constructor(options: string | ResourceOptions) {
if (typeof options === 'string') {
this.options = { resourceName: options }
} else {
this.options = options;
}
}
get(options: GetOptions = {}) {
const resourceKey = options.resourceKey || this.options.resourceKey;
const { resourceName } = this.options;
if (!resourceKey) {
return Promise.resolve({ data: {} });
}
return request(`/api/${resourceName}:get/${resourceKey}`);
}
save(values: any, options: SaveOptions = {}) {
const resourceKey = options.resourceKey || this.options.resourceKey;
const { resourceName } = this.options;
const url = `/api/${resourceName}:${resourceKey ? `update/${resourceKey}` : 'create'}`;
return request(url, {
method: 'post',
data: values,
});
}
static make(options: string | Resource | ResourceOptions) {
if (typeof options === 'string') {
return new Resource({ resourceName: options });
}
if (options instanceof Resource) {
return options;
}
if (typeof options === 'object' && options.resourceName) {
return new Resource(options);
}
throw 'resource 参数错误';
}
}

View File

@ -1,12 +0,0 @@
import { Input, FormItem } from '@formily/antd';
import { createSchemaField } from '@formily/react';
export const SchemaField = createSchemaField({
components: {
Input,
FormItem,
},
scope: {},
});
export default SchemaField;

View File

@ -1,35 +0,0 @@
/**
* title: 基本使用
*/
import React from 'react';
import { FormBlock } from '@nocobase/client';
const fields = [
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
default: 'abcdefg',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'please enter',
},
},
];
export default () => {
return (
<FormBlock
resource={'users'}
fields={fields}
effects={{
onFormValuesChange(form) {
console.log('aaaa', form.values);
},
}}
/>
);
};

View File

@ -1,37 +0,0 @@
/**
* title: 更新数据表单
* desc: 设置 `resource` `resourceKey`
*/
import React from 'react';
import { FormBlock } from '@nocobase/client';
export default () => {
return (
<FormBlock
resource={{
resourceName: 'users',
resourceKey: 1,
}}
fields={[
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
default: 'abcdefg',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'please enter',
},
},
]}
effects={{
onFormValuesChange(form) {
console.log('aaaa', form.values);
},
}}
/>
);
};

View File

@ -1,64 +0,0 @@
import React from 'react';
import { FormBlock, Drawer } from '@nocobase/client';
export default () => {
const items = [
{ id: 1, username: '文本一' },
{ id: 2, username: '文本二' },
];
return (
<ul>
{items.map((item) => (
<li>
<a
onClick={() => {
Drawer.open({
title: 'Form',
content: ({ resolve, closeWithConfirm }) => (
<FormBlock
initialValues={item}
resource={{
resourceName: 'users',
resourceKey: item.id,
}}
fields={[
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
default: 'aa',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
]}
effects={{
onFormValuesChange(form) {
if (form.modified && closeWithConfirm) {
closeWithConfirm(true);
}
console.log('aaaa', form);
},
}}
onSuccess={() => {
return new Promise((r) => {
setTimeout(() => {
r(null);
resolve();
}, 2000);
});
}}
/>
),
});
}}
>
{item.username}
</a>
</li>
))}
</ul>
);
};

View File

@ -1,65 +0,0 @@
import React from 'react';
import { Drawer, FormBlock } from '@nocobase/client';
export default () => {
const items = [
{ id: 1, username: '文本一' },
{ id: 2, username: '文本二' },
];
return (
<ul>
{items.map((item) => (
<li>
<a
onClick={() => {
Drawer.open({
title: '详情',
content: ({ resolve, closeWithConfirm }) => (
<FormBlock
readPretty
initialValues={item}
resource={{
resourceName: 'users',
resourceKey: item.id,
}}
fields={[
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
default: 'aa',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
]}
effects={{
onFormValuesChange(form) {
if (form.modified && closeWithConfirm) {
closeWithConfirm(true);
}
console.log('aaaa', form);
},
}}
onSuccess={() => {
return new Promise((r) => {
setTimeout(() => {
r(null);
resolve();
}, 2000);
});
}}
/>
),
});
}}
>
{item.username}
</a>
</li>
))}
</ul>
);
};

View File

@ -1,94 +1,502 @@
---
title: Form - 表单
nav:
title: 组件
path: /client
title: 组件
path: /client
group:
order: 2
title: Blocks
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Form - 表单
## 示
## 代码演
### 基本使用
### 常规表单
<code src="./demos/demo1.tsx"/>
```tsx
import React from 'react';
import { SchemaBlock } from '../';
### 更新数据表单
<code src="./demos/demo2.tsx"/>
### 抽屉表单
<code src="./demos/demo3.tsx"/>
### 阅读模式
<code src="./demos/demo4.tsx"/>
## Props API
可能包括四部分:
- IFormLayoutProps表单布局相关参数
- IFormProps用于初始化 form 实例
- ISchemaField表单字段配置
- ResourceOptions用于资源定位
暂时只提供了一些核心的参数
### resource <Badge>ResourceOptions</Badge>
这部分参数还有待改进
```ts
export interface ResourceOptions {
resourceName: string;
associatedKey?: string;
associatedName?: string;
resourceKey?: string;
}
```
### fields <Badge>ISchemaField</Badge>
字段配置
```ts
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
component: {
type: 'string',
default: 'aa',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
const schema = {
type: 'object',
properties: {
form: {
type: 'object',
'x-component': 'Form',
properties: {
username: {
type: 'string',
title: '用户名',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
password: {
type: 'string',
title: '密码',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
},
}
}
},
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### readPretty
### 栅格布局表单
详情展示
```tsx
import React from 'react';
import { SchemaBlock } from '../';
### initialValues <Badge>IFormProps</Badge>
const schema = {
type: 'void',
name: 'item3',
'x-decorator': 'Card',
'x-component': 'Form',
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-component-props': {
title: 'block11',
},
properties: {
field1: {
type: 'string',
required: true,
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
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',
},
properties: {
field2: {
type: 'string',
required: true,
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block211',
},
properties: {
field3: {
type: 'string',
required: true,
title: '字段3',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
properties: {
field4: {
type: 'string',
required: true,
title: '字段4',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block311',
},
properties: {
field5: {
type: 'string',
required: true,
title: '字段5',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
}
初始值
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### effects <Badge>IFormProps</Badge>
### 表单设计器
支持以下 Effect Hooks
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { uid } from '@formily/shared';
- [Form Effect Hooks](https://core.formilyjs.org/api/entry/form-effect-hooks)
- [Field Effect Hooks](https://core.formilyjs.org/api/entry/field-effect-hooks)
const schema = {
type: 'void',
name: `form_${uid()}`,
'x-decorator': 'Card',
'x-component': 'Form',
properties: {
grid: {
type: 'void',
'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-component-props': {
title: 'block11',
},
'x-designable-bar': 'Input.DesignableBar',
properties: {
field1: {
type: 'string',
required: true,
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
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-designable-bar': 'Input.DesignableBar',
properties: {
field2: {
type: 'string',
required: true,
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block211',
},
'x-designable-bar': 'Input.DesignableBar',
properties: {
field3: {
type: 'string',
required: true,
title: '字段3',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
'x-designable-bar': 'Input.DesignableBar',
properties: {
field4: {
type: 'string',
required: true,
title: '字段4',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block311',
},
properties: {
field5: {
type: 'string',
required: true,
title: '字段5',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
[`gr_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[`gc_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[`gb_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[`gbn_${uid()}`]: {
type: 'void',
'x-component': 'AddNew.FormItem',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
};
### onSuccess
数据提交成功的回调函数
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,67 +1,23 @@
import React, { useEffect, useMemo } from 'react';
import { Form as Formily, FormButtonGroup, Submit, Reset } from '@formily/antd';
import React, { useMemo } from 'react';
import { createForm } from '@formily/core';
import { SchemaField, fields2properties, parseEffects } from '../../';
import { Resource, ResourceOptions } from '../../resource';
import { FormProvider, useFieldSchema } from '@formily/react';
import { DesignableSchemaField } from '../SchemaField';
import { DesignableBar } from './DesignableBar';
export interface FormBlockProps {
effects?: any;
fields?: any;
initialValues?: any;
onSuccess?: any;
resource: string | Resource | ResourceOptions;
readPretty?: boolean;
[key: string]: any;
}
export const Form = (props) => {
const form = useMemo(() => createForm(), []);
const schema = useFieldSchema();
export const FormBlock = (props: FormBlockProps) => {
const { initialValues, fields = [], effects, onSuccess, readPretty } = props;
const form = useMemo(
() =>
createForm({
readPretty,
initialValues,
effects: (form) => parseEffects(effects, form),
}),
[],
);
const resource = Resource.make(props.resource);
useEffect(() => {
if (!initialValues) {
resource.get().then(({ data }) => {
form.setInitialValues(data);
});
}
}, [initialValues]);
return (
<Formily
form={form}
layout={'vertical'}
onAutoSubmit={async (values) => {
console.log(form.values, values);
try {
const { data } = await resource.save(values);
if (onSuccess) {
return onSuccess(data);
}
return data;
} catch (error) {}
}}
>
<SchemaField
<FormProvider form={form}>
<DesignableSchemaField
schema={{
type: 'object',
properties: fields2properties(fields),
properties: schema.properties,
}}
/>
{!readPretty && (
<FormButtonGroup>
<Submit></Submit>
<Reset></Reset>
</FormButtonGroup>
)}
</Formily>
</FormProvider>
);
};
export default FormBlock;
Form.DesignableBar = DesignableBar;

View File

@ -1,13 +1,25 @@
import React, { useState } from 'react';
import React, { useState, useContext } from 'react';
import { useDrag, mergeRefs, useDrop } from './DND';
import classNames from 'classnames';
import { useField } from '@formily/react';
import {
useField,
observer,
RecursionField,
Schema,
SchemaOptionsContext,
} from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '.';
import { MenuOutlined, ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import get from 'lodash/get';
export const Block = (props) => {
const { children, title } = props;
export const Block = observer((props: any) => {
const { children, title, DesignableBar } = props;
const field = useField<Formily.Core.Models.Field>();
const { isDragging, dragRef, previewRef } = useDrag({
type: 'grid',
@ -24,12 +36,19 @@ export const Block = (props) => {
path: field.address.segments,
},
});
console.log('useDrag', 'previewElement', field.address.segments);
console.log('ActionBar', DesignableBar);
const { isOver, onTopHalf, dropRef } = useDrop({
accept: 'grid',
data: {},
canDrop: !isDragging,
});
const options = useContext(SchemaOptionsContext);
const DesignableBarComponent = get(options.components, DesignableBar);
console.log({ DesignableBarComponent, DesignableBar });
const { addBlock } = useSchemaQuery();
return (
<div
@ -38,19 +57,67 @@ export const Block = (props) => {
className={classNames('block', { 'top-half': onTopHalf, hover: isOver })}
// style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
>
<ActionBar dragRef={dragRef}/>
<div
style={{ textAlign: 'center', lineHeight: '60px', background: '#f1f1f1' }}
>
{children} {field.path.entire}
</div>
{DesignableBarComponent && (
<DefaultActionBar DesignableBarComponent={DesignableBarComponent} dragRef={dragRef} />
)}
{children}
</div>
);
};
});
const ActionBar = ({ dragRef }) => {
// function overlay() {
// return <Menu>
// <Menu.Item
// onClick={() => {
// addBlock(
// {},
// {
// insertBefore: true,
// },
// );
// setActive(false);
// }}
// icon={<ArrowUpOutlined />}
// >
// 在上方插入区块
// </Menu.Item>
// <Menu.Item
// onClick={() => {
// addBlock();
// setActive(false);
// }}
// icon={<ArrowDownOutlined />}
// >
// 在下方插入区块
// </Menu.Item>
// <Menu.Divider />
// <Menu.Item
// onClick={() => {
// removeBlock();
// setActive(false);
// }}
// icon={<DeleteOutlined />}
// >
// 删除区块
// </Menu.Item>
// </Menu>
// }
function Overlay(props) {
return (
<>
<Menu.Item>1</Menu.Item>
<Menu.Item>2</Menu.Item>
<Menu.Divider />
<Menu.Item></Menu.Item>
</>
);
}
const DefaultActionBar = ({ DesignableBarComponent, dragRef }) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
// return <MenuOutlined className={'draggable'} ref={dragRef} />;
return (
<div className={classNames('action-bar', { active })}>
<Dropdown
@ -58,40 +125,7 @@ const ActionBar = ({ dragRef }) => {
trigger={['click']}
visible={active}
onVisibleChange={setActive}
overlay={
<Menu>
<Menu.Item
onClick={() => {
addBlock({}, {
insertBefore: true,
});
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</Menu>
}
overlay={<Menu>{DesignableBarComponent ? <DesignableBarComponent /> : <Overlay />}</Menu>}
>
<MenuOutlined className={'draggable'} ref={dragRef} />
</Dropdown>

View File

@ -1,113 +0,0 @@
.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;
}
}
}
.row-divider {
position: relative;
&.hover {
&::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 12px;
width: 100%;
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 {
position: absolute;
top: 5px;
right: 8px;
}
}
.draggable {
cursor: grab !important;
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Row, Col } from '../';
import './demo5.less';
export default () => {
return (

View File

@ -1,36 +1,8 @@
import { Grid, Row, Col, Block, SchemaFieldWithDesigner } from '../';
import './demo5.less';
import { Card } from 'antd';
import classNames from 'classnames';
import React, { useMemo } from 'react';
import {
FormProvider,
FormConsumer,
useField,
useFieldSchema,
ISchema,
Schema,
} from '@formily/react';
import { createForm } from '@formily/core';
import React from 'react';
import { SchemaBlock } from '../../';
import { ISchema } from '@formily/json-schema';
function Designer(props: { schema?: ISchema }) {
const form = useMemo(() => createForm({}), []);
const { schema } = props;
return (
<div>
<FormProvider form={form}>
<SchemaFieldWithDesigner schema={schema} />
{/* <FormConsumer>
{(form) => {
return <div>{JSON.stringify(form.values, null, 2)}</div>;
}}
</FormConsumer> */}
</FormProvider>
</div>
);
}
const schema = new Schema({
const schema: ISchema = {
type: 'object',
properties: {
grid: {
@ -52,9 +24,17 @@ const schema = new Schema({
block11: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block11',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block11
</div>
),
},
},
},
@ -72,6 +52,17 @@ const schema = new Schema({
'x-component-props': {
title: 'block21',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block21
</div>
),
},
},
},
@ -91,9 +82,17 @@ const schema = new Schema({
block211: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block211',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block211
</div>
),
},
},
},
@ -108,9 +107,17 @@ const schema = new Schema({
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block221
</div>
),
},
},
},
@ -119,7 +126,7 @@ const schema = new Schema({
row3: {
type: 'void',
'x-component': 'Grid.Row',
"x-component-props": {
'x-component-props': {
isLast: true,
},
properties: {
@ -134,9 +141,17 @@ const schema = new Schema({
block311: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block311',
},
'x-content': (
<div
style={{
padding: 24,
textAlign: 'center',
background: 'rgb(241, 241, 241)',
}}
>
block311
</div>
),
},
},
},
@ -145,64 +160,8 @@ const schema = new Schema({
},
},
},
})
};
export default () => {
return (
<Card>
<Designer
schema={schema}
/>
{/* <Grid
onDrop={(e) => {
console.log('onDrop', e, e.data);
}}
>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[1, 2, 3].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[4, 5, 6].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[7, 8].map((index) => (
<Col size={1 / 3}>
<Block>col {index}</Block>
</Col>
))}
</Row>
<Row
onColResize={(e) => {
console.log(e.data);
}}
>
{[9].map((index) => (
<Col size={1}>
<Block>col {index}</Block>
</Col>
))}
</Row>
</Grid> */}
</Card>
);
return <SchemaBlock schema={schema} />;
};

View File

@ -73,16 +73,7 @@ group:
- 100%
## 代码演示
<!--
### 基本用法
<code src="./demos/demo2.tsx"/>
### 内嵌区块
<code src="./demos/demo3.tsx"/> -->
### useDrag & useDrop

View File

@ -7,31 +7,12 @@ import {
useField,
} from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaField } from '../../fields';
import set from 'lodash/set';
import './style.less';
export const SchemaDesignerContext = createContext<Schema>(new Schema({}));
export const SchemaRefreshContext = createContext(null);
export function SchemaFieldWithDesigner(props: { schema?: ISchema }) {
function Container(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<SchemaRefreshContext.Provider
value={() => {
refresh(Math.random());
}}
>
<SchemaDesignerContext.Provider value={schema}>
<SchemaField schema={schema} />
</SchemaDesignerContext.Provider>
{/* <pre>{JSON.stringify(schema.toJSON(), null, 2)}</pre> */}
</SchemaRefreshContext.Provider>
);
}
return <Container schema={new Schema(props.schema)} />;
}
import {
DesignableSchemaContext,
RefreshDesignableSchemaContext,
} from '../SchemaField';
export function removeProperty(property: Schema) {
property.parent.removeProperty(property.name);
@ -71,8 +52,8 @@ export const getSchemaAddressSegments = (schema: Schema) => {
};
export function useSchemaQuery() {
const context = useContext(SchemaDesignerContext);
const refresh = useContext(SchemaRefreshContext);
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const field = useField();
const form = useForm();
@ -126,15 +107,15 @@ export function useSchemaQuery() {
const name = names[index];
const property = row.properties[name];
const addProperty = isOver ? addPropertyAfter : addPropertyBefore;
const count = Object.keys(row.properties).length+1;
const count = Object.keys(row.properties).length + 1;
return (data) => {
const other = 1-(1/count);
Object.keys(row.properties).forEach(name => {
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 => {
form.setFieldState(segments.join('.'), (state) => {
state.componentProps.size = other * state.componentProps.size;
console.log({state}, other * state.componentProps.size);
console.log({ state }, other * state.componentProps.size);
});
});
addProperty(property, {
@ -142,7 +123,7 @@ export function useSchemaQuery() {
name: `c_${uid()}`,
'x-component': 'Grid.Col',
'x-component-props': {
size: 1/count,
size: 1 / count,
},
properties: {
[data.name]: data,
@ -177,18 +158,18 @@ export function useSchemaQuery() {
removeProperty(schema.parent);
const cols = [];
let allSize = 0;
Object.keys(schema.parent.parent.properties).forEach(name => {
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 => {
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;
form.setFieldState(segments.join('.'), (state) => {
state.componentProps.size = state.componentProps.size / allSize;
});
}
}
@ -219,7 +200,7 @@ export function useSchemaQuery() {
},
},
},
})
});
} else {
addProperty(schema, data);
}
@ -241,18 +222,18 @@ export function useSchemaQuery() {
source.parent.parent.removeProperty(source.parent.name);
const cols = [];
let allSize = 0;
Object.keys(source.parent.parent.properties).forEach(name => {
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 => {
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;
form.setFieldState(segments.join('.'), (state) => {
state.componentProps.size = state.componentProps.size / allSize;
});
}
} else {

View File

@ -1,143 +1,114 @@
.drop-column {
position: absolute;
top: 0;
left: -12px;
height: calc(100% - 24px);
width: 24px;
// border: 1px dashed rgb(232, 232, 232);
&.active {
.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;
width: 12px;
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
height: 100%;
position: absolute;
left: 5px;
background: #e6f7ff;
}
}
&.last {
left: auto;
right: -24px;
}
}
.drop-block {
height: 12px;
position: absolute;
bottom: -12px;
width: 100%;
left: 0;
// background: #e6f7ff;
// pointer-events: none;
&.active {
background: #e6f7ff;
}
}
// .ant-formily-item + .drop-block {
// position: absolute;
// bottom: 12px;
// width: 100%;
// }
.grid-row {
position: relative;
&.active {
&::after {
content: '';
display: block;
height: 12px;
position: absolute;
bottom: 5px;
width: 100%;
width: 12px;
background: #e6f7ff;
}
}
}
.grid-block {
.row-divider {
position: relative;
padding-bottom: 1px;
&.active {
&.hover {
&::after {
content: '';
display: block;
height: 12px;
position: absolute;
bottom: 5px;
left: 0;
top: 50%;
transform: translateY(-50%);
height: 12px;
width: 100%;
background: #e6f7ff;
}
}
&.grid-block--Input {
&: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;
.drop-block {
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;
}
}
}rgb(172, 172, 172)
.grid-block-actions {
position: absolute;
top: 5px;
right: 5px;
z-index: 2;
display: none;
}
> .action-bar {
position: absolute;
right: 10px;
top: 5px;
z-index: 2;
display: none;
&.active {
display: inline-block;
}
}
&:hover,
&.hover {
> .action-bar {
display: inline-block;
}
}
}
.grid-block .ant-card {
margin-bottom: 24px;
}
.drop-row {
height: 12px;
// background: #e6f7ff;
position: absolute;
bottom: 5px;
width: 100%;
// background: #e6f7ff;
&.active {
background: #e6f7ff;
}
&.first {
top: -17px;
z-index: 2;
bottom: auto;
right: 8px;
z-index: 222;
}
}
.draggable {
cursor: move !important; /* fallback: no `url()` support or images disabled */
cursor: -webkit-grab !important; /* Chrome 1-21, Safari 4+ */
cursor: -moz-grab !important; /* Firefox 1.5-26 */
cursor: grab !important; /* W3C standards syntax, should come least */
}
.draggable:active {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
tr.drop-over-downward td {
border-bottom: 1px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 1px dashed #1890ff;
cursor: grab !important;
}

View File

@ -1,338 +0,0 @@
import React, { useContext, createContext, useState } from 'react';
import { Schema, useFieldSchema, useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { SchemaField } from '../../fields';
import set from 'lodash/set';
export const SchemaDesignerContext = createContext<Schema>(new Schema({}));
export const SchemaRefreshContext = createContext(null);
export function SchemaFieldWithDesigner(props) {
function Container(props) {
const { schema } = props;
const [, refresh] = useState(0);
return (
<SchemaRefreshContext.Provider
value={() => {
refresh(Math.random());
}}
>
<SchemaDesignerContext.Provider value={schema}>
<SchemaField schema={schema} />
</SchemaDesignerContext.Provider>
</SchemaRefreshContext.Provider>
);
}
return <Container schema={new Schema(props.schema)} />;
}
export const getFullPaths = (schema: Schema) => {
if (!schema) {
return [];
}
const paths = [schema.name];
if (schema.parent && schema.parent.name) {
paths.unshift(...getFullPaths(schema.parent));
}
return paths;
};
export function useSchema() {
const context = useContext(SchemaDesignerContext);
const refresh = useContext(SchemaRefreshContext);
const fieldSchema = useFieldSchema();
const paths = getFullPaths(fieldSchema);
let schema: Schema = context;
const names = [...paths];
while (names.length) {
schema = schema.properties[names.shift()];
}
return { schema, fieldSchema, refresh };
}
export function getSchema(context) {
return (paths) => {
const fullPaths = Array.isArray(paths) ? paths : getFullPaths(paths);
let s: Schema = context;
const names = [...fullPaths];
while (names.length) {
s = s.properties[names.shift()];
}
return s;
};
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export function useSchemaQuery() {
const context = useContext(SchemaDesignerContext);
const form = useForm();
const { schema, refresh } = useSchema();
return {
resizeColumn() {
function getPrevProperty() {
const names = Object.keys(schema.parent.properties);
const index = names.indexOf(schema.name as string);
return schema.parent.properties[names[index-1]];
}
console.log('onMouseDown', schema.name);
console.log('onMouseDown', schema.parent.properties);
console.log('onMouseDown', getFullPaths(getPrevProperty()));
form.setFieldState(getFullPaths(getPrevProperty()).join('.'), (state) => {
const span = state.componentProps.span;
let width = state.componentProps.width||50;
width -= 1;
state.componentProps = {
span: 1*span - 1,
width,
style: {
flex: `0 0 ${width}%`,
maxWidth: `${width}%`,
},
};
});
console.log('onMouseDown', getFullPaths(schema));
form.setFieldState(getFullPaths(schema).join('.'), (state) => {
const span = state.componentProps.span;
console.log({span})
let width = state.componentProps.width||50;
width += 1;
state.componentProps = {
span: 1*span + 1,
style: {
flex: `0 0 ${width}%`,
maxWidth: `${width}%`,
},
};
});
refresh();
},
removeBlock() {
schema.parent.removeProperty(schema.name);
refresh();
},
addBlock: (data, up = false) => {
const blockSchema = block({
type: 'string',
title: `Block ${uid()}`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
});
if ('Grid.Column' === schema.parent['x-component']) {
if (Object.keys(schema.parent.parent.properties).length > 1) {
up
? addPropertyBefore(schema, blockSchema)
: addPropertyAfter(schema, blockSchema);
} else {
const rowSchema = row(column(blockSchema));
up
? addPropertyAfter(schema.parent.parent, rowSchema)
: addPropertyAfter(schema.parent.parent, rowSchema);
}
}
console.log('x-component', schema.parent['x-component']);
refresh();
// schema.parent['x-component']
},
insertAfter: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
if (!source || !target) {
return;
}
console.log({
sourcePath,
source,
target,
targetPath,
sourceParentproperties: source.parent.properties,
});
const names = [];
Object.keys(target.parent.properties).forEach((name) => {
// if (names.includes(source.name)) {
// return;
// }
// names.push(name);
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
// names.push(source.name);
source.parent.removeProperty(source.name);
console.log('source.parent.properties', source.parent.properties);
target.parent.addProperty(source.name, source.toJSON());
}
});
},
insertAfterWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
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(rowSchema.name, rowSchema);
}
});
},
insertBeforeWithAddRow: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const rowSchema = row(column(source.toJSON()));
source.parent.removeProperty(source.name);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(rowSchema.name, rowSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
},
appendToRowWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.properties).length + 1;
target.addProperty(colSchema.name, colSchema);
console.log('target.properties', target.properties);
Object.keys(target.properties).forEach((name) => {
const prop = target.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
insertBeforeWithAddColumn: (sourcePath, targetPath) => {
const source = getSchema(context)(sourcePath);
const target = getSchema(context)(targetPath);
const colSchema = column(source.toJSON());
source.parent.removeProperty(source.name);
const len = Object.keys(target.parent.properties).length + 1;
console.log('x-component-props.span', 24 / len);
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(colSchema.name, colSchema);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
const json = property.toJSON();
target.parent.addProperty(property.name, json);
});
Object.keys(target.parent.properties).forEach((name) => {
const prop = target.parent.properties[name];
form.setFieldState(getFullPaths(prop).join('.'), (state) => {
state.componentProps = {
span: 24 / len,
};
});
});
},
};
}
export const grid = (...rows: any[]) => {
const rowProperties = {};
rows.forEach((row, index) => {
set(row, 'x-component-props.rowOrder', index);
rowProperties[row.name] = row;
});
const name = `g_${uid()}`;
return {
type: 'void',
name,
'x-component': 'Grid',
'x-component-props': {},
properties: rowProperties,
};
};
export const row = (...cols: any[]) => {
const rowName = `r_${uid()}`;
const colsProperties = {};
cols.forEach((col, index) => {
set(col, 'x-component-props.columnOrder', index);
set(col, 'x-component-props.span', 24 / cols.length);
colsProperties[col.name] = col;
});
return {
type: 'void',
name: rowName,
'x-component': 'Grid.Row',
'x-component-props': {},
properties: colsProperties,
};
};
export const column = (...blocks: any[]) => {
const colName = `c_${uid()}`;
const properties = {};
blocks.forEach((item) => {
properties[item.name] = item;
});
return {
name: colName,
type: 'void',
'x-component': 'Grid.Column',
'x-read-pretty': true,
'x-component-props': {
labelCol: 6,
wrapperCol: 10,
span: 24,
},
properties,
};
};
export const block = (...fields: any[]) => {
const blockName = `b_${uid()}`;
const properties = {};
fields.forEach((item) => {
const name = item.name || `f_${uid()}`;
// item.title = `${item.title} ${name}`;
properties[name] = item;
});
const lastComponentType = fields[fields.length - 1]['x-component'];
return {
name: blockName,
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
lastComponentType,
},
'x-read-pretty': false,
properties,
};
};

View File

@ -0,0 +1,57 @@
---
title: IconPicker - 图标选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# IconPicker - 图标选择器
```tsx
/**
* title: 图标
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'icon',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'icon',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
}
};
export default () => {
return (
<div>
<SchemaBlock schema={schema} />
</div>
);
};
```

View File

@ -2,7 +2,78 @@ import React, { useState } from 'react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input, Popover, Button } from 'antd';
import { LoadingOutlined, CloseOutlined } from '@ant-design/icons';
import { icons, hasIcon, Icon } from '../../components/Icon';
import {
createFromIconfontCN,
UserOutlined,
TeamOutlined,
DatabaseOutlined,
DashboardOutlined,
SettingOutlined,
TableOutlined,
MenuOutlined,
HistoryOutlined,
NotificationOutlined,
} from '@ant-design/icons';
import * as antIcons from '@ant-design/icons';
export const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_2261954_u9jzwc44ug.js'],
});
export const icons = new Map<string, any>();
export function registerIcon(type: string, icon: any = IconFont) {
icons.set(type.toLowerCase(), icon);
}
export function hasIcon(type: string) {
if (!type) {
return false;
}
return icons.has(type.toLowerCase());
}
export function registerIcons(components) {
Object.keys(components).forEach((type) => {
registerIcon(type, components[type]);
});
}
Object.keys(antIcons).forEach((name) => {
if (name.endsWith('Outlined')) {
registerIcon(name, antIcons[name]);
}
});
registerIcons({
// HistoryOutlined,
// MenuOutlined,
// TableOutlined,
// SettingOutlined,
// TeamOutlined,
// UserOutlined,
// DatabaseOutlined,
// DashboardOutlined,
// NotificationOutlined,
});
interface IconProps {
type: string;
[key: string]: any;
}
export function Icon(props: IconProps) {
const { type = '', ...restProps } = props;
if (type && icons.has(type.toLowerCase())) {
const IconComponent = icons.get(type.toLowerCase());
if (IconComponent === IconFont) {
return <IconFont type={type} />;
}
return <IconComponent {...restProps} />;
}
return <IconFont type={type} />;
}
function IconField(props: any) {
const { value, onChange } = props;
@ -17,7 +88,9 @@ function IconField(props: any) {
setVisible(val);
}}
content={
<div style={{width: '26em', maxHeight: '20em', overflowY: 'auto'}}>
<div
style={{ width: '26em', maxHeight: '20em', overflowY: 'auto' }}
>
{[...icons.keys()].map((key) => (
<span
style={{ fontSize: 18, marginRight: 10, cursor: 'pointer' }}

View File

@ -1,90 +0,0 @@
---
title: Block
nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
path: /client/blocks
---
# 区块
## 示例
```tsx
import React from 'react';
import { createBlock, GridBlock } from '@nocobase/client';
function Hello({ name }) {
return <div>Hello {name}</div>;
}
const Block = createBlock({
components: {
Hello,
GridBlock,
},
});
const blocks = [
{
name: 'gb1',
'x-component': 'Hello',
'x-row': 0,
'x-column': 0,
'x-sort': 1,
},
{
name: 'gb2',
'x-component': 'Hello',
'x-row': 0,
'x-column': 0,
'x-sort': 2,
},
{
name: 'gb3',
'x-component': 'Hello',
'x-row': 0,
'x-column': 1,
'x-sort': 0,
},
{
name: 'gb4',
'x-component': 'Hello',
'x-row': 1,
'x-column': 0,
'x-sort': 0,
},
{
name: 'gb5',
'x-component': 'Hello',
'x-row': 2,
'x-column': 0,
'x-sort': 0,
},
];
export default () => {
return (
<div>
<Block x-component={'Hello'} name={'demo1'}/>
<Block x-component={'Hello'} name={'demo2'}/>
<Block x-component={'GridBlock'} blocks={blocks}/>
</div>
);
};
```
## API
### createBlock
- components 区块组件
创建区块
### Block
- x-component

View File

@ -1,46 +1,36 @@
import React, { createContext, useContext } from 'react';
import React, { useMemo } from 'react';
import { createForm } from '@formily/core';
import { FormProvider } from '@formily/react';
import { DesignableSchemaField, SchemaField } from './SchemaField';
export * from './form';
export * from './descriptions';
export * from './table';
export * from './SchemaField';
export const BlockContext = createContext({});
export const SchemaBlock = ({ schema, onlyRenderProperties = false, designable = true }) => {
const form = useMemo(() => createForm(), []);
export interface BlockProps {
name: string;
blocks?: any;
'x-component'?: any;
[key: string]: any;
}
let s = schema;
export interface CreateBlockOptions {
components?: any;
}
export function createBlock(options: CreateBlockOptions) {
const { components } = options;
function Block(props: BlockProps) {
const { ['x-component']: component } = props;
const Component = component ? components[component] : null;
if (!Component) {
return null;
}
return (
<BlockContext.Provider value={components}>
<Component {...props}/>
</BlockContext.Provider>
)
if (onlyRenderProperties) {
s = {
type: 'object',
properties: schema.properties,
};
} else if (schema.name) {
s = {
type: 'object',
properties: {
[schema.name]: schema,
},
};
}
return Block;
}
export function useBlock(props: any) {
const components = useContext(BlockContext);
const { 'x-component': component } = props;
const Component = component ? components[component] : null;
return {
Component,
}
}
return (
<FormProvider form={form}>
{designable ? (
<DesignableSchemaField schema={s} />
) : (
<SchemaField schema={s} />
)}
</FormProvider>
);
};

View File

@ -0,0 +1,109 @@
---
title: InputNumber - 数字框
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# InputNumber - 数字框
### 数字框
```tsx
/**
* title: 数字框
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'number',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'number',
type: 'number',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 百分比
```tsx
/**
* title: 百分比
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'number',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: 'please enter',
formatter: value => `${value}%`,
parser: value => value.replace('%', ''),
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'number',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'InputNumber',
'x-component-props': {
placeholder: 'please enter',
formatter: value => `${value}%`,
parser: value => value.replace('%', ''),
},
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,10 +1,10 @@
import { connect, mapReadPretty } from '@formily/react'
import { InputNumber as AntdNumber } from 'antd'
import { Descriptions } from '../descriptions'
import { Display } from '../display'
export const InputNumber = connect(
AntdNumber,
mapReadPretty(Descriptions.InputNumber)
mapReadPretty(Display.InputNumber)
)
export default InputNumber

View File

@ -0,0 +1,69 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu, Switch } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
export const DesignableBar = (props) => {
const { schema, addBlock, removeBlock, refresh } = useSchemaQuery();
const [active, setActive] = useState(false);
const field = useField();
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item onClick={() => {}}>
<Switch
onChange={(checked) => {
const key = Object.keys(schema.properties).shift();
schema.properties[key].required = checked;
refresh();
field.query(key).take((target: Formily.Core.Models.Field) => {
target.required = checked;
});
}}
checkedChildren="必填"
unCheckedChildren="非必填"
/>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</>
);
};

View File

@ -0,0 +1,260 @@
---
title: Input - 输入框
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Input - 输入框
输入框是非常常用的控件,参数可以组合成许多字段,如单行文本、多行文本、手机号、邮箱、网址等。
## 代码演示
### 单行文本
```tsx
/**
* title: 单行文本
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 多行文本
```tsx
/**
* title: 多行文本
* desc: 用于多行输入,使用 `Input.TextArea` 组件,`ellipsis=true` 时超出隐藏
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
name: 'name1',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-reactions': {
target: '*(read1,read2)',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read1: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
read2: {
interface: 'string',
type: 'string',
title: `阅读模式(超出隐藏)`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {
ellipsis: true,
},
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 电子邮箱
```tsx
/**
* title: 电子邮箱
* desc: 加上邮箱验证 `x-validator=email`
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'email',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'email',
},
},
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 手机号
```tsx
/**
* title: 手机号
* desc: 加上手机号验证 `x-validator=phone`
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'phone',
'x-reactions': {
target: 'name2',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-validator': 'phone',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 网址
```tsx
/**
* title: 网址
* desc: 加上 URL 验证 `x-validator=url`
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Input.URL',
'x-validator': 'url',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input.URL',
'x-validator': 'url',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -2,12 +2,14 @@ import React from 'react'
import { connect, mapProps, mapReadPretty } from '@formily/react'
import { Input as AntdInput } from 'antd'
import { InputProps, TextAreaProps } from 'antd/lib/input'
import { Descriptions } from '../descriptions'
import { LoadingOutlined } from '@ant-design/icons'
import { Display } from '../display';
import { DesignableBar } from './DesignableBar';
type ComposedInput = React.FC<InputProps> & {
TextArea?: React.FC<TextAreaProps>
URL?: React.FC<InputProps>
DesignableBar?: React.FC<any>
}
export const Input: ComposedInput = connect(
@ -26,10 +28,11 @@ export const Input: ComposedInput = connect(
),
}
}),
mapReadPretty(Descriptions.Input)
mapReadPretty(Display.Input)
)
Input.TextArea = connect(AntdInput.TextArea, mapReadPretty(Descriptions.TextArea))
Input.URL = connect(AntdInput, mapReadPretty(Descriptions.URL))
Input.DesignableBar = DesignableBar;
Input.TextArea = connect(AntdInput.TextArea, mapReadPretty(Display.TextArea))
Input.URL = connect(AntdInput, mapReadPretty(Display.URL))
export default Input

View File

@ -1,19 +0,0 @@
---
title: Kanban - 看板
nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
path: /client/blocks
---
# Kanban - 看板
## 代码演示
### 基本使用
### 设计器模式

View File

@ -0,0 +1,68 @@
---
title: Markdown 编辑器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Markdown 编辑器
## 代码演示
### Markdown 编辑器
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
name: 'name1',
'x-decorator': 'FormItem',
'x-component': 'Markdown',
'x-reactions': {
target: '*(read1,read2)',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read1: {
interface: 'string',
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Markdown',
},
read2: {
interface: 'string',
type: 'string',
title: `阅读模式(超出隐藏)`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Markdown',
'x-component-props': {
ellipsis: true,
},
},
},
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -2,7 +2,7 @@ import React from 'react';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { Input as AntdInput } from 'antd';
import { InputProps, TextAreaProps } from 'antd/lib/input';
import { Descriptions } from '../descriptions';
import { Display } from '../display';
import { LoadingOutlined } from '@ant-design/icons';
import micromark from 'micromark';
@ -27,7 +27,7 @@ export const Markdown = connect(
let value = (
<div dangerouslySetInnerHTML={{ __html: micromark(text || '') }} />
);
return <Descriptions.TextArea {...props} text={text} value={value} />;
return <Display.TextArea {...props} text={text} value={value} />;
}),
);

View File

@ -0,0 +1,130 @@
---
title: Menu - 菜单
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Menu - 菜单
需要 antd v4.16+ 支持,在此之前的 Menu.Item 不支持 Fragment 包裹。
## 代码演示
### 横向菜单
```tsx
/**
* title: 横向菜单
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
'x-component': 'Menu',
// 'x-decorator': 'Menu.Designable',
'x-component-props': {
mode: 'horizontal',
},
properties: {
item1: {
type: 'void',
title: `菜单1`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
item3: {
type: 'void',
title: '菜单组',
'x-component': 'Menu.SubMenu',
properties: {
item4: {
type: 'void',
title: `子菜单1`,
'x-component': 'Menu.Item',
},
}
},
},
}
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
### 竖向菜单
```tsx
/**
* title: 竖向菜单
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
menu1: {
type: 'void',
name: 'name1',
'x-component': 'Menu',
// 'x-decorator': 'Menu.Designable',
'x-component-props': {
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`,
'x-component': 'Menu.Item',
},
item2: {
type: 'void',
title: `菜单2`,
'x-component': 'Menu.Item',
},
},
},
},
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -0,0 +1,751 @@
import React, {
Children,
createContext,
useContext,
useEffect,
useState,
} from 'react';
import {
connect,
observer,
mapProps,
mapReadPretty,
useField,
useFieldSchema,
RecursionField,
Schema,
} from '@formily/react';
import {
Menu as AntdMenu,
MenuProps,
MenuItemProps,
SubMenuProps,
DividerProps,
Dropdown,
Modal,
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,
PlusOutlined,
LinkOutlined,
AppstoreAddOutlined,
EditOutlined,
DeleteOutlined,
ArrowRightOutlined,
SettingOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
} from '@ant-design/icons';
import './style.less';
import { Icon } from '../icon-picker';
import {
useHistory,
} from 'react-router-dom';
export type MenuType = React.FC<MenuProps & { hideSubMenu?: boolean }> & {
Item?: React.FC<MenuItemProps>;
SubMenu?: React.FC<SubMenuProps>;
Divider?: React.FC<DividerProps>;
Designable?: React.FC<any>;
AddNew?: React.FC<any>;
Link?: React.FC<MenuItemProps>;
Url?: React.FC<MenuItemProps & { url: string }>;
};
export function removeProperty(property: Schema) {
property.parent.removeProperty(property.name);
}
export function addPropertyBefore(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
});
}
export function addPropertyAfter(target, prop) {
Object.keys(target.parent.properties).forEach((name) => {
const property = target.parent.properties[name];
property.parent.removeProperty(property.name);
target.parent.addProperty(property.name, property.toJSON());
if (name === target.name) {
target.parent.addProperty(prop.name, prop);
}
});
}
export function useSchemaQuery(segments?: any[]) {
const context = useContext(DesignableSchemaContext);
const refresh = useContext(RefreshDesignableSchemaContext);
const fieldSchema = useFieldSchema();
const field = useField();
const getSchemaByPath = (path) => {
let s: Schema = context;
const names = [...path];
console.log('names', [...names], path, context)
// 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,
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();
},
};
}
const AddNewAction = () => {
const field = useField();
const segments = [...field.address.segments];
// add new 节点是后加的,并不在菜单树上,要通过父节点处理 add 逻辑
segments.pop();
const { appendChild } = useSchemaQuery(segments);
return (
<Dropdown
overlayStyle={{
minWidth: 150,
}}
trigger={['click']}
overlay={
<AntdMenu>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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 }}
>
<MenuOutlined />
</AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单组`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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',
},
},
});
});
}}
>
<GroupOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<LinkOutlined />
</AntdMenu.Item>
</AntdMenu>
}
>
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
</Dropdown>
);
};
const SettingAction = () => {
const field = useField();
const { schema, refresh, remove, insertBefore, insertAfter, appendChild } =
useSchemaQuery();
const text = schema['x-component'] === 'Menu.SubMenu' ? '当前菜单组' : '当前菜单';
return (
<div
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
className={'designable-action'}
>
<Dropdown
overlayStyle={{
minWidth: 150,
}}
trigger={['click']}
overlay={
<AntdMenu>
<AntdMenu.Item
onClick={() => {
FormDialog('编辑菜单', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="菜单名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name=".icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
{/* <FormDialog.Footer>
<span style={{ marginLeft: 4 }}></span>
</FormDialog.Footer> */}
</FormLayout>
);
})
.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();
});
}}
>
<EditOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<ArrowRightOutlined />
</AntdMenu.Item>
<AntdMenu.Divider />
<AntdMenu.SubMenu icon={<ArrowUpOutlined />} title={`${text}`}>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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 }}
>
<MenuOutlined />
</AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单组`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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',
},
},
});
});
}}
>
<GroupOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<LinkOutlined />
</AntdMenu.Item>
</AntdMenu.SubMenu>
<AntdMenu.SubMenu icon={<ArrowDownOutlined />} title={`${text}`}>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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 }}
>
<MenuOutlined />
</AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单组`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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',
},
},
}),
);
});
}}
>
<GroupOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<LinkOutlined />
</AntdMenu.Item>
</AntdMenu.SubMenu>
{schema['x-component'] === 'Menu.SubMenu' ? (
<AntdMenu.SubMenu icon={<ArrowRightOutlined />} title={`${text}`}>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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 }}
>
<MenuOutlined />
</AntdMenu.Item>
<AntdMenu.Item
onClick={() => {
FormDialog(`新建菜单组`, () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.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',
},
},
}),
);
});
}}
>
<GroupOutlined />
</AntdMenu.Item>
<AntdMenu.Item>
<LinkOutlined />
</AntdMenu.Item>
</AntdMenu.SubMenu>
) : null}
<AntdMenu.Divider />
<AntdMenu.Item
onClick={() => {
Modal.confirm({
title: '删除菜单',
content: '确认删除此菜单项吗?',
onOk: remove,
});
}}
>
<DeleteOutlined />
</AntdMenu.Item>
</AntdMenu>
}
>
<MenuOutlined />
</Dropdown>
</div>
);
};
export const Menu: MenuType = observer((props) => {
const { hideSubMenu, ...others } = props;
// const schema = useFieldSchema();
// console.log({ schema }, 'Menu');
return <AntdMenu {...others} />;
});
Menu.AddNew = observer((props) => {
const field = useField();
const { schema } = useSchemaQuery();
console.log('AddNew', field.address.segments);
return (
<AntdMenu.ItemGroup
key="add"
className={'menu-add'}
title={<AddNewAction />}
></AntdMenu.ItemGroup>
);
});
Menu.Url = observer((props) => {
const field = useField();
const { schema, refresh } = useSchemaQuery();
return (
<AntdMenu.Item
{...props}
schema={schema}
// @ts-ignore
eventKey={schema.name}
key={schema.name}
onClick={(e) => {
window.open(props.url);
}}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{field.title} <SettingAction />
</AntdMenu.Item>
);
});
Menu.Link = observer((props) => {
const history = useHistory();
const field = useField();
const { schema, refresh } = useSchemaQuery();
return (
<AntdMenu.Item
{...props}
schema={schema}
// @ts-ignore
eventKey={schema.name}
key={schema.name}
onClick={(e) => {
history.push(schema.name as string);
}}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{field.title} <SettingAction />
</AntdMenu.Item>
);
});
Menu.Item = observer((props) => {
const field = useField();
const { schema, refresh } = useSchemaQuery();
return (
<AntdMenu.Item
{...props}
schema={schema}
// @ts-ignore
eventKey={schema.name}
key={schema.name}
icon={props.icon ? <Icon type={props.icon as string} /> : undefined}
>
{field.title} <SettingAction />
</AntdMenu.Item>
);
});
// Menu
Menu.Designable = (props) => {
return props.children;
};
Menu.SubMenu = observer((props) => {
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 <Menu.Item {...props} />;
}
return (
<AntdMenu.SubMenu
{...props}
// @ts-ignore
schema={schema}
title={
<>
{schema.title} <SettingAction />
</>
}
eventKey={schema.name}
key={schema.name}
icon={
props.icon ? (
<RecursionField
name={schema.name}
schema={
new Schema({
type: 'string',
'x-read-pretty': true,
default: props.icon,
'x-component': 'IconPicker',
})
}
onlyRenderProperties
/>
) : undefined
}
/>
);
});
Menu.Divider = observer(AntdMenu.Divider);
export default Menu;

View File

@ -0,0 +1,34 @@
.designable-action {
position: absolute;
right: 16px;
display: none;
z-index: 10;
}
.ant-menu-dark.ant-menu-horizontal {
width: 100%;
}
.ant-menu-dark .ant-menu-item-active .designable-action {
background-color: #001529;
display: inline-block;
}
.ant-menu-dark .ant-menu-item-active:hover .designable-action,
.ant-menu-dark .ant-menu-item-active.ant-menu-item-selected .designable-action {
background-color: #1890ff;
display: inline-block;
}
.ant-menu-light .ant-menu-submenu-active > .ant-menu-submenu-title .designable-action,
.ant-menu-light .ant-menu-item-active .designable-action {
background-color: #fff;
display: inline-block;
}
.ant-menu-sub.ant-menu-inline .ant-menu-submenu-active > .ant-menu-submenu-title .designable-action,
.ant-menu-sub.ant-menu-inline .ant-menu-item-active .designable-action {
background-color: #fafafa;
}
.ant-menu-light .ant-menu-item-selected .designable-action {
background-color: #e6f7ff !important;
}

View File

@ -0,0 +1,54 @@
---
title: Password - 密码
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Password - 密码
## 代码演示
### 密码框
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Password',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -0,0 +1,72 @@
---
title: Radio - 单选框
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Radio - 单选框
## 代码演示
### 单选框
```tsx
/**
* title: 单选框
*/
import React from 'react';
import { SchemaBlock } from '../';
const options = [
{
label: '选项1',
value: 1,
color: 'red',
},
{
label: '选项2',
value: 2,
color: 'blue',
},
];
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
enum: options,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -0,0 +1,273 @@
---
title: Select - 选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Select - 选择器
## 基本用法
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const options = [
{
label: '选项1',
value: 1,
color: 'red',
},
{
label: '选项2',
value: 2,
color: 'blue',
},
];
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
name: 'name1',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
enum: options,
name: 'name2',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema}/>
);
};
```
## 多选
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const options = [
{
label: '选项1',
value: 1,
color: 'red',
},
{
label: '选项2',
value: 2,
color: 'blue',
},
];
const schema = {
type: 'object',
properties: {
input: {
interface: 'string',
type: 'string',
title: `编辑模式`,
name: 'name1',
enum: options,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
mode: 'tags',
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'string',
type: 'string',
title: `阅读模式`,
enum: options,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
mode: 'tags',
},
},
}
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```
## 分组
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const dataSource = [
{
label: 'Manager',
children: [
{value: 'jack', label: 'Jack'},
],
},
{
label: 'Engineer',
children: [
{value: 'lucy', label: 'Lucy'},
],
},
];
const schema = {
type: 'object',
properties: {
input: {
interface: 'select',
type: 'number',
title: `编辑模式`,
enum: dataSource,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: 'please enter',
},
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'select',
type: 'number',
title: `阅读模式`,
required: true,
enum: dataSource,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
placeholder: 'please enter',
},
}
}
};
export default () => {
return (
<SchemaBlock schema={schema}/>
);
};
```
## 值为对象
```tsx
import React from 'react';
import { SchemaBlock } from '../';
const dataSource = [
{
id: 1,
title: '标题1',
},
{
id: 2,
title: '标题2',
},
{
id: 3,
title: '标题3',
},
];
const schema = {
type: 'object',
properties: {
input: {
interface: 'select',
type: 'string',
title: `编辑模式`,
enum: dataSource,
'x-decorator': 'FormItem',
'x-component': 'Select.Object',
'x-component-props': {
placeholder: 'please enter',
labelField: 'title',
valueField: 'id',
labelInValue: true,
},
'x-reactions': {
target: 'name2',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
interface: 'select',
type: 'string',
title: `阅读模式`,
required: true,
enum: dataSource,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Select.Object',
'x-component-props': {
placeholder: 'please enter',
labelField: 'title',
valueField: 'id',
labelInValue: true,
},
}
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -7,7 +7,7 @@ import { SelectProps } from 'antd/lib/select';
import { isArr, isValid } from '@formily/shared';
import { Field } from '@formily/core/esm/models/Field';
import { useAction } from '../../state';
import { Descriptions } from '../descriptions';
import { Display } from '../display';
import { useRequest } from 'ahooks';
import uniq from 'lodash/uniq';
@ -179,7 +179,7 @@ Select.Object = connect(
};
},
),
mapReadPretty(Descriptions.ObjectSelect),
mapReadPretty(Display.ObjectSelect),
);
export default Select;

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
export const ColumnDesignableBar = (props) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</>
);
};

View File

@ -0,0 +1,53 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import { useField, observer, RecursionField, Schema } from '@formily/react';
import { Dropdown, Menu } from 'antd';
import { useSchemaQuery } from '../grid';
import {
MenuOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
} from '@ant-design/icons';
export const DesignableBar = (props) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<>
<Menu.Item
onClick={() => {
addBlock(
{},
{
insertBefore: true,
},
);
setActive(false);
}}
icon={<ArrowUpOutlined />}
>
</Menu.Item>
<Menu.Item
onClick={() => {
addBlock();
setActive(false);
}}
icon={<ArrowDownOutlined />}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={() => {
removeBlock();
setActive(false);
}}
icon={<DeleteOutlined />}
>
</Menu.Item>
</>
);
};

View File

@ -1,57 +0,0 @@
import React from 'react';
import { TableBlock } from '@nocobase/client';
export default () => {
return (
<div>
<TableBlock
resource={'users'}
initialValues={[
{ input: '文本1', input2: 'a' },
{ input: '文本2', input2: 'b' },
{ input: '文本3', input2: 'c' },
]}
details={[
{
name: 'tab1',
title: 'Tab1',
blocks: [],
},
{
name: 'tab2',
title: 'Tab2',
blocks: [],
},
]}
fields={[
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'input',
required: true,
default: 'abcdefg',
'x-decorator': 'Column',
'x-component': 'Input',
'x-component-props': {
placeholder: 'please enter',
},
},
{
interface: 'string',
type: 'string',
title: `单行文本2`,
name: 'input2',
required: true,
default: 'abcdefg',
'x-decorator': 'Column',
'x-component': 'Input',
'x-component-props': {
placeholder: 'please enter',
},
},
]}
/>
</div>
);
};

View File

@ -4,18 +4,106 @@ nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Table - 表格
## 代码演示
```tsx
import React from 'react';
import { SchemaBlock } from '../';
import { uid } from '@formily/shared';
### 基本使用
<code src="./demos/demo1.tsx"/>
### 设计器模式
const schema = {
name: `t_${uid()}`,
type: 'array',
'x-component': 'Table',
default: [{ field1: 'a1', field2: 'b1' }, { field1: 'a2', field2: 'b2' }],
'x-component-props': {},
properties: {
[`a_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
action1: {
type: 'object',
title: '按钮1',
'x-component': 'Table.BulkAction.Popover',
'x-component-props': {},
properties: {},
},
action2: {
type: 'void',
title: '按钮2',
'x-component': 'Table.BulkAction',
'x-component-props': {},
},
},
},
[`a_${uid()}`]: {
type: 'void',
'x-component': 'Table.ActionBar',
'x-component-props': {
align: 'bottom',
},
properties: {
pagination: {
type: 'void',
'x-component': 'Table.Pagination',
'x-component-props': {
defaultCurrent: 1,
total: 50,
},
},
},
},
[`c_${uid()}`]: {
type: 'void',
title: '字段1',
'x-component': 'Table.Column',
'x-component-props': {
title: 'z1',
width: 100,
// DesignableBar: 'Table.Column.DesignableBar',
},
'x-designable-bar': 'Table.Column.DesignableBar',
properties: {
field1: {
type: 'string',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
[`c_${uid()}`]: {
type: 'void',
title: '字段2',
'x-component': 'Table.Column',
properties: {
field2: {
type: 'string',
title: '字段2',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,100 +1,108 @@
import React from 'react';
import { Table, Tabs, Card, Button } from 'antd';
import { Drawer } from '../../components/Drawer';
import { Resource, ResourceOptions } from '../../resource';
import { useRequest } from 'ahooks';
import { Field } from '../../';
import {
useFieldSchema,
Schema,
observer,
RecursionField,
useField,
} from '@formily/react';
import { Table as AntdTable } from 'antd';
import { DesignableBar } from './DesignableBar';
import { ColumnDesignableBar } from './ColumnDesignableBar';
export function fields2columns(fields: any[]) {
if (!Array.isArray(fields)) {
return [];
function useTableColumns(props) {
const schema = useFieldSchema();
const { dataSource } = props;
function findColumns(schema: Schema): Schema[] {
return schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Table.Column') {
return [...columns, current];
}
return [...columns, ...findColumns(current)];
}, []);
}
return fields.map((field) => {
field.dataIndex = field.name.split('.');
field.render = (value, data, index) => {
return (
<Field
schema={{
'x-decorator': 'Column',
'x-read-pretty': true,
...field,
}}
data={{
...data,
__index__: index,
}}
/>
);
return findColumns(schema).map((item) => {
const columnProps = item['x-component-props'] || {};
return {
title: item.title,
dataIndex: item.name,
...columnProps,
render(value, record) {
console.log({ item });
const index = dataSource.indexOf(record);
item.mapProperties((s) => {
s['title'] = undefined;
s['x-read-pretty'] = true;
return s;
})
return (
<RecursionField schema={item} name={index} onlyRenderProperties />
);
},
};
return field;
});
}
export interface DetailOptions {
name: string;
title: string;
blocks?: any;
}
function useTableActionBars() {
const schema = useFieldSchema();
export interface TableBlockProps {
fields?: any;
details?: Array<DetailOptions>;
initialValues?: any;
resource: string | Resource | ResourceOptions;
[key: string]: any;
}
function findActionBars(schema: Schema) {
const actionBars = {
top: [],
bottom: [],
};
return schema.reduceProperties((bars, current) => {
if (current['x-component'] === 'Table.ActionBar') {
const align = current['x-component-props']?.['align'] || 'top';
bars[align].push(current);
export function TableBlock(props: TableBlockProps) {
const { fields = [], details = [], initialValues } = props;
const columns = fields2columns(fields);
const resource = Resource.make(props.resource);
const { data, loading } = useRequest(() => {
if (initialValues) {
return Promise.resolve({
list: initialValues,
return bars;
}
const nested = findActionBars(current);
Object.keys(nested).forEach((align) => {
bars[align].push(...nested[align]);
});
}
return resource.list();
});
console.log({ columns, data });
return (
<Table
loading={loading}
dataSource={data?.list}
columns={columns}
onRow={(data, index) => {
return {
onClick(e) {
Drawer.open({
bodyStyle: {
padding: 0,
},
content: ({ resolve }) => {
const tabList = details.map((detail) => {
return {
key: detail.name,
tab: detail.title,
};
});
return (
<div>
<Card
bordered={false}
title={'详情页面'}
tabList={tabList}
tabProps={{ size: 'small' }}
>
aaa
</Card>
</div>
);
},
});
},
};
}}
/>
);
return bars;
}, actionBars);
}
return findActionBars(schema);
}
export default TableBlock;
function useTableDetails() {
const schema = useFieldSchema();
function findDetails(schema: Schema) {
return schema.reduceProperties((items, current) => {
if (current['x-component'] === 'Table.Details') {
return [...items, current];
}
return [...items, ...findDetails(current)];
}, []);
}
return findDetails(schema);
}
export const Table: any = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>();
const dataSource = field.value;
const columns = useTableColumns({ dataSource });
const actionBars = useTableActionBars();
const details = useTableDetails();
console.log({ columns, actionBars, details });
return (
<>
<AntdTable columns={columns} dataSource={dataSource} />
</>
);
});
Table.Column = () => null;
Table.Column.DesignableBar = ColumnDesignableBar;
Table.DesignableBar = DesignableBar;

View File

@ -0,0 +1,74 @@
---
title: Tabs - 标签页
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Tabs - 标签页
## 代码演示
### 基本使用
```tsx
/**
* title: 基本使用
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
tabs: {
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',
},
},
},
},
},
}
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -0,0 +1,6 @@
import { observer, connect } from '@formily/react';
import React from 'react';
import { FormTab } from '@formily/antd';
import '@formily/antd/lib/form-tab/style';
export const Tabs = FormTab;

View File

@ -0,0 +1,57 @@
---
title: TimePicker - 时间选择器
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# TimePicker - 时间选择器
## 代码演示
### 日期选择器
```tsx
/**
* title: 日期选择器
*/
import React from 'react';
import { SchemaBlock } from '../';
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'TimePicker',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'TimePicker',
},
}
}
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -5,7 +5,7 @@ import {
TimePickerProps as AntdTimePickerProps,
TimeRangePickerProps,
} from 'antd/lib/time-picker'
import { Descriptions } from '../descriptions'
import { Display } from '../display'
import { formatMomentValue, momentable } from '@formily/antd/esm/__builtins__'
type ComposedTimePicker = React.FC<AntdTimePickerProps> & {
@ -32,13 +32,13 @@ const mapTimeFormat = function () {
export const TimePicker: ComposedTimePicker = connect(
AntdTimePicker,
mapProps(mapTimeFormat()),
mapReadPretty(Descriptions.TimePicker)
mapReadPretty(Display.TimePicker)
)
TimePicker.RangePicker = connect(
AntdTimePicker.RangePicker,
mapProps(mapTimeFormat()),
mapReadPretty(Descriptions.TimeRangePicker)
mapReadPretty(Display.TimeRangePicker)
)
export default TimePicker

View File

@ -0,0 +1,75 @@
---
title: Upload - 上传
nav:
title: 组件
path: /client
group:
order: 1
title: 区块 - Blocks
path: /client/blocks
---
# Upload - 上传
## 代码演示
### 上传
```tsx
import React from 'react';
import { Button } from 'antd'
import { UploadOutlined, InboxOutlined } from '@ant-design/icons'
import Upload from './';
import { SchemaBlock, registerComponents } from '../';
const NormalUpload = (props) => {
return (
<Upload
{...props}
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
headers={{
authorization: 'authorization-text',
}}
>
<Button icon={<UploadOutlined />}>上传文件</Button>
</Upload>
)
}
registerComponents({
NormalUpload,
});
const schema = {
type: 'object',
properties: {
input: {
type: 'string',
title: `编辑模式`,
'x-decorator': 'FormItem',
'x-component': 'NormalUpload',
'x-reactions': {
target: 'read',
fulfill: {
state: {
value: '{{$self.value}}',
},
},
},
},
read: {
type: 'string',
title: `阅读模式`,
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'NormalUpload',
},
}
};
export default () => {
return (
<SchemaBlock schema={schema} />
);
};
```

View File

@ -1,12 +0,0 @@
---
title: ActionBar - 操作栏
nav:
title: 组件
path: /client
group:
order: 3
title: 其他
path: /client/others
---
# ActionBar - 操作栏

View File

@ -1,17 +1,5 @@
import React from 'react';
import {
createFromIconfontCN,
UserOutlined,
TeamOutlined,
DatabaseOutlined,
DashboardOutlined,
SettingOutlined,
TableOutlined,
MenuOutlined,
HistoryOutlined,
NotificationOutlined,
} from '@ant-design/icons';
import { createFromIconfontCN } from '@ant-design/icons';
import * as antIcons from '@ant-design/icons';
export const IconFont = createFromIconfontCN({
@ -32,29 +20,17 @@ export function hasIcon(type: string) {
}
export function registerIcons(components) {
Object.keys(components).forEach(type => {
Object.keys(components).forEach((type) => {
registerIcon(type, components[type]);
});
}
Object.keys(antIcons).forEach(name => {
Object.keys(antIcons).forEach((name) => {
if (name.endsWith('Outlined')) {
registerIcon(name, antIcons[name]);
}
});
registerIcons({
// HistoryOutlined,
// MenuOutlined,
// TableOutlined,
// SettingOutlined,
// TeamOutlined,
// UserOutlined,
// DatabaseOutlined,
// DashboardOutlined,
// NotificationOutlined,
});
interface IconProps {
type: string;
[key: string]: any;

View File

@ -1,155 +0,0 @@
---
title: MenuEditor - 菜单编辑器
nav:
title: 组件
path: /client
group:
order: 3
title: 其他
path: /client/others
---
# MenuEditor - 菜单编辑器
基于 Ant Design Menu 组件实现的菜单编辑器,考虑到可视化编辑的体验,目前只支持`竖向菜单`和`混合菜单`两种场景。
## 代码演示
### 竖向菜单
```tsx
import React from 'react';
import MenuEditor from './';
const items = [
{
icon: 'AimOutlined',
name: 'name1',
title: '菜单组1',
children: [
{
icon: 'CiOutlined',
name: 'name3',
title: '子菜单1',
},
{
icon: 'CiOutlined',
name: 'name4',
title: '子菜单2',
},
{
icon: 'CiOutlined',
name: 'name5',
title: '子菜单组1',
children: [
{
icon: 'CiOutlined',
name: 'name6',
title: '子菜单6',
},
{
icon: 'CiOutlined',
name: 'name7',
title: '子菜单7',
},
{
icon: 'CiOutlined',
name: 'name8',
title: '子菜单8',
},
],
},
],
},
{
icon: 'CiOutlined',
name: 'name2',
title: '菜单1',
},
];
export default () => {
return (
<MenuEditor style={{ width: 256 }} items={items} mode={'vertical'}/>
)
}
```
### 混合菜单
```tsx
import React from 'react';
import MenuEditor from './';
const items = [
{
name: 'name1',
title: '菜单组1',
children: [
{
name: 'name3',
title: '子菜单1',
},
{
name: 'name4',
title: '子菜单2',
},
{
name: 'name5',
title: '子菜单组1',
children: [
{
name: 'name6',
title: '子菜单6',
},
{
name: 'name7',
title: '子菜单7',
},
{
name: 'name8',
title: '子菜单8',
},
],
},
],
},
{
name: 'name9',
title: '菜单组2',
children: [
{
name: 'name10',
title: '子菜单10',
},
{
name: 'name11',
title: '子菜单11',
},
{
name: 'name12',
title: '子菜单12',
},
{
name: 'name13',
title: '子菜单13',
},
],
},
{
name: 'name2',
title: '菜单1',
},
];
export default () => {
const [key, setKey] = React.useState('name1');
return (
<MenuEditor onSelect={(info) => {
setKey(info.key);
}} defaultValue={key} items={items} mode={'mix'}>
<div style={{ padding: 24 }}>这是正文 {key}</div>
</MenuEditor>
)
}
```

View File

@ -1,385 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Button, Dropdown, Modal } from 'antd';
import {
AppstoreOutlined,
MenuOutlined,
SettingOutlined,
PlusOutlined,
DashboardOutlined,
LinkOutlined,
GroupOutlined,
EditOutlined,
ArrowRightOutlined,
DeleteOutlined,
} from '@ant-design/icons';
import './style.less';
import Icon from '../Icon';
import { FormDialog, FormLayout } from '@formily/antd';
import { SchemaField } from '../../fields';
import { useDynamicList } from 'ahooks';
const { SubMenu } = Menu;
export function generateName(): string {
return `${Math.random()
.toString(36)
.replace('0.', '')
.slice(-4)
.padStart(4, '0')}`;
}
export function AddNewAction(props) {
const { data } = props;
const insert = (values: MenuItemOptions) => {
props.insert && props.insert({ ...values, name: generateName() });
};
const prefix = data ? `${data.title}后面` : '';
const menu = (
<Menu>
<Menu.Item
key="page"
onClick={() => {
FormDialog(`${prefix}新建菜单`, () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaField.String
name="title"
required
title="名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.open({})
.then(insert);
}}
>
<MenuOutlined />
</Menu.Item>
<Menu.Item
key="group"
onClick={() => {
FormDialog(`${prefix}新建分组`, () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaField.String
name="title"
required
title="分组名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
</FormLayout>
);
})
.open({})
.then((data) =>
insert({ ...data, name: generateName(), children: [] }),
);
}}
>
<GroupOutlined />
</Menu.Item>
<Menu.Item
key="link"
onClick={() => {
FormDialog(`${prefix}添加链接`, () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaField.String
name="title"
required
title="标题"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="link"
title="链接"
x-decorator="FormItem"
x-component="Input"
/>
</SchemaField>
</FormLayout>
);
})
.open({})
.then(insert);
}}
>
<LinkOutlined />
</Menu.Item>
</Menu>
);
return (
<Dropdown
trigger={['click']}
overlay={menu}
>
{props.children || <PlusOutlined />}
</Dropdown>
);
}
export function SettingAction(props) {
const { initialValues, replace, remove } = props;
const menu = (
<Menu>
<Menu.Item
onClick={() => {
console.log({ initialValues });
FormDialog('编辑菜单', () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaField.String
name="title"
required
title="菜单名称"
x-decorator="FormItem"
x-component="Input"
/>
<SchemaField.String
name="icon"
title="图标"
x-decorator="FormItem"
x-component="IconPicker"
/>
</SchemaField>
{/* <FormDialog.Footer>
<span style={{ marginLeft: 4 }}></span>
</FormDialog.Footer> */}
</FormLayout>
);
})
.open({
initialValues,
})
.then(replace);
}}
key="edit"
>
<EditOutlined />
</Menu.Item>
<Menu.Item key="move">
<ArrowRightOutlined />
</Menu.Item>
<Menu.Item
onClick={() => {
Modal.confirm({
title: '确定菜单',
content: '确认删除此菜单项吗?',
onOk: remove,
});
}}
key="delete"
>
<DeleteOutlined />
</Menu.Item>
</Menu>
);
return (
<Dropdown
trigger={['click']}
overlay={menu}
>
<MenuOutlined />
</Dropdown>
);
}
export interface MenuItemOptions {
name: string;
title: string;
icon: string;
children?: MenuItemOptions[];
[key: string]: any;
}
export interface MenuProps {
mode?: 'vertical' | 'mix';
defaultValue?: string;
items?: MenuItemOptions[];
children?: any;
[key: string]: any;
}
export function VerticalMenu(props: MenuProps) {
const { items, style, onSelect } = props;
const { list, remove, replace, insert, push, resetList } =
useDynamicList(items);
useEffect(() => {
resetList(items);
}, [items]);
const renderItems = (items: MenuItemOptions[]) => {
return items.map((item, index) => {
if (item.children) {
return (
<SubMenu
key={item.name}
title={
<span>
{item.icon && <Icon type={item.icon} />}
{item.title}{' '}
<span
onClick={(e) => {
e.stopPropagation();
}}
className={'menu-icons'}
>
<SettingAction
data={item}
initialValues={item}
remove={() => remove(index)}
replace={(data: MenuItemOptions) => replace(index, data)}
/>
<AddNewAction
data={item}
insert={(data: MenuItemOptions) => insert(index + 1, data)}
/>
</span>
</span>
}
>
{renderItems(item.children)}
</SubMenu>
);
}
return (
<Menu.Item
key={item.name}
icon={item.icon && <Icon type={item.icon} />}
>
{item.title}{' '}
<span
onClick={(e) => {
e.stopPropagation();
}}
className={'menu-icons'}
>
<SettingAction
data={item}
initialValues={item}
remove={() => remove(index)}
replace={(data: MenuItemOptions) => replace(index, data)}
/>
<AddNewAction
data={item}
insert={(data: MenuItemOptions) => insert(index + 1, data)}
/>
</span>
</Menu.Item>
);
});
};
return (
<Menu onSelect={onSelect} style={style} mode={'inline'}>
{renderItems(list)}
<Menu.ItemGroup
key="add"
className={'menu-add'}
title={
<AddNewAction insert={(data: MenuItemOptions) => push(data)}>
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
</AddNewAction>
}
></Menu.ItemGroup>
</Menu>
);
}
export function MixMenu(props: MenuProps) {
const { items, defaultValue, children, onSelect } = props;
const { list, insert, replace, remove, resetList } = useDynamicList(items);
const [value, setValue] = useState<any>(defaultValue);
const currentItem = list.find((item) => item.name === value);
useEffect(() => {
resetList(items);
}, [items]);
console.log({ list, currentItem });
return (
<Layout>
<Layout.Header>
<Menu
onSelect={(info) => {
setValue(info.key);
onSelect && onSelect(info);
}}
defaultSelectedKeys={[value]}
theme={'dark'}
mode={'horizontal'}
>
{list.map((item, index) => (
<Menu.Item
key={item.name}
icon={item.icon && <Icon type={item.icon} />}
>
{item.title}{' '}
<span
onClick={(e) => {
e.stopPropagation();
}}
className={'menu-icons'}
>
<SettingAction
data={item}
initialValues={item}
remove={() => remove(index)}
replace={(data: MenuItemOptions) => replace(index, data)}
/>
<AddNewAction
data={item}
insert={(data: MenuItemOptions) => insert(index + 1, data)}
/>
</span>
</Menu.Item>
))}
</Menu>
</Layout.Header>
<Layout>
{currentItem && currentItem.children && (
<Layout.Sider width={256}>
<VerticalMenu onSelect={onSelect} items={currentItem.children} />
</Layout.Sider>
)}
<Layout.Content>{children}</Layout.Content>
</Layout>
</Layout>
);
}
export default (props: MenuProps) => {
return props.mode === 'vertical' ? (
<VerticalMenu {...props} />
) : (
<MixMenu {...props} />
);
};

View File

@ -1,58 +0,0 @@
.ant-layout-header {
line-height: 48px;
height: 48px;
}
.ant-menu-item {
.menu-icons {
margin-left: 10px;
opacity: 0;
// position: absolute;
}
&:hover {
.menu-icons {
opacity: 1;
}
}
&.menu-add {
background-color: transparent !important;
&::after {
display: none;
}
}
}
.ant-menu-inline {
.menu-icons {
position: absolute;
right: 10px;
}
}
.ant-menu-submenu-title {
.menu-icons {
position: absolute;
background-color: #fff;
display: none;
z-index: 10;
}
&:hover {
.menu-icons {
display: inline-block;
}
}
}
.ant-menu-horizontal {
.menu-icons {
position: absolute;
right: 0;
background: #1890ff;
padding-left: 10px;
height: auto;
line-height: 1rem;
top: 50%;
transform: translateY(-50%);
transition: all 0.3s ease-in;
}
}

View File

@ -0,0 +1,446 @@
export default {
type: 'object',
properties: {
grid: {
type: 'void',
'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-component-props': {
title: 'block11',
},
properties: {
form1: {
type: 'void',
name: 'item3',
'x-decorator': 'Card',
'x-component': 'Form',
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-component-props': {
title: 'block11',
},
properties: {
field1: {
type: 'string',
required: true,
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
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',
},
properties: {
field2: {
type: 'string',
required: true,
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block211',
},
properties: {
field3: {
type: 'string',
required: true,
title: '字段3',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
properties: {
field4: {
type: 'string',
required: true,
title: '字段4',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block311',
},
properties: {
field5: {
type: 'string',
required: true,
title: '字段5',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
},
},
},
},
},
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',
},
properties: {
form2: {
type: 'void',
name: 'item3',
'x-decorator': 'Card',
'x-component': 'Form',
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-component-props': {
title: 'block11',
},
properties: {
field1: {
type: 'string',
required: true,
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
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',
},
properties: {
field2: {
type: 'string',
required: true,
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block211',
},
properties: {
field3: {
type: 'string',
required: true,
title: '字段3',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
properties: {
field4: {
type: 'string',
required: true,
title: '字段4',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block311',
},
properties: {
field5: {
type: 'string',
required: true,
title: '字段5',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
},
},
},
},
},
},
},
},
},
},
}

View File

@ -0,0 +1,38 @@
import { uid } from '@formily/shared';
export default {
type: 'object',
properties: {
[`g_${uid()}`]: {
type: 'void',
'x-component': 'Grid',
properties: {
[`gr_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Row',
properties: {
[`gc_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 1,
},
properties: {
[`gb_${uid()}`]: {
type: 'void',
'x-component': 'Grid.Block',
properties: {
[`gbn_${uid()}`]: {
type: 'void',
'x-component': 'AddNew',
},
},
},
},
},
},
},
},
},
},
}

View File

@ -0,0 +1,63 @@
export default {
type: 'object',
properties: {
input: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '邮箱或用户名',
style: {
// width: 240,
},
},
},
textarea: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
style: {
// width: 240,
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
width: '100%',
},
},
title: '登录',
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/register',
},
title: '注册账号',
},
},
},
},
}

View File

@ -0,0 +1,196 @@
export default {
type: 'void',
name: 'item3',
'x-decorator': 'Card',
'x-component': 'Form',
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-component-props': {
title: 'block11',
},
properties: {
field1: {
type: 'string',
required: true,
title: '字段1',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
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',
},
properties: {
field2: {
type: 'string',
required: true,
title: '字段2',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block211',
},
properties: {
field3: {
type: 'string',
required: true,
title: '字段3',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
col22: {
type: 'void',
'x-component': 'Grid.Col',
'x-component-props': {
size: 2 / 3,
isLast: true,
},
properties: {
block221: {
type: 'void',
'x-component': 'Grid.Block',
'x-component-props': {
title: 'block221',
},
properties: {
field4: {
type: 'string',
required: true,
title: '字段4',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
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-component-props': {
title: 'block311',
},
properties: {
field5: {
type: 'string',
required: true,
title: '字段5',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
},
},
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Space',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '提交',
},
reset: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
// block: true,
useAction: '{{ useLogin }}',
style: {
// width: '100%',
},
},
title: '重置',
},
},
},
},
}

View File

@ -0,0 +1,17 @@
export default {
type: 'object',
properties: {
input: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '邮箱或用户名',
style: {
// width: 240,
},
},
},
},
}

View File

@ -0,0 +1,92 @@
export default {
name: 'arr',
type: 'array',
'x-component': 'Table',
default: [{ field1: 'a', field2: 'b' }, { field1: 'a', field2: 'b' }],
'x-component-props': {},
properties: {
actionBar1: {
type: 'void',
'x-component': 'Table.ActionBar',
properties: {
action1: {
type: 'object',
title: '按钮1',
'x-component': 'Table.Action.Popover',
'x-component-props': {
},
properties: {
},
},
action2: {
type: 'void',
title: '按钮2',
'x-component': 'Table.Action.Button',
'x-component-props': {
label: '按钮2',
},
},
},
},
actionBar2: {
type: 'void',
'x-component': 'Table.ActionBar',
'x-component-props': {
align: 'bottom',
},
properties: {
pagination: {
type: 'void',
'x-component': 'Table.Pagination',
'x-component-props': {
defaultCurrent: 1,
total: 50,
},
},
},
},
details: {
type: 'void',
'x-component': 'Table.Details',
properties: {},
},
col1: {
type: 'void',
title: '字段1',
'x-component': 'Table.Column',
'x-component-props': {
title: 'z1',
width: 100,
},
properties: {
field1: {
type: 'string',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
col2: {
type: 'void',
title: '字段2',
'x-component': 'Table.Column',
properties: {
field2: {
type: 'string',
title: '字段2',
required: true,
'x-decorator-props': {
feedbackLayout: 'popover',
},
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
},
},
}

View File

@ -0,0 +1,63 @@
export default {
type: 'object',
properties: {
input: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '邮箱或用户名',
style: {
// width: 240,
},
},
},
textarea: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
style: {
// width: 240,
},
},
},
actions: {
type: 'void',
// 'x-decorator': 'Div',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
width: '100%',
},
},
title: '登录',
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/register',
},
title: '注册账号',
},
},
},
},
};

Some files were not shown because too many files have changed in this diff Show More