feat(P1-1): KPI单值人工录入 — 新增POST /kpis/{id}/values + 前端录入弹窗
M02绩效管理闭环工具:客户/流程/学习层台账数据可直接在KPI详情录入 - 后端: 单值录入(期间+实际值), 同期间重复自动更新, 来源=manual/verified - 前端: 历史数据Tab新增'录入数据'按钮+弹窗(期间选择+数值输入) - 验证: C_SATISFACTION 2026-08录入85→更新88, 历史数据联动显示
This commit is contained in:
+51
-1
@@ -9,7 +9,7 @@ import json
|
|||||||
from app.database import get_db
|
from app.database import get_db
|
||||||
from app.deps import get_entity_id
|
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.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
|
from app.api.kpi_governance import validate_kpi_payload, kpi_issues_message
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/cma/kpis", tags=["KPI字典"],
|
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}")
|
@router.get("/{kpi_id}")
|
||||||
def get_kpi(kpi_id: int, db: Session = Depends(get_db), entity_id: int = Depends(get_entity_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()
|
kpi = db.query(KPIDefinition).filter(KPIDefinition.id == kpi_id).first()
|
||||||
|
|||||||
@@ -145,6 +145,9 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="历史数据" name="history">
|
<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" />
|
<v-chart :option="chartOption" autoresize class="full-width-chart" />
|
||||||
<el-table :data="values" size="small" class="data-table">
|
<el-table :data="values" size="small" class="data-table">
|
||||||
<el-table-column prop="period" label="期间" width="100" />
|
<el-table-column prop="period" label="期间" width="100" />
|
||||||
@@ -311,6 +314,26 @@
|
|||||||
<el-button @click="showFullNetwork = false">关闭</el-button>
|
<el-button @click="showFullNetwork = false">关闭</el-button>
|
||||||
</template>
|
</template>
|
||||||
</MyDialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -328,6 +351,38 @@ const route = useRoute()
|
|||||||
const kpi = ref<any>(null)
|
const kpi = ref<any>(null)
|
||||||
const values = ref<any[]>([])
|
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) {
|
function formatValue(v: any) {
|
||||||
if (v === null || v === undefined || isNaN(Number(v))) return '--'
|
if (v === null || v === undefined || isNaN(Number(v))) return '--'
|
||||||
|
|||||||
Reference in New Issue
Block a user