Selaa lähdekoodia

feat: batch power timing task

Casper Dai 2 vuotta sitten
vanhempi
sitoutus
6d709902fe

+ 9 - 14
src/views/device/detail/components/DeviceInvoke/mixins/TaskDialog.vue → src/components/dialog/TaskDialog/index.vue

@@ -50,14 +50,9 @@
 </template>
 
 <script>
+import { Frequency } from '@/constant'
 import { parseTime } from '@/utils'
 
-export const Freq = {
-  DAILY: 0,
-  WEEKLY: 1,
-  ONCE: 2
-}
-
 export default {
   name: 'TaskDialog',
   props: {
@@ -79,17 +74,17 @@ export default {
     freqOptions () {
       return this.singleTime
         ? [
-          { value: Freq.DAILY, label: '每天' },
-          { value: Freq.WEEKLY, label: '每周' },
-          { value: Freq.ONCE, label: '单次' }
+          { value: Frequency.DAILY, label: '每天' },
+          { value: Frequency.WEEKLY, label: '每周' },
+          { value: Frequency.ONCE, label: '单次' }
         ]
         : [
-          { value: Freq.DAILY, label: '每天' },
-          { value: Freq.WEEKLY, label: '每周' }
+          { value: Frequency.DAILY, label: '每天' },
+          { value: Frequency.WEEKLY, label: '每周' }
         ]
     },
     isWeekly () {
-      return this.task.freq === Freq.WEEKLY
+      return this.task.freq === Frequency.WEEKLY
     },
     weeks () {
       return this.cron
@@ -114,7 +109,7 @@ export default {
     }
   },
   methods: {
-    show ({ freq = Freq.DAILY, dayOfWeek, executeTime } = {}) {
+    show ({ freq = Frequency.DAILY, dayOfWeek, executeTime } = {}) {
       this.task = {
         freq,
         dayOfWeek: dayOfWeek ? dayOfWeek.split(',') : [],
@@ -134,7 +129,7 @@ export default {
           return
         }
         if (this.task.dayOfWeek.length === 7) {
-          task.freq = Freq.DAILY
+          task.freq = Frequency.DAILY
         } else {
           task.dayOfWeek = dayOfWeek.join(',')
         }

+ 8 - 1
src/constant.js

@@ -313,7 +313,8 @@ export const RoleAccess = {
 export const AlarmLevelInfo = {
   0: '提示性预警',
   1: '中级预警',
-  2: '紧急预警'
+  2: '紧急预警',
+  9999: '自定义'
 }
 
 export const AlarmStrategies = [
@@ -390,3 +391,9 @@ export const Quality = {
     frameRate: 20
   }
 }
+
+export const Frequency = {
+  DAILY: 0,
+  WEEKLY: 1,
+  ONCE: 2
+}

+ 6 - 6
src/router/index.js

@@ -269,16 +269,16 @@ export const asyncRoutes = [
         component: () => import('@/views/device/record/index'),
         meta: { title: '视频回采' }
       },
-      {
-        path: 'group',
-        component: () => import('@/views/device/group/index'),
-        meta: { title: '分组管理' }
-      },
+      // {
+      //   path: 'group',
+      //   component: () => import('@/views/device/group/index'),
+      //   meta: { title: '分组管理' }
+      // },
       {
         path: 'power',
         component: () => import('@/views/device/power/index'),
         access: Access.MANAGE_DEVICE,
-        meta: { title: '批量开关电源' }
+        meta: { title: '批量电源操作' }
       }
     ]
   },

+ 28 - 0
src/utils/adapter/index.js

@@ -21,6 +21,7 @@ export * from './nova'
 
 const deviceCache = new Map()
 const cbMap = new Map()
+const injectMap = new Map()
 
 let client = null
 export function startMonitor () {
@@ -109,6 +110,10 @@ export function startMonitor () {
           ...data.data
         })
       }
+    } else {
+      injectMap.get(id)?.forEach(cb => {
+        cb(message)
+      })
     }
   })
 
@@ -175,6 +180,29 @@ export function removeListener (id, cb) {
   cbSet.delete(cb)
 }
 
+export function addInjectListener (id, cb) {
+  let cbSet = injectMap.get(id)
+  if (!cbSet) {
+    injectMap.set(id, cbSet = new Set())
+  }
+  if (cbSet.has(cb)) {
+    console.log('inject', id, '->', 'exsit')
+    return
+  }
+  console.log('inject add', id, '->', 'success')
+  cbSet.add(cb)
+}
+
+export function removeInjectListener (id, cb) {
+  const cbSet = injectMap.get(id)
+  if (!cbSet || !cbSet.has(cb)) {
+    console.log('inject', id, '->', 'not exsit')
+    return
+  }
+  console.log('inject remove', id, '->', 'success')
+  cbSet.delete(cb)
+}
+
 let waiting = new Set()
 let running = new Set()
 let fetching = false

+ 12 - 10
src/utils/adapter/nova.js

@@ -72,15 +72,17 @@ export function parsePowerSwitchStatus (powers) {
 export function parseCachePower ({ switchStatus, typeArray = '[]' }) {
   if (switchStatus > Power.LOADING) {
     typeArray = JSON.parse(typeArray)
-    const powers = switchStatus.toString(2).split('').map((val, index) => {
-      return {
-        connectIndex: 0,
-        portIndex: 0,
-        powerIndex: index,
-        type: typeArray?.[index] || '未知',
-        action: Number(val)
-      }
-    })
+    // 默认8个电源
+    const powers = switchStatus.toString(2).padStart(typeArray.length || 8, '0').split('')
+      .map((val, index) => {
+        return {
+          connectIndex: 0,
+          portIndex: 0,
+          powerIndex: index,
+          type: typeArray[index] || '未知',
+          action: Number(val)
+        }
+      })
     return {
       switchStatus: parsePowerSwitchStatus(powers),
       realSwitchStatus: switchStatus,
@@ -92,7 +94,7 @@ export function parseCachePower ({ switchStatus, typeArray = '[]' }) {
 
 export function getPowerStatus ({ current_status_info }) {
   console.log('nova power', current_status_info)
-  return current_status_info.filter(({ portIndex }) => portIndex !== RELAY_KEY).map(({ portIndex, connectIndex, updatePowerIndexStates }) => {
+  return current_status_info.filter(({ portIndex, updatePowerIndexStates }) => portIndex !== RELAY_KEY && updatePowerIndexStates.length).map(({ portIndex, connectIndex, updatePowerIndexStates }) => {
     return {
       switchStatus: parsePowerSwitchStatus(updatePowerIndexStates),
       realSwitchStatus: parseInt(updatePowerIndexStates.map(({ action }) => action).join(''), 2),

+ 26 - 1
src/utils/index.js

@@ -5,7 +5,8 @@ import {
   AssetType,
   AssetTypeInfo,
   SCREEN_TIME_KEY,
-  TimeType
+  TimeType,
+  Frequency
 } from '@/constant'
 
 export const EventBus = new Vue()
@@ -341,3 +342,27 @@ export function transformDatasetAssetToAsset (asset) {
   }
   return asset
 }
+
+// dayOfWeek,周一至周日对应1~7
+// 最终cron数据对应的为秒、分、时、日、月、星期、年,例如'30 0 12 14 11 ? 2022'
+export function transformToCron (executeTime, freq, startTime, dayOfWeek) {
+  let suffix = null
+  switch (freq) {
+    case Frequency.DAILY:
+      suffix = ['*', '*', '?', '*']
+      break
+    case Frequency.WEEKLY:
+      suffix = ['?', '*', dayOfWeek, '*']
+      break
+    default:
+      suffix = startTime.split('-').reverse()
+      suffix.splice(2, 0, '?')
+      break
+  }
+  return executeTime
+    .split(':')
+    .map(val => Number(val))
+    .reverse()
+    .concat(suffix)
+    .join(' ')
+}

+ 1 - 1
src/views/device/detail/components/Anolg/components/BoxOperation.vue

@@ -26,7 +26,7 @@ import {
 } from '@/utils/adapter/nova'
 
 export default {
-  name: 'AnolgBoxStatus',
+  name: 'AnolgBoxOperation',
   props: {
     device: {
       type: Object,

+ 33 - 86
src/views/device/detail/components/DeviceInfo/components/Power.vue

@@ -1,12 +1,6 @@
 <template>
   <div>
-    <div class="l-flex--row c-sibling-item--v u-color--black u-font-size--sm u-bold">
-      <div class="c-sibling-item">状态</div>
-      <!-- <i
-        class="c-sibling-item el-icon-refresh u-font-size has-active"
-        @click="onRefresh"
-      /> -->
-    </div>
+    <div class="l-flex--row c-sibling-item--v u-color--black u-font-size--sm u-bold">状态</div>
     <schema-table
       ref="powerTable"
       class="c-sibling-item--v near"
@@ -26,46 +20,36 @@
         :schema="multiTaskSchema"
       />
     </template>
-    <template v-if="hasRelay">
-      <div class="c-sibling-item--v u-color--black u-font-size--sm u-bold">播控盒定时任务</div>
-      <schema-table
-        ref="relayTable"
-        class="c-sibling-item--v near"
-        :schema="relayTaskSchema"
-      />
-    </template>
   </div>
 </template>
 
 <script>
-import { ThirdPartyDevice } from '@/constant'
+import {
+  ThirdPartyDevice,
+  Frequency
+} from '@/constant'
 import { parseTime } from '@/utils'
+import { publish } from '@/utils/mqtt'
 import {
-  addListener as adapterAddListener,
-  removeListener as adapterRemoveListener
+  addListener,
+  removeListener,
+  addInjectListener,
+  removeInjectListener
 } from '@/utils/adapter'
 import {
   Status,
   RELAY_KEY,
   GET_POWER_STATUS,
-  GET_MULTI_POWER_TIMING,
-  GET_RELAY_POWER_TIMING
+  GET_MULTI_POWER_TIMING
 } from '@/utils/adapter/nova'
 import { toDate } from '@/utils/event'
-import {
-  send,
-  addListener,
-  removeListener
-} from '../../../monitor'
-import { Freq } from '../../DeviceInvoke/mixins/TaskDialog'
 
 const ErrorMessage = {
   TIMEOUT: '暂未获取到操作反馈,请稍后重试',
   BUSY: '终端被他人占用',
   PASSWORD: '登录密码错误,请联系管理员',
   [GET_POWER_STATUS]: '获取电源状态超时,请稍后重试',
-  [GET_MULTI_POWER_TIMING]: '获取定时数据超时,请回读查看',
-  [GET_RELAY_POWER_TIMING]: '获取定时数据超时,请回读查看'
+  [GET_MULTI_POWER_TIMING]: '获取定时数据超时,请回读查看'
 }
 const FOREVER = '4016-06-06'
 
@@ -83,8 +67,7 @@ export default {
       hasMulti: false,
       hasRelay: false,
       powerSchema: {
-        singlePage: true,
-        // list: this.getPowers,
+        nonPagination: true,
         list: this.getCachePowers,
         cols: [
           { label: '设备', render: ({ portIndex }) => portIndex === RELAY_KEY ? '播控盒' : '控电设备'/* `控电设备${portIndex}` */ },
@@ -106,30 +89,7 @@ export default {
           { prop: 'freqInfo', label: '执行方式' },
           { label: '生效日期', render: ({ freq, startTime, endTime }) => {
             switch (freq) {
-              case Freq.ONCE:
-                return startTime
-              default:
-                return endTime === FOREVER ? '永久有效' : `${startTime} 至 ${endTime}`
-            }
-          }, 'min-width': 120 },
-          { type: 'tag', render: task => this.isExpired(task)
-            ? { type: 'warning', label: '已过期' }
-            : task.enable
-              ? { type: 'success', label: '启用' }
-              : { type: 'danger', label: '停用' } }
-        ]
-      },
-      relayTaskSchema: {
-        singlePage: true,
-        list: this.getRelayTasks,
-        cols: [
-          { label: '电源类型', render: ({ typeKey, type }) => this.$typeMap[typeKey]?.label || type },
-          { label: '执行动作', render: ({ action }) => this.actionInfo[action] },
-          { prop: 'executeTime', label: '执行时间' },
-          { prop: 'freqInfo', label: '执行方式' },
-          { label: '生效日期', render: ({ freq, startTime, endTime }) => {
-            switch (freq) {
-              case Freq.ONCE:
+              case Frequency.ONCE:
                 return startTime
               default:
                 return endTime === FOREVER ? '永久有效' : `${startTime} 至 ${endTime}`
@@ -147,12 +107,12 @@ export default {
   created () {
     this.$tableData = {}
     this.$tableStatus = {}
-    addListener('multifunction', this.onMessage)
-    adapterAddListener(this.device.id, this.onPowerMessage)
+    addListener(this.device.id, this.onPowerMessage)
+    addInjectListener(this.device.id, this.onMessage)
   },
   beforeDestroy () {
-    removeListener('multifunction', this.onMessage)
-    adapterRemoveListener(this.device.id, this.onPowerMessage)
+    removeListener(this.device.id, this.onPowerMessage)
+    removeInjectListener(this.device.id, this.onMessage)
     clearInterval(this.$cacheTimer)
     Object.keys(this.$tableStatus).forEach(key => {
       clearTimeout(this.$tableStatus[key].timer)
@@ -174,7 +134,10 @@ export default {
         }, 1000)
       })
     },
-    onPowerMessage (value) {
+    onPowerMessage (value, key) {
+      if (this.hasMulti || key && key !== ThirdPartyDevice.MULTI_FUNCTION_CARD) {
+        return
+      }
       const multiCard = value[ThirdPartyDevice.MULTI_FUNCTION_CARD]
       if (multiCard.status > Status.LOADING) {
         const map = {}
@@ -208,14 +171,15 @@ export default {
       console.log('invoke', invoke, inputs)
       const timestamp = `${Date.now()}`
       const messageId = `${invoke}_${timestamp}`
-      return send(
-        '/multifunctionCard/invoke',
-        {
+      return publish(
+        `${this.device.productId}/${this.device.id}/multifunctionCard/invoke`,
+        JSON.stringify({
           messageId,
           timestamp,
           'function': invoke,
           inputs: inputs || []
-        }
+        }),
+        true
       ).then(
         () => messageId,
         () => {
@@ -286,9 +250,6 @@ export default {
         case GET_MULTI_POWER_TIMING:
           tableData = this.setMultiPowerTasks(data.data)
           break
-        case GET_RELAY_POWER_TIMING:
-          tableData = this.setRelayPowerTasks(data)
-          break
         default:
           break
       }
@@ -354,40 +315,26 @@ export default {
       }
       return tasks
     },
-    getRelayTasks () {
-      return this.getData(GET_RELAY_POWER_TIMING)
-    },
-    setRelayPowerTasks (data) {
-      console.log('GET_RELAY_POWER_TIMING', data)
-      const tasks = []
-      data.relayPolicyTask.forEach(task => {
-        tasks.push({
-          action: task.status ^ 1,
-          ...this.transfromDataToTask(task)
-        })
-      })
-      return tasks
-    },
     transfromDataToTask ({ type, flag, powerIndex, enable, startTime, endTime, cron }) {
       const strArr = cron[0].split(' ')
       const freq = strArr[3] === '*' && strArr[5] === '?'
-        ? Freq.DAILY
+        ? Frequency.DAILY
         : strArr[5] === '?'
-          ? Freq.ONCE
-          : Freq.WEEKLY
+          ? Frequency.ONCE
+          : Frequency.WEEKLY
       return {
         type, flag, powerIndex, enable, startTime, endTime,
         freq,
         freqInfo: this.getFreqInfo(freq, strArr[5]),
-        dayOfWeek: freq === Freq.WEEKLY ? strArr[5] : '',
+        dayOfWeek: freq === Frequency.WEEKLY ? strArr[5] : '',
         executeTime: `${strArr[2].padStart(2, '0')}:${strArr[1].padStart(2, '0')}:${strArr[0].padStart(2, '0')}`
       }
     },
     getFreqInfo (freq, val = '') {
       switch (freq) {
-        case Freq.ONCE:
+        case Frequency.ONCE:
           return '单次'
-        case Freq.DAILY:
+        case Frequency.DAILY:
           return '每天'
         default:
           return `每周${val.split(',').map(val => ['', '一', '二', '三', '四', '五', '六', '日'][val])}`

+ 32 - 55
src/views/device/detail/components/DeviceInvoke/MultifunctionCardPowerSwitch.vue

@@ -39,7 +39,7 @@
       </template>
     </c-dialog>
     <task-dialog
-      ref="editDialog"
+      ref="taskDialog"
       :title="dialogTitle"
       cron
       single-time
@@ -97,11 +97,19 @@
 
 <script>
 import { mapGetters } from 'vuex'
-import { ThirdPartyDevice } from '@/constant'
-import { parseTime } from '@/utils'
 import {
-  addListener as adapterAddListener,
-  removeListener as adapterRemoveListener
+  ThirdPartyDevice,
+  Frequency
+} from '@/constant'
+import {
+  parseTime,
+  transformToCron
+} from '@/utils'
+import {
+  addListener,
+  removeListener,
+  addInjectListener,
+  removeInjectListener
 } from '@/utils/adapter'
 import {
   Status,
@@ -120,12 +128,7 @@ import {
   savePowerLogger,
   sendDeviceAlarm
 } from '@/api/platform'
-import {
-  addListener,
-  removeListener
-} from '../../monitor'
 import baseMixin from './mixins/base'
-import TaskDialog, { Freq } from './mixins/TaskDialog'
 
 const ErrorMessage = {
   TIMEOUT: '暂未获取到操作反馈,请回读查看',
@@ -149,9 +152,6 @@ const PowerAction = {
 
 export default {
   name: 'MultifunctionCardPowerSwitch',
-  components: {
-    TaskDialog
-  },
   mixins: [baseMixin],
   data () {
     return {
@@ -290,7 +290,7 @@ export default {
           { prop: 'freqInfo', label: '执行方式' },
           { label: '生效日期', render: ({ freq, startTime, endTime }) => {
             switch (freq) {
-              case Freq.ONCE:
+              case Frequency.ONCE:
                 return startTime
               default:
                 return endTime === FOREVER ? '永久有效' : `${startTime} 至 ${endTime}`
@@ -318,13 +318,13 @@ export default {
     }
   },
   mounted () {
-    addListener('multifunction', this.onMessage)
-    adapterAddListener(this.device.id, this.onCacheMessage)
+    addListener(this.device.id, this.onCacheMessage)
+    addInjectListener(this.device.id, this.onMessage)
   },
   beforeDestroy () {
     this.$openDialog = false
-    removeListener('multifunction', this.onMessage)
-    adapterRemoveListener(this.device.id, this.onCacheMessage)
+    removeListener(this.device.id, this.onCacheMessage)
+    removeInjectListener(this.device.id, this.onMessage)
   },
   methods: {
     isDisableDate (date) {
@@ -812,23 +812,23 @@ export default {
     transfromDataToTask ({ type, flag, powerIndex, enable, startTime, endTime, cron }) {
       const strArr = cron[0].split(' ')
       const freq = strArr[3] === '*' && strArr[5] === '?'
-        ? Freq.DAILY
+        ? Frequency.DAILY
         : strArr[5] === '?'
-          ? Freq.ONCE
-          : Freq.WEEKLY
+          ? Frequency.ONCE
+          : Frequency.WEEKLY
       return {
         type, flag, powerIndex, enable, startTime, endTime,
         freq,
         freqInfo: this.getFreqInfo(freq, strArr[5]),
-        dayOfWeek: freq === Freq.WEEKLY ? strArr[5] : '',
+        dayOfWeek: freq === Frequency.WEEKLY ? strArr[5] : '',
         executeTime: `${strArr[2].padStart(2, '0')}:${strArr[1].padStart(2, '0')}:${strArr[0].padStart(2, '0')}`
       }
     },
     getFreqInfo (freq, val = '') {
       switch (freq) {
-        case Freq.ONCE:
+        case Frequency.ONCE:
           return '单次'
-        case Freq.DAILY:
+        case Frequency.DAILY:
           return '每天'
         default:
           return `每周${val.split(',').map(val => ['', '一', '二', '三', '四', '五', '六', '日'][val])}`
@@ -929,7 +929,7 @@ export default {
         map[from.portIndex].commands.push({
           conditions: [{
             type, flag, action, powerIndex, enable, startTime, endTime,
-            cron: [this.getCron(executeTime, freq, startTime, dayOfWeek)]
+            cron: [transformToCron(executeTime, freq, startTime, dayOfWeek)]
           }]
         })
       })
@@ -958,7 +958,7 @@ export default {
         }
         map[from.portIndex].conditions.push({
           type, flag, action, powerIndex, enable, startTime, endTime,
-          cron: [this.getCron(executeTime, freq, startTime, dayOfWeek)]
+          cron: [transformToCron(executeTime, freq, startTime, dayOfWeek)]
         })
       })
       savePowerLogger({
@@ -973,33 +973,10 @@ export default {
         return {
           type, powerIndex, enable, startTime, endTime,
           status: action ^ 1,
-          cron: [this.getCron(executeTime, freq, startTime, dayOfWeek)]
+          cron: [transformToCron(executeTime, freq, startTime, dayOfWeek)]
         }
       })
     },
-    // dayOfWeek,周一至周日对应1~7
-    // 最终cron数据对应的为秒、分、时、日、月、星期、年,例如'30 0 12 14 11 ? 2022'
-    getCron (executeTime, freq, startTime, dayOfWeek) {
-      let suffix = null
-      switch (freq) {
-        case Freq.DAILY:
-          suffix = ['*', '*', '?', '*']
-          break
-        case Freq.WEEKLY:
-          suffix = ['?', '*', dayOfWeek, '*']
-          break
-        default:
-          suffix = startTime.split('-').reverse()
-          suffix.splice(2, 0, '?')
-          break
-      }
-      return executeTime
-        .split(':')
-        .map(val => Number(val))
-        .reverse()
-        .concat(suffix)
-        .join(' ')
-    },
     onToggle (task) {
       this.hasChanged = true
       task.flag = `${Date.now()}`
@@ -1012,7 +989,7 @@ export default {
       this.taskTime = today
       this.taskDate = [today, today]
       this.taskAction = PowerAction.OPEN
-      this.$refs.editDialog.show()
+      this.$refs.taskDialog.show()
     },
     onEdit (task) {
       const { from, type, freq, dayOfWeek, executeTime, startTime, endTime, action } = task
@@ -1022,7 +999,7 @@ export default {
       this.taskTime = startTime
       this.taskDate = [startTime, endTime]
       this.taskAction = action
-      this.$refs.editDialog.show({ freq, dayOfWeek, executeTime })
+      this.$refs.taskDialog.show({ freq, dayOfWeek, executeTime })
     },
     onSave ({ value, done }) {
       const { freq, executeTime, dayOfWeek = '' } = value
@@ -1042,14 +1019,14 @@ export default {
           return
         }
       }
-      if (freq !== Freq.ONCE && (!this.taskDate[0] || !this.taskDate[1]) || freq === Freq.ONCE && !this.taskTime) {
+      if (freq !== Frequency.ONCE && (!this.taskDate[0] || !this.taskDate[1]) || freq === Frequency.ONCE && !this.taskTime) {
         this.$message({
           type: 'warning',
           message: '请选择生效日期'
         })
         return
       }
-      const endTime = freq === Freq.ONCE ? this.taskTime : this.taskDate[1]
+      const endTime = freq === Frequency.ONCE ? this.taskTime : this.taskDate[1]
       if (this.isExpired({ endTime, executeTime })) {
         this.$message({
           type: 'warning',
@@ -1072,7 +1049,7 @@ export default {
         freqInfo: this.getFreqInfo(freq, dayOfWeek),
         executeTime,
         dayOfWeek,
-        startTime: freq === Freq.ONCE ? this.taskTime : this.taskDate[0],
+        startTime: freq === Frequency.ONCE ? this.taskTime : this.taskDate[0],
         endTime,
         flag: `${Date.now()}`
       }

+ 1 - 1
src/views/device/detail/components/DeviceInvoke/ScreenLight.vue

@@ -45,7 +45,7 @@
       </template>
     </c-dialog>
     <task-dialog
-      ref="editDialog"
+      ref="taskDialog"
       :title="dialogTitle"
       @confirm="onSave"
     >

+ 1 - 1
src/views/device/detail/components/DeviceInvoke/ScreenVolume.vue

@@ -50,7 +50,7 @@
       </template>
     </c-dialog>
     <task-dialog
-      ref="editDialog"
+      ref="taskDialog"
       :title="dialogTitle"
       @confirm="onSave"
     >

+ 2 - 6
src/views/device/detail/components/DeviceInvoke/mixins/task.js

@@ -7,12 +7,8 @@ import {
   deactivateTask
 } from '@/api/device'
 import baseMixin from './base'
-import TaskDialog from './TaskDialog'
 
 export default {
-  components: {
-    TaskDialog
-  },
   mixins: [baseMixin],
   data () {
     return {
@@ -64,13 +60,13 @@ export default {
     onAdd () {
       this.isAdd = true
       this.task = this.createTask()
-      this.$refs.editDialog.show()
+      this.$refs.taskDialog.show()
     },
     onEdit (task) {
       this.isAdd = false
       this.task = this.createTask(task)
       this.$task = task
-      this.$refs.editDialog.show(task)
+      this.$refs.taskDialog.show(task)
     },
     createTask () {
       // implement in component

+ 2 - 11
src/views/device/detail/monitor.js

@@ -16,8 +16,6 @@ export const Type = {
 
 const types = new Map()
 
-const parser = (inst, message) => JSON.parse(message)
-
 export function start (device) {
   if (productId) {
     stop()
@@ -27,8 +25,7 @@ export function start (device) {
   subscribe([
     `${productId}/${deviceId}/online`,
     `${productId}/${deviceId}/offline`,
-    `${productId}/${deviceId}/resource/progress`,
-    `${productId}/${deviceId}/multifunctionCard/invoke/reply`
+    `${productId}/${deviceId}/resource/progress`
   ], onMessage)
   createType('online', { parser: onlineParser })
   createType('download', { type: Type.CACHE, parser: downloadParser, reset: true, default () {
@@ -37,7 +34,6 @@ export function start (device) {
       mark: {}
     }
   } })
-  createType('multifunction', { parser })
 }
 
 export function stop () {
@@ -45,8 +41,7 @@ export function stop () {
     unsubscribe([
       `${productId}/${deviceId}/online`,
       `${productId}/${deviceId}/offline`,
-      `${productId}/${deviceId}/resource/progress`,
-      `${productId}/${deviceId}/multifunctionCard/invoke/reply`
+      `${productId}/${deviceId}/resource/progress`
     ], onMessage)
     productId = null
     deviceId = null
@@ -79,8 +74,6 @@ export function removeListener (type, cb) {
 
 function getTypeBySend (topic) {
   switch (topic) {
-    case '/multifunctionCard/invoke/reply':
-      return ['multifunction']
     default:
       return []
   }
@@ -123,8 +116,6 @@ function getTypeByTopic (topic) {
       return ['online']
     case 'resource/progress':
       return ['download']
-    case 'multifunctionCard/invoke/reply':
-      return ['multifunction']
     default:
       return []
   }

+ 157 - 0
src/views/device/power/components/DevicePowerTask.vue

@@ -0,0 +1,157 @@
+<script>
+import { ThirdPartyDevice } from '@/constant'
+import { publish } from '@/utils/mqtt'
+import {
+  addListener,
+  removeListener,
+  addInjectListener,
+  removeInjectListener
+} from '@/utils/adapter'
+import {
+  Status,
+  GET_MULTI_POWER_TIMING,
+  SET_MULTI_POWER_TIMING
+} from '@/utils/adapter/nova'
+import { savePowerLogger } from '@/api/platform'
+
+export default {
+  name: 'DevicePowerTask',
+  props: {
+    device: {
+      type: Object,
+      required: true
+    },
+    task: {
+      type: Object,
+      required: true
+    }
+  },
+  data () {
+    return {
+      status: 0,
+      tag: '',
+      taskType: null
+    }
+  },
+  mounted () {
+    addListener(this.device.id, this.onMessage)
+    addInjectListener(this.device.id, this.onInjectMessage)
+  },
+  beforeDestroy () {
+    removeListener(this.device.id, this.onMessage)
+    removeInjectListener(this.device.id, this.onInjectMessage)
+  },
+  methods: {
+    onMessage (value) {
+      const multiCard = value[ThirdPartyDevice.MULTI_FUNCTION_CARD]
+      if (multiCard.status === Status.OK) {
+        if (!this.taskType) {
+          const { connectIndex, portIndex } = multiCard.powers[0]
+          const screenPower = multiCard.powers.find(({ type }) => type === '屏体电源')
+          if (screenPower) {
+            this.tag = screenPower.type
+          } else {
+            this.tag = multiCard.powers[0].type
+          }
+          this.taskType = { connectIndex, portIndex }
+          this.onSync()
+        }
+      }
+    },
+    onInjectMessage (message) {
+      if (message.messageId === this.$messageId) {
+        this.$messageId = null
+        if (message.code !== 0) {
+          this.status = 3
+          return
+        }
+        const data = message.data ? JSON.parse(message.data.replaceAll("'", '"')) : {}
+        if (data.logined === false) {
+          this.status = 3
+          return
+        }
+        switch (message.function) {
+          case GET_MULTI_POWER_TIMING:
+            this.status = 1
+            this.sendTasks(data.data)
+            break
+          case SET_MULTI_POWER_TIMING:
+            this.status = 2
+            break
+          default:
+            break
+        }
+      }
+    },
+    onSync () {
+      this.sendTopic(GET_MULTI_POWER_TIMING, { sn: this.device.serialNumber }).then(messageId => {
+        this.$messageId = messageId
+      })
+    },
+    sendTasks (tasks) {
+      const data = []
+      if (tasks.length) {
+        tasks.forEach(({ conditions, ...params }) => {
+          data.push({
+            ...params,
+            conditions: [
+              {
+                ...this.task,
+                type: this.tag
+              },
+              ...conditions.map(({ status, ...item }) => item)
+            ]
+          })
+        })
+      } else {
+        data.push({
+          ...this.taskType,
+          enable: true,
+          conditions: [
+            {
+              ...this.task,
+              type: this.tag
+            }
+          ]
+        })
+      }
+      this.sendTopic(SET_MULTI_POWER_TIMING, {
+        sn: this.device.serialNumber,
+        taskInfo: data
+      }).then(messageId => {
+        this.$messageId = messageId
+        const { action, freqInfo, startTime, endTime, executeTime, type } = this.task
+        savePowerLogger({
+          description: '定时任务',
+          method: this.device.name,
+          params: `启用 ${startTime === endTime ? startTime : `${startTime} - ${endTime}`} ${freqInfo} ${executeTime} ${['开启', '关闭'][action]} ${type}`
+        })
+      })
+    },
+    sendTopic (invoke, inputs) {
+      const timestamp = `${Date.now()}`
+      const messageId = `${invoke}_${timestamp}`
+      return publish(
+        `${this.device.productId}/${this.device.id}/multifunctionCard/invoke`,
+        JSON.stringify({
+          messageId,
+          timestamp,
+          'function': invoke,
+          inputs: JSON.stringify(inputs || [])
+        }),
+        true
+      ).then(() => messageId)
+    }
+  },
+  render (h) {
+    return h('el-tag', {
+      staticClass: 'o-tag u-readonly',
+      props: {
+        size: 'medium',
+        'disable-transitions': true,
+        type: ['primary', 'warning', 'success', 'danger'][this.status]
+      }
+    }, ['同步中', '等待中', '成功', '失败'][this.status])
+  }
+}
+</script>

+ 163 - 6
src/views/device/power/index.vue

@@ -27,12 +27,68 @@
       title="任务详情"
       :schema="detailSchema"
     />
+    <task-dialog
+      ref="taskDialog"
+      title="定时任务"
+      cron
+      single-time
+      @confirm="onSaveTask"
+    >
+      <template #default="{ freq }">
+        <div class="c-grid-form__label u-required">生效日期</div>
+        <el-date-picker
+          v-if="freq === 2"
+          key="taskTime"
+          v-model="taskTime"
+          type="date"
+          placeholder="选择日期"
+          value-format="yyyy-MM-dd"
+          :picker-options="singleTimePickerOptions"
+        />
+        <el-date-picker
+          v-else
+          key="taskDate"
+          v-model="taskDate"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="yyyy-MM-dd"
+          :picker-options="pickerOptions"
+          :clearable="false"
+        />
+        <div class="c-grid-form__label">执行动作</div>
+        <div class="l-flex--row c-grid-form__option">
+          <el-radio
+            v-model="taskAction"
+            :label="0"
+          >
+            开启
+          </el-radio>
+          <el-radio
+            v-model="taskAction"
+            :label="1"
+          >
+            关闭
+          </el-radio>
+        </div>
+      </template>
+    </task-dialog>
+    <table-dialog
+      ref="syncTaskDialog"
+      title="定时任务同步"
+      :schema="asyncSchema"
+    />
   </wrapper>
 </template>
 
 <script>
 import { mapGetters } from 'vuex'
-import { getDevicesWithPower } from '@/api/device'
+import { Frequency } from '@/constant'
+import {
+  parseTime,
+  transformToCron
+} from '@/utils'
 import {
   subscribe,
   unsubscribe
@@ -43,7 +99,12 @@ import {
   getOperationResult,
   getOperationResults
 } from '@/api/platform'
+import { toDate } from '@/utils/event'
+import { getDevicesWithPower } from '@/api/device'
 import DevicePower from './components/DevicePower.vue'
+import DevicePowerTask from './components/DevicePowerTask.vue'
+
+const FOREVER = '4016-06-06'
 
 export default {
   name: 'DeviceList',
@@ -59,14 +120,12 @@ export default {
         buttons: [
           { label: '批量开启电源', on: this.onOpen },
           { label: '批量关闭电源', on: this.onClose },
-          { label: '历史任务', on: this.onViewHistory }
-        ],
-        filters: [
-          { type: 'refresh', label: '刷新' }
+          { label: '历史开关任务', on: this.onViewHistory },
+          { label: '批量定时任务', on: this.onAddTimingTask }
         ],
         cols: [
           { type: 'selection', selectable: this.canSelect },
-          { prop: 'name', label: '设备名称', 'min-width': 120 },
+          { prop: 'name', label: '设备名称', 'min-width': 60 },
           { 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' },
@@ -101,6 +160,51 @@ export default {
             }
           }, width: 160, align: 'center' }
         ]
+      },
+      pickerOptions: {
+        disabledDate: this.isDisableDate,
+        shortcuts: [
+          {
+            text: '本月',
+            onClick (picker) {
+              const end = new Date()
+              const start = new Date()
+              end.setDate(1)
+              end.setMonth(end.getMonth() + 1)
+              end.setDate(0)
+              picker.$emit('pick', [start, end])
+            }
+          },
+          {
+            text: '今年',
+            onClick (picker) {
+              const start = new Date().getFullYear()
+              const startYear = new Date()
+              const endYear = new Date(start, 11, 31)
+              picker.$emit('pick', [startYear, endYear])
+            }
+          },
+          {
+            text: '永久生效',
+            onClick (picker) {
+              const start = new Date()
+              const end = new Date(FOREVER)
+              picker.$emit('pick', [start, end])
+            }
+          }
+        ]
+      },
+      taskTime: '',
+      taskDate: ['', ''],
+      taskAction: 0,
+      asyncSchema: {
+        nonPagination: true,
+        list: this.getSyncDevices,
+        cols: [
+          { prop: 'name', label: '设备名称', 'min-width': 60 },
+          { label: '状态', render: (device, h) => h(DevicePowerTask, { props: { device, task: this.$task } }), width: 120, align: 'center' },
+          { prop: 'address', label: '地址' }
+        ]
       }
     }
   },
@@ -124,6 +228,9 @@ export default {
     ], this.onMessage)
   },
   methods: {
+    isDisableDate (date) {
+      return date < Date.now()
+    },
     onMessage (topic) {
       const result = /^\d+\/(\d+)\/(online|offline)$/.exec(topic)
       if (!result) {
@@ -245,6 +352,56 @@ export default {
         }
         return { data: data.taskDetails }
       })
+    },
+    onAddTimingTask () {
+      if (!this.$selectionItems?.length) {
+        this.$message({
+          type: 'warning',
+          message: '请先选择需要操作的设备'
+        })
+        return
+      }
+      const today = parseTime(new Date(), '{y}-{m}-{d}')
+      this.taskTime = today
+      this.taskDate = [today, today]
+      this.taskAction = 0
+      this.$refs.taskDialog.show()
+    },
+    onSaveTask ({ value, done }) {
+      const { freq, executeTime, dayOfWeek = '' } = value
+      if (freq !== Frequency.ONCE && (!this.taskDate[0] || !this.taskDate[1]) || freq === Frequency.ONCE && !this.taskTime) {
+        this.$message({
+          type: 'warning',
+          message: '请选择生效日期'
+        })
+        return
+      }
+      const endTime = freq === Frequency.ONCE ? this.taskTime : this.taskDate[1]
+      if (this.isExpired({ endTime, executeTime })) {
+        this.$message({
+          type: 'warning',
+          message: '生效时间已过期,请重新选择'
+        })
+        return
+      }
+      const startTime = freq === Frequency.ONCE ? this.taskTime : this.taskDate[0]
+      this.$task = {
+        enable: true,
+        powerIndex: 0,
+        action: this.taskAction,
+        startTime,
+        endTime,
+        flag: `${Date.now()}`,
+        cron: [transformToCron(executeTime, freq, startTime, dayOfWeek)]
+      }
+      done()
+      this.$refs.syncTaskDialog.show()
+    },
+    isExpired ({ endTime, executeTime }) {
+      return endTime !== FOREVER && toDate(`${endTime} ${executeTime}`) <= Date.now()
+    },
+    getSyncDevices () {
+      return Promise.resolve({ data: Object.freeze([...this.$selectionItems]) })
     }
   }
 }

+ 1 - 1
src/views/external/box/components/Device.vue

@@ -259,7 +259,7 @@ export default {
           productId: '',
           serialNumber: '',
           mac: '',
-          range: ['00:00:00', '23:59:59'],
+          range: ['08:00:00', '22:00:00'],
           address: '',
           longitude: '',
           latitude: '',

+ 7 - 7
src/views/screen/review/workflow/api.js

@@ -74,21 +74,21 @@ export function getWorkflow (workflowId) {
 
 export function getWorkflowHistory (workflowId) {
   return request({
-    url: `/workflow/${workflowId}/history `,
+    url: `/workflow/${workflowId}/history`,
     method: 'GET'
   })
 }
 
 export function resolveFirstLevel (workflowId) {
   return send({
-    url: `/workflow/first/${workflowId}/reviewed `,
+    url: `/workflow/first/${workflowId}/reviewed`,
     method: 'POST'
   })
 }
 
 export function rejectFirstLevel (workflowId, reason) {
   return send({
-    url: `/workflow/first/${workflowId}/reject `,
+    url: `/workflow/first/${workflowId}/reject`,
     method: 'POST',
     data: { reason }
   })
@@ -96,14 +96,14 @@ export function rejectFirstLevel (workflowId, reason) {
 
 export function resolveSecondLevel (workflowId) {
   return send({
-    url: `/workflow/second/${workflowId}/reviewed `,
+    url: `/workflow/second/${workflowId}/reviewed`,
     method: 'POST'
   })
 }
 
 export function rejectSecondLevel (workflowId, reason) {
   return send({
-    url: `/workflow/second/${workflowId}/reject `,
+    url: `/workflow/second/${workflowId}/reject`,
     method: 'POST',
     data: { reason }
   })
@@ -111,14 +111,14 @@ export function rejectSecondLevel (workflowId, reason) {
 
 export function resolveFinal (workflowId) {
   return send({
-    url: `/workflow/third/${workflowId}/approval  `,
+    url: `/workflow/third/${workflowId}/approval`,
     method: 'POST'
   })
 }
 
 export function rejectFinal (workflowId, reason) {
   return send({
-    url: `/workflow/third/${workflowId}/reject `,
+    url: `/workflow/third/${workflowId}/reject`,
     method: 'POST',
     data: { reason }
   })