feat: 预测性成本智能·宏观敏感性因素联动(IMA 2026.7完整版)
- 内置3宏观因素: 原油价格/美元汇率/CPI通胀率 - 敏感性引擎: 按KPI类别推断弹性(成本类油价0.15/利润类0.12/营收类0.08), 方向+因素涨KPI涨 - 负值KPI(亏损)方向反转修复: 油价涨→净利更亏 - API: GET /predict/kpi-forecast/sensitivity?pct=10 → KPI×因素矩阵(±pct调整后预测) - 前端: 敏感性幅度选择(±5/10/20%) + 敏感性矩阵表(同向/反向+↑↓调整值) - 诚实标注: 模型弹性(规则推断,非历史回归), 后续可用宏观历史数据回归校准 - 验证: Chrome实测页面+API矩阵, pytest 46 passed
This commit is contained in:
@@ -15,7 +15,13 @@
|
||||
<el-option label="6期" :value="6" />
|
||||
<el-option label="12期" :value="12" />
|
||||
</el-select>
|
||||
<el-button type="primary" :loading="loading" style="margin-left: 16px;" @click="loadData">刷新预测</el-button>
|
||||
<span class="ctrl-label" style="margin-left: 16px;">敏感性幅度</span>
|
||||
<el-select v-model="sensPct" style="width: 90px" @change="loadSensitivity">
|
||||
<el-option label="±5%" :value="5" />
|
||||
<el-option label="±10%" :value="10" />
|
||||
<el-option label="±20%" :value="20" />
|
||||
</el-select>
|
||||
<el-button type="primary" :loading="loading" style="margin-left: 16px;" @click="loadAll">刷新预测</el-button>
|
||||
</div>
|
||||
<div class="ctrl-right">
|
||||
<span class="mapping-hint">基于 kpi_values 历史数据 · 线性回归/移动平均 · 置信度诚实标注</span>
|
||||
@@ -92,6 +98,39 @@
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 宏观敏感性联动(IMA 2026.7) -->
|
||||
<el-card shadow="never" style="margin-top: 12px;">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>🌐 宏观敏感性因素联动</span>
|
||||
<el-tag size="small" type="warning">模型弹性(规则推断,非历史回归)</el-tag>
|
||||
<span class="empty-hint">因素变动 {{ sensPct }}% 对预测值的影响</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="sensList" size="small" border stripe max-height="360">
|
||||
<el-table-column prop="kpi.name" label="KPI" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column label="类别" width="70" align="center">
|
||||
<template #default="{ row }">{{ catLabel(row.category) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="基准预测" width="100" align="right">
|
||||
<template #default="{ row }">{{ fmtVal(row.next_target) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-for="f in sensFactors" :key="f.key" :label="f.name" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="factorOf(row, f.key)">
|
||||
<span :style="{ color: factorOf(row, f.key).direction === '+' ? '#F56C6C' : '#67C23A' }">
|
||||
{{ factorOf(row, f.key).direction === '+' ? '同向' : '反向' }}
|
||||
</span>
|
||||
<span class="sens-val">↑{{ fmtVal(factorOf(row, f.key).adj_up) }} ↓{{ fmtVal(factorOf(row, f.key).adj_down) }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="note-line" style="margin-top:8px;">
|
||||
弹性系数按KPI类别推断(成本类对油价最敏感0.15、利润类0.12、营收类0.08),MVP规则模型,后续可用宏观历史数据回归校准。
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 预测说明 -->
|
||||
<el-card shadow="never" style="margin-top: 12px;">
|
||||
<div class="note-line">
|
||||
@@ -114,6 +153,18 @@ const selected = ref<any>(null)
|
||||
const chartRef = ref<HTMLElement | null>(null)
|
||||
let chart: any = null
|
||||
|
||||
// ── 宏观敏感性联动 ──
|
||||
const sensPct = ref(10)
|
||||
const sensList = ref<any[]>([])
|
||||
const sensFactors = ref<any[]>([])
|
||||
|
||||
function catLabel(c: string) {
|
||||
return { profit: '利润', revenue: '营收', cost: '成本', cash: '现金流' }[c] || c
|
||||
}
|
||||
function factorOf(row: any, key: string) {
|
||||
return (row.factors || []).find((f: any) => f.factor_key === key)
|
||||
}
|
||||
|
||||
function lastHistory(row: any) {
|
||||
const h = row.history || []
|
||||
return h.length ? h[h.length - 1].value : null
|
||||
@@ -164,6 +215,20 @@ async function loadData() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSensitivity() {
|
||||
try {
|
||||
const r: any = await predictApi.kpiForecastSensitivity({ pct: sensPct.value, periods: periods.value, model: model.value })
|
||||
sensList.value = r.data || []
|
||||
sensFactors.value = r.factors || []
|
||||
} catch (e: any) {
|
||||
console.error('敏感性加载失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
await Promise.all([loadData(), loadSensitivity()])
|
||||
}
|
||||
|
||||
function onSelect(row: any) {
|
||||
selected.value = row
|
||||
renderChart()
|
||||
@@ -207,7 +272,7 @@ function resizeChart() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAll()
|
||||
window.addEventListener('resize', resizeChart)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
@@ -267,6 +332,11 @@ onBeforeUnmount(() => {
|
||||
font-size: 12px;
|
||||
color: #C0C4CC;
|
||||
}
|
||||
.sens-val {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.trend-chart {
|
||||
height: 520px;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user