Răsfoiți Sursa

feat(power): batch switch

Casper Dai 2 ani în urmă
părinte
comite
a1d380d6b7

+ 8 - 0
src/api/device.js

@@ -570,3 +570,11 @@ export function getStatusReport (id) {
     data: { deviceIds: id }
   })
 }
+
+export function getDevicesWithPower (params) {
+  return request({
+    url: '/device/bond/multiFunction/list',
+    method: 'GET',
+    params
+  })
+}

+ 6 - 0
src/router/index.js

@@ -273,6 +273,12 @@ export const asyncRoutes = [
         path: 'group',
         component: () => import('@/views/device/group/index'),
         meta: { title: '分组管理' }
+      },
+      {
+        path: 'power',
+        component: () => import('@/views/device/power/index'),
+        access: Access.MANAGE_DEVICE,
+        meta: { title: '批量开关电源' }
       }
     ]
   },

+ 84 - 0
src/views/device/power/components/DevicePower.vue

@@ -0,0 +1,84 @@
+<script>
+import { ThirdPartyDevice } from '@/constant'
+import { parseTime } from '@/utils'
+import {
+  addListener,
+  removeListener
+} from '@/utils/adapter'
+import {
+  Power,
+  Status
+} from '@/utils/adapter/nova'
+
+export default {
+  name: 'DevicePower',
+  props: {
+    device: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      colorClass: ''
+    }
+  },
+  computed: {
+    online () {
+      return this.device.onlineStatus === 1
+    }
+  },
+  watch: {
+    online (val) {
+      if (val) {
+        addListener(this.device.id, this.onMessage)
+      } else {
+        removeListener(this.device.id, this.onMessage)
+      }
+    }
+  },
+  mounted () {
+    if (this.online) {
+      addListener(this.device.id, this.onMessage)
+    }
+  },
+  beforeDestroy () {
+    if (this.online) {
+      removeListener(this.device.id, this.onMessage)
+    }
+  },
+  methods: {
+    onMessage (value) {
+      const multiCard = value[ThirdPartyDevice.MULTI_FUNCTION_CARD]
+      this.device.power = multiCard.status
+      if (multiCard.status === Status.OK) {
+        this.device.timestamp = parseTime(multiCard.timestamp, '{y}-{m}-{d} {h}:{i}:{s}')
+        switch (multiCard.realSwitchStatus) {
+          case Power.ON:
+            this.colorClass = 'u-color--success'
+            break
+          case Power.OFF:
+            this.colorClass = 'u-color--error dark'
+            break
+          default:
+            this.colorClass = 'u-color--warning'
+            break
+        }
+      }
+    }
+  },
+  render (h) {
+    if (!this.online || this.device.power === Status.NONE) {
+      return h('i', null, '-')
+    }
+    if (this.device.power === Status.LOADING) {
+      return h('i', {
+        staticClass: 'el-icon-loading'
+      })
+    }
+    return h('i', {
+      staticClass: `o-status ${this.colorClass}`
+    })
+  }
+}
+</script>

+ 250 - 0
src/views/device/power/index.vue

