fix(strategy-map): 战略地图三处 P1 缺陷(D1 完备度路由 422 / D2 连线删除不落库 / D3 窄屏拖拽失效)

D1 GET /api/cma/maps/completeness/objectives 恒 422:真抢占者是先注册的
   objectives.router GET /{map_id}/objectives(map_id 被当 int 解析)。把
   map_completeness / map_summary 两个字面路径路由器前移到 objectives 之前,
   不动 prefix、不删参数化路由。新增同前缀族全量巡检用例。

D2 DELETE /{id}/connections 返回 200 不落库:canvas_data 为裸 Column(JSON),
   就地/换列表赋值不被 ORM 判脏。模型改 MutableDict.as_mutable(JSON)(不改 DDL),
   add_connection / delete_connection_by_key 均显式 flag_modified 双保险。

D3 窄视口 .kp-panel(fixed/z-1000) 遮画布,onTreeDragOver 用 elementFromPoint
   取节点恒 null ⇒ 拖拽建连线失效。改为遍历画布内 [data-node-key] 做 rect 包含
   判定、多点取中心最近者,不再回退 elementFromPoint。

测试:新增 backend/tests/test_map_route_shadowing_20261020.py(路由解析断言到
endpoint.__module__)与 test_map_connection_persistence_20261020.py(新 session
原生 SQL 复读 + 负测)。零生产写、零上线。
This commit is contained in:
Hermes CI Fix
2026-10-21 02:04:28 +08:00
parent 86e24c6310
commit b736e0727a
6 changed files with 275 additions and 5 deletions
+27 -2
View File
@@ -994,10 +994,35 @@ function onTreeDragStart(payload: { dimKey: string; index: number; obj: any }) {
}
function onTreeDragOver(e: DragEvent) {
if (!treeDragSource.value) return
const el = document.elementFromPoint(e.clientX, e.clientY)?.closest('[data-node-key]') as HTMLElement | null
const key = el?.getAttribute('data-node-key') || null
const key = hitTestTreeNodeAt(e.clientX, e.clientY)
dragConnectTarget.value = key && key !== treeDragSource.value.key ? key : null
}
/**
* D3(2026-10-20):窄视口下 .kp-panel 是 fixed + z-index:1000 的浮层,会盖住画布主体;
* 旧实现用 document.elementFromPoint() 取节点 —— 命中面板时 .closest('[data-node-key]') 恒 null,
* 拖拽建连线必走 cancelLink()。改为**几何命中**:直接遍历画布内全部 [data-node-key],
* 用 getBoundingClientRect() 做包含判断(rect 已含 transform: scale,与 clientX/Y 同坐标系);
* 多点重叠时取中心距拖拽点最近者。**不再回退 elementFromPoint**(否则窄屏复发)。
* 性能:19 个节点的 rect 遍历一帧一次 ≈ 微秒级,无需 rAF/节流,避免节流造成 drop 时刻判据滞后。
*/
function hitTestTreeNodeAt(x: number, y: number): string | null {
const box = scrollWrapRef.value
if (!box) return null
const nodes = box.querySelectorAll<HTMLElement>('[data-node-key]')
let bestKey: string | null = null
let bestDist = Number.POSITIVE_INFINITY
for (let i = 0; i < nodes.length; i++) {
const r = nodes[i].getBoundingClientRect()
if (r.width === 0 && r.height === 0) continue // 隐藏层/折叠层
if (x < r.left || x > r.right || y < r.top || y > r.bottom) continue
const dx = x - (r.left + r.width / 2)
const dy = y - (r.top + r.height / 2)
const d = dx * dx + dy * dy
if (d < bestDist) { bestDist = d; bestKey = nodes[i].getAttribute('data-node-key') }
}
return bestKey
}
function onTreeDragLeave(e: DragEvent) {
const related = (e as any).relatedTarget as Node | null
const box = scrollWrapRef.value