Skip to content

Commit bb2116e

Browse files
committed
feat: 后续目标节点
1 parent 81a5759 commit bb2116e

7 files changed

Lines changed: 231 additions & 6 deletions

File tree

src/components/flow/nodes/PipelineNode/ClassicContent.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import { type NodeProps } from "@xyflow/react";
33

44
import style from "../../../../styles/flow/nodes.module.less";
55
import type { PipelineNodeDataType } from "../../../../stores/flow";
6+
import { useFlowStore } from "../../../../stores/flow";
67
import { useConfigStore } from "../../../../stores/configStore";
78
import { KVElem } from "../components/KVElem";
89
import { PipelineNodeHandles } from "../components/NodeHandles";
10+
import { SourceHandleTypeEnum, TargetHandleTypeEnum, NodeTypeEnum } from "../constants";
911
import { JsonHelper } from "../../../../utils/data/jsonHelper";
1012
import {
1113
mergeFieldSortConfig,
@@ -14,10 +16,35 @@ import {
1416

1517
/**经典风格Pipeline节点内容 */
1618
export const ClassicContent = memo(
17-
({ data }: { data: PipelineNodeDataType; props: NodeProps }) => {
19+
({ data, props }: { data: PipelineNodeDataType; props: NodeProps }) => {
20+
const nodeId = props.id;
21+
22+
const edges = useFlowStore((state) => state.edges);
23+
const nodes = useFlowStore((state) => state.nodes);
24+
const { nextItems, errorItems } = useMemo(() => {
25+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
26+
const outEdges = edges.filter((e) => e.source === nodeId);
27+
const nextItems: { label: string; variant: "normal" | "jumpback" | "anchor" }[] = [];
28+
const errorItems: { label: string; variant: "normal" | "jumpback" | "anchor" }[] = [];
29+
const sorted = [...outEdges].sort((a, b) => a.label - b.label);
30+
for (const e of sorted) {
31+
const targetNode = nodeMap.get(e.target);
32+
const label = (targetNode?.data as any)?.label ?? e.target;
33+
const isJumpBack = e.targetHandle === TargetHandleTypeEnum.JumpBack;
34+
const isAnchor = targetNode?.type === NodeTypeEnum.Anchor || !!e.attributes?.anchor;
35+
const variant = isJumpBack ? "jumpback" : isAnchor ? "anchor" : "normal";
36+
if (e.sourceHandle === SourceHandleTypeEnum.Next) nextItems.push({ label, variant });
37+
else if (e.sourceHandle === SourceHandleTypeEnum.Error) errorItems.push({ label, variant });
38+
}
39+
return { nextItems, errorItems };
40+
}, [edges, nodes, nodeId]);
41+
1842
const showNodeDetailFields = useConfigStore(
1943
(state) => state.configs.showNodeDetailFields,
2044
);
45+
const showNodeFlowSection = useConfigStore(
46+
(state) => state.configs.showNodeFlowSection,
47+
);
2148
const fieldSortConfig = useConfigStore(
2249
(state) => state.configs.fieldSortConfig,
2350
);
@@ -112,6 +139,28 @@ export const ClassicContent = memo(
112139
</ul>
113140
)}
114141
</ul>
142+
{showNodeFlowSection && (nextItems.length > 0 || errorItems.length > 0) && (
143+
<div className={style.flowSection}>
144+
{nextItems.length > 0 && (
145+
<div className={style.flowRow}>
146+
<span className={`${style.flowTag} ${style.flowTagNext}`}>next</span>
147+
<span className={style.flowArrow}></span>
148+
{nextItems.map((item, i) => (
149+
<span key={i} className={`${style.flowTag} ${style.flowTagTarget} ${style[`flowTarget-${item.variant}`]}`}>{item.label}</span>
150+
))}
151+
</div>
152+
)}
153+
{errorItems.length > 0 && (
154+
<div className={style.flowRow}>
155+
<span className={`${style.flowTag} ${style.flowTagError}`}>on_error</span>
156+
<span className={style.flowArrow}></span>
157+
{errorItems.map((item, i) => (
158+
<span key={i} className={`${style.flowTag} ${style.flowTagTarget} ${style[`flowTarget-${item.variant}`]}`}>{item.label}</span>
159+
))}
160+
</div>
161+
)}
162+
</div>
163+
)}
115164
<PipelineNodeHandles direction={data.handleDirection} />
116165
</>
117166
);

src/components/flow/nodes/PipelineNode/ModernContent.tsx

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import classNames from "classnames";
44

55
import style from "../../../../styles/flow/nodes.module.less";
66
import type { PipelineNodeDataType } from "../../../../stores/flow";
7+
import { useFlowStore } from "../../../../stores/flow";
78
import { useConfigStore } from "../../../../stores/configStore";
9+
import { SourceHandleTypeEnum, TargetHandleTypeEnum, NodeTypeEnum } from "../constants";
810
import IconFont from "../../../iconfonts";
911
import { KVElem } from "../components/KVElem";
1012
import { PipelineNodeHandles } from "../components/NodeHandles";
@@ -32,14 +34,18 @@ const focusDisplayNameMap: Record<string, string> = (() => {
3234

3335
/**现代风格Pipeline节点内容 */
3436
export const ModernContent = memo(
35-
({ data }: { data: PipelineNodeDataType; props: NodeProps }) => {
37+
({ data, props }: { data: PipelineNodeDataType; props: NodeProps }) => {
38+
const nodeId = props.id;
3639
const headerRef = useRef<HTMLDivElement>(null);
3740
const [headerHeight, setHeaderHeight] = useState(0);
3841

3942
// 是否显示节点模板图片
4043
const showNodeTemplateImages = useConfigStore(
4144
(state) => state.configs.showNodeTemplateImages,
4245
);
46+
const showNodeFlowSection = useConfigStore(
47+
(state) => state.configs.showNodeFlowSection,
48+
);
4349
// 是否渲染节点详细字段
4450
const showNodeDetailFields = useConfigStore(
4551
(state) => state.configs.showNodeDetailFields,
@@ -52,6 +58,27 @@ export const ModernContent = memo(
5258
[fieldSortConfig],
5359
);
5460

61+
// 读取出边,按 sourceHandle 分为 next/on_error 两组,保留顺序
62+
const edges = useFlowStore((state) => state.edges);
63+
const nodes = useFlowStore((state) => state.nodes);
64+
const { nextItems, errorItems } = useMemo(() => {
65+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
66+
const outEdges = edges.filter((e) => e.source === nodeId);
67+
const nextItems: { label: string; variant: "normal" | "jumpback" | "anchor" }[] = [];
68+
const errorItems: { label: string; variant: "normal" | "jumpback" | "anchor" }[] = [];
69+
const sorted = [...outEdges].sort((a, b) => a.label - b.label);
70+
for (const e of sorted) {
71+
const targetNode = nodeMap.get(e.target);
72+
const label = (targetNode?.data as any)?.label ?? e.target;
73+
const isJumpBack = e.targetHandle === TargetHandleTypeEnum.JumpBack;
74+
const isAnchor = targetNode?.type === NodeTypeEnum.Anchor || !!e.attributes?.anchor;
75+
const variant = isJumpBack ? "jumpback" : isAnchor ? "anchor" : "normal";
76+
if (e.sourceHandle === SourceHandleTypeEnum.Next) nextItems.push({ label, variant });
77+
else if (e.sourceHandle === SourceHandleTypeEnum.Error) errorItems.push({ label, variant });
78+
}
79+
return { nextItems, errorItems };
80+
}, [edges, nodes, nodeId]);
81+
5582
useEffect(() => {
5683
if (headerRef.current) {
5784
const height = headerRef.current.offsetHeight;
@@ -267,6 +294,30 @@ export const ModernContent = memo(
267294
)}
268295
</div>
269296

297+
{/* 流程连接区域 */}
298+
{showNodeFlowSection && (nextItems.length > 0 || errorItems.length > 0) && (
299+
<div className={style.flowSection}>
300+
{nextItems.length > 0 && (
301+
<div className={style.flowRow}>
302+
<span className={`${style.flowTag} ${style.flowTagNext}`}>next</span>
303+
<span className={style.flowArrow}></span>
304+
{nextItems.map((item, i) => (
305+
<span key={i} className={`${style.flowTag} ${style.flowTagTarget} ${style[`flowTarget-${item.variant}`]}`}>{item.label}</span>
306+
))}
307+
</div>
308+
)}
309+
{errorItems.length > 0 && (
310+
<div className={style.flowRow}>
311+
<span className={`${style.flowTag} ${style.flowTagError}`}>on_error</span>
312+
<span className={style.flowArrow}></span>
313+
{errorItems.map((item, i) => (
314+
<span key={i} className={`${style.flowTag} ${style.flowTagTarget} ${style[`flowTarget-${item.variant}`]}`}>{item.label}</span>
315+
))}
316+
</div>
317+
)}
318+
</div>
319+
)}
320+
270321
{/* 模板图片区域 */}
271322
{showNodeTemplateImages && templatePaths.length > 0 && (
272323
<NodeTemplateImages templatePaths={templatePaths} />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// 预设颜色池(排除 next/#5ec697、error/#ec4899、jumpback/#f97316、norm/#2563eb)
2+
// [background, text, border]
3+
const PALETTE: [string, string, string][] = [
4+
["#f3e5f5", "#6a1b9a", "#e1bee7"], // 紫
5+
["#e0f7fa", "#00695c", "#b2ebf2"], // 青
6+
["#fff8e1", "#f57f17", "#ffecb3"], // 琥珀
7+
["#fbe9e7", "#bf360c", "#ffccbc"], // 深橙
8+
["#e8eaf6", "#283593", "#c5cae9"], // 靛
9+
["#f1f8e9", "#33691e", "#dcedc8"], // 浅绿
10+
["#fce4ec", "#880e4f", "#f8bbd0"], // 深粉
11+
["#e8f5e9", "#1b5e20", "#c8e6c9"], // 深绿
12+
["#ede7f6", "#4527a0", "#d1c4e9"], // 深紫
13+
["#e3f2fd", "#0d47a1", "#bbdefb"], // 深蓝
14+
["#fff3e0", "#bf360c", "#ffe0b2"], // 橙褐
15+
["#fafafa", "#212121", "#e0e0e0"], // 炭灰
16+
];
17+
18+
function hashLabel(label: string): number {
19+
let h = 0;
20+
for (let i = 0; i < label.length; i++) {
21+
h = (h * 31 + label.charCodeAt(i)) >>> 0;
22+
}
23+
return h;
24+
}
25+
26+
export function getFlowTagColor(label: string): { background: string; color: string; border: string } {
27+
const [background, color, border] = PALETTE[hashLabel(label) % PALETTE.length];
28+
return { background, color, border };
29+
}

src/components/panels/settings/settingsDefinitions.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,15 +237,26 @@ export const settingsDefinitions: ConfigItemDef[] = [
237237
{
238238
key: "showNodeTemplateImages",
239239
category: "node",
240-
label: "节点显示模板图片",
241-
tipTitle: "节点显示模板图片",
240+
label: "显示模板图片",
241+
tipTitle: "显示模板图片",
242242
tipContent:
243243
"开启时,现代风格节点底部会显示 template 字段的图片缩略图。需要连接本地服务后生效。",
244244
type: "switch",
245245
checkedChildren: "显示",
246246
unCheckedChildren: "隐藏",
247247
order: 3,
248248
},
249+
{
250+
key: "showNodeFlowSection",
251+
category: "node",
252+
label: "显示后续目标节点",
253+
tipTitle: "显示后续目标节点",
254+
tipContent: "开启时,节点底部会显示 next 和 on_error 的目标节点。",
255+
type: "switch",
256+
checkedChildren: "显示",
257+
unCheckedChildren: "隐藏",
258+
order: 4,
259+
},
249260
{
250261
key: "defaultHandleDirection",
251262
category: "node",

src/data/updateLogs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ export const nextPreview: ForecastSection = {
106106
export const updateLogs: UpdateLogItem[] = [
107107
{
108108
version: "1.6.1",
109-
date: "2026-6-",
109+
date: "2026-6-4",
110110
type: "perf",
111111
updates: {
112-
// features: [""],
112+
features: ["🎯 节点可显示后续目标节点,可在设置面板切换是否显示"],
113113
perfs: [
114114
"还原连接线路径时会自动进行 external 节点的就近连接重计算",
115115
"优化资源健康检查相关术语",

src/stores/configStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const configCategoryMap: Record<string, ConfigCategory> = {
4444
nodeStyle: "node",
4545
showNodeDetailFields: "node",
4646
showNodeTemplateImages: "node",
47+
showNodeFlowSection: "node",
4748
enableNodeSnap: "node",
4849
snapOnlyInViewport: "node",
4950
defaultHandleDirection: "node",
@@ -157,6 +158,8 @@ const defaultConfigs = {
157158
inlinePanelScale: 0.8,
158159
// 节点显示 template 图片
159160
showNodeTemplateImages: true,
161+
// 显示流程连接区域(next/on_error)
162+
showNodeFlowSection: true,
160163
// 渲染节点详细字段
161164
showNodeDetailFields: true,
162165
// 节点磁吸对齐
@@ -219,6 +222,8 @@ export type ConfigState = {
219222
inlinePanelScale: number;
220223
// 节点显示 template 图片
221224
showNodeTemplateImages: boolean;
225+
// 显示流程连接区域(next/on_error)
226+
showNodeFlowSection: boolean;
222227
// 渲染节点详细字段
223228
showNodeDetailFields: boolean;
224229
// 节点磁吸对齐

src/styles/flow/nodes.module.less

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,86 @@
223223
}
224224
}
225225

226+
// 流程连接区域(next / on_error)
227+
.flowSection {
228+
border-top: 1px dashed #e0e0e0;
229+
padding: 6px 10px 8px;
230+
display: flex;
231+
flex-direction: column;
232+
gap: 4px;
233+
234+
.flowRow {
235+
display: flex;
236+
align-items: center;
237+
gap: 4px;
238+
flex-wrap: wrap;
239+
min-height: 20px;
240+
}
241+
242+
.flowArrow {
243+
font-size: 10px;
244+
color: #bbb;
245+
flex-shrink: 0;
246+
}
247+
248+
// 通用 tag 基础样式
249+
.flowTag {
250+
display: inline-flex;
251+
align-items: center;
252+
height: 18px;
253+
padding: 0 6px;
254+
border-radius: 9px;
255+
font-size: 10px;
256+
font-weight: 500;
257+
white-space: nowrap;
258+
flex-shrink: 0;
259+
}
260+
261+
.flowTagNext {
262+
background-color: mix(@next-color, #fff, 15%);
263+
color: darken(@next-color, 15%);
264+
border: 1px solid mix(@next-color, #fff, 35%);
265+
}
266+
267+
.flowTagError {
268+
background-color: mix(@error-color, #fff, 15%);
269+
color: darken(@error-color, 15%);
270+
border: 1px solid mix(@error-color, #fff, 35%);
271+
}
272+
273+
.flowTagAttr {
274+
background-color: mix(@jumpback-color, #fff, 12%);
275+
color: darken(@jumpback-color, 10%);
276+
border: 1px solid mix(@jumpback-color, #fff, 30%);
277+
}
278+
279+
.flowTagTarget {
280+
.ellipsis();
281+
max-width: 90px;
282+
background-color: #f0f0f0;
283+
color: rgba(0, 0, 0, 0.65);
284+
border: 1px solid #e0e0e0;
285+
font-weight: 400;
286+
}
287+
288+
// normal: 灰色(默认,同上)
289+
.flowTarget-normal {}
290+
291+
// jumpback: 橙色
292+
.flowTarget-jumpback {
293+
background-color: mix(@jumpback-color, #fff, 15%);
294+
color: darken(@jumpback-color, 10%);
295+
border-color: mix(@jumpback-color, #fff, 35%);
296+
}
297+
298+
// anchor: 青绿色
299+
.flowTarget-anchor {
300+
background-color: mix(#4a9d8e, #fff, 15%);
301+
color: darken(#4a9d8e, 10%);
302+
border-color: mix(#4a9d8e, #fff, 35%);
303+
}
304+
}
305+
226306
// 模板图片区域
227307
.nodeTemplateImages {
228308
display: flex;

0 commit comments

Comments
 (0)