Files
screen/src/modules/dashboardBig/views/oee.vue
2026-05-19 19:26:41 +08:00

713 lines
19 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 class="screen-wrapper">
<div class="screen-content">
<header class="screen-header">
<h1 class="title">OEE综合监控大屏</h1>
<div class="header-right">
<span class="current-shift">当前班组{{ currentShift }}</span>
<span class="time">{{ currentTime }}</span>
<button v-if="isFullscreen" class="exit-fullscreen-btn" @click="exitFullscreen" title="退出全屏">
<span> 退出全屏</span>
</button>
</div>
</header>
<main class="screen-body">
<div class="kpi-grid">
<div class="kpi-card">
<div class="card-header">OEE</div>
<div class="kpi-value">{{ oeeData.oee.toFixed(1) }}%</div>
<div class="kpi-trend" :class="getTrendClass(oeeData.oeeTrend)">
<span>{{ oeeData.oeeTrend > 0 ? '↑' : '↓' }}</span>
{{ Math.abs(oeeData.oeeTrend).toFixed(1) }}%
</div>
</div>
<div class="kpi-card">
<div class="card-header">时间稼动率</div>
<div class="kpi-value">{{ oeeData.availability.toFixed(1) }}%</div>
<div class="kpi-trend" :class="getTrendClass(oeeData.availabilityTrend)">
<span>{{ oeeData.availabilityTrend > 0 ? '↑' : '↓' }}</span>
{{ Math.abs(oeeData.availabilityTrend).toFixed(1) }}%
</div>
</div>
<div class="kpi-card">
<div class="card-header">性能稼动率</div>
<div class="kpi-value">{{ oeeData.performance.toFixed(1) }}%</div>
<div class="kpi-trend" :class="getTrendClass(oeeData.performanceTrend)">
<span>{{ oeeData.performanceTrend > 0 ? '↑' : '↓' }}</span>
{{ Math.abs(oeeData.performanceTrend).toFixed(1) }}%
</div>
</div>
<div class="kpi-card">
<div class="card-header">良品率</div>
<div class="kpi-value">{{ oeeData.quality.toFixed(1) }}%</div>
<div class="kpi-trend" :class="getTrendClass(oeeData.qualityTrend)">
<span>{{ oeeData.qualityTrend > 0 ? '↑' : '↓' }}</span>
{{ Math.abs(oeeData.qualityTrend).toFixed(1) }}%
</div>
</div>
</div>
<div class="chart-row">
<div class="chart-box flex-2">
<div class="box-header">OEE趋势分析</div>
<div ref="trendChartRef" class="chart"></div>
</div>
<div class="chart-box flex-1">
<div class="box-header">7大损失分布</div>
<div ref="lossChartRef" class="chart"></div>
</div>
</div>
<div class="chart-row">
<div class="chart-box flex-1">
<div class="box-header">设备状态监控</div>
<div ref="statusChartRef" class="chart"></div>
</div>
<div class="chart-box flex-2">
<div class="box-header">停机事件明细</div>
<div class="event-table">
<el-table :data="stoppageList" border size="small" max-height="280">
<el-table-column prop="startDate" label="停机时间" width="160" />
<el-table-column prop="duration" label="时长(分钟)" width="100" align="center" />
<el-table-column prop="stopType" label="停机类型" width="120" />
<el-table-column prop="unit" label="设备单元" width="100" />
<el-table-column prop="shift" label="班组" width="80" />
<el-table-column prop="remark" label="原因说明" />
</el-table>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, onUnmounted, nextTick } from 'vue'
import * as echarts from 'echarts'
import { getOeeDailySummary, getOeeLossSummary, getOeeStoppageEvents } from '@/api/acidOee'
const currentTime = ref('')
const currentShift = ref('甲班')
const isFullscreen = ref(false)
const trendChartRef = ref(null)
const lossChartRef = ref(null)
const statusChartRef = ref(null)
let trendChart = null
let lossChart = null
let statusChart = null
let timeInterval = null
let dataInterval = null
let resizeObserver = null
let fullscreenChangeHandler = null
const oeeData = ref({
oee: 0,
availability: 0,
performance: 0,
quality: 0,
oeeTrend: 0,
availabilityTrend: 0,
performanceTrend: 0,
qualityTrend: 0
})
const summaryList = ref([])
const lossList = ref([])
const stoppageList = ref([])
const getTrendClass = (value) => {
if (value > 0) return 'positive'
if (value < 0) return 'negative'
return 'zero'
}
const updateTime = () => {
currentTime.value = new Date().toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const loadData = async () => {
try {
const today = new Date().toISOString().split('T')[0]
const monthStart = today.substring(0, 7) + '-01'
const [summaryRes, lossRes, eventsRes] = await Promise.all([
getOeeDailySummary({ startDate: monthStart, endDate: today }),
getOeeLossSummary({ topN: 7 }),
getOeeStoppageEvents({ pageSize: 10 })
])
if (summaryRes && summaryRes.length > 0) {
summaryList.value = summaryRes
const latest = summaryRes[0]
const previous = summaryRes[1] || latest
oeeData.value = {
oee: latest.oee || 0,
availability: latest.availability || 0,
performance: latest.performanceTon || 0,
quality: latest.quality || 0,
oeeTrend: (latest.oee || 0) - (previous.oee || 0),
availabilityTrend: (latest.availability || 0) - (previous.availability || 0),
performanceTrend: (latest.performanceTon || 0) - (previous.performanceTon || 0),
qualityTrend: (latest.quality || 0) - (previous.quality || 0)
}
}
if (lossRes) {
lossList.value = lossRes
}
if (eventsRes) {
stoppageList.value = eventsRes
}
updateCharts()
} catch (error) {
console.error('加载OEE数据失败:', error)
loadMockData()
}
}
const loadMockData = () => {
summaryList.value = [
{ statDate: '05-11', oee: 85.2, availability: 91.5, performanceTon: 88.7, quality: 97.2 },
{ statDate: '05-12', oee: 86.8, availability: 92.8, performanceTon: 89.5, quality: 97.8 },
{ statDate: '05-13', oee: 85.9, availability: 91.2, performanceTon: 89.2, quality: 97.5 },
{ statDate: '05-14', oee: 87.2, availability: 93.5, performanceTon: 90.1, quality: 98.0 },
{ statDate: '05-15', oee: 86.5, availability: 92.1, performanceTon: 89.8, quality: 97.5 }
]
lossList.value = [
{ lossCategoryName: '设备故障', lossTimeMin: 350, lossTimeRate: 31.8 },
{ lossCategoryName: '换模换线', lossTimeMin: 85, lossTimeRate: 24.5 },
{ lossCategoryName: '空转停机', lossTimeMin: 55, lossTimeRate: 15.8 },
{ lossCategoryName: '速度损失', lossTimeMin: 45, lossTimeRate: 12.9 },
{ lossCategoryName: '质量损失', lossTimeMin: 25, lossTimeRate: 7.2 },
{ lossCategoryName: '启动损失', lossTimeMin: 15, lossTimeRate: 4.3 },
{ lossCategoryName: '管理损失', lossTimeMin: 6, lossTimeRate: 1.7 }
]
stoppageList.value = [
{ startDate: '2024-05-15 14:25:00', duration: 120, stopType: '设备故障', unit: '1#轧机', shift: '甲班', remark: '电机故障' },
{ startDate: '2024-05-15 13:15:00', duration: 45, stopType: '换模换线', unit: '2#轧机', shift: '乙班', remark: '更换模具' },
{ startDate: '2024-05-15 11:30:00', duration: 15, stopType: '计划停机', unit: '1#轧机', shift: '甲班', remark: '例行检查' }
]
oeeData.value = {
oee: 86.5,
availability: 92.1,
performance: 89.8,
quality: 97.5,
oeeTrend: 0.5,
availabilityTrend: 0.3,
performanceTrend: 0.2,
qualityTrend: 0.1
}
updateCharts()
}
const updateCharts = () => {
updateTrendChart()
updateLossChart()
updateStatusChart()
}
const updateTrendChart = () => {
if (!trendChart) return
const dates = summaryList.value.map(item => item.statDate)
const oeeValues = summaryList.value.map(item => item.oee)
const avalValues = summaryList.value.map(item => item.availability)
const perfValues = summaryList.value.map(item => item.performanceTon)
const qualValues = summaryList.value.map(item => item.quality)
trendChart.setOption({
backgroundColor: 'transparent',
grid: { top: 40, right: 20, bottom: 30, left: 50 },
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f',
textStyle: { color: '#fff' }
},
legend: {
data: ['OEE', '时间稼动率', '性能稼动率', '良品率'],
bottom: 0,
textStyle: { color: '#a0c4e8' }
},
xAxis: {
type: 'category',
data: dates,
axisLine: { lineStyle: { color: '#3a5a8a' } },
axisTick: { show: false },
axisLabel: { color: '#a0c4e8' }
},
yAxis: {
type: 'value',
min: 70,
max: 100,
axisLine: { show: false },
splitLine: { lineStyle: { color: '#1e3a5f', type: 'dashed' } },
axisLabel: { color: '#a0c4e8' }
},
series: [
{
name: 'OEE',
type: 'line',
smooth: true,
data: oeeValues,
lineStyle: { color: '#00d4ff', width: 3 },
itemStyle: { color: '#00d4ff' },
symbol: 'circle',
symbolSize: 8,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(0, 212, 255, 0.3)' },
{ offset: 1, color: 'rgba(0, 212, 255, 0.05)' }
])
}
},
{
name: '时间稼动率',
type: 'line',
smooth: true,
data: avalValues,
lineStyle: { color: '#00ff88', width: 2 },
itemStyle: { color: '#00ff88' }
},
{
name: '性能稼动率',
type: 'line',
smooth: true,
data: perfValues,
lineStyle: { color: '#7c63ff', width: 2 },
itemStyle: { color: '#7c63ff' }
},
{
name: '良品率',
type: 'line',
smooth: true,
data: qualValues,
lineStyle: { color: '#ff9f43', width: 2 },
itemStyle: { color: '#ff9f43' }
}
]
})
}
const updateLossChart = () => {
if (!lossChart) return
lossChart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
formatter: '{b}: {c}分钟 ({d}%)',
backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f',
textStyle: { color: '#fff' }
},
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
data: lossList.value.map((item, index) => ({
value: item.lossTimeMin || 0,
name: item.lossCategoryName || '损失' + (index + 1),
itemStyle: {
color: ['#ff6b6b', '#ffa94d', '#ffd43b', '#69db7c', '#74c0fc', '#b197fc', '#ff8fab'][index]
}
})),
label: { show: false },
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
})
}
const updateStatusChart = () => {
if (!statusChart) return
statusChart.setOption({
backgroundColor: 'transparent',
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(10, 20, 40, 0.9)',
borderColor: '#1e3a5f',
textStyle: { color: '#fff' }
},
legend: { bottom: 0, textStyle: { color: '#a0c4e8' } },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
data: [
{ value: 65, name: '运行中', itemStyle: { color: '#00ff88' } },
{ value: 15, name: '待机', itemStyle: { color: '#ffd43b' } },
{ value: 12, name: '故障', itemStyle: { color: '#ff6b6b' } },
{ value: 8, name: '维护', itemStyle: { color: '#74c0fc' } }
],
label: { show: false },
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
})
}
const handleResize = () => {
nextTick(() => {
trendChart?.resize()
lossChart?.resize()
statusChart?.resize()
})
}
onMounted(() => {
updateTime()
timeInterval = setInterval(updateTime, 1000)
nextTick(() => {
if (trendChartRef.value) {
trendChart = echarts.init(trendChartRef.value)
}
if (lossChartRef.value) {
lossChart = echarts.init(lossChartRef.value)
}
if (statusChartRef.value) {
statusChart = echarts.init(statusChartRef.value)
}
loadData()
window.addEventListener('resize', handleResize)
if (window.ResizeObserver) {
resizeObserver = new ResizeObserver(() => {
handleResize()
})
const container = document.querySelector('.screen-wrapper')
if (container) {
resizeObserver.observe(container)
}
}
dataInterval = setInterval(loadData, 30000)
})
window.addEventListener('refresh-data', handleRefresh)
fullscreenChangeHandler = () => {
isFullscreen.value = !!document.fullscreenElement
}
document.addEventListener('fullscreenchange', fullscreenChangeHandler)
})
const handleRefresh = () => {
loadData()
}
const exitFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(err => {
console.error('退出全屏失败:', err)
})
}
}
onBeforeUnmount(() => {
if (timeInterval) {
clearInterval(timeInterval)
timeInterval = null
}
if (dataInterval) {
clearInterval(dataInterval)
dataInterval = null
}
window.removeEventListener('resize', handleResize)
window.removeEventListener('refresh-data', handleRefresh)
if (resizeObserver) {
resizeObserver.disconnect()
resizeObserver = null
}
if (fullscreenChangeHandler) {
document.removeEventListener('fullscreenchange', fullscreenChangeHandler)
fullscreenChangeHandler = null
}
if (trendChart) {
trendChart.dispose()
trendChart = null
}
if (lossChart) {
lossChart.dispose()
lossChart = null
}
if (statusChart) {
statusChart.dispose()
statusChart = null
}
})
onUnmounted(() => {
// 确保清理完成
})
</script>
<style lang="scss" scoped>
.screen-wrapper {
width: 100%;
min-height: 100vh;
height: 100%;
background: linear-gradient(180deg, #0a1428 0%, #0d1b34 50%, #0a1428 100%);
overflow-y: auto;
overflow-x: hidden;
margin: 0;
padding: 0;
}
.screen-content {
background: transparent;
color: #ffffff;
width: 100%;
min-height: 100vh;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.screen-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
background: linear-gradient(90deg, rgba(0, 168, 204, 0.9) 0%, rgba(0, 212, 255, 0.8) 50%, rgba(0, 168, 204, 0.9) 100%);
margin-bottom: 20px;
border-bottom: 2px solid rgba(0, 212, 255, 0.4);
.title {
font-size: 26px;
font-weight: bold;
color: #00d4ff;
letter-spacing: 3px;
margin: 0;
text-shadow: 0 0 20px rgba(0, 212, 255, 0.5);
}
.header-right {
display: flex;
gap: 25px;
align-items: center;
.current-shift {
font-size: 16px;
color: #a0c4e8;
font-weight: 600;
}
.time {
font-size: 18px;
color: #00d4ff;
font-family: 'Courier New', monospace;
font-weight: bold;
}
.exit-fullscreen-btn {
padding: 8px 16px;
border: 1px solid rgba(255, 107, 107, 0.5);
border-radius: 6px;
background: rgba(255, 107, 107, 0.2);
color: #ff6b6b;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: rgba(255, 107, 107, 0.3);
border-color: #ff6b6b;
box-shadow: 0 0 15px rgba(255, 107, 107, 0.3);
}
}
}
}
.screen-body {
padding: 0;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 20px;
.kpi-card {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.9) 0%, rgba(10, 20, 40, 0.95) 100%);
border: 1px solid rgba(0, 212, 255, 0.2);
border-radius: 8px;
padding: 0;
text-align: center;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
.card-header {
background: linear-gradient(90deg, #00a8cc 0%, #00d4ff 50%, #00a8cc 100%);
padding: 10px 15px;
font-size: 14px;
font-weight: bold;
color: #0a1428;
letter-spacing: 2px;
}
.kpi-value {
font-size: 40px;
font-weight: bold;
color: #00d4ff;
margin: 15px 0;
text-shadow: 0 0 15px rgba(0, 212, 255, 0.6);
}
.kpi-trend {
font-size: 14px;
padding: 4px 12px;
border-radius: 15px;
display: inline-block;
margin-bottom: 15px;
font-weight: 600;
&.positive {
background: rgba(0, 255, 136, 0.15);
color: #00ff88;
border: 1px solid rgba(0, 255, 136, 0.3);
}
&.negative {
background: rgba(255, 107, 107, 0.15);
color: #ff6b6b;
border: 1px solid rgba(255, 107, 107, 0.3);
}
&.zero {
background: rgba(160, 196, 232, 0.15);
color: #a0c4e8;
border: 1px solid rgba(160, 196, 232, 0.3);
}
}
}
}
.chart-row {
display: flex;
gap: 16px;
margin-bottom: 16px;
.chart-box {
background: linear-gradient(180deg, rgba(14, 40, 80, 0.85) 0%, rgba(10, 20, 40, 0.9) 100%);
border: 1px solid rgba(0, 212, 255, 0.15);
border-radius: 8px;
padding: 0;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
&.flex-1 {
flex: 1;
}
&.flex-2 {
flex: 2;
}
.box-header {
background: linear-gradient(90deg, rgba(0, 168, 204, 0.8) 0%, rgba(0, 212, 255, 0.6) 100%);
padding: 12px 18px;
font-size: 14px;
font-weight: bold;
color: #0a1428;
letter-spacing: 2px;
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
}
.chart {
height: 300px;
width: 100%;
padding: 15px;
}
.event-table {
flex: 1;
padding: 15px;
}
}
}
:deep(.el-table) {
background: rgba(10, 20, 40, 0.8);
border: 1px solid rgba(0, 212, 255, 0.2);
.el-table__header-wrapper {
.el-table__header {
th {
background: rgba(0, 212, 255, 0.15);
color: #00d4ff;
font-weight: bold;
border-bottom: 1px solid rgba(0, 212, 255, 0.3);
}
}
}
.el-table__body-wrapper {
.el-table__body {
tr {
td {
color: #a0c4e8;
border-bottom: 1px solid rgba(0, 212, 255, 0.1);
}
&:nth-child(even) {
background: rgba(0, 212, 255, 0.05);
}
&:hover {
background: rgba(0, 212, 255, 0.1);
}
}
}
}
}
@media screen and (max-width: 1200px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.chart-row {
flex-direction: column;
}
}
@media screen and (max-width: 768px) {
.kpi-grid {
grid-template-columns: 1fr;
}
}
</style>