feat: CMA P1+P2 Round1 — 成本法对比+杜邦分析+本量利

This commit is contained in:
Hermes CI Fix
2026-07-16 16:39:24 +08:00
parent 46fa811d52
commit 4b25f46124
8728 changed files with 1772671 additions and 151 deletions
+153 -1
View File
@@ -207,6 +207,76 @@
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<!-- Tab 6: 成本法对比(CMA P1) -->
<el-tab-pane label="成本法对比" name="comparison">
<div v-loading="compLoading">
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center;">
<span style="font-weight:600;font-size:14px;">分析对象:</span>
<el-radio-group v-model="compEntity" @change="loadComparison">
<el-radio value="hanke">陕西酣客(白酒经销)</el-radio>
<el-radio value="bohai">陕西博海科技(IT服务)</el-radio>
</el-radio-group>
<el-button type="primary" size="small" @click="loadComparison" :loading="compLoading">刷新</el-button>
</div>
<div v-if="compData.traditional" class="comp-table-wrapper">
<el-table :data="compRows" border stripe size="small" style="width:100%;">
<el-table-column prop="metric" label="指标" width="120" fixed>
<template #default="{ row }">
<span :style="{ fontWeight: row.bold ? 700 : 400 }">{{ row.metric }}</span>
</template>
</el-table-column>
<el-table-column label="传统成本法" width="160" align="center">
<template #header>
<div style="text-align:center;">传统成本法</div>
<div style="font-size:11px;color:#999;text-align:center;">(账面当前)</div>
</template>
<template #default="{ row }">
<span :style="{ color: row.t_color || '#333', fontWeight: row.t_bold ? 700 : 400 }">{{ row.traditional }}</span>
</template>
</el-table-column>
<el-table-column label="变动成本法" width="160" align="center">
<template #header>
<div style="text-align:center;">变动成本法</div>
<div style="font-size:11px;color:#999;text-align:center;">(Model C)</div>
</template>
<template #default="{ row }">
<span :style="{ color: row.v_color || '#333', fontWeight: row.v_bold ? 700 : 400 }">{{ row.variable }}</span>
</template>
</el-table-column>
<el-table-column label="作业成本法" width="160" align="center">
<template #header>
<div style="text-align:center;">作业成本法</div>
<div style="font-size:11px;color:#999;text-align:center;">(ABC)</div>
</template>
<template #default="{ row }">
<span :style="{ color: row.a_color || '#333', fontWeight: row.a_bold ? 700 : 400 }">{{ row.abc }}</span>
</template>
</el-table-column>
</el-table>
<!-- 注释 -->
<el-card style="margin-top:12px;background:#fafafa;" size="small" v-if="compNotes.length > 0">
<div v-for="(note, idx) in compNotes" :key="idx" style="font-size:12px;color:#666;padding:2px 0;">
{{ note }}
</div>
</el-card>
<!-- 决策启示 -->
<el-card style="margin-top:12px;border-left:4px solid #409eff;">
<template #header><span style="font-weight:600;">📋 决策启示</span></template>
<div v-for="insight in compInsights" :key="insight.method" class="insight-row">
<div class="insight-method">{{ insight.method }}</div>
<div class="insight-conclusion">{{ insight.conclusion }}</div>
<div class="insight-decision">{{ insight.decision }}</div>
<div class="insight-detail">{{ insight.detail }}</div>
</div>
</el-card>
</div>
<el-empty v-else-if="!compLoading" description="选择实体后点击「刷新」加载数据" />
</div>
</el-tab-pane>
</el-tabs>
<!-- 标准成本表单弹窗 -->
@@ -283,7 +353,7 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { costApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
@@ -506,6 +576,88 @@ async function doAllocate() {
allocSaving.value = false
}
// ============================
// 成本法对比(CMA P1)
// ============================
const compLoading = ref(false)
const compEntity = ref('hanke')
const compData = ref<any>({})
const compRows = computed(() => {
if (!compData.value.traditional) return []
const d = compData.value
const fmt = (v: any) => {
if (v === null || v === undefined) return '--'
const n = Number(v)
if (n >= 10000) return (n / 10000).toFixed(1) + '亿'
return n.toFixed(1) + '万'
}
const pct = (v: any) => v !== null && v !== undefined ? v.toFixed(2) + '%' : '--'
return [
{
metric: '收入', bold: true,
traditional: fmt(d.traditional.revenue),
variable: fmt(d.variable.revenue),
abc: fmt(d.abc.revenue),
},
{
metric: '成本', bold: true,
traditional: fmt(d.traditional.cost),
variable: fmt(d.variable.cost),
abc: fmt(d.abc.cost),
},
{
metric: '毛利', bold: true,
traditional: fmt(d.traditional.gross_profit),
variable: fmt(d.variable.gross_profit),
abc: fmt(d.abc.gross_profit),
t_color: d.traditional.gross_profit < 0 ? '#f56c6c' : '#67c23a',
v_color: d.variable.gross_profit > 0 ? '#67c23a' : '#f56c6c',
a_color: d.abc.gross_profit > 0 ? '#67c23a' : '#f56c6c',
t_bold: true, v_bold: true, a_bold: true,
},
{
metric: '毛利率', bold: false,
traditional: pct(d.traditional.gross_margin),
variable: pct(d.variable.gross_margin),
abc: pct(d.abc.gross_margin),
t_color: d.traditional.gross_margin < 0 ? '#f56c6c' : '#67c23a',
v_color: d.variable.gross_margin > 10 ? '#67c23a' : '#e6a23c',
a_color: d.abc.gross_margin > 10 ? '#67c23a' : '#e6a23c',
},
{
metric: '费用', bold: true,
traditional: fmt(d.traditional.expenses),
variable: fmt(d.variable.expenses),
abc: fmt(d.abc.expenses),
},
{
metric: '净利润', bold: true,
traditional: fmt(d.traditional.net_profit),
variable: fmt(d.variable.net_profit),
abc: fmt(d.abc.net_profit),
t_color: d.traditional.net_profit < 0 ? '#f56c6c' : '#67c23a',
v_color: d.variable.net_profit < 0 ? '#f56c6c' : '#67c23a',
a_color: d.abc.net_profit < 0 ? '#f56c6c' : '#67c23a',
t_bold: true, v_bold: true, a_bold: true,
},
]
})
const compNotes = computed(() => {
const notes = compData.value.notes
if (!notes) return []
return Object.values(notes)
})
const compInsights = computed(() => compData.value.insights || [])
async function loadComparison() {
compLoading.value = true
try {
const r: any = await costApi.comparison({ entity: compEntity.value })
compData.value = r.data || r
} catch (e) { ElMessage.error('加载对比数据失败') }
compLoading.value = false
}
onMounted(() => {
loadOverview()
loadStandardCosts()