| ¶Ô±ÈÐÂÎļþ | 
 |  |  | 
 |  |  | <template> | 
 |  |  |   <div class="app-container iot-monitor"> | 
 |  |  |     <div class="header"> | 
 |  |  |       <div class="title">宿¶å·¥åµçæ§ï¼IoTï¼</div> | 
 |  |  |       <div class="actions"> | 
 |  |  |         <el-button type="primary" @click="toggleCollecting">{{ collecting ? 'æåéé' : 'å¯å¨éé' }}</el-button> | 
 |  |  |         <el-button @click="resetAll">éç½®</el-button> | 
 |  |  |         <span class="ts">䏿¬¡æ´æ°æ¶é´ï¼{{ lastUpdatedDisplay }}</span> | 
 |  |  |       </div> | 
 |  |  |     </div> | 
 |  |  |  | 
 |  |  |     <el-alert | 
 |  |  |       title="è¾¹ç¼é¢è¦è§åï¼è½´æ¿ç£¨æ-æ¯å¨å¼å离åºçº¿Â±5%触ååè¦ï¼æ¸©åº¦/ååè¶ç触åæé" | 
 |  |  |       type="info" | 
 |  |  |       :closable="false" | 
 |  |  |       show-icon | 
 |  |  |       class="rule-alert" | 
 |  |  |     /> | 
 |  |  |  | 
 |  |  |     <el-row :gutter="16"> | 
 |  |  |       <el-col v-for="dev in devices" :key="dev.id" :span="12"> | 
 |  |  |         <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']"> | 
 |  |  |           <template #header> | 
 |  |  |             <div class="card-header"> | 
 |  |  |               <div class="card-title"> | 
 |  |  |                 <span class="device-name">{{ dev.name }}</span> | 
 |  |  |                 <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? 'åè¦' : 'æ£å¸¸' }}</el-tag> | 
 |  |  |               </div> | 
 |  |  |               <div class="meta">ç±»åï¼{{ dev.type }}ï½åºçº¿æ¯å¨ï¼{{ dev.baseline.vibration.toFixed(2) }} mm/s</div> | 
 |  |  |             </div> | 
 |  |  |           </template> | 
 |  |  |  | 
 |  |  |           <div class="metrics"> | 
 |  |  |             <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }"> | 
 |  |  |               <div class="metric-head"> | 
 |  |  |                 <span>æ¯å¨(mm/s)</span> | 
 |  |  |                 <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '±5%è¶ç' : 'åºçº¿Â±5%' }}</el-tag> | 
 |  |  |               </div> | 
 |  |  |               <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div> | 
 |  |  |               <Echarts | 
 |  |  |                 :xAxis="[{ type: 'category', data: xAxisLabels }]" | 
 |  |  |                 :yAxis="[{ type: 'value', name: 'mm/s' }]" | 
 |  |  |                 :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]" | 
 |  |  |                 :tooltip="{ trigger: 'axis' }" | 
 |  |  |                 :grid="{ left: 40, right: 10, top: 10, bottom: 20 }" | 
 |  |  |                 :chartStyle="{ height: '160px', width: '100%' }" | 
 |  |  |                 :lineColors="['#409EFF']" | 
 |  |  |               /> | 
 |  |  |             </div> | 
 |  |  |  | 
 |  |  |             <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }"> | 
 |  |  |               <div class="metric-head"> | 
 |  |  |                 <span>温度(°C)</span> | 
 |  |  |                 <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? 'è¶ç' : '20~80' }}</el-tag> | 
 |  |  |               </div> | 
 |  |  |               <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div> | 
 |  |  |               <Echarts | 
 |  |  |                 :xAxis="[{ type: 'category', data: xAxisLabels }]" | 
 |  |  |                 :yAxis="[{ type: 'value', name: '°C' }]" | 
 |  |  |                 :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]" | 
 |  |  |                 :tooltip="{ trigger: 'axis' }" | 
 |  |  |                 :grid="{ left: 40, right: 10, top: 10, bottom: 20 }" | 
 |  |  |                 :chartStyle="{ height: '160px', width: '100%' }" | 
 |  |  |                 :lineColors="['#E6A23C']" | 
 |  |  |               /> | 
 |  |  |             </div> | 
 |  |  |  | 
 |  |  |             <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }"> | 
 |  |  |               <div class="metric-head"> | 
 |  |  |                 <span>åå(MPa)</span> | 
 |  |  |                 <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? 'è¶ç' : '0.2~1.5' }}</el-tag> | 
 |  |  |               </div> | 
 |  |  |               <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div> | 
 |  |  |               <Echarts | 
 |  |  |                 :xAxis="[{ type: 'category', data: xAxisLabels }]" | 
 |  |  |                 :yAxis="[{ type: 'value', name: 'MPa' }]" | 
 |  |  |                 :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]" | 
 |  |  |                 :tooltip="{ trigger: 'axis' }" | 
 |  |  |                 :grid="{ left: 40, right: 10, top: 10, bottom: 20 }" | 
 |  |  |                 :chartStyle="{ height: '160px', width: '100%' }" | 
 |  |  |                 :lineColors="['#67C23A']" | 
 |  |  |               /> | 
 |  |  |             </div> | 
 |  |  |           </div> | 
 |  |  |         </el-card> | 
 |  |  |       </el-col> | 
 |  |  |     </el-row> | 
 |  |  |   </div> | 
 |  |  |    | 
 |  |  | </template> | 
 |  |  |  | 
 |  |  | <script setup> | 
 |  |  | import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue' | 
 |  |  | import { ElNotification } from 'element-plus' | 
 |  |  | import Echarts from '@/components/Echarts/echarts.vue' | 
 |  |  |  | 
 |  |  | defineOptions({ name: 'IoTMonitor' }) | 
 |  |  |  | 
 |  |  | const windowSize = 30 | 
 |  |  | const collecting = ref(true) | 
 |  |  | const lastUpdated = ref(Date.now()) | 
 |  |  | const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString()) | 
 |  |  |  | 
 |  |  | const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`)) | 
 |  |  |  | 
 |  |  | function makeSeries(fill, decimals = 2) { | 
 |  |  |   return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals))) | 
 |  |  | } | 
 |  |  |  | 
 |  |  | const devices = reactive([ | 
 |  |  |   { | 
 |  |  |     id: 'water-pump', | 
 |  |  |     name: 'æ°´æ³µ', | 
 |  |  |     type: 'åºå®è®¾å¤', | 
 |  |  |     baseline: { vibration: 9 }, | 
 |  |  |     initial: { temperature: 40, pressure: 0.70 }, | 
 |  |  |     alerts: { vibration: false, temperature: false, pressure: false }, | 
 |  |  |     hasAlert: false, | 
 |  |  |     series: { | 
 |  |  |       vibration: makeSeries(9), | 
 |  |  |       temperature: makeSeries(40, 1), | 
 |  |  |       pressure: makeSeries(0.7, 2), | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  |   { | 
 |  |  |     id: 'fluid-supply-truck', | 
 |  |  |     name: '便¶²è½¦', | 
 |  |  |     type: 'ç§»å¨è£
å¤', | 
 |  |  |     baseline: { vibration: 7 }, | 
 |  |  |     initial: { temperature: 30, pressure: 0.60 }, | 
 |  |  |     alerts: { vibration: false, temperature: false, pressure: false }, | 
 |  |  |     hasAlert: false, | 
 |  |  |     series: { | 
 |  |  |       vibration: makeSeries(7), | 
 |  |  |       temperature: makeSeries(30, 1), | 
 |  |  |       pressure: makeSeries(0.6, 2), | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  |   { | 
 |  |  |     id: 'fracturing-truck', | 
 |  |  |     name: 'åè£è½¦', | 
 |  |  |     type: 'ç§»å¨è£
å¤', | 
 |  |  |     baseline: { vibration: 12 }, | 
 |  |  |     initial: { temperature: 65, pressure: 1.40 }, | 
 |  |  |     alerts: { vibration: false, temperature: false, pressure: false }, | 
 |  |  |     hasAlert: false, | 
 |  |  |     series: { | 
 |  |  |       vibration: makeSeries(12), | 
 |  |  |       temperature: makeSeries(65, 1), | 
 |  |  |       pressure: makeSeries(1.4, 2), | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  |   { | 
 |  |  |     id: 'oil-tank-truck', | 
 |  |  |     name: 'æ²¹ç½è½¦', | 
 |  |  |     type: 'ç§»å¨è£
å¤', | 
 |  |  |     baseline: { vibration: 6 }, | 
 |  |  |     initial: { temperature: 28, pressure: 0.50 }, | 
 |  |  |     alerts: { vibration: false, temperature: false, pressure: false }, | 
 |  |  |     hasAlert: false, | 
 |  |  |     series: { | 
 |  |  |       vibration: makeSeries(6), | 
 |  |  |       temperature: makeSeries(28, 1), | 
 |  |  |       pressure: makeSeries(0.5, 2), | 
 |  |  |     }, | 
 |  |  |   }, | 
 |  |  | ]) | 
 |  |  |  | 
 |  |  | function currentValue(arr) { | 
 |  |  |   return arr[arr.length - 1] ?? 0 | 
 |  |  | } | 
 |  |  |  | 
 |  |  | function pushWindow(arr, val) { | 
 |  |  |   if (arr.length >= windowSize) arr.shift() | 
 |  |  |   arr.push(val) | 
 |  |  | } | 
 |  |  |  | 
 |  |  | function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) } | 
 |  |  |  | 
 |  |  | function tickDevice(dev) { | 
 |  |  |   const vibBase = dev.baseline.vibration | 
 |  |  |   // æ¯å¨ï¼åºçº¿Â±2%éæºæ³¢å¨ï¼5%æ¦ç触å8%~12%å°å³°æ¨¡æåè¦ | 
 |  |  |   const spike = Math.random() < 0.05 | 
 |  |  |   const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04)) | 
 |  |  |   const vibVal = Number(vibNoise.toFixed(2)) | 
 |  |  |   pushWindow(dev.series.vibration, vibVal) | 
 |  |  |  | 
 |  |  |   // æ¸©åº¦ï¼ç¼æ
¢éæºæ¸¸èµ°ï¼å¹¶æ·»å å¶å髿¸©åç§» | 
 |  |  |   const tPrev = currentValue(dev.series.temperature) | 
 |  |  |   const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0) | 
 |  |  |   const tVal = Number(clamp(tDrift, 15, 95).toFixed(1)) | 
 |  |  |   pushWindow(dev.series.temperature, tVal) | 
 |  |  |  | 
 |  |  |   // ååï¼å°å¹
æ³¢å¨ï¼å¶åä½å/é«å | 
 |  |  |   const pPrev = currentValue(dev.series.pressure) | 
 |  |  |   const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0) | 
 |  |  |   const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2)) | 
 |  |  |   pushWindow(dev.series.pressure, pVal) | 
 |  |  |  | 
 |  |  |   // è¾¹ç¼è®¡ç®éå¼å¤æ | 
 |  |  |   const vibDelta = Math.abs(vibVal - vibBase) / vibBase | 
 |  |  |   const vibAlert = vibDelta > 0.05 | 
 |  |  |   const tAlert = tVal < 20 || tVal > 80 | 
 |  |  |   const pAlert = pVal < 0.2 || pVal > 1.5 | 
 |  |  |  | 
 |  |  |   const prevHasAlert = dev.hasAlert | 
 |  |  |   dev.alerts.vibration = vibAlert | 
 |  |  |   dev.alerts.temperature = tAlert | 
 |  |  |   dev.alerts.pressure = pAlert | 
 |  |  |   dev.hasAlert = vibAlert || tAlert || pAlert | 
 |  |  |  | 
 |  |  |   if (dev.hasAlert && !prevHasAlert) { | 
 |  |  |     const reasons = [] | 
 |  |  |     if (vibAlert) reasons.push(`æ¯å¨å离±5% (å½å ${vibVal} / åºçº¿ ${vibBase})`) | 
 |  |  |     if (tAlert) reasons.push(`温度è¶ç (å½å ${tVal}°C, ææ 20~80°C) `) | 
 |  |  |     if (pAlert) reasons.push(`ååè¶ç (å½å ${pVal}MPa, ææ 0.2~1.5MPa) `) | 
 |  |  |     ElNotification({ | 
 |  |  |       title: `${dev.name} åè¦`, | 
 |  |  |       message: reasons.join('ï¼'), | 
 |  |  |       type: vibAlert ? 'error' : 'warning', | 
 |  |  |       duration: 5000, | 
 |  |  |     }) | 
 |  |  |   } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | let timer = null | 
 |  |  | function start() { | 
 |  |  |   if (timer) return | 
 |  |  |   timer = setInterval(() => { | 
 |  |  |     if (!collecting.value) return | 
 |  |  |     devices.forEach(tickDevice) | 
 |  |  |     lastUpdated.value = Date.now() | 
 |  |  |   }, 10000) | 
 |  |  | } | 
 |  |  |  | 
 |  |  | function stop() { | 
 |  |  |   if (timer) { | 
 |  |  |     clearInterval(timer) | 
 |  |  |     timer = null | 
 |  |  |   } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | function toggleCollecting() { collecting.value = !collecting.value } | 
 |  |  |  | 
 |  |  | function resetAll() { | 
 |  |  |   devices.forEach(dev => { | 
 |  |  |     dev.series.vibration = makeSeries(dev.baseline.vibration) | 
 |  |  |     const t0 = dev.initial?.temperature ?? 45 | 
 |  |  |     const p0 = dev.initial?.pressure ?? 0.8 | 
 |  |  |     dev.series.temperature = makeSeries(t0, 1) | 
 |  |  |     dev.series.pressure = makeSeries(p0, 2) | 
 |  |  |     dev.alerts.vibration = false | 
 |  |  |     dev.alerts.temperature = false | 
 |  |  |     dev.alerts.pressure = false | 
 |  |  |     dev.hasAlert = false | 
 |  |  |   }) | 
 |  |  |   lastUpdated.value = Date.now() | 
 |  |  | } | 
 |  |  |  | 
 |  |  | onMounted(() => { | 
 |  |  |   start() | 
 |  |  | }) | 
 |  |  |  | 
 |  |  | onBeforeUnmount(() => { | 
 |  |  |   stop() | 
 |  |  | }) | 
 |  |  | </script> | 
 |  |  |  | 
 |  |  | <style lang="scss" scoped> | 
 |  |  | .iot-monitor { | 
 |  |  |   .header { | 
 |  |  |     display: flex; | 
 |  |  |     align-items: center; | 
 |  |  |     justify-content: space-between; | 
 |  |  |     margin-bottom: 12px; | 
 |  |  |     .title { font-size: 18px; font-weight: 600; } | 
 |  |  |     .actions { display: flex; align-items: center; gap: 8px; } | 
 |  |  |     .ts { color: #909399; font-size: 12px; } | 
 |  |  |   } | 
 |  |  |   .rule-alert { margin-bottom: 12px; } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .device-card { | 
 |  |  |   margin-bottom: 16px; | 
 |  |  |   transition: border-color 0.2s ease, box-shadow 0.2s ease; | 
 |  |  |   &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; } | 
 |  |  |   .card-header { | 
 |  |  |     display: flex; flex-direction: column; gap: 4px; | 
 |  |  |     .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; } | 
 |  |  |     .meta { color: #909399; font-size: 12px; } | 
 |  |  |   } | 
 |  |  |   .metrics { | 
 |  |  |     display: grid; | 
 |  |  |     grid-template-columns: 1fr; | 
 |  |  |     gap: 12px; | 
 |  |  |   } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .metric { | 
 |  |  |   border: 1px solid #ebeef5; | 
 |  |  |   border-radius: 6px; | 
 |  |  |   padding: 8px 8px 0 8px; | 
 |  |  |   &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; } | 
 |  |  |   &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; } | 
 |  |  | } | 
 |  |  |  | 
 |  |  | .metric-alert { | 
 |  |  |   border-color: #F56C6C; | 
 |  |  |   background: #FFF6F6; | 
 |  |  | } | 
 |  |  |  | 
 |  |  | @media (min-width: 1200px) { | 
 |  |  |   .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; } | 
 |  |  | } | 
 |  |  | </style> | 
 |  |  |  | 
 |  |  |  |