288 lines
18 KiB
Plaintext
288 lines
18 KiB
Plaintext
<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>
|