From 35974514da702a9619693922a4067b82d27dc4b6 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Wed, 26 Aug 2026 22:34:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(P1-1):=20KPI=E5=8D=95=E5=80=BC=E4=BA=BA?= =?UTF-8?q?=E5=B7=A5=E5=BD=95=E5=85=A5=20=E2=80=94=20=E6=96=B0=E5=A2=9EPOS?= =?UTF-8?q?T=20/kpis/{id}/values=20+=20=E5=89=8D=E7=AB=AF=E5=BD=95?= =?UTF-8?q?=E5=85=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit M02绩效管理闭环工具:客户/流程/学习层台账数据可直接在KPI详情录入 - 后端: 单值录入(期间+实际值), 同期间重复自动更新, 来源=manual/verified - 前端: 历史数据Tab新增'录入数据'按钮+弹窗(期间选择+数值输入) - 验证: C_SATISFACTION 2026-08录入85→更新88, 历史数据联动显示 --- backend/app/api/kpis.py | 52 +++++++++++++++++++++++++++++- frontend/src/views/KPIDetail.vue | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/backend/app/api/kpis.py b/backend/app/api/kpis.py index 58b08d35..9ffbaf22 100644 --- a/backend/app/api/kpis.py +++ b/backend/app/api/kpis.py @@ -9,7 +9,7 @@ import json from app.database import get_db from app.deps import get_entity_id from app.auth_middleware import require_auth, require_role, filter_kpis_by_role, kpi_visible_dims -from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog, Entity, KPICausality, KPIHierarchy +from app.models import StrategicMap, MapObjective, KPIDefinition, KPIValue, KPIAlert, OperationLog, Entity, KPICausality, KPIHierarchy, User from app.api.kpi_governance import validate_kpi_payload, kpi_issues_message router = APIRouter(prefix="/api/cma/kpis", tags=["KPI字典"], @@ -475,6 +475,56 @@ def get_kpi_causality_chain( # 动态路由(必须在静态路由之后) # ============================================================ +@router.post("/{kpi_id}/values") +def create_kpi_value( + kpi_id: int, + data: dict, + db: Session = Depends(get_db), + entity_id: int = Depends(get_entity_id), + current_user: User = Depends(require_auth), +): + """录入KPI单值(人工数据录入,用于客户/流程/学习层台账数据) + Body: {period: '2026-08', actual_value: 85} + """ + kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() + if not kpi: + raise HTTPException(404, "KPI不存在") + if kpi.entity_id != entity_id: + raise HTTPException(404, "KPI不存在") + + period = data.get("period") + actual_value = data.get("actual_value") + if not period or actual_value is None: + raise HTTPException(400, "缺少必要参数: period, actual_value") + + # 同一期间重复录入 → 更新 + existing = db.query(KPIValue).filter( + KPIValue.kpi_id == kpi_id, + KPIValue.period == period, + KPIValue.source_type == "manual", + ).first() + if existing: + existing.actual_value = float(actual_value) + existing.data_status = "verified" + existing.remark = f"人工录入(更新) by {current_user.username}" + db.commit() + return {"message": "已更新", "id": existing.id} + + new_val = KPIValue( + kpi_id=kpi_id, + entity_id=entity_id, + period=period, + actual_value=float(actual_value), + source_type="manual", + source_batch=f"manual-{current_user.username}-{datetime.now().strftime('%Y%m%d')}", + data_status="verified", + remark=f"人工录入 by {current_user.username}", + ) + db.add(new_val) + db.commit() + return {"message": "已录入", "id": new_val.id, "period": period, "actual_value": float(actual_value)} + + @router.get("/{kpi_id}") def get_kpi(kpi_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_id)): kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first() diff --git a/frontend/src/views/KPIDetail.vue b/frontend/src/views/KPIDetail.vue index 2c29a616..2ccbb380 100644 --- a/frontend/src/views/KPIDetail.vue +++ b/frontend/src/views/KPIDetail.vue @@ -145,6 +145,9 @@ +
+ ➕ 录入数据 +
@@ -311,6 +314,26 @@ 关闭 + + + + + + {{ kpi?.kpi_code }} {{ kpi?.kpi_name }} + + + + + + + {{ kpi?.unit || '' }} + + + + @@ -328,6 +351,38 @@ const route = useRoute() const kpi = ref(null) const values = ref([]) +// ── 人工录入数据 ── +const showAddValue = ref(false) +const addValueSaving = ref(false) +const addValueForm = ref({ period: '', actual_value: 0 }) +function openAddValueDialog() { + const now = new Date() + addValueForm.value = { + period: `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`, + actual_value: 0, + } + showAddValue.value = true +} +async function submitAddValue() { + if (!addValueForm.value.period) { ElMessage.warning('请选择期间'); return } + addValueSaving.value = true + try { + const r: any = await api.post(`/kpis/${kpi.value.id}/values`, { + period: addValueForm.value.period, + actual_value: addValueForm.value.actual_value, + }) + ElMessage.success(r.message || '已保存') + showAddValue.value = false + // 刷新历史数据 + const kv: any = await api.get('/kpis/' + kpi.value.id) + values.value = kv.values || [] + } catch (e: any) { + ElMessage.error(e?.response?.data?.detail || e?.message || '保存失败') + } finally { + addValueSaving.value = false + } +} + // 数值格式化(历史数据对齐显示用) function formatValue(v: any) { if (v === null || v === undefined || isNaN(Number(v))) return '--'