HMY 9 месяцев назад
Родитель
Сommit
03be5a53dc

+ 1 - 0
ui/package.json

@@ -34,6 +34,7 @@
     "splitpanes": "3.1.5",
     "vue": "3.4.31",
     "vue-cropper": "1.1.1",
+    "vue-echarts": "^7.0.3",
     "vue-router": "4.4.0",
     "vuedraggable": "4.1.0"
   },

+ 16 - 0
ui/src/api/hnyz/alarmEvent.js

@@ -42,3 +42,19 @@ export function delAlarmEvent(alarmId) {
     method: 'delete'
   })
 }
+
+// 最新n条告警信息
+export function getLatestAlarmEvent(num) {
+  return request({
+    url: '/hnyz/alarmEvent/latest/' + num,
+    method: 'get'
+  })
+}
+
+//近七天的告警信息统计
+export function getAlarmEventWeeklyCount() {
+  return request({
+    url: '/hnyz/alarmEvent/weeklyCount',  
+    method: 'get'
+  })
+}

+ 8 - 0
ui/src/api/hnyz/equipment.js

@@ -62,4 +62,12 @@ export function changeStatus(equipmentId, status) {
       status: status
     }
   })
+}
+
+//获取首页顶部区域的统计数据
+export function getDashboardStats() {
+  return request({
+    url: '/hnyz/equipment/getDashboardStats',
+    method: 'get',
+  })
 }

+ 8 - 0
ui/src/api/monitor/logininfor.js

@@ -32,3 +32,11 @@ export function cleanLogininfor() {
     method: 'delete'
   })
 }
+
+// 获取最近运行日志
+export function getLatestRunLogs(count = 5) {
+  return request({
+    url: `/monitor/logininfor/latest?count=${count}`,
+    method: 'get'
+  })
+}

+ 11 - 3
ui/src/assets/dcs/iconfont/iconfont.css

