财务裁定 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(表格显示 -),平均执行率只对非空取平均
This commit is contained in:
@@ -61,21 +61,24 @@
|
||||
<div v-if="prevRevenue" class="pb-compare">
|
||||
<div class="pb-compare-item">
|
||||
<span class="pb-compare-label">营收较上期</span>
|
||||
<span :class="revChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
<span v-if="revChg != null" :class="revChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
{{ revChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(revChg) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
<span v-else style="color:#909399">缺上期数据</span>
|
||||
</div>
|
||||
<div class="pb-compare-item">
|
||||
<span class="pb-compare-label">净利润较上期</span>
|
||||
<span :class="profitChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
<span v-if="profitChg != null" :class="profitChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
{{ profitChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(profitChg) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
<span v-else style="color:#909399">缺上期数据</span>
|
||||
</div>
|
||||
<div class="pb-compare-item">
|
||||
<span class="pb-compare-label">净利率较上期</span>
|
||||
<span :class="rateChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
<span v-if="rateChg != null" :class="rateChg >= 0 ? 'pb-pos' : 'pb-neg'">
|
||||
{{ rateChg >= 0 ? '↑' : '↓' }} {{ (Math.abs(rateChg) * 100).toFixed(1) }}%
|
||||
</span>
|
||||
<span v-else style="color:#909399">缺上期数据</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<number | null>(() => {
|
||||
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<number | null>(() => {
|
||||
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<number | null>(() => {
|
||||
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(() => {
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
</template></el-table-column>
|
||||
<el-table-column prop="actual_value" label="实际值" width="130"><template #default="{ row }">{{ formatNumber(row.actual_value, row.unit) }}</template></el-table-column>
|
||||
<el-table-column prop="deviation_value" label="偏差" width="130"><template #default="{ row }"><span :style="{color: row.deviation_value > 0 ? '#f56c6c' : row.deviation_value < 0 ? '#67c23a' : '#999'}">{{ row.deviation_value > 0 ? '+' : '' }}{{ formatNumber(row.deviation_value, row.unit) }}</span></template></el-table-column>
|
||||
<el-table-column label="执行率" width="100"><template #default="{ row }"><el-progress :percentage="row.execution_rate || 0" :status="row.execution_rate > 100 ? 'exception' : row.execution_rate > 80 ? 'warning' : 'success'" :stroke-width="16" :text-inside="true" /></template></el-table-column>
|
||||
<el-table-column label="执行率" width="100"><template #default="{ row }"><el-progress v-if="row.execution_rate != null" :percentage="row.execution_rate" :status="row.execution_rate > 100 ? 'exception' : row.execution_rate > 80 ? 'warning' : 'success'" :stroke-width="16" :text-inside="true" /><span v-else style="color:#909399">-</span></template></el-table-column>
|
||||
<el-table-column label="预警" width="80"><template #default="{ row }"><el-tag v-if="row.alert_level === 'red'" size="small" type="danger">严重</el-tag><el-tag v-else-if="row.alert_level === 'yellow'" size="small" type="warning">关注</el-tag><el-tag v-else size="small" type="success">正常</el-tag></template></el-table-column>
|
||||
</el-table>
|
||||
|
||||
@@ -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<any[]>([]); const execSummaryCards = ref<any[]>([])
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -78,12 +78,15 @@
|
||||
<span v-if="isStale(k)" class="kpi-stale-tag">非当期</span>{{ targetLabel(k) }} {{ k.target_value ?? '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 缺失值 NULL 语义(财务裁定 2026-09-21 §2):actual 缺失时不得显示 0% 进度 -->
|
||||
<el-progress
|
||||
:percentage="k.target_value ? Math.min(100, Math.round((k.actual_value || 0) / k.target_value * 100)) : 0"
|
||||
v-if="k.actual_value != null && k.target_value"
|
||||
:percentage="Math.min(100, Math.round(k.actual_value / k.target_value * 100))"
|
||||
:status="k.level === 'green' ? 'success' : k.level === 'red' ? 'exception' : undefined"
|
||||
:stroke-width="6"
|
||||
:show-text="false"
|
||||
/>
|
||||
<div v-else style="height:6px;line-height:6px;color:#c0c4cc;font-size:11px">—</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user