feat: 优化界面显示,对deck添加编辑功能
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { useEditStore } from '../store';
|
||||
import { Button, Input, message, Modal, Table } from 'antd';
|
||||
import { Button, Input, message, Modal, Table, Tooltip } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { Form } from 'antd';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useToCodeEditor } from '@/pages/code-editor';
|
||||
|
||||
import { CardBlank } from '@/components/card/CardBlank';
|
||||
import { DeleteOutlined, EditOutlined, ForkOutlined, GoldOutlined, PlusOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import { isObjectNull } from '@/utils/is-null';
|
||||
const FormModal = () => {
|
||||
const [form] = Form.useForm();
|
||||
const editStore = useEditStore(
|
||||
@@ -15,6 +17,7 @@ const FormModal = () => {
|
||||
showEdit: state.showEditModal,
|
||||
setShowEdit: state.setShowEditModal,
|
||||
formData: state.formData,
|
||||
setFormData: state.setFormData,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
@@ -22,17 +25,26 @@ const FormModal = () => {
|
||||
useEffect(() => {
|
||||
const open = editStore.showEdit;
|
||||
if (open) {
|
||||
form.setFieldsValue(editStore.formData || {});
|
||||
} else {
|
||||
form.resetFields();
|
||||
if (isObjectNull(editStore.formData.data)) {
|
||||
form.setFieldsValue({});
|
||||
} else form.setFieldsValue(editStore.formData);
|
||||
}
|
||||
}, [editStore.showEdit]);
|
||||
const onFinish = async (values: any) => {
|
||||
let defaultData = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: {},
|
||||
};
|
||||
if (!isEdit) {
|
||||
values.data = defaultData;
|
||||
}
|
||||
editStore.updateData(values);
|
||||
};
|
||||
const onClose = () => {
|
||||
editStore.setShowEdit(false);
|
||||
form.resetFields();
|
||||
editStore.setFormData({});
|
||||
};
|
||||
const isEdit = editStore.formData.id;
|
||||
return (
|
||||
@@ -52,9 +64,15 @@ const FormModal = () => {
|
||||
<Form.Item name='title' label='title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{/* <Form.Item name='code' label='code'>
|
||||
<TextArea value={containerStore.formData.code} />
|
||||
</Form.Item> */}
|
||||
<Form.Item name='description' label='description'>
|
||||
<Input.TextArea lang={'markdown'} />
|
||||
</Form.Item>
|
||||
<Form.Item name='type' label='type' noStyle hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name='data' label='data' noStyle hidden>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item label=' ' colon={false}>
|
||||
<Button type='primary' htmlType='submit'>
|
||||
Submit
|
||||
@@ -86,99 +104,80 @@ export const List = () => {
|
||||
editStore.getList();
|
||||
}, []);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
render: (text: string) => {
|
||||
return (
|
||||
<div
|
||||
className='w-40 truncate cursor-pointer'
|
||||
title={text}
|
||||
onClick={() => {
|
||||
copy(text);
|
||||
message.success('copy success');
|
||||
}}>
|
||||
{text}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Title',
|
||||
dataIndex: 'title',
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Operation',
|
||||
dataIndex: 'operation',
|
||||
render: (text: string, record: any) => {
|
||||
return (
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={() => {
|
||||
editStore.setFormData(record);
|
||||
editStore.setShowEdit(true);
|
||||
}}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navicate('/panel/flow/' + record.id);
|
||||
}}>
|
||||
Flow
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navicate('/panel/deck/' + record.id);
|
||||
}}>
|
||||
Deck
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
toCodeEditor.toPagePage(record);
|
||||
}}>
|
||||
Source Data Editor
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
onClick={() => {
|
||||
editStore.deleteData(record.id);
|
||||
}}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className='w-full h-full flex flex-col'>
|
||||
<div className='mb-2 w-full p-2 bg-white rounded-lg'>
|
||||
<div className='w-full h-full flex'>
|
||||
<div className='p-2 bg-white rounded-r-lg'>
|
||||
<Button
|
||||
className='w-20 '
|
||||
className='w-10 '
|
||||
type='primary'
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
editStore.setFormData({});
|
||||
editStore.setShowEdit(true);
|
||||
}}>
|
||||
Add
|
||||
</Button>
|
||||
}}></Button>
|
||||
</div>
|
||||
<div className='flex-grow overflow-scroll'>
|
||||
<Table
|
||||
pagination={false}
|
||||
scroll={{
|
||||
y: 600,
|
||||
}}
|
||||
loading={editStore.loading}
|
||||
dataSource={editStore.list}
|
||||
rowKey='id'
|
||||
columns={columns}
|
||||
/>
|
||||
<div className='flex-grow overflow-scroll scrollbar mt-4'>
|
||||
<div className=''>
|
||||
<div className=' flex flex-wrap gap-10 justify-center h-full overflow-auto scrollbar'>
|
||||
{editStore.list.length > 0 &&
|
||||
editStore.list.map((item, index) => {
|
||||
return (
|
||||
<div className='card w-[300px]' key={index}>
|
||||
<div className='card-title'>{item.title}</div>
|
||||
<div className='card-subtitle'> {item.description}</div>
|
||||
<div className='mt-4'>
|
||||
<Button.Group>
|
||||
<Tooltip title='Edit'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
editStore.setFormData(item);
|
||||
editStore.setShowEdit(true);
|
||||
}}
|
||||
icon={<EditOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='to flow'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navicate('/panel/flow/' + item.id);
|
||||
}}
|
||||
icon={<ForkOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='to deck'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
navicate('/panel/deck/' + item.id);
|
||||
}}
|
||||
icon={<GoldOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='to code editor'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
toCodeEditor.toPagePage(item);
|
||||
}}
|
||||
icon={<ToolOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title='delete'>
|
||||
<Button
|
||||
onClick={() => {
|
||||
editStore.deleteData(item.id);
|
||||
}}
|
||||
icon={<DeleteOutlined />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<CardBlank className='w-[300px]' />
|
||||
{editStore.list.length === 0 && <div className='text-center text-gray-500'>No data</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-2'></div>
|
||||
<FormModal />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,13 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { Container, useAddNode, ContainerMenusList, useMenuFlow } from '@abearxiong/flows';
|
||||
import { useMenuEmitter, ContainerMenusKeys } from '@abearxiong/flows';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Button } from 'antd';
|
||||
import { Button, message, Tooltip } from 'antd';
|
||||
import { usePanelStore } from '../store';
|
||||
import { CompassOutlined, PlusOutlined, SaveOutlined } from '@ant-design/icons';
|
||||
import { generateId } from '@/utils/nanoid';
|
||||
import { useMessage } from '@/hooks';
|
||||
import { NodeProperties } from './properties/NodeProperties';
|
||||
import { emitter } from '@abearxiong/container';
|
||||
// router: Router
|
||||
const nodeTypes = {
|
||||
container: Container,
|
||||
@@ -36,7 +41,6 @@ export const Flow = () => {
|
||||
const ReactFlowApp = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState<any>([]);
|
||||
|
||||
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
const panelStore = usePanelStore(
|
||||
useShallow((state) => {
|
||||
@@ -49,15 +53,47 @@ const ReactFlowApp = () => {
|
||||
useEffect(() => {
|
||||
if (panelStore.data?.id) {
|
||||
const { data } = panelStore.data || {};
|
||||
const nodes = data.nodes || [];
|
||||
const edges = data.edges || [];
|
||||
setNodes(nodes);
|
||||
console.log('data', panelStore.data);
|
||||
const nodes = [...data.nodes];
|
||||
const edges = [...data.edges];
|
||||
console.log('nodes', nodes);
|
||||
if (nodes.length === 0) {
|
||||
nodes.push({
|
||||
id: panelStore.data.id,
|
||||
data: {
|
||||
label: '容器',
|
||||
root: true,
|
||||
},
|
||||
position: {
|
||||
x: 100,
|
||||
y: 100,
|
||||
},
|
||||
type: 'container',
|
||||
});
|
||||
}
|
||||
console.log('nodes', nodes);
|
||||
setNodes(nodes as any);
|
||||
setEdges(edges);
|
||||
} else {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
}
|
||||
}, [panelStore.data]);
|
||||
useEffect(() => {
|
||||
emitter.on('setNodes', _setNodes);
|
||||
emitter.on('setEdges', _setEdges);
|
||||
return () => {
|
||||
emitter.off('setNodes', _setNodes);
|
||||
emitter.off('setEdges', _setEdges);
|
||||
};
|
||||
}, []);
|
||||
const _setNodes = (data: any) => {
|
||||
setNodes(data);
|
||||
};
|
||||
const _setEdges = (data: any) => {
|
||||
setEdges(data);
|
||||
};
|
||||
|
||||
const { menuCom, onContextMenu, onClose } = useMenuFlow(
|
||||
(item, cacheData) => {
|
||||
emit({
|
||||
@@ -72,11 +108,37 @@ const ReactFlowApp = () => {
|
||||
|
||||
const { emit } = useMenuEmitter<ContainerMenusKeys>({
|
||||
preview: ({ menu, data }) => {
|
||||
console.log('preview', data);
|
||||
console.log('preview', data, message);
|
||||
if (data?.data?.cid) {
|
||||
window.open(`/container/preview/${data.data.cid}`, '_blank');
|
||||
} else {
|
||||
message.error('未绑定容器');
|
||||
}
|
||||
},
|
||||
code: ({ menu, data }) => {
|
||||
// console.log('edit', data);
|
||||
const nodeData = data?.data;
|
||||
if (!nodeData.cid) {
|
||||
message.error('请先绑定容器');
|
||||
return;
|
||||
}
|
||||
message.error('developing');
|
||||
},
|
||||
copy: ({ menu, data }) => {
|
||||
message.error('developing');
|
||||
},
|
||||
delete: ({ menu, data }) => {
|
||||
console.log('delete', data);
|
||||
const nodeData = data?.data;
|
||||
if (nodeData.root) {
|
||||
message.error('root node can not be deleted');
|
||||
return;
|
||||
}
|
||||
setNodes((nodes) => nodes.filter((item: any) => item.id !== data.id));
|
||||
},
|
||||
internalData: ({ menu, data }) => {
|
||||
message.error('developing');
|
||||
},
|
||||
});
|
||||
const { onNeedAdd, onAdd, onMouseMove, adding } = useAddNode();
|
||||
const onSave = useCallback(() => {
|
||||
@@ -124,26 +186,41 @@ const ReactFlowApp = () => {
|
||||
}}>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
{/* <Background gap={[14, 14]} size={2} color='#E4E5E7' /> */}
|
||||
<Background color='#000' />
|
||||
<Panel>{menuCom}</Panel>
|
||||
<Panel>
|
||||
<div className='flex gap-2'>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={(e) => {
|
||||
onNeedAdd({ id: nanoid(6), data: { label: '容器' }, type: 'container' }, e);
|
||||
}}>
|
||||
测试添加按钮
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onSave();
|
||||
}}>
|
||||
保存
|
||||
</Button>
|
||||
<Button.Group>
|
||||
<Tooltip title='添加节点'>
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
onNeedAdd({ id: 'flow' + generateId(), data: { label: '容器' }, type: 'container' }, e);
|
||||
}}></Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='save'>
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
onClick={() => {
|
||||
onSave();
|
||||
}}></Button>
|
||||
</Tooltip>
|
||||
<Tooltip title='preview'>
|
||||
<Button
|
||||
icon={<CompassOutlined />}
|
||||
onClick={() => {
|
||||
const id = panelStore.data?.id;
|
||||
if (!id) {
|
||||
message.error('ID is required');
|
||||
return;
|
||||
}
|
||||
window.open(`/panel/deck/${id}`, '_blank');
|
||||
}}></Button>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</Panel>
|
||||
<NodeProperties />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
2
src/pages/panel/flow/message.ts
Normal file
2
src/pages/panel/flow/message.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
export const emitter = new EventEmitter();
|
||||
103
src/pages/panel/flow/properties/NodeProperties.tsx
Normal file
103
src/pages/panel/flow/properties/NodeProperties.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Panel, useReactFlow, useStore, useStoreApi } from '@xyflow/react';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { Button, Form, Input, message } from 'antd';
|
||||
import { Select } from '@/pages/container/module/Select';
|
||||
import { SaveOutlined } from '@ant-design/icons';
|
||||
import { emitter } from '@abearxiong/container';
|
||||
import { usePanelStore } from '../../store';
|
||||
export const NodeProperties = () => {
|
||||
const reactflow = useReactFlow();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
const panelStore = usePanelStore((state) => {
|
||||
return {
|
||||
updateNodeData: state.updateNodeData,
|
||||
};
|
||||
});
|
||||
const store = useStore((state) => {
|
||||
const setNode = (node: any) => {
|
||||
const newNodes = state.nodes.map((item) => {
|
||||
if (item.id === node.id) {
|
||||
// return { ...item, ...node };
|
||||
return { ...node };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
// console.log('newNodes', newNodes);
|
||||
// state.setNodes(newNodes); // 会丢失数据,因为最终没有调用context的setNodes方法
|
||||
emitter.emit('setNodes', newNodes);
|
||||
};
|
||||
return {
|
||||
nodesFocusable: state.nodesFocusable,
|
||||
selected: state.nodes.filter((node) => node.selected),
|
||||
setNode: setNode,
|
||||
};
|
||||
});
|
||||
const [nodeData] = store.selected as any[];
|
||||
useEffect(() => {
|
||||
if (!nodeData) return;
|
||||
// console.log('nodeData', nodeData);
|
||||
const { data } = nodeData || {};
|
||||
form.setFieldsValue({
|
||||
cid: data.cid,
|
||||
title: data.title,
|
||||
});
|
||||
}, [nodeData]);
|
||||
const onSave = () => {
|
||||
const values = form.getFieldsValue();
|
||||
// console.log('values', values);
|
||||
const { cid, title } = values;
|
||||
if (!cid) {
|
||||
message.error('请选择容器');
|
||||
return;
|
||||
}
|
||||
const { data, id, position, type } = nodeData || {};
|
||||
// console.log('data', data, cid);
|
||||
const newNodeData = {
|
||||
id,
|
||||
position,
|
||||
selected: true,
|
||||
type,
|
||||
data: {
|
||||
...data,
|
||||
cid,
|
||||
title,
|
||||
},
|
||||
};
|
||||
// console.log('newNodeData', newNodeData, nodeData);
|
||||
store.setNode(newNodeData);
|
||||
panelStore.updateNodeData(newNodeData);
|
||||
};
|
||||
return (
|
||||
<Panel title='节点属性' position='bottom-center' className='w-full'>
|
||||
<div className={clsx('w-full h-[200px] card', store.selected.length > 0 ? '' : 'hidden')}>
|
||||
<div className='card-title'>
|
||||
{nodeData?.data?.label}
|
||||
<Button.Group className='ml-2'>
|
||||
<Button onClick={onSave} icon={<SaveOutlined />}></Button>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
<Form form={form}>
|
||||
<Form.Item label='名称' name='cid'>
|
||||
<Select
|
||||
onChange={(e, options) => {
|
||||
if (Array.isArray(options)) {
|
||||
} else {
|
||||
const title = options?.label;
|
||||
form.setFieldsValue({ title });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label='标题' name='title'>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
);
|
||||
};
|
||||
@@ -3,16 +3,10 @@ import { Outlet } from 'react-router';
|
||||
export const Main = () => {
|
||||
return (
|
||||
<div className='flex w-full h-full flex-col bg-gray-200'>
|
||||
<div className='h-12 bg-white p-2 mb-2'>Deck And Flow</div>
|
||||
<div
|
||||
className='flex'
|
||||
style={{
|
||||
height: 'calc(100vh - 4rem)',
|
||||
}}>
|
||||
<div className='flex-grow overflow-hidden mx-2'>
|
||||
<div className='w-full h-full rounded-lg'>
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className='layout-menu'>Deck And Flow</div>
|
||||
<div className='flex-grow w-full'>
|
||||
<div className='w-full h-full overflow-hidden'>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ type PanelStore = {
|
||||
setData: (data: any) => void;
|
||||
getPanel: (id?: string) => Promise<void>;
|
||||
saveNodesEdges: (data: { nodes?: any[]; edges?: any[]; viewport?: any }) => Promise<void>;
|
||||
updateNodeData: (data: any) => Promise<void>;
|
||||
};
|
||||
export const usePanelStore = create<PanelStore>((set, get) => {
|
||||
return {
|
||||
@@ -78,5 +79,23 @@ export const usePanelStore = create<PanelStore>((set, get) => {
|
||||
message.error(res.msg || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateNodeData: async (data) => {
|
||||
// const { getList } = get();
|
||||
const { id, data: panelData } = get();
|
||||
const res = await query.post({
|
||||
path: 'page',
|
||||
key: 'updateNode',
|
||||
data: {
|
||||
id: id,
|
||||
nodeData: data,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
// getList();
|
||||
} else {
|
||||
message.error(res.msg || 'Request failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user