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
@@ -61,7 +61,10 @@
<div class="mc-kr-section">
<div class="mc-kr-header">
<span>关键结果({{ form.krs.length }}条)</span>
<span>
关键结果({{ form.krs.length }}条)
<span class="mc-kr-min-hint">每个目标至少保留 1 条</span>
</span>
<span style="display:flex;gap:6px;">
<el-button size="small" plain @click="autoDivideWeights">⚖️ 自动平分</el-button>
<el-button size="small" type="primary" plain @click="addKr">+ 添加KR</el-button>
@@ -72,7 +75,7 @@
<div class="mc-kr-row-top">
<span class="mc-kr-index">#{{ ki + 1 }}</span>
<input v-model="kr.name" class="mc-input mc-kr-name" placeholder="KR名称" />
<el-button v-if="form.krs.length > 1" size="small" text type="danger" @click="removeKr(ki)">×</el-button>
<el-button v-if="form.krs.length > 1" size="small" text type="danger" title="删除该关键结果" @click="removeKr(ki)">×</el-button>
</div>
<div class="mc-kr-row-bottom">
<div class="mc-kr-field">
@@ -937,6 +940,13 @@ function planStatusLabel(s: string): string {
font-weight: 500;
color: #606266;
}
/* 单条 KR 口径提示(2026-09-16):不允许删到 0,UI 明示 */
.mc-kr-min-hint {
margin-left: 8px;
font-size: 11px;
font-weight: 400;
color: #909399;
}
.mc-kr-row {
border: 1px solid #e4e7ed;
border-radius: 6px;
@@ -51,6 +51,26 @@
<span class="kr-sub-index">{{ ki + 1 }}</span>
<span class="kr-sub-name" :title="kr.name">{{ kr.name }}</span>
<span v-if="kr.target_value" class="kr-sub-target">目标: {{ kr.target_value }}</span>
<!-- KR删除入口(2026-09-16):hover 显示,内联 SVG(Tabler trash),禁用 Emoji -->
<button
type="button"
class="kr-sub-del"
:title="canDeleteKr ? '删除该关键结果' : '每个目标至少保留 1 条关键结果'"
:aria-label="`删除关键结果 ${kr.name || ki + 1}`"
@click.stop="$emit('delete-kr', { index: ki, kr })"
>
<svg
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"
aria-hidden="true"
>
<path d="M4 7h16" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12" />
<path d="M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
</button>
</div>
<div class="kr-sub-bottom">
<div class="kr-sub-progress-wrap">
@@ -178,6 +198,7 @@ const emit = defineEmits<{
(e: 'kpi-click', kpiCode: string): void
(e: 'plan-click'): void
(e: 'edit-kr', payload: { index: number; kr: any }): void
(e: 'delete-kr', payload: { index: number; kr: any }): void
(e: 'add-kr'): void
}>()
@@ -188,6 +209,17 @@ const hasKrs = computed(() => {
return props.objective?.krs && Array.isArray(props.objective.krs) && props.objective.krs.length > 0
})
/**
* 是否允许删除 KR。
* 口径(2026-09-16):每个目标至少保留 1 条关键结果——不允许删到 0。
* 依据:后端 maps.py `_sync_objectives_krs` 在 `krs` 为空时跳过同步(`if not krs: continue`),
* 删到 0 会让 krs 表残留孤儿行;且无 KR 的目标无法计算达成率,与「KR 是 O 的度量单元」口径相悖。
* 单条 KR 时按钮仍显示(可发现),点击给出明确提示而不是静默置灰。
*/
const canDeleteKr = computed(() => {
return Array.isArray(props.objective?.krs) && props.objective.krs.length > 1
})
const objIcon = computed(() => {
if (props.objective.icon && props.iconMap) {
return (props.iconMap as Record<string, string>)[props.objective.icon]
@@ -484,6 +516,35 @@ function getKrProgressText(kr: any): string {
color: #909399;
flex-shrink: 0;
}
/* KR 删除入口:hover 才显形,避免与 KR 文本拥挤;键盘聚焦时同样可见(可访问性) */
.kr-sub-del {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
padding: 0;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: #c0c4cc;
cursor: pointer;
opacity: 0;
transition: opacity .15s, color .15s, background .15s, border-color .15s;
}
.kr-sub-item:hover .kr-sub-del { opacity: 1; }
.kr-sub-del:focus-visible {
opacity: 1;
outline: none;
border-color: #f56c6c;
}
.kr-sub-del:hover {
color: #f56c6c;
background: #fef0f0;
border-color: #fbc4c4;
}
.kr-sub-del:active { transform: scale(0.92); }
.kr-sub-bottom {
display: flex;
align-items: center;
@@ -53,6 +53,7 @@
@kpi-click="(code: string) => $emit('kpi-click', code)"
@plan-click="$emit('show-plan-list', { dimKey: layerKey, idx: index, obj: element })"
@edit-kr="(p: any) => $emit('edit-kr', { dimKey: layerKey, idx: index, obj: element, ki: p.index, kr: p.kr })"
@delete-kr="(p: any) => $emit('delete-kr', { dimKey: layerKey, idx: index, obj: element, ki: p.index, kr: p.kr })"
@add-kr="$emit('add-kr', { dimKey: layerKey, idx: index, obj: element })"
/>
</div>
@@ -116,6 +117,7 @@ const emit = defineEmits<{
(e: 'update:color', val: string): void
(e: 'update:icon', val: string): void
(e: 'edit-kr', payload: { dimKey: string; idx: number; obj: any; ki: number; kr: any }): void
(e: 'delete-kr', payload: { dimKey: string; idx: number; obj: any; ki: number; kr: any }): void
(e: 'add-kr', payload: { dimKey: string; idx: number; obj: any }): void
}>()