Files
cma-management/frontend/src/views/CostDashboard.vue
T

680 lines
34 KiB
Vue
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: 成本总览 -->
<el-tab-pane label="成本总览" name="overview">
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap;align-items:center;">
<el-select v-model="overviewPeriod" placeholder="期间" style="width:110px;" @change="loadOverview">
<el-option v-for="p in periodOptions" :key="p" :label="p" :value="p" />
</el-select>
<el-button type="primary" @click="loadOverview">刷新</el-button>
</div>
<el-row :gutter="16" style="margin-bottom:16px;">
<el-col :span="6">
<el-card shadow="hover"><div class="stat-card"><div class="stat-label">总成本</div><div class="stat-value">{{ formatMoney(overviewData.total_cost || 0) }}</div></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-card"><div class="stat-label">ERP同步成本</div><div class="stat-value" style="color:#409eff;">{{ formatMoney(overviewData.erp_cost || 0) }}</div></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-card"><div class="stat-label">手工录入成本</div><div class="stat-value" style="color:#e6a23c;">{{ formatMoney(overviewData.manual_cost || 0) }}</div></div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover"><div class="stat-card"><div class="stat-label">产品数</div><div class="stat-value" style="color:#67c23a;">{{ (overviewData.products || []).length }}</div></div></el-card>
</el-col>
</el-row>
<!-- 成本结构 -->
<el-row :gutter="16">
<el-col :span="12">
<el-card>
<template #header>成本结构(按类型)</template>
<div v-if="(overviewData.structure || []).length > 0">
<div v-for="s in overviewData.structure" :key="s.cost_type" style="margin-bottom:12px;">
<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:4px;">
<span>{{ costTypeLabel(s.cost_type) }}</span>
<span>{{ formatMoney(s.amount) }}({{ s.ratio }}%)</span>
</div>
<el-progress :percentage="s.ratio" :stroke-width="14" :color="costTypeColor(s.cost_type)" />
</div>
</div>
<div v-else style="text-align:center;color:#999;padding:20px;">暂无成本数据,请先在「成本录入」中添加</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<template #header>各产品成本</template>
<div v-if="(overviewData.products || []).length > 0">
<div v-for="p in overviewData.products" :key="p.product_code" style="display:flex;justify-content:space-between;padding:6px 0;border-bottom:1px solid #f5f5f5;font-size:13px;">
<span>{{ p.product_name || p.product_code }}</span>
<span style="font-weight:500;">{{ formatMoney(p.total_cost) }}</span>
</div>
</div>
<div v-else style="text-align:center;color:#999;padding:20px;">暂无产品数据</div>
</el-card>
</el-col>
</el-row>
<!-- 成本趋势 -->
<el-card style="margin-top:16px;">
<template #header>成本趋势(近6个月)</template>
<div v-if="(overviewData.trend || []).length > 0" style="display:flex;align-items:flex-end;gap:12px;height:180px;padding:20px 0;overflow-x:auto;">
<div v-for="(t, idx) in overviewData.trend" :key="idx" style="display:flex;flex-direction:column;align-items:center;min-width:80px;">
<div class="trend-bar" :style="{ height: trendHeight(t.total_cost, maxTrend) + 'px' }" :title="formatMoney(t.total_cost)"></div>
<span style="font-size:11px;color:#909399;margin-top:6px;">{{ t.period }}</span>
<span style="font-size:10px;">{{ formatMoney(t.total_cost) }}</span>
</div>
</div>
<div v-else style="text-align:center;color:#999;padding:20px;">暂无趋势数据</div>
</el-card>
</el-tab-pane>
<!-- Tab 2: 标准成本管理 -->
<el-tab-pane label="标准成本" name="standard">
<div style="margin-bottom:12px;">
<el-button type="primary" @click="showStdForm = true; stdForm = {}">+ 新建标准成本</el-button>
<el-select v-model="stdProductFilter" placeholder="产品" clearable style="width:150px;margin-left:8px;" @change="loadStandardCosts">
<el-option v-for="p in productOptions" :key="p" :label="p" :value="p" />
</el-select>
</div>
<el-table :data="standardCosts" v-loading="stdLoading" border stripe size="small" style="width:100%;">
<el-table-column prop="product_code" label="产品编码" width="100" />
<el-table-column prop="product_name" label="产品名称" width="120" />
<el-table-column prop="cost_type" label="成本类型" width="90">
<template #default="{ row }"><el-tag size="small" :type="costTypeTag(row.cost_type)">{{ costTypeLabel(row.cost_type) }}</el-tag></template>
</el-table-column>
<el-table-column prop="item_name" label="项目名称" min-width="140" />
<el-table-column prop="standard_quantity" label="标准用量" width="90" align="right" />
<el-table-column prop="unit" label="单位" width="60" />
<el-table-column prop="standard_price" label="标准单价" width="90" align="right">
<template #default="{ row }">{{ formatMoney(row.standard_price) }}</template>
</el-table-column>
<el-table-column prop="standard_cost" label="标准成本" width="100" align="right">
<template #default="{ row }"><span style="font-weight:500;">{{ formatMoney(row.standard_cost) }}</span></template>
</el-table-column>
<el-table-column prop="version" label="版本" width="60" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="editStd(row)">编辑</el-button>
<el-button size="small" type="danger" @click="deleteStd(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<!-- Tab 3: 成本录入 -->
<el-tab-pane label="成本录入" name="entry">
<div style="margin-bottom:12px;">
<el-button type="primary" @click="showActualForm = true; actualForm = {}">+ 录入实际成本</el-button>
<el-select v-model="entryPeriod" placeholder="期间" style="width:110px;margin-left:8px;" @change="loadActualCosts">
<el-option v-for="p in periodOptions" :key="p" :label="p" :value="p" />
</el-select>
</div>
<el-table :data="actualCosts" v-loading="actualLoading" border stripe size="small" style="width:100%;">
<el-table-column prop="period" label="期间" width="80" />
<el-table-column prop="product_code" label="产品编码" width="90" />
<el-table-column prop="product_name" label="产品名称" width="120" />
<el-table-column prop="cost_type" label="成本类型" width="90">
<template #default="{ row }"><el-tag size="small" :type="costTypeTag(row.cost_type)">{{ costTypeLabel(row.cost_type) }}</el-tag></template>
</el-table-column>
<el-table-column prop="item_name" label="项目" min-width="120" />
<el-table-column prop="actual_quantity" label="实际用量" width="90" align="right" />
<el-table-column prop="actual_price" label="实际单价" width="90" align="right">
<template #default="{ row }">{{ formatMoney(row.actual_price) }}</template>
</el-table-column>
<el-table-column prop="actual_cost" label="实际成本" width="100" align="right">
<template #default="{ row }"><span style="font-weight:500;">{{ formatMoney(row.actual_cost) }}</span></template>
</el-table-column>
<el-table-column prop="source" label="来源" width="70" />
</el-table>
</el-tab-pane>
<!-- Tab 4: 量差价差分析 -->
<el-tab-pane label="量差价差" name="variance">
<div style="display:flex;gap:12px;margin-bottom:16px;align-items:center;">
<el-select v-model="varProduct" placeholder="选择产品" style="width:200px;">
<el-option v-for="p in productOptions" :key="p" :label="p" :value="p" />
</el-select>
<el-select v-model="varPeriod" placeholder="期间" style="width:110px;">
<el-option v-for="p in periodOptions" :key="p" :label="p" :value="p" />
</el-select>
<el-button type="primary" @click="loadVariance" :loading="varLoading">分析</el-button>
</div>
<div v-if="varianceData.summary">
<el-row :gutter="16" style="margin-bottom:16px;">
<el-col :span="6"><el-card shadow="hover"><div class="stat-card"><div class="stat-label">标准成本</div><div class="stat-value" style="color:#409eff;">{{ formatMoney(varianceData.summary.total_standard_cost) }}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat-card"><div class="stat-label">实际成本</div><div class="stat-value" style="color:#e6a23c;">{{ formatMoney(varianceData.summary.total_actual_cost) }}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat-card"><div class="stat-label">总差异</div><div class="stat-value" :style="{ color: varianceData.summary.total_variance > 0 ? '#f56c6c' : '#67c23a' }">{{ formatMoney(varianceData.summary.total_variance) }}</div></div></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><div class="stat-card"><div class="stat-label">差异率</div><div class="stat-value" :style="{ color: varianceData.summary.variance_rate > 0 ? '#f56c6c' : '#67c23a' }">{{ varianceData.summary.variance_rate }}%</div></div></el-card></el-col>
</el-row>
<el-table :data="varianceData.items" border stripe size="small" style="width:100%;">
<el-table-column prop="cost_type" label="类型" width="80">
<template #default="{ row }"><el-tag size="small" :type="costTypeTag(row.cost_type)">{{ costTypeLabel(row.cost_type) }}</el-tag></template>
</el-table-column>
<el-table-column prop="item_name" label="项目" width="140" />
<el-table-column prop="standard_cost" label="标准" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.standard_cost) }}</template></el-table-column>
<el-table-column prop="actual_cost" label="实际" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.actual_cost) }}</template></el-table-column>
<el-table-column prop="qty_variance" label="量差" width="100" align="right">
<template #default="{ row }"><span :style="{ color: row.qty_variance > 0 ? '#f56c6c' : '#67c23a' }">{{ formatMoney(row.qty_variance) }}</span></template>
</el-table-column>
<el-table-column prop="price_variance" label="价差" width="100" align="right">
<template #default="{ row }"><span :style="{ color: row.price_variance > 0 ? '#f56c6c' : '#67c23a' }">{{ formatMoney(row.price_variance) }}</span></template>
</el-table-column>
<el-table-column prop="total_variance" label="总差异" width="100" align="right">
<template #default="{ row }"><span :style="{ color: row.total_variance > 0 ? '#f56c6c' : '#67c23a', fontWeight:500 }">{{ formatMoney(row.total_variance) }}</span></template>
</el-table-column>
</el-table>
</div>
<el-empty v-else-if="!varLoading" description="请选择产品后点击「分析」" style="padding:40px 0;" />
</el-tab-pane>
<!-- Tab 5: ABC作业成本 -->
<el-tab-pane label="ABC作业成本" name="abc">
<el-tabs v-model="abcTab">
<el-tab-pane label="作业中心" name="activities">
<div style="margin-bottom:12px;"><el-button type="primary" @click="showAbcForm = true; abcForm = {}">+ 新建作业中心</el-button></div>
<el-table :data="abcActivities" v-loading="abcLoading" border stripe size="small" style="width:100%;">
<el-table-column prop="activity_code" label="编码" width="100" />
<el-table-column prop="activity_name" label="作业名称" width="160" />
<el-table-column prop="cost_driver" label="成本动因" width="120" />
<el-table-column prop="driver_unit" label="动因单位" width="70" />
<el-table-column prop="total_cost" label="总成本" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.total_cost) }}</template></el-table-column>
<el-table-column prop="driver_volume" label="动因总量" width="90" align="right" />
<el-table-column prop="driver_rate" label="分配率" width="90" align="right">{{ formatMoney(row.driver_rate) }}</el-table-column>
<el-table-column label="操作" width="80"><template #default="{ row }"><el-button size="small" @click="editAbc(row)">编辑</el-button></template></el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="成本分配" name="allocations">
<div style="margin-bottom:12px;display:flex;gap:8px;">
<el-button type="primary" @click="showAllocForm = true; allocForm = {}">+ 执行分配</el-button>
<el-select v-model="allocPeriod" placeholder="期间" style="width:110px;" @change="loadAllocations">
<el-option v-for="p in periodOptions" :key="p" :label="p" :value="p" />
</el-select>
</div>
<el-table :data="abcAllocations" v-loading="allocLoading" border stripe size="small" style="width:100%;">
<el-table-column prop="period" label="期间" width="80" />
<el-table-column prop="activity_id" label="作业ID" width="70" />
<el-table-column prop="product_code" label="产品" width="90" />
<el-table-column prop="product_name" label="产品名" width="120" />
<el-table-column prop="driver_consumed" label="动因消耗" width="90" align="right" />
<el-table-column prop="allocated_cost" label="分配成本" width="100" align="right"><template #default="{ row }">{{ formatMoney(row.allocated_cost) }}</template></el-table-column>
</el-table>
</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>
<!-- 标准成本表单弹窗 -->
<MyDialog v-model="showStdForm" :title="stdForm.id ? '编辑标准成本' : '新建标准成本'" :width="550">
<el-form :model="stdForm" label-width="110px">
<el-form-item label="产品编码"><el-input v-model="stdForm.product_code" /></el-form-item>
<el-form-item label="产品名称"><el-input v-model="stdForm.product_name" /></el-form-item>
<el-form-item label="成本类型">
<el-select v-model="stdForm.cost_type"><el-option label="材料" value="material" /><el-option label="人工" value="labor" /><el-option label="制造费用" value="overhead" /></el-select>
</el-form-item>
<el-form-item label="项目名称"><el-input v-model="stdForm.item_name" /></el-form-item>
<el-form-item label="标准用量"><el-input-number v-model="stdForm.standard_quantity" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="单位"><el-input v-model="stdForm.unit" style="width:150px;" /></el-form-item>
<el-form-item label="标准单价"><el-input-number v-model="stdForm.standard_price" :min="0" :precision="2" style="width:200px;" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showStdForm = false">取消</el-button>
<el-button type="primary" @click="saveStd" :loading="stdSaving">保存</el-button>
</template>
</MyDialog>
<!-- 实际成本录入弹窗 -->
<MyDialog v-model="showActualForm" title="录入实际成本" :width="550">
<el-form :model="actualForm" label-width="110px">
<el-form-item label="期间"><el-input v-model="actualForm.period" placeholder="2026-05" /></el-form-item>
<el-form-item label="产品编码"><el-input v-model="actualForm.product_code" /></el-form-item>
<el-form-item label="产品名称"><el-input v-model="actualForm.product_name" /></el-form-item>
<el-form-item label="成本类型"><el-select v-model="actualForm.cost_type"><el-option label="材料" value="material" /><el-option label="人工" value="labor" /><el-option label="制造费用" value="overhead" /></el-select></el-form-item>
<el-form-item label="项目名称"><el-input v-model="actualForm.item_name" /></el-form-item>
<el-form-item label="实际用量"><el-input-number v-model="actualForm.actual_quantity" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="实际单价"><el-input-number v-model="actualForm.actual_price" :min="0" :precision="2" style="width:200px;" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showActualForm = false">取消</el-button>
<el-button type="primary" @click="saveActual" :loading="actualSaving">保存</el-button>
</template>
</MyDialog>
<!-- ABC作业中心弹窗 -->
<MyDialog v-model="showAbcForm" :title="abcForm.id ? '编辑作业中心' : '新建作业中心'" :width="500">
<el-form :model="abcForm" label-width="110px">
<el-form-item label="作业编码"><el-input v-model="abcForm.activity_code" /></el-form-item>
<el-form-item label="作业名称"><el-input v-model="abcForm.activity_name" /></el-form-item>
<el-form-item label="成本动因"><el-input v-model="abcForm.cost_driver" /></el-form-item>
<el-form-item label="动因单位"><el-input v-model="abcForm.driver_unit" /></el-form-item>
<el-form-item label="总成本"><el-input-number v-model="abcForm.total_cost" :min="0" :precision="2" style="width:200px;" /></el-form-item>
<el-form-item label="动因总量"><el-input-number v-model="abcForm.driver_volume" :min="0" style="width:200px;" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAbcForm = false">取消</el-button>
<el-button type="primary" @click="saveAbc" :loading="abcSaving">保存</el-button>
</template>
</MyDialog>
<!-- ABC分配弹窗 -->
<MyDialog v-model="showAllocForm" title="执行ABC成本分配" :width="450">
<el-form :model="allocForm" label-width="110px">
<el-form-item label="作业中心">
<el-select v-model="allocForm.activity_id" style="width:250px;">
<el-option v-for="a in abcActivities" :key="a.id" :label="a.activity_name" :value="a.id" />
</el-select>
</el-form-item>
<el-form-item label="期间"><el-input v-model="allocForm.period" placeholder="2026-05" /></el-form-item>
<el-form-item label="产品编码"><el-input v-model="allocForm.product_code" /></el-form-item>
<el-form-item label="产品名称"><el-input v-model="allocForm.product_name" /></el-form-item>
<el-form-item label="动因消耗量"><el-input-number v-model="allocForm.driver_consumed" :min="0" :precision="2" style="width:200px;" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAllocForm = false">取消</el-button>
<el-button type="primary" @click="doAllocate" :loading="allocSaving">执行分配</el-button>
</template>
</MyDialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { costApi } from '../api/index'
import MyDialog from '../components/MyDialog.vue'
const activeTab = ref('overview')
const abcTab = ref('activities')
// ── 期间选项 ──
const now = new Date()
const periodOptions = computed(() => {
const opts: string[] = []
for (let i = 0; i < 12; i++) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1)
opts.push(`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`)
}
return opts
})
// ── 工具函数 ──
const costTypeMap: Record<string, string> = { material: '材料', labor: '人工', overhead: '制造费用' }
const costTypeTagMap: Record<string, string> = { material: 'primary', labor: 'success', overhead: 'warning' }
function costTypeLabel(t: string) { return costTypeMap[t] || t }
function costTypeTag(t: string) { return costTypeTagMap[t] || 'info' }
function formatMoney(v: any) {
if (v === null || v === undefined) return '--'
return Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
// ============================
// 成本总览
// ============================
const overviewPeriod = ref(periodOptions.value[0] || now.getFullYear() + '-' + String(now.getMonth()+1).padStart(2,'0'))
const overviewData = ref<any>({})
const productOptions = ref<string[]>([])
const maxTrend = ref(0)
function getEntityId() {
return Number(localStorage.getItem('cma_entity_id') || 1)
}
async function loadOverview() {
try {
const r: any = await costApi.dashboard({ period: overviewPeriod.value, entity_id: getEntityId() })
overviewData.value = r.data || r
// 提取产品列表
const products = overviewData.value.products || []
productOptions.value = [...new Set(products.map((p: any) => p.product_code))]
// 计算趋势最高值
const trend = overviewData.value.overview?.trend || []
if (trend.length > 0) {
maxTrend.value = Math.max(...trend.map((t: any) => t.total_cost), 1)
}
} catch (e) { ElMessage.error('加载成本总览失败') }
}
function trendHeight(val: number, max: number) {
if (!max) return 0
return Math.max(4, (val / max) * 140)
}
// ============================
// 标准成本管理
// ============================
const stdLoading = ref(false)
const standardCosts = ref<any[]>([])
const showStdForm = ref(false)
const stdForm = ref<any>({})
const stdSaving = ref(false)
const stdProductFilter = ref('')
async function loadStandardCosts() {
stdLoading.value = true
try {
const params: any = { entity_id: getEntityId() }
if (stdProductFilter.value) params.product_code = stdProductFilter.value
const r: any = await costApi.listStandardCosts(params)
standardCosts.value = r.data || []
} catch (e) { ElMessage.error('加载标准成本失败') }
stdLoading.value = false
}
function editStd(row: any) {
stdForm.value = { ...row }
showStdForm.value = true
}
async function saveStd() {
stdSaving.value = true
try {
if (stdForm.value.id) {
await costApi.updateStandardCost(stdForm.value.id, stdForm.value)
ElMessage.success('已更新')
} else {
await costApi.createStandardCost(stdForm.value)
ElMessage.success('已创建')
}
showStdForm.value = false
loadStandardCosts()
} catch (e) { ElMessage.error('保存失败') }
stdSaving.value = false
}
async function deleteStd(row: any) {
try {
await ElMessageBox.confirm(`确认删除「${row.item_name}」的标准成本?`, '确认')
await costApi.deleteStandardCost(row.id)
ElMessage.success('已删除')
loadStandardCosts()
} catch (e: any) { if (e !== 'cancel') ElMessage.error('删除失败') }
}
// ============================
// 成本录入
// ============================
const actualLoading = ref(false)
const actualCosts = ref<any[]>([])
const entryPeriod = ref(overviewPeriod.value)
const showActualForm = ref(false)
const actualForm = ref<any>({})
const actualSaving = ref(false)
async function loadActualCosts() {
actualLoading.value = true
try {
const params: any = { period: entryPeriod.value, entity_id: getEntityId() }
const r: any = await costApi.listActualCosts(params)
actualCosts.value = r.data || []
} catch (e) { ElMessage.error('加载实际成本失败') }
actualLoading.value = false
}
async function saveActual() {
actualSaving.value = true
try {
await costApi.createActualCost(actualForm.value)
ElMessage.success('已录入')
showActualForm.value = false
loadActualCosts()
} catch (e) { ElMessage.error('保存失败') }
actualSaving.value = false
}
// ============================
// 量差价差分析
// ============================
const varProduct = ref('')
const varPeriod = ref(overviewPeriod.value)
const varLoading = ref(false)
const varianceData = ref<any>({})
async function loadVariance() {
if (!varProduct.value) { ElMessage.warning('请选择产品'); return }
varLoading.value = true
try {
const r: any = await costApi.variance({ product_code: varProduct.value, period: varPeriod.value })
varianceData.value = r
} catch (e) { ElMessage.error('加载差异分析失败') }
varLoading.value = false
}
// ============================
// ABC作业成本
// ============================
const abcLoading = ref(false)
const abcActivities = ref<any[]>([])
const showAbcForm = ref(false)
const abcForm = ref<any>({})
const abcSaving = ref(false)
async function loadAbcActivities() {
abcLoading.value = true
try {
const r: any = await costApi.listAbcActivities()
abcActivities.value = r.data || []
} catch (e) { /* ignore */ }
abcLoading.value = false
}
function editAbc(row: any) {
abcForm.value = { ...row }
showAbcForm.value = true
}
async function saveAbc() {
abcSaving.value = true
try {
await costApi.createAbcActivity(abcForm.value)
ElMessage.success('已保存')
showAbcForm.value = false
loadAbcActivities()
} catch (e) { ElMessage.error('保存失败') }
abcSaving.value = false
}
// ABC分配
const showAllocForm = ref(false)
const allocForm = ref<any>({})
const allocPeriod = ref(overviewPeriod.value)
const allocLoading = ref(false)
const abcAllocations = ref<any[]>([])
const allocSaving = ref(false)
async function loadAllocations() {
allocLoading.value = true
try {
const r: any = await costApi.listAbcAllocations({ period: allocPeriod.value, entity_id: getEntityId() })
abcAllocations.value = r.data || []
} catch (e) { /* ignore */ }
allocLoading.value = false
}
async function doAllocate() {
if (!allocForm.value.activity_id || !allocForm.value.product_code) {
ElMessage.warning('请填写必要信息')
return
}
allocSaving.value = true
try {
const r: any = await costApi.doAbcAllocate(allocForm.value)
ElMessage.success(`分配完成:${r.allocated_cost || ''}`)
showAllocForm.value = false
loadAllocations()
} catch (e) { ElMessage.error('分配失败') }
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, entity_id: getEntityId() })
compData.value = r.data || r
} catch (e) { ElMessage.error('加载对比数据失败') }
compLoading.value = false
}
onMounted(() => {
loadOverview()
loadStandardCosts()
loadActualCosts()
loadAbcActivities()
loadAllocations()
})
</script>
<style scoped>
.stat-card { text-align: center; padding: 8px 0; }
.stat-label { font-size: 13px; color: #909399; margin-bottom: 8px; }
.stat-value { font-size: 24px; font-weight: bold; color: #303133; }
.trend-bar { width: 40px; border-radius: 4px 4px 0 0; background: linear-gradient(to top, #409eff, #79bbff); transition: height 0.3s; }
</style>