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 ?? '-' }} + +
—