From e89629228e2748b89841030da0cae0fc06e5e0e2 Mon Sep 17 00:00:00 2001 From: fullstack-bot Date: Mon, 21 Sep 2026 16:07:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(cma):=20=E7=BC=BA=E5=A4=B1=E5=80=BC=20NULL?= =?UTF-8?q?=20=E8=AF=AD=E4=B9=89=E2=80=94=E2=80=94=E5=90=8C=E7=8E=AF?= =?UTF-8?q?=E6=AF=94/=E6=89=A7=E8=A1=8C=E7=8E=87/=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E4=B8=8D=E6=8A=8A=E7=BC=BA=E5=A4=B1=E5=BD=93=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 财务裁定 2026-09-21(博海 2026-08 批次 §2):前端对缺失值须显式 NULL 语义, 同环比不得按 0 参与。 - ProfitBreakdown: revChg/profitChg/rateChg 上期缺失→null(原 return 0 显示为持平 0.0%), 去掉 prev || 0.01 假分母;模板 null 显示「缺上期数据」 - MyDashboard: 进度条 actual 缺失不再显示 0%,改显 — - DeviationDashboard: 趋势图 actual/budget 保留 null,barHeight 对 null 不画柱 - BudgetManagement: 执行率/偏差额 actual 缺失→null(表格显示 -),平均执行率只对非空取平均 --- .../src/components/charts/ProfitBreakdown.vue | 26 ++++++++++++------- frontend/src/views/BudgetManagement.vue | 4 +-- frontend/src/views/DeviationDashboard.vue | 11 ++++---- frontend/src/views/MyDashboard.vue | 5 +++- 4 files changed, 28 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/charts/ProfitBreakdown.vue b/frontend/src/components/charts/ProfitBreakdown.vue index 1ddb8e46..90ddb6eb 100644 --- a/frontend/src/components/charts/ProfitBreakdown.vue +++ b/frontend/src/components/charts/ProfitBreakdown.vue @@ -61,21 +61,24 @@
营收较上期 - + {{ revChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(revChg) * 100).toFixed(1) }}% + 缺上期数据
净利润较上期 - + {{ profitChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(profitChg) * 100).toFixed(1) }}% + 缺上期数据
净利率较上期 - + {{ rateChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(rateChg) * 100).toFixed(1) }}% + 缺上期数据
@@ -129,19 +132,22 @@ const costRate = computed(() => { return (props.cost / props.revenue * 100).toFixed(1) + '%' }) -const revChg = computed(() => { - if (!props.prevRevenue) return 0 +// 缺失值 NULL 语义(财务裁定 2026-09-21 §2):上期缺失/为 0 时返回 null, +// 不得回落 0(0 会被显示成「持平」,把「无数据」当真实 0 参与同环比)。 +const revChg = computed(() => { + if (props.prevRevenue == null || props.prevRevenue === 0) return null return (props.revenue - props.prevRevenue) / props.prevRevenue }) -const profitChg = computed(() => { - if (!props.prevProfit) return 0 +const profitChg = computed(() => { + if (props.prevProfit == null || props.prevProfit === 0) return null return (props.profit - props.prevProfit) / Math.abs(props.prevProfit) }) -const rateChg = computed(() => { - if (!props.prevRevenue || !props.prevProfit) return 0 +const rateChg = computed(() => { + if (props.prevRevenue == null || props.prevProfit == null || props.prevRevenue === 0) return null const cur = props.profit / props.revenue const prev = props.prevProfit / props.prevRevenue - return (cur - prev) / Math.abs(prev || 0.01) + if (prev === 0) return null + return (cur - prev) / Math.abs(prev) }) const detailItems = computed(() => { diff --git a/frontend/src/views/BudgetManagement.vue b/frontend/src/views/BudgetManagement.vue index ef759609..50c5fc92 100644 --- a/frontend/src/views/BudgetManagement.vue +++ b/frontend/src/views/BudgetManagement.vue @@ -200,7 +200,7 @@ - + @@ -1094,7 +1094,7 @@ async function loadVersionDiff() { if (selectedVersions.value.length !== 2) retu const execFilterYear = ref(currentYear); const execFilterMonth = ref(new Date().getMonth()); const execFilterDim = ref(''); const execLoading = ref(false); const execReport = ref([]); const execSummaryCards = ref([]) async function loadExecutionReport() { execLoading.value = true - try { const r: any = await budgetApi.deviationReport({ year: execFilterYear.value, month: execFilterMonth.value, dimension: execFilterDim.value || undefined, hierarchical: true, entity_id: getEntityId() }); const items: any[] = r.items || []; const en = items.map((i: any) => ({ ...i, execution_rate: i.budget_value && i.budget_value > 0 ? Math.round((i.actual_value || 0) / i.budget_value * 100) : 0, deviation_value: (i.actual_value || 0) - (i.budget_value || 0) })); execReport.value = en; const hb = en.filter((i: any) => i.budget_value != null); const ob = en.filter((i: any) => i.is_over_budget); const ar = hb.length > 0 ? Math.round(hb.reduce((s, i) => s + (i.execution_rate || 0), 0) / hb.length) : 0; const tb = hb.reduce((s, i) => s + (i.budget_value || 0), 0); const ta = hb.reduce((s, i) => s + (i.actual_value || 0), 0); execSummaryCards.value = [{ label: '有预算KPI', value: `${hb.length}/${en.length}`, color: '#409eff' }, { label: '超预算KPI', value: ob.length.toString(), color: '#f56c6c' }, { label: '平均执行率', value: `${ar}%`, color: ar > 100 ? '#f56c6c' : '#67c23a' }, { label: '预算执行进度', value: tb > 0 ? `${Math.round(ta / tb * 100)}%` : '-', color: '#e6a23c' }] } catch (e) { ElMessage.error('加载执行报告失败') } + try { const r: any = await budgetApi.deviationReport({ year: execFilterYear.value, month: execFilterMonth.value, dimension: execFilterDim.value || undefined, hierarchical: true, entity_id: getEntityId() }); const items: any[] = r.items || []; const en = items.map((i: any) => ({ ...i, execution_rate: (i.budget_value && i.budget_value > 0 && i.actual_value != null) ? Math.round(i.actual_value / i.budget_value * 100) : null, deviation_value: i.actual_value != null ? i.actual_value - (i.budget_value ?? 0) : null })); execReport.value = en; const hb = en.filter((i: any) => i.budget_value != null); const ob = en.filter((i: any) => i.is_over_budget); const hbR = hb.filter((i: any) => i.execution_rate != null); const ar = hbR.length > 0 ? Math.round(hbR.reduce((s: number, i: any) => s + i.execution_rate, 0) / hbR.length) : null; const tb = hb.reduce((s, i) => s + (i.budget_value || 0), 0); const ta = hb.reduce((s, i) => s + (i.actual_value || 0), 0); execSummaryCards.value = [{ label: '有预算KPI', value: `${hb.length}/${en.length}`, color: '#409eff' }, { label: '超预算KPI', value: ob.length.toString(), color: '#f56c6c' }, { label: '平均执行率', value: ar == null ? '-' : `${ar}%`, color: (ar ?? 0) > 100 ? '#f56c6c' : '#67c23a' }, { label: '预算执行进度', value: tb > 0 ? `${Math.round(ta / tb * 100)}%` : '-', color: '#e6a23c' }] } catch (e) { ElMessage.error('加载执行报告失败') } execLoading.value = false } diff --git a/frontend/src/views/DeviationDashboard.vue b/frontend/src/views/DeviationDashboard.vue index 9ecd7d02..b87b18c2 100644 --- a/frontend/src/views/DeviationDashboard.vue +++ b/frontend/src/views/DeviationDashboard.vue @@ -342,19 +342,20 @@ async function loadTrend() { const keyB = b.period || '' return keyA.localeCompare(keyB) }) + // 缺失值 NULL 语义(财务裁定 2026-09-21 §2):actual/budget 缺失保留 null(不回落 0) trendData.value = monthly.map((i: any) => ({ period: i.period, - actual: i.actual_value || 0, - budget: i.budget_value || 0, + actual: i.actual_value ?? null, + budget: i.budget_value ?? null, deviation: i.deviation_rate, })) - maxTrend.value = Math.max(...trendData.value.flatMap((d: any) => [d.actual, d.budget]), 1) + maxTrend.value = Math.max(...trendData.value.flatMap((d: any) => [d.actual ?? 0, d.budget ?? 0]), 1) } catch (e) { /* ignore */ } trendLoading.value = false } -function barHeight(val: number, max: number) { - if (!max) return 0 +function barHeight(val: number | null, max: number) { + if (!max || val == null) return 0 // 缺失值 NULL 语义:不画柱,不以 0 参与 return Math.max(4, (val / max) * 150) } diff --git a/frontend/src/views/MyDashboard.vue b/frontend/src/views/MyDashboard.vue index c52556f7..2bd33200 100644 --- a/frontend/src/views/MyDashboard.vue +++ b/frontend/src/views/MyDashboard.vue @@ -78,12 +78,15 @@ 非当期{{ targetLabel(k) }} {{ k.target_value ?? '-' }} + +
—