841 lines
36 KiB
Vue
841 lines
36 KiB
Vue
<template>
|
||
<div v-loading="loading" class="app-container line-report">
|
||
<!-- 查询条件 -->
|
||
<div class="filter-area">
|
||
<div class="line-tabs-row">
|
||
<el-radio-group v-model="actionTypes" size="small" @change="handleQuery">
|
||
<el-radio-button label="">全部</el-radio-button>
|
||
<el-radio-button v-for="line in lineOptions" :key="line.value" :label="line.value">{{ line.label }}</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
<el-form label-width="70px" inline class="filter-form">
|
||
<el-form-item label="品质">
|
||
<muti-select
|
||
v-model="queryParams.qualityStatusCsv"
|
||
:options="dict.type.coil_quality_status"
|
||
placeholder="品质"
|
||
clearable
|
||
style="width: 150px;"
|
||
size="small"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="规格">
|
||
<memo-input
|
||
v-model="queryParams.itemSpecification"
|
||
style="width: 130px;"
|
||
storage-key="coilSpec"
|
||
placeholder="规格"
|
||
clearable
|
||
size="small"
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="材质">
|
||
<muti-select
|
||
v-model="queryParams.itemMaterial"
|
||
style="width: 150px;"
|
||
:options="dict.type.coil_material"
|
||
placeholder="材质"
|
||
clearable
|
||
size="small"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="厂家">
|
||
<muti-select
|
||
v-model="queryParams.itemManufacturer"
|
||
style="width: 150px;"
|
||
:options="dict.type.coil_manufacturer"
|
||
placeholder="厂家"
|
||
clearable
|
||
size="small"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="入场号">
|
||
<el-input
|
||
v-model="queryParams.enterCoilNo"
|
||
style="width: 150px;"
|
||
placeholder="入场钢卷号"
|
||
clearable
|
||
size="small"
|
||
@keyup.enter.native="handleQuery"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div class="time-row">
|
||
<span class="time-label">时间范围</span>
|
||
<el-date-picker
|
||
v-model="startTime"
|
||
style="width: 170px;"
|
||
type="datetime"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="开始"
|
||
size="small"
|
||
/>
|
||
<span class="time-sep">—</span>
|
||
<el-date-picker
|
||
v-model="endTime"
|
||
style="width: 170px;"
|
||
type="datetime"
|
||
value-format="yyyy-MM-dd HH:mm:ss"
|
||
placeholder="结束"
|
||
size="small"
|
||
/>
|
||
<span class="time-label period-label">周期</span>
|
||
<el-select v-model="periodType" style="width: 90px;" size="small" @change="recalcPeriod">
|
||
<el-option label="按日" value="day" />
|
||
<el-option label="按周" value="week" />
|
||
<el-option label="按月" value="month" />
|
||
</el-select>
|
||
<el-button-group size="small" class="quick-btns">
|
||
<el-button @click="setQuickPeriod('thisMonth')">本月</el-button>
|
||
<el-button @click="setQuickPeriod('lastMonth')">上月</el-button>
|
||
<el-button @click="setQuickPeriod('thisQuarter')">本季</el-button>
|
||
<el-button @click="setQuickPeriod('thisYear')">今年</el-button>
|
||
<el-button @click="setQuickPeriod('lastYear')">去年</el-button>
|
||
</el-button-group>
|
||
<el-button type="primary" size="small" icon="el-icon-search" @click="handleQuery">查询</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 口径说明 -->
|
||
<div class="explain-bar">
|
||
<span class="explain-icon">i</span>
|
||
<span class="explain-text">
|
||
<strong>统计口径:</strong>按{{ periodLabel }}周期拆分,统计各期产出量与消耗量。
|
||
<span class="explain-divider" />
|
||
<strong>M卷定义:</strong>钢卷经部分加工后剩余部分返回原料库并重新编号,新钢卷号中包含字母"M"(且"M"不在前5位)的钢卷定义为M卷,不计入产出统计。
|
||
<span class="explain-divider" />
|
||
<strong>成品率</strong> = 产出总重 / 消耗总重 × 100%
|
||
<strong>正品率</strong> = (1 - 异常率) × 100%
|
||
<strong>损耗率</strong> = 1 - 成品率
|
||
<strong>异常率</strong> = (异常库位钢卷数 + 特殊品质状态钢卷数) / 产出总数 × 100%
|
||
</span>
|
||
</div>
|
||
|
||
<!-- 周期数据摘要 -->
|
||
<div v-if="periodData.length > 0" class="summary-bar">
|
||
<div class="summary-item">
|
||
<span class="summary-label">周期数</span>
|
||
<span class="summary-value">{{ periodData.length }}</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<span class="summary-label">产出合计</span>
|
||
<span class="summary-value">{{ totalOutCount }}卷 / {{ totalOutWeight }}t</span>
|
||
</div>
|
||
<div class="summary-item">
|
||
<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>
|
||
|
||
<!-- 折线图区域 -->
|
||
<div class="chart-grid">
|
||
<div v-for="(cfg, idx) in chartConfigs" :key="idx" class="chart-group">
|
||
<div class="group-title">{{ cfg.title }}</div>
|
||
<div :ref="'chart_' + idx" class="chart-container" :style="{ height: cfg.height || '280px' }" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 明细表格 -->
|
||
<div v-if="periodData.length > 0" class="detail-section">
|
||
<div class="group-title">各期明细</div>
|
||
<div class="table-wrapper">
|
||
<el-table :data="periodData" border size="small" stripe height="400px" style="width: 100%">
|
||
<el-table-column prop="periodLabel" label="周期" min-width="100" fixed />
|
||
<el-table-column label="产出统计" align="center">
|
||
<el-table-column prop="outCount" label="数量" min-width="70" />
|
||
<el-table-column prop="outTotalWeight" label="总重(t)" min-width="90" />
|
||
<el-table-column prop="outAvgWeight" label="均重(t)" min-width="90" />
|
||
</el-table-column>
|
||
<el-table-column label="消耗统计" align="center">
|
||
<el-table-column prop="lossCount" label="数量" min-width="70" />
|
||
<el-table-column prop="lossTotalWeight" label="总重(t)" min-width="90" />
|
||
<el-table-column prop="lossAvgWeight" label="均重(t)" min-width="90" />
|
||
</el-table-column>
|
||
<el-table-column label="品质比率" align="center">
|
||
<el-table-column prop="passRate" label="成品率" min-width="75" />
|
||
<el-table-column prop="passRate2" label="正品率" min-width="75" />
|
||
<el-table-column prop="lossRate" label="损耗率" min-width="75" />
|
||
<el-table-column prop="abRate" label="异常率" min-width="75" />
|
||
</el-table-column>
|
||
<el-table-column label="M卷统计" align="center">
|
||
<el-table-column prop="mOutCount" label="产出数量" min-width="70" />
|
||
<el-table-column prop="mOutTotalWeight" label="产出总重(t)" min-width="90" />
|
||
<el-table-column prop="mOutAvgWeight" label="产出均重(t)" min-width="90" />
|
||
<el-table-column prop="mLossCount" label="消耗数量" min-width="70" />
|
||
<el-table-column prop="mLossTotalWeight" label="消耗总重(t)" min-width="90" />
|
||
<el-table-column prop="mLossAvgWeight" label="消耗均重(t)" min-width="90" />
|
||
<el-table-column prop="mPassRate" label="成品率" min-width="75" />
|
||
<el-table-column prop="mLossRate" label="损耗率" min-width="75" />
|
||
<el-table-column prop="mPassRate2" label="正品率" min-width="75" />
|
||
<el-table-column prop="mAbRate" label="异常率" min-width="75" />
|
||
</el-table-column>
|
||
<el-table-column label="异常库位" align="center">
|
||
<el-table-column prop="abTechCount" label="技术部" min-width="70" />
|
||
<el-table-column prop="abMiniCount" label="小钢卷库" min-width="75" />
|
||
<el-table-column prop="abRubbishCount" label="废品库" min-width="70" />
|
||
<el-table-column prop="abReturnCount" label="退货库" min-width="70" />
|
||
<el-table-column prop="abTechRate" label="技术部占比" min-width="85" />
|
||
<el-table-column prop="abMiniRate" label="小钢卷占比" min-width="85" />
|
||
<el-table-column prop="abRubbishRate" label="废品库占比" min-width="85" />
|
||
<el-table-column prop="abReturnRate" label="退货库占比" min-width="85" />
|
||
</el-table-column>
|
||
<el-table-column label="M异常库位" align="center">
|
||
<el-table-column prop="mAbTechCount" label="技术部" min-width="70" />
|
||
<el-table-column prop="mAbMiniCount" label="小钢卷库" min-width="75" />
|
||
<el-table-column prop="mAbRubbishCount" label="废品库" min-width="70" />
|
||
<el-table-column prop="mAbReturnCount" label="退货库" min-width="70" />
|
||
<el-table-column prop="mAbTechRate" label="技术部占比" min-width="85" />
|
||
<el-table-column prop="mAbMiniRate" label="小钢卷占比" min-width="85" />
|
||
<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>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import * as echarts from 'echarts'
|
||
import { listForPeriodComparison } from '@/api/wms/coil'
|
||
import { listLightPendingAction } from '@/api/wms/pendingAction'
|
||
import MemoInput from '@/components/MemoInput'
|
||
import MutiSelect from '@/components/MutiSelect'
|
||
import { calcSummary, calcMSummary, calcAbSummary } from '@/views/wms/report/js/calc'
|
||
|
||
export default {
|
||
name: 'LineReport',
|
||
components: { MemoInput, MutiSelect },
|
||
dicts: ['coil_material', 'coil_manufacturer', 'coil_quality_status'],
|
||
data() {
|
||
const addZero = (num) => num.toString().padStart(2, '0')
|
||
const now = new Date()
|
||
const monthStart = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-01 00:00:00`
|
||
const monthEnd = `${now.getFullYear()}-${addZero(now.getMonth() + 1)}-${addZero(now.getDate())} 23:59:59`
|
||
|
||
return {
|
||
chartInstances: [],
|
||
loading: false,
|
||
startTime: monthStart,
|
||
endTime: monthEnd,
|
||
periodType: 'day',
|
||
queryParams: {
|
||
enterCoilNo: '', currentCoilNo: '', itemName: '', itemSpecification: '',
|
||
itemMaterial: '', itemManufacturer: '', qualityStatusCsv: ''
|
||
},
|
||
actionTypes: '11,120,201,520',
|
||
lineOptions: [
|
||
{ label: '酸轧线', value: '11,120,201,520' }, { label: '镀锌线', value: '202,501,521' },
|
||
{ label: '双机架', value: '205,504,524' }, { label: '镀铬线', value: '206,505,525' },
|
||
{ label: '拉矫线', value: '204,503,523' }, { label: '脱脂线', value: '203,502,522' }
|
||
],
|
||
allOutList: [],
|
||
allLossList: [],
|
||
periodData: [],
|
||
lengthThreshold: 0,
|
||
thicknessThreshold: 0
|
||
}
|
||
},
|
||
computed: {
|
||
periodLabel() {
|
||
const map = { day: '日', week: '周', month: '月' }
|
||
return map[this.periodType] || '日'
|
||
},
|
||
totalOutCount() {
|
||
return this.periodData.reduce((s, i) => s + (i.outCount || 0), 0)
|
||
},
|
||
totalOutWeight() {
|
||
return this.periodData.reduce((s, i) => s + (parseFloat(i.outTotalWeight) || 0), 0).toFixed(2)
|
||
},
|
||
totalLossCount() {
|
||
return this.periodData.reduce((s, i) => s + (i.lossCount || 0), 0)
|
||
},
|
||
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: 数量/总重 ======
|
||
{
|
||
title: '产出与消耗数量/总重',
|
||
series: [
|
||
{ key: 'outCount', label: '产出数量(卷)', color: '#409eff', yAxisIndex: 0 },
|
||
{ key: 'lossCount', label: '消耗数量(卷)', color: '#f56c6c', yAxisIndex: 0 },
|
||
{ key: 'outTotalWeight', label: '产出总重(t)', color: '#67c23a', yAxisIndex: 1 },
|
||
{ key: 'lossTotalWeight', label: '消耗总重(t)', color: '#909399', yAxisIndex: 1 }
|
||
],
|
||
yAxis: [
|
||
{ type: 'value', name: '数量(卷)' },
|
||
{ type: 'value', name: '重量(t)' }
|
||
],
|
||
height: '280px'
|
||
},
|
||
{
|
||
title: 'M-卷产出与消耗数量/总重',
|
||
series: [
|
||
{ key: 'mOutCount', label: 'M-产出数量(卷)', color: '#409eff', yAxisIndex: 0 },
|
||
{ key: 'mLossCount', label: 'M-消耗数量(卷)', color: '#f56c6c', yAxisIndex: 0 },
|
||
{ key: 'mOutTotalWeight', label: 'M-产出总重(t)', color: '#67c23a', yAxisIndex: 1 },
|
||
{ key: 'mLossTotalWeight', label: 'M-消耗总重(t)', color: '#909399', yAxisIndex: 1 }
|
||
],
|
||
yAxis: [
|
||
{ type: 'value', name: '数量(卷)' },
|
||
{ type: 'value', name: '重量(t)' }
|
||
],
|
||
height: '280px'
|
||
},
|
||
// ====== Row 2: 均重 ======
|
||
{
|
||
title: '产出与消耗均重',
|
||
series: [
|
||
{ key: 'outAvgWeight', label: '产出均重(t)', color: '#e6a23c' },
|
||
{ key: 'lossAvgWeight', label: '消耗均重(t)', color: '#b37feb' }
|
||
],
|
||
yAxis: [{ type: 'value', name: '均重(t)' }],
|
||
height: '280px'
|
||
},
|
||
{
|
||
title: 'M-卷均重',
|
||
series: [
|
||
{ key: 'mOutAvgWeight', label: 'M-产出均重(t)', color: '#e6a23c' },
|
||
{ key: 'mLossAvgWeight', label: 'M-消耗均重(t)', color: '#b37feb' }
|
||
],
|
||
yAxis: [{ type: 'value', name: '均重(t)' }],
|
||
height: '280px'
|
||
},
|
||
// ====== Row 3: 品质比率 ======
|
||
{
|
||
title: '品质比率',
|
||
series: [
|
||
{ key: 'passRate', label: '成品率', color: '#67c23a', percent: true },
|
||
{ key: 'passRate2', label: '正品率', color: '#409eff', percent: true },
|
||
{ key: 'lossRate', label: '损耗率', color: '#f56c6c', percent: true },
|
||
{ key: 'abRate', label: '异常率', color: '#e6a23c', percent: true }
|
||
],
|
||
yAxis: [{ type: 'value', name: '%', axisLabel: { formatter: '{value}%' }}],
|
||
height: '280px'
|
||
},
|
||
{
|
||
title: 'M-卷品质比率',
|
||
series: [
|
||
{ key: 'mPassRate', label: 'M-成品率', color: '#67c23a', percent: true },
|
||
{ key: 'mPassRate2', label: 'M-正品率', color: '#409eff', percent: true },
|
||
{ key: 'mLossRate', label: 'M-损耗率', color: '#f56c6c', percent: true },
|
||
{ key: 'mAbRate', label: 'M-异常率', color: '#e6a23c', percent: true }
|
||
],
|
||
yAxis: [{ type: 'value', name: '%', axisLabel: { formatter: '{value}%' }}],
|
||
height: '280px'
|
||
},
|
||
// ====== Row 4: 异常库位 ======
|
||
{
|
||
title: '异常库位分布(钢卷数与占比)',
|
||
series: [
|
||
{ key: 'abTechCount', label: '技术部', color: '#409eff', yAxisIndex: 0 },
|
||
{ key: 'abMiniCount', label: '小钢卷库', color: '#67c23a', yAxisIndex: 0 },
|
||
{ key: 'abRubbishCount', label: '废品库', color: '#e6a23c', yAxisIndex: 0 },
|
||
{ key: 'abReturnCount', label: '退货库', color: '#f56c6c', yAxisIndex: 0 },
|
||
{ key: 'abTechRate', label: '技术部占比', color: '#409eff', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'abMiniRate', label: '小钢卷占比', color: '#67c23a', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'abRubbishRate', label: '废品库占比', color: '#e6a23c', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'abReturnRate', label: '退货库占比', color: '#f56c6c', yAxisIndex: 1, percent: true, dash: true }
|
||
],
|
||
yAxis: [
|
||
{ type: 'value', name: '钢卷数' },
|
||
{ type: 'value', name: '占比(%)', axisLabel: { formatter: '{value}%' }}
|
||
],
|
||
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: [
|
||
{ key: 'mAbTechCount', label: '技术部', color: '#409eff', yAxisIndex: 0 },
|
||
{ key: 'mAbMiniCount', label: '小钢卷库', color: '#67c23a', yAxisIndex: 0 },
|
||
{ key: 'mAbRubbishCount', label: '废品库', color: '#e6a23c', yAxisIndex: 0 },
|
||
{ key: 'mAbReturnCount', label: '退货库', color: '#f56c6c', yAxisIndex: 0 },
|
||
{ key: 'mAbTechRate', label: '技术部占比', color: '#409eff', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'mAbMiniRate', label: '小钢卷占比', color: '#67c23a', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'mAbRubbishRate', label: '废品库占比', color: '#e6a23c', yAxisIndex: 1, percent: true, dash: true },
|
||
{ key: 'mAbReturnRate', label: '退货库占比', color: '#f56c6c', yAxisIndex: 1, percent: true, dash: true }
|
||
],
|
||
yAxis: [
|
||
{ type: 'value', name: '钢卷数' },
|
||
{ type: 'value', name: '占比(%)', axisLabel: { formatter: '{value}%' }}
|
||
],
|
||
height: '280px'
|
||
}
|
||
]
|
||
}
|
||
},
|
||
watch: {
|
||
periodData: {
|
||
deep: true,
|
||
handler() {
|
||
this.$nextTick(() => this.renderCharts())
|
||
}
|
||
}
|
||
},
|
||
created() {
|
||
this.handleQuery()
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => this.renderCharts())
|
||
},
|
||
beforeDestroy() {
|
||
this.disposeCharts()
|
||
},
|
||
methods: {
|
||
// ====== 时间快捷选择 ======
|
||
setQuickPeriod(type) {
|
||
const now = new Date(); const addZero = n => n.toString().padStart(2, '0')
|
||
const fd = d => `${d.getFullYear()}-${addZero(d.getMonth() + 1)}-${addZero(d.getDate())}`
|
||
switch (type) {
|
||
case 'thisMonth':
|
||
this.startTime = `${fd(new Date(now.getFullYear(), now.getMonth(), 1))} 00:00:00`
|
||
this.endTime = `${fd(now)} 23:59:59`
|
||
this.periodType = 'day'
|
||
break
|
||
case 'lastMonth': {
|
||
const lm = new Date(now.getFullYear(), now.getMonth() - 1, 1)
|
||
const lme = new Date(now.getFullYear(), now.getMonth(), 0)
|
||
this.startTime = `${fd(lm)} 00:00:00`
|
||
this.endTime = `${fd(lme)} 23:59:59`
|
||
this.periodType = 'day'
|
||
break
|
||
}
|
||
case 'thisQuarter': {
|
||
const qs = Math.floor(now.getMonth() / 3) * 3
|
||
this.startTime = `${fd(new Date(now.getFullYear(), qs, 1))} 00:00:00`
|
||
this.endTime = `${fd(now)} 23:59:59`
|
||
this.periodType = 'month'
|
||
break
|
||
}
|
||
case 'thisYear':
|
||
this.startTime = `${now.getFullYear()}-01-01 00:00:00`
|
||
this.endTime = `${fd(now)} 23:59:59`
|
||
this.periodType = 'month'
|
||
break
|
||
case 'lastYear':
|
||
this.startTime = `${now.getFullYear() - 1}-01-01 00:00:00`
|
||
this.endTime = `${now.getFullYear() - 1}-12-31 23:59:59`
|
||
this.periodType = 'month'
|
||
break
|
||
}
|
||
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()
|
||
},
|
||
// 切换周期类型时仅前端重新汇总,无需重新请求接口
|
||
recalcPeriod() {
|
||
if (this.allOutList.length === 0 && this.allLossList.length === 0) return
|
||
this.disposeCharts()
|
||
this.chartInstances = []
|
||
this.splitByPeriod()
|
||
this.$nextTick(() => this.renderCharts())
|
||
},
|
||
async fetchData() {
|
||
this.loading = true
|
||
this.disposeCharts()
|
||
this.chartInstances = []
|
||
this.periodData = []
|
||
this.getAlarmThreshold()
|
||
|
||
try {
|
||
const baseQuery = {
|
||
enterCoilNo: this.queryParams.enterCoilNo,
|
||
currentCoilNo: this.queryParams.currentCoilNo,
|
||
itemName: this.queryParams.itemName,
|
||
itemSpecification: this.queryParams.itemSpecification,
|
||
itemMaterial: this.queryParams.itemMaterial,
|
||
itemManufacturer: this.queryParams.itemManufacturer,
|
||
qualityStatusCsv: this.queryParams.qualityStatusCsv
|
||
}
|
||
|
||
// 1. 获取整个时间范围内的所有待处理操作
|
||
const actionRes = await listLightPendingAction({
|
||
...baseQuery,
|
||
startTime: this.startTime,
|
||
endTime: this.endTime,
|
||
actionTypes: this.actionTypes,
|
||
actionStatus: 2
|
||
})
|
||
|
||
const actions = actionRes.data || []
|
||
if (actions.length === 0) {
|
||
this.loading = false
|
||
return
|
||
}
|
||
|
||
// 2. 提取产出和消耗的钢卷ID
|
||
const outIds = actions.map(i => i.processedCoilIds).filter(Boolean).join(',')
|
||
const lossIds = actions.filter(i => i.coilId).map(i => i.coilId).join(',')
|
||
|
||
if (!outIds) {
|
||
this.loading = false
|
||
return
|
||
}
|
||
|
||
// 3. 批量获取产出和消耗钢卷数据
|
||
const mapItems = list => (list || []).map(item => {
|
||
const [th, w] = item.specification?.split('*') || []
|
||
return { ...item, computedThickness: parseFloat(th), computedWidth: parseFloat(w) }
|
||
})
|
||
|
||
const [outRes, lossRes] = await Promise.all([
|
||
listForPeriodComparison({
|
||
...baseQuery, coilIds: outIds, startTime: '', endTime: '',
|
||
selectType: 'product', pageSize: 99999, pageNum: 1
|
||
}),
|
||
lossIds ? listForPeriodComparison({
|
||
...baseQuery, coilIds: lossIds, startTime: '', endTime: '',
|
||
selectType: 'raw_material', pageSize: 99999, pageNum: 1
|
||
}) : Promise.resolve({ data: [] })
|
||
])
|
||
|
||
this.allOutList = mapItems(outRes)
|
||
this.allLossList = mapItems(lossRes)
|
||
|
||
// 4. 按周期拆分数据
|
||
this.splitByPeriod()
|
||
|
||
// 5. 渲染图表
|
||
this.$nextTick(() => this.renderCharts())
|
||
} catch (e) {
|
||
console.error('数据获取失败:', e)
|
||
this.$message.error('数据获取失败')
|
||
}
|
||
|
||
this.loading = false
|
||
},
|
||
|
||
// ====== 周期拆分 ======
|
||
splitByPeriod() {
|
||
const periods = this.generatePeriods()
|
||
this.periodData = periods.map(p => {
|
||
const st = p.start.getTime(); const et = p.end.getTime()
|
||
|
||
const outList = this.allOutList.filter(c => {
|
||
const t = new Date(c.createTime || c.createDate).getTime()
|
||
return t >= st && t <= et
|
||
})
|
||
const lossList = this.allLossList.filter(c => {
|
||
const t = new Date(c.createTime || c.createDate).getTime()
|
||
return t >= st && t <= et
|
||
})
|
||
|
||
const summary = calcSummary(outList, lossList)
|
||
const mSummary = calcMSummary(outList, lossList)
|
||
const abSummary = calcAbSummary(outList)
|
||
const abMap = {}
|
||
abSummary.forEach(i => { abMap[i.label] = i.value })
|
||
// M-异常库位(处理M后的非M卷产出)
|
||
const isMcoil = c => c.currentCoilNo && c.currentCoilNo.includes('M') && c.currentCoilNo.indexOf('M') > 4
|
||
const nonMOut = outList.filter(c => !isMcoil(c))
|
||
const mAbSummary = calcAbSummary(nonMOut)
|
||
const mAbMap = {}
|
||
mAbSummary.forEach(i => { mAbMap[i.label] = i.value })
|
||
|
||
// 告警统计(长度告警、厚度告警的数量和总重)
|
||
const alertSummary = this.calcAlertSummary(outList)
|
||
|
||
return {
|
||
periodLabel: p.label,
|
||
// 基础统计
|
||
...summary,
|
||
// M卷统计
|
||
mOutCount: mSummary.outCount,
|
||
mOutTotalWeight: mSummary.outTotalWeight,
|
||
mLossCount: mSummary.lossCount,
|
||
mLossTotalWeight: mSummary.lossTotalWeight,
|
||
mOutAvgWeight: mSummary.outAvgWeight,
|
||
mLossAvgWeight: mSummary.lossAvgWeight,
|
||
mPassRate: mSummary.passRate,
|
||
mLossRate: mSummary.lossRate,
|
||
mPassRate2: mSummary.passRate2,
|
||
mAbRate: mSummary.abRate,
|
||
// 异常库位统计
|
||
abTechCount: abMap['技术部钢卷数'] || 0,
|
||
abMiniCount: abMap['小钢卷库钢卷数'] || 0,
|
||
abRubbishCount: abMap['废品库钢卷数'] || 0,
|
||
abReturnCount: abMap['退货库钢卷数'] || 0,
|
||
abTechRate: abMap['技术部占比'] || '0.00%',
|
||
abMiniRate: abMap['小钢卷库占比'] || '0.00%',
|
||
abRubbishRate: abMap['废品库占比'] || '0.00%',
|
||
abReturnRate: abMap['退货库占比'] || '0.00%',
|
||
// M异常库位统计
|
||
mAbTechCount: mAbMap['技术部钢卷数'] || 0,
|
||
mAbMiniCount: mAbMap['小钢卷库钢卷数'] || 0,
|
||
mAbRubbishCount: mAbMap['废品库钢卷数'] || 0,
|
||
mAbReturnCount: mAbMap['退货库钢卷数'] || 0,
|
||
mAbTechRate: mAbMap['技术部占比'] || '0.00%',
|
||
mAbMiniRate: mAbMap['小钢卷库占比'] || '0.00%',
|
||
mAbRubbishRate: mAbMap['废品库占比'] || '0.00%',
|
||
mAbReturnRate: mAbMap['退货库占比'] || '0.00%',
|
||
// 告警统计
|
||
...alertSummary
|
||
}
|
||
})
|
||
},
|
||
generatePeriods() {
|
||
const periods = []
|
||
const start = new Date(this.startTime)
|
||
const end = new Date(this.endTime)
|
||
|
||
if (this.periodType === 'day') {
|
||
const endMs = end.getTime()
|
||
const cur = new Date(start)
|
||
while (cur.getTime() <= endMs) {
|
||
const dayEnd = new Date(cur)
|
||
dayEnd.setHours(23, 59, 59, 999)
|
||
periods.push({
|
||
label: `${cur.getFullYear()}-${String(cur.getMonth() + 1).padStart(2, '0')}-${String(cur.getDate()).padStart(2, '0')}`,
|
||
start: new Date(cur),
|
||
end: new Date(Math.min(dayEnd.getTime(), endMs))
|
||
})
|
||
cur.setDate(cur.getDate() + 1)
|
||
cur.setHours(0, 0, 0, 0)
|
||
}
|
||
} else if (this.periodType === 'week') {
|
||
const endMs = end.getTime()
|
||
const cur = new Date(start)
|
||
const day = cur.getDay()
|
||
const diff = day === 0 ? -6 : 1 - day
|
||
cur.setDate(cur.getDate() + diff)
|
||
cur.setHours(0, 0, 0, 0)
|
||
|
||
while (cur.getTime() <= endMs) {
|
||
const weekEnd = new Date(cur)
|
||
weekEnd.setDate(weekEnd.getDate() + 6)
|
||
weekEnd.setHours(23, 59, 59, 999)
|
||
periods.push({
|
||
label: `${cur.getFullYear()}-W${this.getWeekNumber(cur)}`,
|
||
start: new Date(Math.max(cur.getTime(), start.getTime())),
|
||
end: new Date(Math.min(weekEnd.getTime(), endMs))
|
||
})
|
||
cur.setDate(cur.getDate() + 7)
|
||
}
|
||
} else if (this.periodType === 'month') {
|
||
const endMs = end.getTime()
|
||
const cur = new Date(start.getFullYear(), start.getMonth(), 1)
|
||
while (cur.getTime() <= endMs) {
|
||
const monthEnd = new Date(cur.getFullYear(), cur.getMonth() + 1, 0, 23, 59, 59, 999)
|
||
periods.push({
|
||
label: `${cur.getFullYear()}-${String(cur.getMonth() + 1).padStart(2, '0')}`,
|
||
start: new Date(Math.max(cur.getTime(), start.getTime())),
|
||
end: new Date(Math.min(monthEnd.getTime(), endMs))
|
||
})
|
||
cur.setMonth(cur.getMonth() + 1)
|
||
}
|
||
}
|
||
|
||
return periods
|
||
},
|
||
getWeekNumber(d) {
|
||
const temp = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
||
temp.setDate(temp.getDate() + 3 - (temp.getDay() + 6) % 7)
|
||
const week1 = new Date(temp.getFullYear(), 0, 4)
|
||
return Math.round(((temp - week1) / 86400000 + week1.getDay() + 1) / 7)
|
||
},
|
||
|
||
// ====== 图表渲染 ======
|
||
renderCharts() {
|
||
this.disposeCharts()
|
||
if (this.periodData.length === 0) return
|
||
|
||
const xLabels = this.periodData.map(p => p.periodLabel)
|
||
|
||
this.chartConfigs.forEach((cfg, idx) => {
|
||
const dom = this.$refs['chart_' + idx]
|
||
if (!dom || !dom[0]) return
|
||
|
||
const chart = echarts.init(dom[0])
|
||
this.chartInstances.push(chart)
|
||
|
||
const yAxis = cfg.yAxis || [{ type: 'value' }]
|
||
const series = cfg.series.map(s => ({
|
||
name: s.label,
|
||
type: 'line',
|
||
smooth: true,
|
||
symbol: s.dash ? 'diamond' : 'circle',
|
||
symbolSize: 6,
|
||
lineStyle: { width: 2, color: s.color, type: s.dash ? 'dashed' : 'solid' },
|
||
itemStyle: { color: s.color },
|
||
yAxisIndex: s.yAxisIndex || 0,
|
||
data: this.periodData.map(p => {
|
||
const raw = p[s.key]
|
||
if (s.percent) {
|
||
return parseFloat(raw || '0') || 0
|
||
}
|
||
return raw !== undefined && raw !== null ? parseFloat(raw) || raw : 0
|
||
})
|
||
}))
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
formatter: function(params) {
|
||
let html = params[0].axisValue + '<br/>'
|
||
params.forEach(p => {
|
||
const val = typeof p.value === 'number' ? p.value.toFixed(2) : p.value
|
||
html += p.marker + ' ' + p.seriesName + ':' + val + '<br/>'
|
||
})
|
||
return html
|
||
}
|
||
},
|
||
legend: { data: cfg.series.map(s => s.label), top: 5 },
|
||
grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true },
|
||
xAxis: { type: 'category', data: xLabels, axisLabel: { rotate: xLabels.length > 15 ? 45 : 0, fontSize: 11 }},
|
||
yAxis: yAxis,
|
||
dataZoom: [
|
||
{ type: 'inside', start: 0, end: 100 },
|
||
{ type: 'slider', start: 0, end: 100, height: 20, bottom: 5 }
|
||
],
|
||
series: series
|
||
}
|
||
|
||
chart.setOption(option)
|
||
|
||
const handler = () => chart.resize()
|
||
window.addEventListener('resize', handler)
|
||
chart._resizeHandler = handler
|
||
})
|
||
},
|
||
disposeCharts() {
|
||
this.chartInstances.forEach(chart => {
|
||
if (chart._resizeHandler) {
|
||
window.removeEventListener('resize', chart._resizeHandler)
|
||
}
|
||
chart.dispose()
|
||
})
|
||
this.chartInstances = []
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.line-report { min-width: 900px; }
|
||
.filter-area { background: #fff; padding: 8px 12px; margin-bottom: 8px; border-bottom: 1px solid #e4e7ed; }
|
||
.line-tabs-row { margin-bottom: 8px; padding-bottom: 6px; border-bottom: 1px solid #eee; }
|
||
.filter-form { margin-bottom: 0; }
|
||
.filter-form .el-form-item { margin-bottom: 6px; margin-right: 4px; }
|
||
.filter-form >>> .el-form-item__label { font-size: 12px; }
|
||
.time-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding-top: 6px; border-top: 1px solid #ebeef5; }
|
||
.time-label { font-size: 12px; color: #909399; font-weight: 600; white-space: nowrap; }
|
||
.period-label { color: #409eff; margin-left: 4px; }
|
||
.time-sep { color: #c0c4cc; font-size: 12px; }
|
||
.quick-btns { margin-left: auto; }
|
||
.explain-bar { display: flex; align-items: flex-start; gap: 6px; padding: 6px 12px; margin-bottom: 8px; background: #f0f9ff; border-radius: 4px; font-size: 12px; color: #606266; line-height: 1.6; }
|
||
.explain-icon { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; min-width: 16px; border-radius: 50%; background: #909399; color: #fff; font-size: 11px; font-weight: 700; font-style: normal; margin-top: 2px; }
|
||
.explain-text { flex: 1; }
|
||
.explain-divider { display: inline-block; width: 0; height: 0; margin: 0 2px; border-left: 1px solid #c0c4cc; }
|
||
|
||
.summary-bar { display: flex; gap: 16px; padding: 10px 12px; background: #f5f7fa; border-radius: 4px; margin-bottom: 10px; flex-wrap: wrap; }
|
||
.summary-item { display: flex; flex-direction: column; align-items: center; min-width: 100px; }
|
||
.summary-label { font-size: 12px; color: #909399; white-space: nowrap; }
|
||
.summary-value { font-size: 16px; font-weight: 600; color: #303133; margin-top: 2px; white-space: nowrap; }
|
||
|
||
.chart-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||
.group-title { font-size: 13px; font-weight: 600; color: #303133; padding: 5px 0; margin-bottom: 6px; border-bottom: 2px solid #409eff; }
|
||
.chart-container { width: 100%; background: #fff; border: 1px solid #ebeef5; border-radius: 4px; }
|
||
|
||
.detail-section { margin-top: 8px; }
|
||
.table-wrapper { overflow-x: auto; }
|
||
.detail-section >>> .el-table th { background-color: #f5f7fa; font-size: 12px; }
|
||
.detail-section >>> .el-table td { font-size: 12px; }
|
||
</style>
|