diff --git a/backend/app/api/kpis.py b/backend/app/api/kpis.py index 61f203b6..3645fb5a 100644 --- a/backend/app/api/kpis.py +++ b/backend/app/api/kpis.py @@ -485,21 +485,52 @@ def get_kpi(kpi_id: int, db: Session = Depends(get_db), entity_id: int = Depends result = kpi_to_dict(kpi) # 附带历史数据(前端KPI详情"历史数据"Tab依赖) # 2026-08-26修复: 原实现只返回定义信息,values缺失导致历史数据Tab空白 + # 2026-08-26增强: values对齐KPI元数据(目标值/偏差/红黄绿判定/单位) vals = db.query(KPIValue).filter( KPIValue.kpi_id == kpi_id, KPIValue.actual_value.isnot(None), ).order_by(KPIValue.period.asc()).all() - result["values"] = [ - { + is_reverse = kpi.kpi_code in REVERSE_INDICATORS + target = kpi.target_value + result["values"] = [] + for v in vals: + # 判定红黄绿(对齐工作台语义:正向≥0.9绿/≥0.7黄/否则红;反向≤目标绿/≤1.1倍黄/否则红) + level = "info" + score = None + if target and v.actual_value is not None: + if is_reverse: + if v.actual_value <= target: + level = "green" + elif v.actual_value <= target * 1.1: + level = "yellow" + else: + level = "red" + else: + ratio = v.actual_value / target + if ratio >= 0.9: + level = "green" + elif ratio >= 0.7: + level = "yellow" + else: + level = "red" + score, _ = _calc_five_tier_score(v.actual_value, target, is_reverse=is_reverse) + # 偏差率(相对目标) + deviation = None + if target and target != 0 and v.actual_value is not None: + deviation = round((v.actual_value - target) / target * 100, 1) + result["values"].append({ "id": v.id, "period": v.period, "actual_value": v.actual_value, + "target_value": target, + "unit": kpi.unit or "", + "deviation_pct": deviation, + "level": level, + "score": score, "source_type": v.source_type, "data_status": v.data_status, "source_batch": v.source_batch, - } - for v in vals - ] + }) return result diff --git a/frontend/src/views/KPIDetail.vue b/frontend/src/views/KPIDetail.vue index 24387383..61df0f08 100644 --- a/frontend/src/views/KPIDetail.vue +++ b/frontend/src/views/KPIDetail.vue @@ -147,12 +147,33 @@ - - - - + + + + + + + + + + + + + + + + @@ -304,6 +325,14 @@ import { useFormGuard } from '../composables/useFormGuard' const route = useRoute() const kpi = ref(null) const values = ref([]) + +// 数值格式化(历史数据对齐显示用) +function formatValue(v: any) { + if (v === null || v === undefined || isNaN(Number(v))) return '--' + const n = Number(v) + // 整数不带小数,非整数保留2位 + return n.toLocaleString('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 2 }) +} const mapOptions = ref([]) const activeTab = ref('basic')