This commit is contained in:
chenos 2021-06-02 11:09:01 +08:00
parent cfc04814c6
commit 5e0277d451
30 changed files with 1553 additions and 312 deletions

View File

@ -24,6 +24,10 @@
"ahooks": "^2.10.2",
"axios": "^0.21.1",
"lodash": "^4.17.21",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dnd-preview": "^6.0.2",
"react-dnd-touch-backend": "^14.0.0",
"react-helmet": "^6.1.0",
"react-hooks-global-state": "^1.0.1",
"umi-request": "^1.3.5"

View File

@ -1,34 +0,0 @@
---
title: Block - 区块
nav:
title: 组件
path: /client
group:
order: 2
title: Blocks
path: /client/blocks
---
# Block - 区块
```tsx
import React from 'react';
import BlockEditor from './';
const items = [
{
type: 'page',
name: 'block1',
},
{
type: 'page',
name: 'block2',
}
]
export default () => {
return (
<BlockEditor items={items} />
)
}
```

View File

@ -1,94 +0,0 @@
import React from 'react';
import { Dropdown, Menu, Card, Button, Popover } from 'antd';
import { useDynamicList } from 'ahooks';
import { MenuOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import './style.less';
import { FormDialog, FormLayout } from '@formily/antd';
import { SchemaField } from '../../fields';
export function AddNewAction(props) {
const { insert, children } = props;
const menu = (
<Menu>
<Menu.Item
key="page"
onClick={() => {
FormDialog(`新建区块`, () => {
return (
<FormLayout labelCol={6} wrapperCol={10}>
<SchemaField>
<SchemaField.String
name="name"
required
title="数据源"
x-decorator="FormItem"
x-component="Input"
/>
</SchemaField>
</FormLayout>
);
})
.open({})
.then(insert);
}}
>
<MenuOutlined />
</Menu.Item>
</Menu>
);
return <Dropdown overlay={menu}>{children || <PlusOutlined />}</Dropdown>;
}
export function SettingAction(props) {
const { remove } = props;
const menu = (
<Menu>
<Menu.Item onClick={() => remove()} key="delete">
<DeleteOutlined />
</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu}>
<MenuOutlined />
</Dropdown>
);
}
export function BlockItem(props) {
const { remove, insert, replace } = props;
return (
<div className={'block-item'}>
<div className={'block-item-actions'}>
<SettingAction replace={replace} remove={remove} />
<AddNewAction insert={insert} />
</div>
<div className={'block-item-body'}>
<Card bordered={false} title={'这是测试区块'}>
</Card>
</div>
</div>
);
}
export default (props) => {
const { list, push, remove, replace, insert } = useDynamicList(props.items);
return (
<div className={'block-list'}>
{list.map((item, index) => (
<BlockItem
insert={(data) => insert(index + 1, data)}
replace={(data) => replace(index, data)}
remove={() => remove(index)}
key={index}
schema={item}
/>
))}
<br />
<AddNewAction insert={(data) => push(data)}>
<Button block type={'dashed'} icon={<PlusOutlined />}></Button>
</AddNewAction>
</div>
);
};

View File

@ -1,20 +0,0 @@
.block-list {
background: #f0f2f5;
padding: 0 24px 24px;
}
.block-item {
position: relative;
padding-top: 24px;
.block-item-actions {
display: none;
position: absolute;
top: 0;
right: 0;
z-index: 1;
}
&:hover {
.block-item-actions {
display: inline-block;
}
}
}

View File

@ -10,3 +10,10 @@ group:
---
# Calendar - 日历
## 代码演示
### 基本使用
### 设计器模式

View File

@ -11,3 +11,9 @@ group:
# Chart - 图表
## 代码演示
### 基本使用
### 设计器模式

View File

