|
|
@@ -175,7 +175,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
const nodes: LayoutNode[] = []; // 所有节点列表
|
|
|
const edges: LayoutEdge[] = []; // 所有连接线列表
|
|
|
|
|
|
- const NODE_HEIGHT = 80; // 节点间距(纵向)
|
|
|
+ const NODE_HEIGHT = 110; // 节点间距(纵向)
|
|
|
const centerX = dimensions.width / 2; // 主链节点的 X 坐标(居中)
|
|
|
const HORIZONTAL_OFFSET = 0; // 子节点水平偏移量
|
|
|
|
|
|
@@ -213,17 +213,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
let lastNode = currentNode; // 记录最后一个节点,用于连接到下一个主链节点
|
|
|
globalY += NODE_HEIGHT; // 更新全局 Y 坐标
|
|
|
|
|
|
- // 递归展开 sub_goals
|
|
|
- if (goal.sub_goals && goal.sub_goals.length > 0) {
|
|
|
- goal.sub_goals.forEach((subGoal) => {
|
|
|
- const subX = x + HORIZONTAL_OFFSET; // 子节点向右偏移
|
|
|
- const result = expandNode(subGoal, subX, level + 1, nodeId);
|
|
|
- localNodes.push(...result.nodes);
|
|
|
- lastNode = result.lastNode; // 更新最后一个节点
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- // 展开 msgGroup(消息组)
|
|
|
+ // 1. 先展开 msgGroup(消息组)- 按照用户需求,消息显示在 sub_goals 之前
|
|
|
const messages = msgGroups[nodeId];
|
|
|
if (messages && messages.length > 0) {
|
|
|
messages.forEach((msg) => {
|
|
|
@@ -242,6 +232,16 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ // 2. 再递归展开 sub_goals
|
|
|
+ if (goal.sub_goals && goal.sub_goals.length > 0) {
|
|
|
+ goal.sub_goals.forEach((subGoal) => {
|
|
|
+ const subX = x + HORIZONTAL_OFFSET; // 子节点向右偏移
|
|
|
+ const result = expandNode(subGoal, subX, level + 1, nodeId);
|
|
|
+ localNodes.push(...result.nodes);
|
|
|
+ lastNode = result.lastNode; // 更新最后一个节点
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return { nodes: localNodes, firstNode: currentNode, lastNode };
|
|
|
};
|
|
|
|
|
|
@@ -308,6 +308,26 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
const childNodes = current.allNodes.slice(1);
|
|
|
const directChildren = childNodes.filter((n) => n.parentId === current.firstNode.id);
|
|
|
|
|
|
+ // Logic A: Messages Group Arc (A -> s1)
|
|
|
+ // 检查是否有消息节点且后面跟着子目标节点
|
|
|
+ // 如果有,绘制一条蓝色弧线从主节点到第一个子目标节点,包裹所有消息节点
|
|
|
+ const firstSubgoalIndex = directChildren.findIndex((n) => n.type === "goal");
|
|
|
+ if (firstSubgoalIndex > 0) {
|
|
|
+ const target = directChildren[firstSubgoalIndex];
|
|
|
+ const messages = directChildren.slice(0, firstSubgoalIndex);
|
|
|
+ const arcId = `arc-msg-${current.firstNode.id}-${target.id}`;
|
|
|
+ edges.push({
|
|
|
+ id: arcId,
|
|
|
+ source: current.firstNode,
|
|
|
+ target: target,
|
|
|
+ type: "arc",
|
|
|
+ level: 1,
|
|
|
+ collapsible: true,
|
|
|
+ collapsed: collapsedEdges.has(arcId),
|
|
|
+ children: messages,
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
// 连接第一个节点到第一个子节点
|
|
|
if (directChildren.length > 0) {
|
|
|
edges.push({
|
|
|
@@ -407,6 +427,50 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
collapsible: false,
|
|
|
collapsed: false,
|
|
|
});
|
|
|
+
|
|
|
+ // Logic C: Last Child Arc (s2 -> B)
|
|
|
+ // 如果最后一个直接子节点是 sub_goal 且有子节点,绘制弧线连接到下一个主节点
|
|
|
+ const lastChild = directChildren[directChildren.length - 1];
|
|
|
+ const lastChildChildren = nodes.filter((n) => n.parentId === lastChild.id);
|
|
|
+
|
|
|
+ if (lastChild.type === "goal" && lastChildChildren.length > 0) {
|
|
|
+ const arcId = `arc-${lastChild.id}-${next.firstNode.id}`;
|
|
|
+ edges.push({
|
|
|
+ id: arcId,
|
|
|
+ source: lastChild,
|
|
|
+ target: next.firstNode,
|
|
|
+ type: "arc",
|
|
|
+ level: 1,
|
|
|
+ collapsible: true,
|
|
|
+ collapsed: collapsedEdges.has(arcId),
|
|
|
+ children: lastChildChildren,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 补充绘制最后一个子节点的内部连线(直线)
|
|
|
+ // 1. 连接 lastChild 到第一个孙节点
|
|
|
+ edges.push({
|
|
|
+ id: `line-${lastChild.id}-${lastChildChildren[0].id}`,
|
|
|
+ source: lastChild,
|
|
|
+ target: lastChildChildren[0],
|
|
|
+ type: "line",
|
|
|
+ level: lastChild.level + 1,
|
|
|
+ collapsible: false,
|
|
|
+ collapsed: false,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 2. 连接孙节点之间
|
|
|
+ for (let k = 0; k < lastChildChildren.length - 1; k++) {
|
|
|
+ edges.push({
|
|
|
+ id: `line-${lastChildChildren[k].id}-${lastChildChildren[k + 1].id}`,
|
|
|
+ source: lastChildChildren[k],
|
|
|
+ target: lastChildChildren[k + 1],
|
|
|
+ type: "line",
|
|
|
+ level: lastChild.level + 1,
|
|
|
+ collapsible: false,
|
|
|
+ collapsed: false,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
} else {
|
|
|
// 情况 4:没有子节点,直接绘制直线
|
|
|
@@ -452,10 +516,20 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
* 并且调整后续节点的位置,填补折叠后的空白
|
|
|
*/
|
|
|
const visibleData = useMemo(() => {
|
|
|
- const NODE_HEIGHT = 80;
|
|
|
+ const NODE_HEIGHT = 110;
|
|
|
|
|
|
// 1. 找出所有折叠的边
|
|
|
- const collapsedEdgesList = layoutData.edges.filter((e) => e.collapsed);
|
|
|
+ const allCollapsedEdges = layoutData.edges.filter((e) => e.collapsed);
|
|
|
+
|
|
|
+ // 筛选出“有效”的折叠边:如果一个折叠边被另一个更大的折叠边包含(即其节点被隐藏),则忽略它
|
|
|
+ // 避免重复计算 shiftY
|
|
|
+ const collapsedEdgesList = allCollapsedEdges.filter((edge) => {
|
|
|
+ return !allCollapsedEdges.some((other) => {
|
|
|
+ if (edge === other) return false;
|
|
|
+ // 如果当前边的源节点或目标节点在另一个折叠边的子节点列表中,说明当前边是被包裹在内部的
|
|
|
+ return other.children?.some((child) => child.id === edge.source.id || child.id === edge.target.id);
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
// 2. 过滤节点:隐藏被折叠的节点
|
|
|
const visibleNodesRaw = layoutData.nodes.filter((node) => {
|
|
|
@@ -582,9 +656,9 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
*/
|
|
|
const getArcPath = (source: LayoutNode, target: LayoutNode, level: number) => {
|
|
|
const sx = source.x;
|
|
|
- const sy = source.y;
|
|
|
+ const sy = source.y + 25; // 从节点底部出发
|
|
|
const tx = target.x;
|
|
|
- const ty = target.y;
|
|
|
+ const ty = target.y - 25; // 到节点顶部结束
|
|
|
|
|
|
// 弧线向右偏移,偏移量根据层级递减
|
|
|
// 外层弧线偏移量大,内层弧线偏移量小
|
|
|
@@ -604,7 +678,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
* @returns SVG 路径字符串
|
|
|
*/
|
|
|
const getLinePath = (source: LayoutNode, target: LayoutNode) => {
|
|
|
- return `M ${source.x},${source.y} L ${target.x},${target.y}`;
|
|
|
+ return `M ${source.x},${source.y + 25} L ${target.x},${target.y - 25}`;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -745,16 +819,47 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
- <path
|
|
|
- key={edge.id}
|
|
|
- d={path}
|
|
|
- fill="none"
|
|
|
- stroke={color}
|
|
|
- strokeWidth={strokeWidth}
|
|
|
- markerEnd="url(#arrow-default)" // 箭头
|
|
|
- style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
|
|
|
- onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
|
|
|
- />
|
|
|
+ <g key={edge.id}>
|
|
|
+ <path
|
|
|
+ d={path}
|
|
|
+ fill="none"
|
|
|
+ stroke={color}
|
|
|
+ strokeWidth={strokeWidth}
|
|
|
+ markerEnd="url(#arrow-default)" // 箭头
|
|
|
+ style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
|
|
|
+ onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
|
|
|
+ />
|
|
|
+ {/* 折叠状态提示徽章 */}
|
|
|
+ {edge.collapsed && (
|
|
|
+ <g
|
|
|
+ transform={`translate(${(edge.source.x + edge.target.x) / 2},${
|
|
|
+ (edge.source.y + edge.target.y) / 2
|
|
|
+ })`}
|
|
|
+ onClick={(e) => {
|
|
|
+ e.stopPropagation();
|
|
|
+ toggleCollapse(edge.id);
|
|
|
+ }}
|
|
|
+ style={{ cursor: "pointer" }}
|
|
|
+ >
|
|
|
+ <circle
|
|
|
+ r={10}
|
|
|
+ fill="#FFFFFF"
|
|
|
+ stroke={color}
|
|
|
+ strokeWidth={1}
|
|
|
+ />
|
|
|
+ <text
|
|
|
+ x={0}
|
|
|
+ y={4}
|
|
|
+ fontSize={10}
|
|
|
+ fill={color}
|
|
|
+ textAnchor="middle"
|
|
|
+ fontWeight="bold"
|
|
|
+ >
|
|
|
+ {edge.children ? edge.children.length : "+"}
|
|
|
+ </text>
|
|
|
+ </g>
|
|
|
+ )}
|
|
|
+ </g>
|
|
|
);
|
|
|
})}
|
|
|
</g>
|
|
|
@@ -784,7 +889,7 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
<rect
|
|
|
x={-70}
|
|
|
y={-25}
|
|
|
- width={140}
|
|
|
+ width={150}
|
|
|
height={50}
|
|
|
rx={8}
|
|
|
fill={isGoal ? "#E3F2FD" : "#F5F5F5"} // 目标节点浅蓝色,消息节点灰色
|
|
|
@@ -793,15 +898,22 @@ export const FlowChart: FC<FlowChartProps> = ({ goals, msgGroups = {}, onNodeCli
|
|
|
/>
|
|
|
{/* 节点文本(带 Tooltip) */}
|
|
|
<Tooltip content={text}>
|
|
|
- <text
|
|
|
- x={0}
|
|
|
- y={5}
|
|
|
- fontSize={12}
|
|
|
- fill={textColor}
|
|
|
- textAnchor="middle"
|
|
|
+ <foreignObject
|
|
|
+ x={-70}
|
|
|
+ y={-25}
|
|
|
+ width={150}
|
|
|
+ height={50}
|
|
|
>
|
|
|
- {text.length > 10 ? text.slice(0, 10) + "..." : text}
|
|
|
- </text>
|
|
|
+ <div
|
|
|
+ className="w-full h-full flex items-center justify-center text-xs text-center leading-[1.2] px-1 box-border line-clamp-3"
|
|
|
+ style={{
|
|
|
+ color: textColor,
|
|
|
+ WebkitBoxPack: "center", // 垂直居中
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {text}
|
|
|
+ </div>
|
|
|
+ </foreignObject>
|
|
|
</Tooltip>
|
|
|
</g>
|
|
|
);
|