Files
cma-management/frontend/src/views/PredictDashboard.vue.bak2
T

288 lines
18 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<h3>预测模拟</h3>
<el-tabs v-model="activeTab" style="margin-top:16px;">
<!-- Tab 1: CVP本量利分析 -->
<el-tab-pane label="本量利分析" name="cvp">
<el-row :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入参数</template>
<el-form :model="cvpForm" label-width="140px">
<el-form-item label="产品单价"><el-input-number v-model="cvpForm.unit_price" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="单位变动成本"><el-input-number v-model="cvpForm.unit_variable_cost" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="固定成本"><el-input-number v-model="cvpForm.fixed_cost" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="实际销量(可选)"><el-input-number v-model="cvpForm.actual_volume" :min="0" style="width:200px;" /></el-form-item>
<el-form-item label="目标利润(可选)"><el-input-number v-model="cvpForm.target_profit" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="doCvp" :loading="cvpLoading">计算</el-button>
<el-button @click="cvpForm = { unit_price: 100, unit_variable_cost: 60, fixed_cost: 50000, actual_volume: 2000, target_profit: 20000 }">填充示例</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="cvpResult.bep_units">
<template #header>分析结果</template>
<el-row :gutter="12">
<el-col :span="8"><div class="result-card"><div class="r-label">保本销量</div><div class="r-value">{{ cvpResult.bep_units }} 件</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">保本销售额</div><div class="r-value">{{ formatMoney(cvpResult.bep_revenue) }}</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">边际贡献率</div><div class="r-value blue">{{ cvpResult.contribution_ratio }}%</div></div></el-col>
</el-row>
<el-row :gutter="12" style="margin-top:12px;">
<el-col :span="8" v-if="cvpResult.safety_margin_units !== undefined"><div class="result-card"><div class="r-label">安全边际(量)</div><div class="r-value green">{{ cvpResult.safety_margin_units }} 件</div></div></el-col>
<el-col :span="8" v-if="cvpResult.safety_margin_ratio !== undefined"><div class="result-card"><div class="r-label">安全边际率</div><div class="r-value" :class="safetyColor(cvpResult.safety_margin_ratio)">{{ cvpResult.safety_margin_ratio }}%</div></div></el-col>
<el-col :span="8" v-if="cvpResult.actual_profit !== undefined"><div class="result-card"><div class="r-label">当前利润</div><div class="r-value orange">{{ formatMoney(cvpResult.actual_profit) }}</div></div></el-col>
</el-row>
<el-row :gutter="12" style="margin-top:12px;" v-if="cvpResult.target_units">
<el-col :span="12"><div class="result-card"><div class="r-label">目标利润所需销量</div><div class="r-value purple">{{ cvpResult.target_units }} 件</div></div></el-col>
<el-col :span="12"><div class="result-card"><div class="r-label">目标利润所需收入</div><div class="r-value purple">{{ formatMoney(cvpResult.target_revenue) }}</div></div></el-col>
</el-row>
</el-card>
<el-empty v-else-if="!cvpLoading" description="输入参数后点击「计算」" style="padding:40px 0;" />
</el-col>
</el-row>
</el-tab-pane>
<!-- Tab 2: 投资决策 -->
<el-tab-pane label="投资决策" name="investment">
<el-row :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入参数</template>
<el-form :model="invForm" label-width="140px">
<el-form-item label="初始投资"><el-input-number v-model="invForm.initial_investment" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="折现率(%)"><el-input-number v-model="invForm.discount_rate" :min="0" :max="100" :precision="1" style="width:200px;" /></el-form-item>
<el-form-item label="现金流(每年)">
<div v-for="(cf, idx) in invForm.cash_flows" :key="idx" style="display:flex;gap:8px;margin-bottom:4px;">
<span style="line-height:32px;width:80px;">第{{ idx+1 }}年</span>
<el-input-number v-model="invForm.cash_flows[idx]" :min="0" :precision="2" style="width:150px;" />
<el-button v-if="invForm.cash_flows.length > 1" size="small" type="danger" @click="invForm.cash_flows.splice(idx, 1)">×</el-button>
</div>
<el-button size="small" @click="invForm.cash_flows.push(0)">+ 添加年份</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="doInvestment" :loading="invLoading">计算</el-button>
<el-button @click="fillInvExample">填充示例</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="invResult.npv_analysis">
<template #header>分析结果</template>
<el-row :gutter="12">
<el-col :span="8"><div class="result-card"><div class="r-label">净现值 (NPV)</div><div class="r-value" :class="invResult.npv_analysis.npv > 0 ? 'green' : 'red'">{{ formatMoney(invResult.npv_analysis.npv) }}</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">内部收益率 (IRR)</div><div class="r-value blue">{{ invResult.irr_analysis.irr }}%</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">盈利指数 (PI)</div><div class="r-value purple">{{ invResult.npv_analysis.profitability_index }}</div></div></el-col>
</el-row>
<el-row :gutter="12" style="margin-top:12px;">
<el-col :span="8"><div class="result-card"><div class="r-label">静态回收期</div><div class="r-value">{{ invResult.irr_analysis.payback_period || '--' }} 年</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">动态回收期</div><div class="r-value">{{ invResult.irr_analysis.discounted_payback_period || '>N期' }} 年</div></div></el-col>
<el-col :span="8"><div class="result-card"><div class="r-label">可行性</div><div class="r-value" :class="invResult.npv_analysis.is_viable ? 'green' : 'red'">{{ invResult.npv_analysis.is_viable ? '✅ 可行' : '❌ 不可行' }}</div></div></el-col>
</el-row>
</el-card>
<el-empty v-else-if="!invLoading" description="输入参数后点击「计算」" style="padding:40px 0;" />
</el-col>
</el-row>
</el-tab-pane>
<!-- Tab 3: 敏感性分析 -->
<el-tab-pane label="敏感性分析" name="sensitivity">
<el-row :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入参数</template>
<el-form :model="sensForm" label-width="140px">
<el-form-item label="基准收入"><el-input-number v-model="sensForm.base_revenue" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="基准成本"><el-input-number v-model="sensForm.base_cost" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="变动步长(%)"><el-input-number v-model="sensForm.step" :min="1" :max="50" style="width:200px;" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="doSensitivity" :loading="sensLoading">计算</el-button>
<el-button @click="sensForm = { base_revenue: 1000000, base_cost: 700000, step: 5 }">填充示例</el-button>
</el-form-item>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="sensResult.base_revenue">
<template #header>
敏感性分析结果 — 基准利润:{{ formatMoney(sensResult.base_profit) }}
</template>
<el-table :data="sensResult.factors" border stripe size="small" style="width:100%;" max-height="400">
<el-table-column prop="change_pct" label="变动%" width="70" align="center">
<template #default="{ row }"><span :style="{ color: row.change_pct > 0 ? '#f56c6c' : '#67c23a', fontWeight:600 }">{{ row.change_pct > 0 ? '+' : '' }}{{ row.change_pct }}%</span></template>
</el-table-column>
<el-table-column prop="revenue_change_profit" label="收入变动→利润" width="130" align="right"><template #default="{ row }">{{ formatMoney(row.revenue_change_profit) }}</template></el-table-column>
<el-table-column prop="revenue_sensitivity" label="收入敏感度" width="100" align="right"><template #default="{ row }"><span :style="{ color: row.revenue_sensitivity < 0 ? '#f56c6c' : '#67c23a' }">{{ row.revenue_sensitivity > 0 ? '+' : '' }}{{ row.revenue_sensitivity }}%</span></template></el-table-column>
<el-table-column prop="cost_change_profit" label="成本变动→利润" width="130" align="right"><template #default="{ row }">{{ formatMoney(row.cost_change_profit) }}</template></el-table-column>
<el-table-column prop="cost_sensitivity" label="成本敏感度" width="100" align="right"><template #default="{ row }"><span :style="{ color: row.cost_sensitivity < 0 ? '#f56c6c' : '#67c23a' }">{{ row.cost_sensitivity > 0 ? '+' : '' }}{{ row.cost_sensitivity }}%</span></template></el-table-column>
</el-table>
</el-card>
<el-empty v-else-if="!sensLoading" description="输入参数后点击「计算」" style="padding:40px 0;" />
</el-col>
</el-row>
</el-tab-pane>
<!-- Tab 4: 情景模拟 -->
<el-tab-pane label="情景模拟" name="scenario">
<el-row :gutter="16">
<el-col :span="10">
<el-card>
<template #header>输入三个情景</template>
<el-form label-width="120px">
<div style="margin-bottom:12px;">
<el-tag type="success" size="small">乐观情景</el-tag>
<div style="display:flex;gap:8px;margin-top:4px;">
<el-input-number v-model="scenarioForm.optimistic.revenue" placeholder="收入" :min="0" :precision="2" style="width:150px;" />
<el-input-number v-model="scenarioForm.optimistic.cost" placeholder="成本" :min="0" :precision="2" style="width:150px;" />
</div>
</div>
<div style="margin-bottom:12px;">
<el-tag type="warning" size="small">中性情景</el-tag>
<div style="display:flex;gap:8px;margin-top:4px;">
<el-input-number v-model="scenarioForm.base.revenue" :min="0" :precision="2" style="width:150px;" />
<el-input-number v-model="scenarioForm.base.cost" :min="0" :precision="2" style="width:150px;" />
</div>
</div>
<div style="margin-bottom:12px;">
<el-tag type="danger" size="small">悲观情景</el-tag>
<div style="display:flex;gap:8px;margin-top:4px;">
<el-input-number v-model="scenarioForm.pessimistic.revenue" :min="0" :precision="2" style="width:150px;" />
<el-input-number v-model="scenarioForm.pessimistic.cost" :min="0" :precision="2" style="width:150px;" />
</div>
</div>
<el-button type="primary" @click="doScenario" :loading="scenarioLoading">模拟</el-button>
<el-button @click="fillScenarioExample">填充示例</el-button>
</el-form>
</el-card>
</el-col>
<el-col :span="14">
<el-card v-if="scenarioResult.scenarios">
<template #header>
模拟结果
<span style="margin-left:12px;font-size:12px;color:#999;">
期望利润:{{ formatMoney(scenarioResult.expected_profit) }}
| 标准差:{{ formatMoney(scenarioResult.std_deviation) }}
</span>
</template>
<el-table :data="scenarioResult.scenarios" border stripe size="small" style="width:100%;">
<el-table-column prop="scenario" label="情景" width="80">
<template #default="{ row }">
<el-tag :type="row.scenario === '乐观' ? 'success' : row.scenario === '中性' ? 'warning' : 'danger'" size="small">{{ row.scenario }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="revenue" label="收入" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.revenue) }}</template></el-table-column>
<el-table-column prop="cost" label="成本" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.cost) }}</template></el-table-column>
<el-table-column prop="profit" label="利润" width="100" align="right">
<template #default="{ row }"><span :style="{ color: row.profit >= 0 ? '#67c23a' : '#f56c6c', fontWeight:600 }">{{ formatMoney(row.profit) }}</span></template>
</el-table-column>
<el-table-column prop="profit_margin" label="利润率" width="80" align="right"><template #default="{ row }">{{ row.profit_margin }}%</template></el-table-column>
<el-table-column prop="deviation_from_base" label="偏差" width="100" align="right">
<template #default="{ row }"><span :style="{ color: row.deviation_from_base > 0 ? '#67c23a' : '#f56c6c' }">{{ row.deviation_from_base > 0 ? '+' : '' }}{{ formatMoney(row.deviation_from_base) }}</span></template>
</el-table-column>
<el-table-column prop="deviation_pct" label="偏差%" width="80" align="right"><template #default="{ row }">{{ row.deviation_pct }}%</template></el-table-column>
</el-table>
</el-card>
<el-empty v-else-if="!scenarioLoading" description="输入三个情景后点击「模拟」" style="padding:40px 0;" />
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { predictApi } from '../api/index'
const activeTab = ref('cvp')
function formatMoney(v: any) {
if (v === null || v === undefined) return '--'
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
function safetyColor(r: number) { return r > 25 ? 'green' : r > 10 ? 'orange' : 'red' }
// ── CVP ──
const cvpLoading = ref(false)
const cvpForm = ref({ unit_price: 100, unit_variable_cost: 60, fixed_cost: 50000, actual_volume: 2000, target_profit: 20000 })
const cvpResult = ref<any>({})
async function doCvp() {
cvpLoading.value = true
try {
cvpResult.value = await predictApi.cvp(cvpForm.value) as any
} catch { ElMessage.error('CVP计算失败') }
cvpLoading.value = false
}
// ── 投资决策 ──
const invLoading = ref(false)
const invForm = ref({ initial_investment: 100000, discount_rate: 10, cash_flows: [30000, 40000, 50000, 40000] })
const invResult = ref<any>({})
function fillInvExample() {
invForm.value = { initial_investment: 100000, discount_rate: 10, cash_flows: [30000, 40000, 50000, 40000] }
}
async function doInvestment() {
invLoading.value = true
try {
invResult.value = await predictApi.investment(invForm.value) as any
} catch { ElMessage.error('投资决策计算失败') }
invLoading.value = false
}
// ── 敏感性 ──
const sensLoading = ref(false)
const sensForm = ref({ base_revenue: 1000000, base_cost: 700000, step: 5 })
const sensResult = ref<any>({})
async function doSensitivity() {
sensLoading.value = true
try {
sensResult.value = await predictApi.sensitivity(sensForm.value) as any
} catch { ElMessage.error('敏感性分析失败') }
sensLoading.value = false
}
// ── 情景 ──
const scenarioLoading = ref(false)
const scenarioForm = ref({
optimistic: { revenue: 130, cost: 90 },
base: { revenue: 100, cost: 100 },
pessimistic: { revenue: 80, cost: 110 },
})
const scenarioResult = ref<any>({})
function fillScenarioExample() {
scenarioForm.value = {
optimistic: { revenue: 130, cost: 90 },
base: { revenue: 100, cost: 100 },
pessimistic: { revenue: 80, cost: 110 },
}
}
async function doScenario() {
scenarioLoading.value = true
try {
scenarioResult.value = await predictApi.scenario(scenarioForm.value) as any
} catch { ElMessage.error('情景模拟失败') }
scenarioLoading.value = false
}
</script>
<style scoped>
.result-card { background: #f5f7fa; border-radius: 8px; padding: 16px; text-align: center; }
.r-label { font-size: 12px; color: #909399; margin-bottom: 6px; }
.r-value { font-size: 22px; font-weight: bold; color: #303133; }
.r-value.green { color: #67c23a; }
.r-value.blue { color: #409eff; }
.r-value.orange { color: #e6a23c; }
.r-value.red { color: #f56c6c; }
.r-value.purple { color: #8b5cf6; }
</style>