feat: 新增多类业务功能并优化页面展示

1. 新增钢卷周期对比查询API,增加冷轧卷、花纹板物料类型
2. 优化库存积压统计逻辑,支持成品和原料数据合并计算
3. 新增告警统计功能,实现长度/厚度告警的数量和重量统计
4. 替换岗位管理页面为冷轧厂业务流程泳道图页面
This commit is contained in:
2026-06-17 11:01:47 +08:00
parent 7b7f4b902e
commit 791be3e1a5
5 changed files with 573 additions and 46 deletions

View File

@@ -529,4 +529,13 @@ export function getExportColumns() {
url: '/wms/materialCoil/exportColumns',
method: 'get',
})
}
export function listForPeriodComparison(data) {
return request({
url: '/wms/materialCoil/listForPeriodComparison',
method: 'post',
timeout: 600000,
data: data
})
}

View File

@@ -123,7 +123,7 @@ import * as echarts from 'echarts'
import { getCoilHoardingStats, listCoilHoardingDetail } from '@/api/cost/coil'
import WarehouseSelect from '@/components/KLPService/WarehouseSelect'
const PRODUCT_NAMES = ['镀锌卷', '镀铬卷', '冷硬卷', '热轧卷板']
const PRODUCT_NAMES = ['镀锌卷', '镀铬卷', '冷硬卷', '热轧卷板', '冷轧卷', '花纹板']
function parseFirstCreateTime(row) {
try {
@@ -237,17 +237,37 @@ export default {
fetchDimension() {
this.dimLoading = true
const base = this.buildQuery()
const promises = PRODUCT_NAMES.map(v => {
const body = { ...base, itemName: v }
return getCoilHoardingStats(body).then(res => ({
const promises = PRODUCT_NAMES.flatMap(v => {
const baseBody = { ...base, itemName: v }
const productReq = getCoilHoardingStats({ ...baseBody, selectType: 'product' }).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)
}))
const rawReq = getCoilHoardingStats({ ...baseBody, selectType: 'raw_material' }).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)
}))
return [productReq, rawReq]
})
Promise.all(promises).then(data => {
this.dimensionData = data.filter(d => d.count > 0)
const map = {}
data.forEach(d => {
if (!map[d.label]) {
map[d.label] = { label: d.label, avgDays: 0, avgCost: 0, count: 0 }
}
const prev = map[d.label]
const totalCount = prev.count + d.count
if (totalCount > 0) {
prev.avgDays = parseFloat(((prev.avgDays * prev.count + d.avgDays * d.count) / totalCount).toFixed(2))
prev.avgCost = parseFloat(((prev.avgCost * prev.count + d.avgCost * d.count) / totalCount).toFixed(2))
}
prev.count = totalCount
})
this.dimensionData = Object.values(map).filter(d => d.count > 0)
this.$nextTick(() => this.updateDimChart())
}).finally(() => { this.dimLoading = false })
},

View File

