React Flow
React Flow是一个功能强大的 React 组件库,用于构建交互式的可视化图形编辑器和数据流应用程序。它提供了丰富的功能和高度可定制性。我们可以自定义node节点,可以自定义edge边缘。可扩展性很强。官网案例也比较详细。
React DND
用于在 React 应用程序中实现拖放功能。它提供了一套强大的 API,可以帮助开发者轻松地在应用程序中添加拖放功能。官网是说,HTML5的拖放API比较笨重,并且也存来浏览器兼容性问题,这个库是在底层抹平了这些,让开发者更关注与应用开发。
ok,介绍完两个关键库,那么就来说说我们需求是什么, 我们需求是自定义一些组件, 将这些组件拖拽到我们的画布中,然后可以建立互相之间的关系。
- 首先我们需要先创建一个基本的布局。
import { Button } from "@nextui-org/react";
import { Canvas, TableBox } from "../../components";
function Index() {
return (
<div className="flex">
<TableBox className="w-fit" />
<div className="flex flex-col flex-1">
<div className="border border-solid p-2 flex justify-end">
<Button color="secondary">生成模型</Button>
</div>
<Canvas />
</div>
</div>
);
}
export default Index;
- 创建自定义组件,渲染在我们的左侧栏
import TableItem from "./table-item";
import clsx from "clsx";
interface IProps {
className?: string;
}
function TableBox(props: IProps) {
const { className } = props;
return (
<div
className={clsx(
"border border-solid p-4 min-h-full overflow-y-scroll flex flex-col",
className
)}
>
<div className="grid grid-cols-2 gap-x-4">
<TableItem id="0" data={{ name: "User" }} />
<TableItem id="1" data={{ name: "Price" }} />
</div>
</div>
);
}
export { TableBox };
这个组件因为我们是需要拖放到React Flow的画布中,它有固定的props,而我们自定义传值的属性,都是在提供的data对象中。 它提供了 NodeProps ,并接受一个泛型,这个泛型就是我们自定义的data属性, 方便我们定义类型。
Handle组件是我们连接的那个小圆点。
useDrag则是dnd提供的hooks,type是我们当前拖拽的这个类型,而item其中的属性则是我们在Drop时可以获取到的属性。得保证ID唯一。 在画布中,我们会将Props传过来再次渲染这个组件。
import { Card, Image, CardHeader, CardBody } from "@nextui-org/react";
import { useDrag } from "react-dnd";
import { Handle, NodeProps, Position } from "reactflow";
import { DataTableSvg } from "../../assets/icons";
interface DataProps {
name: string;
isCanvas?: boolean;
}
type IProps = Partial<NodeProps<DataProps>>;
function TableItem(props: IProps) {
const { id, isConnectable, data } = props;
const { name, isCanvas = false } = data || {};
const [_, drag] = useDrag(() => ({
type: "table",
item: { id, ...props.data },
}));
return (
<Card radius="md" ref={isCanvas ? null : drag} className="overflow-visible">
{isCanvas && (
<Handle
type="target"
position={Position.Top}
isConnectable={isConnectable}
/>
)}
<CardHeader className="pb-0 pt-2 px-4 flex-col items-start">
<p className="text-tiny uppercase font-bold">{name}</p>
</CardHeader>
<CardBody className="overflow-visible py-2">
<Image
alt="Card background"
src={DataTableSvg}
width={50}
height={50}
/>
</CardBody>
{isCanvas && (
<Handle
type="source"
position={Position.Bottom}
isConnectable={isConnectable}
/>
)}
</Card>
);
}
export default TableItem;
- React-flow 画布组件
这里我使用了自定义node TableItem 和自定义edge modalEdge, 在useDrop的对象drop这个回调函数中,item则是我们刚才在drag传过来的props,然后我们拿到这个props,再去重新渲染这个组件,就形成了闭环。
import { useMemo } from "react";
import { useDrop } from "react-dnd";
import ReactFlow, {
Controls,
Background,
useNodesState,
useEdgesState,
addEdge,
Connection,
} from "reactflow";
import "reactflow/dist/style.css";
import TableItem from "../table-box/table-item";
import ModalEdge from "./modalEdge";
function Canvas() {
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [_, drop] = useDrop(() => ({
accept: "table",
drop: (item: any, monitor) => {
const offset = monitor.getSourceClientOffset();
const position = {
x: offset?.x ?? 0,
y: offset?.y ?? 0,
};
setNodes((nds) => [
...nds,
{
id: item.id,
type: "talbeItem",
position,
data: { isCanvas: true, ...item },
},
]);
},
}));
const onConnect = (params: Connection) => {
setEdges((eds) => {
const edge = { ...params, type: "modalEdge" };
return addEdge(edge, eds);
});
};
const nodeTypes = useMemo(() => ({ talbeItem: TableItem }), []);
const edgeTypes = useMemo(() => ({ modalEdge: ModalEdge }), []);
return (
<div style={{ width: "100%", height: "100vh" }} ref={drop}>
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
>
<Controls />
<Background gap={12} size={1} />
</ReactFlow>
</div>
);
}
export { Canvas };
import {
BaseEdge,
EdgeLabelRenderer,
EdgeProps,
getStraightPath,
useReactFlow,
} from "reactflow";
import { DeleteSvg } from "../../assets/icons";
type IProps = EdgeProps;
export default function ModalEdge(props: IProps) {
const { id, sourceX, sourceY, targetX, targetY } = props;
const { setEdges } = useReactFlow();
const [edgePath, labelX, labelY] = getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
<BaseEdge id={id} path={edgePath} />
<EdgeLabelRenderer>
<div
className="flex"
style={{
position: "absolute",
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: "all",
}}
>
<img
src={DeleteSvg}
onClick={() => {
setEdges((es) => es.filter((e) => e.id !== id));
}}
/>
</div>
</EdgeLabelRenderer>
</>
);
}