feat: 优化界面显示,对deck添加编辑功能

This commit is contained in:
2024-09-26 21:08:38 +08:00
parent 02a1752a13
commit 12f1084612
29 changed files with 801 additions and 165 deletions

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -0,0 +1,2 @@
import { EventEmitter } from 'eventemitter3';
export const emitter = new EventEmitter();

View 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>
);
};

View File

@@ -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>

View File

@@ -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');
}
},
};
});