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:
@@ -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_id(KR落库用)
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user