feat(cma): strategy map lane KR delete entry (cma-map-kr-delete-entry-20260916)

- MapNode.vue: hover trash icon per KR row emitting delete-kr (inline SVG, no emoji)

- StrategyLayer.vue / MapCanvas.vue: passthrough + MyDialog second confirm + okrApi.deleteKr + whole-map save

- MapsDialog: single-KR policy hint (keep at least 1 KR per objective)
This commit is contained in:
Hermes CI Fix
2026-09-16 15:26:38 +08:00
parent 7c73204ade
commit 27d3e1cd73
4 changed files with 169 additions and 2 deletions
+94
View File
@@ -89,6 +89,7 @@
@node-click="(dk: string, oi: number, obj: any) => onNodeClick(dk, oi, obj)"
@reorder="(dk: string) => onLayerReorder(dk)"
@edit-kr="(p: any) => onEditKr(p.dimKey, p.idx, p.obj, p.ki, p.kr)"
@delete-kr="(p: any) => onDeleteKr(p.dimKey, p.idx, p.obj, p.ki, p.kr)"
@add-kr="(p: any) => onAddKr(p.dimKey, p.idx, p.obj)"
/>
@@ -167,6 +168,20 @@
@delete-connection="onManagerDeleteConnection"
@update-connection="onManagerUpdateConnection"
/>
<!-- KR 删除二次确认(2026-09-16):项目统一弹窗组件 MyDialog(el-dialog 有历史 bug) -->
<MyDialog v-model="showKrDeleteConfirm" title="删除关键结果" :width="420">
<div class="kr-del-confirm-text">
确定删除关键结果「<strong>{{ krDeleteTarget?.name }}</strong>」?
</div>
<div class="kr-del-confirm-hint">
删除后该 KR 会从目标中移除,并同步删除 krs 表中对应记录(不可撤销)。
</div>
<template #footer>
<el-button size="small" @click="cancelDeleteKr">取消</el-button>
<el-button size="small" type="danger" :loading="krDeleting" @click="confirmDeleteKr">删除</el-button>
</template>
</MyDialog>
</div>
<!-- P1-3: 关联知识点 -->
<div style="margin-top:12px;">
@@ -189,6 +204,7 @@ import ConnectionLines from "../components/strategy/ConnectionLines.vue"
import MapCanvasToolbar from "../components/map-canvas/MapCanvasToolbar.vue"
import MapCanvasDialogs from "../components/map-canvas/MapCanvasDialogs.vue"
import ConnectionManagerDialog from "../components/map-canvas/ConnectionManagerDialog.vue"
import MyDialog from "../components/MyDialog.vue"
import html2canvas from "html2canvas"
// ── 四层泳道配置 ──
@@ -966,6 +982,67 @@ function onAddKr(dimKey: string, oi: number, obj: any) {
debouncedSaveCanvas()
}
// ── KR 删除(2026-09-16:泳道内删除入口,补齐「目标展开后没有删除图标」缺口) ──
const showKrDeleteConfirm = ref(false)
const krDeleting = ref(false)
const krDeleteTarget = ref<{ dimKey: string; oi: number; ki: number; name: string } | null>(null)
/**
* KR 删除入口:单条时不允许删到 0(口径见 _reports/cma-map-kr-delete-entry-20260916-return.md)
* 依据:① 后端 maps.py `_sync_objectives_krs` 在目标 krs 为空时 `continue`(跳过同步),
* 删到 0 不会传播到 krs 表 → 残留孤儿行;② 无 KR 的目标无法度量达成率。
* 单条 KR 时按钮仍然显示(可发现),点击给出明确提示,避免"图标消失=功能缺失"的观感。
*/
function onDeleteKr(dimKey: string, oi: number, obj: any, ki: number, kr: any) {
const dim = dimensions.find((d: any) => d.key === dimKey)
const target = dim?.objectives?.[oi]
const list: any[] = target?.krs || []
if (!target || ki < 0 || ki >= list.length) return
if (list.length <= 1) {
ElMessage.warning('每个目标至少保留 1 条关键结果;如需替换请先添加新的 KR')
return
}
krDeleteTarget.value = { dimKey, oi, ki, name: kr?.name || `KR ${ki + 1}` }
showKrDeleteConfirm.value = true
}
function cancelDeleteKr() {
showKrDeleteConfirm.value = false
krDeleteTarget.value = null
}
async function confirmDeleteKr() {
const t = krDeleteTarget.value
if (!t || krDeleting.value) return
const dim = dimensions.find((d: any) => d.key === t.dimKey)
const target = dim?.objectives?.[t.oi]
const kr = target?.krs?.[t.ki]
if (!target || !kr) { cancelDeleteKr(); return }
krDeleting.value = true
// 落库 id:_objective_id 由地图保存时后端回写;_kr_id 由 KR 同步/地图保存时回写
const objectiveId: number | null = target._objective_id || null
const krId: number | null = kr._kr_id || kr.id || null
const removedName = kr.name || `KR ${t.ki + 1}`
// 1) 本地移除(界面即时无残留)
target.krs.splice(t.ki, 1)
cancelDeleteKr()
try {
if (objectiveId && krId) {
// 2) 精确删除 krs 表对应行
await okrApi.deleteKr(objectiveId, krId)
}
// 3) 地图 JSON 为权威:整体保存(服务端 _sync_objectives_krs 亦会清理标题不在 JSON 中的 KR 行)
if (isSaving) await new Promise(r => setTimeout(r, 600))
await saveCanvas(true)
ElMessage.success(`关键结果「${removedName}」已删除`)
} catch (e: any) {
ElMessage.error(`删除失败:${e?.response?.data?.detail || e?.message || '未知错误'}`)
} finally {
krDeleting.value = false
}
nextTick(triggerRecalc)
}
// ── 数据迁移:旧格式 kpis[] 自动转为 krs[] ──
function migrateOldKpiFormat() {
for (const dim of dimensions) {
@@ -1373,4 +1450,21 @@ onUnmounted(() => {
opacity: 0.5;
pointer-events: none;
}
/* ── KR 删除确认(MyDialog 内容样式,2026-09-16) ── */
.kr-del-confirm-text {
font-size: 14px;
color: #303133;
line-height: 1.6;
}
.kr-del-confirm-text strong {
color: #f56c6c;
word-break: break-all;
}
.kr-del-confirm-hint {
margin-top: 8px;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
</style>