Files
klp-oa/klp-ui/src/views/wms/report/line.vue
砂糖 29eab97af4 refactor(wms report): 优化产线筛选组件样式与默认值
1. 将产线下拉选择替换为单选按钮组,提升筛选交互体验
2. 设置产线筛选默认值为酸轧线对应编码
3. 新增工序配置工具类与产线元数据工具类,统一管理工序与产线数据
2026-06-30 10:02:49 +08:00

841 lines
36 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 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>