fix(maps): 画布连线键与目标改名解耦(objective_id 锚定)+ nodeRefs 位置兜底 + 保守反错绑断言(P3)
ci/woodpecker/push/main Pipeline failed

- map_connections 读侧以 from/to_objective_id 反查当前 dimensions 树定位节点,改名/重排不再静默断线
- 悬停/拖拽期 map_objectives 未返回时按位置兜底(仅同层同序位有效),并记录 stale_objective_id
- 新增 tests/test_conn_key_rename_stability.py(含 2 条保守反错绑负测用例)
- 零 DDL、零 health_score 改动(问3=A)
This commit is contained in:
wecom-fullstack
2026-10-23 15:49:36 +08:00
parent 986dec516e
commit b3f31f9ac9
5 changed files with 785 additions and 54 deletions
+28
View File
@@ -210,6 +210,7 @@
:layer-refs="layerRefs"
:node-refs="compiledNodeRefs"
:layer-keys="layerKeysOrdered"
:conn-keys="connKeys"
:connections="connections"
:temp-line="tempLine"
:container-key="recalcTrigger"
@@ -319,6 +320,8 @@ import MapTargetTree from "../components/strategy-map/MapTargetTree.vue"
import MapNodeDetail from "../components/strategy-map/MapNodeDetail.vue"
import MapCompleteness from "../components/strategy-map/MapCompleteness.vue"
import ConnectionLines from "../components/strategy/ConnectionLines.vue"
// P3:连线端点解析键(`层-序号`)—— 目标改名后线仍指向同一节点
import { buildKeyIndex, type ConnectionKeyEntry } from "../utils/connKeys"
import MapCanvasToolbar from "../components/map-canvas/MapCanvasToolbar.vue"
import MapDimensionFilter from "../components/map-canvas/MapDimensionFilter.vue"
import CaliberBadge from "../components/CaliberBadge.vue"
@@ -403,6 +406,8 @@ const undoStack = ref<{ dimensions: any; connections: any[] }[]>([])
// 连线
const connections = ref<any[]>([])
/** P3:服务端解析键(与 connections 逐下标对齐);空 ⇒ 回落原标识串(历史/降级) */
const connKeys = ref<ConnectionKeyEntry[]>([])
const linkingFrom = ref<{ key: string; obj: any } | null>(null)
const tempLine = ref<{ x1: number; y1: number; x2: number; y2: number } | null>(null)
const dragConnectTarget = ref<string | null>(null)
@@ -797,6 +802,22 @@ watch(zoomLevel, () => {
nextTick(() => connectionLinesRef.value?.recalcAll())
})
// ── P3:服务端解析键 ──
// 为什么必须取服务端:canvas_data 里存的是写入时刻的标识串(`层:中文名`),
// 目标改名后名字串失效;服务端能由 from/to_objective_id 反查当前 `${层}-${序号}`。
// 取不到(接口异常/离线)⇒ 置空,画布回落原标识串,行为与改造前一致(不更坏)。
async function refreshConnKeys() {
const id = selectedMap.value
if (!id) { connKeys.value = []; return }
try {
const r: any = await api.get(`/maps/${id}/connections`)
const serverConns = r?.data?.connections || []
connKeys.value = buildKeyIndex(loadedCanvas || connections.value || [], serverConns)
} catch (e) {
connKeys.value = []
}
}
// ── 画布加载 ──
function loadCanvas() {
caliberFacts.value = caliberGapFor(currentCaliberEntityId())
@@ -842,6 +863,7 @@ function loadCanvas() {
loadedDims = deepClone(dimensions)
connections.value = map?.canvas_data?.connections || []
loading.value = false
refreshConnKeys()
nextTick(() => {
triggerRecalc()
updateGridMode()
@@ -982,6 +1004,7 @@ function undoLast() {
connections.value = JSON.parse(JSON.stringify(snap.connections))
selectedNode.value = null
markDirty()
refreshConnKeys() // P3:撤销可能改动目标顺序/名字,解析键随之刷新
nextTick(() => { triggerRecalc(); updateZoomWrapHeight(); loadObjectiveLevels() })
ElMessage.success('已撤销上一步')
}
@@ -1716,6 +1739,9 @@ async function saveCanvas(silent?: boolean) {
if (res?.updated_at) baseUpdatedAt.value = res.updated_at // 采纳服务端新基准
refreshBaselines(res?.dimensions) // 基线回到服务端现值(响应体为权威)
markClean()
// P3:保存后刷新解析键(改名/调序都会让 `${层}-${序号}` 变,线要跟着节点走)
if (payload.canvas_data) loadedCanvas = deepClone(connections.value)
refreshConnKeys()
if (!silent) ElMessage.success("已保存")
} catch (e: any) {
if (e?.response?.status === 409) {
@@ -1742,6 +1768,8 @@ async function saveConnections() {
base_updated_at: currentBaseStamp(),
})
if (res?.updated_at) baseUpdatedAt.value = res.updated_at
loadedCanvas = deepClone(connections.value)
refreshConnKeys() // P3:连线新增/删除后服务端重算解析键
} catch (e: any) {
if (e?.response?.status === 409) {
autoSaveBlocked.value = true