Files
klp-oa/klp-ui/src/views/wms/cost/dashboard/index.vue
2025-12-02 17:58:16 +08:00

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