Przeglądaj źródła

feat: map dashboard

Casper Dai 2 lat temu
rodzic
commit
2efed3befc

+ 7 - 1
src/router/index.js

@@ -49,12 +49,18 @@ export const asyncRoutes = [
     path: '/',
     redirect: '/home',
     component: Layout,
+    meta: { title: '首页', icon: 'home' },
     children: [
       {
         name: 'home',
         path: 'home',
         component: () => import('@/views/dashboard/index'),
-        meta: { title: '首页', icon: 'home' }
+        meta: { title: '全局概览' }
+      },
+      {
+        path: 'map',
+        component: () => import('@/views/dashboard/map/index'),
+        meta: { title: '地图位置' }
       }
     ]
   },

+ 15 - 15
src/views/dashboard/Dashboard.vue

@@ -175,6 +175,20 @@ export default {
         this.refreshDevices()
       }
     },
+    onGroupLoaded () {
+      this.loading = false
+    },
+    onGroupChanged ({ path, name }) {
+      if (!this.group || this.group.path !== path) {
+        this.group = { path, name }
+        this.refreshDevices(true)
+      }
+    },
+    onChooseDepartment () {
+      this.$refs.departmentDrawer.show().then(visible => {
+        this.loading = !visible
+      })
+    },
     onRefresh () {
       this.refreshDevices()
     },
@@ -236,20 +250,6 @@ export default {
           }
         }
       )
-    },
-    onGroupLoaded () {
-      this.loading = false
-    },
-    onGroupChanged ({ path, name }) {
-      if (!this.group || this.group.path !== path) {
-        this.group = { path, name }
-        this.refreshDevices(true)
-      }
-    },
-    onChooseDepartment () {
-      this.$refs.departmentDrawer.show().then(visible => {
-        this.loading = !visible
-      })
     }
   }
 }
