feat(KR): KR完整修复 — krs表打通+方向符号operator+权重自由输入

- 后端okr.py: KR读取从ActionPlan改为krs表, 新增KR CRUD API(POST/PUT/DELETE /okr/{objective_id}/krs) + 批量sync
- operator方向符号: krs表加operator/tolerance/weight/sort_order/monthly_milestones列
- progress方向感知计算(>=/>: current/target, <=/<: target/current, =: 容差), 达成→status=achieved
- 关联KPI自动继承方向(threshold_green解析: F_COST_RATIO<=18等)
- maps.py: 保存地图时自动同步objectives+krs表, JSON→krs数据迁移脚本
- 前端: 权重下拉改自由数字输入(可小数33.33) + 方向选择器(≥/≤/>/</=) + 自动平分按钮 + KPI方向继承提示
- pytest: 10个新测试(krs CRUD/方向感知/权重校验/多租户隔离) + 更新旧KR测试
- 迁移: 现有strategic_maps JSON 12条KR已写入krs表
This commit is contained in:
Hermes CI Fix
2026-08-27 10:59:56 +08:00
parent 87cb9e7db2
commit 61936302b2
11 changed files with 1030 additions and 123 deletions
+37 -1
View File
@@ -161,7 +161,7 @@
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, nextTick, onUnmounted } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"
import { mapApi, kpiApi, okrTemplateApi } from "../api/index"
import { mapApi, kpiApi, okrTemplateApi, okrApi } from "../api/index"
import KnowledgePanel from '../components/KnowledgePanel.vue'
import api from "../api/index"
import { LAYER_CONFIG, LAYER_KEYS, getLayerList } from "../config/layers"
@@ -654,8 +654,16 @@ function onNodeSave(data: any) {
if (!data.krs || data.krs.length === 0) {
node.krs = dim.objectives[idx].krs || []
}
// 保留后端回写的 objective_idKR落库用)
if (dim.objectives[idx]._objective_id) {
node._objective_id = dim.objectives[idx]._objective_id
}
dim.objectives[idx] = { ...node, ...extra }
ElMessage.success("节点已更新")
// KR完整修复: 同步krs表(objective_id存在时直接调API
if (node._objective_id && data.krs && data.krs.length > 0) {
syncNodeKrs(node._objective_id, data.krs)
}
}
} else {
// 新增节点
@@ -673,6 +681,34 @@ function onNodeSave(data: any) {
nextTick(triggerRecalc)
}
// ── KR完整修复(2026-08-27): 节点KR同步到krs表 ──
async function syncNodeKrs(objectiveId: number, krs: any[]) {
try {
const payload = (krs || []).map((kr: any) => ({
id: kr._kr_id || kr.id || null,
title: kr.name || kr.title || "",
operator: kr.operator || ">=",
target_value: kr.target_value ?? null,
weight: kr.weight ?? null,
kpi_code: kr.kpi_code || null,
due_date: kr.due_date || null,
})).filter((k: any) => k.title)
if (payload.length === 0) return
const res: any = await okrApi.syncKrs(objectiveId, payload)
// 回写 _kr_id,避免下次重复创建
if (res?.krs) {
const idByTitle: Record<string, number> = {}
res.krs.forEach((r: any) => { idByTitle[r.title] = r.id })
krs.forEach((kr: any) => {
if (kr.name && idByTitle[kr.name]) kr._kr_id = idByTitle[kr.name]
})
}
} catch (e: any) {
// 静默失败:地图JSON仍是权威保存,krs表同步失败不阻塞画布
console.warn("[KR同步] 节点KR同步失败:", e?.response?.data?.detail || e?.message)
}
}
// ── 用户自定义O自动入库(OKR模板库第3层) ──
let userTemplateCooldown: Record<string, number> = {}
async function saveUserTemplate(data: any, layer: string) {