389 lines
10 KiB
Vue
389 lines
10 KiB
Vue
|
|
<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>
|
|||
|
|
|