improve calendar

This commit is contained in:
chenos 2021-08-19 17:03:25 +08:00
parent f3bdaf641e
commit dfd4b821ff
15 changed files with 1401 additions and 136 deletions

View File

@ -30,6 +30,7 @@
"@types/mockjs": "^1.0.3",
"@types/node": "^12.6.8",
"@types/react": "^17.0.0",
"@types/react-big-calendar": "^0.33.1",
"@types/react-dom": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.8.2",

View File

@ -144,6 +144,7 @@ export const loadChinaRegionData = (
export const SchemaField = createSchemaField({
scope: {
Table,
Calendar,
useAsyncDataSource,
ChinaRegion: {
loadData: loadChinaRegionData,

View File

@ -1,3 +1,5 @@
import { createContext } from "react";
import { Schema } from "@formily/react";
export const VisibleContext = createContext<any>([]);
export const BlockSchemaContext = createContext<Schema>(null);

View File

@ -70,6 +70,7 @@ import {
useDisplayedMapContext,
} from '../../constate';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { BlockSchemaContext } from '../../context';
const generateGridBlock = (schema: ISchema) => {
const name = schema.name || uid();
@ -457,10 +458,129 @@ function generateCardItemSchema(component) {
'x-decorator': 'CardItem',
default: [],
properties: {
toolbar: {
type: 'void',
'x-component': 'Calendar.Toolbar',
properties: {
today: {
type: 'void',
title: '今天',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Today',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'today',
},
},
nav: {
type: 'void',
title: '翻页',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Nav',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'nav',
},
},
title: {
type: 'void',
title: '标题',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Title',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'title',
},
},
viewSelect: {
type: 'void',
title: '视图切换',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.ViewSelect',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'viewSelect',
},
},
filter: {
type: 'void',
title: '过滤',
'x-align': 'right',
'x-designable-bar': 'Calendar.Filter.DesignableBar',
'x-component': 'Calendar.Filter',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'filter',
},
},
create: {
type: 'void',
title: '新增',
'x-align': 'right',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Action',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component-props': {
type: 'primary',
},
properties: {
modal: {
type: 'void',
title: '新增数据',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
useOkAction: '{{ Calendar.useCreateAction }}',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},
},
},
},
event: {
type: 'void',
'x-component': 'Calendar.Event',
properties: {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Tabs',
'x-designable-bar': 'Tabs.DesignableBar',
properties: {
[uid()]: {
type: 'void',
title: '详情',
'x-designable-bar': 'Tabs.TabPane.DesignableBar',
'x-component': 'Tabs.TabPane',
'x-component-props': {},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.PaneItem',
},
},
},
},
},
},
},
},
},
},
@ -598,7 +718,7 @@ function generateCardItemSchema(component) {
},
column3: {
type: 'void',
title: '所属数据表',
title: '选择数据源',
'x-component': 'Table.Column',
properties: {
'collection.title': {
@ -683,7 +803,7 @@ function generateCardItemSchema(component) {
},
'collection.title': {
type: 'string',
title: '所属数据表',
title: '选择数据源',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-read-pretty': true,
@ -934,7 +1054,7 @@ AddNew.CardItem = observer((props: any) => {
<Menu.ItemGroup
className={'display-fields'}
key={`${view.key}-select`}
title={'所属数据表'}
title={'选择数据源'}
>
{collections?.map((item) => (
<Menu.Item
@ -968,7 +1088,7 @@ AddNew.CardItem = observer((props: any) => {
<Menu.ItemGroup
className={'display-fields'}
key={`${view.key}-select`}
title={'所属数据表'}
title={'选择数据源'}
>
{collections?.map((item) => (
<Menu.Item
@ -982,7 +1102,7 @@ AddNew.CardItem = observer((props: any) => {
schema={{
type: 'object',
properties: {
titleField: {
title: {
type: 'string',
title: '标题字段',
required: true,
@ -992,12 +1112,12 @@ AddNew.CardItem = observer((props: any) => {
(field) => {
return {
label: field?.uiSchema.title,
name: field?.name,
value: field?.name,
};
},
),
},
startTimeField: {
start: {
title: '开始日期字段',
required: true,
'x-decorator': 'FormItem',
@ -1009,11 +1129,11 @@ AddNew.CardItem = observer((props: any) => {
?.map((field) => {
return {
label: field?.uiSchema.title,
name: field?.name,
value: field?.name,
};
}),
},
endTimeField: {
end: {
title: '结束日期字段',
'x-decorator': 'FormItem',
'x-component': 'Select',
@ -1024,7 +1144,7 @@ AddNew.CardItem = observer((props: any) => {
?.map((field) => {
return {
label: field?.uiSchema.title,
name: field?.name,
value: field?.name,
};
}),
},
@ -1039,6 +1159,7 @@ AddNew.CardItem = observer((props: any) => {
if (schema['key']) {
data['key'] = uid();
}
console.log('fieldNames', values);
if (collectionName) {
data['x-component-props'] =
data['x-component-props'] || {};
@ -1046,12 +1167,7 @@ AddNew.CardItem = observer((props: any) => {
collectionName;
data['x-component-props']['collectionName'] =
collectionName;
data['x-component-props']['titleField'] =
values.titleField;
data['x-component-props']['startTimeField'] =
values.startTimeField;
data['x-component-props']['endTimeField'] =
values.endTimeField;
data['x-component-props']['fieldNames'] = values;
}
if (isGridBlock(schema)) {
path.pop();
@ -1100,7 +1216,7 @@ AddNew.CardItem = observer((props: any) => {
<Menu.ItemGroup
className={'display-fields'}
key={`${view.key}-select`}
title={'所属数据表'}
title={'选择数据源'}
>
{collections?.map((item) => (
<Menu.SubMenu
@ -1159,10 +1275,10 @@ AddNew.CardItem = observer((props: any) => {
// </Menu.Item>
))}
</Menu.ItemGroup>
<Menu.Divider></Menu.Divider>
{/* <Menu.Divider></Menu.Divider>
<Menu.Item icon={<PlusOutlined />} key={`addNew${view.key}`}>
</Menu.Item>
</Menu.Item> */}
</Menu.SubMenu>
))}
</Menu.ItemGroup>
@ -1435,7 +1551,9 @@ AddNew.PaneItem = observer((props: any) => {
const { schema, insertBefore, insertAfter, appendChild } = useDesignable();
const path = useSchemaPath();
const [visible, setVisible] = useState(false);
const blockSchema = useContext(BlockSchemaContext);
const useResource = `{{ ${blockSchema['x-component']}.useResource }}`;
console.log('AddNew.PaneItem.useResource', useResource)
return (
<Dropdown
trigger={['click']}
@ -1458,8 +1576,7 @@ AddNew.PaneItem = observer((props: any) => {
'x-component': 'Form',
'x-read-pretty': true,
'x-component-props': {
useResource: '{{ Table.useResource }}',
useValues: '{{ Table.useTableRowRecord }}',
useResource,
},
'x-designable-bar': 'Form.DesignableBar',
properties: {
@ -1511,8 +1628,7 @@ AddNew.PaneItem = observer((props: any) => {
'x-decorator': 'CardItem',
'x-component': 'Form',
'x-component-props': {
useResource: '{{ Table.useResource }}',
useValues: '{{ Table.useTableRowRecord }}',
useResource,
showDefaultButtons: true,
},
'x-designable-bar': 'Form.DesignableBar',

View File

@ -0,0 +1,85 @@
import React, { useContext, useMemo, useRef, useState } from 'react';
import { createForm } from '@formily/core';
import {
SchemaOptionsContext,
Schema,
useFieldSchema,
observer,
SchemaExpressionScopeContext,
FormProvider,
ISchema,
useField,
useForm,
RecursionField,
} from '@formily/react';
import {
useSchemaPath,
SchemaField,
useDesignable,
removeSchema,
updateSchema,
} from '../';
import get from 'lodash/get';
import { Button, Dropdown, Menu, Space } from 'antd';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import cls from 'classnames';
import { FormDialog, FormLayout } from '@formily/antd';
// import './style.less';
import AddNew from '../add-new';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import { isGridRowOrCol } from '../grid';
import constate from 'constate';
import { useEffect } from 'react';
import { uid } from '@formily/shared';
import { getSchemaPath } from '../../components/schema-renderer';
import { useDisplayedMapContext } from '../../constate';
import { DragHandle } from '../../components/Sortable';
export const ActionDesignableBar = (props: any) => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const isPopup = Object.keys(schema.properties || {}).length > 0;
const displayed = useDisplayedMapContext();
const field = useField();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={2}>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
onClick={async () => {
const displayName =
schema?.['x-decorator-props']?.['displayName'];
const data = remove();
await removeSchema(data);
if (displayName) {
displayed.remove(displayName);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
};

View File

@ -0,0 +1,300 @@
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { observer, RecursionField, Schema } from '@formily/react';
import React, { useState } from 'react';
import { createSchema, ISchema, removeSchema, updateSchema } from '..';
import {
findPropertyByPath,
getSchemaPath,
useDesignable,
} from '../../components/schema-renderer';
import { DisplayedMapProvider, useDisplayedMapContext } from '../../constate';
import cls from 'classnames';
import { Button, Dropdown, Menu, Space } from 'antd';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { uid } from '@formily/shared';
import { PlusOutlined } from '@ant-design/icons';
import { Droppable, SortableItem } from '../../components/Sortable';
export const ActionBar = observer((props: any) => {
const { align = 'top' } = props;
// const { schema, designable } = useDesignable();
const { root, schema, insertAfter, remove, appendChild } = useDesignable();
const moveToAfter = (path1, path2, extra = {}) => {
if (!path1 || !path2) {
return;
}
if (path1.join('.') === path2.join('.')) {
return;
}
const data = findPropertyByPath(root, path1);
if (!data) {
return;
}
remove(path1);
return insertAfter(
{
...data.toJSON(),
...extra,
},
path2,
);
};
const [dragOverlayContent, setDragOverlayContent] = useState('');
return (
<DndContext
onDragStart={(event) => {
setDragOverlayContent(event.active.data?.current?.title || '');
// const previewRef = event.active.data?.current?.previewRef;
// if (previewRef) {
// setDragOverlayContent(previewRef?.current?.innerHTML);
// } else {
// setDragOverlayContent('');
// }
}}
onDragEnd={async (event) => {
const path1 = event.active?.data?.current?.path;
const path2 = event.over?.data?.current?.path;
const align = event.over?.data?.current?.align;
const draggable = event.over?.data?.current?.draggable;
if (!path1 || !path2) {
return;
}
if (path1.join('.') === path2.join('.')) {
return;
}
if (!draggable) {
console.log('alignalignalignalign', align);
const p = findPropertyByPath(root, path1);
if (!p) {
return;
}
remove(path1);
const data = appendChild(
{
...p.toJSON(),
'x-align': align,
},
path2,
);
await updateSchema(data);
} else {
const data = moveToAfter(path1, path2, {
'x-align': align,
});
await updateSchema(data);
}
}}
>
<DragOverlay style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}>
{dragOverlayContent}
{/* <div style={{ transform: 'translateX(-100%)' }} dangerouslySetInnerHTML={{__html: dragOverlayContent}}></div> */}
</DragOverlay>
<DisplayedMapProvider>
<div className={cls('nb-action-bar', `align-${align}`)}>
<div style={{ width: '50%' }}>
<Actions align={'left'} />
</div>
<div style={{ marginLeft: 'auto', width: '50%', textAlign: 'right' }}>
<Actions align={'right'} />
</div>
<AddActionButton />
</div>
</DisplayedMapProvider>
</DndContext>
);
});
function generateActionSchema(type) {
const actions: { [key: string]: ISchema } = {
today: {
type: 'void',
title: '今天',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Today',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'today',
},
},
nav: {
type: 'void',
title: '翻页',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Nav',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'nav',
},
},
title: {
type: 'void',
title: '标题',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Title',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'title',
},
},
viewSelect: {
type: 'void',
title: '视图切换',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.ViewSelect',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'viewSelect',
},
},
filter: {
type: 'void',
title: '过滤',
'x-align': 'right',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Filter',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'filter',
},
},
create: {
type: 'void',
title: '新增',
'x-align': 'right',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Action',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component-props': {
type: 'primary',
},
properties: {
modal: {
type: 'void',
title: '新增数据',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
useOkAction: '{{ Calendar.useCreateAction }}',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},
},
};
return actions[type];
}
function AddActionButton() {
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { appendChild, remove } = useDesignable();
const { schema, designable } = useDesignable();
if (!designable) {
return null;
}
return (
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={setVisible}
overlay={
<Menu>
<Menu.ItemGroup title={'操作展示'}>
{[
{ title: '今天', name: 'today' },
{ title: '翻页', name: 'nav' },
{ title: '标题', name: 'title' },
{ title: '视图切换', name: 'viewSelect' },
{ title: '筛选', name: 'filter' },
{ title: '新增', name: 'create' },
].map((item) => (
<SwitchMenuItem
key={item.name}
checked={displayed.has(item.name)}
title={item.title}
onChange={async (checked) => {
if (!checked) {
const s = displayed.get(item.name) as Schema;
const path = getSchemaPath(s);
displayed.remove(item.name);
const removed = remove(path);
await removeSchema(removed);
} else {
const s = generateActionSchema(item.name);
const data = appendChild(s);
await createSchema(data);
}
}}
/>
))}
</Menu.ItemGroup>
<Menu.Divider />
<Menu.SubMenu disabled title={'自定义'}>
<Menu.Item style={{ minWidth: 120 }}></Menu.Item>
<Menu.Item></Menu.Item>
<Menu.Item></Menu.Item>
</Menu.SubMenu>
</Menu>
}
>
<Button
className={'designable-btn designable-btn-dash'}
style={{ marginLeft: 8 }}
type={'dashed'}
icon={<PlusOutlined />}
>
</Button>
</Dropdown>
);
}
function Actions(props: any) {
const { align = 'left' } = props;
const { schema, designable } = useDesignable();
return (
<Droppable
id={`${schema.name}-${align}`}
className={`action-bar-align-${align}`}
data={{ align, path: getSchemaPath(schema) }}
>
<Space>
{schema.mapProperties((s) => {
const currentAlign = s['x-align'] || 'left';
if (currentAlign !== align) {
return null;
}
return (
<SortableItem
id={s.name}
data={{
align,
draggable: true,
title: s.title,
path: getSchemaPath(s),
}}
>
<RecursionField name={s.name} schema={s} />
</SortableItem>
);
})}
</Space>
</Droppable>
);
}

View File

@ -12,12 +12,18 @@ import {
useForm,
RecursionField,
} from '@formily/react';
import { useSchemaPath, SchemaField, useDesignable, removeSchema } from '../';
import {
useSchemaPath,
SchemaField,
useDesignable,
removeSchema,
updateSchema,
} from '../';
import get from 'lodash/get';
import { Button, Dropdown, Menu, Space } from 'antd';
import { Button, Dropdown, Menu, Select, Space } from 'antd';
import { MenuOutlined, DragOutlined } from '@ant-design/icons';
import cls from 'classnames';
import { FormLayout } from '@formily/antd';
import { FormDialog, FormLayout } from '@formily/antd';
// import './style.less';
import AddNew from '../add-new';
import { DraggableBlockContext } from '../../components/drag-and-drop';
@ -28,6 +34,8 @@ import { uid } from '@formily/shared';
import { getSchemaPath } from '../../components/schema-renderer';
import { useCollection, useCollectionContext } from '../../constate';
import { useTable } from '../table';
import { fieldsToFilterColumns } from './';
import { set } from 'lodash';
export const DesignableBar = observer((props) => {
const field = useField();
@ -35,11 +43,15 @@ export const DesignableBar = observer((props) => {
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
const { props: tableProps } = useTable();
const collectionName = field.componentProps?.collectionName || tableProps?.collectionName;
const { collection } = useCollection({ collectionName });
const collectionName =
field.componentProps?.collectionName || tableProps?.collectionName;
const { collection, fields } = useCollection({ collectionName });
const fieldNames = field.componentProps.fieldNames;
return (
<div className={cls('designable-bar', { active: visible })}>
<div className={'designable-info'}>{collection?.title || collection?.name}</div>
<div className={'designable-info'}>
{collection?.title || collection?.name}
</div>
<span
onClick={(e) => {
e.stopPropagation();
@ -57,15 +69,143 @@ export const DesignableBar = observer((props) => {
}}
overlay={
<Menu>
{/* <Menu.Item
key={'update'}
onClick={() => {
field.readPretty = true;
<Menu.Item>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<Select
bordered={false}
size={'small'}
style={{ minWidth: 160 }}
value={field.componentProps?.fieldNames.title}
onChange={async (title) => {
field.componentProps.fieldNames = {
...fieldNames,
title,
};
// set(field.componentProps, 'fieldNames.title', value);
set(
schema['x-component-props'],
'fieldNames.title',
title,
);
await updateSchema(schema);
}}
options={fields?.map((field) => {
return {
label: field?.uiSchema.title,
value: field?.name,
};
})}
/>
</div>
</Menu.Item>
<Menu.Item>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<Select
bordered={false}
size={'small'}
style={{ minWidth: 160 }}
value={field.componentProps?.fieldNames.start}
onChange={async (value) => {
field.componentProps.fieldNames = {
...fieldNames,
start: value,
};
// set(field.componentProps, 'fieldNames.start', value);
set(
schema['x-component-props'],
'fieldNames.start',
value,
);
await updateSchema(schema);
}}
options={fields
?.filter((field) => field.dataType === 'date')
?.map((field) => {
return {
label: field?.uiSchema.title,
value: field?.name,
};
})}
/>
</div>
</Menu.Item>
<Menu.Item>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<Select
bordered={false}
size={'small'}
style={{ minWidth: 160 }}
value={field.componentProps?.fieldNames.end}
onChange={async (value) => {
field.componentProps.fieldNames = {
...fieldNames,
end: value,
};
set(
schema['x-component-props'],
'fieldNames.end',
value,
);
await updateSchema(schema);
}}
options={fields
?.filter((field) => field.dataType === 'date')
?.map((field) => {
return {
label: field?.uiSchema.title,
value: field?.name,
};
})}
/>
</div>
</Menu.Item>
<Menu.Item
key={'defaultFilter'}
onClick={async () => {
const { defaultFilter } = await FormDialog(
'设置筛选范围',
() => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
defaultFilter: {
type: 'object',
'x-component': 'Filter',
properties: fieldsToFilterColumns(fields),
},
},
}}
/>
</FormLayout>
);
},
).open({
initialValues: {
defaultFilter:
field?.componentProps?.defaultFilter || {},
},
});
schema['x-component-props']['defaultFilter'] =
defaultFilter;
field.componentProps.defaultFilter = defaultFilter;
await updateSchema(schema);
}}
>
</Menu.Item> */}
{/* <Menu.Divider /> */}
</Menu.Item>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={async () => {

View File

@ -1,19 +1,128 @@
import { ISchema } from '@formily/react';
import React from 'react';
import { SchemaRenderer } from '../../';
import { ISchema, SchemaRenderer } from '../../';
import { Calendar } from '..';
import events from './events';
import { uid } from '@formily/shared';
const schema: ISchema = {
type: 'array',
name: 'calendar1',
'x-component': 'Calendar',
'x-component-props': {
fieldNames: {
title: '',
startTime: '',
endTime: '',
},
},
default: events,
properties: {
toolbar: {
type: 'void',
'x-component': 'Calendar.Toolbar',
properties: {
today: {
type: 'void',
title: '今天',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Today',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'today',
},
},
nav: {
type: 'void',
title: '翻页',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Nav',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'nav',
},
},
title: {
type: 'void',
title: '标题',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.Title',
'x-align': 'left',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'title',
},
},
viewSelect: {
type: 'void',
title: '视图切换',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Calendar.ViewSelect',
'x-align': 'right',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'viewSelect',
},
},
filter: {
type: 'void',
title: '过滤',
'x-align': 'right',
'x-designable-bar': 'Calendar.Filter.DesignableBar',
'x-component': 'Calendar.Filter',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'filter',
},
},
create: {
type: 'void',
title: '新增',
'x-align': 'right',
'x-designable-bar': 'Calendar.ActionDesignableBar',
'x-component': 'Action',
'x-decorator': 'AddNew.Displayed',
'x-decorator-props': {
displayName: 'create',
},
'x-component-props': {
type: 'primary',
},
properties: {
modal: {
type: 'void',
title: '新增数据',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
// useOkAction: '{{ Calendar.useTableCreateAction }}',
},
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},
},
},
},
event: {
type: 'void',
'x-component': 'Calendar.Event',
properties: {
[uid()]: {
type: 'void',
'x-component': 'Grid',
'x-component-props': {
addNewComponent: 'AddNew.FormItem',
},
},
},
},
},

View File

@ -0,0 +1,35 @@
.ant-btn-group {
position: relative;
&:hover {
.designable-bar {
display: block;
}
}
.designable-bar {
pointer-events: none;
display: none;
position: absolute;
top: -1px;
right: -1px;
left: -1px;
bottom: -1px;
border-radius: 2px;
border: 2px solid #1890ff;
&.active {
display: block;
}
.designable-bar-actions {
pointer-events: all;
position: absolute;
right: 0;
line-height: 1rem;
background-color: #1890ff;
color: #fff;
z-index: 10;
padding: 0 3px;
.anticon {
font-size: 10px;
}
}
}
}

View File

@ -15,6 +15,12 @@ group:
<pre lang="tsx">
<Calendar>
<Calendar.Toolbar>
<Calendar.Nav/>
<Calendar.Title/>
<Calendar.ViewSelect/>
<Calendar.Filter/>
</Calendar.Toolbar>
<Calendar.Event>
// 查看日历事件详情
</Calendar.Event>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import {
Calendar as BigCalendar,
Views,
@ -7,11 +7,55 @@ import {
import moment from 'moment';
import * as dates from 'react-big-calendar/lib/utils/dates';
import { navigate } from 'react-big-calendar/lib/utils/constants';
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Space, Button, Radio, Select } from 'antd';
import {
LeftOutlined,
RightOutlined,
SettingOutlined,
} from '@ant-design/icons';
import {
Space,
Button,
Radio,
Select,
Popover,
Dropdown,
Menu,
Drawer,
} from 'antd';
import './style2.less';
import { observer, useField } from '@formily/react';
import './designable.less';
import {
FormProvider,
observer,
RecursionField,
Schema,
useField,
useForm,
} from '@formily/react';
import { DesignableBar } from './DesignableBar';
import { createContext } from 'react';
import { useContext } from 'react';
import { SchemaField, useDesignable } from '../../components/schema-renderer';
import { ActionBar } from './ActionBar';
import { ActionDesignableBar } from './Action';
import { createForm } from '@formily/core';
import { flatten, get } from 'lodash';
import {
CollectionProvider,
useCollectionContext,
useDisplayedMapContext,
} from '../../constate';
import { FormButtonGroup, FormDialog, FormLayout, Submit } from '@formily/antd';
import IconPicker from '../../components/icon-picker';
import { DragHandle } from '../../components/Sortable';
import SwitchMenuItem from '../../components/SwitchMenuItem';
import { removeSchema, updateSchema } from '..';
import { MenuOutlined } from '@ant-design/icons';
import { interfaces } from '../database-field/interfaces';
import cls from 'classnames';
import { Resource } from '../../resource';
import { useRequest } from 'ahooks';
const localizer = momentLocalizer(moment);
@ -28,6 +72,10 @@ interface ToolbarProps {
onView?: (view: string) => void;
}
export const RecordContext = createContext(null);
export const ToolbarContext = createContext<ToolbarProps>(null);
export const CalendarContext = createContext(null);
function Toolbar(props: ToolbarProps) {
const {
label,
@ -37,99 +85,508 @@ function Toolbar(props: ToolbarProps) {
onView,
localizer: { messages },
} = props;
const { schema } = useDesignable();
const toolBarSchema: Schema = schema.reduceProperties((buf, current) => {
if (current['x-component'] === 'Calendar.Toolbar') {
return current;
}
return buf;
}, null);
return (
<div style={{ marginBottom: 8 }}>
<Space>
<Button
onClick={() => {
onNavigate(navigate.TODAY);
}}
>
</Button>
<div className="ant-btn-group">
<Button
icon={<LeftOutlined />}
onClick={() => onNavigate(navigate.PREVIOUS)}
></Button>
<Button
icon={<RightOutlined />}
onClick={() => onNavigate(navigate.NEXT)}
></Button>
</div>
<div style={{ fontSize: '1.75em', fontWeight: 300 }}>{label}</div>
</Space>
<Space style={{ float: 'right' }}>
<Select value={view} onChange={onView}>
{views.map((name) => (
<Select.Option value={name}>{messages[name]}</Select.Option>
))}
</Select>
<Button></Button>
<Button type={'primary'}></Button>
</Space>
</div>
<ToolbarContext.Provider value={props}>
<RecursionField name={toolBarSchema.name} schema={toolBarSchema} />
</ToolbarContext.Provider>
);
}
const messages: any = {
allDay: '',
previous: (
<div>
<LeftOutlined />
</div>
),
next: (
<div>
<RightOutlined />
</div>
),
today: '今天',
month: '月',
week: '周',
work_week: '工作日',
day: '天',
agenda: '列表',
date: '日期',
time: '时间',
event: '事件',
noEventsInRange: '无',
showMore: (count) => `还有 ${count}`,
};
const useCalendar = () => {
return useContext(CalendarContext);
};
const toEvents = (data: any[], fieldNames: any) => {
return data?.map((item) => {
return {
id: get(item, fieldNames.id || 'id'),
title: get(item, fieldNames.title) || '无标题',
start: get(item, fieldNames.start),
end: get(item, fieldNames.end || fieldNames.start),
};
});
};
export const Calendar: any = observer((props: any) => {
const field = useField<Formily.Core.Models.ArrayField>();
const { collectionName, fieldNames = {} } = props;
console.log('Calendar', props);
const { schema } = useDesignable();
const eventSchema: Schema = schema.reduceProperties((buf, current) => {
if (current['x-component'] === 'Calendar.Event') {
return current;
}
return buf;
}, null);
const resource = Resource.make(collectionName);
const service = useRequest(
(params) => {
if (!collectionName) {
return Promise.resolve({});
}
return resource.list({
...params,
perPage: -1,
});
},
{
formatResult: (data) => data?.data,
onSuccess(data) {
const events = toEvents(data, fieldNames) || [];
// console.log('events', events);
field.setValue(events);
},
// refreshDeps: [props.fieldNames],
},
);
useEffect(() => {
service.run({
defaultFilter: props?.defaultFilter,
sort: props?.fieldNames?.start || 'created_at',
});
}, [props?.fieldNames, props.defaultFilter]);
const [visible, setVisible] = useState(false);
const [record, setRecord] = useState<any>({});
console.log('field.value', field.value);
return (
<div style={{ height: 700 }}>
<BigCalendar
popup
selectable
events={field.value || []}
views={allViews}
step={60}
showMultiDayTimes
messages={{
allDay: '',
previous: (
<div>
<LeftOutlined />
</div>
),
next: (
<div>
<RightOutlined />
</div>
),
today: '今天',
month: '月',
week: '周',
work_week: '工作日',
day: '天',
agenda: '列表',
date: '日期',
time: '时间',
event: '事件',
noEventsInRange: '无',
showMore: (count) => `还有 ${count}`,
}}
formats={{
monthHeaderFormat: 'Y年M月',
agendaDateFormat: 'M月DD日',
dayHeaderFormat: 'Y年M月DD日',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'Y年M月', culture);
}
return `${local.format(start, 'Y年M月', culture)} - ${local.format(
end,
'Y年M月',
culture,
)}`;
},
}}
defaultDate={new Date(2015, 3, 1)}
components={{
toolbar: Toolbar,
}}
localizer={localizer}
/>
<CollectionProvider collectionName={props.collectionName}>
<CalendarContext.Provider value={{ resource, service, props }}>
<div {...props} style={{ height: 700 }}>
<Drawer
width={'50%'}
visible={visible}
onClose={() => {
setVisible(false);
}}
title={'查看数据'}
bodyStyle={{
background: '#f0f2f5',
paddingTop: 0,
}}
>
<RecordContext.Provider value={record}>
<RecursionField
name={eventSchema.name}
schema={eventSchema}
onlyRenderProperties
/>
</RecordContext.Provider>
</Drawer>
<BigCalendar
popup
selectable
events={Array.isArray(field.value.slice()) ? field.value.slice() : []}
views={allViews}
step={60}
showMultiDayTimes
messages={messages}
onSelectSlot={(slotInfo) => {
console.log('onSelectSlot', slotInfo);
}}
onDoubleClickEvent={(event) => {
console.log('onDoubleClickEvent');
}}
onSelectEvent={(event) => {
const record = field.value?.find((item) => item.id === event.id);
if (!record) {
return;
}
setRecord(record);
setVisible(true);
console.log('onSelectEvent');
}}
formats={{
monthHeaderFormat: 'Y年M月',
agendaDateFormat: 'M月DD日',
dayHeaderFormat: 'Y年M月DD日',
dayRangeHeaderFormat: ({ start, end }, culture, local) => {
if (dates.eq(start, end, 'month')) {
return local.format(start, 'Y年M月', culture);
}
return `${local.format(
start,
'Y年M月',
culture,
)} - ${local.format(end, 'Y年M月', culture)}`;
},
}}
defaultDate={new Date()}
components={{
toolbar: Toolbar,
}}
localizer={localizer}
/>
</div>
</CalendarContext.Provider>
</CollectionProvider>
);
});
Calendar.useCreateAction = () => {
const { service, resource } = useCalendar();
const form = useForm();
return {
async run() {
await resource.create(form.values);
await form.reset();
return service.refresh();
},
};
};
Calendar.useResource = ({ onSuccess }) => {
const { props } = useCalendar();
const { collection } = useCollectionContext();
const record = useContext(RecordContext);
const resource = Resource.make({
resourceName: collection?.name || props.collectionName,
resourceKey: record['id'],
});
console.log(
'collection?.name || props.collectionName',
collection?.name || props.collectionName,
);
const service = useRequest(
(params?: any) => {
console.log('Table.useResource', params);
return resource.get(params);
},
{
formatResult: (result) => result?.data,
onSuccess,
refreshDeps: [record],
// manual,
},
);
return { resource, service, initialValues: service.data, ...service };
};
Calendar.Toolbar = ActionBar;
Calendar.Title = observer(() => {
const { DesignableBar } = useDesignable();
const { label } = useContext(ToolbarContext);
return (
<div
className="ant-btn-group"
style={{ fontSize: '1.75em', fontWeight: 300 }}
>
{label}
<DesignableBar />
</div>
);
});
Calendar.Today = observer((props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(ToolbarContext);
return (
<Button
onClick={() => {
onNavigate(navigate.TODAY);
}}
>
<DesignableBar />
</Button>
);
});
Calendar.Nav = observer((props) => {
const { DesignableBar } = useDesignable();
const { onNavigate } = useContext(ToolbarContext);
return (
<div className="ant-btn-group">
<Button
icon={<LeftOutlined />}
onClick={() => onNavigate(navigate.PREVIOUS)}
></Button>
<Button
icon={<RightOutlined />}
onClick={() => onNavigate(navigate.NEXT)}
></Button>
<DesignableBar />
</div>
);
});
Calendar.ViewSelect = observer((props) => {
const { DesignableBar } = useDesignable();
const {
views,
view,
onView,
localizer: { messages },
} = useContext(ToolbarContext);
return (
<div className="ant-btn-group">
<Select value={view} onChange={onView}>
{views.map((name) => (
<Select.Option value={name}>{messages[name]}</Select.Option>
))}
</Select>
<DesignableBar />
</div>
);
});
export const fieldsToFilterColumns = (fields: any[], options: any = {}) => {
const { fieldNames = [] } = options;
const properties = {};
fields.forEach((field, index) => {
if (fieldNames?.length && !fieldNames.includes(field.name)) {
return;
}
const fieldOption = interfaces.get(field.interface);
if (!fieldOption.operations) {
return;
}
properties[`column${index}`] = {
type: 'void',
title: field?.uiSchema?.title,
'x-component': 'Filter.Column',
'x-component-props': {
operations: fieldOption.operations,
},
properties: {
[field.name]: {
...field.uiSchema,
'x-decorator': 'FormilyFormItem',
title: null,
},
},
};
});
return properties;
};
Calendar.Filter = observer((props: any) => {
const { service } = useCalendar();
const { fieldNames = [] } = props;
const { schema, DesignableBar } = useDesignable();
const form = useMemo(() => createForm(), []);
const { fields = [] } = useCollectionContext();
const [visible, setVisible] = useState(false);
const obj = flatten(form.values.filter || {});
console.log('flatten', obj, Object.values(obj));
const count = Object.values(obj).filter((i) =>
Array.isArray(i) ? i.length : i,
).length;
const icon = props.icon || 'FilterOutlined';
const properties = fieldsToFilterColumns(fields, { fieldNames });
schema.mapProperties((p) => {
properties[p.name] = p;
});
return (
<Popover
trigger={['click']}
placement={'bottomLeft'}
visible={visible}
onVisibleChange={setVisible}
content={
<div>
<FormProvider form={form}>
<SchemaField
schema={{
type: 'object',
properties: {
filter: {
type: 'object',
'x-component': 'Filter',
properties,
},
},
}}
/>
<FormButtonGroup align={'right'}>
<Submit
onSubmit={() => {
const { filter } = form.values;
console.log('Table.Filter', form.values);
setVisible(false);
// return service.run({
// ...service.params[0],
// filter,
// });
}}
>
</Submit>
</FormButtonGroup>
</FormProvider>
</div>
}
>
<Button icon={<IconPicker type={icon} />}>
{count > 0 ? `${count} 个筛选项` : schema.title}
<DesignableBar />
</Button>
</Popover>
);
});
Calendar.Filter.DesignableBar = () => {
const { schema, remove, refresh, insertAfter } = useDesignable();
const [visible, setVisible] = useState(false);
const displayed = useDisplayedMapContext();
const { fields } = useCollectionContext();
const field = useField();
let fieldNames = field.componentProps.fieldNames || [];
if (fieldNames.length === 0) {
fieldNames = fields.map((field) => field.name);
}
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space>
<DragHandle />
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.ItemGroup title={'筛选字段'}>
{fields
.filter((collectionField) => {
const option = interfaces.get(collectionField.interface);
return option.operations?.length;
})
.map((collectionField) => (
<SwitchMenuItem
title={collectionField?.uiSchema?.title}
checked={fieldNames.includes(collectionField.name)}
onChange={async (checked) => {
if (checked) {
fieldNames.push(collectionField.name);
} else {
const index = fieldNames.indexOf(
collectionField.name,
);
if (index > -1) {
fieldNames.splice(index, 1);
}
}
console.log({ fieldNames, field });
schema['x-component-props']['fieldNames'] =
fieldNames;
field.componentProps.fieldNames = fieldNames;
updateSchema(schema);
}}
/>
))}
</Menu.ItemGroup>
<Menu.Divider />
<Menu.Item
onClick={async (e) => {
setVisible(false);
const values = await FormDialog('修改名称和图标', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
title: {
type: 'string',
title: '按钮名称',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
icon: {
type: 'string',
title: '按钮图标',
'x-decorator': 'FormItem',
'x-component': 'IconPicker',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
title: schema['title'],
icon: schema['x-component-props']?.['icon'],
},
});
schema['title'] = values.title;
schema['x-component-props']['icon'] = values.icon;
field.componentProps.icon = values.icon;
field.title = values.title;
updateSchema(schema);
refresh();
}}
>
</Menu.Item>
<Menu.Divider />
<Menu.Item
onClick={async () => {
const displayName =
schema?.['x-decorator-props']?.['displayName'];
const data = remove();
await removeSchema(data);
if (displayName) {
displayed.remove(displayName);
}
setVisible(false);
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
};
Calendar.DesignableBar = DesignableBar;
Calendar.ActionDesignableBar = ActionDesignableBar;

View File

@ -20,13 +20,17 @@ import { useDesignable } from '../';
import { AddNew } from '../add-new';
import { BlockItem } from '../block-item';
import { DraggableBlockContext } from '../../components/drag-and-drop';
import { BlockSchemaContext } from '../../context';
export const CardItem: any = connect((props) => {
const { schema } = useDesignable();
return (
<BlockItem>
<Card bordered={false} {...props}>
{props.children}
</Card>
</BlockItem>
<BlockSchemaContext.Provider value={schema}>
<BlockItem>
<Card bordered={false} {...props}>
{props.children}
</Card>
</BlockItem>
</BlockSchemaContext.Provider>
);
});

View File

@ -35,8 +35,9 @@ export const DesignableBar = observer((props) => {
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
const { props: tableProps } = useTable();
const collectionName = field.componentProps?.collectionName || tableProps?.collectionName;
const { collection } = useCollection({ collectionName });
const { collection } = useCollectionContext();
// const collectionName = field.componentProps?.collectionName || tableProps?.collectionName;
// const { collection } = useCollection({ collectionName });
return (
<div className={cls('designable-bar', { active: visible })}>
<div className={'designable-info'}>{collection?.title || collection?.name}</div>

View File

@ -61,7 +61,7 @@ const FormMain = (props: any) => {
const { resource, run, service } = useResource({
onSuccess: (initialValues: any) => {
console.log('onSuccess', { initialValues });
// form.setInitialValues(initialValues);
form.setInitialValues(initialValues);
form.setValues(initialValues);
},
});

View File

@ -3864,6 +3864,14 @@
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
"@types/react-big-calendar@^0.33.1":
version "0.33.1"
resolved "https://registry.npmjs.org/@types/react-big-calendar/-/react-big-calendar-0.33.1.tgz#48463e5b7d27a82dbf5808dea529c03708514b31"
integrity sha512-xX5BaxS+7K62+gtEeg0DqaBInNXDSk2LgkHhbgtqjo/CqwEqB8cwuhIE34XnL33z+pzbn/h7i+0W5fJq0kShdQ==
dependencies:
"@types/prop-types" "*"
"@types/react" "*"
"@types/react-dom@^16.9.8", "@types/react-dom@^17.0.0", "@types/react-dom@^17.0.3":
version "17.0.9"
resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"