improve code

This commit is contained in:
chenos 2021-08-03 12:12:29 +08:00
parent 754be7d1d1
commit c23d2c5c3c
7 changed files with 340 additions and 182 deletions

View File

@ -38,13 +38,16 @@ export interface SortableItemProps {}
export const SortableItemContext = createContext<any>({}); export const SortableItemContext = createContext<any>({});
export function DragHandle() { export function DragHandle(props) {
const { component, ...others } = props;
const Icon = component || DragOutlined;
return ( return (
<SortableItemContext.Consumer> <SortableItemContext.Consumer>
{({ setDraggableNodeRef, attributes, listeners }) => {({ setDraggableNodeRef, attributes, listeners }) =>
setDraggableNodeRef && ( setDraggableNodeRef && (
<DragOutlined <Icon
ref={setDraggableNodeRef} ref={setDraggableNodeRef}
{...others}
{...attributes} {...attributes}
{...listeners} {...listeners}
/> />

View File

@ -236,22 +236,27 @@ function generateCardItemSchema(component) {
'x-component': 'Action.Dropdown', 'x-component': 'Action.Dropdown',
'x-component-props': {}, 'x-component-props': {},
properties: { properties: {
[uid()]: { // [uid()]: {
type: 'void', // type: 'void',
title: '操作 1', // title: '操作 1',
'x-component': 'Menu.Action', // 'x-component': 'Menu.Action',
'x-component-props': { // 'x-component-props': {
style: { // style: {
minWidth: 150, // minWidth: 150,
}, // },
disabled: true, // disabled: true,
}, // },
}, // },
[uid()]: { [uid()]: {
type: 'void', type: 'void',
name: 'action1', name: 'action1',
title: '查看', title: '查看',
'x-component': 'Menu.Action', 'x-component': 'Menu.Action',
'x-component-props': {
style: {
minWidth: 150,
},
},
'x-designable-bar': 'Table.Action.DesignableBar', 'x-designable-bar': 'Table.Action.DesignableBar',
'x-action-type': 'view', 'x-action-type': 'view',
properties: { properties: {

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { forwardRef, useState } from 'react';
import { import {
observer, observer,
connect, connect,
@ -11,7 +11,7 @@ import {
FormConsumer, FormConsumer,
} from '@formily/react'; } from '@formily/react';
import { ArrayCollapse, FormLayout } from '@formily/antd'; import { ArrayCollapse, FormLayout } from '@formily/antd';
import { uid } from '@formily/shared'; import { uid, isValid } from '@formily/shared';
import '@formily/antd/lib/form-tab/style'; import '@formily/antd/lib/form-tab/style';
import { import {
Collapse, Collapse,
@ -32,6 +32,7 @@ import {
DatabaseOutlined, DatabaseOutlined,
PlusOutlined, PlusOutlined,
CloseOutlined, CloseOutlined,
MenuOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import cls from 'classnames'; import cls from 'classnames';
import './style.less'; import './style.less';
@ -39,8 +40,53 @@ import Modal from 'antd/lib/modal/Modal';
import { clone, cloneDeep, get } from 'lodash'; import { clone, cloneDeep, get } from 'lodash';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { createOrUpdateCollection, deleteCollection } from '..'; import {
collectionMoveToAfter,
createOrUpdateCollection,
deleteCollection,
} from '..';
import { useCollectionsContext } from '../../constate/Collections'; import { useCollectionsContext } from '../../constate/Collections';
import {
DragHandle,
SortableItem,
SortableItemContext,
} from '../../components/Sortable';
import { DndContext, DragOverlay } from '@dnd-kit/core';
import { createPortal } from 'react-dom';
interface SelectOptionProps {
id: any;
title: string;
data?: any;
onRemove?: any;
showRemove?: boolean;
}
function SelectOption(props: SelectOptionProps) {
const { id, data, onRemove, showRemove } = props;
return (
<SortableItem id={id} data={data}>
<div
style={{
display: 'flex',
// justifyContent: 'space-between',
alignItems: 'center',
}}
>
<DragHandle
component={forwardRef<any>((props, ref) => {
return <MenuOutlined {...props} ref={ref} />;
})}
/>
<span style={{ width: 8 }} />
{data.title}
{showRemove && (
<DeleteOutlined style={{ marginLeft: 'auto' }} onClick={onRemove} />
)}
</div>
</SortableItem>
);
}
export const DatabaseCollection = observer((props) => { export const DatabaseCollection = observer((props) => {
const field = useField<Formily.Core.Models.ArrayField>(); const field = useField<Formily.Core.Models.ArrayField>();
@ -51,6 +97,7 @@ export const DatabaseCollection = observer((props) => {
const form = useForm(); const form = useForm();
const [newValue, setNewValue] = useState(''); const [newValue, setNewValue] = useState('');
const { loading, refresh, collections = [] } = useCollectionsContext(); const { loading, refresh, collections = [] } = useCollectionsContext();
const [dragOverlayContent, setDragOverlayContent] = useState('');
useEffect(() => { useEffect(() => {
field.setValue(collections); field.setValue(collections);
@ -87,101 +134,118 @@ export const DatabaseCollection = observer((props) => {
}} }}
title={ title={
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<Select <DndContext
loading={loading} onDragStart={(event) => {
value={activeIndex} setDragOverlayContent(event.active?.data?.current?.title || '');
style={{ minWidth: 300, textAlign: 'center' }}
onChange={(value) => {
setActiveIndex(value as any);
}} }}
open={open} onDragEnd={async (event) => {
onDropdownVisibleChange={setOpen} const sourceName = event.active?.data?.current?.name;
optionLabelProp={'label'} const targetName = event.over?.data?.current?.name;
dropdownRender={(menu) => { console.log({ sourceName, targetName });
return ( await collectionMoveToAfter(sourceName, targetName);
<div> await refresh();
{menu}
<Divider style={{ margin: '5px 0 4px' }} />
<div
style={{
cursor: 'pointer',
padding: '5px 12px',
}}
>
<Input.Search
size={'middle'}
placeholder={'新增数据表'}
enterButton={<PlusOutlined />}
value={newValue}
onChange={(e) => {
setNewValue(e.target.value);
}}
onSearch={async (value) => {
const data = {
name: `t_${uid()}`,
title: value,
fields: getDefaultFields(),
};
field.push(data);
setActiveIndex(field.value.length - 1);
setOpen(false);
setNewValue('');
await createOrUpdateCollection(data);
await refresh();
}}
/>
</div>
</div>
);
}} }}
> >
{field.value?.map((item, index) => { {createPortal(
return ( <DragOverlay
<Select.Option zIndex={2000}
key={index} style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}
value={index} >
label={`${item.title || '未命名'}${ {dragOverlayContent}
item.unsaved ? ' (未保存)' : '' </DragOverlay>,
}`} document.body,
> )}
<div <Select
style={{ loading={loading}
display: 'flex', value={activeIndex}
justifyContent: 'space-between', style={{ minWidth: 300, textAlign: 'center' }}
alignItems: 'center', onChange={(value) => {
}} setActiveIndex(value as any);
> }}
{item.title || '未命名'}{' '} open={open}
{item.unsaved ? '(未保存)' : ''} onDropdownVisibleChange={setOpen}
{item.privilege !== 'undelete' && ( optionLabelProp={'label'}
<DeleteOutlined dropdownRender={(menu) => {
onClick={async (e) => { return (
e.stopPropagation(); <div>
field.remove(index); {menu}
if (field.value?.length === 0) { <Divider style={{ margin: '5px 0 4px' }} />
field.push({ <div
name: `t_${uid()}`, style={{
unsaved: true, cursor: 'pointer',
fields: getDefaultFields(), padding: '5px 12px',
}); }}
} >
if (activeIndex === index) { <Input.Search
setActiveIndex(0); size={'middle'}
} else if (activeIndex > index) { placeholder={'新增数据表'}
setActiveIndex(activeIndex - 1); enterButton={<PlusOutlined />}
} value={newValue}
if (item.name) { onChange={(e) => {
await deleteCollection(item.name); setNewValue(e.target.value);
await refresh(); }}
} onSearch={async (value) => {
const data = {
name: `t_${uid()}`,
title: value,
fields: getDefaultFields(),
};
field.push(data);
setActiveIndex(field.value.length - 1);
setOpen(false);
setNewValue('');
await createOrUpdateCollection(data);
await refresh();
}} }}
/> />
)} </div>
</div> </div>
</Select.Option> );
); }}
})} >
</Select> {field.value?.map((item, index) => {
return (
<Select.Option
key={index}
value={index}
label={`${item.title || '未命名'}${
item.unsaved ? ' (未保存)' : ''
}`}
>
<SelectOption
id={item.name}
title={item.title || '未命名'}
data={{
title: item.title,
name: item.name,
}}
showRemove={item.privilege !== 'undelete'}
onRemove={async (e) => {
e.stopPropagation();
field.remove(index);
if (field.value?.length === 0) {
field.push({
name: `t_${uid()}`,
unsaved: true,
fields: getDefaultFields(),
});
}
if (activeIndex === index) {
setActiveIndex(0);
} else if (activeIndex > index) {
setActiveIndex(activeIndex - 1);
}
if (item.name) {
await deleteCollection(item.name);
await refresh();
}
}}
/>
</Select.Option>
);
})}
</Select>
</DndContext>
</div> </div>
} }
visible={visible} visible={visible}
@ -228,88 +292,124 @@ export const DatabaseField: any = observer((props) => {
} }
}, []); }, []);
const [activeKey, setActiveKey] = useState(null); const [activeKey, setActiveKey] = useState(null);
const [dragOverlayContent, setDragOverlayContent] = useState('');
return ( return (
<div> <div>
<Collapse <DndContext
activeKey={activeKey} onDragStart={(event) => {
onChange={(key) => { setDragOverlayContent(event.active?.data?.current?.title || '');
setActiveKey(key); }}
onDragEnd={async (event) => {
const fromIndex = event.active?.data?.current?.index;
const toIndex = event.over?.data?.current?.index;
if (isValid(fromIndex) && isValid(toIndex)) {
field.move(fromIndex, toIndex);
}
}} }}
className={cls({ empty: !field.value?.length })}
accordion
> >
{field.value?.map((item, index) => { {createPortal(
if (!item.interface) { <DragOverlay
return; zIndex={2000}
} style={{ pointerEvents: 'none', whiteSpace: 'nowrap' }}
const schema = cloneDeep(interfaces.get(item.interface)); >
if (!schema) { {dragOverlayContent}
console.error('schema invalid'); </DragOverlay>,
return; document.body,
} )}
const path = field.address.concat(index); <Collapse
const errors = field.form.queryFeedbacks({ activeKey={activeKey}
type: 'error', onChange={(key) => {
address: `*(${path},${path}.*)`, setActiveKey(key);
}); }}
return ( className={cls({ empty: !field.value?.length })}
<Collapse.Panel accordion
header={ >
<> {field.value?.map((item, index) => {
{(item.uiSchema && item.uiSchema.title) || ( if (!item.interface) {
<i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i> return;
)}{' '} }
<Tag const schema = cloneDeep(interfaces.get(item.interface));
className={item.privilege ? cls(item.privilege) : undefined} if (!schema) {
console.error('schema invalid');
return;
}
const path = field.address.concat(index);
const errors = field.form.queryFeedbacks({
type: 'error',
address: `*(${path},${path}.*)`,
});
return (
<Collapse.Panel
header={
<SortableItem
id={item.key}
className={'sortable-item'}
data={{
index,
title: item?.uiSchema?.title,
}}
> >
{schema.title} <DragHandle className={'drag-handle'} />
</Tag> {(item.uiSchema && item.uiSchema.title) || (
<span style={{ color: 'rgba(0, 0, 0, 0.25)', fontSize: 14 }}> <i style={{ color: 'rgba(0, 0, 0, 0.25)' }}></i>
{item.name} )}{' '}
</span> <Tag
</> className={
} item.privilege ? cls(item.privilege) : undefined
extra={ }
item.privilege === 'undelete' >
? [] {schema.title}
: [ </Tag>
<Badge key={'1'} count={errors.length} />, <span
<DeleteOutlined style={{ color: 'rgba(0, 0, 0, 0.25)', fontSize: 14 }}
key={'2'} >
onClick={(e) => { {item.name}
e.stopPropagation(); </span>
field.remove(index); </SortableItem>
}}
/>,
]
}
key={item.key}
>
<RecursionField
key={`${item.key}_${index}`}
name={index}
schema={
new Schema({
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
// labelCol: 4,
// wrapperCol: 20,
},
properties: schema.properties,
},
},
})
} }
/> extra={
</Collapse.Panel> item.privilege === 'undelete'
); ? []
})} : [
</Collapse> <Badge key={'1'} count={errors.length} />,
<DeleteOutlined
key={'2'}
onClick={(e) => {
e.stopPropagation();
field.remove(index);
}}
/>,
]
}
key={item.key}
forceRender
>
<RecursionField
key={`${item.key}_${index}`}
name={index}
schema={
new Schema({
type: 'object',
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
layout: 'vertical',
// labelCol: 4,
// wrapperCol: 20,
},
properties: schema.properties,
},
},
})
}
/>
</Collapse.Panel>
);
})}
</Collapse>
</DndContext>
<Dropdown <Dropdown
placement={'bottomCenter'} placement={'bottomCenter'}
overlayClassName={'all-fields'} overlayClassName={'all-fields'}

View File

@ -26,3 +26,28 @@
color: rgba(0, 0, 0, 0.25); color: rgba(0, 0, 0, 0.25);
cursor: not-allowed; cursor: not-allowed;
} }
.ant-collapse-header {
position: relative;
&:hover {
.drag-handle {
opacity: 1;
}
}
.drag-handle {
opacity: 0;
margin-right: 6px;
background-color: #fafafa;
position: relative;
z-index: 3;
}
.ant-collapse-arrow {
position: absolute;
top: 18px;
}
.ant-collapse-extra {
position: absolute;
right: 16px;
top: 12px;
}
}

View File

@ -60,6 +60,20 @@ export async function createSchema(schema: ISchema) {
}); });
} }
export async function collectionMoveToAfter(source, target) {
if (source && target) {
return request(`collections:sort/${source}`, {
method: 'post',
data: {
field: 'sort',
target: {
name: target,
},
},
});
}
}
export async function updateSchema(schema: ISchema) { export async function updateSchema(schema: ISchema) {
if (!schema) { if (!schema) {
return; return;

View File

@ -46,6 +46,9 @@ export const Tabs: any = observer((props: any) => {
if (!path1 || !path2) { if (!path1 || !path2) {
return; return;
} }
if (path1.join('.') === path2.join('.')) {
return;
}
const data = findPropertyByPath(root, path1); const data = findPropertyByPath(root, path1);
if (!data) { if (!data) {
return; return;

View File

@ -6,7 +6,7 @@ import { cloneDeep, omit } from 'lodash';
export const findAll = async (ctx: actions.Context, next: actions.Next) => { export const findAll = async (ctx: actions.Context, next: actions.Next) => {
const Collection = ctx.db.getModel('collections'); const Collection = ctx.db.getModel('collections');
const collections = await Collection.findAll(Collection.parseApiJson({ const collections = await Collection.findAll(Collection.parseApiJson({
sort: '-created_at', sort: 'sort',
})); }));
const data = []; const data = [];
for (const collection of collections) { for (const collection of collections) {
@ -29,6 +29,14 @@ export const createOrUpdate = async (ctx: actions.Context, next: actions.Next) =
} else { } else {
await collection.update(values); await collection.update(values);
} }
if (values.fields) {
values.fields = values.fields.map((field, index) => {
return {
...field,
sort: index + 1,
}
})
}
await collection.updateAssociations(values); await collection.updateAssociations(values);
await collection.migrate(); await collection.migrate();
} catch (error) { } catch (error) {