gaoluyang
2025-08-13 a36ebcb8f190ec8701223440a9bf12ae2954f25c
设备监控页面添加
已修改1个文件
已添加1个文件
330 ■■■■■ 文件已修改
src/router/index.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/iotMonitor/index.vue 317 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js
@@ -72,6 +72,19 @@
    ]
  },
  {
    path: '/equipment',
    component: Layout,
    redirect: '/equipment/iot-monitor',
    children: [
      {
        path: 'iot-monitor',
        component: () => import('@/views/equipmentManagement/iotMonitor/index.vue'),
        name: 'IoTMonitor',
        meta: { title: 'IoT监控', icon: 'monitor', noCache: true }
      }
    ]
  },
  {
    path: '/main/MobileChat',
    component: Layout,
    redirect: '',
src/views/equipmentManagement/iotMonitor/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,317 @@
<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>