From 47add25f6e7edf1b20d2fddb4919c1d97e4da294 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期四, 23 十月 2025 16:57:06 +0800
Subject: [PATCH] 新公司部署相关配置修改

---
 src/views/equipment/monitoring/equipment.vue |  304 ++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 304 insertions(+), 0 deletions(-)

diff --git a/src/views/equipment/monitoring/equipment.vue b/src/views/equipment/monitoring/equipment.vue
new file mode 100644
index 0000000..ad1a4b4
--- /dev/null
+++ b/src/views/equipment/monitoring/equipment.vue
@@ -0,0 +1,304 @@
+<template>
+  <div class="app-container">
+    <el-row :gutter="12" class="mb12">
+      <el-col :span="18">
+        <el-card class="compact-card">
+          <template #header>
+            <div class="card-header">
+              <span>璁惧瀹炴椂鐩戞祴</span>
+              <div class="header-actions">
+                <span class="mr8">鍛婅閫氶亾锛�</span>
+                <el-switch v-model="alarmChannels.platform" active-text="骞冲彴" @change="onSaveChannels" />
+                <el-switch v-model="alarmChannels.sms" class="ml8" active-text="鐭俊" @change="onSaveChannels" />
+                <el-switch v-model="alarmChannels.voice" class="ml8" active-text="璇煶" @change="onSaveChannels" />
+              </div>
+            </div>
+          </template>
+
+          <el-table :data="equipmentRows" size="small" :border="true" :height="tableHeight">
+            <el-table-column prop="name" label="璁惧" min-width="240" />
+            <el-table-column prop="location" label="浣嶇疆" min-width="200" />
+            <el-table-column label="鐘舵��" width="110">
+              <template #default="scope">
+                <el-tag :type="statusTagType(scope.row.status)">{{ renderStatus(scope.row.status) }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="娓╁害(鈩�)" min-width="140">
+              <template #default="scope">
+                <span :class="overClass(scope.row, 'temperatureC')">{{ fmt(scope.row.metrics.temperatureC) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鍘嬪姏(bar)" min-width="140">
+              <template #default="scope">
+                <span :class="overClass(scope.row, 'pressureBar')">{{ fmt(scope.row.metrics.pressureBar) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鐢垫祦(A)" min-width="140">
+              <template #default="scope">
+                <span :class="overClass(scope.row, 'currentA')">{{ fmt(scope.row.metrics.currentA) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鐢靛帇(V)" min-width="140">
+              <template #default="scope">
+                <span :class="overClass(scope.row, 'voltageV')">{{ fmt(scope.row.metrics.voltageV) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鍔熺巼鍥犳暟" min-width="160">
+              <template #default="scope">
+                <span :class="overClass(scope.row, 'powerFactor', true)">{{ fmt(scope.row.metrics.powerFactor) }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="鎺у埗" width="220">
+              <template #default="scope">
+                <el-button
+                  type="success"
+                  size="small"
+                  plain
+                  icon="VideoPlay"
+                  @click="onControl(scope.row, 'START')"
+                  v-hasPermi="['monitor:equipment:control']"
+                >鍚敤</el-button>
+                <el-button
+                  type="danger"
+                  size="small"
+                  plain
+                  icon="VideoPause"
+                  @click="onControl(scope.row, 'STOP')"
+                  v-hasPermi="['monitor:equipment:control']"
+                >鍋滄満</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-card>
+      </el-col>
+
+      <el-col :span="6">
+        <el-card class="mb12 compact-card">
+          <template #header>
+            <span>闃堝�艰缃�</span>
+          </template>
+          <el-form label-width="100px" :inline="false" size="small">
+            <el-form-item label="娓╁害涓婇檺(鈩�)">
+              <el-input-number v-model="thresholds.temperatureC" :min="20" :max="150" :step="1" />
+            </el-form-item>
+            <el-form-item label="鍘嬪姏涓婇檺(bar)">
+              <el-input-number v-model="thresholds.pressureBar" :min="0" :max="40" :step="0.5" />
+            </el-form-item>
+            <el-form-item label="鐢垫祦涓婇檺(A)">
+              <el-input-number v-model="thresholds.currentA" :min="1" :max="500" :step="1" />
+            </el-form-item>
+            <el-form-item label="鐢靛帇涓婇檺(V)">
+              <el-input-number v-model="thresholds.voltageV" :min="360" :max="500" :step="1" />
+            </el-form-item>
+            <el-form-item label="鍔熺巼鍥犳暟涓嬮檺">
+              <el-input-number v-model="thresholds.powerFactor" :min="0.5" :max="1" :step="0.01" :precision="2" />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Check" @click="onSaveThresholds">淇濆瓨</el-button>
+              <el-button type="warning" icon="Refresh" @click="onResetThresholds">閲嶇疆</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+
+        <el-card class="compact-card">
+          <template #header>
+            <span>鍛婅浜嬩欢</span>
+          </template>
+          <el-scrollbar max-height="480px">
+            <el-empty v-if="alarmEvents.length === 0" description="鏆傛棤鍛婅" />
+            <el-timeline v-else>
+              <el-timeline-item
+                v-for="(ev, idx) in alarmEvents"
+                :key="idx"
+                :timestamp="formatTs(ev.ts)"
+                :type="ev.level === 'CRITICAL' ? 'danger' : 'warning'"
+              >
+                <div class="alarm-entry">
+                  <div class="alarm-title">{{ ev.name }}锛坽{ ev.location }}锛�</div>
+                  <div class="alarm-body">
+                    <div>鎸囨爣锛歿{ renderField(ev.field) }} = {{ ev.value }}锛岄槇鍊� {{ ev.comparator }} {{ ev.threshold }}</div>
+                    <div>閫氶亾锛歿{ renderChannels(ev.channels) }}</div>
+                  </div>
+                </div>
+              </el-timeline-item>
+            </el-timeline>
+          </el-scrollbar>
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+  
+</template>
+
+<script setup>
+import { ElNotification } from 'element-plus'
+import { onMounted, onBeforeUnmount, reactive, ref, computed } from 'vue'
+import {
+  subscribeEquipmentData,
+  detectAlarms,
+  getThresholds,
+  saveThresholds,
+  getAlarmChannels,
+  saveAlarmChannels,
+  listEquipments,
+  sendControlCommand
+} from '@/api/equipment/monitoring/equipment.js'
+
+const thresholds = reactive(getThresholds())
+const alarmChannels = reactive(getAlarmChannels())
+
+const equipmentDataMap = reactive(new Map())
+const equipmentRows = computed(() => Array.from(equipmentDataMap.values()))
+
+const alarmEvents = ref([])
+let unsubscribe = null
+const tableHeight = ref(520)
+
+function updateHeights() {
+  // 棰勭暀椤堕儴瀵艰埅銆佸崱鐗囧ご銆佸唴杈硅窛绛夌┖闂达紝閬垮厤鍑虹幇澶栭儴婊氬姩鏉�
+  const reserve = 260
+  const h = window.innerHeight - reserve
+  tableHeight.value = Math.max(420, h)
+}
+
+function pushAlarm(equipmentRow, overItem) {
+  const channelUsed = { ...alarmChannels }
+  const event = {
+    ts: Date.now(),
+    equipmentId: equipmentRow.id,
+    name: equipmentRow.name,
+    location: equipmentRow.location,
+    field: overItem.field,
+    value: +overItem.value.toFixed ? +overItem.value.toFixed(2) : overItem.value,
+    threshold: overItem.threshold,
+    comparator: overItem.field === 'powerFactor' ? '<' : '>',
+    level: 'CRITICAL',
+    channels: channelUsed
+  }
+  alarmEvents.value.unshift(event)
+  if (alarmEvents.value.length > 100) alarmEvents.value.pop()
+  if (channelUsed.platform) {
+    ElNotification({ title: '璁惧鍛婅', message: `${event.name} ${renderField(event.field)} 瓒呴檺`, type: 'error', duration: 3000 })
+  }
+  if (channelUsed.sms) {
+    /* eslint-disable no-console */
+    console.log(`[SMS] 鍙戦�佽嚦鍊肩彮鍛橈細${event.name} ${renderField(event.field)} 瓒呴檺`) 
+  }
+  if (channelUsed.voice) {
+    /* eslint-disable no-console */
+    console.log(`[VOICE] IVR澶栧懠锛�${event.name} ${renderField(event.field)} 瓒呴檺`) 
+  }
+}
+
+function startStream() {
+  const baseList = listEquipments()
+  baseList.forEach(e => {
+    equipmentDataMap.set(e.id, {
+      id: e.id,
+      name: e.name,
+      location: e.location,
+      status: 'RUNNING',
+      metrics: { temperatureC: 0, pressureBar: 0, currentA: 0, voltageV: 0, powerFactor: 0 }
+    })
+  })
+  unsubscribe = subscribeEquipmentData((msg) => {
+    const row = equipmentDataMap.get(msg.equipmentId)
+    if (!row) return
+    row.status = msg.status
+    row.metrics = msg.metrics
+    // 鍛婅妫�娴�
+    const overs = detectAlarms(row.metrics, thresholds)
+    overs.forEach(item => pushAlarm(row, item))
+  }, { streaming: false })
+}
+
+function onSaveThresholds() {
+  saveThresholds({ ...thresholds })
+  ElNotification({ title: '淇濆瓨鎴愬姛', message: '闃堝�煎凡鏇存柊', type: 'success' })
+}
+
+function onResetThresholds() {
+  const latest = getThresholds()
+  Object.assign(thresholds, latest)
+}
+
+function onSaveChannels() {
+  saveAlarmChannels({ ...alarmChannels })
+}
+
+async function onControl(row, action) {
+  const res = await sendControlCommand(row.id, action)
+  if (res && res.code === 200) {
+    ElNotification({ title: '鎺у埗鎸囦护涓嬪彂', message: `${row.name} ${action === 'START' ? '鍚敤' : '鍋滄満'} 鎸囦护宸插彈鐞哷, type: 'success' })
+  }
+}
+
+function statusTagType(status) {
+  if (status === 'RUNNING') return 'success'
+  if (status === 'STOPPED') return 'info'
+  return 'warning'
+}
+
+function renderStatus(status) {
+  if (status === 'RUNNING') return '杩愯'
+  if (status === 'STOPPED') return '鍋滄満'
+  return '寮傚父'
+}
+
+function fmt(v) {
+  if (v === null || v === undefined) return '-'
+  return typeof v === 'number' ? v.toFixed(2) : v
+}
+
+function overClass(row, field, reverse = false) {
+  const v = row.metrics[field]
+  const t = thresholds[field]
+  if (v === undefined || t === undefined) return ''
+  const over = reverse ? v < t : v > t
+  return over ? 'text-danger' : ''
+}
+
+function renderField(field) {
+  switch (field) {
+    case 'temperatureC': return '娓╁害'
+    case 'pressureBar': return '鍘嬪姏'
+    case 'currentA': return '鐢垫祦'
+    case 'voltageV': return '鐢靛帇'
+    case 'powerFactor': return '鍔熺巼鍥犳暟'
+    default: return field
+  }
+}
+
+function formatTs(ts) {
+  const d = new Date(ts)
+  const p2 = (n) => n.toString().padStart(2, '0')
+  return `${d.getFullYear()}-${p2(d.getMonth()+1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}`
+}
+
+onMounted(() => {
+  startStream()
+  updateHeights()
+  window.addEventListener('resize', updateHeights)
+})
+
+onBeforeUnmount(() => {
+  if (typeof unsubscribe === 'function') unsubscribe()
+  window.removeEventListener('resize', updateHeights)
+})
+</script>
+
+<style scoped>
+.mb12 { margin-bottom: 12px; }
+.mr8 { margin-right: 8px; }
+.ml8 { margin-left: 8px; }
+.card-header { display: flex; align-items: center; justify-content: space-between; }
+.header-actions { display: flex; align-items: center; }
+.alarm-entry { line-height: 1.6; }
+.alarm-title { font-weight: 600; margin-bottom: 2px; }
+.text-danger { color: #f56c6c; font-weight: 600; }
+.compact-card :deep(.el-card__body) { padding: 12px; }
+:deep(.el-table__cell) { padding: 6px 8px; }
+:deep(.el-form-item) { margin-bottom: 8px; }
+</style>
+
+

--
Gitblit v1.9.3