@@ -263,7 +263,7 @@ export default {
   background-color: #fff;
 
   &__title {
-    max-width: 200px;
+    width: 200px;
   }
 
   &__item > div:first-child {

+ 14 - 0
src/views/dashboard/map/assets/icon_position1.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="19px" viewBox="0 0 14 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_position备份 2</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="大屏设备监控系统-单屏" transform="translate(-300.000000, -661.000000)">
+            <g id="地图位置" transform="translate(80.000000, 437.000000)">
+                <g id="icon_position备份-2" transform="translate(220.000000, 224.000000)">
+                    <ellipse id="椭圆形" fill="#333333" cx="7" cy="17" rx="4" ry="2"></ellipse>
+                    <path d="M11.9497475,2.05025253 C14.6212886,4.72179371 14.6820055,9.01549316 12.131898,11.7607688 L11.9497475,11.9497475 L7.70710678,16.1923882 C7.31658249,16.5829124 6.68341751,16.5829124 6.29289322,16.1923882 L2.05025253,11.9497475 L2.05025253,11.9497475 C-0.683417511,9.21607743 -0.683417511,4.78392257 2.05025253,2.05025253 C4.78392257,-0.683417511 9.21607743,-0.683417511 11.9497475,2.05025253 Z" id="路径" fill="#04A681"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 14 - 0
src/views/dashboard/map/assets/icon_position2.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="19px" viewBox="0 0 14 19" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>icon_position备份 4</title>
+    <g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="大屏设备监控系统-单屏" transform="translate(-314.000000, -552.000000)">
+            <g id="地图位置" transform="translate(80.000000, 437.000000)">
+                <g id="icon_position备份-4" transform="translate(234.000000, 115.000000)">
+                    <ellipse id="椭圆形" fill="#333333" cx="7" cy="17" rx="4" ry="2"></ellipse>
+                    <path d="M11.9497475,2.05025253 C14.6212886,4.72179371 14.6820055,9.01549316 12.131898,11.7607688 L11.9497475,11.9497475 L7.70710678,16.1923882 C7.31658249,16.5829124 6.68341751,16.5829124 6.29289322,16.1923882 L2.05025253,11.9497475 L2.05025253,11.9497475 C-0.683417511,9.21607743 -0.683417511,4.78392257 2.05025253,2.05025253 C4.78392257,-0.683417511 9.21607743,-0.683417511 11.9497475,2.05025253 Z" id="路径" fill="#F90C0C"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 368 - 0
src/views/dashboard/map/index.vue

@@ -0,0 +1,368 @@
+<template>
+  <wrapper
+    fill
+    margin
+    horizontal
+  >
+    <div class="l-flex__fill l-flex--col c-sibling-item c-device-map">
+      <div class="l-flex__none l-flex--row c-count">
+        <div
+          class="l-flex__none c-count__title u-color--black u-bold has-active u-ellipsis"
+          @click="onChooseDepartment"
+        >
+          <i
+            v-if="loading"
+            class="el-icon-loading"
+          />
+          {{ group.name }}
+        </div>
+        <div class="l-flex__none c-count__item u-color--black u-bold u-text--center">
+          <div>总数</div>
+          <i
+            v-if="monitor.loading"
+            class="el-icon-loading"
+          />
+          <div v-else>{{ monitor.total }}</div>
+        </div>
+        <div class="l-flex__none c-count__item u-color--success dark u-bold u-text--center">
+          <div>● 在线</div>
+          <i
+            v-if="monitor.loading"
+            class="el-icon-loading"
+          />
+          <div v-else>{{ monitor.online }}</div>
+        </div>
+        <div class="l-flex__none c-count__item u-color--error dark u-bold u-text--center">
+          <div>● 离线</div>
+          <i
+            v-if="monitor.loading"
+            class="el-icon-loading"
+          />
+          <div v-else>{{ monitor.offline }}</div>
+        </div>
+        <div class="l-flex__none c-count__item u-color--info u-bold u-text--center">
+          <div>● 未启用</div>
+          <i
+            v-if="monitor.loading"
+            class="el-icon-loading"
+          />
+          <div v-else>{{ monitor.inactive }}</div>
+        </div>
+        <i
+          class="el-icon-refresh o-icon md u-color--blue has-active"
+          @click="onRefresh"
+        />
+      </div>
+      <div
+        ref="map"
+        class="l-flex__fill u-relative"
+      >
+        <i
+          v-if="deviceOptions.loaded"
+          class="o-place el-icon-place has-active"
+          @click="onPlace"
+        />
+        <device
+          v-if="device"
+          :key="device.id"
+          class="c-device-map__device"
+          :device="device"
+        />
+      </div>
+    </div>
+    <!-- <div
+      ref="devicelist"
+      v-loading="!deviceOptions.loaded"
+      class="l-flex__none l-flex--col c-sibling-item u-width--lg u-overflow-y--auto"
+    >
+      <device
+        v-for="item in deviceOptions.list"
+        :key="item.id"
+        class="c-sibling-item--v"
+        :device="item"
+      />
+    </div> -->
+    <department-drawer
+      ref="departmentDrawer"
+      @change="onGroupChanged"
+      @loaded="onGroupLoaded"
+    />
+  </wrapper>
+</template>
+
+<script>
+import AMapLoader from '@amap/amap-jsapi-loader'
+import {
+  subscribe,
+  unsubscribe
+} from '@/utils/mqtt'
+import {
+  getDevicesByQuery,
+  getDeviceStatisticsByPath
+} from '@/api/device'
+import Device from '../components/Device.vue'
+
+const onlineIcon = require('./assets/icon_position1.svg')
+const offlineIcon = require('./assets/icon_position2.svg')
+
+export default {
+  name: 'DeviceMap',
+  components: {
+    Device
+  },
+  data () {
+    return {
+      monitor: { loading: true },
+      deviceOptions: {
+        list: [],
+        loaded: false
+      },
+      loading: true,
+      group: {},
+      device: null
+    }
+  },
+  computed: {
+    mapDevices () {
+      return this.deviceOptions.list.filter(i => i.longitude && i.latitude)
+    }
+  },
+  created () {
+    this.$timer = -1
+    subscribe([
+      '+/+/online',
+      '+/+/offline',
+      '+/+/calendar/update'
+    ], this.onMessage)
+  },
+  beforeDestroy () {
+    clearTimeout(this.$timer)
+    this.monitor = { loading: true }
+    unsubscribe([
+      '+/+/online',
+      '+/+/offline',
+      '+/+/calendar/update'
+    ], this.onMessage)
+    this.map?.destroy()
+  },
+  methods: {
+    onMessage (topic) {
+      if (!this.deviceOptions.loaded) {
+        return
+      }
+      const result = /^\d+\/(\d+)\/(online|offline)$/.exec(topic)
+      if (!result) {
+        return
+      }
+      const deviceId = result[1]
+      const status = result[2]
+      const device = this.deviceOptions.list.find(({ id }) => id === deviceId)
+      if (device) {
+        const onlineStatus = device.onlineStatus === 1 ? 'online' : 'offline'
+        if (status === onlineStatus) {
+          return
+        }
+        this.refreshDevices()
+      }
+    },
+    onGroupLoaded () {
+      this.loading = false
+    },
+    onGroupChanged ({ path, name }) {
+      if (!this.group || this.group.path !== path) {
+        this.group = { path, name }
+        this.refreshDevices(true)
+      }
+    },
+    onChooseDepartment () {
+      this.$refs.departmentDrawer.show().then(visible => {
+        this.loading = !visible
+      })
+    },
+    onRefresh () {
+      this.refreshDevices()
+    },
+    refreshDevices (force) {
+      if (!force && this.monitor.loading) {
+        return
+      }
+      const monitor = {
+        loading: true,
+        total: '-',
+        online: '-',
+        offline: '-',
+        inactive: '-'
+      }
+      this.device = null
+      this.map?.destroy()
+      this.map = null
+      this.monitor = monitor
+      clearTimeout(this.$timer)
+      this.deviceOptions = { loaded: false }
+      getDeviceStatisticsByPath(this.group.path).then(({ data }) => {
+        const { deactivatedTotal, notConnectedTotal, offLineTotal, onLineTotal, total } = data
+        monitor.total = total
+        monitor.online = onLineTotal
+        monitor.offline = offLineTotal + notConnectedTotal
+        monitor.inactive = deactivatedTotal
+      }).finally(() => {
+        monitor.loading = false
+        if (!this.monitor.loading) {
+          this.getDevices(this.monitor.total - this.monitor.inactive)
+        }
+      })
+    },
+    sort (a, b) {
+      if (a.onlineStatus === b.onlineStatus) {
+        return a.createTime <= b.createTime ? 1 : -1
+      }
+      return a.onlineStatus === 1 ? -1 : 1
+    },
+    getDevices (total) {
+      if (!total || total === '-') {
+        this.deviceOptions = { list: [], loaded: true }
+        return
+      }
+      const options = { list: [], loaded: false }
+      this.deviceOptions = options
+      getDevicesByQuery({
+        pageNum: 1,
+        pageSize: total,
+        activate: 1,
+        org: this.group.path
+      }, { custom: true }).then(
+        ({ data }) => {
+          options.list = data.sort(this.sort)
+          options.loaded = true
+          this.initMap()
+        },
+        ({ isCancel }) => {
+          if (!isCancel && !this.monitor.loading) {
+            this.$timer = setTimeout(total => {
+              this.getDevices(total)
+            }, 2000, total)
+          }
+        }
+      )
+    },
+    onDeviceChange (device) {
+      if (!device) {
+        this.map.setFitView()
+        return
+      }
+      this.activeDevice(device.id, [device.marker])
+    },
+    initMap () {
+      AMapLoader.load({
+        key: process.env.VUE_APP_GAODE_MAP_KEY,
+        version: '2.0',
+        plugins: ['']
+      }).then(AMap => {
+        this.map = new AMap.Map(this.$refs.map)
+        const markerList = []
+        for (const device of this.mapDevices) {
+          const marker = new AMap.Marker({
+            position: [+device.longitude, +device.latitude],
+            icon: new AMap.Icon({
+              image: device.onlineStatus === 1 ? onlineIcon : offlineIcon,
+              size: new AMap.Size(32, 32),
+              imageSize: new AMap.Size(32, 32)
+            }),
+            label: {
+              content: `<div class='o-online-status--${device.onlineStatus === 1 ? 'online' : 'offline'}'>${device.name}</div>`,
+              direction: 'top',
+              offset: new AMap.Pixel(0, -10) // 设置文本标注偏移量
+            },
+            extData: {
+              id: device.id
+            }
+          })
+          marker.on('click', e => {
+            this.activeDevice(e.target.getExtData().id, e.target)
+          })
+          device.marker = marker
+          markerList.unshift(marker)
+        }
+        this.map.add(markerList)
+        this.map.setFitView()
+        this.map.on('click', () => {
+          this.device = null
+        })
+      })
+    },
+    onPlace () {
+      this.map.setFitView()
+    },
+    activeDevice (id, marker) {
+      this.map.setFitView(marker)
+      this.device = this.mapDevices.find(device => device.id === id)
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+@mixin markLabel {
+  color: #ffffff;
+  border: none;
+  padding: 5px 10px;
+}
+
+.c-device-map {
+  border-radius: $radius $radius 0 0;
+  background-color: #fff;
+
+  &__device {
+    position: absolute;
+    top: $spacing;
+    left: $spacing;
+    width: $width--lg;
+    z-index: 9;
+  }
+
+  ::v-deep {
+    .o-online-status {
+      &--online {
+        @include markLabel();
+        background-color: #333333;
+      }
+      &--offline {
+        @include markLabel();
+        background-color: #f90c0c;
+      }
+    }
+
+    .amap-marker-label {
+      border: none;
+      padding: 0;
+    }
+  }
+}
+
+.c-count {
+  justify-content: space-between;
+  padding: $spacing--xs $spacing;
+
+  &__title {
+    width: 200px;
+  }
+
+  &__item > div:first-child {
+    margin-bottom: 10px;
+  }
+}
+
+.o-place {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  right: $spacing;
+  bottom: $spacing;
+  width: 48px;
+  height: 48px;
+  font-size: 24px;
+  border-radius: 50%;
+  background-color: #fff;
+  z-index: 9;
+}
+</style>