@@ -1,6 +1,8 @@
<template>
<div class="app-container">
<!-- 顶部搜索与操作栏 -->
<div>
<iframe style="width: 100%; height: calc(100vh - 200px);" src="/冷轧厂业务流程泳道图(1).html" frameborder="0"></iframe>
</div>
<!-- <div class="app-container">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="岗位名称" prop="postName">
<el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable @keyup.enter.native="handleQuery" />
@@ -28,11 +30,9 @@
</div>
</div>
<div ref="treeChart" class="chart-container"></div>
<!-- 岗位信息及操作 -->
</div>
<!-- 添加或修改岗位对话框 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="上级岗位" prop="parentId">
@@ -42,24 +42,6 @@
<el-form-item label="岗位名称" prop="postName">
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
</el-form-item>
<!-- <el-form-item label="岗位类型" prop="postType">
<el-select v-model="form.postType" placeholder="请选择岗位类型">
<el-option label="生产岗" value="PRODUCTION" />
<el-option label="质检岗" value="QUALITY" />
<el-option label="维修岗" value="MAINTENANCE" />
<el-option label="技术岗" value="TECHNICAL" />
<el-option label="管理岗" value="MANAGEMENT" />
</el-select>
</el-form-item>
<el-form-item label="岗位级别" prop="postLevel">
<el-select v-model="form.postLevel" placeholder="请选择岗位级别">
<el-option label="初级" value="JUNIOR" />
<el-option label="中级" value="MIDDLE" />
<el-option label="高级" value="SENIOR" />
<el-option label="班长" value="LEAD" />
<el-option label="经理" value="MANAGER" />
</el-select>
</el-form-item> -->
<el-form-item label="显示顺序" prop="postSort">
<el-input-number v-model="form.postSort" :min="0" :max="999" controls-position="right" />
</el-form-item>
@@ -79,7 +61,6 @@
</div>
</el-dialog>
<!-- 添加或修改岗位职责对话框 -->
<el-dialog :title="dutyTitle" :visible.sync="dutyOpen" width="600px" append-to-body>
<el-form ref="dutyForm" :model="dutyForm" :rules="dutyRules" label-width="100px">
<el-form-item label="职责名称" prop="dutyName">
@@ -88,17 +69,6 @@
<el-form-item label="职责内容" prop="dutyContent">
<el-input v-model="dutyForm.dutyContent" type="textarea" :rows="4" placeholder="请输入职责内容" />
</el-form-item>
<!-- <el-form-item label="职责类型" prop="dutyType">
<el-select v-model="dutyForm.dutyType" placeholder="请选择职责类型">
<el-option label="主要职责" value="MAIN" />
<el-option label="次要职责" value="SECONDARY" />
<el-option label="安全职责" value="SAFETY" />
<el-option label="质量职责" value="QUALITY" />
</el-select>
</el-form-item> -->
<!-- <el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="dutyForm.sortOrder" :min="0" :max="999" controls-position="right" />
</el-form-item> -->
<el-form-item label="备注" prop="remark">
<el-input v-model="dutyForm.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
@@ -109,7 +79,6 @@
</div>
</el-dialog>
<!-- 岗位职责查看对话框双击节点打开 -->
<el-dialog :title="dutyDialogTitle" :visible.sync="dutyDialogVisible" width="700px" append-to-body>
<div class="duty-dialog-wrap">
<div class="duty-dialog-toolbar">
@@ -132,7 +101,7 @@
</div>
</div>
</el-dialog>
</div>
</div> -->
</template>
<script>

View File

