¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | | |
| | | |