feat(wms/report): 新增产线报表页面,支持多维度数据统计与可视化
该页面实现了产线相关的钢卷产出、消耗、品质、异常库位等数据的统计分析,支持按日/周/月维度拆分数据,提供折线图可视化展示与明细表格查询,包含快捷时间范围选择、多条件筛选功能
This commit is contained in:
740
klp-ui/src/views/wms/report/line.vue
Normal file
740
klp-ui/src/views/wms/report/line.vue
Normal file
@@ -0,0 +1,740 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="app-container line-report">
|
||||
<!-- 查询条件 -->
|
||||
<div class="filter-area">
|
||||
<el-form label-width="70px" inline class="filter-form">
|
||||
<el-form-item label="产线">
|
||||
<el-select v-model="actionTypes" style="width: 150px;" placeholder="产线" clearable size="small" @change="handleQuery">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option v-for="line in lineOptions" :key="line.value" :label="line.label" :value="line.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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>
|
||||
|
||||
<!-- 折线图区域 -->
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
import { listLightCoil } 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: '',
|
||||
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: []
|
||||
}
|
||||
},
|
||||
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)
|
||||
},
|
||||
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'
|
||||
},
|
||||
{
|
||||
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()
|
||||
},
|
||||
|
||||
// ====== 数据获取 ======
|
||||
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 = []
|
||||
|
||||
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([
|
||||
listLightCoil({
|
||||
...baseQuery, coilIds: outIds, startTime: '', endTime: '',
|
||||
selectType: 'product', pageSize: 99999, pageNum: 1
|
||||
}),
|
||||
lossIds ? listLightCoil({
|
||||
...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 })
|
||||
|
||||
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%'
|
||||
}
|
||||
})
|
||||
},
|
||||
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; }
|
||||
.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>
|
||||
Reference in New Issue
Block a user