@@ -126,6 +126,10 @@
<span class="summary-label">消耗合计</span>
<span class="summary-value">{{ totalLossCount }} / {{ totalLossWeight }}t</span>
</div>
<div class="summary-item">
<span class="summary-label">告警合计</span>
<span class="summary-value">{{ totalAlertCount }} / {{ totalAlertWeight }}t</span>
</div>
</div>
<!-- 折线图区域 -->
@@ -190,6 +194,14 @@
<el-table-column prop="mAbRubbishRate" label="废品库占比" min-width="85" />
<el-table-column prop="mAbReturnRate" label="退货库占比" min-width="85" />
</el-table-column>
<el-table-column label="告警统计" align="center">
<el-table-column prop="lengthAlertCount" label="长度告警数" min-width="85" />
<el-table-column prop="thicknessAlertCount" label="厚度告警数" min-width="85" />
<el-table-column prop="totalAlertCount" label="总告警数" min-width="75" />
<el-table-column prop="lengthAlertWeight" label="长度告警总重(t)" min-width="105" />
<el-table-column prop="thicknessAlertWeight" label="厚度告警总重(t)" min-width="105" />
<el-table-column prop="totalAlertWeight" label="总告警总重(t)" min-width="95" />
</el-table-column>
</el-table>
</div>
</div>
@@ -198,7 +210,7 @@
<script>
import * as echarts from 'echarts'
import { listLightCoil } from '@/api/wms/coil'
import { listForPeriodComparison } from '@/api/wms/coil'
import { listLightPendingAction } from '@/api/wms/pendingAction'
import MemoInput from '@/components/MemoInput'
import MutiSelect from '@/components/MutiSelect'
@@ -232,7 +244,9 @@ export default {
],
allOutList: [],
allLossList: [],
periodData: []
periodData: [],
lengthThreshold: 0,
thicknessThreshold: 0
}
},
computed: {
@@ -252,6 +266,12 @@ export default {
totalLossWeight() {
return this.periodData.reduce((s, i) => s + (parseFloat(i.lossTotalWeight) || 0), 0).toFixed(2)
},
totalAlertCount() {
return this.periodData.reduce((s, i) => s + (i.totalAlertCount || 0), 0)
},
totalAlertWeight() {
return this.periodData.reduce((s, i) => s + (parseFloat(i.totalAlertWeight) || 0), 0).toFixed(2)
},
chartConfigs() {
return [
// ====== Row 1: 数量/总重 ======
@@ -344,6 +364,27 @@ export default {
],
height: '280px'
},
// ====== Row 5: 告警统计 ======
{
title: '告警数量趋势',
series: [
{ key: 'lengthAlertCount', label: '长度告警(卷)', color: '#f56c6c', yAxisIndex: 0 },
{ key: 'thicknessAlertCount', label: '厚度告警(卷)', color: '#e6a23c', yAxisIndex: 0 },
{ key: 'totalAlertCount', label: '总告警(卷)', color: '#409eff', yAxisIndex: 0 }
],
yAxis: [{ type: 'value', name: '数量(卷)' }],
height: '280px'
},
{
title: '告警总重趋势',
series: [
{ key: 'lengthAlertWeight', label: '长度告警总重(t)', color: '#f56c6c', yAxisIndex: 0 },
{ key: 'thicknessAlertWeight', label: '厚度告警总重(t)', color: '#e6a23c', yAxisIndex: 0 },
{ key: 'totalAlertWeight', label: '总告警总重(t)', color: '#409eff', yAxisIndex: 0 }
],
yAxis: [{ type: 'value', name: '重量(t)' }],
height: '280px'
},
{
title: 'M-异常库位分布(钢卷数与占比)',
series: [
@@ -422,6 +463,58 @@ export default {
this.handleQuery()
},
// ====== 告警阈值 ======
getAlarmThreshold() {
this.getConfigKey('material.warning.length').then(response => { this.lengthThreshold = parseFloat(response.msg) || 0 })
this.getConfigKey('material.warning.thickness').then(response => { this.thicknessThreshold = parseFloat(response.msg) || 0 })
},
// 计算一个周期内产出钢卷的告警统计(长度告警、厚度告警、总告警的数量和总重)
calcAlertSummary(outList) {
const lt = this.lengthThreshold
const tt = this.thicknessThreshold
let lengthAlertCount = 0
let thicknessAlertCount = 0
let totalAlertCount = 0
let lengthAlertWeight = 0
let thicknessAlertWeight = 0
let totalAlertWeight = 0
outList.forEach(row => {
const actualLength = row.actualLength || 0
const theoreticalLength = row.theoreticalLength || 1
const lengthDiff = actualLength - theoreticalLength
const theoreticalThickness = row.theoreticalThickness || 0
const computedThickness = row.computedThickness || 0
const thicknessDiff = theoreticalThickness - computedThickness
const weight = parseFloat(row.netWeight) || 0
const isLengthAbnormal = Math.abs(lengthDiff) / theoreticalLength > lt
const isThicknessAbnormal = thicknessDiff > tt
if (isLengthAbnormal) {
lengthAlertCount++
lengthAlertWeight += weight
}
if (isThicknessAbnormal) {
thicknessAlertCount++
thicknessAlertWeight += weight
}
if (isLengthAbnormal || isThicknessAbnormal) {
totalAlertCount++
totalAlertWeight += weight
}
})
return {
lengthAlertCount,
thicknessAlertCount,
totalAlertCount,
lengthAlertWeight: lengthAlertWeight.toFixed(2),
thicknessAlertWeight: thicknessAlertWeight.toFixed(2),
totalAlertWeight: totalAlertWeight.toFixed(2)
}
},
// ====== 数据获取 ======
handleQuery() {
this.fetchData()
@@ -439,6 +532,7 @@ export default {
this.disposeCharts()
this.chartInstances = []
this.periodData = []
this.getAlarmThreshold()
try {
const baseQuery = {
@@ -482,11 +576,11 @@ export default {
})
const [outRes, lossRes] = await Promise.all([
listLightCoil({
listForPeriodComparison({
...baseQuery, coilIds: outIds, startTime: '', endTime: '',
selectType: 'product', pageSize: 99999, pageNum: 1
}),
lossIds ? listLightCoil({
lossIds ? listForPeriodComparison({
...baseQuery, coilIds: lossIds, startTime: '', endTime: '',
selectType: 'raw_material', pageSize: 99999, pageNum: 1
}) : Promise.resolve({ data: [] })
@@ -535,6 +629,9 @@ export default {
const mAbMap = {}
mAbSummary.forEach(i => { mAbMap[i.label] = i.value })
// 告警统计(长度告警、厚度告警的数量和总重)
const alertSummary = this.calcAlertSummary(outList)
return {
periodLabel: p.label,
// 基础统计
@@ -567,7 +664,9 @@ export default {
mAbTechRate: mAbMap['技术部占比'] || '0.00%',
mAbMiniRate: mAbMap['小钢卷库占比'] || '0.00%',
mAbRubbishRate: mAbMap['废品库占比'] || '0.00%',
mAbReturnRate: mAbMap['退货库占比'] || '0.00%'
mAbReturnRate: mAbMap['退货库占比'] || '0.00%',
// 告警统计
...alertSummary
}
})
},