@@ -1,8 +1,8 @@
 @font-face {
   font-family: "iconfont"; /* Project id 4857906 */
-  src: url('iconfont.woff2?t=1749286315155') format('woff2'),
-       url('iconfont.woff?t=1749286315155') format('woff'),
-       url('iconfont.ttf?t=1749286315155') format('truetype');
+  src: url('iconfont.woff2?t=1751435086840') format('woff2'),
+       url('iconfont.woff?t=1751435086840') format('woff'),
+       url('iconfont.ttf?t=1751435086840') format('truetype');
 }
 
 .iconfont {
@@ -13,6 +13,14 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-TrendUp:before {
+  content: "\e602";
+}
+
+.icon-TrendDown:before {
+  content: "\e70c";
+}
+
 .icon-leftArrow:before {
   content: "\e62e";
 }

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
ui/src/assets/dcs/iconfont/iconfont.js


+ 14 - 0
ui/src/assets/dcs/iconfont/iconfont.json

@@ -5,6 +5,20 @@
   "css_prefix_text": "icon-",
   "description": "",
   "glyphs": [
+    {
+      "icon_id": "14376541",
+      "name": "上升趋势",
+      "font_class": "TrendUp",
+      "unicode": "e602",
+      "unicode_decimal": 58882
+    },
+    {
+      "icon_id": "30053477",
+      "name": "趋势下降",
+      "font_class": "TrendDown",
+      "unicode": "e70c",
+      "unicode_decimal": 59148
+    },
     {
       "icon_id": "44203903",
       "name": "向左",

BIN
ui/src/assets/dcs/iconfont/iconfont.ttf


BIN
ui/src/assets/dcs/iconfont/iconfont.woff


BIN
ui/src/assets/dcs/iconfont/iconfont.woff2


+ 84 - 0
ui/src/components/HnyzDcs/AlarmBarChartComponent.vue

@@ -0,0 +1,84 @@
+<template>
+    <el-skeleton :loading="loading" animated>
+        <template #default>
+            <div style="height: 235px;">
+                <v-chart class="h-full" :option="option" />
+            </div>
+        </template>
+    </el-skeleton>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { use } from 'echarts/core'
+import VChart from 'vue-echarts'
+
+import {
+    BarChart
+} from 'echarts/charts'
+
+import {
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+} from 'echarts/components'
+
+import {
+    CanvasRenderer
+} from 'echarts/renderers'
+
+import { getAlarmEventWeeklyCount } from '@/api/hnyz/alarmEvent'
+use([
+    CanvasRenderer,
+    BarChart,
+    TitleComponent,
+    TooltipComponent,
+    GridComponent,
+    LegendComponent
+])
+const loading = ref(true)
+const option = ref({})
+
+const { proxy } = getCurrentInstance()
+const { alarm_type } = proxy.useDict('alarm_type')
+
+onMounted(() => {
+    getAlarmEventWeeklyCount().then(res => {
+        if (res.code === 200) {
+            const xAxisLabels = alarm_type.value.map(item => item.label)
+            const valueMap = Object.fromEntries(res.data.map(item => [item.alarm_type, item.alarmCount]))
+            const barData = alarm_type.value.map(item => valueMap[item.value] || 0)
+
+            option.value = {
+                title: { text: '近期告警统计', left: 'center' },
+                tooltip: {},
+                xAxis: {
+                    type: 'category',
+                    data: xAxisLabels
+                },
+                yAxis: { type: 'value' },
+                series: [
+                    {
+                        name: '告警次数',
+                        type: 'bar',
+                        data: barData,
+                        itemStyle: {
+                            color: '#f56c6c'
+                        }
+                    }
+                ]
+            }
+            loading.value = false
+        }
+    })
+})
+
+</script>
+
+<style scoped lang="scss">
+.stat-card {
+    display: flex;
+    align-items: center;
+}
+</style>

+ 377 - 45
ui/src/views/index.vue

@@ -1,73 +1,405 @@
 <template>
-  <div class="app-container home">
+  <div class="dashboard_container">
+    <!-- 顶部区域:统计卡片 + 右上角按钮 -->
+    <div class="dashboard_header">
+      <div class="stats_area">
+        <el-row :gutter="20" class="dashboard_section">
+          <el-col :span="6" v-for="(item, index) in stats" :key="index">
+            <el-card shadow="hover" :class="['stat_card', item.bgClass]">
+              <div class="stat_content">
+                <el-icon class="stat_bg_icon">
+                  <component :is="item.icon" />
+                </el-icon>
+                <div class="stat_text">
+                  <div class="stat_number">{{ item.value }}</div>
+                  <div class="stat_label">{{ item.label }}</div>
+                </div>
+              </div>
+              <div class="stat_trend">
+                <i :class="[
+                  'iconfont',
+                  item.trend >= 0 ? 'icon-TrendUp' : 'icon-TrendDown',
+                  item.trend >= 0 ? 'text_success' : 'text_danger',
+                ]"></i>
+                <span :class="item.trend >= 0 ? 'text_success' : 'text_danger'">
+                  {{ item.trend >= 0 ? '+' : '' }}{{ item.trend }}
+                </span>
+                <span class="stat_trend_note">
+                  {{ item.trend > 0 ? '比昨日增长' : item.trend < 0 ? '较昨日下降' : '持平' }} </span>
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+      </div>
+      <!-- 右上角按钮 -->
+      <div class="button_area">
+        <el-card shadow="hover" class="button_card" header="快速导航">
+          <div class="rightTop_btn">
+            <el-button type="primary" class="nav_button" @click="toControl">上海控制页面</el-button>
+            <el-button type="success" class="nav_button" @click="toConfig">上海组态页面</el-button>
+            <el-button type="success" class="nav_button" @click="toConfig">湖南组态页面</el-button>
+          </div>
+        </el-card>
+      </div>
+    </div>
+
+    <el-row :gutter="20" class="dashboard_section">
+      <el-col :span="16">
+        <el-card shadow="hover" header="设备信息" class="card_block card_block_center">
+          <div class="equipment_info_container">
+            <div class="image_area">
+              <el-image :src="homeBg" fit="contain" style="width: 100%; height: 200px" />
+            </div>
+            <div class="info_area">
+              <el-descriptions :column="2" border>
+                <el-descriptions-item label="设备名称">{{ selectedDevice.equipmentName }}</el-descriptions-item>
+                <el-descriptions-item label="标识码">{{ selectedDevice.code }}</el-descriptions-item>
+                <el-descriptions-item label="设备种类">{{ selectedDevice.equipmentType }}</el-descriptions-item>
+                <el-descriptions-item label="绑定流程">{{ selectedDevice.flowName }}</el-descriptions-item>
+                <el-descriptions-item label="通讯协议">{{ selectedDevice.protocolName }}</el-descriptions-item>
+                <el-descriptions-item label="PLC">{{ selectedDevice.plcName }}</el-descriptions-item>
+                <el-descriptions-item label="设备全称">{{ selectedDevice.title || '暂无' }}</el-descriptions-item>
+                <el-descriptions-item label="状态">
+                  <el-tag :type="selectedDevice.status === '0' ? 'success' : 'danger'">
+                    {{ selectedDevice.status === '0' ? '启用' : '禁用' }}
+                  </el-tag>
+                </el-descriptions-item>
+              </el-descriptions>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+
+
+      <el-col :span="8">
+        <el-card shadow="hover" header="告警柱状图" class="card_block card_block_center">
+          <AlarmBarChart />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 底部公告、日志 -->
+    <el-row :gutter="20" class="dashboard_section">
+      <el-col :span="8">
+        <el-card shadow="hover" header="系统公告" class="card_block ">
+          <el-carousel :interval="3000" type="card" height="130px">
+            <el-carousel-item v-for="(item, i) in announcements" :key="i">
+              <div class="text_center">{{ item }}</div>
+            </el-carousel-item>
+          </el-carousel>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover" header="最新告警" class="card_block card_block_bottom">
+          <el-timeline>
+            <el-timeline-item v-for="(log, index) in alerts" :key="index" :timestamp="log.updateTime" type="danger">
+              {{ log.message }}
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+      </el-col>
+
+      <el-col :span="8">
+        <el-card shadow="hover" header="运行日志" class="card_block card_block_bottom">
+          <el-timeline>
+            <el-timeline-item v-for="(log, index) in logs" :key="index" :timestamp="log.time" color="#0bbd87">
+              {{ log.message }}
+            </el-timeline-item>
+          </el-timeline>
+        </el-card>
+      </el-col>
+    </el-row>
   </div>
 </template>
 
-<script setup name="Index">
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import AlarmBarChart from '@/components/HnyzDcs/AlarmBarChartComponent.vue'
+import homeBg from '@/assets/dcs/homeBg.svg'
+import { listEquipment, getDashboardStats } from "@/api/hnyz/equipment";
+import { getLatestAlarmEvent } from "@/api/hnyz/alarmEvent";
+import { getLatestRunLogs } from '@/api/monitor/logininfor';
+import { Cpu, Printer, Finished, Warning } from '@element-plus/icons-vue'
+const route = useRoute()
+const router = useRouter()
+//设备id
+const iconMap = {
+  cpu: Cpu,
+  printer: Printer,
+  finished: Finished,
+  warning: Warning
+}
+const staticStatsConfig = [
+  { key: 'device', label: '设备总数', icon: 'cpu', bgClass: 'bg_blue' },
+  { key: 'register', label: '寄存器总数', icon: 'printer', bgClass: 'bg_green' },
+  { key: 'flow', label: '流程总数', icon: 'finished', bgClass: 'bg_orange' },
+  { key: 'alarm', label: '今日告警信息数', icon: 'warning', bgClass: 'bg_red' }
+]
+
+const selectedDevice = ref({})
+
+const stats = ref([])
+
+onMounted(async () => {
+  // 获取顶部区域 统计数据
+  const res = await getDashboardStats()
+  if (res.code === 200) {
+    stats.value = staticStatsConfig.map(item => {
+      const stat = res.data[item.key] || { value: 0, trend: 0 }
+      return {
+        label: item.label,
+        icon: iconMap[item.icon],
+        bgClass: item.bgClass,
+        value: stat.value,
+        trend: stat.trend,
+      }
+    })
+  }
+  getDeviceInfo()// 获取设备信息
+  getLatestAlerts(5)// 获取最新告警信息
+  getLatestLogs(5)// 获取最新运行日志
+})
+
+// 获取设备信息
+function getDeviceInfo() {
+  const queryParams = {
+    code: route.query.code || 'D1'
+  }
+  listEquipment(queryParams).then(response => {
+    const equipmentInfo = response.rows[0];
+    selectedDevice.value = equipmentInfo;
+  });
+}
+const alerts = ref([])
+// 获取最新告警信息
+function getLatestAlerts(num) {
+  getLatestAlarmEvent(num || 5).then(response => {
+    alerts.value = response.data;
+    console.log(alerts.value)
+  });
+}
+
+const announcements = ['欢迎使用工业监控系统', '系统将于今晚12点维护', '新功能上线:告警邮件通知']
+
 
+// const logs = [
+//   { time: '10:00:01', message: '系统启动完成' },
+//   { time: '10:05:23', message: '用户 admin 登录成功' },
+// ]
+const logs = ref([])
+// 获取最新运行日志
+function getLatestLogs(num) {
+  getLatestRunLogs(num || 5).then(response => {
+    logs.value = response.data;
+  });
+}
+// 按钮跳转
+function toControl() {
+  // 跳转控制页面
+  router.push('/flowSelect')
+}
+function toConfig() {
+  // 跳转组态页面
+  router.push('/m1sj')
+}
 </script>
 
 <style scoped lang="scss">
-.home {
-  blockquote {
-    padding: 10px 20px;
-    margin: 0 0 20px;
-    font-size: 17.5px;
-    border-left: 5px solid #eee;
-  }
-  hr {
-    margin-top: 20px;
+.dashboard_container {
+  background-color: #f5f7fa;
+  padding: 20px;
+  min-height: 100vh;
+  box-sizing: border-box;
+  overflow-x: hidden;
+
+  .dashboard_header {
+    display: flex;
+    justify-content: space-between;
     margin-bottom: 20px;
-    border: 0;
-    border-top: 1px solid #eee;
+
+    .stats_area {
+      width: 75%;
+    }
+
+    .button_area {
+      width: 23%;
+      min-width: 180px;
+
+      .button_card {
+        // height: 100%;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        border-radius: 12px;
+
+        .rightTop_btn {
+          display: flex;
+          flex-wrap: wrap;
+          gap: 12px;
+        }
+
+        .nav_button {
+          // margin-bottom: 16px;
+          font-weight: 600;
+          font-size: 16px;
+          transition: all 0.3s ease;
+          border-radius: 6px;
+          margin: 0;
+
+          // &:last-child {
+          //   margin-bottom: 0;
+          // }
+
+          &:hover {
+            transform: scale(1.05);
+          }
+        }
+      }
+    }
   }
-  .col-item {
+
+  .dashboard_section {
     margin-bottom: 20px;
   }
 
-  ul {
-    padding: 0;
-    margin: 0;
-  }
+  .stat_card {
+    position: relative;
+    border-radius: 12px;
+    padding: 16px;
+    color: #333;
+    min-height: 110px;
+    overflow: hidden;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
 
-  font-family: "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
-  font-size: 13px;
-  color: #676a6c;
-  overflow-x: hidden;
+    .stat_content {
+      display: flex;
+      flex-direction: column;
+      z-index: 2;
 
-  ul {
-    list-style-type: none;
-  }
+      .stat_bg_icon {
+        position: absolute;
+        right: 12px;
+        bottom: 12px;
+        font-size: 60px;
+        opacity: 0.08;
+        z-index: 1;
+      }
+
+      .stat_text {
+        z-index: 2;
+
+        .stat_number {
+          font-size: 26px;
+          font-weight: 700;
+          margin-bottom: 4px;
+        }
+
+        .stat_label {
+          font-size: 14px;
+          color: #666;
+        }
+      }
+    }
 
-  h4 {
-    margin-top: 0px;
+    .stat_trend {
+      display: flex;
+      align-items: center;
+      font-size: 13px;
+      margin-top: 8px;
+      z-index: 2;
+
+      .stat_trend_note {
+        margin-left: 6px;
+        font-size: 12px;
+        color: #999;
+      }
+    }
   }
 
-  h2 {
-    margin-top: 10px;
-    font-size: 26px;
-    font-weight: 100;
+  .card_block {
+    border-radius: 10px;
   }
 
-  p {
-    margin-top: 10px;
+  .card_block_center {
+    height: 320px;
 
-    b {
-      font-weight: 700;
+    ::v-deep(.el-card__body) {
+      height: calc(100% - 48px); // 减去 header 高度(大概 48px)
+      overflow-y: auto;
+      padding-top: 10px;
     }
+
+    .equipment_info_container {
+      display: flex;
+      gap: 20px;
+      align-items: center;
+      padding: 10px;
+
+      .image_area {
+        flex: 1;
+      }
+
+      .info_area {
+        flex: 2;
+        overflow: auto;
+        max-height: 240px;
+      }
+    }
+
   }
 
-  .update-log {
-    ol {
-      display: block;
-      list-style-type: decimal;
-      margin-block-start: 1em;
-      margin-block-end: 1em;
-      margin-inline-start: 0;
-      margin-inline-end: 0;
-      padding-inline-start: 40px;
+  .card_block_bottom {
+    height: 250px;
+
+    ::v-deep(.el-card__body) {
+      height: calc(100% - 48px); // 减去 header 高度(大概 48px)
+      overflow-y: auto;
+      padding-top: 10px;
     }
   }
+
+
+  .device_info {
+    margin-top: 12px;
+  }
+
+  .text_hint {
+    color: #aaa;
+    text-align: center;
+    padding: 10px;
+  }
+
+  .text_center {
+    text-align: center;
+  }
+
+  .text_success {
+    color: #67c23a;
+  }
+
+  .text_danger {
+    color: #f56c6c;
+  }
+
+  /* 卡片背景配色 */
+  .bg_blue {
+    background-color: #e6f7ff;
+  }
+
+  .bg_green {
+    background-color: #f0f9eb;
+  }
+
+  .bg_orange {
+    background-color: #fff7e6;
+  }
+
+  .bg_red {
+    background-color: #fff0f0;
+  }
 }
 </style>
-

Некоторые файлы не были показаны из-за большого количества измененных файлов