Forráskód Böngészése

feat: pdu状态检测+定时配置

lihao16 1 napja
szülő
commit
ddd276bd55

+ 43 - 0
src/api/external.js

@@ -354,6 +354,49 @@ export function pduOutletsStatus (id) {
   })
 }
 
+// PDU Task
+export function getPduTasks (query) {
+  const { pageNum: pageIndex, pageSize, ...params } = query
+  return request({
+    url: '/device/pdu/task/list',
+    method: 'GET',
+    params: addTenant({
+      pageIndex, pageSize,
+      ...params
+    })
+  })
+}
+
+export function addPduTask (data, deviceId) {
+  return add({
+    url: `/device/pdu/task/${deviceId}`,
+    method: 'POST',
+    data
+  })
+}
+
+export function updatePduTask (data) {
+  return update({
+    url: '/device/pdu/task',
+    method: 'PUT',
+    data
+  })
+}
+
+export function deletePduTask ({ id }) {
+  return del({
+    url: `/device/pdu/task/${id}`,
+    method: 'DELETE'
+  }, '该定时任务')
+}
+
+export function submitPduTasks ({ deviceId }) {
+  return request({
+    url: `/device/pdu/task/submit/${deviceId}`,
+    method: 'GET'
+  })
+}
+
 export function plcCommand (deviceId, status) {
   return messageSend({
     url: '/device/thirdplc/command',

+ 277 - 0
src/components/dialog/PduTaskDialog/index.vue

@@ -0,0 +1,277 @@
+<template>
+  <confirm-dialog
+    ref="dialog"
+    append-to-body
+    v-bind="$attrs"
+    @confirm="onSave"
+  >
+    <template #default>
+      <div
+        v-for="(task, index) in tasks"
+        :key="index"
+        class="c-sibling-item--v c-grid-form u-align-self--center"
+      >
+        <div class="l-flex--row c-grid-form__row c-grid-form__option u-font-size--sm u-color--black u-bold">
+          <i
+            v-if="multi && tasks.length > 1"
+            class="c-sibling-item el-icon-delete has-active"
+            @click="onRemoveBlock(index)"
+          />
+        </div>
+        <div class="c-grid-form__label c-grid-form__auto">
+          插座
+        </div>
+        <el-radio-group
+          v-model="task.slotNo"
+          class="l-flex--row c-grid-form__auto"
+          size="mini"
+          fill="#1c5cb0"
+          :min="1"
+        >
+          <el-radio-button
+            v-for="slotNo in slotNos"
+            :key="slotNo.value"
+            :label="slotNo.label"
+            :value="slotNo.value"
+          >
+            插座{{ slotNo.value }}
+          </el-radio-button>
+        </el-radio-group>
+        <div class="c-grid-form__label">
+          生效日期
+        </div>
+        <el-date-picker
+          v-model="task.date"
+          class="u-width--lg"
+          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 style="display: flex; flex-wrap: wrap; gap: 6px;">
+          <div
+            v-for="(item, tindex) in task.executeTime"
+            :key="tindex"
+            class="l-flex--row inline"
+          >
+            <template v-if="strictly">
+              <el-time-picker
+                v-model="item.time"
+                class="c-sibling-item u-width--lg"
+                is-range
+                range-separator="-"
+                :start-placeholder="startPlaceholder"
+                :end-placeholder="endPlaceholder"
+                value-format="HH:mm:00"
+                format="HH:mm"
+                placeholder="选择时间范围"
+                clearable
+              />
+            </template>
+            <template v-else>
+              <el-time-picker
+                v-model="item.start"
+                class="u-width--xs"
+                :placeholder="startPlaceholder"
+                value-format="HH:mm:00"
+                format="HH:mm"
+                clearable
+                @change="onTimeChanged(item)"
+              />
+              <span class="has-padding--h">
+                -
+              </span>
+              <el-time-picker
+                v-model="item.end"
+                class="c-sibling-item u-width--xs"
+                :placeholder="endPlaceholder"
+                value-format="HH:mm:00"
+                format="HH:mm"
+                clearable
+                @change="onTimeChanged(item)"
+              />
+            </template>
+            <el-tooltip
+              v-if="item.warning"
+              placement="top"
+              effect="dark"
+              :content="item.warning"
+            >
+              <i class="c-sibling-item el-icon-warning u-color--error dark u-font-size--md" />
+            </el-tooltip>
+            <i
+              v-if="task.executeTime.length > 1"
+              class="c-sibling-item el-icon-delete u-font-size--md has-active"
+              @click="onRemoveTime(index, tindex)"
+            />
+          </div>
+          <!--          <i
+            class="c-grid-form__option l-flex&#45;&#45;row el-icon-circle-plus-outline u-font-size&#45;&#45;md has-active"
+            style="margin-left: 6px;"
+            @click="onAddTime(index)"
+          />-->
+        </div>
+      </div>
+    </template>
+  </confirm-dialog>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import {
+  FOREVER,
+  ONE_DAY
+} from '@/constant.js'
+import { parseTime } from '@/utils'
+import { toDate } from '@/utils/event.js'
+
+export default {
+  name: 'PduTaskDialog',
+  props: {
+    multi: {
+      type: Boolean,
+      default: true
+    },
+    startPlaceholder: {
+      type: String,
+      default: '开启时间'
+    },
+    endPlaceholder: {
+      type: String,
+      default: '关闭时间'
+    },
+    strictly: {
+      type: [Boolean, String],
+      default: false
+    }
+  },
+  data () {
+    return {
+      slotNos: this.getSlotNos(),
+      pickerOptions: {
+        disabledDate: this.isDisableDate
+      },
+      tasks: []
+    }
+  },
+  computed: {
+    ...mapGetters(['account'])
+  },
+  methods: {
+    getSlotNos () {
+      return [
+        { value: '1', label: '1' },
+        { value: '2', label: '2' },
+        { value: '3', label: '3' },
+        { value: '4', label: '4' }
+      ]
+    },
+    isDisableDate (date) {
+      return date <= Date.now() - ONE_DAY
+    },
+    isExpired (endDate) {
+      return endDate !== FOREVER && toDate(`${endDate} 00:00:00`) <= Date.now() - ONE_DAY
+    },
+    show ({ startDate, endDate, executeTime, taskId } = {}) {
+      console.log(startDate, endDate, executeTime, taskId)
+      const today = parseTime(new Date(), '{y}-{m}-{d}')
+      this.tasks = [{
+        date: startDate && endDate ? [startDate, endDate] : [today, today],
+        executeTime: executeTime
+          ? executeTime.map(this.createTime)
+          : [this.createTime()],
+        flag: Date.now(),
+        taskId,
+        slotNo: '1'
+      }]
+      this.$refs.dialog.show()
+    },
+    createTime (data) {
+      const start = data?.start?.replace(/\d{2}$/, '00') || ''
+      const end = data?.end?.replace(/\d{2}$/, '00') || ''
+      return this.strictly
+        ? { time: start && end ? [start, end] : null, warning: '' }
+        : { start, end, warning: '' }
+    },
+    onAddBlock () {
+      const today = parseTime(new Date(), '{y}-{m}-{d}')
+      this.tasks.push({
+        flag: Date.now(),
+        date: [today, today],
+        executeTime: [this.createTime()],
+        slotNo: '1'
+      })
+    },
+    onRemoveBlock (index) {
+      this.tasks.splice(index, 1)
+    },
+    onAddTime (index) {
+      this.tasks[index].executeTime.push(this.createTime())
+    },
+    onRemoveTime (index, tindex) {
+      this.tasks[index].executeTime.splice(tindex, 1)
+    },
+    onTimeChanged (executeTime) {
+      const { start, end } = executeTime
+      if (start && end && start >= end) {
+        executeTime.warning = '开启时间必须先于关闭时间'
+      } else {
+        executeTime.warning = ''
+      }
+    },
+    hasTime (data) {
+      return this.strictly
+        ? !!data.time
+        : !!data.start || !!data.end
+    },
+    transformTime (data) {
+      return this.strictly
+        ? `${data.time[0]}_${data.time[1]}`
+        : `${data.start || ''}_${data.end || ''}`
+    },
+    onSave (done) {
+      console.log(this.tasks)
+      if (this.tasks.some(({ executeTime }) => executeTime.some(({ warning }) => !!warning))) {
+        this.$message({
+          type: 'warning',
+          message: '请先完成异常数据的修改'
+        })
+        return
+      }
+      const tasks = this.tasks.filter(({ date, executeTime }) => !this.isExpired(date[1]) && executeTime.some(this.hasTime))
+      if (!tasks.length) {
+        this.$message({
+          type: 'warning',
+          message: '请至少添加一个有效执行时间'
+        })
+        return
+      }
+      this.$emit('confirm', {
+        value: tasks.sort((a, b) => b.flag - a.flag).map(({ flag, date, executeTime, slotNo, taskId }) => {
+          return {
+            flag: `${this.account}_${flag}`,
+            startDate: date[0],
+            endDate: date[1],
+            slotNo,
+            taskId,
+            executeTime: [...new Set(executeTime.filter(this.hasTime).map(this.transformTime))].sort().map(time => {
+              const timeArr = time.split('_')
+              return {
+                start: timeArr[0] || '',
+                end: timeArr[1] || ''
+              }
+            })
+          }
+        }),
+        done
+      })
+    }
+  }
+}
+</script>

+ 59 - 4
src/utils/adapter/monitor.js

@@ -69,17 +69,26 @@ export function startMonitor () {
 
   client.on('message', (topic, payload) => {
     console.log('Monitor topic', topic)
-    const result = /^(\d+)\/(\d+)\/(screen|multifunctionCard\/invoke\/reply)$/.exec(topic)
+    const result = /^(\d+)\/(\d+)\/(screen|multifunctionCard\/invoke\/reply|pdu)$/.exec(topic)
     if (!result) {
       return
     }
     const id = result[2]
     console.log('monitor cache', id)
-    const message = JSON.parse(decodePayload(topic, payload))
+    let message = null
+    try {
+      message = JSON.parse(decodePayload(topic, payload))
+    } catch (e) {
+      console.log('monitor error', e)
+      return
+    }
+    // const message = utf8ArrayBufferToString(topic, payload)
 
     const timestamp = Number(message.timestamp) || Date.now()
 
     if (result[3] === 'screen') {
+      // message = JSON.parse(decodePayload(topic, payload))
+      console.log('monitor screen message', message)
       const {
         versionName,
         versionCode,
@@ -106,6 +115,38 @@ export function startMonitor () {
       return
     }
 
+    if (result[3] === 'pdu') {
+      // message = JSON.parse(utf8ArrayBufferToString(payload))
+      console.log('monitor pdu message', message)
+      // PDU 数据处理逻辑 - 仅读取第一个数据
+      /* const messageJson = JSON.parse(message)
+      const firstSocket = messageJson && messageJson.data.length > 0 ? messageJson.data[0] : null
+      console.log('firstSocket', firstSocket)
+      emit(id, ThirdPartyDevice.PDU, {
+        timestamp,
+        socket: firstSocket
+          ? {
+            current: firstSocket['current(A)'],
+            voltage: firstSocket['voltage(V)'],
+            power: firstSocket['power(kW)'],
+            powerFactor: firstSocket.power_factor,
+            slotNo: firstSocket.slot_no,
+            name: firstSocket.name,
+            state: firstSocket.state,
+            locked: firstSocket.locked,
+            energy: firstSocket['energy(kWh)']
+          }
+          : null
+      }) */
+      if (message.success) {
+        emit(id, ThirdPartyDevice.PDU, {
+          ...message.data
+        })
+      }
+      return
+    }
+    // message = JSON.parse(decodePayload(topic, payload))
+    console.log('monitor card message', message)
     if (message.function === GET_POWER_STATUS) {
       const data = getPowerStatusByMessage(message)
       if (data.success) {
@@ -153,6 +194,10 @@ function getCacheById (id) {
         status: Status.LOADING,
         receivers: []
       },
+      [ThirdPartyDevice.PDU]: {
+        status: Status.LOADING,
+        receivers: []
+      },
       screen: null
     })
   }
@@ -178,6 +223,13 @@ function getFreshCacheById (id) {
       }
       update = true
     }
+    if (cache[ThirdPartyDevice.PDU].status > Status.LOADING && (Date.now() - cache[ThirdPartyDevice.PDU].timestamp > EXPIRED_MILLISECOND)) {
+      cache[ThirdPartyDevice.PDU] = {
+        status: Status.LOADING,
+        receivers: []
+      }
+      update = true
+    }
     if (cache.screen && (Date.now() - cache.screen.timestamp > EXPIRED_MILLISECOND)) {
       cache.screen = null
       update = true
@@ -232,7 +284,8 @@ function doSubscribe () {
         subscribeIds.forEach(id => {
           topics.push(
             `+/${id}/screen`,
-            `+/${id}/multifunctionCard/invoke/reply`
+            `+/${id}/multifunctionCard/invoke/reply`,
+            `+/${id}/pdu`
           )
         })
         client.subscribe(topics)
@@ -267,6 +320,7 @@ export function addListener (id, cb) {
   cbSet.add(cb)
   checkStatus(id, ThirdPartyDevice.MULTI_FUNCTION_CARD)
   checkStatus(id, ThirdPartyDevice.RECEIVING_CARD)
+  checkStatus(id, ThirdPartyDevice.PDU)
   cb(cache)
 }
 
@@ -279,7 +333,8 @@ function doUnsubscribe () {
         unsubscribeIds.forEach(id => {
           topics.push(
             `+/${id}/screen`,
-            `+/${id}/multifunctionCard/invoke/reply`
+            `+/${id}/multifunctionCard/invoke/reply`,
+            `+/${id}/pdu`
           )
           cbMap.delete(id)
           injectMap.delete(id)

+ 4 - 0
src/utils/adapter/nova.js

@@ -29,6 +29,10 @@ export const SET_RELAY_POWER_STATUS = 'SetRelayPowerStatusAsync' // 9.29.1.7、
 export const SET_POWER_MODE = 'SetPowerModeAsync' // 9.29.2.1、设置终端电源模式
 export const GET_POWER_MODE = 'GetPowerModeAsync' // 9.29.2.2、获取终端电源模式
 
+// PDU
+export const GET_PDU_STATUS = 'GetPDUStatusAsync'
+export const GET_PDU_TASK = 'GetPDUTaskAsync'
+
 export function getPowerStatusByMessage (message) {
   const data = checkMessage(message)
   if (!data.success) {

+ 7 - 1
src/utils/mqtt.js

@@ -135,8 +135,14 @@ export function decodePayload (topic, payload) {
 }
 
 const decoder = new TextDecoder('utf-8')
+// eslint-disable-next-line consistent-return
 function utf8ArrayBufferToString (buffer) {
-  return decoder.decode(buffer)
+  try {
+    return decoder.decode(buffer)
+  } catch (e) {
+    console.log('utf8ArrayBufferToString', e)
+    return null
+  }
 }
 
 function decodeMessage (topic, arrayBuffer, message) {

+ 5 - 0
src/views/dashboard/components/DeviceCard.vue

@@ -115,6 +115,11 @@
         class="l-flex__auto l-flex--row u-font-size u-bold u-text--center"
         v-html="statusInfo"
       />
+      <div
+        v-if="hasPdu && !current"
+        class="l-flex__auto l-flex--row u-font-size u-bold u-text--center"
+        v-html="pduStatusInfo"
+      />
     </div>
     <div class="l-flex__none l-flex--row c-sibling-item--v o-device__next u-color--black light u-font-size--xs u-text--center">
       <auto-text

+ 17 - 2
src/views/dashboard/components/DeviceCardSimple.vue

@@ -7,7 +7,9 @@
   >
     <div class="l-flex__none l-flex--row c-sibling-item--v o-device__block">
       <div class="l-flex__none l-flex--row center c-sibling-item o-device__status u-font-size">
-        <template v-if="hasStatus">●</template>
+        <template v-if="hasStatus">
+          ●
+        </template>
         <i
           v-else
           class="el-icon-loading"
@@ -35,8 +37,12 @@
         class="l-flex__fill c-sibling-item o-device__info"
         :class="statusInfoColorClass"
       >
+        <span :class="pduStatusInfoColorClass">
+          {{ pduStatusInfo }}
+        </span>
         {{ statusInfo }}
       </div>
+
       <div
         v-if="isOnline && volume > -1"
         class="l-flex__none l-flex--row inline c-sibling-item near o-device__volume u-color--white has-active"
@@ -50,7 +56,9 @@
             class="c-sibling-item"
             icon-class="volume"
           />
-          <span class="c-sibling-item nearest">{{ volumeTip }}</span>
+          <span class="c-sibling-item nearest">
+            {{ volumeTip }}
+          </span>
         </template>
       </div>
     </div>
@@ -92,6 +100,13 @@ export default {
         : this.isOnline && !this.hasPower
           ? 'u-color--info'
           : ''
+    },
+    pduStatusInfoColorClass () {
+      return this.hasPdu && this.hasPduStatus && !this.isPduOpened
+        ? 'u-color--info'
+        : this.isOnline && this.hasPduStatus && this.isPduOpened
+          ? 'u-color--success dark'
+          : ''
     }
   },
   methods: {

+ 47 - 2
src/views/dashboard/components/mixins/device.js

@@ -19,7 +19,10 @@ export default {
       powerStatus: Status.LOADING,
       powerSwitchStatus: Power.LOADING,
       hasPower: true,
-      volume: -1
+      volume: -1,
+      pduStatus: Status.LOADING,
+      pduSwitchStatus: Power.LOADING,
+      hasPdu: false
     }
   },
   computed: {
@@ -38,14 +41,20 @@ export default {
     hasStatus () {
       return !this.isOnline || !this.hasPower || this.hasPowerRealStatus
     },
+    hasPduStatus () {
+      return this.pduStatus !== Status.LOADING
+    },
     isPowerOpened () {
       return this.hasPowerRealStatus && this.powerStatus === Status.OK && this.powerSwitchStatus !== Power.OFF
     },
+    isPduOpened () {
+      return this.hasPdu && this.pduStatus === Status.OK && this.pduSwitchStatus !== Power.OFF
+    },
     statusInfo () {
       return this.hasStatus
         ? this.hasPower && this.hasPowerRealStatus
           ? this.powerStatus === Status.WARNING
-            ? `电源状态${this.powerSwitchStatus === Power.LOADING ? '检测' : ''}异常`
+            ? `屏幕开启状态${this.powerSwitchStatus === Power.LOADING ? '检测' : ''}异常`
             : this.isPowerOpened
               ? '屏幕已开启'
               : '屏幕未开启'
@@ -58,6 +67,15 @@ export default {
               : '当前播控器已离线'
         : '检测中...'
     },
+    pduStatusInfo () {
+      return this.hasPdu
+        ? this.pduStatus === Status.WARNING
+          ? `电源状态${this.pduSwitchStatus === Power.LOADING ? '检测' : ''}异常`
+          : this.isPduOpened
+            ? '电源已开启'
+            : '电源未开启'
+        : ''
+    },
     volumeTip () {
       if (this.volume > -1) {
         return parseVolume(this.volume)
@@ -90,6 +108,33 @@ export default {
       if (value.screen) {
         this.volume = value.screen.volume
       }
+      const pdus = value[ThirdPartyDevice.PDU]
+      console.log('pdus', pdus)
+      if (pdus) {
+        try {
+          const pdu = value[ThirdPartyDevice.PDU][0]
+          // const socket = pdu.socket
+          const pduStatus = pdu.state
+          switch (pduStatus) {
+            case 'ON':
+              this.pduStatus = Status.OK
+              this.pduSwitchStatus = Power.ON
+              break
+            case 'OFF':
+              this.pduStatus = Status.OK
+              this.pduSwitchStatus = Power.OFF
+              break
+            default:
+              this.pduStatus = Status.WARNING
+          }
+          this.hasPdu = this.pduStatus > Status.NONE
+          console.log('pduStatus:', this.pduStatus)
+          console.log('pduSwitchStatus:', this.pduSwitchStatus)
+          console.log('hasPdu:', this.hasPdu)
+        } catch (e) {
+          this.pduStatus = Status.WARNING
+        }
+      }
       const multiCard = value[ThirdPartyDevice.MULTI_FUNCTION_CARD]
       const powerStatus = multiCard.status
       this.powerStatus = powerStatus

+ 4 - 2
src/views/device/detail/components/DeviceAlarm.vue

@@ -29,7 +29,8 @@ const DeviceAlarmTypes = [
   { value: 2, label: '屏幕监测' },
   { value: 3, label: '摄像头状态' },
   { value: 4, label: '传感器预警' },
-  { value: 5, label: '电源管理' }
+  { value: 5, label: '电源管理' },
+  { value: 6, label: '人流数据异常' }
 ]
 
 const DeviceAlarmType = {
@@ -37,7 +38,8 @@ const DeviceAlarmType = {
   2: [0, 2, 3, 4, 5, 10, 11, 13, 35],
   3: [7, 8, 33, 34],
   4: [18, 19, 20, 21, 22, 23],
-  5: [36, 37, 38, 39, 42]
+  5: [36, 37, 38, 39, 42],
+  6: [69]
 }
 
 export default {

+ 794 - 0
src/views/device/detail/components/DeviceInvoke/PduSchedule.vue

@@ -0,0 +1,794 @@
+<template>
+  <div class="l-flex--col center has-border radius has-padding">
+    <i
+      class="o-icon lg u-pointer"
+      :class="iconClass"
+      @click="invoke"
+    />
+    <div class="has-padding u-color--black u-bold">
+      {{ powerStatusTip }}
+    </div>
+    <c-dialog
+      ref="dialog"
+      size="lg"
+      title="PDU定时"
+      :before-close="handleClose"
+      @close="onClose"
+    >
+      <template #default>
+        <tabbar
+          :items="tabs"
+          :active="active"
+          @click="onClickTab"
+        >
+          <div
+            v-if="hasChanged"
+            class="u-font-size--sm u-color--error has-padding--h"
+          >
+            设置需点击【应用】后生效
+          </div>
+        </tabbar>
+        <div
+          v-loading="loading"
+          class="l-flex__auto l-flex--col"
+        >
+          <schema-table
+            :key="active"
+            ref="table"
+            :schema="schema"
+          />
+        </div>
+      </template>
+    </c-dialog>
+    <pdu-task-dialog
+      ref="pduTaskDialog"
+      :title="dialogTitle"
+      :multi="isAdd"
+      @confirm="onSave"
+    >
+      <template #default>
+        <div class="c-grid-form u-align-self--center">
+          <template v-if="isAdd">
+            <schema-select
+              v-model="taskType"
+              class="u-width--sm"
+              :schema="typeSelectSchema"
+            />
+          </template>
+          <div
+            v-else
+            class="l-flex--row c-grid-form__option u-color--blue u-bold"
+          >
+            {{ taskType }}
+          </div>
+        </div>
+      </template>
+    </pdu-task-dialog>
+  </div>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import {
+  FOREVER, ONE_DAY, ThirdPartyDevice
+} from '@/constant.js'
+import {
+  parseTime, transformScreenTaskInfo
+} from '@/utils'
+import {
+  addInjectListener,
+  addListener,
+  GET_PDU_STATUS,
+  GET_PDU_TASK,
+  removeInjectListener,
+  removeListener,
+  Status
+} from '@/utils/adapter'
+import { toDate } from '@/utils/event.js'
+import baseMixin from './mixins/base.js'
+import {
+  addPduTask, deletePduTask, getPduTasks, submitPduTasks, updatePduTask
+} from '@/api/external'
+
+/* const ErrorMessage = {
+  TIMEOUT: '暂未获取到操作反馈,请回读查看',
+  TIMEOUT_RETRY: '暂未获取到操作反馈,请稍后重试',
+  DEFAULT: '操作异常,请稍后重试',
+  BUSY: '终端被他人占用',
+  PASSWORD: '登录密码错误,请联系管理员',
+  [GET_PDU_STATUS]: '获取电源状态超时,请稍后重试',
+  [GET_PDU_TASK]: '获取定时任务超时,请回读查看'
+} */
+
+export default {
+  name: 'PduPowerSwitch',
+  mixins: [baseMixin],
+  data () {
+    return {
+      topic: '/multifunctionCard/invoke',
+      powerStatus: Status.LOADING,
+      active: GET_PDU_STATUS,
+      tabs: [
+        { key: GET_PDU_STATUS, name: '电源状态' }
+      ],
+      powerSchema: {
+        props: {
+          size: 'small'
+        },
+        nonPagination: true,
+        list: this.getPowers,
+        /* buttons: [
+          { label: '一键开启', on: this.onSwitchOpen },
+          { label: '一键关闭', on: this.onSwitchClose }
+        ], */
+        cols: [
+          { prop: 'slot_no', label: '序号', 'align': 'center' },
+          { prop: 'name', label: '电源端口', 'align': 'center' },
+          { prop: 'state', label: '状态', 'align': 'center' }
+        ]
+      },
+      typeSelectSchema: { options: [] },
+      actionInfo: ['开启', '关闭'],
+      timingStatus: 0,
+      hasChanged: false,
+      isAdd: true,
+      taskType: ''
+    }
+  },
+  computed: {
+    ...mapGetters(['account']),
+    powerStatusTip () {
+      switch (this.powerStatus) {
+        case Status.OK:
+          return 'PDU定时'
+        default:
+          return '检测PDU中'
+      }
+    },
+    iconClass () {
+      return this.powerStatus === Status.OK ? 'ok' : ''
+    },
+    loading () {
+      return this.active !== GET_PDU_STATUS && !this.timingStatus
+    },
+    schema () {
+      switch (this.active) {
+        case GET_PDU_STATUS:
+          return this.powerSchema
+        case GET_PDU_TASK:
+          return this.taskSchema
+        default:
+          return null
+      }
+    },
+    taskSchema () {
+      return {
+        props: {
+          size: 'small'
+        },
+        nonPagination: true,
+        list: () =>
+          // 传递 device.id 作为参数
+          // eslint-disable-next-line implicit-arrow-linebreak
+          getPduTasks({ deviceId: this.device.id })
+            .then(result => {
+              // 在接口调用结束后结束 loading
+              this.timingStatus = 1
+              return result
+            })
+            .catch(error => {
+              // 处理错误情况也要结束 loading
+              this.timingStatus = 1
+              throw error
+            }),
+        buttons: [
+          { type: 'add', label: '新增', on: this.onAdd },
+          { label: '应用', render: () => this.hasChanged, on: this.onSubmitPowerTasks }
+        ].filter(Boolean),
+        filters: [].filter(Boolean),
+        cols: [
+          {
+            prop: 'slotNo',
+            label: '插座序号',
+            align: 'center',
+            render: task => task.slotNo || '-'
+          },
+          {
+            prop: 'startDate',
+            label: '开始日期',
+            render: ({ startDate }) => startDate ? startDate.split(' ')[0] : '-'
+          },
+          {
+            prop: 'endDate',
+            label: '结束日期',
+            render: ({ endDate }) => endDate ? endDate.split(' ')[0] : '-'
+          },
+          {
+            label: '开屏时间',
+            align: 'center',
+            render: ({ powerOnTime }) => powerOnTime || '-',
+            'show-overflow-tooltip': false
+          },
+          {
+            label: '关屏时间',
+            align: 'center',
+            render: ({ powerOffTime }) => powerOffTime || '-',
+            'show-overflow-tooltip': false
+          },
+          {
+            type: 'tag', render: task => this.isExpired(task)
+              ? { type: 'warning', label: '已过期' }
+              : task.status === 1
+                ? { type: 'success', label: '启用' }
+                : { type: 'danger', label: '停用' }
+          },
+          {
+            type: 'invoke',
+            render: [
+              { label: ({ status }) => status === 1 ? '停用' : '启用', on: this.onToggle },
+              { label: '编辑', on: this.onEdit },
+              { label: '删除', on: this.onDel }
+            ],
+            width: 140
+          }
+        ]
+      }
+    },
+    dialogTitle () {
+      return this.isAdd ? '新增定时任务' : '编辑定时任务'
+    }
+  },
+  mounted () {
+    addListener(this.device.id, this.onCacheMessage)
+    addInjectListener(this.device.id, this.onMessage)
+  },
+  beforeDestroy () {
+    this.$openDialog = false
+    removeListener(this.device.id, this.onCacheMessage)
+    removeInjectListener(this.device.id, this.onMessage)
+  },
+  methods: {
+    isExpired ({ endDate }) {
+      // 如果是 FOREVER 常量,则永不过期
+      if (endDate === FOREVER) {
+        return false
+      }
+      // 从日期字符串中提取日期部分
+      const dateStr = typeof endDate === 'string' ? endDate.split(' ')[0] : endDate
+      // 判断是否过期
+      return toDate(`${dateStr} 00:00:00`) <= Date.now() - ONE_DAY
+    },
+    handleClose (done) {
+      if (this.hasChanged) {
+        this.$confirm(
+          '设置未保存,是否放弃修改?<p class="u-color--error">若需保存请点击【应用】</p>',
+          '温馨提示',
+          {
+            dangerouslyUseHTMLString: true,
+            type: 'warning',
+            confirmButtonText: '放弃'
+          }
+        )
+          .then(() => {
+            this.$openDialog = false
+            done()
+          })
+        return
+      }
+      this.$openDialog = false
+      done()
+    },
+    onClickTab (val) {
+      if (this.hasChanged) {
+        this.$confirm(
+          '设置未保存,是否放弃修改?<p class="u-color--error">若需保存请点击【应用】</p>',
+          '温馨提示',
+          {
+            dangerouslyUseHTMLString: true,
+            type: 'warning',
+            confirmButtonText: '放弃'
+          }
+        )
+          .then(() => {
+            this.hasChanged = false
+            this.onClickTab(val)
+          })
+        return
+      }
+      if (val !== GET_PDU_STATUS && (val !== this.active || !this.timingStatus)) {
+        this.getTasksByKey(val)
+      }
+      this.active = val
+    },
+    invoke () {
+      if (this.powerStatus !== Status.OK) {
+        this.$message({
+          type: 'warning',
+          message: '获取PDU信息中,请稍后尝试'
+        })
+        return
+      }
+      this.$openDialog = true
+      this.getPowerStatus()
+    },
+    onCacheMessage (value) {
+      try {
+        console.log('cache message', value)
+        const pdu = value[ThirdPartyDevice.PDU]
+        const socket = pdu[0]
+        const pduStatus = socket.state
+        this.$pdu = pdu
+        console.log('pdu schedule socket', socket)
+        if (pduStatus === 'ON' || pduStatus === 'OFF') {
+          this.powerStatus = Status.OK
+        } else {
+          this.powerStatus = Status.LOADING
+        }
+      } catch (e) {
+        console.log('cache pdu message error', e)
+      }
+    },
+    /* fetchPowerStatus () {
+      this.sendTopic(
+        GET_PDU_STATUS,
+        JSON.stringify({ sn: this.device.serialNumber }),
+        true
+      )
+    }, */
+    onMessage (message) {
+      console.log('on message', message)
+    },
+    onClose () {
+      this.$powers = []
+      this.$taskPorts = []
+      this.$tasks = []
+      this.closeAsyncLoading()
+    },
+    getPowerStatus () {
+      this.$powers = []
+      this.hasChanged = false
+      console.log('this.$pdu', this.$pdu)
+      this.setCachePowerStatus(this.$pdu)
+      if (this.$openDialog) {
+        this.$refs.dialog?.show()
+      }
+      this.$refs.table?.pageTo(1)
+    },
+    setCachePowerStatus (powersData) {
+      const map = {}
+      const options = []
+      console.log('powersData', powersData)
+
+      // 将对象转换为数组
+      let powersArray = []
+      if (powersData && typeof powersData === 'object' && !Array.isArray(powersData)) {
+        // 提取数字键的值组成数组
+        powersArray = Object.keys(powersData)
+          .filter(key => key !== 'timestamp')
+          .map(key => powersData[key])
+      } else if (Array.isArray(powersData)) {
+        powersArray = powersData
+      }
+
+      console.log('powersArray', powersArray)
+
+      if (powersArray.length === 0) {
+        return
+      }
+
+      const timestamp = parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}')
+      this.$powers = [{
+        connectIndex: powersArray[0].slot_no,
+        portIndex: powersArray[0].slot_no,
+        powers: powersArray.map(power => {
+          const { connectIndex, portIndex, type } = power
+          if (type) {
+            if (!map[type]) {
+              map[type] = {
+                connectIndex,
+                portIndex,
+                type
+              }
+              options.push({
+                value: type,
+                label: type
+              })
+            }
+          }
+          return {
+            timestamp,
+            ...power
+          }
+        })
+      }]
+      this.tabs = [
+        { key: GET_PDU_STATUS, name: '电源状态' },
+        { key: GET_PDU_TASK, name: '定时控制' }
+      ]
+      this.$typeMap = map
+      this.typeSelectSchema = { options }
+      this.$refs.table?.pageTo(1)
+    },
+    getPowers () {
+      const data = []
+      this.$powers.forEach(({ powers }) => {
+        powers.forEach(power => {
+          data.push(power)
+        })
+      })
+      return Promise.resolve({ data })
+    },
+    /* onSwitchPower (power) {
+      const { powerIndex, type, action } = power
+      const targetAction = action ^ 1
+      this.$confirm(
+        `立即${this.actionInfo[targetAction]}电源 ${powerIndex} ${type}?<p class="u-color--blue">确认操作后请等待数据同步</p>`,
+        '操作确认',
+        {
+          dangerouslyUseHTMLString: true,
+          type: 'warning'
+        }
+      )
+        .then(() => {
+          savePowerLogger({
+            description: `手动${this.actionInfo[targetAction]} 设备【${this.device.name}】 ${type} 端口${powerIndex}`,
+            method: `${this.actionInfo[targetAction]}电源`,
+            params: `${this.device.id} ${this.device.name}`
+          })
+          sendDeviceAlarm({
+            deviceId: this.device.id,
+            errorEnumId: targetAction ? 36 : 37,
+            message: `用户【${this.account}】 手动${this.actionInfo[targetAction]} ${type} 端口${powerIndex}`
+          })
+          this.onSwitchPowerSingle(power)
+        })
+    }, */
+    /* onSwitchPowerSingle (power) {
+      const { connectIndex, portIndex, powerIndex, type } = power
+      this.sendTopic(
+        SET_POWER_STATUS,
+        JSON.stringify({
+          sn: this.device.serialNumber,
+          info: {
+            data: [{
+              connectIndex, portIndex,
+              conditions: this.$powers
+                .find(item => item.portIndex === portIndex)
+                .powers
+                .filter(power => power.type === type)
+                .map(power => {
+                  return {
+                    type,
+                    powerIndex: power.powerIndex,
+                    action: power.powerIndex === powerIndex ? power.action ^ 1 : power.action
+                  }
+                })
+            }]
+          }
+        }),
+        this.getPowerStatus
+      )
+    }, */
+    /* onSwitchTypePower (power) {
+      const { type, action } = power
+      const targetAction = action ^ 1
+      this.$confirm(
+        `立即${this.actionInfo[targetAction]}电源 ${type}?<p class="u-color--blue">确认操作后请等待数据同步</p>`,
+        '操作确认',
+        {
+          dangerouslyUseHTMLString: true,
+          type: 'warning'
+        }
+      ).then(() => {
+        savePowerLogger({
+          description: `手动${this.actionInfo[targetAction]} 设备【${this.device.name}】 ${type}`,
+          method: `${this.actionInfo[targetAction]}电源`,
+          params: `${this.device.id} ${this.device.name}`
+        })
+        sendDeviceAlarm({
+          deviceId: this.device.id,
+          errorEnumId: targetAction ? 36 : 37,
+          message: `用户【${this.account}】 手动${this.actionInfo[targetAction]} ${type}`
+        })
+        this.onSwitchPowerMulti(power)
+      })
+    }, */
+    /* onSwitchPowerMulti (power) {
+      const { connectIndex, portIndex, type } = power
+      const action = power.action ^ 1
+      this.sendTopic(
+        SET_POWER_STATUS,
+        JSON.stringify({
+          sn: this.device.serialNumber,
+          info: {
+            data: [{
+              connectIndex, portIndex,
+              conditions: [{
+                powerIndex: 0,
+                type,
+                action
+              }]
+            }]
+          }
+        }),
+        this.getPowerStatus
+      )
+    }, */
+    /* onSwitchOpen () {
+      this.onSwitch(PowerStatus.OPEN)
+    }, */
+    /* onSwitchClose () {
+      this.onSwitch(PowerStatus.CLOSE)
+    }, */
+    /* onSwitch (action) {
+      if (!this.$powers.length) {
+        return
+      }
+      this.$confirm(
+        `立即${this.actionInfo[action]}所有电源?<p class="u-color--blue">确认操作后请等待数据同步</p>`,
+        '操作确认',
+        {
+          dangerouslyUseHTMLString: true,
+          type: 'warning'
+        }
+      ).then(() => {
+        savePowerLogger({
+          description: `手动${this.actionInfo[action]} 设备【${this.device.name}】 所有电源`,
+          method: `${this.actionInfo[action]}电源`,
+          params: `${this.device.id} ${this.device.name}`
+        })
+        sendDeviceAlarm({
+          deviceId: this.device.id,
+          errorEnumId: action ? 36 : 37,
+          message: `用户【${this.account}】 手动${this.actionInfo[action]} 所有电源`
+        })
+        this.sendTopic(
+          SET_POWER_STATUS,
+          JSON.stringify({
+            sn: this.device.serialNumber,
+            info: {
+              data: this.$powers.map(({ connectIndex, portIndex, powers }) => {
+                return {
+                  connectIndex, portIndex,
+                  conditions: powers.map(({ powerIndex, type }) => {
+                    return { powerIndex, type, action }
+                  })
+                }
+              })
+            }
+          }),
+          this.getPowerStatus
+        )
+      })
+    }, */
+    getTasksByKey () {
+      this.timingStatus = 0
+      this.hasChanged = false
+      this.$taskPorts = []
+      this.$tasks = []
+      this.$refs.table?.pageTo(1)
+      /* this.sendTopic(
+        key,
+        JSON.stringify({ sn: this.device.serialNumber }),
+        true
+      ) */
+    },
+    getPowerTasks () {
+      this.getTasksByKey(this.active)
+    },
+    setMultiPowerTasks (data) {
+      console.log('GET_PDU_TASK', data)
+      const tasks = []
+      const ports = []
+      const map = {}
+      data.forEach(({ connectIndex, portIndex, conditions }) => {
+        ports.push({ connectIndex, portIndex })
+        conditions.forEach(task => {
+          if (task.flag) {
+            const flagArr = `${task.flag}`.split('_')
+            const flag = flagArr.length > 2 ? `${flagArr[0]}_${flagArr[1]}` : task.flag
+            if (!map[flag]) {
+              tasks.push(map[flag] = {
+                connectIndex, portIndex, flag,
+                ...this.transfromDataToTask(task)
+              })
+            }
+            if (flagArr.length > 2) {
+              map[flag].executeTime[flagArr[2]] = {
+                ...map[flag].executeTime[flagArr[2]],
+                ...this.transformDataToExecuteTime(task)
+              }
+            } else {
+              map[flag].executeTime.push(this.transformDataToExecuteTime(task))
+            }
+          } else {
+            tasks.push({
+              connectIndex, portIndex,
+              ...this.transfromDataToTask(task)
+            })
+          }
+        })
+      })
+      this.$tasks = tasks.map(task => {
+        task.info = transformScreenTaskInfo(task.startDate, task.endDate, task.dayOfWeek, task.executeTime)
+        return task
+      })
+      this.$taskPorts = ports
+      this.timingStatus = 1
+      this.$refs.table?.pageTo(1)
+    },
+    /* transfromDataToTask ({ enable, type, startTime, endTime, cron }) {
+      const { dayOfWeek } = transformCron(cron)
+      return {
+        enable, type, dayOfWeek,
+        startDate: startTime,
+        endDate: endTime,
+        executeTime: []
+      }
+    }, */
+    /* transformDataToExecuteTime ({ action, cron }) {
+      return action === PowerStatus.OPEN
+        ? { start: transformCron(cron).executeTime }
+        : { end: transformCron(cron).executeTime }
+    }, */
+    onSubmitPowerTasks () {
+      const deviceId = this.device.id
+      submitPduTasks({ deviceId })
+        .then(
+          () => {
+            this.hasChanged = false
+            this.$message({
+              type: 'success',
+              message: '应用成功'
+            })
+          }
+        )
+    },
+    /* getMultiPowerTaskData () {
+      const map = {}
+      const data = []
+      this.$taskPorts.forEach(({ connectIndex, portIndex }) => {
+        if (!map[portIndex]) {
+          data.push(map[portIndex] = {
+            connectIndex,
+            portIndex,
+            enable: true,
+            conditions: []
+          })
+        }
+      })
+      this.$tasks.forEach(({ connectIndex, portIndex, flag, type, enable, startDate, endDate, dayOfWeek, executeTime }) => {
+        if (!map[portIndex]) {
+          data.push(map[portIndex] = {
+            connectIndex,
+            portIndex,
+            enable: true,
+            conditions: []
+          })
+        }
+        const tasks = []
+        executeTime.forEach(({ start, end }, index) => {
+          start && tasks.push({
+            type,
+            enable,
+            powerIndex: 0,
+            flag: `${flag}_${index}`,
+            action: PowerStatus.OPEN,
+            startTime: startDate,
+            endTime: endDate,
+            cron: [transformToCron(startDate, endDate, dayOfWeek, start)]
+          })
+          end && tasks.push({
+            type,
+            enable,
+            powerIndex: 0,
+            flag: `${flag}_${index}`,
+            action: PowerStatus.CLOSE,
+            startTime: startDate,
+            endTime: endDate,
+            cron: [transformToCron(startDate, endDate, dayOfWeek, end)]
+          })
+        })
+        map[portIndex].conditions.push(...tasks)
+      })
+      savePowerLogger({
+        description: `设置设备【${this.device.name}】的电源定时任务`,
+        method: '电源定时任务设置',
+        params: JSON.stringify({
+          id: this.device.id,
+          name: this.device.name,
+          tasks: this.$tasks
+        })
+      })
+      return data.filter(({ conditions }) => conditions.length)
+    }, */
+    onToggle (task) {
+      this.hasChanged = true
+      task.enable = task.status === 1
+      console.log('task.enable : ', task.enable)
+      task.status = task.enable ? 0 : 1
+      updatePduTask(task)
+        .then(() => {
+          getPduTasks({ deviceId: this.device.id })
+          this.$refs.table.pageTo(1)
+        })
+        .finally(() => {
+        })
+    },
+    onAdd () {
+      this.isAdd = true
+      // this.taskType = this.typeSelectSchema.options[0]?.value
+      this.$refs.pduTaskDialog.show()
+    },
+    onEdit (task) {
+      // 创建 executeTime 数组,符合 PduTaskDialog 的期望格式
+      const executeTime = []
+      // 如果有 powerOnTime 或 powerOffTime,则创建执行时间对象
+      if (task.powerOnTime || task.powerOffTime) {
+        executeTime.push({
+          start: task.powerOnTime || '',
+          end: task.powerOffTime || ''
+        })
+      } else {
+        // 如果都没有,则添加一个空的执行时间对象
+        executeTime.push({ start: '', end: '' })
+      }
+      const taskId = task.id
+      const { startDate, endDate } = task
+      this.isAdd = false
+      this.$task = task
+      this.$refs.pduTaskDialog.show({
+        startDate,
+        endDate,
+        executeTime,
+        taskId
+      })
+    },
+    onSave ({ value, done }) {
+      if (this.isAdd) {
+        const deviceId = this.device.id
+        addPduTask(value, deviceId)
+          .then(() => {
+            // 重新加载表格数据
+            getPduTasks({ deviceId: this.device.id })
+            this.$refs.table.pageTo(1)
+          })
+          .finally(() => {
+            done()
+          })
+      } else {
+        const task = value[0]
+        updatePduTask(task)
+          .then(() => {
+            // 重新加载表格数据
+            getPduTasks({ deviceId: this.device.id })
+            this.$refs.table.pageTo(1)
+          })
+          .finally(() => {
+            done()
+          })
+      }
+      this.hasChanged = true
+    },
+    onDel (task) {
+      this.hasChanged = true
+      deletePduTask({ id: task.id })
+        .then(() => {
+          // 重新加载表格数据
+          getPduTasks({ deviceId: this.device.id })
+          this.$refs.table.pageTo(1)
+        })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.o-icon {
+  background-image: url("~@/assets/icon_screen_switch.png");
+
+  &.ok {
+    background-image: url("~@/assets/icon_device_switch.png");
+  }
+}
+</style>

+ 5 - 2
src/views/device/detail/components/DeviceInvoke/index.vue

@@ -7,6 +7,7 @@ import DeviceStandby from './DeviceStandby.vue'
 import DeviceReboot from './DeviceReboot.vue'
 import PLCSwitch from './PLCSwitch.vue'
 import MultifunctionCardPowerSwitch from './MultifunctionCardPowerSwitch.vue'
+import PduSchedule from '@/views/device/detail/components/DeviceInvoke/PduSchedule'
 
 export default {
   name: 'DeviceInvoke',
@@ -18,7 +19,8 @@ export default {
     DeviceStandby,
     DeviceReboot,
     PLCSwitch,
-    MultifunctionCardPowerSwitch
+    MultifunctionCardPowerSwitch,
+    PduSchedule
   },
   props: {
     online: {
@@ -48,7 +50,8 @@ export default {
           __STAGING__ && h('PLCSwitch', { props: this.$attrs }),
           __STAGING__ && h('ScreenLight', { props: this.$attrs }),
           h('ScreenVolume', { props: this.$attrs }),
-          h('DeviceReboot', { props: this.$attrs })
+          h('DeviceReboot', { props: this.$attrs }),
+          h('PduSchedule', { props: this.$attrs })
         ].filter(Boolean)
         : [
           h('DeviceStandby', { props: {

+ 57 - 13
src/views/external/box/components/Device.vue

@@ -11,35 +11,45 @@
     >
       <template #default>
         <div class="c-grid-form u-align-self--center">
-          <span class="c-grid-form__label u-required">名称</span>
+          <span class="c-grid-form__label u-required">
+            名称
+          </span>
           <el-input
             v-model.trim="currObj.name"
             placeholder="最多30个字符"
             maxlength="30"
             clearable
           />
-          <span class="c-grid-form__label u-required">配置</span>
+          <span class="c-grid-form__label u-required">
+            配置
+          </span>
           <schema-select
             v-model="currObj.productId"
             class="u-width"
             :schema="productSelectSchema"
             placeholder="请选择配置"
           />
-          <span class="c-grid-form__label u-required">型号</span>
+          <span class="c-grid-form__label u-required">
+            型号
+          </span>
           <el-autocomplete
             v-model="currObj.remark"
             class="u-width"
             :fetch-suggestions="querySearch"
             placeholder="播控器的系列或型号"
           />
-          <span class="c-grid-form__label u-required">序列号</span>
+          <span class="c-grid-form__label u-required">
+            序列号
+          </span>
           <el-input
             v-model.trim="currObj.serialNumber"
             placeholder="最多50个字符"
             maxlength="50"
             clearable
           />
-          <span class="c-grid-form__label u-required">MAC</span>
+          <span class="c-grid-form__label u-required">
+            MAC
+          </span>
           <el-input
             v-model.trim="currObj.mac"
             class="u-width"
@@ -47,7 +57,9 @@
             maxlength="17"
             clearable
           />
-          <div class="c-grid-form__label u-required">开关机时间</div>
+          <div class="c-grid-form__label u-required">
+            开关机时间
+          </div>
           <el-time-picker
             v-model="currObj.range"
             class="u-width u-pointer"
@@ -55,7 +67,9 @@
             value-format="HH:mm:ss"
             :clearable="false"
           />
-          <span class="c-grid-form__label u-required">地址</span>
+          <span class="c-grid-form__label u-required">
+            地址
+          </span>
           <el-input
             v-model.trim="currObj.address"
             type="textarea"
@@ -64,9 +78,13 @@
             :rows="2"
             show-word-limit
           />
-          <span class="c-grid-form__label">坐标</span>
+          <span class="c-grid-form__label">
+            坐标
+          </span>
           <div class="l-flex--row c-grid-form__option">
-            <span class="c-sibling-item">{{ currObj.longitude }},{{ currObj.latitude }}</span>
+            <span class="c-sibling-item">
+              {{ currObj.longitude }},{{ currObj.latitude }}
+            </span>
             <i
               class="c-sibling-item el-icon-edit u-color--blue has-active"
               @click="onEditCoordinate"
@@ -87,14 +105,18 @@
     >
       <template #default>
         <div class="c-grid-form u-align-self--center">
-          <span class="c-grid-form__label u-required">序列号</span>
+          <span class="c-grid-form__label u-required">
+            序列号
+          </span>
           <el-input
             v-model.trim="currObj.serialNumber"
             placeholder="最多50个字符"
             maxlength="50"
             clearable
           />
-          <span class="c-grid-form__label u-required">MAC</span>
+          <span class="c-grid-form__label u-required">
+            MAC
+          </span>
           <el-input
             v-model.trim="currObj.mac"
             class="u-width"
@@ -102,7 +124,9 @@
             maxlength="17"
             clearable
           />
-          <span class="c-grid-form__label">配置</span>
+          <span class="c-grid-form__label">
+            配置
+          </span>
           <schema-select
             ref="productSelect"
             v-model="currObj.productId"
@@ -201,7 +225,15 @@ export default {
           //   ? { label: '设备名称', render: (data, h) => data.empty ? h('span', { staticClass: 'u-color--info' }, '暂无备份设备') : data.name, 'min-width': 120 }
           //   : { prop: 'name', label: '设备名称', 'min-width': 120 },
           { type: 'refresh' },
-          { prop: 'name', label: '设备名称', 'min-width': 120 },
+          { label: '设备名称', 'min-width': 120, render: (data, h) => data.empty
+            ? ''
+            : h('edit-input', {
+              props: {
+                value: data.name,
+                placeholder: '-'
+              },
+              on: { edit: val => this.onEditName(data, val) }
+            }), 'class-name': 'c-edit-column' },
           { label: '型号', render: (data, h) => data.empty
             ? ''
             : h('edit-input', {
@@ -532,6 +564,18 @@ export default {
         device.remark = oldVal
       })
     },
+    onEditName (device, { newVal, oldVal }) {
+      if (newVal === oldVal) {
+        return
+      }
+      device.name = newVal
+      updateDevice({
+        id: device.id,
+        name: newVal
+      }).catch(() => {
+        device.name = oldVal
+      })
+    },
     onReplace (device) {
       this.$device = device
       this.currObj = {

+ 1 - 1
src/views/external/components/PduConfigDialog.vue

@@ -37,7 +37,7 @@
       </span>
       <el-input
         v-model.trim="item.identifier"
-        placeholder="最多50个字符"
+        placeholder="设备mac,例:18d79350592b"
         maxlength="50"
         clearable
       />