二级系统联合寻找数据
This commit is contained in:
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeLossPareto',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data : []
|
||||
const names = list.map(d => d.lossCategoryName)
|
||||
const mins = list.map(d => Number(d.lossTimeMin || 0))
|
||||
const percents = list.map(d => Number(d.lossTimeRate || 0))
|
||||
const cum = []
|
||||
percents.reduce((acc, cur) => {
|
||||
const v = acc + cur
|
||||
cum.push(Math.min(100, v))
|
||||
return v
|
||||
}, 0)
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: params => {
|
||||
const title = (params && params[0] && params[0].axisValue) || ''
|
||||
const lines = (params || []).map(p => {
|
||||
const suffix = p.seriesName === '累计占比' ? ' %' : ' min'
|
||||
return `${p.marker}${p.seriesName}:${this.toFixed3(p.value)}${suffix}`
|
||||
})
|
||||
return [title, ...lines].join('<br/>')
|
||||
}
|
||||
},
|
||||
grid: { left: 40, right: 60, top: 30, bottom: 80 },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: { interval: 0, rotate: 40, fontSize: 10 }
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '损失时间 (min)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '累计占比 (%)',
|
||||
min: 0,
|
||||
max: 100,
|
||||
position: 'right',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
}
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: '损失时间',
|
||||
type: 'bar',
|
||||
data: mins,
|
||||
itemStyle: { color: '#F56C6C' }
|
||||
},
|
||||
{
|
||||
name: '累计占比',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: cum,
|
||||
smooth: true,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeStoppageTop',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
topN: {
|
||||
type: Number,
|
||||
default: 10
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data.slice() : []
|
||||
list.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
||||
const top = list.slice(0, this.topN)
|
||||
const names = top.map((d, idx) => `${idx + 1}. ${d.stopType || '未知'}`)
|
||||
const mins = top.map(d => Number(((d.duration || 0) / 60).toFixed(3)))
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'shadow' },
|
||||
formatter: params => {
|
||||
const p = params && params[0]
|
||||
if (!p) return ''
|
||||
return `${p.name}<br/>${p.marker}${p.seriesName}:${this.toFixed3(p.value)} min`
|
||||
}
|
||||
},
|
||||
grid: { left: 140, right: 20, top: 20, bottom: 20 },
|
||||
xAxis: {
|
||||
type: 'value',
|
||||
name: '停机时间 (min)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'category',
|
||||
data: names,
|
||||
axisLabel: { fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '停机时间 (min)',
|
||||
type: 'bar',
|
||||
data: mins,
|
||||
itemStyle: { color: '#67C23A' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div class="oee-chart" ref="chartRef"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
export default {
|
||||
name: 'OeeTrendChart',
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initChart()
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.chart) {
|
||||
this.chart.dispose()
|
||||
this.chart = null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
data: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.render()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toFixed3(val) {
|
||||
const n = Number(val)
|
||||
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||
},
|
||||
initChart() {
|
||||
if (!this.$refs.chartRef) return
|
||||
this.chart = echarts.init(this.$refs.chartRef)
|
||||
},
|
||||
render() {
|
||||
if (!this.chart) this.initChart()
|
||||
if (!this.chart) return
|
||||
const list = Array.isArray(this.data) ? this.data : []
|
||||
const x = list.map(d => d.statDate)
|
||||
const oee = list.map(d => Number(d.oee || 0))
|
||||
const a = list.map(d => Number(d.availability || 0))
|
||||
const p = list.map(d => Number(d.performanceTon || 0))
|
||||
const q = list.map(d => Number(d.quality || 0))
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: params => {
|
||||
const title = (params && params[0] && params[0].axisValue) || ''
|
||||
const lines = (params || []).map(p0 => `${p0.marker}${p0.seriesName}:${this.toFixed3(p0.value)} %`)
|
||||
return [title, ...lines].join('<br/>')
|
||||
}
|
||||
},
|
||||
legend: { top: 0, left: 'center' },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
xAxis: { type: 'category', data: x },
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '百分比 (%)',
|
||||
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||
},
|
||||
series: [
|
||||
{ name: 'OEE', type: 'line', data: oee, smooth: true },
|
||||
{ name: 'A', type: 'line', data: a, smooth: true },
|
||||
{ name: 'P_ton', type: 'line', data: p, smooth: true },
|
||||
{ name: 'Q', type: 'line', data: q, smooth: true }
|
||||
]
|
||||
}
|
||||
this.chart.setOption(option)
|
||||
},
|
||||
handleResize() {
|
||||
this.chart && this.chart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,745 @@
|
||||
<template>
|
||||
<div class="oee-report-page">
|
||||
<!-- 查询条件概览(去掉大标题,只保留区间提示) -->
|
||||
<el-card class="oee-header-card" shadow="never">
|
||||
<div class="oee-header">
|
||||
<div class="oee-title-block">
|
||||
<div class="oee-subtitle">
|
||||
查询区间:
|
||||
<span v-if="queryRange && queryRange.length === 2">
|
||||
{{ queryRange[0] }} ~ {{ queryRange[1] }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="oee-query-bar">
|
||||
<el-date-picker
|
||||
v-model="queryRange"
|
||||
type="daterange"
|
||||
unlink-panels
|
||||
size="small"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
:picker-options="pickerOptions"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="el-icon-search"
|
||||
@click="handleSearch"
|
||||
>
|
||||
查询
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-refresh"
|
||||
@click="handleReset"
|
||||
>
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
icon="el-icon-document"
|
||||
@click="handleExportWord"
|
||||
>
|
||||
导出 Word
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-row :gutter="16" class="oee-main-row">
|
||||
<!-- 左侧:报表主体(Word 风格) -->
|
||||
<el-col :span="18">
|
||||
<el-card class="oee-word-card" shadow="never">
|
||||
<!-- KPI 一览(报表头部表格) -->
|
||||
<div class="oee-section-title">一、KPI 总览(酸轧线 SY)</div>
|
||||
<div class="oee-help-text">
|
||||
展示所选期间酸轧线整体 OEE 及 A/P/Q 等关键指标,用于快速判断本期综合表现好坏。
|
||||
</div>
|
||||
<el-table
|
||||
:data="[kpi]"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-kpi-table"
|
||||
v-loading="loading.summary"
|
||||
>
|
||||
<el-table-column prop="oee" label="OEE (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.oee) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="availability" label="时间稼动率 A (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.availability) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="performanceTon" label="性能稼动率 P_ton (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.performanceTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quality" label="良品率 Q (%)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatPercent(scope.row.quality) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loadingTimeMin" label="负荷时间 (min)" align="center" />
|
||||
<el-table-column prop="downtimeMin" label="停机时间 (min)" align="center" />
|
||||
<el-table-column prop="runTimeMin" label="运转时间 (min)" align="center" />
|
||||
<el-table-column prop="totalOutputTon" label="总产量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.totalOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" align="center" />
|
||||
<el-table-column prop="goodOutputTon" label="良品量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.goodOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="defectOutputTon" label="次品量 (吨)" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ formatNumber(scope.row.defectOutputTon) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 日明细(趋势表格风格,方便导出 Word) -->
|
||||
<div class="oee-section-title">二、日明细(用于趋势分析)</div>
|
||||
<div class="oee-help-text">
|
||||
按天拆分 A/P/Q 及产量等指标,用于观察本月趋势、波动点以及与重大事件的对应关系。
|
||||
</div>
|
||||
<el-table
|
||||
:data="summaryList"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-daily-table"
|
||||
v-loading="loading.summary"
|
||||
>
|
||||
<el-table-column prop="statDate" label="日期" />
|
||||
<el-table-column prop="oee" label="OEE (%)" >
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.oee) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="availability" label="A (%)" >
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.availability) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="performanceTon" label="P_ton (%)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.performanceTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="quality" label="Q (%)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.quality) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="loadingTimeMin" label="负荷 (min)"/>
|
||||
<el-table-column prop="downtimeMin" label="停机 (min)"/>
|
||||
<el-table-column prop="runTimeMin" label="运转 (min)"/>
|
||||
<el-table-column prop="totalOutputTon" label="总产量 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.totalOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" />
|
||||
<el-table-column prop="goodOutputTon" label="良品 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.goodOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="defectOutputTon" label="次品 (吨)">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.defectOutputTon) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- OEE/A/P/Q 趋势图 -->
|
||||
<oee-trend-chart :data="summaryList" />
|
||||
|
||||
<!-- 理论节拍(统计口径) -->
|
||||
<div class="oee-section-title">三、理论节拍(统计口径)</div>
|
||||
<div class="oee-help-text">
|
||||
基于历史“优良日”统计得到的理论节拍(中位数),用于作为性能稼动率计算的稳定标尺。
|
||||
</div>
|
||||
<div
|
||||
v-if="idealCycle && idealCycle.dailyComparePoints && idealCycle.dailyComparePoints.length"
|
||||
class="oee-reg-section"
|
||||
>
|
||||
<div class="reg-chart-block">
|
||||
<div class="reg-chart-title">日粒度:理论耗时 vs 实际运转时间</div>
|
||||
<div class="oee-help-text">
|
||||
按天对比“理想应耗时间”和“实际运转时间”,可一眼看出哪几天效率偏低、存在明显损失空间。
|
||||
</div>
|
||||
<div ref="regDailyCompareChart" class="reg-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 7 大损失 -->
|
||||
<div class="oee-section-title">四、7 大损失汇总(按 stop_type 分类)</div>
|
||||
<div class="oee-help-text">
|
||||
将所有停机事件按 stop_type 归类,统计时间占比和次数,用于确定“先从哪几类损失下手改善”。
|
||||
</div>
|
||||
<el-table
|
||||
:data="loss7List"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-loss7-table"
|
||||
v-loading="loading.loss7"
|
||||
>
|
||||
<el-table-column prop="lossCategoryName" label="损失类别" min-width="160" />
|
||||
<el-table-column prop="lossTimeMin" label="损失时间 (min)" width="140" />
|
||||
<el-table-column prop="lossTimeRate" label="占比 (%)" width="110">
|
||||
<template slot-scope="scope">
|
||||
{{ formatPercent(scope.row.lossTimeRate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="count" label="次数" width="90" />
|
||||
<el-table-column prop="avgDurationMin" label="平均时长 (min)" width="140">
|
||||
<template slot-scope="scope">
|
||||
{{ formatNumber(scope.row.avgDurationMin) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 7 大损失帕累托图 -->
|
||||
<oee-loss-pareto :data="loss7List" />
|
||||
|
||||
<!-- 停机事件明细 -->
|
||||
<div class="oee-section-title">五、停机/损失事件明细</div>
|
||||
<div class="oee-help-text">
|
||||
罗列每一条停机/损失事件,包含时间段、区域、机组和备注,方便班组和工艺人员对照现场记录进行原因分析。
|
||||
</div>
|
||||
<el-table
|
||||
:data="eventList"
|
||||
border
|
||||
size="mini"
|
||||
class="oee-events-table"
|
||||
v-loading="loading.events"
|
||||
>
|
||||
<el-table-column prop="startDate" label="开始时间" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDateTime(scope.row.startDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="endDate" label="结束时间" min-width="150">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDateTime(scope.row.endDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="duration" label="时长 (s)" width="100" />
|
||||
<el-table-column prop="durationMin" label="时长 (min)" width="110">
|
||||
<template slot-scope="scope">
|
||||
{{ Math.max(1, Math.floor((scope.row.duration || 0) / 60)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="stopType" label="停机类型 (stop_type)" min-width="160" />
|
||||
<el-table-column prop="area" label="区域" width="100" />
|
||||
<el-table-column prop="unit" label="机组" width="100" />
|
||||
<el-table-column prop="shift" label="班次" width="80" />
|
||||
<el-table-column prop="crew" label="班组" width="80" />
|
||||
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||
</el-table>
|
||||
|
||||
<!-- 停机 TopN 条形图 -->
|
||||
<oee-stoppage-top :data="eventList" :top-n="10" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:公式与口径说明(支撑材料) -->
|
||||
<el-col :span="6">
|
||||
<el-card class="oee-formula-card" shadow="never">
|
||||
<div slot="header" class="clearfix">
|
||||
<span>公式与口径说明</span>
|
||||
</div>
|
||||
<div class="formula-block">
|
||||
<div class="formula-title">OEE 总公式</div>
|
||||
<div class="formula-eq">OEE = A × P × Q</div>
|
||||
<ul class="formula-list">
|
||||
<li>A(时间稼动率) = (负荷时间 − 停机时间) / 负荷时间</li>
|
||||
<li>P(性能稼动率,吨维度) = (理论节拍 × 产量吨) / 实际运转时间</li>
|
||||
<li>Q(良品率) = 良品吨 / 总产量吨</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="formula-block">
|
||||
<div class="formula-title">关键字段定义</div>
|
||||
<ul class="formula-list">
|
||||
<li><b>负荷时间</b>:计划生产时间扣除计划停机后的时间</li>
|
||||
<li><b>停机时间</b>:所有停机/中断(按 stop_type 汇总)的总时长</li>
|
||||
<li><b>实际运转时间</b>:负荷时间 − 停机时间</li>
|
||||
<li><b>理论节拍</b>:按“优良日统计口径”得到的稳定节拍(分钟/吨),由理论节拍接口提供</li>
|
||||
<li><b>良品/次品</b>:按 WMS `quality_status` 判断,C+/C/C-/D+/D/D- 为次品</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="formula-block" v-if="idealCycle">
|
||||
<div class="formula-title">当前理论节拍(统计口径)</div>
|
||||
<el-descriptions :column="1" size="small" class="reg-desc">
|
||||
<el-descriptions-item label="理论节拍 (min/吨)">
|
||||
{{ formatNumber(idealCycle.idealCycleTimeMinPerTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="生产节拍中位数 (min/吨)">
|
||||
{{ formatNumber(idealCycle.medianCycleTimeMinPerTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="样本天数">
|
||||
{{ idealCycle.sampleDays || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="吨数下限 (吨)">
|
||||
{{ formatNumber(idealCycle.minWeightTon) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="停机占比上限">
|
||||
{{
|
||||
idealCycle.maxDowntimeRate != null
|
||||
? (Number(idealCycle.maxDowntimeRate) * 100).toFixed(0) + '%'
|
||||
: '-'
|
||||
}}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
exportOeeWord,
|
||||
fetchOeeSummary,
|
||||
fetchOeeLoss7,
|
||||
fetchOeeEvents,
|
||||
fetchOeeIdealCycle
|
||||
} from '@/api/da/oee'
|
||||
import * as echarts from 'echarts'
|
||||
import OeeTrendChart from './components/OeeTrendChart.vue'
|
||||
import OeeLossPareto from './components/OeeLossPareto.vue'
|
||||
import OeeStoppageTop from './components/OeeStoppageTop.vue'
|
||||
|
||||
export default {
|
||||
name: 'DaOeeIndex',
|
||||
components: {
|
||||
OeeTrendChart,
|
||||
OeeLossPareto,
|
||||
OeeStoppageTop
|
||||
},
|
||||
data() {
|
||||
const today = new Date()
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
return {
|
||||
queryRange: [
|
||||
this.formatDate(firstDay),
|
||||
this.formatDate(today)
|
||||
],
|
||||
pickerOptions: {
|
||||
disabledDate(time) {
|
||||
// 不限制选择范围,保留扩展空间
|
||||
return false
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
summary: false,
|
||||
loss7: false,
|
||||
events: false,
|
||||
idealCycle: false,
|
||||
export: false
|
||||
},
|
||||
summaryList: [],
|
||||
loss7List: [],
|
||||
eventList: [],
|
||||
idealCycle: null,
|
||||
regDailyCompareChart: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
kpi() {
|
||||
const list = Array.isArray(this.summaryList) ? this.summaryList : []
|
||||
if (list.length === 0) {
|
||||
return {
|
||||
oee: 0,
|
||||
availability: 0,
|
||||
performanceTon: 0,
|
||||
quality: 0,
|
||||
loadingTimeMin: 0,
|
||||
downtimeMin: 0,
|
||||
runTimeMin: 0,
|
||||
totalOutputTon: 0,
|
||||
totalOutputCoil: 0,
|
||||
goodOutputTon: 0,
|
||||
defectOutputTon: 0
|
||||
}
|
||||
}
|
||||
let sumLoading = 0
|
||||
let sumDowntime = 0
|
||||
let sumRun = 0
|
||||
let sumTotalTon = 0
|
||||
let sumGoodTon = 0
|
||||
let sumDefectTon = 0
|
||||
let sumCoil = 0
|
||||
let sumOee = 0
|
||||
let sumA = 0
|
||||
let sumP = 0
|
||||
let sumQ = 0
|
||||
|
||||
list.forEach(row => {
|
||||
sumLoading += row.loadingTimeMin || 0
|
||||
sumDowntime += row.downtimeMin || 0
|
||||
sumRun += row.runTimeMin || 0
|
||||
sumTotalTon += Number(row.totalOutputTon || 0)
|
||||
sumGoodTon += Number(row.goodOutputTon || 0)
|
||||
sumDefectTon += Number(row.defectOutputTon || 0)
|
||||
sumCoil += row.totalOutputCoil || 0
|
||||
sumOee += Number(row.oee || 0)
|
||||
sumA += Number(row.availability || 0)
|
||||
sumP += Number(row.performanceTon || 0)
|
||||
sumQ += Number(row.quality || 0)
|
||||
})
|
||||
|
||||
const n = list.length
|
||||
const defectAgg = Math.max(0, sumTotalTon - sumGoodTon)
|
||||
return {
|
||||
oee: n ? sumOee / n : 0,
|
||||
availability: n ? sumA / n : 0,
|
||||
performanceTon: n ? sumP / n : 0,
|
||||
quality: n ? sumQ / n : 0,
|
||||
loadingTimeMin: sumLoading,
|
||||
downtimeMin: sumDowntime,
|
||||
runTimeMin: sumRun,
|
||||
totalOutputTon: sumTotalTon,
|
||||
totalOutputCoil: sumCoil,
|
||||
goodOutputTon: sumGoodTon,
|
||||
defectOutputTon: defectAgg || sumDefectTon
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.loadAll()
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.handleResize)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.handleResize)
|
||||
if (this.regDailyCompareChart) {
|
||||
this.regDailyCompareChart.dispose()
|
||||
this.regDailyCompareChart = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatDate(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||
const day = (`0${d.getDate()}`).slice(-2)
|
||||
return `${y}-${m}-${day}`
|
||||
},
|
||||
formatPercent(value) {
|
||||
if (value === null || value === undefined || isNaN(value)) {
|
||||
return '-'
|
||||
}
|
||||
const num = Number(value)
|
||||
return num.toFixed(1)
|
||||
},
|
||||
formatNumber(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '-'
|
||||
}
|
||||
const num = Number(value)
|
||||
if (isNaN(num)) return '-'
|
||||
return num.toFixed(2)
|
||||
},
|
||||
formatDateTime(val) {
|
||||
if (!val) return ''
|
||||
// 后端可能返回字符串或时间戳,这里做容错
|
||||
if (typeof val === 'string') return val
|
||||
try {
|
||||
const d = new Date(val)
|
||||
const y = d.getFullYear()
|
||||
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||
const day = (`0${d.getDate()}`).slice(-2)
|
||||
const hh = (`0${d.getHours()}`).slice(-2)
|
||||
const mm = (`0${d.getMinutes()}`).slice(-2)
|
||||
const ss = (`0${d.getSeconds()}`).slice(-2)
|
||||
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
buildQuery() {
|
||||
const [start, end] = this.queryRange || []
|
||||
return {
|
||||
startDate: start,
|
||||
endDate: end
|
||||
}
|
||||
},
|
||||
async loadAll() {
|
||||
await Promise.all([
|
||||
this.loadSummary(),
|
||||
this.loadLoss7(),
|
||||
this.loadEvents(),
|
||||
this.loadIdealCycle()
|
||||
])
|
||||
},
|
||||
async loadSummary() {
|
||||
this.loading.summary = true
|
||||
try {
|
||||
const res = await fetchOeeSummary(this.buildQuery())
|
||||
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||
|
||||
this.summaryList = res.data
|
||||
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧日汇总失败')
|
||||
} finally {
|
||||
this.loading.summary = false
|
||||
}
|
||||
},
|
||||
async loadLoss7() {
|
||||
this.loading.loss7 = true
|
||||
try {
|
||||
const res = await fetchOeeLoss7(this.buildQuery())
|
||||
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||
this.loss7List = res.data
|
||||
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧 7 大损失失败')
|
||||
} finally {
|
||||
this.loading.loss7 = false
|
||||
}
|
||||
},
|
||||
async loadEvents() {
|
||||
this.loading.events = true
|
||||
try {
|
||||
const res = await fetchOeeEvents(this.buildQuery())
|
||||
// 后端 TableDataInfo 结构:{ rows, total }
|
||||
this.eventList = (res && Array.isArray(res.rows)) ? res.rows : []
|
||||
} catch (e) {
|
||||
this.$message.error('加载酸轧停机事件失败')
|
||||
} finally {
|
||||
this.loading.events = false
|
||||
}
|
||||
},
|
||||
async loadIdealCycle() {
|
||||
this.loading.idealCycle = true
|
||||
try {
|
||||
const res = await fetchOeeIdealCycle(this.buildQuery())
|
||||
this.idealCycle = res && res.data ? res.data : null
|
||||
this.$nextTick(() => {
|
||||
this.renderIdealCycleChart()
|
||||
})
|
||||
} catch (e) {
|
||||
this.$message.error('加载理论节拍失败')
|
||||
} finally {
|
||||
this.loading.idealCycle = false
|
||||
}
|
||||
},
|
||||
handleSearch() {
|
||||
if (!this.queryRange || this.queryRange.length !== 2) {
|
||||
this.$message.warning('请选择查询日期范围')
|
||||
return
|
||||
}
|
||||
this.loadAll()
|
||||
},
|
||||
handleReset() {
|
||||
const today = new Date()
|
||||
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
this.queryRange = [this.formatDate(firstDay), this.formatDate(today)]
|
||||
this.loadAll()
|
||||
},
|
||||
async handleExportWord() {
|
||||
this.loading.export = true
|
||||
try {
|
||||
const res = await exportOeeWord(this.buildQuery())
|
||||
const blob = new Blob([res], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||
})
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `酸轧线OEE报表_${this.queryRange[0]}_${this.queryRange[1]}.docx`
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
} catch (e) {
|
||||
this.$message.error('导出 Word 失败')
|
||||
} finally {
|
||||
this.loading.export = false
|
||||
}
|
||||
},
|
||||
renderIdealCycleChart() {
|
||||
if (!this.idealCycle) return
|
||||
const dailyPoints = Array.isArray(this.idealCycle.dailyComparePoints)
|
||||
? this.idealCycle.dailyComparePoints
|
||||
: []
|
||||
if (this.$refs.regDailyCompareChart && dailyPoints.length) {
|
||||
if (!this.regDailyCompareChart) {
|
||||
this.regDailyCompareChart = echarts.init(this.$refs.regDailyCompareChart)
|
||||
}
|
||||
const categories = dailyPoints.map(d => d.statDate)
|
||||
const actualRun = dailyPoints.map(d => Number(d.actualRunTimeMin || 0))
|
||||
const theoretical = dailyPoints.map(d => Number(d.theoreticalTimeMin || 0))
|
||||
const compareOption = {
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||
legend: { top: 0, left: 'center' },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: categories,
|
||||
axisLabel: { fontSize: 10 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '时间 (min)',
|
||||
nameLocation: 'middle',
|
||||
nameGap: 35
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '实际运转时间',
|
||||
type: 'line',
|
||||
data: actualRun,
|
||||
smooth: true,
|
||||
symbolSize: 4,
|
||||
itemStyle: { color: '#409EFF' }
|
||||
},
|
||||
{
|
||||
name: '理论耗时',
|
||||
type: 'line',
|
||||
data: theoretical,
|
||||
smooth: true,
|
||||
symbolSize: 4,
|
||||
itemStyle: { color: '#E6A23C' }
|
||||
}
|
||||
]
|
||||
}
|
||||
this.regDailyCompareChart.setOption(compareOption)
|
||||
}
|
||||
},
|
||||
handleResize() {
|
||||
this.regDailyCompareChart && this.regDailyCompareChart.resize()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.oee-report-page {
|
||||
padding: 16px;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.oee-header-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.oee-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.oee-title-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.oee-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.oee-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.oee-query-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.oee-main-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.oee-word-card {
|
||||
min-height: 600px;
|
||||
background: #ffffff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.oee-section-title {
|
||||
margin: 12px 0 6px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.oee-help-text {
|
||||
margin: 0 0 6px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.oee-kpi-table,
|
||||
.oee-daily-table,
|
||||
.oee-loss7-table,
|
||||
.oee-events-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.oee-formula-card {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.formula-block {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.formula-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.formula-eq {
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-style: italic;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.formula-list {
|
||||
padding-left: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.formula-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reg-desc {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.reg-chart {
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reg-chart canvas {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user