@@ -0,0 +1,250 @@
+<template>
+  <wrapper
+    fill
+    margin
+    padding
+    background
+    horizontal
+  >
+    <department-tree
+      class="c-sibling-item c-sidebar u-width--md"
+      @change="onGroupChanged"
+    />
+    <schema-table
+      ref="table"
+      class="c-sibling-item far"
+      row-key="rowKey"
+      :schema="schema"
+    />
+    <table-dialog
+      ref="historyDialog"
+      title="历史任务"
+      :schema="historySchema"
+    />
+    <table-dialog
+      ref="detailDialog"
+      title="任务详情"
+      :schema="detailSchema"
+    />
+  </wrapper>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import { getDevicesWithPower } from '@/api/device'
+import {
+  subscribe,
+  unsubscribe
+} from '@/utils/mqtt'
+import { Status } from '@/utils/adapter/nova'
+import {
+  toggleDevicePower,
+  getOperationResult,
+  getOperationResults
+} from '@/api/platform'
+import DevicePower from './components/DevicePower.vue'
+
+export default {
+  name: 'DeviceList',
+  data () {
+    return {
+      schema: {
+        nonPagination: true,
+        list: this.getDevicesWithPower,
+        listeners: {
+          'selection-change': this.onSelectionChange,
+          'row-click': this.onRowClick
+        },
+        buttons: [
+          { label: '批量开启电源', on: this.onOpen },
+          { label: '批量关闭电源', on: this.onClose },
+          { label: '历史任务', on: this.onViewHistory }
+        ],
+        filters: [
+          { type: 'refresh', label: '刷新' }
+        ],
+        cols: [
+          { type: 'selection', selectable: this.canSelect },
+          { prop: 'name', label: '设备名称', 'min-width': 120 },
+          { label: '设备状态', render: ({ onlineStatus }, h) => h('i', { staticClass: `o-status ${onlineStatus === 1 ? 'u-color--success' : 'u-color--error dark'}` }), 'width': 80, 'align': 'center' },
+          { label: '电源状态', render: (device, h) => h(DevicePower, { props: { device } }), width: 80, align: 'center' },
+          { prop: 'timestamp', label: '同步时间', width: 160, align: 'center' },
+          { prop: 'address', label: '地址' }
+        ]
+      },
+      historySchema: {
+        list: this.getOperationResults,
+        cols: [
+          { prop: 'createTime', label: '执行时间', align: 'center' },
+          { label: '执行动作', render: ({ action }) => ['开启电源', '关闭电源'][action], align: 'center' },
+          { label: '执行结果', type: 'tag', render: ({ operationStatus }) => {
+            return {
+              type: ['primary', 'success', 'danger'][operationStatus],
+              label: ['执行中', '成功', '存在失败'][operationStatus]
+            }
+          }, width: 160, align: 'center' },
+          { type: 'invoke', render: [
+            { label: '详情', on: this.onViewDetail }
+          ] }
+        ]
+      },
+      detailSchema: {
+        nonPagination: true,
+        list: this.getOperationResult,
+        cols: [
+          { prop: 'deviceName', label: '设备名称' },
+          { label: '执行结果', type: 'tag', render: ({ executeStatus, failReason }) => {
+            return {
+              type: executeStatus ? 'success' : failReason === 'EXECUTING' ? 'primary' : 'danger',
+              label: executeStatus ? '成功' : failReason === 'EXECUTING' ? '执行中' : '失败'
+            }
+          }, width: 160, align: 'center' }
+        ]
+      }
+    }
+  },
+  computed: {
+    ...mapGetters(['tenant'])
+  },
+  created () {
+    this.$timer = -1
+    this.$deitalTimer = -1
+    subscribe([
+      '+/+/online',
+      '+/+/offline'
+    ], this.onMessage)
+  },
+  beforeDestroy () {
+    clearTimeout(this.$timer)
+    clearTimeout(this.$deitalTimer)
+    unsubscribe([
+      '+/+/online',
+      '+/+/offline'
+    ], this.onMessage)
+  },
+  methods: {
+    onMessage (topic) {
+      const result = /^\d+\/(\d+)\/(online|offline)$/.exec(topic)
+      if (!result) {
+        return
+      }
+      const deviceId = result[1]
+      const onlineStatus = result[2] === 'online' ? 1 : 2
+      const device = this.$refs.table?.getData().find(({ id }) => id === deviceId)
+      if (device) {
+        device.onlineStatus = onlineStatus
+        this.$refs.table?.getInst().toggleRowSelection(device, false)
+      }
+    },
+    onGroupChanged (group) {
+      this.$group = group
+      this.$refs.table?.pageTo(1)
+    },
+    canSelect ({ onlineStatus, power }) {
+      return onlineStatus === 1 && power === Status.OK
+    },
+    onSelectionChange (val) {
+      this.$selectionItems = val
+    },
+    onRowClick (row) {
+      if (row.onlineStatus === 1 && row.power === Status.OK) {
+        this.$refs.table?.getInst().toggleRowSelection(row)
+      }
+    },
+    getDevicesWithPower () {
+      return getDevicesWithPower({
+        activate: 1,
+        [this.tenant === this.$group.path ? 'tenant' : 'org']: this.$group.path
+      }).then(({ data }) => {
+        const now = Date.now()
+        return { data: data.map(device => {
+          device.rowKey = `${now}_${device.id}`
+          device.power = Status.LOADING
+          device.timestamp = '-'
+          return device
+        }) }
+      })
+    },
+    onOpen () {
+      this.onToggleStatus(0)
+    },
+    onClose () {
+      this.onToggleStatus(1)
+    },
+    onToggleStatus (action) {
+      if (!this.$selectionItems?.length) {
+        this.$message({
+          type: 'warning',
+          message: '请先选择需要操作的设备'
+        })
+        return
+      }
+      const length = this.$selectionItems.length
+      getOperationResult().then(
+        ({ data }) => {
+          if (data && data.operationStatus === 0) {
+            return this.$confirm(
+              '覆盖执行后原有任务将会中断',
+              '当前存在执行中的任务,覆盖执行?',
+              { type: 'warning' }
+            ).then(
+              () => true,
+              () => false
+            )
+          }
+          return true
+        },
+        ({ isCancel }) => {
+          if (!isCancel) {
+            this.$message({
+              type: 'warning',
+              message: '繁忙中,请稍后查询'
+            })
+          }
+          return false
+        }
+      ).then(done => {
+        if (done) {
+          if (length !== this.$selectionItems.length) {
+            this.$message({
+              type: 'warning',
+              message: '部分设备在线状态发生变化,请重新确认选择的设备'
+            })
+            return
+          }
+          toggleDevicePower({ action, deviceIds: this.$selectionItems.map(({ id }) => id) })
+        }
+      })
+    },
+    onViewHistory () {
+      this.$refs.historyDialog.show()
+    },
+    getOperationResults (params) {
+      clearTimeout(this.$timer)
+      return getOperationResults(params).then(data => {
+        if (data.data[0]?.operationStatus === 0) {
+          this.$timer = setTimeout(() => {
+            this.$refs.historyDialog?.getTable()?.refreshCurrentPageOnBackground()
+          }, 2000)
+        }
+        return data
+      })
+    },
+    onViewDetail ({ operationId }) {
+      this.$taskId = operationId
+      this.$refs.detailDialog.show()
+    },
+    getOperationResult () {
+      clearTimeout(this.$deitalTimer)
+      return getOperationResult(this.$taskId).then(({ data }) => {
+        if (data.operationStatus === 0) {
+          this.$deitalTimer = setTimeout(() => {
+            this.$refs.detailDialog?.getTable()?.refreshCurrentPageOnBackground()
+          }, 2000)
+        }
+        return { data: data.taskDetails }
+      })
+    }
+  }
+}
+</script>