@ -4,25 +4,27 @@
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={[
{
interface: 'string',
type: 'string',
title: `单行文本`,
name: 'username',
required: true,
default: 'abcdefg',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'please enter',
},
},
]}
fields={fields}
effects={{
onFormValuesChange(form) {
console.log('aaaa', form.values);

View File

@ -0,0 +1,150 @@
import React, { useContext } from 'react';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import classNames from 'classnames';
import { useSchema } from '../../fields';
import { AcceptContext } from './Grid';
import { useField } from '@formily/react';
export function DropFirstRow() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'first-row',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div
className={classNames('drop-row', 'first', { active })}
ref={drop}
></div>
);
}
export function DropRow() {
const { schema } = useSchema();
const field = useField();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'row',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return <div className={classNames('drop-row', { active })} ref={drop}></div>;
}
export function DropColumn() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'column',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div className={classNames('drop-column', { active })} ref={drop}></div>
);
}
export function DropLastColumn() {
const field = useField();
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({
gridType: 'last-column',
schema,
segments: field.address.segments,
}),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
}),
[schema],
);
const active = canDrop && isOver;
return (
<div
className={classNames('drop-column', 'last', { active })}
ref={drop}
></div>
);
}
export function DropBlock({ canDrop }) {
const { schema } = useSchema();
const accept = useContext(AcceptContext);
const [{ isOver }, drop] = useDrop(
() => ({
accept,
drop: () => ({ schema }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
canDrop: () => canDrop,
}),
[canDrop],
);
console.log({ canDrop });
const active = canDrop && isOver;
return (
<div className={classNames('drop-block', { active })} ref={drop}></div>
);
}
export default {
DropRow,
DropColumn,
DropLastColumn,
DropBlock,
};

View File

@ -0,0 +1,319 @@
import React, { createContext, useContext, useState } from 'react';
import { Row, Col, Dropdown, Menu } from 'antd';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
DropFirstRow,
DropRow,
DropColumn,
DropBlock,
DropLastColumn,
} from './Drop';
import {
CloseOutlined,
DeleteOutlined,
MenuOutlined,
PlusOutlined,
ArrowDownOutlined,
ArrowUpOutlined,
} from '@ant-design/icons';
import {
getFullPaths,
SchemaDesignerContext,
useSchemaQuery,
useSchema,
} from '../../fields';
import { TouchBackend } from 'react-dnd-touch-backend';
import { usePreview } from 'react-dnd-preview';
import classNames from 'classnames';
import './style.less';
import { useField, useFieldSchema } from '@formily/react';
const Preview = () => {
const accept = useContext(AcceptContext);
const { item, style, display, itemType } = usePreview();
if (itemType !== accept) {
return null;
}
if (!display) {
return null;
}
if (!item.ref) {
return null;
}
if (!item.ref.current) {
return null;
}
const el = item.ref.current as HTMLDivElement;
console.log({ itemType });
return (
<div
style={{
...style,
height: el.clientHeight,
width: el.clientWidth,
zIndex: 9999,
opacity: 0.8,
// left: `-${el.clientWidth}px`,
}}
>
<div
style={{
transform: 'translate(-90%, -5%)',
}}
dangerouslySetInnerHTML={{ __html: el.outerHTML }}
/>
</div>
);
};
export interface GridPorps {
children?: React.ReactNode;
}
export interface GridRowPorps {
children?: React.ReactNode;
rowOrder?: number;
}
export interface GridColumnPorps {
children?: React.ReactNode;
span?: any;
}
export interface GridBlockProps {
children?: React.ReactNode;
lastComponentType?: string;
}
export type GridComponent = React.FC<GridPorps> & {
Row?: React.FC<GridRowPorps>;
Column?: React.FC<GridColumnPorps>;
Block?: React.FC<GridBlockProps>;
};
export const AcceptContext = createContext(null);
export const Grid: GridComponent = (props) => {
const { children } = props;
const schema = useFieldSchema();
const field = useField();
console.log({ accept: schema.name, schema: schema.toJSON() });
return (
<div className={'grid'} style={{ marginTop: 24 }}>
<AcceptContext.Provider value={schema.name}>
<DndProvider
backend={TouchBackend}
options={{ enableMouseEvents: true }}
>
{children}
<Preview />
</DndProvider>
</AcceptContext.Provider>
</div>
);
};
Grid.Row = (props) => {
const { children, rowOrder } = props;
const { schema } = useSchema();
const field = useField();
const accept = useContext(AcceptContext);
console.log({ accept });
const [{ canDrop, isOverCurrent }, drop] = useDrop(() => ({
accept,
drop: (item, monitor) => {
const didDrop = monitor.didDrop();
if (didDrop) {
return;
}
return { gridType: 'row', schema, segments: field.address.segments };
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
isOverCurrent: monitor.isOver({ shallow: true }),
}),
}));
const active = canDrop && isOverCurrent;
return (
<div
ref={drop}
className={classNames('grid-row', `grid-row-order-${rowOrder}`, {
active,
})}
>
{rowOrder === 0 && <DropFirstRow />}
<Row gutter={24}>{children}</Row>
<DropLastColumn />
{/* <DropRow /> */}
</div>
);
};
Grid.Column = (props) => {
const { children, span } = props;
return (
<Col span={span}>
<DropColumn />
{children}
</Col>
);
};
interface DropResult {
[key: string]: any;
}
Grid.Block = (props) => {
const { children, lastComponentType } = props;
const { schema, refresh } = useSchema();
const fieldSchema = useFieldSchema();
const field = useField();
const accept = useContext(AcceptContext);
const context = useContext(SchemaDesignerContext);
const {
insertAfter,
insertAfterWithAddRow,
insertBeforeWithAddRow,
insertBeforeWithAddColumn,
appendToRowWithAddColumn,
} = useSchemaQuery();
const ref = React.useRef();
console.log({ accept });
const [{ opacity, isDragging }, drag, preview] = useDrag(
() => ({
type: accept,
item: {
ref,
preview,
schema,
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
opacity: monitor.isDragging() ? 0.9 : 1,
}),
end: (item, monitor) => {
const dropResult = monitor.getDropResult<DropResult>();
if (item && dropResult) {
if (dropResult.gridType === 'block') {
insertAfter(field.address.segments, dropResult.segments);
} else if (dropResult.gridType === 'row') {
insertAfterWithAddRow(field.address.segments, dropResult.segments);
} else if (dropResult.gridType === 'column') {
insertBeforeWithAddColumn(
field.address.segments,
dropResult.segments,
);
} else if (dropResult.gridType === 'last-column') {
appendToRowWithAddColumn(
field.address.segments,
dropResult.segments,
);
} else if (dropResult.gridType === 'first-row') {
insertBeforeWithAddRow(
field.address.segments,
dropResult.segments,
);
}
refresh();
}
},
}),
[field, schema],
);
const segments = field.address.segments;
const [{ canDrop, isOver }, drop] = useDrop(
() => ({
accept,
drop: () => {
console.log('source.segments', segments);
return { gridType: 'block', segments, schema };
},
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
// hover: (item, monitor) => {
// console.log(monitor.getSourceClientOffset());
// },
canDrop: () => !isDragging,
}),
[isDragging, schema],
);
const active = canDrop && isOver;
drop(ref);
console.log({ isDragging });
return (
<div
ref={ref}
style={{ opacity }}
className={classNames(`grid-block`, `grid-block--${lastComponentType}`, {
active,
})}
>
<ActionBar dragRef={drag} />
{children}
{/* <DropBlock canDrop={!isDragging} /> */}
</div>
);
};
const ActionBar = ({ dragRef }) => {
const { addBlock, removeBlock } = useSchemaQuery();
const [active, setActive] = useState(false);
return (
<div className={classNames('action-bar', { active })}>
<Dropdown
overlayStyle={{ minWidth: 200 }}
trigger={['click']}
visible={active}
onVisibleChange={setActive}
overlay={
<Menu>
<Menu.Item
onClick={() => {
addBlock({}, 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>
}
>
<MenuOutlined className={'draggable'} ref={dragRef} />
</Dropdown>
</div>
);
};
export default Grid;

View File

@ -1,15 +0,0 @@
#components-grid-demo [class~='ant-row'] {
.ant-col {
// background: rgba(128, 128, 128, 0.08);
> div {
padding: 16px;
border-radius: 0;
color: #fff;
margin-bottom: 8px;
margin-top: 8px;
min-height: 30px;
text-align: center;
background: rgba(0,146,255,.75);
}
}
}

View File

@ -0,0 +1,133 @@
import React, { useMemo } from 'react';
import { FormProvider, FormConsumer, useField, useFieldSchema } from '@formily/react';
import { createForm } from '@formily/core';
import {
SchemaFieldWithDesigner,
registerFieldComponents,
useSchema,
} from '../../../fields';
import { grid, row, column, block } from '../utils';
import { blocks2properties } from '../utils';
function Designer(props) {
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>
);
}
function Hello(props) {
const schema = useFieldSchema();
return (
<div style={{ marginBottom: 24, padding: '1rem', background: '#f9f9f9', minHeight: 50, lineHeight: '50px' }}>
Hello {schema.title}
</div>
);
}
registerFieldComponents({ Hello });
const blocks = [
{
type: 'string',
title: `Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 4`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 2,
},
{
type: 'string',
title: `Block 5`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 6`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 2,
blockOrder: 1,
},
];
const schema = blocks2properties(blocks);
export default () => {
console.log({schema});
return (
<div>
<Designer
schema={{
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[schema.name]: schema,
},
},
},
}}
/>
{/* <pre>{JSON.stringify(schema, null, 2)}</pre> */}
</div>
);
};

View File

@ -0,0 +1,200 @@
import React, { useMemo } from 'react';
import { FormProvider, FormConsumer, useField, useFieldSchema } from '@formily/react';
import { createForm } from '@formily/core';
import {
SchemaFieldWithDesigner,
registerFieldComponents,
} from '../../../fields';
import { grid, row, column, block } from '../utils';
import { blocks2properties } from '../utils';
import { Card } from 'antd';
function Designer(props) {
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>
);
}
function Hello(props) {
const schema = useFieldSchema();
return (
<div style={{ marginBottom: 24, padding: '1rem', background: '#f9f9f9', minHeight: 50, lineHeight: '50px' }}>
Hello {schema.title}
</div>
);
}
registerFieldComponents({ Hello, Designer, Card });
const nested = blocks2properties([
{
type: 'string',
title: `Nested Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Nested Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Nested Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
]);
const blocks = [
{
type: 'string',
title: `Block 1`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 2`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 3`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 2,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 4`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 1,
columnOrder: 1,
blockOrder: 2,
},
{
type: 'string',
title: `Block 5`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 1,
blockOrder: 1,
},
{
type: 'string',
title: `Block 6`,
required: true,
'x-read-pretty': false,
// 'x-decorator': 'FormItem',
'x-component': 'Hello',
rowOrder: 3,
columnOrder: 2,
blockOrder: 1,
},
{
type: 'string',
title: `Block 7`,
required: true,
'x-read-pretty': false,
'x-decorator': 'Card',
'x-decorator-props': {
title: '内嵌区块(只能在当前区域内部拖拽)',
},
'x-component': 'Designer',
'x-component-props': {
schema: {
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[nested.name]: nested,
},
},
},
},
},
rowOrder: 4,
columnOrder: 1,
blockOrder: 1,
},
];
const schema = blocks2properties(blocks);
export default () => {
console.log({schema});
return (
<div>
<Designer
schema={{
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
},
properties: {
[schema.name]: schema,
},
},
},
}}
/>
{/* <pre>{JSON.stringify(schema, null, 2)}</pre> */}
</div>
);
};

View File

@ -5,72 +5,56 @@ nav:
path: /client
group:
order: 2
title: Blocks
title: Blocks
path: /client/blocks
---
# Grid - 栅格
## 示例
基于行Row和列Col来定义区块Block的外部框架。
```tsx
import React from 'react';
import { BlockContext, GridBlock } from '@nocobase/client';
import './demo.less';
## 代码演示
function Hello({ name }) {
return <div>Hello {name}</div>;
}
### 基本用法
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,
},
];
<code src="./demos/demo2.tsx"/>
export default () => {
return (
<BlockContext.Provider value={{
Hello,
}}>
<div id={'components-grid-demo'}>
<GridBlock
blocks={blocks}
/>
</div>
</BlockContext.Provider >
)
### 内嵌区块
<code src="./demos/demo3.tsx"/>
## API 说明
### Grid
只能在同一个 Grid 里拖拽布局
### Grid.Row
### Grid.Column
### Grid.Block
区块
### BlockOptions
```ts
interface BlockOptions {
rowOrder: number;
columnOrder: number;
blockOrder: number;
}
```
- rowOrder第几行
- columnOrder第几列
- blockOrder某单元格内部区块排序
### blocks2properties
原始 schema 需要至少 grid->row->col->block->custom 五层嵌套,写起来非常繁琐,`blocks2properties` 方法可以简化配置。

View File

@ -1,49 +0,0 @@
import React from 'react';
import { Row, Col } from 'antd';
import { useBlock } from '../';
import set from 'lodash/set';
export function BlockItem(props: any) {
const { Component } = useBlock(props);
if (!Component) {
return null;
}
console.log('BlockItem')
return <Component {...props} />
}
export interface GridBlockProps {
blocks: any[];
}
export function GridBlock(props: GridBlockProps) {
const { blocks = [] } = props;
const obj = {
rows: [],
};
blocks.forEach(block => {
const path = ['rows', block['x-row'], block['x-column'], block['x-sort']];
console.log(path);
set(obj, path, block);
});
console.log({obj})
return (
<div>
{obj.rows.map((cols, rowIndex) => {
return (
<Row justify="space-around" align="middle" key={rowIndex}>
{cols.map((items, colIndex) => {
return (
<Col span={24/cols.length} key={colIndex}>
{items.map((item, key) => <BlockItem key={key} {...item} />) }
</Col>
)
})}
</Row>
);
})}
</div>
);
}
export default GridBlock;

View File

@ -0,0 +1,143 @@
.drop-column {
position: absolute;
top: 0;
left: -12px;
height: calc(100% - 24px);
width: 24px;
// border: 1px dashed rgb(232, 232, 232);
&.active {
&::after {
content: '';
display: block;
width: 12px;
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%;
background: #e6f7ff;
}
}
}
.grid-block {
position: relative;
padding-bottom: 1px;
&.active {
&::after {
content: '';
display: block;
height: 12px;
position: absolute;
bottom: 5px;
width: 100%;
background: #e6f7ff;
}
}
&.grid-block--Input {
margin-bottom: 0;
.drop-block {
bottom: 0;
}
}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;
}
}
.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;
}

