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>
|
||
|