Commit 5f2560da75516afe43bd4277d3556a594cc4fe8c
1 parent
4253c4be
fix(graph-console): org/work graph 节点布局改为从上到下
节点多时左右排列会把屏幕撑成很宽的多栏视图,改成按依赖深度自上而下、 同波并行节点左右排开,更贴合画布容器的竖直滚动。backward 回退边路由 相应改到节点带右侧走车道(原来在下方)。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U7xYyvv3pzuV9diMVUH5QL
Showing
1 changed file
with
35 additions
and
32 deletions
web/src/components/WorkGraphCanvas.vue
| @@ -214,10 +214,12 @@ const waves = computed(() => { | @@ -214,10 +214,12 @@ const waves = computed(() => { | ||
| 214 | for (const id of wave) remaining.delete(id); | 214 | for (const id of wave) remaining.delete(id); |
| 215 | out.push(wave); | 215 | out.push(wave); |
| 216 | } | 216 | } |
| 217 | - if (remaining.size) out.push([...remaining]); // 兜底:环里的节点单独摆一列,别让它们消失 | 217 | + if (remaining.size) out.push([...remaining]); // 兜底:环里的节点单独摆一行,别让它们消失 |
| 218 | return out; | 218 | return out; |
| 219 | }); | 219 | }); |
| 220 | 220 | ||
| 221 | +// 布局方向:wave(依赖深度)走 y 轴自上而下,同波内的并行节点走 x 轴左右排开—— | ||
| 222 | +// 节点一多,从左到右排会把屏幕撑成很宽的 4 栏,改成自上而下更贴合竖直滚动的画布容器。 | ||
| 221 | const laidOut = computed(() => { | 223 | const laidOut = computed(() => { |
| 222 | const byId = new Map(props.nodes.map((n) => [String(n.defNodeId), n])); | 224 | const byId = new Map(props.nodes.map((n) => [String(n.defNodeId), n])); |
| 223 | const out = []; | 225 | const out = []; |
| @@ -231,8 +233,8 @@ const laidOut = computed(() => { | @@ -231,8 +233,8 @@ const laidOut = computed(() => { | ||
| 231 | status: n.status || 'pending', | 233 | status: n.status || 'pending', |
| 232 | nodeKind: n.nodeKind, | 234 | nodeKind: n.nodeKind, |
| 233 | attempt: n.attempt || 0, | 235 | attempt: n.attempt || 0, |
| 234 | - x: PAD + wi * (NW + GAP_X), | ||
| 235 | - y: PAD + ni * (NH + GAP_Y), | 236 | + x: PAD + ni * (NW + GAP_X), |
| 237 | + y: PAD + wi * (NH + GAP_Y), | ||
| 236 | }); | 238 | }); |
| 237 | }); | 239 | }); |
| 238 | }); | 240 | }); |
| @@ -255,16 +257,16 @@ const forwardEdges = computed(() => | @@ -255,16 +257,16 @@ const forwardEdges = computed(() => | ||
| 255 | + (cond === 'fail' ? `|fail_reason=${e.failReason || '(兜底边:任何未匹配的失败)'}` : '') | 257 | + (cond === 'fail' ? `|fail_reason=${e.failReason || '(兜底边:任何未匹配的失败)'}` : '') |
| 256 | // 这条边传的是什么(graph_edge_contracts)。悬停在线上就能看全,不占布局 | 258 | // 这条边传的是什么(graph_edge_contracts)。悬停在线上就能看全,不占布局 |
| 257 | + (c ? `|数据契约 ${c.title}(? = 可选)` : ''), | 259 | + (c ? `|数据契约 ${c.title}(? = 可选)` : ''), |
| 258 | - x1: a.x + NW, y1: a.y + NH / 2, | ||
| 259 | - x2: b.x, y2: b.y + NH / 2, | 260 | + x1: a.x + NW / 2, y1: a.y + NH, |
| 261 | + x2: b.x + NW / 2, y2: b.y, | ||
| 260 | contract: c, | 262 | contract: c, |
| 261 | }]; | 263 | }]; |
| 262 | }), | 264 | }), |
| 263 | ); | 265 | ); |
| 264 | 266 | ||
| 265 | /** | 267 | /** |
| 266 | - * 契约药丸的位置 = 边的中点(两列之间的空白带),宽度按字数估。 | ||
| 267 | - * kind 名最长按 7 字符截断(`acceptance` → `accepta…`):GAP_X 只有 52px, | 268 | + * 契约药丸的位置 = 边的中点(两行之间的空白带),宽度按字数估。 |
| 269 | + * kind 名最长按 7 字符截断(`acceptance` → `accepta…`):药丸宽度封顶到节点列宽, | ||
| 268 | * 不截会盖到相邻节点上——完整名字在 <title> 里。 | 270 | * 不截会盖到相邻节点上——完整名字在 <title> 里。 |
| 269 | */ | 271 | */ |
| 270 | const contractPills = computed(() => | 272 | const contractPills = computed(() => |
| @@ -278,7 +280,8 @@ const contractPills = computed(() => | @@ -278,7 +280,8 @@ const contractPills = computed(() => | ||
| 278 | key: e.key, | 280 | key: e.key, |
| 279 | x: (e.x1 + e.x2) / 2, | 281 | x: (e.x1 + e.x2) / 2, |
| 280 | y: (e.y1 + e.y2) / 2, | 282 | y: (e.y1 + e.y2) / 2, |
| 281 | - w: Math.min(GAP_X + 10, text.length * 5.9 + 12), | 283 | + // 边现在竖着走(同列上下相邻),药丸宽度受节点列宽约束,不再是 GAP_X |
| 284 | + w: Math.min(NW - 16, text.length * 5.9 + 12), | ||
| 282 | text, | 285 | text, |
| 283 | title: `数据契约:${e.contract.title}(? = 可选输入,缺了不算错)`, | 286 | title: `数据契约:${e.contract.title}(? = 可选输入,缺了不算错)`, |
| 284 | optionalOnly: e.contract.optionalOnly, | 287 | optionalOnly: e.contract.optionalOnly, |
| @@ -290,12 +293,12 @@ const contractPills = computed(() => | @@ -290,12 +293,12 @@ const contractPills = computed(() => | ||
| 290 | * backward 边的曲线路由(§八.5 点名的「真实手写前端工程量」)。 | 293 | * backward 边的曲线路由(§八.5 点名的「真实手写前端工程量」)。 |
| 291 | * | 294 | * |
| 292 | * 重叠问题拆成两半,各用一招解决,不引图形库: | 295 | * 重叠问题拆成两半,各用一招解决,不引图形库: |
| 293 | - * ① 与节点重叠 —— 回退边一律走**节点带下方**的空白区,起终点都取节点底边中点, | ||
| 294 | - * 三次贝塞尔两个控制点都压在同一条泳道高度上 → 曲线永远不穿过任何节点矩形; | ||
| 295 | - * ② 与其它回退边重叠 —— 按 x 区间做贪心分道(区间重叠的边必然分到不同泳道), | ||
| 296 | - * 泳道纵向拉开 LANE_H,画布高度随泳道数增长。区间不重叠的边共用一道,不浪费高度。 | ||
| 297 | - * 代价:泳道数 = 最大同时重叠的回退边数,多到十几条时画布会明显变高; | ||
| 298 | - * 升级路径是改成上下双侧分流(奇偶道走节点带上方),本版不做。 | 296 | + * ① 与节点重叠 —— 回退边一律走**节点带右侧**的空白区,起终点都取节点右边中点, |
| 297 | + * 三次贝塞尔两个控制点都压在同一条泳道横坐标上 → 曲线永远不穿过任何节点矩形; | ||
| 298 | + * ② 与其它回退边重叠 —— 按 y 区间做贪心分道(区间重叠的边必然分到不同泳道), | ||
| 299 | + * 泳道横向拉开 LANE_H,画布宽度随泳道数增长。区间不重叠的边共用一道,不浪费宽度。 | ||
| 300 | + * 代价:泳道数 = 最大同时重叠的回退边数,多到十几条时画布会明显变宽; | ||
| 301 | + * 升级路径是改成左右双侧分流(奇偶道走节点带左侧),本版不做。 | ||
| 299 | */ | 302 | */ |
| 300 | const backwardEdges = computed(() => { | 303 | const backwardEdges = computed(() => { |
| 301 | const raw = []; | 304 | const raw = []; |
| @@ -304,13 +307,13 @@ const backwardEdges = computed(() => { | @@ -304,13 +307,13 @@ const backwardEdges = computed(() => { | ||
| 304 | const a = posById.value.get(String(e.from)); | 307 | const a = posById.value.get(String(e.from)); |
| 305 | const b = posById.value.get(String(e.to)); | 308 | const b = posById.value.get(String(e.to)); |
| 306 | if (!a || !b) return; | 309 | if (!a || !b) return; |
| 307 | - const sx = a.x + NW / 2, sy = a.y + NH; | ||
| 308 | - const tx = b.x + NW / 2, ty = b.y + NH; | 310 | + const sx = a.x + NW, sy = a.y + NH / 2; |
| 311 | + const tx = b.x + NW, ty = b.y + NH / 2; | ||
| 309 | raw.push({ key: e.id || `${e.from}-${e.to}-${i}`, edgeId: e.id, sx, sy, tx, ty, | 312 | raw.push({ key: e.id || `${e.from}-${e.to}-${i}`, edgeId: e.id, sx, sy, tx, ty, |
| 310 | - lo: Math.min(sx, tx), hi: Math.max(sx, tx) }); | 313 | + lo: Math.min(sy, ty), hi: Math.max(sy, ty) }); |
| 311 | }); | 314 | }); |
| 312 | 315 | ||
| 313 | - // 贪心分道:按区间左端排序,塞进第一条不与已占区间重叠的泳道 | 316 | + // 贪心分道:按区间上端排序,塞进第一条不与已占区间重叠的泳道 |
| 314 | const lanes = []; | 317 | const lanes = []; |
| 315 | raw.sort((p, q) => p.lo - q.lo); | 318 | raw.sort((p, q) => p.lo - q.lo); |
| 316 | for (const e of raw) { | 319 | for (const e of raw) { |
| @@ -321,13 +324,13 @@ const backwardEdges = computed(() => { | @@ -321,13 +324,13 @@ const backwardEdges = computed(() => { | ||
| 321 | } | 324 | } |
| 322 | 325 | ||
| 323 | return raw.map((e) => { | 326 | return raw.map((e) => { |
| 324 | - const laneY = nodeBandH.value + PAD + (e.lane + 1) * LANE_H; | 327 | + const laneX = nodeBandW.value + PAD + (e.lane + 1) * LANE_H; |
| 325 | return { | 328 | return { |
| 326 | ...e, | 329 | ...e, |
| 327 | - d: `M${e.sx},${e.sy} C${e.sx},${laneY} ${e.tx},${laneY} ${e.tx},${e.ty}`, | ||
| 328 | - // 三次贝塞尔 t=0.5 处的坐标(两个控制点同 y,纵向化简为 (sy+ty+6*laneY)/8) | ||
| 329 | - mx: (e.sx + e.tx) / 2, | ||
| 330 | - my: (e.sy + e.ty + 6 * laneY) / 8, | 330 | + d: `M${e.sx},${e.sy} C${laneX},${e.sy} ${laneX},${e.ty} ${e.tx},${e.ty}`, |
| 331 | + // 三次贝塞尔 t=0.5 处的坐标(两个控制点同 x,横向化简为 (sx+tx+6*laneX)/8) | ||
| 332 | + mx: (e.sx + e.tx + 6 * laneX) / 8, | ||
| 333 | + my: (e.sy + e.ty) / 2, | ||
| 331 | loopCount: latestLoopCount(e.edgeId), | 334 | loopCount: latestLoopCount(e.edgeId), |
| 332 | }; | 335 | }; |
| 333 | }); | 336 | }); |
| @@ -350,21 +353,21 @@ const anomalyNodes = computed(() => laidOut.value.filter((n) => n.status === 'bl | @@ -350,21 +353,21 @@ const anomalyNodes = computed(() => laidOut.value.filter((n) => n.status === 'bl | ||
| 350 | const anomalyBand = computed(() => { | 353 | const anomalyBand = computed(() => { |
| 351 | if (!backwardEdges.value.length) return null; | 354 | if (!backwardEdges.value.length) return null; |
| 352 | return { | 355 | return { |
| 353 | - x: PAD / 2, | ||
| 354 | - y: nodeBandH.value + PAD / 2, | ||
| 355 | - w: W.value - PAD, | ||
| 356 | - h: laneCount.value * LANE_H + LANE_H / 2, | 356 | + x: nodeBandW.value + PAD / 2, |
| 357 | + y: PAD / 2, | ||
| 358 | + w: laneCount.value * LANE_H + LANE_H / 2, | ||
| 359 | + h: H.value - PAD, | ||
| 357 | }; | 360 | }; |
| 358 | }); | 361 | }); |
| 359 | 362 | ||
| 360 | -const nodeBandH = computed(() => { | 363 | +const nodeBandW = computed(() => { |
| 361 | const maxRow = Math.max(...waves.value.map((w) => w.length), 1); | 364 | const maxRow = Math.max(...waves.value.map((w) => w.length), 1); |
| 362 | - return PAD + maxRow * (NH + GAP_Y) - GAP_Y; | 365 | + return PAD + maxRow * (NW + GAP_X) - GAP_X; |
| 363 | }); | 366 | }); |
| 364 | 367 | ||
| 365 | -const W = computed(() => | ||
| 366 | - Math.max(320, PAD * 2 + Math.max(waves.value.length, 1) * (NW + GAP_X) - GAP_X)); | ||
| 367 | -const H = computed(() => nodeBandH.value + PAD + laneCount.value * LANE_H + (laneCount.value ? LANE_H : 0)); | 368 | +const W = computed(() => nodeBandW.value + PAD + laneCount.value * LANE_H + (laneCount.value ? LANE_H : 0)); |
| 369 | +const H = computed(() => | ||
| 370 | + Math.max(320, PAD * 2 + Math.max(waves.value.length, 1) * (NH + GAP_Y) - GAP_Y)); | ||
| 368 | 371 | ||
| 369 | function trim(s) { | 372 | function trim(s) { |
| 370 | const str = String(s ?? ''); | 373 | const str = String(s ?? ''); |