View File

@ -0,0 +1,94 @@
import set from 'lodash/set';
import { uid } from '@formily/shared';
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()}`;
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,
};
};
export function blocks2properties(blocks: any[]) {
const obj = {
rows: [],
};
blocks.forEach(block => {
const path = ['rows', block['rowOrder'], block['columnOrder'], block['blockOrder']];
console.log(path);
set(obj, path, block);
});
return grid(...obj.rows.filter(Boolean).map((cols) => {
return row(
...cols.filter(Boolean).map((items: any[]) => {
console.log({items: items.filter(Boolean)})
return column(...items.filter(Boolean).map(item => block(item)));
}),
);
}));
}

View File

@ -3,7 +3,6 @@ import React, { createContext, useContext } from 'react';
export * from './form';
export * from './descriptions';
export * from './table';
export * from './grid';
export const BlockContext = createContext({});

View File

@ -11,3 +11,9 @@ group:
# Kanban - 看板
## 代码演示
### 基本使用
### 设计器模式

View File

@ -11,6 +11,11 @@ group:
# Table - 表格
## 示例
## 代码演示
### 基本使用
<code src="./demos/demo1.tsx"/>
### 设计器模式

View File

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Layout, Menu, Button, Dropdown } from 'antd';
import { Layout, Menu, Button, Dropdown, Modal } from 'antd';
import {
AppstoreOutlined,
MenuOutlined,
@ -132,7 +132,12 @@ export function AddNewAction(props) {
);
return (
<Dropdown overlay={menu}>{props.children || <PlusOutlined />}</Dropdown>
<Dropdown
trigger={['click']}
overlay={menu}
>
{props.children || <PlusOutlined />}
</Dropdown>
);
}
@ -179,14 +184,26 @@ export function SettingAction(props) {
<Menu.Item key="move">
<ArrowRightOutlined />
</Menu.Item>
<Menu.Item onClick={() => remove()} key="delete">
<Menu.Item
onClick={() => {
Modal.confirm({
title: '确定菜单',
content: '确认删除此菜单项吗?',
onOk: remove,
});
}}
key="delete"
>
<DeleteOutlined />
</Menu.Item>
</Menu>
);
return (
<Dropdown overlay={menu}>
<Dropdown
trigger={['click']}
overlay={menu}
>
<MenuOutlined />
</Dropdown>
);

View File

@ -41,4 +41,18 @@
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

@ -19,7 +19,6 @@ export function RouteSwitch(props: RouteSwitchProps) {
<Switch>
{routes.map((route, key) => {
if (route.type === 'redirect') {
console.log(route);
return (
<Redirect
key={key}

View File

@ -0,0 +1,20 @@
import React from 'react';
import { connect, mapProps, mapReadPretty, useField } from '@formily/react';
import { Card } from 'antd';
function Test(props) {
return (
<Card>Block</Card>
)
}
export const Block = connect(Test, mapProps(
(props, field) => {
// console.log({ props, field });
return {
...props,
};
},
));
export default Block;

View File

@ -102,4 +102,16 @@ export default () => {
<Field schema={schema} data={data} />
);
};
```
```
## 参数说明
### dataRequest vs dataSource
- dataSource 静态选项value 为 string 或 number。
- dataRequest 动态选项value 为 string 或者 number 时,需要异步获取 label也可以是 object无需异步获取数据。
### 常用配置项
- changeOnSelect当此项为 true 时,点选每级菜单选项值都会发生变化
- maxLeveldataRequest 参数,省市区字段时可以用于配置最大选中级别

View File

@ -19,7 +19,7 @@ export const Checkbox: ComposedCheckbox = connect(
onInput: 'onChange',
},
(props, field) => {
console.log({ props, field });
// console.log({ props, field });
return {
...props,
};

View File

@ -112,3 +112,17 @@ export default () => {
);
};
```
## 参数说明
### dateFormat
日期格式
### showTime
是否显示时间
### timeFormat
时间格式

