feat: O→KR→KPI Phase 0 - KR sub-nodes on canvas, KR edit dialog, data migration

- MapNode.vue: Add collapsible KR sub-list with progress bars under each O node
- MapCanvasDialogs.vue: Add KPI selector per KR, editing mode (skip template step, pre-fill data), validation (max 5 KR, target value required)
- StrategyLayer.vue: Pass through edit-kr/add-kr events
- MapCanvas.vue: Data migration (old kpis[]→krs[]), KR add/edit handlers, pass editingObj/allKpis to dialog
This commit is contained in:
Hermes CI Fix
2026-07-26 18:01:59 +08:00
parent 84749a35ca
commit b593e0383f
4 changed files with 356 additions and 14 deletions
+61 -1
View File
@@ -76,6 +76,8 @@
@show-plan-list="(p: any) => showPlanList(p.dimKey, p.idx, p.obj)"
@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)"
@add-kr="(p: any) => onAddKr(p.dimKey, p.idx, p.obj)"
/>
<!-- 跨层箭头指示器(根据视角切换方向) -->
@@ -121,6 +123,8 @@
:plan-popup-obj-name="planPopupObjName"
:plan-popup-list="planPopupList"
:editing-layer-key="editingLayerKey"
:editing-obj="editingNodeData"
:all-kpis="allKpis"
@update:show-node-dialog="v => showNodeDialog = v"
@update:show-causality-dialog="v => showCausalityDialog = v"
@update:show-versions="v => showVersions = v"
@@ -357,6 +361,8 @@ function loadCanvas() {
}
// 确保四层都存在
ensureFourLayers()
// 数据迁移:旧格式 kpis[] → krs[]
migrateOldKpiFormat()
connections.value = map?.canvas_data?.connections || []
loading.value = false
nextTick(() => {
@@ -534,6 +540,11 @@ function openEditDialog(dimKey: string, oi: number, obj: any) {
owner: obj.owner || '',
isLeading: obj.isLeading ?? obj.is_leading ?? false,
description: obj.description || '',
// O+KR 扩展字段(传递给弹窗预填)
krs: obj.krs || [],
o_name: obj.name,
o_description: obj.description || '',
_rawObjective: obj, // 保留原始引用以便直接修改KR
}
// 保存编辑索引以便更新
editingNodeData.value._dimKey = dimKey
@@ -567,11 +578,15 @@ function onNodeSave(data: any) {
// 编辑已有节点
const idx = editingNodeData.value._index
if (dim.objectives[idx]) {
// 保留原有kpis等额外字段
// 保留原有kpis/krs等额外字段(当新数据未提供时)
const extra = {
kpis: dim.objectives[idx].kpis || [],
icon: dim.objectives[idx].icon || 'target',
}
// 如果新数据没有提供krs,保留旧krs
if (!data.krs || data.krs.length === 0) {
node.krs = dim.objectives[idx].krs || []
}
dim.objectives[idx] = { ...node, ...extra }
ElMessage.success("节点已更新")
}
@@ -604,6 +619,51 @@ async function deleteNode(dimKey: string, oi: number) {
} catch { /* cancel */ }
}
// ── KR操作 ──
function onEditKr(dimKey: string, oi: number, obj: any, ki: number, kr: any) {
// 打开O编辑弹窗并定位到对应KR
openEditDialog(dimKey, oi, obj)
}
function onAddKr(dimKey: string, oi: number, obj: any) {
const dim = dimensions.find((d: any) => d.key === dimKey)
if (!dim || !dim.objectives[oi]) return
const target = dim.objectives[oi]
if (!target.krs) target.krs = []
if (target.krs.length >= 5) {
ElMessage.warning('每个目标最多5个关键结果')
return
}
target.krs.push({
name: "",
target_value: "",
weight: 33,
kpi_code: null,
sort_order: target.krs.length + 1,
})
ElMessage.success("已添加空白KR请编辑完善")
debouncedSaveCanvas()
}
// ── 数据迁移:旧格式 kpis[] 自动转为 krs[] ──
function migrateOldKpiFormat() {
for (const dim of dimensions) {
for (const obj of dim.objectives || []) {
// 仅当有 kpis 数组 且 没有 krs 时执行迁移
if (obj.kpis && Array.isArray(obj.kpis) && obj.kpis.length > 0 && (!obj.krs || obj.krs.length === 0)) {
obj.krs = obj.kpis.map((kpiCode: string, idx: number) => ({
name: kpiNameMap[kpiCode] || kpiCode,
target_value: "",
weight: Math.round(100 / obj.kpis.length),
kpi_code: kpiCode,
sort_order: idx + 1,
}))
console.log(`[迁移] 目标「${obj.name}」: ${obj.kpis.length} 条KPI已转为KR`)
}
}
}
}
// ── 保存 ──
let saveTimer: ReturnType<typeof setTimeout> | null = null
let autoSaveTimer: ReturnType<typeof setInterval> | null = null