成本模块

This commit is contained in:
2025-12-02 17:58:16 +08:00
parent be91905508
commit 4b9cce2777
22 changed files with 4808 additions and 3 deletions

View File

@@ -0,0 +1,388 @@
<template>
<div class="app-container cost-dashboard">
<!-- 成本概览 -->
<div class="overview-section">
<div class="overview-item">
<div class="overview-label">今日总成本</div>
<div class="overview-value primary">{{ formatMoney(todayCost) }}</div>
<div class="overview-desc"></div>
</div>
<div class="overview-item">
<div class="overview-label">在库钢卷数</div>
<div class="overview-value warning">{{ totalCoils }}</div>
<div class="overview-desc"></div>
</div>
<div class="overview-item">
<div class="overview-label">总净重</div>
<div class="overview-value success">{{ formatWeight(totalNetWeight) }}</div>
<div class="overview-desc"></div>
</div>
<div class="overview-item">
<div class="overview-label">当前成本标准</div>
<div class="overview-value info">{{ formatMoney(currentUnitCost) }}</div>
<div class="overview-desc">//</div>
</div>
</div>
<!-- 快速入口 -->
<div class="action-section">
<div class="section-header">
<span>快速入口</span>
<el-tooltip content="批量日计算改为后端定时任务,这里仅提供配置与明细入口" placement="top">
<i class="el-icon-question"></i>
</el-tooltip>
</div>
<div class="action-buttons">
<el-button type="warning" icon="el-icon-setting" @click="goToStandardConfig">
成本标准配置
</el-button>
<el-button type="info" icon="el-icon-view" @click="goToDetail">
查看成本明细
</el-button>
</div>
</div>
<!-- 成本趋势图表 -->
<div class="chart-section">
<div class="section-header">
<span>成本趋势近30天</span>
<el-date-picker
v-model="trendDateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
style="width: 300px;"
value-format="yyyy-MM-dd"
@change="loadTrendData"
/>
</div>
<div id="costTrendChart" style="height: 300px;"></div>
</div>
<!-- 成本分布统计 -->
<div class="stat-section">
<div class="stat-item">
<div class="section-header">
<span>按库区统计</span>
</div>
<el-table :data="warehouseStats" stripe style="width: 100%">
<el-table-column prop="warehouseName" label="库区名称" />
<el-table-column prop="coilCount" label="钢卷数" align="right" />
<el-table-column prop="totalCost" label="总成本" align="right">
<template slot-scope="scope">
{{ formatMoney(scope.row.totalCost) }}
</template>
</el-table-column>
</el-table>
</div>
<div class="stat-item">
<div class="section-header">
<span>按物品类型统计</span>
</div>
<el-table :data="itemTypeStats" stripe style="width: 100%">
<el-table-column prop="itemType" label="物品类型">
<template slot-scope="scope">
{{ scope.row.itemType === 'raw_material' ? '原料' : '成品' }}
</template>
</el-table-column>
<el-table-column prop="coilCount" label="钢卷数" align="right" />
<el-table-column prop="totalCost" label="总成本" align="right">
<template slot-scope="scope">
{{ formatMoney(scope.row.totalCost) }}
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script>
import {
queryCostSummary,
queryCostTrend,
getCurrentCostStandard,
getCostOverview
} from '@/api/wms/cost'
import * as echarts from 'echarts'
export default {
name: 'CostDashboard',
data() {
return {
todayCost: 0,
totalCoils: 0,
totalNetWeight: 0,
totalGrossWeight: 0,
avgStorageDays: 0,
currentUnitCost: 10000,
trendDateRange: [],
trendChart: null,
warehouseStats: [],
itemTypeStats: []
}
},
mounted() {
this.initTrendDateRange()
this.loadOverviewData()
this.loadTrendData()
this.loadStatsData()
this.loadCurrentStandard()
},
beforeDestroy() {
if (this.trendChart) {
this.trendChart.dispose()
}
},
methods: {
initTrendDateRange() {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 30 * 24 * 60 * 60 * 1000)
this.trendDateRange = [
start.toISOString().split('T')[0],
end.toISOString().split('T')[0]
]
},
async loadOverviewData() {
try {
const res = await getCostOverview()
if (res.code === 200 && res.data) {
this.todayCost = res.data.totalCost || 0
this.totalCoils = res.data.totalCoils || 0
this.totalNetWeight = res.data.totalNetWeight || 0
this.totalGrossWeight = res.data.totalGrossWeight || 0
this.avgStorageDays = res.data.avgStorageDays || 0
}
} catch (error) {
console.error('加载概览数据失败:', error)
}
},
async loadTrendData() {
if (!this.trendDateRange || this.trendDateRange.length !== 2) return
try {
const startDate = this.trendDateRange[0] instanceof Date
? this.trendDateRange[0].toISOString().split('T')[0]
: this.trendDateRange[0]
const endDate = this.trendDateRange[1] instanceof Date
? this.trendDateRange[1].toISOString().split('T')[0]
: this.trendDateRange[1]
const res = await queryCostTrend(startDate, endDate)
if (res.code === 200 && res.data) {
this.renderTrendChart(res.data)
}
} catch (error) {
console.error('加载趋势数据失败:', error)
}
},
renderTrendChart(data) {
this.$nextTick(() => {
if (!this.trendChart) {
this.trendChart = echarts.init(document.getElementById('costTrendChart'))
}
const dates = data.map(item => item.date)
const costs = data.map(item => item.totalCost)
const option = {
tooltip: {
trigger: 'axis',
formatter: (params) => {
return `${params[0].name}<br/>成本: ${this.formatMoney(params[0].value)}`
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: dates,
axisLine: { lineStyle: { color: '#C0C4CC' } }
},
yAxis: {
type: 'value',
axisLine: { lineStyle: { color: '#C0C4CC' } },
axisLabel: {
formatter: (value) => {
if (value >= 10000) {
return (value / 10000).toFixed(1) + '万'
}
return value
}
}
},
series: [{
data: costs,
type: 'line',
smooth: true,
itemStyle: { color: '#409EFF' },
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
{ offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
]
}
}
}]
}
this.trendChart.setOption(option)
})
},
async loadStatsData() {
try {
const today = new Date().toISOString().split('T')[0]
const res = await queryCostSummary(today, today, 'warehouse', null)
if (res.code === 200) {
this.warehouseStats = res.data.details || []
const itemTypeRes = await queryCostSummary(today, today, 'itemType', null)
if (itemTypeRes.code === 200) {
this.itemTypeStats = itemTypeRes.data.details || []
}
}
} catch (error) {
console.error('加载统计数据失败:', error)
}
},
async loadCurrentStandard() {
try {
const res = await getCurrentCostStandard()
if (res.code === 200 && res.data) {
this.currentUnitCost = res.data.unitCost || 10000
}
} catch (error) {
console.error('加载成本标准失败:', error)
}
},
goToStandardConfig() {
this.$router.push('/wms/cost/standard')
},
goToDetail() {
this.$router.push('/wms/cost/detail')
},
formatMoney(value) {
if (!value) return '0.00'
return Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
},
formatWeight(value) {
if (!value) return '0.000'
return Number(value).toFixed(3)
}
}
}
</script>
<style lang="scss" scoped>
.cost-dashboard {
padding: 20px;
background: #ffffff;
}
.overview-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
.overview-item {
flex: 1;
text-align: center;
.overview-label {
font-size: 14px;
color: #606266;
margin-bottom: 10px;
}
.overview-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
&.primary {
color: #409eff;
}
&.warning {
color: #e6a23c;
}
&.success {
color: #67c23a;
}
&.info {
color: #909399;
}
}
.overview-desc {
font-size: 12px;
color: #909399;
}
}
}
.action-section {
margin-bottom: 20px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
margin-bottom: 15px;
font-size: 16px;
color: #303133;
.el-icon-question {
color: #909399;
cursor: help;
margin-left: 5px;
}
}
.action-buttons {
display: flex;
gap: 15px;
}
.chart-section {
margin-bottom: 20px;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
}
.stat-section {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.stat-item {
flex: 1;
padding: 20px;
background: #fafafa;
border-radius: 4px;
border: 1px solid #ebeef5;
}
</style>