195 lines
6.3 KiB
TypeScript
195 lines
6.3 KiB
TypeScript
import type { FC } from 'react';
|
|
import { Select, Divider } from 'antd';
|
|
// import { Node } from '@antv/x6';
|
|
import type { GraphRef } from '../types'
|
|
import { PlusOutlined, MinusOutlined, FileAddOutlined } from '@ant-design/icons'
|
|
|
|
interface CanvasToolbarProps {
|
|
miniMapRef: React.RefObject<HTMLDivElement>;
|
|
graphRef: GraphRef;
|
|
isHandMode: boolean;
|
|
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
|
zoomLevel: number;
|
|
canUndo: boolean;
|
|
canRedo: boolean;
|
|
onUndo: () => void;
|
|
onRedo: () => void;
|
|
addNotes: () => void;
|
|
}
|
|
|
|
const CanvasToolbar: FC<CanvasToolbarProps> = ({
|
|
miniMapRef,
|
|
graphRef,
|
|
// isHandMode,
|
|
// setIsHandMode,
|
|
zoomLevel,
|
|
// canUndo,
|
|
// canRedo,
|
|
// onUndo,
|
|
// onRedo,
|
|
addNotes,
|
|
}) => {
|
|
// 整理布局函数
|
|
/*
|
|
const handleLayout = () => {
|
|
if (!graphRef.current) return;
|
|
const nodes = graphRef.current.getNodes();
|
|
const edges = graphRef.current.getEdges();
|
|
|
|
// 如果没有连线,使用垂直布局避免节点重叠
|
|
if (edges.length === 0) {
|
|
nodes.forEach((node, index) => {
|
|
const nodeData = node.getData();
|
|
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
|
const nodeHeight = isSpecialNode ? 220 : 50;
|
|
const xPosition = 100;
|
|
const yPosition = index * (nodeHeight + 100) + 100;
|
|
node.setPosition(xPosition, yPosition);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 简单的树布局算法
|
|
const nodeMap = new Map<string, Node>();
|
|
const children = new Map<string, string[]>();
|
|
const roots: string[] = [];
|
|
|
|
// 初始化节点映射
|
|
nodes.forEach(node => {
|
|
nodeMap.set(node.id, node);
|
|
children.set(node.id, []);
|
|
});
|
|
|
|
// 构建父子关系
|
|
edges.forEach(edge => {
|
|
const sourceId = edge.getSourceCellId();
|
|
const targetId = edge.getTargetCellId();
|
|
if (sourceId && targetId) {
|
|
children.get(sourceId)?.push(targetId);
|
|
}
|
|
});
|
|
|
|
// 找到根节点
|
|
const hasParent = new Set<string>();
|
|
edges.forEach(edge => {
|
|
const targetId = edge.getTargetCellId();
|
|
if (targetId) hasParent.add(targetId);
|
|
});
|
|
|
|
nodes.forEach(node => {
|
|
if (!hasParent.has(node.id)) {
|
|
roots.push(node.id);
|
|
}
|
|
});
|
|
|
|
// 布局参数
|
|
const levelWidths: number[] = [];
|
|
const baseNodeSpacing = 120;
|
|
let currentY = 100;
|
|
|
|
// 计算每层的最大宽度
|
|
const calculateLevelWidths = (nodeId: string, level: number) => {
|
|
const node = nodeMap.get(nodeId);
|
|
if (!node) return;
|
|
|
|
const nodeData = node.getData();
|
|
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
|
const nodeWidth = isSpecialNode ? 400 : 160;
|
|
const gap = isSpecialNode ? 150 : 100;
|
|
|
|
levelWidths[level] = Math.max(levelWidths[level] || 0, nodeWidth + gap);
|
|
|
|
const childIds = children.get(nodeId) || [];
|
|
childIds.forEach((childId: string) => calculateLevelWidths(childId, level + 1));
|
|
};
|
|
|
|
roots.forEach(rootId => calculateLevelWidths(rootId, 0));
|
|
|
|
// 递归布局函数
|
|
const layoutNode = (nodeId: string, level: number, parentY: number): number => {
|
|
const node = nodeMap.get(nodeId);
|
|
if (!node) return parentY;
|
|
|
|
const nodeData = node.getData();
|
|
const isSpecialNode = nodeData?.isGroup || nodeData?.type === 'if-else';
|
|
const nodeHeight = isSpecialNode ? 220 : 50;
|
|
const verticalGap = isSpecialNode ? 80 : 40;
|
|
const spacing = baseNodeSpacing + nodeHeight + verticalGap;
|
|
|
|
const xPosition = levelWidths.slice(0, level).reduce((sum, width) => sum + width, 100);
|
|
|
|
const childIds = children.get(nodeId) || [];
|
|
|
|
if (childIds.length === 0) {
|
|
// 叶子节点
|
|
node.setPosition(xPosition, currentY);
|
|
currentY += spacing;
|
|
return currentY - spacing;
|
|
} else {
|
|
// 非叶子节点,先布局子节点
|
|
const childPositions: number[] = [];
|
|
childIds.forEach((childId: string) => {
|
|
const childY = layoutNode(childId, level + 1, currentY);
|
|
childPositions.push(childY);
|
|
});
|
|
|
|
// 父节点居中,确保有足够间隙
|
|
const minY = Math.min(...childPositions);
|
|
const maxY = Math.max(...childPositions);
|
|
const centerY = (minY + maxY) / 2;
|
|
node.setPosition(xPosition, centerY);
|
|
return centerY;
|
|
}
|
|
};
|
|
|
|
// 布局所有根节点
|
|
roots.forEach(rootId => {
|
|
layoutNode(rootId, 0, currentY);
|
|
currentY += 300; // 不同树之间的间距
|
|
});
|
|
};
|
|
*/
|
|
return (
|
|
<>
|
|
{/* 小地图 */}
|
|
<div ref={miniMapRef} className="rb:absolute rb:bottom-15 rb:right-8 rb:z-1000 rb:rounded-lg rb:overflow-hidden"></div>
|
|
{/* 缩放控制按钮 */}
|
|
<div className="rb:h-8.5 rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:shadow-[0px_2px_6px_0px_rgba(33,35,50,0.15)] rb:px-3 rb:py-2 rb:absolute rb:bottom-5 rb:right-8 rb:flex rb:flex-row rb:items-center rb:gap-4 rb:z-1000">
|
|
<MinusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(-0.1)} />
|
|
<Select
|
|
value={Math.round(zoomLevel * 100)}
|
|
onChange={(value: number | string) => {
|
|
if (value === 'fit') {
|
|
graphRef.current?.zoomToFit({ padding: 20 });
|
|
} else {
|
|
graphRef.current?.zoomTo((value as number) / 100);
|
|
}
|
|
}}
|
|
labelRender={(props) => {
|
|
console.log('props', props)
|
|
return `${props.value}%`
|
|
}}
|
|
className="rb:w-20 rb:h-4!"
|
|
options={[
|
|
{ label: '25%', value: 25 },
|
|
{ label: '50%', value: 50 },
|
|
{ label: '75%', value: 75 },
|
|
{ label: '100%', value: 100 },
|
|
{ label: '125%', value: 125 },
|
|
{ label: '150%', value: 150 },
|
|
{ label: '200%', value: 200 },
|
|
{ label: '自适应', value: 'fit' },
|
|
]}
|
|
variant='borderless'
|
|
size="small"
|
|
/>
|
|
<PlusOutlined className="rb:text-[16px] rb:cursor-pointer" onClick={() => graphRef.current?.zoom(0.1)} />
|
|
<Divider type="vertical" className="rb:h-4" />
|
|
<FileAddOutlined onClick={addNotes} />
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default CanvasToolbar;
|