|
|
@@ -23,6 +23,7 @@ import { Tooltip } from "@douyinfe/semi-ui";
|
|
|
interface FlowChartProps {
|
|
|
goals: Goal[]; // 目标节点列表
|
|
|
msgGroups?: Record<string, Message[]>; // 消息组,key 是 goal_id
|
|
|
+ invalidBranches?: Message[][]; // 失效分支列表
|
|
|
onNodeClick?: (node: Goal | Message, edge?: EdgeType) => void; // 节点点击回调
|
|
|
onSubTraceClick?: (parentGoal: Goal, entry: SubTraceEntry) => void; // 子追踪点击回调
|
|
|
}
|
|
|
@@ -52,6 +53,7 @@ interface LayoutNode {
|
|
|
type: "goal" | "subgoal" | "message"; // 节点类型
|
|
|
level: number; // 嵌套层级(0 表示主链节点,1 表示子节点,2 表示孙节点...)
|
|
|
parentId?: string; // 父节点 ID
|
|
|
+ isInvalid?: boolean; // 是否为失效节点
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -67,13 +69,13 @@ interface LayoutEdge {
|
|
|
collapsible: boolean; // 是否可折叠
|
|
|
collapsed: boolean; // 是否已折叠
|
|
|
children?: LayoutNode[]; // 折叠时隐藏的子节点列表
|
|
|
+ isInvalid?: boolean; // 是否为失效连接线
|
|
|
}
|
|
|
|
|
|
const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps> = (
|
|
|
- { goals, msgGroups = {}, onNodeClick, onSubTraceClick },
|
|
|
+ { goals, msgGroups = {}, invalidBranches, onNodeClick, onSubTraceClick },
|
|
|
ref,
|
|
|
) => {
|
|
|
- console.log("%c [ msgGroups ]-33", "font-size:13px; background:pink; color:#bf2c9f;", msgGroups);
|
|
|
// 过滤掉有父节点的 goals,只保留主链节点
|
|
|
goals = goals.filter((g) => !g.parent_id);
|
|
|
|
|
|
@@ -276,7 +278,6 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
allNodes: result.nodes,
|
|
|
});
|
|
|
});
|
|
|
- console.log("%c [ displayGoals ]-261", "font-size:13px; background:pink; color:#bf2c9f;", displayGoals);
|
|
|
|
|
|
/**
|
|
|
* 生成连接线
|
|
|
@@ -291,7 +292,6 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
* 3. 递归处理所有层级的节点
|
|
|
*/
|
|
|
|
|
|
- console.log("%c [ mainChainInfo ]-285", "font-size:13px; background:pink; color:#bf2c9f;", mainChainInfo);
|
|
|
for (let i = 0; i < mainChainInfo.length - 1; i++) {
|
|
|
const current = mainChainInfo[i];
|
|
|
const next = mainChainInfo[i + 1];
|
|
|
@@ -495,8 +495,90 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 处理失效分支(invalidBranches)
|
|
|
+ if (invalidBranches && invalidBranches.length > 0) {
|
|
|
+ const validMsgMap = new Map<number, LayoutNode>();
|
|
|
+ nodes.forEach((n) => {
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ const seq = (n.data as any).sequence;
|
|
|
+ if (typeof seq === "number") {
|
|
|
+ validMsgMap.set(seq, n);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Map to store invalid nodes by their anchor parent ID
|
|
|
+ const invalidNodesByAnchor = new Map<string, LayoutNode[]>();
|
|
|
+
|
|
|
+ invalidBranches.forEach((branch) => {
|
|
|
+ if (branch.length === 0) return;
|
|
|
+ const firstMsg = branch[0];
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
+ const pSeq = (firstMsg as any).parent_sequence;
|
|
|
+
|
|
|
+ if (typeof pSeq === "number") {
|
|
|
+ const parentNode = validMsgMap.get(pSeq);
|
|
|
+ if (parentNode) {
|
|
|
+ let currentParent = parentNode;
|
|
|
+ const X_OFFSET = -200; // 向左偏移
|
|
|
+ const currentBranchNodes: LayoutNode[] = [];
|
|
|
+
|
|
|
+ branch.forEach((msg, idx) => {
|
|
|
+ const nodeId = `invalid-${msg.id || Math.random()}`;
|
|
|
+ const node: LayoutNode = {
|
|
|
+ id: nodeId,
|
|
|
+ x: parentNode.x + X_OFFSET,
|
|
|
+ y: parentNode.y + (idx + 1) * NODE_HEIGHT,
|
|
|
+ data: msg,
|
|
|
+ type: "message",
|
|
|
+ level: parentNode.level,
|
|
|
+ parentId: parentNode.id,
|
|
|
+ isInvalid: true,
|
|
|
+ };
|
|
|
+ nodes.push(node);
|
|
|
+ currentBranchNodes.push(node);
|
|
|
+
|
|
|
+ edges.push({
|
|
|
+ id: `edge-${currentParent.id}-${node.id}`,
|
|
|
+ source: currentParent,
|
|
|
+ target: node,
|
|
|
+ type: "line",
|
|
|
+ level: 0,
|
|
|
+ collapsible: false,
|
|
|
+ collapsed: false,
|
|
|
+ isInvalid: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ currentParent = node;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Store in map
|
|
|
+ if (!invalidNodesByAnchor.has(parentNode.id)) {
|
|
|
+ invalidNodesByAnchor.set(parentNode.id, []);
|
|
|
+ }
|
|
|
+ invalidNodesByAnchor.get(parentNode.id)!.push(...currentBranchNodes);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // Associate invalid nodes with collapsible edges
|
|
|
+ // If a parent node is hidden (part of a collapsed edge), its invalid children should also be hidden
|
|
|
+ edges.forEach((edge) => {
|
|
|
+ if (edge.collapsible && edge.children) {
|
|
|
+ const extraChildren: LayoutNode[] = [];
|
|
|
+ edge.children.forEach((child) => {
|
|
|
+ if (invalidNodesByAnchor.has(child.id)) {
|
|
|
+ extraChildren.push(...invalidNodesByAnchor.get(child.id)!);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (extraChildren.length > 0) {
|
|
|
+ edge.children.push(...extraChildren);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return { nodes, edges };
|
|
|
- }, [displayGoals, dimensions, msgGroups, collapsedEdges]);
|
|
|
+ }, [displayGoals, dimensions, msgGroups, collapsedEdges, invalidBranches]);
|
|
|
|
|
|
// 暴露给父组件的方法
|
|
|
useImperativeHandle(
|
|
|
@@ -844,13 +926,13 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
|
|
|
if (sourceIsMessage || targetIsMessage) {
|
|
|
// msgGroup 相关的连接线用灰色
|
|
|
- color = "#9E9E9E";
|
|
|
+ color = "#94a3b8"; // Slate 400
|
|
|
} else if (sourceIsMainGoal && targetIsMainGoal) {
|
|
|
// 主节点之间的连接线用绿色
|
|
|
- color = "#4CAF50";
|
|
|
+ color = "#10b981"; // Emerald 500
|
|
|
} else {
|
|
|
// sub_goals 之间的连接线用蓝色
|
|
|
- color = "#2196F3";
|
|
|
+ color = "#3b82f6"; // Blue 500
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
@@ -858,9 +940,10 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
<path
|
|
|
d={path}
|
|
|
fill="none"
|
|
|
- stroke={color}
|
|
|
+ stroke={edge.isInvalid ? "#cbd5e1" : color} // 失效边使用浅灰色 (Slate 300)
|
|
|
strokeWidth={strokeWidth}
|
|
|
- markerEnd="url(#arrow-default)" // 箭头
|
|
|
+ strokeDasharray={edge.isInvalid ? "5,5" : undefined} // 失效边使用虚线
|
|
|
+ markerEnd={edge.isInvalid ? undefined : "url(#arrow-default)"} // 失效边不显示箭头
|
|
|
style={{ cursor: edge.collapsible ? "pointer" : "default" }} // 可折叠的显示手型光标
|
|
|
onClick={() => edge.collapsible && toggleCollapse(edge.id)} // 点击切换折叠状态
|
|
|
/>
|
|
|
@@ -906,11 +989,15 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
const data = node.data as Goal;
|
|
|
const text = isGoal ? data.description : (node.data as Message).description || "";
|
|
|
|
|
|
- let textColor = "#2196F3"; // 默认蓝色
|
|
|
+ let textColor = "#3b82f6"; // Blue 500
|
|
|
if (node.type === "message") {
|
|
|
- textColor = "#9E9E9E"; // 消息节点灰色
|
|
|
+ textColor = "#64748b"; // Slate 500
|
|
|
} else if (node.type === "goal" && node.level === 0) {
|
|
|
- textColor = "#4CAF50"; // 主节点绿色
|
|
|
+ textColor = "#10b981"; // Emerald 500
|
|
|
+ }
|
|
|
+
|
|
|
+ if (node.isInvalid) {
|
|
|
+ textColor = "#94a3b8"; // Slate 400
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
@@ -927,28 +1014,35 @@ const FlowChartComponent: ForwardRefRenderFunction<FlowChartRef, FlowChartProps>
|
|
|
width={150}
|
|
|
height={50}
|
|
|
rx={8}
|
|
|
- fill={isGoal ? "#E3F2FD" : "#F5F5F5"} // 目标节点浅蓝色,消息节点灰色
|
|
|
- stroke={selectedNodeId === node.id ? "#2196F3" : "#BDBDBD"} // 选中节点蓝色边框
|
|
|
+ fill={isGoal ? "#eff6ff" : "#f8fafc"} // Blue 50 / Slate 50
|
|
|
+ stroke={selectedNodeId === node.id ? "#3b82f6" : node.isInvalid ? "#cbd5e1" : "#e2e8f0"} // Selected: Blue 500, Invalid: Slate 300, Default: Slate 200
|
|
|
strokeWidth={selectedNodeId === node.id ? 2 : 1}
|
|
|
+ strokeDasharray={node.isInvalid ? "5,5" : undefined} // 失效节点虚线边框
|
|
|
+ style={{
|
|
|
+ filter:
|
|
|
+ selectedNodeId === node.id
|
|
|
+ ? "drop-shadow(0 4px 6px rgb(59 130 246 / 0.3))"
|
|
|
+ : "drop-shadow(0 1px 2px rgb(0 0 0 / 0.05))",
|
|
|
+ }}
|
|
|
/>
|
|
|
{/* 节点文本(带 Tooltip) */}
|
|
|
- <Tooltip content={text}>
|
|
|
- <foreignObject
|
|
|
- x={-70}
|
|
|
- y={-25}
|
|
|
- width={150}
|
|
|
- height={50}
|
|
|
- >
|
|
|
+ <foreignObject
|
|
|
+ x={-70}
|
|
|
+ y={-25}
|
|
|
+ width={150}
|
|
|
+ height={50}
|
|
|
+ >
|
|
|
+ <Tooltip content={text}>
|
|
|
<div
|
|
|
className="w-full h-full overflow-hidden flex items-center justify-center"
|
|
|
style={{
|
|
|
color: textColor,
|
|
|
}}
|
|
|
>
|
|
|
- <text className="text-xs line-clamp-3 px-1">{text}</text>
|
|
|
+ <span className="text-xs line-clamp-3 px-1">{text}</span>
|
|
|
</div>
|
|
|
- </foreignObject>
|
|
|
- </Tooltip>
|
|
|
+ </Tooltip>
|
|
|
+ </foreignObject>
|
|
|
</g>
|
|
|
);
|
|
|
})}
|