View File

@ -1,6 +1,15 @@
import React, { useMemo } from 'react';
import React, { createContext, useMemo, useState, useContext } from 'react';
import { Form, FormItem, FormLayout, Space } from '@formily/antd';
import { FormProvider, createSchemaField } from '@formily/react';
import {
FormProvider,
createSchemaField,
useField,
useFieldSchema,
Schema,
useForm,
} from '@formily/react';
import set from 'lodash/set';
import { uid } from '@formily/shared';
import { createForm } from '@formily/core';
import * as core from '@formily/core';
import { Select } from './select';
@ -17,6 +26,8 @@ import { Markdown } from './markdown';
import { Password } from './password';
import { Radio } from './radio';
import { TimePicker } from './time-picker';
import { Block } from './block';
import { Grid } from '../blocks/grid/Grid';
export function fields2properties(fields: any) {
const items = Array.isArray(fields) ? fields : [fields];
@ -50,23 +61,26 @@ const fieldComponents = {
Radio,
Space,
TimePicker,
Block,
Grid,
Column: ({ children }) => children,
Blank: ({ children }) => children,
};
const fieldScope = {};
export function registerFieldComponents(components) {
Object.keys(components).forEach(displayName => {
Object.keys(components).forEach((displayName) => {
fieldComponents[displayName] = components[displayName];
});
}
export function getFieldComponent(displayName: string) {
return fieldComponents[displayName]
return fieldComponents[displayName];
}
export function registerFieldScope(scopes) {
Object.keys(scopes).forEach(name => {
Object.keys(scopes).forEach((name) => {
fieldScope[name] = scopes[name];
});
}
@ -76,6 +90,300 @@ export const SchemaField = createSchemaField({
scope: fieldScope,
});
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 {
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,
};
};
export function parseEffects(effects: any, form?: any) {
if (!effects) {
return;
@ -102,15 +410,17 @@ export interface FieldProps {
export function Field(props: FieldProps) {
const { schema, readPretty, data, effects } = props;
const form = props.form || useMemo(
() =>
createForm({
initialValues: data,
readPretty,
effects: (form) => parseEffects(effects, form),
}),
[],
);
const form =
props.form ||
useMemo(
() =>
createForm({
initialValues: data,
readPretty,
effects: (form) => parseEffects(effects, form),
}),
[],
);
return (
<div>
<FormProvider form={form}>

View File

@ -280,4 +280,15 @@ export default () => {
## 参数说明
输入框组件暂时还没有特殊的参数,其他通用参数参考 [Field 章节](fields)。
输入框组件暂时还没有特殊的参数,其他通用参数参考 [Field 章节](fields)。
常用配置参数:
- type
- title
- name
- required
- default
- x-component-props.placeholder
- x-decorator-props.description
- x-validator

View File

@ -38,7 +38,7 @@ function Menu() {
<ul>
{data.map((item, index) => (
<li key={index}>
<Link to={item.name}>{item.title}</Link>
<Link to={`/admin/${item.name}`}>{item.title}</Link>
</li>
))}
</ul>

View File

@ -2,6 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { Spin } from 'antd';
import { Helmet } from 'react-helmet';
import { usePage, BlockContext } from '../../';
import { useRequest } from 'ahooks';
export function PageTemplate({ route }) {
const { GridBlock } = useContext<any>(BlockContext);
@ -9,11 +10,14 @@ export function PageTemplate({ route }) {
useEffect(() => {
setPreName(route.name);
}, [route.name]);
const { data = {}, loading } = usePage(route.name);
const { data = {}, loading } = useRequest(`/api/routes:getPage?name=${route.name}`, {
// refreshDeps: [route.name],
});
console.log(data.title, { loading, preName, blocks: data.blocks });
if (loading || preName !== route.name) {
return <Spin />;
}
console.log(data.title, { preName, blocks: data.blocks });
console.log(data.title, { loading, preName, blocks: data.blocks });
return (
<div>
<Helmet>