feat(P1-1): KPI单值人工录入 — 新增POST /kpis/{id}/values + 前端录入弹窗

M02绩效管理闭环工具:客户/流程/学习层台账数据可直接在KPI详情录入
- 后端: 单值录入(期间+实际值), 同期间重复自动更新, 来源=manual/verified
- 前端: 历史数据Tab新增'录入数据'按钮+弹窗(期间选择+数值输入)
- 验证: C_SATISFACTION 2026-08录入85→更新88, 历史数据联动显示
This commit is contained in:
Hermes CI Fix
2026-08-26 22:34:23 +08:00
parent ba8e2f112b
commit 35974514da
2 changed files with 106 additions and 1 deletions
+51 -1
View File
@@ -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()
+55
View File
@@ -145,6 +145,9 @@
</el-tab-pane>
<el-tab-pane label="历史数据" name="history">
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
<el-button size="small" type="primary" plain @click="openAddValueDialog"> 录入数据</el-button>
</div>
<v-chart :option="chartOption" autoresize class="full-width-chart" />
<el-table :data="values" size="small" class="data-table">
<el-table-column prop="period" label="期间" width="100" />
@@ -311,6 +314,26 @@
<el-button @click="showFullNetwork = false">关闭</el-button>
</template>
</MyDialog>
<!-- 录入数据对话框 -->
<MyDialog v-model="showAddValue" title="录入KPI数据" :width="420">
<el-form label-width="80px">
<el-form-item label="KPI">
<span>{{ kpi?.kpi_code }} {{ kpi?.kpi_name }}</span>
</el-form-item>
<el-form-item label="期间">
<el-date-picker v-model="addValueForm.period" type="month" value-format="YYYY-MM" placeholder="选择月份" style="width:200px;" />
</el-form-item>
<el-form-item label="实际值">
<el-input-number v-model="addValueForm.actual_value" :precision="2" :step="1" style="width:200px;" />
<span style="margin-left:6px;color:#999;font-size:12px;">{{ kpi?.unit || '' }}</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddValue = false">取消</el-button>
<el-button type="primary" :loading="addValueSaving" @click="submitAddValue">保存</el-button>
</template>
</MyDialog>
</div>
</template>
@@ -328,6 +351,38 @@ const route = useRoute()
const kpi = ref<any>(null)
const values = ref<any[]>([])
// ── 人工录入数据 ──
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 '--'