feat(cost): 新增钢卷囤积成本统计页面及相关接口
新增了钢卷囤积成本统计的API接口,包括囤积统计和明细列表接口,同时新建了对应的页面页面,包含筛选查询、统计卡片、维度对比图表和明细表格功能,实现钢卷囤积成本的可视化管理。
This commit is contained in:
19
klp-ui/src/api/cost/coil.js
Normal file
19
klp-ui/src/api/cost/coil.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
const BASE = '/wms/materialCoil'
|
||||
|
||||
export function getCoilHoardingStats(data) {
|
||||
return request({
|
||||
url: BASE + '/hoardingStatistics',
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function listCoilHoardingDetail(query) {
|
||||
return request({
|
||||
url: BASE + '/listWithQrcode',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
376
klp-ui/src/views/cost/coil.vue
Normal file
376
klp-ui/src/views/cost/coil.vue
Normal file
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="app-container coil-cost-page">
|
||||
<!-- 筛选区 -->
|
||||
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true" label-width="80px">
|
||||
<el-form-item label="入场卷号" prop="enterCoilNo">
|
||||
<el-input v-model="queryParams.enterCoilNo" placeholder="请输入入场钢卷号" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="当前卷号" prop="currentCoilNo">
|
||||
<el-input v-model="queryParams.currentCoilNo" placeholder="请输入当前钢卷号" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="逻辑库位" prop="warehouseId">
|
||||
<warehouse-select v-model="queryParams.warehouseId" placeholder="请选择逻辑库位" clearable style="width:180px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物料类型" prop="materialType">
|
||||
<el-select v-model="queryParams.materialType" placeholder="请选择" clearable style="width:100px">
|
||||
<el-option label="原料" value="原料" />
|
||||
<el-option label="成品" value="成品" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称" prop="itemName">
|
||||
<el-select v-model="queryParams.itemName" placeholder="请选择产品名称" clearable style="width:130px">
|
||||
<el-option label="镀锌卷" value="镀锌卷" />
|
||||
<el-option label="镀铬卷" value="镀铬卷" />
|
||||
<el-option label="冷硬卷" value="冷硬卷" />
|
||||
<el-option label="热轧卷板" value="热轧卷板" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="规格" prop="itemSpecification">
|
||||
<el-input v-model="queryParams.itemSpecification" placeholder="请选择规格" clearable @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="材质" prop="itemMaterial">
|
||||
<el-select v-model="queryParams.itemMaterial" placeholder="请选择材质" clearable style="width:130px">
|
||||
<el-option v-for="d in dict.type.coil_material" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="厂家" prop="itemManufacturer">
|
||||
<el-select v-model="queryParams.itemManufacturer" placeholder="请选择厂家" clearable style="width:130px">
|
||||
<el-option v-for="d in dict.type.coil_manufacturer" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="品质" prop="qualityStatus">
|
||||
<el-select v-model="queryParams.qualityStatus" placeholder="请选择品质" clearable style="width:120px">
|
||||
<el-option v-for="d in dict.type.coil_quality_status" :key="d.value" :label="d.label" :value="d.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="发货时间">
|
||||
<el-date-picker
|
||||
v-model="queryParams.exportTimeRange"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
style="width:240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar :show-search.sync="showSearch" @query-table="handleQuery" />
|
||||
</el-row>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div v-loading="statsLoading" class="stats-row">
|
||||
<div class="stat-item stat-blue">
|
||||
<span class="stat-val">{{ overview.avgHoardingDays }}</span>
|
||||
<span class="stat-label">平均囤积周期(天)</span>
|
||||
</div>
|
||||
<div class="stat-item stat-orange">
|
||||
<span class="stat-val">{{ formatMoney(overview.totalHoardingCost) }}</span>
|
||||
<span class="stat-label">总囤积成本(元)</span>
|
||||
</div>
|
||||
<div class="stat-item stat-red">
|
||||
<span class="stat-val">{{ formatMoney(overview.avgHoardingCost) }}</span>
|
||||
<span class="stat-label">平均单卷成本(元)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 维度对比图表 -->
|
||||
<div class="charts-row">
|
||||
<div class="chart-box chart-box-full">
|
||||
<div class="chart-box-hd">产品维度对比 · 平均囤积天数 & 成本</div>
|
||||
<div ref="dimChart" v-loading="dimLoading" class="chart-box-bd-tall" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 明细表格 -->
|
||||
<el-table v-loading="loading" :data="detailList" border stripe size="mini" style="width:100%">
|
||||
<el-table-column label="入场钢卷号" align="center" prop="enterCoilNo" show-overflow-tooltip />
|
||||
<el-table-column label="当前钢卷号" align="center" prop="currentCoilNo" show-overflow-tooltip />
|
||||
<el-table-column label="产品名称" align="center" prop="itemName" width="80" />
|
||||
<el-table-column label="规格" align="center" prop="specification" width="100" />
|
||||
<el-table-column label="材质" align="center" prop="material" width="85" />
|
||||
<el-table-column label="净重(t)" align="center" prop="netWeight" width="80" />
|
||||
<el-table-column label="入库时间" align="center" prop="createTime" width="150" />
|
||||
<el-table-column label="发货时间" align="center" prop="exportTime" width="150">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.exportTime || '未发货' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="囤积天数" align="center" prop="hoardingDays" width="80" sortable />
|
||||
<el-table-column label="囤积成本(元)" align="center" prop="hoardingCost" width="100" sortable />
|
||||
<el-table-column label="状态" align="center" width="70">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.status === 1" type="success" size="mini">已发货</el-tag>
|
||||
<el-tag v-else type="info" size="mini">在库</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="逻辑库位" align="center" prop="warehouseName" width="100" show-overflow-tooltip />
|
||||
<el-table-column label="厂家" align="center" prop="manufacturer" width="75" show-overflow-tooltip />
|
||||
<el-table-column label="品质" align="center" prop="qualityStatus" width="65" />
|
||||
</el-table>
|
||||
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDetailList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { getCoilHoardingStats, listCoilHoardingDetail } from '@/api/cost/coil'
|
||||
import WarehouseSelect from '@/components/KLPService/WarehouseSelect'
|
||||
|
||||
const PRODUCT_NAMES = ['镀锌卷', '镀铬卷', '冷硬卷', '热轧卷板']
|
||||
|
||||
function parseFirstCreateTime(row) {
|
||||
try {
|
||||
if (row.qrcodeRecord && row.qrcodeRecord.content) {
|
||||
const content = JSON.parse(row.qrcodeRecord.content)
|
||||
if (content.steps && content.steps.length > 0) {
|
||||
const ts = content.steps[0].create_time
|
||||
if (ts) return new Date(ts)
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return null
|
||||
}
|
||||
|
||||
function computeHoarding(row) {
|
||||
const firstTime = parseFirstCreateTime(row)
|
||||
if (!firstTime) return { hoardingDays: '-', hoardingCost: '-' }
|
||||
const endTime = row.exportTime ? new Date(row.exportTime) : new Date()
|
||||
const days = Math.ceil((endTime.getTime() - firstTime.getTime()) / (24 * 3600 * 1000))
|
||||
const weight = parseFloat(row.netWeight) || 0
|
||||
return {
|
||||
hoardingDays: days,
|
||||
hoardingCost: parseFloat((days * weight).toFixed(2))
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'CoilHoardingCost',
|
||||
components: { WarehouseSelect },
|
||||
dicts: ['coil_material', 'coil_manufacturer', 'coil_quality_status'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
statsLoading: false,
|
||||
dimLoading: false,
|
||||
showSearch: true,
|
||||
total: 0,
|
||||
detailList: [],
|
||||
overview: { avgHoardingDays: 0, totalHoardingCost: 0, avgHoardingCost: 0 },
|
||||
dimensionData: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
enterCoilNo: undefined,
|
||||
currentCoilNo: undefined,
|
||||
warehouseId: undefined,
|
||||
materialType: undefined,
|
||||
itemName: undefined,
|
||||
itemSpecification: undefined,
|
||||
itemMaterial: undefined,
|
||||
itemManufacturer: undefined,
|
||||
qualityStatus: undefined,
|
||||
status: '',
|
||||
exportTimeRange: undefined
|
||||
},
|
||||
dimChart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setDefaultDateRange()
|
||||
this.handleQuery()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.dimChart) { this.dimChart.dispose(); this.dimChart = null }
|
||||
},
|
||||
methods: {
|
||||
setDefaultDateRange() {
|
||||
const now = new Date()
|
||||
const first = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||
const fmt = d => d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0')
|
||||
this.queryParams.exportTimeRange = [fmt(first), fmt(now)]
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.fetchOverview()
|
||||
this.fetchDimension()
|
||||
this.getDetailList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm('queryForm')
|
||||
this.setDefaultDateRange()
|
||||
this.queryParams.status = ''
|
||||
this.handleQuery()
|
||||
},
|
||||
buildQuery(overrides) {
|
||||
const p = { ...this.queryParams, dataType: 1 }
|
||||
if (p.exportTimeRange && p.exportTimeRange.length === 2) {
|
||||
p.byExportTimeStart = p.exportTimeRange[0] + ' 00:00:00'
|
||||
p.byExportTimeEnd = p.exportTimeRange[1] + ' 23:59:59'
|
||||
}
|
||||
delete p.exportTimeRange
|
||||
delete p.pageNum
|
||||
delete p.pageSize
|
||||
if (overrides) Object.assign(p, overrides)
|
||||
return p
|
||||
},
|
||||
fetchOverview() {
|
||||
this.statsLoading = true
|
||||
getCoilHoardingStats(this.buildQuery()).then(res => {
|
||||
const d = res.data || {}
|
||||
const shipped = parseInt(d.totalCount) || 0
|
||||
this.overview = {
|
||||
avgHoardingDays: parseFloat(d.avgHoardingDays) || 0,
|
||||
totalHoardingCost: parseFloat((parseFloat(d.avgHoardingCost) * shipped).toFixed(2)) || 0,
|
||||
avgHoardingCost: parseFloat(d.avgHoardingCost) || 0
|
||||
}
|
||||
}).finally(() => { this.statsLoading = false })
|
||||
},
|
||||
fetchDimension() {
|
||||
this.dimLoading = true
|
||||
const base = this.buildQuery()
|
||||
const promises = PRODUCT_NAMES.map(v => {
|
||||
const body = { ...base, itemName: v }
|
||||
return getCoilHoardingStats(body).then(res => ({
|
||||
label: v,
|
||||
avgDays: parseFloat((res.data && res.data.avgHoardingDays) || 0),
|
||||
avgCost: parseFloat((res.data && res.data.avgHoardingCost) || 0),
|
||||
count: parseInt((res.data && res.data.totalCount) || 0)
|
||||
}))
|
||||
})
|
||||
Promise.all(promises).then(data => {
|
||||
this.dimensionData = data.filter(d => d.count > 0)
|
||||
this.$nextTick(() => this.updateDimChart())
|
||||
}).finally(() => { this.dimLoading = false })
|
||||
},
|
||||
getDetailList() {
|
||||
this.loading = true
|
||||
const params = { ...this.queryParams, dataType: 1 }
|
||||
if (params.exportTimeRange && params.exportTimeRange.length === 2) {
|
||||
params.byExportTimeStart = params.exportTimeRange[0] + ' 00:00:00'
|
||||
params.byExportTimeEnd = params.exportTimeRange[1] + ' 23:59:59'
|
||||
}
|
||||
delete params.exportTimeRange
|
||||
listCoilHoardingDetail(params).then(res => {
|
||||
const rows = (res.rows || []).map(row => {
|
||||
const h = computeHoarding(row)
|
||||
return { ...row, hoardingDays: h.hoardingDays, hoardingCost: h.hoardingCost }
|
||||
})
|
||||
this.detailList = rows
|
||||
this.total = res.total || 0
|
||||
this.loading = false
|
||||
this.fetchOverview()
|
||||
}).catch(() => { this.loading = false })
|
||||
},
|
||||
formatMoney(val) {
|
||||
if (val == null || val === '' || isNaN(val)) return '0'
|
||||
const num = Number(val)
|
||||
if (num >= 10000) return (num / 10000).toFixed(2) + '万'
|
||||
return num.toFixed(2)
|
||||
},
|
||||
handleResize() {
|
||||
if (this.dimChart) this.dimChart.resize()
|
||||
},
|
||||
initDimChart() {
|
||||
if (!this.$refs.dimChart) return
|
||||
if (this.dimChart) this.dimChart.dispose()
|
||||
this.dimChart = echarts.init(this.$refs.dimChart)
|
||||
this.updateDimChart()
|
||||
},
|
||||
updateDimChart() {
|
||||
if (!this.dimChart) { this.initDimChart(); return }
|
||||
const data = this.dimensionData || []
|
||||
this.dimChart.setOption({
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }},
|
||||
legend: { data: ['平均囤积天数', '平均囤积成本'], top: 0, textStyle: { fontSize: 11 }},
|
||||
grid: { left: '3%', right: '5%', bottom: '3%', top: '12%', containLabel: true },
|
||||
xAxis: { type: 'category', data: data.map(d => d.label), axisLabel: { fontSize: 11 }},
|
||||
yAxis: [
|
||||
{ type: 'value', name: '天', axisLabel: { fontSize: 10 }},
|
||||
{ type: 'value', name: '元', axisLabel: { fontSize: 10 }}
|
||||
],
|
||||
series: [
|
||||
{ name: '平均囤积天数', type: 'bar', data: data.map(d => d.avgDays),
|
||||
itemStyle: { color: '#409eff' }, label: { show: true, position: 'top', fontSize: 10 }, barMaxWidth: 40 },
|
||||
{ name: '平均囤积成本', type: 'bar', yAxisIndex: 1,
|
||||
data: data.map(d => parseFloat((d.avgCost || 0).toFixed(2))),
|
||||
itemStyle: { color: '#e6a23c' }, label: { show: true, position: 'top', fontSize: 10 }, barMaxWidth: 40 }
|
||||
]
|
||||
}, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mb8 { margin-bottom: 8px }
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 2px;
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-item .stat-val {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.stat-item .stat-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.stat-blue .stat-val { color: #409eff }
|
||||
.stat-orange .stat-val { color: #e6a23c }
|
||||
.stat-red .stat-val { color: #f56c6c }
|
||||
|
||||
.charts-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.chart-box {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
.chart-box-full {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-box-hd {
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.chart-box-bd-tall {
|
||||
height: 300px;
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user