From a253eadeb08479641bdd02c93e46598456fff992 Mon Sep 17 00:00:00 2001 From: Hermes CI Fix Date: Wed, 26 Aug 2026 09:27:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20KPI=E8=AF=A6=E6=83=85=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=AF=B9=E9=BD=90=E5=85=83=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=20=E2=80=94=20=E7=9B=AE=E6=A0=87=E5=80=BC/=E5=81=8F=E5=B7=AE%/?= =?UTF-8?q?=E7=BA=A2=E9=BB=84=E7=BB=BF=E5=88=A4=E5=AE=9A/=E5=8D=95?= =?UTF-8?q?=E4=BD=8D/=E6=95=B0=E6=8D=AE=E6=89=B9=E6=AC=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端: get_kpi的values附带target_value/deviation_pct/level/score/unit 判定对齐工作台语义: 正向≥0.9绿/≥0.7黄/否则红; 反向≤目标绿/≤1.1倍黄/否则红 - 前端: 历史数据表格新增目标值/偏差/判定列, 实际值带单位+红黄着色, 新增数据批次列 - 验证: 渠补率2月84%=黄(超5%), 5月88.3%=红(超10.4%), 收入全部red(远低于1200万目标) --- backend/app/api/kpis.py | 41 ++++++++++++++++++++++++++++---- frontend/src/views/KPIDetail.vue | 37 ++++++++++++++++++++++++---- 2 files changed, 69 insertions(+), 9 deletions(-) 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')