From 790be2d6b10de6b1a4bf9c680376842f01face30 Mon Sep 17 00:00:00 2001 From: wecom-fullstack Date: Tue, 20 Oct 2026 20:35:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(strategy-map):=20=E4=B8=89=E5=A4=84?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=E2=80=94=E2=80=94D1=20=E5=AE=8C=E5=A4=87?= =?UTF-8?q?=E5=BA=A6/=E6=91=98=E8=A6=81=E8=B7=AF=E7=94=B1=E5=85=88?= =?UTF-8?q?=E6=B3=A8=E5=86=8C(=E4=BF=AE=20/completeness/objectives=20422)?= =?UTF-8?q?=E3=80=81D2=20=E8=BF=9E=E7=BA=BF=E5=A2=9E=E5=88=A0=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=20flag=5Fmodified(=E4=BF=AE=20DELETE=20200=20?= =?UTF-8?q?=E4=B8=8D=E8=90=BD=E5=BA=93)=E3=80=81D3=20=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E5=91=BD=E4=B8=AD=E6=94=B9=20rect=20=E5=88=A4=E5=AE=9A(?= =?UTF-8?q?=E4=BF=AE=E7=AA=84=E5=B1=8F=E7=9F=A5=E8=AF=86=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=E9=81=AE=E6=8C=A1=E5=AF=BC=E8=87=B4=E6=8B=96=E6=8B=BD=E5=A4=B1?= =?UTF-8?q?=E6=95=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/api/maps.py | 6 ++++++ backend/app/main.py | 6 ++++-- frontend/src/views/MapCanvas.vue | 15 +++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/backend/app/api/maps.py b/backend/app/api/maps.py index feca4482..a8ec85e2 100644 --- a/backend/app/api/maps.py +++ b/backend/app/api/maps.py @@ -558,6 +558,8 @@ def add_connection(map_id: int, data: dict, db: Session = Depends(get_db), entit "label": data.get("label", ""), }) m.canvas_data["connections"] = conns + from sqlalchemy.orm.attributes import flag_modified + flag_modified(m, "canvas_data") # 同上:JSON 列需显式标脏才能落库 db.commit() # updated_at: 供前端采纳为新基准(并发保护 base_updated_at),避免自伤 409 return {"connections": conns, "updated_at": _stamp(m.updated_at)} @@ -580,6 +582,10 @@ def delete_connection_by_key(map_id: int, data: dict, db: Session = Depends(get_ raise HTTPException(404, "连线不存在") m.canvas_data["connections"] = new_conns + # JSON 列就地/换列表赋值不会被 ORM 自动标记为 dirty ⇒ 必须显式 flag_modified, + # 否则接口返回 200 且响应列表已减 1,但 commit 不 flush,重读仍是旧值(删连线变哑弹)。 + from sqlalchemy.orm.attributes import flag_modified + flag_modified(m, "canvas_data") db.commit() return {"connections": new_conns, "removed": {"from": from_id, "to": to_id}, "updated_at": _stamp(m.updated_at)} diff --git a/backend/app/main.py b/backend/app/main.py index b5c98863..e79135c3 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -40,6 +40,10 @@ app.include_router(auth.router) app.include_router(kpis.router) app.include_router(kpi_governance.router) app.include_router(templates.router) +# ⚠️ 顺序要求:完备度/摘要条路由必须先于 maps/objectives 等「/{map_id}/...」通配路由注册, +# 否则 /maps/completeness/objectives 会被 /{map_id}/objectives 抢先匹配(map_id="completeness" → 422)。 +app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0) +app.include_router(map_summary.router) # 战略地图摘要条(收入分线/新式毛利率/管理指标 · 口径 CMA-A1 终稿) app.include_router(maps.router) app.include_router(dashboard.router) app.include_router(data.router) @@ -56,8 +60,6 @@ app.include_router(action_plans.router) app.include_router(alignment.router) app.include_router(org.router) app.include_router(objectives.router) -app.include_router(map_completeness.router) # 战略地图数据完备度(口径 CMA-CALIBER-MAP-COMPLETENESS-20261018 v1.0) -app.include_router(map_summary.router) # 战略地图摘要条(收入分线/新式毛利率/管理指标 · 口径 CMA-A1 终稿) app.include_router(versions.router) app.include_router(budget.router) app.include_router(value_sources.router) diff --git a/frontend/src/views/MapCanvas.vue b/frontend/src/views/MapCanvas.vue index 58d6420a..7aa36419 100644 --- a/frontend/src/views/MapCanvas.vue +++ b/frontend/src/views/MapCanvas.vue @@ -994,10 +994,21 @@ 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 + // 命中判定改用节点 rect 包含判断:elementFromPoint 会被覆盖层(窄屏时的知识面板 .kp-panel、 + // 连线 SVG 等)挡住 ⇒ 拖拽在窄屏下永远取不到目标节点。rect 判定与层级无关。 + const key = hitTestNodeKey(e.clientX, e.clientY) dragConnectTarget.value = key && key !== treeDragSource.value.key ? key : null } +function hitTestNodeKey(x: number, y: number): string | null { + const nodes = document.querySelectorAll('[data-node-key]') as NodeListOf + for (const el of Array.from(nodes)) { + const r = el.getBoundingClientRect() + if (r.width > 0 && r.height > 0 && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) { + return el.getAttribute('data-node-key') + } + } + return null +} function onTreeDragLeave(e: DragEvent) { const related = (e as any).relatedTarget as Node | null const box = scrollWrapRef.value