Переглянути джерело

feat(dashboard): block diagram

Casper Dai 2 роки тому
батько
коміт
12fd4024f6

+ 234 - 0
src/views/dashboard/components/DeviceCardRect.vue

@@ -0,0 +1,234 @@
+<template>
+  <el-popover
+    placement="top"
+    width="320"
+    trigger="click"
+  >
+    <div>
+      <div
+        class="l-flex--row c-sibling-item--v u-bold has-active"
+        @click="onClick"
+      >
+        <div class="l-flex__fill c-sibling-item">{{ name }}</div>
+        <i class="c-sibling-item--v el-icon-arrow-right" />
+      </div>
+      <div class="l-flex--row c-sibling-item--v near u-color--blue">
+        <i class="l-flex__none c-sibling-item el-icon-location-outline u-font-size" />
+        <auto-text
+          class="l-flex__auto c-sibling-item nearest u-font-size--xs u-bold"
+          :text="address"
+        />
+        <div
+          v-if="isOnline && volume > -1"
+          class="l-flex__none o-device__volume u-color--white u-font-size--sm has-active"
+          @click.stop="onVolume"
+        >
+          <template v-if="volume === 0">
+            <svg-icon icon-class="mute" />
+          </template>
+          <template v-else>
+            <svg-icon
+              class="c-sibling-item"
+              icon-class="volume"
+            />
+            <span class="c-sibling-item nearest">{{ volumeTip }}</span>
+          </template>
+        </div>
+      </div>
+      <div
+        v-if="statusTip"
+        class="c-sibling-item--v"
+      >
+        {{ statusTip }}
+      </div>
+    </div>
+    <template #reference>
+      <div
+        class="o-device u-pointer"
+        :class="statusClass"
+      >
+        <i
+          v-if="!hasStatus"
+          class="el-icon-loading u-color--white"
+        />
+        <i
+          v-if="isOnline && hasStatus"
+          class="o-device__status"
+          :class="switchStatusClass"
+        />
+      </div>
+    </template>
+  </el-popover>
+</template>
+
+<script>
+import { ThirdPartyDevice } from '@/constant'
+import { parseTime } from '@/utils'
+import {
+  Status,
+  Power,
+  addListener,
+  removeListener
+} from '@/utils/adapter'
+import { parseVolume } from '@/utils/control/volume'
+
+export default {
+  name: 'DeviceCardRect',
+  props: {
+    device: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      powerStatus: Status.LOADING,
+      switchStatus: Power.LOADING,
+      hasPower: true,
+      hasPowerRealStatus: false,
+      volume: -1
+    }
+  },
+  computed: {
+    name () {
+      return this.device.name
+    },
+    address () {
+      return this.device.address
+    },
+    isOnline () {
+      return this.device.onlineStatus === 1
+    },
+    hasStatus () {
+      return !this.isOnline || !this.hasPower || this.hasPowerRealStatus
+    },
+    isPowerOpened () {
+      return !this.hasPower || this.hasPowerRealStatus && (this.powerStatus === Status.OK && this.switchStatus !== Power.OFF)
+    },
+    switchStatusClass () {
+      if (this.powerStatus === Status.WARNING) {
+        return ''
+      }
+      return this.switchStatus === Power.ON ? 'on' : this.switchStatus === Power.OFF ? 'off' : 'other'
+    },
+    statusClass () {
+      if (this.hasStatus) {
+        return this.isOnline
+          ? this.hasPower
+            ? this.powerStatus === Status.WARNING
+              ? 'u-color--warning'
+              : 'u-color--success dark'
+            : 'u-color--success dark'
+          : 'u-color--error dark'
+      }
+      return 'u-color--info light'
+    },
+    statusTip () {
+      return this.hasPowerRealStatus
+        ? this.powerStatus === Status.WARNING
+          ? `电源状态异常,${this.switchStatus === Power.LOADING ? '检测' : '最后上报'}时间 ${this.timestamp}`
+          : this.isPowerOpened
+            ? '屏幕已开启'
+            : '屏幕未开启'
+        : this.device.lastOnline
+          ? this.isOnline
+            ? `${this.device.lastOnline} 上线`
+            : `${this.device.lastOnline} 离线`
+          : ''
+    },
+    volumeTip () {
+      if (this.volume > -1) {
+        return parseVolume(this.volume)
+      }
+      return ''
+    }
+  },
+  watch: {
+    isOnline: {
+      handler (val, old) {
+        if (val) {
+          addListener(this.device.id, this.onMessage)
+        } else {
+          if (old == null) {
+            return
+          }
+          removeListener(this.device.id, this.onMessage)
+        }
+      },
+      immediate: true
+    }
+  },
+  beforeDestroy () {
+    if (this.isOnline) {
+      removeListener(this.device.id, this.onMessage)
+    }
+  },
+  methods: {
+    onMessage (value) {
+      if (value.screen) {
+        this.volume = value.screen.volume
+      }
+      const multiCard = value[ThirdPartyDevice.MULTI_FUNCTION_CARD]
+      const powerStatus = multiCard.status
+      this.powerStatus = powerStatus
+      this.timestamp = multiCard.timestamp ? parseTime(multiCard.timestamp, '{y}-{m}-{d} {h}:{i}:{s}') : ''
+      this.hasPower = powerStatus > Status.NONE
+      this.hasPowerRealStatus = powerStatus !== Status.LOADING
+      this.switchStatus = multiCard.switchStatus
+    },
+    onClick () {
+      this.$router.push({
+        name: 'device-detail',
+        params: { id: this.device.id }
+      })
+    },
+    onVolume () {
+      const { id, productId, name } = this.device
+      this.$emit('volume', {
+        value: this.volume,
+        device: { id, productId, name }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.o-device {
+  display: inline-flex;
+  justify-content: center;
+  align-items: center;
+  width: 48px;
+  height: 48px;
+  vertical-align: top;
+  background-color: currentColor;
+
+  &__status {
+    display: inline-flex;
+    justify-content: center;
+    align-items: center;
+    width: 24px;
+    height: 24px;
+
+    &.on {
+      background: url("~@/assets/icon_on.svg") 0 0 / 100% 100% no-repeat;
+    }
+
+    &.off {
+      background: url("~@/assets/icon_off.svg") 0 0 / 100% 100% no-repeat;
+    }
+
+    &.other {
+      width: 40px;
+      background: url("~@/assets/icon_on_2.svg") 0 0 / 100% 100% no-repeat;
+    }
+  }
+
+  &__volume {
+    display: inline-block;
+    padding: $padding--2xs;
+    border-radius: $radius--sm;
+    background-color: $gray--dark;
+  }
+}
+</style>

+ 7 - 0
src/views/dashboard/components/DeviceGroupLevel.vue

@@ -38,6 +38,10 @@ export default {
     path: {
       type: String,
       required: true
+    },
+    total: {
+      type: Number,
+      default: -1
     }
   },
   computed: {
@@ -48,6 +52,9 @@ export default {
   watch: {
     path () {
       this.refreshGroups(true)
+    },
+    total () {
+      this.refreshGroups(true)
     }
   },
   methods: {

+ 134 - 0
src/views/dashboard/components/DeviceGroupRect.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="c-device-group-rect u-overflow-y--auto">
+    <div
+      v-if="hasData"
+      class="c-device-group-rect__content"
+    >
+      <device-card-rect
+        v-for="item in groupOptions.list"
+        :key="item.id"
+        :device="item"
+        @volume="onVolume"
+      />
+      <volume-dialog ref="volumeDialog" />
+    </div>
+    <div
+      v-else-if="groupOptions.loading"
+      class="has-padding--v u-text--center"
+    >
+      <i class="el-icon-loading u-font-size--lg" />
+    </div>
+    <el-empty v-else />
+  </div>
+</template>
+
+<script>
+import { getDevicesByQuery } from '@/api/device'
+import groupMixin from './mixins/group.js'
+import DeviceCardRect from './DeviceCardRect.vue'
+import VolumeDialog from './VolumeDialog.vue'
+
+export default {
+  name: 'DeviceGroupRect',
+  components: {
+    DeviceCardRect,
+    VolumeDialog
+  },
+  mixins: [groupMixin],
+  props: {
+    path: {
+      type: String,
+      required: true
+    },
+    total: {
+      type: Number,
+      default: -1
+    }
+  },
+  computed: {
+    hasData () {
+      return this.groupOptions.list.some(({ onlineStatus }) => onlineStatus === 1)
+    }
+  },
+  watch: {
+    path () {
+      this.refreshGroups(true)
+    },
+    total () {
+      this.refreshGroups(true)
+    }
+  },
+  methods: {
+    refreshGroups (force) {
+      if (!this.path || !force && this.groupOptions.loading) {
+        return
+      }
+      this.onResetGroupOptions()
+
+      const groupOptions = this.createGroupOptions(
+        this.total === 0
+          ? { loaded: true }
+          : { loading: true }
+      )
+      this.groupOptions = groupOptions
+      this.onChanged()
+      if (this.total <= 0) {
+        return
+      }
+
+      getDevicesByQuery({
+        pageNum: 1,
+        pageSize: this.total,
+        activate: 1,
+        org: this.path
+      }, { custom: true }).then(
+        ({ data }) => {
+          if (groupOptions.ignore) {
+            return
+          }
+          const map = {}
+          const topics = []
+          groupOptions.list = data.map(device => {
+            const item = this.transformDevice(device)
+            map[device.id] = item
+            topics.push(...this.createTopics(item))
+            return item
+          })
+          groupOptions.map = map
+          groupOptions.loaded = true
+          this.onChanged()
+          this.onSubscribe(topics)
+        },
+        ({ isCancel }) => {
+          if (isCancel || groupOptions.ignore) {
+            return
+          }
+          this.$timer = setTimeout(this.refreshGroups, 2000)
+        }
+      ).finally(() => {
+        groupOptions.loading = false
+      })
+    },
+    onChanged () {
+      this.$emit('change', [{
+        children: [...this.groupOptions.list]
+      }])
+    },
+    onVolume ({ value, device }) {
+      this.$refs.volumeDialog.show(value, device)
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.c-device-group-rect {
+  min-height: 64px;
+
+  &__content {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 2px;
+  }
+}
+</style>

+ 24 - 1
src/views/dashboard/components/DeviceGroups.vue

@@ -26,6 +26,12 @@
           >
             层级
           </el-dropdown-item>
+          <el-dropdown-item
+            icon="el-icon-menu"
+            command="rect"
+          >
+            方块图
+          </el-dropdown-item>
           <el-dropdown-item
             v-if="isTenantAdmin"
             icon="el-icon-star-off"
@@ -48,6 +54,7 @@
       <slot />
       <div class="l-flex__fill" />
       <div
+        v-if="supportCardStyle"
         class="l-flex__none l-flex--row u-color--blue u-font-size--sm has-active"
         @click="onSwitchCardStyle"
       >
@@ -69,10 +76,11 @@
 import { mapGetters } from 'vuex'
 import DeviceGroupTile from './DeviceGroupTile.vue'
 import DeviceGroupLevel from './DeviceGroupLevel.vue'
+import DeviceGroupRect from './DeviceGroupRect.vue'
 import DeviceGroupAttention from './DeviceGroupAttention.vue'
 import VolumeDrawer from './DrawerVolumeSettings.vue'
 
-const groupStyleCommands = ['tile', 'level']
+const groupStyleCommands = ['tile', 'level', 'rect']
 const cardStyleCommands = ['big', 'medium']
 
 export default {
@@ -80,6 +88,7 @@ export default {
   components: {
     DeviceGroupTile,
     DeviceGroupLevel,
+    DeviceGroupRect,
     DeviceGroupAttention,
     VolumeDrawer
   },
@@ -109,6 +118,8 @@ export default {
       switch (this.groupStyle) {
         case 'attention':
           return '我的关注'
+        case 'rect':
+          return '方块图'
         default:
           return '设备列表'
       }
@@ -117,6 +128,8 @@ export default {
       switch (this.groupStyle) {
         case 'tile':
           return 'el-icon-s-grid'
+        case 'rect':
+          return 'el-icon-menu'
         case 'attention':
           return 'el-icon-star-off'
         default:
@@ -127,6 +140,8 @@ export default {
       switch (this.groupStyle) {
         case 'tile':
           return 'DeviceGroupTile'
+        case 'rect':
+          return 'DeviceGroupRect'
         case 'attention':
           return 'DeviceGroupAttention'
         default:
@@ -136,6 +151,14 @@ export default {
     hasData () {
       return this.groups.some(({ children }) => children.some(({ onlineStatus }) => onlineStatus === 1))
     },
+    supportCardStyle () {
+      switch (this.groupStyle) {
+        case 'rect':
+          return false
+        default:
+          return true
+      }
+    },
     tip () {
       if (this.cardStyle === 'big') {
         return '切换至简洁模式'