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 @@
{{ formatNumber(row.actual_value, row.unit) }}
{{ row.deviation_value > 0 ? '+' : '' }}{{ formatNumber(row.deviation_value, row.unit) }}
-
+ -
严重关注正常
@@ -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 ?? '-' }}
+
+ —