feat: improve kanban

This commit is contained in:
chenos 2021-08-19 23:37:02 +08:00
parent dfd4b821ff
commit 5299a2ece9
11 changed files with 790 additions and 202 deletions

View File

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

View File

@ -5,7 +5,11 @@ const [CollectionsProvider, useCollectionsContext] = constate(() => {
const result = useRequest('collections:findAll', { const result = useRequest('collections:findAll', {
formatResult: (result) => result?.data, formatResult: (result) => result?.data,
}); });
return { ...result, collections: result.data || [] }; return {
...result, collections: result.data || [], findCollection(name) {
return result?.data?.find((item) => item.name === name);
}
};
}); });
export { CollectionsProvider, useCollectionsContext }; export { CollectionsProvider, useCollectionsContext };

View File

@ -1,9 +1,13 @@
import { useEffect } from 'react';
import { useCollectionContext } from '../constate'; import { useCollectionContext } from '../constate';
import { Resource } from '../resource'; import { Resource } from '../resource';
export const useResource = (options: any = {}) => { export const useResource = (options: any = {}) => {
const { collection } = useCollectionContext(); const { collection } = useCollectionContext();
const resource = Resource.make(collection?.name); const resource = Resource.make(collection?.name);
useEffect(() => {
options.onSuccess && options.onSuccess({});
}, []);
return { return {
initialValues: {}, initialValues: {},
loading: false, loading: false,

View File

@ -410,40 +410,66 @@ function generateCardItemSchema(component) {
}, },
], ],
properties: { properties: {
card1: { create: {
type: 'void', type: 'void',
'x-component': 'Kanban.Card', title: '添加卡片',
'x-designable-bar': 'Kanban.AddCardDesignableBar',
'x-component': 'Kanban.Card.AddNew',
// 'x-decorator': 'AddNew.Displayed',
'x-component-props': {
type: 'text',
icon: 'PlusOutlined',
},
properties: { properties: {
item1: { modal: {
type: 'void', type: 'void',
'x-component': 'Kanban.Item', title: '新增数据',
'x-decorator': 'Form',
'x-component': 'Action.Drawer',
'x-component-props': {
useOkAction: '{{ Kanban.useCreateAction }}',
},
properties: { properties: {
title: { [uid()]: {
type: 'string', type: 'void',
// title: '标题', 'x-component': 'Grid',
'x-read-pretty': true, 'x-component-props': {
'x-decorator': 'FormItem', addNewComponent: 'AddNew.FormItem',
'x-component': 'Input', },
}, },
}, },
}, },
}, },
}, },
card1: {
type: 'void',
name: uid(),
'x-decorator': 'Form',
'x-component': 'Kanban.Card',
'x-designable-bar': 'Kanban.Card.DesignableBar',
'x-read-pretty': true,
'x-decorator-props': {
useResource: '{{ Kanban.useResource }}',
},
properties: {},
},
view1: { view1: {
type: 'void', type: 'void',
title: '修改数据',
'x-decorator': 'Form',
'x-component': 'Kanban.Card.View', 'x-component': 'Kanban.Card.View',
'x-component-props': {
useOkAction: '{{ Kanban.useUpdateAction }}',
},
'x-decorator-props': {
useResource: '{{ Kanban.useResource }}',
},
properties: { properties: {
item1: { [uid()]: {
type: 'void', type: 'void',
'x-component': 'Kanban.Item', 'x-component': 'Grid',
properties: { 'x-component-props': {
title: { addNewComponent: 'AddNew.FormItem',
type: 'string',
title: '标题',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
}, },
}, },
}, },
@ -1225,46 +1251,58 @@ AddNew.CardItem = observer((props: any) => {
title={item.title} title={item.title}
> >
<Menu.ItemGroup title={'选择分组字段'}> <Menu.ItemGroup title={'选择分组字段'}>
<Menu.Item {item?.generalFields
style={{ minWidth: 150 }} ?.filter((item) => {
key={`Kanban.collection.${item.name}.${view.key}`} return item?.uiSchema?.enum;
onClick={async () => { })
let data = generateCardItemSchema('Kanban'); ?.map((field) => {
const collectionName = item.name; return (
if (schema['key']) { <Menu.Item
data['key'] = uid(); style={{ minWidth: 150 }}
} key={`Kanban.collection.${field.name}.${view.key}`}
if (collectionName) { onClick={async () => {
data['x-component-props'] = let data = generateCardItemSchema('Kanban');
data['x-component-props'] || {}; const collectionName = item.name;
data['x-component-props']['resource'] = if (schema['key']) {
collectionName; data['key'] = uid();
data['x-component-props']['collectionName'] = }
collectionName; if (collectionName) {
// data['x-component-props']['groupField'] = ''; data['x-component-props'] =
} data['x-component-props'] || {};
if (isGridBlock(schema)) { data['x-component-props']['resource'] =
path.pop(); collectionName;
path.pop(); data['x-component-props'][
data = generateGridBlock(data); 'collectionName'
} else if (isGrid(schema)) { ] = collectionName;
data = generateGridBlock(data); data['x-component-props']['groupName'] =
} field.name;
if (data) { }
let s; if (isGridBlock(schema)) {
if (isGrid(schema)) { path.pop();
s = appendChild(data, [...path]); path.pop();
} else if (defaultAction === 'insertAfter') { data = generateGridBlock(data);
s = insertAfter(data, [...path]); } else if (isGrid(schema)) {
} else { data = generateGridBlock(data);
s = insertBefore(data, [...path]); }
} if (data) {
await createSchema(s); let s;
} if (isGrid(schema)) {
}} s = appendChild(data, [...path]);
> } else if (
1 defaultAction === 'insertAfter'
</Menu.Item> ) {
s = insertAfter(data, [...path]);
} else {
s = insertBefore(data, [...path]);
}
await createSchema(s);
}
}}
>
{field?.uiSchema?.title}
</Menu.Item>
);
})}
</Menu.ItemGroup> </Menu.ItemGroup>
</Menu.SubMenu> </Menu.SubMenu>
// <Menu.Item // <Menu.Item
@ -1553,7 +1591,7 @@ AddNew.PaneItem = observer((props: any) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const blockSchema = useContext(BlockSchemaContext); const blockSchema = useContext(BlockSchemaContext);
const useResource = `{{ ${blockSchema['x-component']}.useResource }}`; const useResource = `{{ ${blockSchema['x-component']}.useResource }}`;
console.log('AddNew.PaneItem.useResource', useResource) console.log('AddNew.PaneItem.useResource', useResource);
return ( return (
<Dropdown <Dropdown
trigger={['click']} trigger={['click']}

View File

@ -12,7 +12,7 @@ import cls from 'classnames';
import { Button, Dropdown, Menu, Space } from 'antd'; import { Button, Dropdown, Menu, Space } from 'antd';
import SwitchMenuItem from '../../components/SwitchMenuItem'; import SwitchMenuItem from '../../components/SwitchMenuItem';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { PlusOutlined } from '@ant-design/icons'; import { SettingOutlined } from '@ant-design/icons';
import { Droppable, SortableItem } from '../../components/Sortable'; import { Droppable, SortableItem } from '../../components/Sortable';
export const ActionBar = observer((props: any) => { export const ActionBar = observer((props: any) => {
@ -257,7 +257,7 @@ function AddActionButton() {
className={'designable-btn designable-btn-dash'} className={'designable-btn designable-btn-dash'}
style={{ marginLeft: 8 }} style={{ marginLeft: 8 }}
type={'dashed'} type={'dashed'}
icon={<PlusOutlined />} icon={<SettingOutlined />}
> >
</Button> </Button>

View File

@ -183,7 +183,7 @@ export const Calendar: any = observer((props: any) => {
console.log('field.value', field.value); console.log('field.value', field.value);
return ( return (
<CollectionProvider collectionName={props.collectionName}> <CollectionProvider collectionName={props.collectionName}>
<CalendarContext.Provider value={{ resource, service, props }}> <CalendarContext.Provider value={{ field, resource, service, props }}>
<div {...props} style={{ height: 700 }}> <div {...props} style={{ height: 700 }}>
<Drawer <Drawer
width={'50%'} width={'50%'}

View File

@ -28,6 +28,7 @@ import { uid } from '@formily/shared';
import { getSchemaPath } from '../../components/schema-renderer'; import { getSchemaPath } from '../../components/schema-renderer';
import { useCollection, useCollectionContext } from '../../constate'; import { useCollection, useCollectionContext } from '../../constate';
import { useTable } from '../table'; import { useTable } from '../table';
import { BlockSchemaContext } from '../../context';
export const DesignableBar = observer((props) => { export const DesignableBar = observer((props) => {
const field = useField(); const field = useField();
@ -35,12 +36,14 @@ export const DesignableBar = observer((props) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext); const { dragRef } = useContext(DraggableBlockContext);
const { props: tableProps } = useTable(); const { props: tableProps } = useTable();
const { collection } = useCollectionContext(); const blockSchema = useContext(BlockSchemaContext);
// const collectionName = field.componentProps?.collectionName || tableProps?.collectionName; const collectionName = blockSchema['x-component-props']?.collectionName;
// const { collection } = useCollection({ collectionName }); const { collection } = useCollection({ collectionName });
return ( return (
<div className={cls('designable-bar', { active: visible })}> <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 <span
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@ -0,0 +1,115 @@
import React, { useContext, useMemo, useRef, useState } from 'react';
import { createForm } from '@formily/core';
import {
SchemaOptionsContext,
Schema,
useFieldSchema,
observer,
SchemaExpressionScopeContext,
FormProvider,
useField,
useForm,
RecursionField,
} from '@formily/react';
import {
useSchemaPath,
SchemaField,
useDesignable,
removeSchema,
ISchema,
createSchema,
} 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 { 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 {
useCollection,
useCollectionContext,
useDisplayedMapContext,
} from '../../constate';
import { useTable } from '../table';
import SwitchMenuItem from '../../components/SwitchMenuItem';
export const CardDesignableBar = observer((props) => {
const field = useField();
const { designable, schema, appendChild, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
const { collection, fields } = useCollectionContext();
const displayed = useDisplayedMapContext();
console.log('useDisplayedMapContext', schema);
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={2}>
<Dropdown
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.ItemGroup className={'display-fields'} title={`字段展示`}>
{fields?.map((field) => (
<SwitchMenuItem
key={field.key}
title={field?.uiSchema?.title}
checked={displayed.has(field.name)}
onChange={async (checked) => {
if (!checked) {
const s: any = displayed.get(field.name);
const p = getSchemaPath(s);
const removed = deepRemove(p);
if (!removed) {
console.log('getSchemaPath', p, removed);
return;
}
const last = removed.pop();
displayed.remove(field.name);
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
return;
}
let data: ISchema = {
key: uid(),
type: 'void',
'x-decorator': 'Form.Field.Item',
'x-designable-bar': 'Form.Field.DesignableBar',
'x-component': 'Form.Field',
'x-component-props': {
fieldName: field.name,
},
};
const s = appendChild(data);
await createSchema(s);
}}
/>
))}
</Menu.ItemGroup>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
});

View File

@ -0,0 +1,201 @@
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, Switch } 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 { RandomNameContext } from '.';
import { useCollectionContext, useDisplayedMapContext } from '../../constate';
import SwitchMenuItem from '../../components/SwitchMenuItem';
export const FieldDesignableBar = observer((props) => {
const field = useField();
const { schema, deepRemove } = useDesignable();
const [visible, setVisible] = useState(false);
const { dragRef } = useContext(DraggableBlockContext);
const randomName = useContext(RandomNameContext);
const displayed = useDisplayedMapContext();
const fieldName = schema['x-component-props']?.['fieldName'];
const { getField } = useCollectionContext();
const collectionField = getField(fieldName);
console.log({ collectionField });
const realField = field
.query(field.address.concat(randomName, fieldName))
.take();
return (
<div className={cls('designable-bar', { active: visible })}>
<span
onClick={(e) => {
e.stopPropagation();
}}
className={cls('designable-bar-actions', { active: visible })}
>
<Space size={2}>
<AddNew.FormItem defaultAction={'insertAfter'} ghost />
{dragRef && <DragOutlined ref={dragRef} />}
<Dropdown
placement={'bottomRight'}
trigger={['click']}
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
overlay={
<Menu>
<Menu.Item
key={'update'}
onClick={async () => {
const values = await FormDialog('修改字段名称', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
fieldName: {
type: 'string',
title: '原字段名称',
'x-read-pretty': true,
'x-decorator': 'FormItem',
'x-component': 'Input',
},
title: {
type: 'string',
title: '自定义名称',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
fieldName: collectionField?.uiSchema?.title,
title: schema['title'],
},
});
const title = values.title || null;
field
.query(field.address.concat(randomName, fieldName))
.take((f) => {
f.title = title || collectionField?.uiSchema?.title;
});
schema['title'] = title;
await updateSchema({
key: schema['key'],
title,
});
}}
>
</Menu.Item>
<Menu.Item
style={{ minWidth: 150 }}
onClick={async () => {
const values = await FormDialog('编辑描述', () => {
return (
<FormLayout layout={'vertical'}>
<SchemaField
schema={{
type: 'object',
properties: {
description: {
type: 'string',
'x-component': 'Input.TextArea',
},
},
}}
/>
</FormLayout>
);
}).open({
initialValues: {
description: schema['description'],
},
});
const description = values.description || null;
realField.description =
description || collectionField?.uiSchema?.description;
schema['description'] = description;
await updateSchema({
key: schema['key'],
description,
});
}}
>
</Menu.Item>
<SwitchMenuItem
title={'必填'}
checked={schema.required as boolean}
onChange={(checked) => {
field
.query(field.address.concat(randomName, fieldName))
.take((f: any) => {
f.required = checked;
schema.required = checked;
updateSchema(schema);
});
}}
/>
<Menu.Divider />
<Menu.Item
key={'delete'}
onClick={async () => {
const removed = deepRemove();
const fieldName =
schema['x-component-props']?.['fieldName'];
console.log({ schema, removed, fieldName });
const last = removed.pop();
displayed.remove(fieldName);
if (isGridRowOrCol(last)) {
await removeSchema(last);
}
}}
>
</Menu.Item>
</Menu>
}
>
<MenuOutlined />
</Dropdown>
</Space>
</span>
</div>
);
});

View File

@ -1,5 +1,11 @@
import React from 'react'; import React, { useEffect } from 'react';
import { observer, RecursionField, Schema, useField } from '@formily/react'; import {
observer,
RecursionField,
Schema,
useField,
useForm,
} from '@formily/react';
import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core'; import { DndContext, DragOverlay, closestCorners } from '@dnd-kit/core';
import { import {
SortableContext, SortableContext,
@ -13,7 +19,7 @@ import './style.less';
import { useState } from 'react'; import { useState } from 'react';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import cls from 'classnames'; import cls from 'classnames';
import { Button, Card, Drawer } from 'antd'; import { Button, Card, Drawer, Tag } from 'antd';
import { import {
SchemaRenderer, SchemaRenderer,
useDesignable, useDesignable,
@ -23,6 +29,13 @@ import { useContext } from 'react';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { DesignableBar } from './DesignableBar'; import { DesignableBar } from './DesignableBar';
import { CollectionProvider, useCollectionContext } from '../../constate';
import { Resource } from '../../resource';
import { useRequest } from 'ahooks';
import { Action } from '../action';
import { Form } from '../form';
import { CardDesignableBar } from './CardDesignableBar';
import { VisibleContext } from '../../context';
export function SortableItem(props) { export function SortableItem(props) {
const { const {
@ -59,16 +72,22 @@ export function SortableItem(props) {
const useColumns = () => { const useColumns = () => {
const field = useField<Formily.Core.Models.ArrayField>(); const field = useField<Formily.Core.Models.ArrayField>();
const { getField } = useCollectionContext();
let groupField = field.componentProps.groupField;
const groupName = field.componentProps.groupName;
const collectionField = getField(groupName);
if (collectionField) {
groupField = collectionField?.uiSchema;
}
const values = field.value; const values = field.value;
const [groups, setGroups] = useState(['A', 'B', 'C']); const columns = groupField?.enum?.map((group) => {
const columns = groups.map((name) => {
return { return {
id: name, ...group,
title: name, items: values?.filter((item) => item[groupName] === group.value) || [],
items: values?.filter((item) => item.type === name) || [],
}; };
}); });
return { values, groups, columns, setGroups }; console.log('useColumns', { values, columns });
return { values, columns };
}; };
interface KanbanCardContextProps { interface KanbanCardContextProps {
@ -79,10 +98,19 @@ interface KanbanCardContextProps {
const KanbanCardContext = createContext<KanbanCardContextProps>(null); const KanbanCardContext = createContext<KanbanCardContextProps>(null);
export const Kanban: any = observer(() => { export const KanbanContext = createContext<any>(null);
export const KanbanColumnContext = createContext<any>(null);
export const useKanban = () => {
return useContext(KanbanContext);
};
const InternalKanban = observer((props: any) => {
const field = useField<Formily.Core.Models.ArrayField>(); const field = useField<Formily.Core.Models.ArrayField>();
const groupName = field.componentProps.groupName;
const { values, columns } = useColumns(); const { values, columns } = useColumns();
const { schema } = useDesignable(); const { schema } = useDesignable();
const { collectionName } = props;
const cardSchema = schema.reduceProperties((prev, current) => { const cardSchema = schema.reduceProperties((prev, current) => {
if (current['x-component'] === 'Kanban.Card') { if (current['x-component'] === 'Kanban.Card') {
return current; return current;
@ -95,148 +123,270 @@ export const Kanban: any = observer(() => {
} }
return prev; return prev;
}, null); }, null);
const addCardSchema = schema.reduceProperties((prev, current) => {
if (current['x-component'] === 'Kanban.Card.AddNew') {
return current;
}
return prev;
}, 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) {
field.setValue(data);
},
manual: true,
// refreshDeps: [props.fieldNames],
},
);
useEffect(() => {
service.run({
defaultFilter: props?.defaultFilter,
sort: 'sort',
});
}, [props.defaultFilter]);
return ( return (
<div> <KanbanContext.Provider value={{ field, resource, service, props }}>
{/* <div> <div>
{/* <div>
<Button></Button> <Button></Button>
</div> */} </div> */}
<div className={'kanban'}> <div className={'kanban'}>
<DndContext <div className={'kanban-container'}>
// collisionDetection={closestCorners} <DndContext
onDragEnd={({ active, over }) => { // collisionDetection={closestCorners}
const source = values.find((item) => item.id === active?.id); onDragEnd={({ active, over }) => {
const target = values.find((item) => item.id === over?.id); const source = values.find((item) => item.id === active?.id);
if (source && target) { const target = values.find((item) => item.id === over?.id);
const fromIndex = values.findIndex( if (source && target) {
(item) => item.id === active?.id, const fromIndex = values.findIndex(
); (item) => item.id === active?.id,
const toIndex = values.findIndex((item) => item.id === over?.id); );
// setValues((items) => { const toIndex = values.findIndex(
// const fromIndex = items.findIndex( (item) => item.id === over?.id,
// (item) => item.id === active?.id, );
// ); // setValues((items) => {
// const toIndex = items.findIndex( // const fromIndex = items.findIndex(
// (item) => item.id === over?.id, // (item) => item.id === active?.id,
// ); // );
// return arrayMove(items, fromIndex, toIndex); // const toIndex = items.findIndex(
// }); // (item) => item.id === over?.id,
field.move(fromIndex, toIndex); // );
} // return arrayMove(items, fromIndex, toIndex);
}} // });
onDragOver={({ active, over }) => { field.move(fromIndex, toIndex);
if (!over) { }
return; }}
} onDragOver={({ active, over }) => {
if (active?.id === over?.id) { if (!over) {
return; return;
} }
const source = values.find((item) => item.id === active?.id); if (active?.id === over?.id) {
if (source && over?.data?.current?.type === 'column') { return;
source.type = over.id; }
return; const source = values.find((item) => item.id === active?.id);
} if (source && over?.data?.current?.type === 'column') {
console.log('active.id', active.id, over?.data?.current); source[groupName] = over.id;
const target = values.find((item) => item.id === over?.id); return;
if (source && target) { }
if (source.type !== target.type) { console.log('active.id', active.id, over?.data?.current);
source.type = target.type; const target = values.find((item) => item.id === over?.id);
} if (source && target) {
} if (source[groupName] !== target[groupName]) {
}} source[groupName] = target[groupName];
> }
<DragOverlay style={{ pointerEvents: 'none' }}>aaa</DragOverlay> }
}}
>
<DragOverlay style={{ pointerEvents: 'none' }}>aaa</DragOverlay>
<SortableContext <SortableContext
strategy={horizontalListSortingStrategy} strategy={horizontalListSortingStrategy}
items={columns} items={
> columns?.map((column) => ({
{columns.map((column) => ( ...column,
<SortableItem id: column.value,
key={column.id} })) || []
type={'column'} }
className={'column'}
id={column.id}
> >
<div className={'column-title'}>{column.title}</div> {columns?.map((column) => (
<SortableContext <SortableItem
strategy={verticalListSortingStrategy} id={column.value}
items={column.items} key={column.value}
> type={'column'}
{column.items.map((item) => { className={'column'}
const index = values.findIndex( >
(value) => value.id === item.id, <KanbanColumnContext.Provider value={column}>
); <div className={'column-title'}>
return ( <Tag color={column.color}>{column.label}</Tag>
<SortableItem </div>
key={item.id} <SortableContext
className={'item'} strategy={verticalListSortingStrategy}
id={item.id} items={column.items}
type={'item'}
> >
<KanbanCardContext.Provider {column.items.map((item) => {
value={{ const index = values.findIndex(
index: index, (value) => value.id === item.id,
record: item, );
viewSchema: cardViewSchema, return (
}} <SortableItem
> key={item.id}
<RecursionField name={index} schema={cardSchema} /> className={'item'}
</KanbanCardContext.Provider> id={item.id}
</SortableItem> type={'item'}
); >
})} <KanbanCardContext.Provider
</SortableContext> value={{
<Button index: index,
type={'text'} record: item,
icon={<PlusOutlined />} viewSchema: cardViewSchema,
onClick={() => { }}
field.push({ >
id: uid(), <RecursionField
type: column.id, name={index}
title: uid(), schema={cardSchema}
}); />
}} </KanbanCardContext.Provider>
</SortableItem>
);
})}
</SortableContext>
<RecursionField
name={addCardSchema?.name}
schema={addCardSchema}
/>
{/* <Button
type={'text'}
icon={<PlusOutlined />}
onClick={() => {
field.push({
id: uid(),
title: uid(),
[groupName]: column.value,
});
}}
>
</Button> */}
</KanbanColumnContext.Provider>
</SortableItem>
))}
<div
id={'addColumn'}
key={'addColumn'}
// type={'column'}
className={'column add-column'}
> >
{/* <span className={'add-column-plus'}/> */}
</Button> <span><PlusOutlined/> </span>
</SortableItem> </div>
))} </SortableContext>
</SortableContext> </DndContext>
</DndContext> </div>
</div>
</div> </div>
</div> </KanbanContext.Provider>
); );
}); });
export const Kanban: any = observer((props: any) => {
return (
<CollectionProvider collectionName={props.collectionName}>
<InternalKanban {...props} />
</CollectionProvider>
);
});
Kanban.useCreateAction = () => {
const { service, resource, props } = useKanban();
const column = useContext(KanbanColumnContext);
const groupName = props.groupName;
const form = useForm();
return {
async run() {
await resource.create({
...form.values,
[groupName]: column.value,
});
await form.reset();
return service.refresh();
},
};
};
Kanban.useUpdateAction = () => {
const { service, resource, props } = useKanban();
const ctx = useContext(KanbanCardContext);
const form = useForm();
return {
async run() {
await resource.save(form.values, {
resourceKey: ctx.record.id,
});
await service.refresh();
},
};
};
Kanban.useResource = ({ onSuccess }) => {
const { props } = useKanban();
const { collection } = useCollectionContext();
const ctx = useContext(KanbanCardContext);
const resource = Resource.make({
resourceName: collection?.name || props.collectionName,
resourceKey: ctx?.record?.id,
});
console.log(
'collection?.name || props.collectionName',
collection?.name || props.collectionName,
);
const service = useRequest(
(params?: any) => {
return Promise.resolve(ctx.record);
},
{
// formatResult: (result) => result?.data,
onSuccess,
refreshDeps: [ctx?.record],
// manual,
},
);
return { resource, service, initialValues: service.data, ...service };
};
Kanban.Card = observer((props) => { Kanban.Card = observer((props) => {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const { index, viewSchema } = useContext(KanbanCardContext); const { index, viewSchema } = useContext(KanbanCardContext);
const { DesignableBar } = useDesignable();
const { children, ...others } = props;
return ( return (
<> <VisibleContext.Provider value={[visible, setVisible]}>
<Card <Card
onClick={(e) => { onClick={(e) => {
setVisible(true); setVisible(true);
}} }}
bordered={false} bordered={false}
{...others}
> >
{props.children} {children}
<DesignableBar />
</Card> </Card>
<Drawer <RecursionField name={index} schema={viewSchema} />
width={'50%'} </VisibleContext.Provider>
title={'查看数据'}
visible={visible}
onClose={() => {
setVisible(false);
}}
>
<RecursionField name={index} schema={viewSchema} />
</Drawer>
</>
); );
}); });
Kanban.Card.View = observer((props) => { Kanban.Card.View = Action.Drawer;
return <div>{props.children}</div>; Kanban.Card.AddNew = Action;
});
Kanban.DesignableBar = DesignableBar; Kanban.DesignableBar = DesignableBar;
Kanban.Card.DesignableBar = CardDesignableBar;

View File

@ -1,9 +1,38 @@
.column { .column {
position: relative; position: relative;
width: 25vw; width: 300px;
background: #f9f9f9; background: #f9f9f9;
margin: 0 12px; margin-right: 24px;
padding: 24px; padding: 24px;
&.add-column {
padding: 0;
margin-right: 0;
background: rgba(241, 139, 98, 0.05);
cursor: pointer;
color: #F18B62;
text-align: center;
justify-content: center;
align-items: center;
display: flex;
.add-column-plus {
position: relative;
&::before {
position: absolute;
width: 1px;
height: 50px;
background: #F18B62;
}
&::after {
position: absolute;
width: 50px;
height: 1px;
background: #F18B62;
}
}
&:hover {
background: rgba(241, 139, 98, 0.1);
}
}
} }
.column-title { .column-title {
@ -14,7 +43,12 @@
.kanban { .kanban {
display: flex; display: flex;
margin: 0 -12px; margin: 0;
overflow: auto;
}
.kanban-container {
display: flex;
} }
.item { .item {
@ -31,4 +65,42 @@
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
} }
.kanban .ant-card-body {
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;
background: none !important;
border: 2px solid rgba(241, 139, 98, 0.3) !important;
&.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;
}
}
}
}