yanghao vor 2 Wochen
Ursprung
Commit
1310ab7289
100 geänderte Dateien mit 29900 neuen und 2051 gelöschten Zeilen
  1. 3 0
      .env.dev
  2. 4 1
      .env.local
  3. 3 0
      .env.prod
  4. 1 1
      index.html
  5. 5 0
      package.json
  6. 387 0
      pnpm-lock.yaml
  7. 53 0
      src/api/pms/video/category.ts
  8. 8 0
      src/api/pms/video/dicts.ts
  9. 61 0
      src/api/pms/video/group.ts
  10. 106 0
      src/api/pms/video/model.ts
  11. 9 10
      src/api/pms/video/product.ts
  12. 44 0
      src/api/pms/video/protocol.ts
  13. 53 0
      src/api/pms/video/salve.ts
  14. 73 0
      src/api/pms/video/sipdevice.ts
  15. 64 0
      src/api/pms/video/temp.ts
  16. 53 0
      src/api/pms/video/template.ts
  17. 1 1
      src/api/rq/iotmeasurebook/index.ts
  18. BIN
      src/assets/imgs/gateway.png
  19. 56 0
      src/assets/imgs/gateway.svg
  20. BIN
      src/assets/imgs/product.png
  21. 66 0
      src/assets/imgs/product.svg
  22. BIN
      src/assets/imgs/video.png
  23. 45 0
      src/assets/imgs/video.svg
  24. BIN
      src/assets/imgs/zlm-logo.png
  25. 57 35
      src/components/SvgIcon/index.vue
  26. 67 10
      src/config/axios/service.ts
  27. 344 0
      src/config/axios/service2.ts
  28. 576 577
      src/locales/zh-CN.ts
  29. 4 0
      src/main.ts
  30. 125 66
      src/router/modules/remaining.ts
  31. 40 0
      src/utils/dateUtil.ts
  32. 87 0
      src/utils/download2.js
  33. 147 0
      src/utils/mqttTool.js
  34. 2 0
      src/views/pms/device/index.vue
  35. 4 6
      src/views/pms/qhse/measure/IotMeasureBookForm.vue
  36. 254 234
      src/views/pms/qhse/measure/index.vue
  37. 0 245
      src/views/pms/video_center/ChannelManage.vue
  38. 350 0
      src/views/pms/video_center/category/index.vue
  39. 216 0
      src/views/pms/video_center/device/alert-user.vue
  40. 161 0
      src/views/pms/video_center/device/allot-import-dialog.vue
  41. 162 0
      src/views/pms/video_center/device/allot-record.vue
  42. 209 0
      src/views/pms/video_center/device/batch-import-dialog.vue
  43. 78 0
      src/views/pms/video_center/device/device-abnormal.vue
  44. 233 0
      src/views/pms/video_center/device/device-alert.vue
  45. 1505 0
      src/views/pms/video_center/device/device-edit.vue
  46. 232 0
      src/views/pms/video_center/device/device-functionlog.vue
  47. 521 0
      src/views/pms/video_center/device/device-history.vue
  48. 162 0
      src/views/pms/video_center/device/device-inline-video.vue
  49. 1255 0
      src/views/pms/video_center/device/device-linkage.vue
  50. 330 0
      src/views/pms/video_center/device/device-log.vue
  51. 933 0
      src/views/pms/video_center/device/device-modbus-task.vue
  52. 313 0
      src/views/pms/video_center/device/device-monitor.vue
  53. 387 0
      src/views/pms/video_center/device/device-recycle.vue
  54. 100 0
      src/views/pms/video_center/device/device-scada.vue
  55. 384 0
      src/views/pms/video_center/device/device-select-allot.vue
  56. 243 0
      src/views/pms/video_center/device/device-statistic.vue
  57. 200 0
      src/views/pms/video_center/device/device-sub.vue
  58. 975 0
      src/views/pms/video_center/device/device-timer.vue
  59. 393 0
      src/views/pms/video_center/device/device-user.vue
  60. 432 0
      src/views/pms/video_center/device/device-variable.vue
  61. 123 0
      src/views/pms/video_center/device/import-record.vue
  62. 1168 0
      src/views/pms/video_center/device/index.vue
  63. 1118 0
      src/views/pms/video_center/device/instruction-parsing.vue
  64. 152 0
      src/views/pms/video_center/device/product-list.vue
  65. 648 0
      src/views/pms/video_center/device/realTime-status.bak.vue
  66. 1073 0
      src/views/pms/video_center/device/realTime-status.vue
  67. 124 0
      src/views/pms/video_center/device/recycle-record.vue
  68. 1036 0
      src/views/pms/video_center/device/running-status.vue
  69. 143 0
      src/views/pms/video_center/device/sub-device-list.vue
  70. 199 0
      src/views/pms/video_center/device/user-list.vue
  71. 0 351
      src/views/pms/video_center/index.vue
  72. 192 0
      src/views/pms/video_center/product/components/Crontab/day.vue
  73. 143 0
      src/views/pms/video_center/product/components/Crontab/hour.vue
  74. 446 0
      src/views/pms/video_center/product/components/Crontab/index.vue
  75. 143 0
      src/views/pms/video_center/product/components/Crontab/min.vue
  76. 143 0
      src/views/pms/video_center/product/components/Crontab/month.vue
  77. 580 0
      src/views/pms/video_center/product/components/Crontab/result.vue
  78. 147 0
      src/views/pms/video_center/product/components/Crontab/second.vue
  79. 255 0
      src/views/pms/video_center/product/components/Crontab/week.vue
  80. 171 0
      src/views/pms/video_center/product/components/Crontab/year.vue
  81. 268 0
      src/views/pms/video_center/product/components/ImageUpload/index.vue
  82. 129 0
      src/views/pms/video_center/product/components/RightToolbar/index.vue
  83. 131 0
      src/views/pms/video_center/product/components/batchImportModbus.vue
  84. 128 0
      src/views/pms/video_center/product/components/batchImportThingsModel.vue
  85. 737 0
      src/views/pms/video_center/product/index.vue
  86. 120 0
      src/views/pms/video_center/product/product-app.vue
  87. 534 0
      src/views/pms/video_center/product/product-authorize.vue
  88. 1759 0
      src/views/pms/video_center/product/product-edit.vue
  89. 332 0
      src/views/pms/video_center/product/product-firmware.vue
  90. 743 0
      src/views/pms/video_center/product/product-modbus.vue
  91. 140 0
      src/views/pms/video_center/product/product-scada.vue
  92. 215 0
      src/views/pms/video_center/product/product-select-template.vue
  93. 1544 0
      src/views/pms/video_center/product/product-things-model.vue
  94. 893 0
      src/views/pms/video_center/product/template/index.vue
  95. 670 0
      src/views/pms/video_center/product/template/parameter.vue
  96. 135 0
      src/views/pms/video_center/product/things-model-list.vue
  97. 339 0
      src/views/pms/video_center/protocol/index.vue
  98. 0 180
      src/views/pms/video_center/shebei.vue
  99. 269 179
      src/views/pms/video_center/sip/channel.vue
  100. 234 155
      src/views/pms/video_center/sip/components/player/DeviceTree.vue

+ 3 - 0
.env.dev

@@ -6,6 +6,9 @@ VITE_DEV=true
 # 请求路径
 VITE_BASE_URL='https://iot.deepoil.cc'
 
+# MQTT服务地址
+VITE_MQTT_SERVER_URL = ''
+
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
 VITE_UPLOAD_TYPE=server
 

+ 4 - 1
.env.local

@@ -4,7 +4,10 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径
-VITE_BASE_URL='https://iot.deepoil.cc'
+VITE_BASE_URL='http://192.168.188.149:48080' # 192.168.188.149:48080 https://iot.deepoil.cc/
+
+# MQTT服务地址
+VITE_MQTT_SERVER_URL = 'ws://localhost:8083/mqtt'
 
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持 S3 服务
 VITE_UPLOAD_TYPE=server

+ 3 - 0
.env.prod

@@ -6,6 +6,9 @@ VITE_DEV=false
 # 请求路径
 VITE_BASE_URL='https://aims.deepoil.cc'
 
+# MQTT服务地址
+VITE_MQTT_SERVER_URL = ''
+
 # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务
 VITE_UPLOAD_TYPE=server
 

+ 1 - 1
index.html

@@ -8,7 +8,7 @@
     ></script>
     <script src="https://api.map.baidu.com/api?v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0"></script>
 
-    <script src="/js/jessibuca-pro/jessibuca-pro.js"></script>
+    <!-- <script src="/js/jessibuca-pro/jessibuca-pro.js"></script> -->
     <script type="text/javascript" src="/js/EasyWasmPlayer.js"></script>
     <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />

+ 5 - 0
package.json

@@ -34,6 +34,7 @@
     "@fullcalendar/vue3": "^6.1.17",
     "@iconify/iconify": "^3.1.1",
     "@microsoft/fetch-event-source": "^2.0.1",
+    "@riophae/vue-treeselect": "^0.4.0",
     "@types/echarts": "^5.0.0",
     "@videojs-player/vue": "^1.0.0",
     "@vueuse/core": "^10.9.0",
@@ -59,6 +60,7 @@
     "element-china-area-data": "^6.1.0",
     "element-plus": "2.9.1",
     "fast-xml-parser": "^4.3.2",
+    "file-saver": "^2.0.5",
     "highlight.js": "^11.9.0",
     "jsencrypt": "^3.3.2",
     "lodash-es": "^4.17.21",
@@ -70,6 +72,7 @@
     "min-dash": "^4.1.1",
     "mitt": "^3.0.1",
     "moment": "^2.30.1",
+    "mqtt": "^5.14.1",
     "nprogress": "^0.2.0",
     "pinia": "^2.1.7",
     "pinia-plugin-persistedstate": "^3.2.1",
@@ -85,8 +88,10 @@
     "vue-dompurify-html": "^4.1.4",
     "vue-echarts": "^7.0.3",
     "vue-i18n": "9.10.2",
+    "vue-json-viewer": "^3.0.4",
     "vue-router": "4.4.5",
     "vue-types": "^5.1.1",
+    "vue3-next-qrcode": "^4.0.0",
     "vue3-signature": "^0.2.4",
     "vuedraggable": "^4.1.0",
     "vuex": "^4.1.0",

Datei-Diff unterdrückt, da er zu groß ist
+ 387 - 0
pnpm-lock.yaml


+ 53 - 0
src/api/pms/video/category.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// 查询产品分类列表
+export function listCategory(query) {
+  return request.get({
+    url: '/rq/iot-category/list',
+   
+    params: query
+  })
+}
+
+// 查询产品简短分类列表
+export function listShortCategory(query) {
+  return request.get({
+    url: '/rq/iot-category/shortlist',
+  
+    params: query,
+  })
+}
+
+// 查询产品分类详细
+export function getCategory(categoryId) {
+  return request.get({
+    url: '/rq/iot-category/' + categoryId,
+ 
+  })
+}
+
+// 新增产品分类
+export function addCategory(data) {
+  return request.post({
+    url: '/rq/iot-category',
+    
+    data: data
+  })
+}
+
+// 修改产品分类
+export function updateCategory(data) {
+  return request.put({
+    url: '/rq/iot-category',
+  
+    data: data
+  })
+}
+
+// 删除产品分类
+export function delCategory(categoryId) {
+  return request.delete({
+    url: `/rq/iot-category/delete?id=${categoryId}`
+  
+  })
+}

+ 8 - 0
src/api/pms/video/dicts.ts

@@ -0,0 +1,8 @@
+import request from '@/config/axios'
+
+// 查询设备列表
+export function getDicts(type) {
+    return request.get({
+        url: `/system/dict-data/type/${type}`,
+    });
+}

+ 61 - 0
src/api/pms/video/group.ts

@@ -0,0 +1,61 @@
+import request from '@/config/axios'
+
+// 查询设备分组列表
+export function listGroup(query) {
+  return request.get({
+    url: '/rq/iot-group/list',
+   
+    params: query
+  })
+}
+
+// 查询设备分组详细
+export function getGroup(groupId) {
+  return request.get({
+    url: '/rq/iot-group/' + groupId,
+  
+  })
+}
+
+// 查询分组下的关联设备ID数组
+export function getDeviceIds(groupId) {
+  return request.get({
+    url: '/rq/iot-group/getDeviceIds/' + groupId,
+  
+  })
+}
+
+// 新增设备分组
+export function addGroup(data) {
+  return request.post({
+    url: '/rq/iot-group',
+ 
+    data: data
+  })
+}
+
+// 修改设备分组
+export function updateGroup(data) {
+  return request.put({
+    url: '/rq/iot-group',
+ 
+    data: data
+  })
+}
+
+// 更新分组下的设备
+export function updateDeviceGroups(data) {
+  return request.put({
+    url: '/rq/iot-group/updateDeviceGroups',
+  
+    data: data
+  })
+}
+
+// 删除设备分组
+export function delGroup(groupId) {
+  return request.delete({
+    url: '/rq/iot-group/' + groupId,
+   
+  })
+}

+ 106 - 0
src/api/pms/video/model.ts

@@ -0,0 +1,106 @@
+import request from '@/config/axios'
+
+// 查询物模型列表
+export function listModel(query) {
+  return request.get({
+    url: '/rq/iot-things-model/page',
+ 
+    params: query,
+  });
+}
+
+// 查询物模型详细
+export function getModel(modelId) {
+  return request.get({
+    url: '/iot/model/' + modelId,
+   
+  });
+}
+
+// 查询物模型对应分享设备用户权限列表
+export function permListModel(productId) {
+  return request.get({
+    url: '/iot/model/permList/' + productId,
+   
+  });
+}
+
+// 新增物模型
+export function addModel(data) {
+  return request.post({
+    url: '/iot/model',
+
+    data: data,
+  });
+}
+
+// 导入通用物模型
+export function importModel(data) {
+  return request.post({
+    url: '/iot/model/import',
+  
+    data: data,
+  });
+}
+
+// 导入excel物模型
+export function importExcel(data) {
+    return request.post({
+        url: '/iot/model/importExcel',
+        
+        data: data,
+    });
+}
+
+
+// 修改物模型
+export function updateModel(data) {
+  return request.put({
+    url: '/iot/model',
+   
+    data: data,
+  });
+}
+
+// 删除物模型
+export function delModel(modelId) {
+  return request.delete({
+    url: '/iot/model/' + modelId,
+   
+  });
+}
+
+// 根据产品ID获取缓存的物模型
+export function cacheJsonThingsModel(productId) {
+  return request.get({
+    url: '/iot/model/cache/' + productId,
+    
+  });
+}
+
+// 同步采集点模板到产品物模型
+export function synchron(data) {
+  return request.post({
+    url: '/iot/model/synchron',
+   
+    data: data,
+  });
+}
+
+// 根据产品ID获取缓存的物模型
+export function getlListModbus(query) {
+  return request.get({
+    url: '/iot/model/listModbus',
+   
+    params: query,
+  });
+}
+
+// 根据产品ID获取缓存的物模型
+export function getWriteList(query) {
+  return request.get({
+    url: '/iot/model/write',
+ 
+    params: query,
+  });
+}

+ 9 - 10
src/api/pms/video/product.ts

@@ -3,7 +3,7 @@ import request from '@/config/axios'
 // 查询产品列表
 export function listProduct(query) {
   return request.get({
-    url: '/iot/product/list',
+    url: '/rq/iot-product/list',
    
     params: query
   })
@@ -12,7 +12,7 @@ export function listProduct(query) {
 // 查询产品列表
 export function listShortProduct() {
   return request.get({
-    url: '/iot/product/shortList',
+    url: '/rq/iot-product/shortList',
     
   })
 }
@@ -20,15 +20,14 @@ export function listShortProduct() {
 // 查询产品详细
 export function getProduct(productId) {
   return request.get({
-    url: '/iot/product/' + productId,
-    
+    url: `/rq/iot-product/get?id=${productId}`,
   })
 }
 
 // 新增产品
 export function addProduct(data) {
   return request.post({
-    url: '/iot/product',
+    url: '/rq/iot-product',
    
     data: data
   })
@@ -37,7 +36,7 @@ export function addProduct(data) {
 // 修改产品
 export function updateProduct(data) {
   return request.put({
-    url: '/iot/product',
+    url: '/rq/iot-product',
    
     data: data
   })
@@ -46,7 +45,7 @@ export function updateProduct(data) {
 // 获取产品下设备的数量
 export function deviceCount(productId) {
   return request.get({
-    url: '/iot/product/deviceCount/' + productId,
+    url: '/rq/iot-product/deviceCount/' + productId,
     
   })
 }
@@ -54,7 +53,7 @@ export function deviceCount(productId) {
 // 更新产品状态
 export function changeProductStatus(data) {
   return request.put({
-    url: '/iot/product/status',
+    url: '/rq/iot-product/status',
    
     data:data
   })
@@ -63,7 +62,7 @@ export function changeProductStatus(data) {
 // 删除产品
 export function delProduct(productId) {
   return request.delete({
-    url: '/iot/product/' + productId,
+    url: '/rq/iot-product/' + productId,
     
   })
 }
@@ -71,7 +70,7 @@ export function delProduct(productId) {
 // 根据采集点模板id查询所有产品
 export function selectByTempleId(params) {
   return request.get({
-    url: '/iot/product/queryByTemplateId',
+    url: '/rq/iot-product/queryByTemplateId',
     
     params: params
   })

+ 44 - 0
src/api/pms/video/protocol.ts

@@ -0,0 +1,44 @@
+import request from '@/config/axios'
+
+// 查询协议列表
+export function listProtocol(query) {
+  return request.get({
+    url: '/rq/iot-protocol/list',
+    
+    params: query
+  })
+}
+
+// 查询协议详细
+export function getProtocol(id) {
+  return request.get({
+    url: '/rq/iot-protocol' + id,
+    
+  })
+}
+
+// 新增协议
+export function addProtocol(data) {
+  return request.post({
+    url: '/rq/iot-protocol',
+  
+    data: data
+  })
+}
+
+// 修改协议
+export function updateProtocol(data) {
+  return request.put({
+    url: '/rq/iot-protocol',
+   
+    data: data
+  })
+}
+
+// 删除协议
+export function delProtocol(id) {
+  return request.delete({
+    url: '/rq/iot-protocol/' + id,
+    
+  })
+}

+ 53 - 0
src/api/pms/video/salve.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// 查询变量模板设备从机列表
+export function listSalve(query) {
+  return request.get({
+    url: '/iot/salve/list',
+ 
+    params: query
+  })
+}
+
+// 查询变量模板设备从机详细
+export function getSalve(id) {
+  return request.get({
+    url: '/iot/salve/' + id,
+  
+  })
+}
+
+// 新增变量模板设备从机
+export function addSalve(data) {
+  return request.post({
+    url: '/iot/salve',
+   
+    data: data
+  })
+}
+
+// 修改变量模板设备从机
+export function updateSalve(data) {
+  return request.put({
+    url: '/iot/salve',
+ 
+    data: data
+  })
+}
+
+// 删除变量模板设备从机
+export function delSalve(id) {
+  return request.delete({
+    url: '/iot/salve/' + id,
+   
+  })
+}
+
+//根据产品id查询从机列表
+export function listByPid(params) {
+    return request.get({
+        url: "/iot/salve/listByPId",
+       
+        params: params,
+    })
+}

+ 73 - 0
src/api/pms/video/sipdevice.ts

@@ -0,0 +1,73 @@
+import request from '@/config/axios'
+
+// 查询监控设备列表
+export function listSipDevice(query) {
+  return request.get({
+    url: '/sip/device/list',
+    params: query
+  })
+}
+
+export function listSipDeviceChannel(deviceId) {
+  return request.get({
+    url: '/sip/device/listchannel/'+ deviceId,
+    
+  })
+}
+
+// 查询监控设备详细
+export function getSipDevice(deviceId) {
+  return request.get({
+    url: '/sip/device/' + deviceId,
+  
+  })
+}
+
+// 新增监控设备
+export function addSipDevice(data) {
+  return request.post({
+    url: '/sip/device',
+  
+    data: data
+  })
+}
+
+// 修改监控设备
+export function updateSipDevice(data) {
+  return request.put({
+    url: '/sip/device',
+  
+    data: data
+  })
+}
+
+// 删除监控设备
+export function delSipDevice(deviceId) {
+  return request.delete({
+    url: '/sip/device/' + deviceId,
+   
+  })
+}
+
+export function delSipDeviceBySipId(sipId) {
+  return request.delete({
+    url: '/sip/device/sipid/' + sipId,
+    
+  })
+}
+
+export function ptzdirection(deviceId,channelId,data) {
+  return request.post({
+    url: '/sip/ptz/direction/'+ deviceId + "/" + channelId,
+   
+    data: data
+  })
+}
+
+export function ptzscale(deviceId,channelId,data) {
+  return request.post({
+    url: '/sip/ptz/scale/'+ deviceId + "/" + channelId,
+    
+    data: data
+  })
+}

+ 64 - 0
src/api/pms/video/temp.ts

@@ -0,0 +1,64 @@
+import request from '@/config/axios'
+
+// 查询设备采集变量模板列表
+export function listTemp(query) {
+  return request.get({
+    url: '/rq/iot-var-temp/page',
+    
+    params: query
+  })
+}
+
+// 查询设备采集变量模板详细
+export function getTemp(templateId) {
+  return request.get({
+    url: '/iot/temp/' + templateId,
+   
+  })
+}
+
+// 新增设备采集变量模板
+export function addTemp(data) {
+  return request.post({
+    url: '/iot/temp',
+  
+    data: data
+  })
+}
+
+// 修改设备采集变量模板
+export function updateTemp(data) {
+  return request.put({
+    url: '/iot/temp',
+   
+    data: data
+  })
+}
+
+// 删除设备采集变量模板
+export function delTemp(templateId) {
+  return request.delete({
+    url: '/iot/temp/' + templateId,
+    
+  })
+}
+
+
+//根据产品查询采集点关联
+export function getDeviceTemp(params){
+  return request.get({
+    url: '/iot/temp/getTemp' ,
+    
+    params: params,
+    
+  })
+}
+
+export function getTempByPId(params){
+  return request.get({
+    url: '/rq/iot-var-temp/getTempByPid',
+    params: params,
+    
+
+  })
+}

+ 53 - 0
src/api/pms/video/template.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// 查询通用物模型列表
+export function listTemplate(query) {
+  return request.get({
+    url: '/rq/iot-things-model-template/list',
+    
+    params: query
+  })
+}
+
+// 查询通用物模型详细
+export function getTemplate(templateId) {
+  return request.get({
+    url: '/iot/template/' + templateId,
+   
+  })
+}
+
+// 新增通用物模型
+export function addTemplate(data) {
+  return request.post({
+    url: '/iot/template',
+  
+    data: data
+  })
+}
+
+// 修改通用物模型
+export function updateTemplate(data) {
+  return request.put({
+    url: '/iot/template',
+   
+    data: data
+  })
+}
+
+// 删除通用物模型
+export function delTemplate(templateId) {
+  return request.delete({
+    url: '/iot/template/' + templateId,
+    
+  })
+}
+
+// 查询通用物模型详细
+export function getAllPoints(params) {
+  return request.get({
+    url: '/rq/iot-things-model-template/getPoints',
+   
+    params: params,
+  })
+}

+ 1 - 1
src/api/rq/iotmeasurebook/index.ts

@@ -1,4 +1,4 @@
-import request from '@/config/axios'
+import request from '@/config/axios/service2'
 
 // 计量器具台账 VO
 export interface IotMeasureBookVO {

BIN
src/assets/imgs/gateway.png


+ 56 - 0
src/assets/imgs/gateway.svg

@@ -0,0 +1,56 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81 80" class="design-iconfont">
+  <path d="M46.3594 57.6562H52.2188V55.3125H48.7031V47.0312H46.3594V57.6562Z" fill="url(#kpkn6ylv3__paint0_linear_6946_492)"/>
+  <path d="M28.7812 57.6562H34.6406V47.0312H32.2969V55.3125H28.7812V57.6562Z" fill="url(#kpkn6ylv3__paint1_linear_6946_492)"/>
+  <path d="M41.6719 47.0312H39.3281V54.1406H41.6719V47.0312Z" fill="url(#kpkn6ylv3__paint2_linear_6946_492)"/>
+  <path d="M25.1875 32.6172C25.375 25.6797 31.1719 20 38.1562 20C38.9531 20 39.75 20.0703 40.5 20.2109C44.4141 20.9375 47.7891 23.5234 49.6406 27.1094C52.8281 26.9922 55.5469 29.4766 55.7344 32.6172C58.5703 33.9531 60.5 36.8359 60.5 40C60.5 44.5234 56.7422 48.2031 52.2188 48.2031H28.7812C24.2578 48.2031 20.5 44.5234 20.5 40C20.5 36.8359 22.3516 33.9531 25.1875 32.6172Z" fill="url(#kpkn6ylv3__paint3_linear_6946_492)"/>
+  <path d="M33.4922 40.375L35.5078 41.5937C36.5859 39.8359 38.4375 38.7812 40.5 38.7812C42.5625 38.7812 44.4141 39.8359 45.4922 41.5937L47.5078 40.375C46.0078 37.9141 43.3828 36.4375 40.5 36.4375C37.6172 36.4375 34.9922 37.9141 33.4922 40.375Z" fill="url(#kpkn6ylv3__paint4_linear_6946_492)"/>
+  <path d="M29.5078 37.9375L31.5 39.1562C33.4219 35.9922 36.7969 34.0937 40.5 34.0937C44.2031 34.0937 47.5781 35.9922 49.5 39.1562L51.4922 37.9375C49.1484 34.0703 45.0234 31.75 40.5 31.75C35.9766 31.75 31.8516 34.0703 29.5078 37.9375Z" fill="url(#kpkn6ylv3__paint5_linear_6946_492)"/>
+  <path d="M37.5001 42.8125L39.4923 44.0547C39.7032 43.7031 40.1017 43.5156 40.5001 43.5156C40.8986 43.5156 41.297 43.7031 41.5079 44.0547L43.5001 42.8125C42.8439 41.7578 41.672 41.2188 40.5001 41.2188C39.3282 41.2188 38.1564 41.7578 37.5001 42.8125Z" fill="url(#kpkn6ylv3__paint6_linear_6946_492)"/>
+  <path d="M40.5 52.9688C42.4453 52.9688 44.0156 54.5391 44.0156 56.4844C44.0156 58.4297 42.4453 60 40.5 60C38.5547 60 36.9844 58.4297 36.9844 56.4844C36.9844 54.5391 38.5547 52.9688 40.5 52.9688Z" fill="url(#kpkn6ylv3__paint7_linear_6946_492)"/>
+  <path d="M54.5625 60C56.5 60 58.0781 58.4219 58.0781 56.4844C58.0781 54.5469 56.5 52.9688 54.5625 52.9688C52.625 52.9688 51.0469 54.5469 51.0469 56.4844C51.0469 58.4219 52.625 60 54.5625 60Z" fill="url(#kpkn6ylv3__paint8_linear_6946_492)"/>
+  <path d="M26.4375 60C28.375 60 29.9531 58.4219 29.9531 56.4844C29.9531 54.5469 28.375 52.9688 26.4375 52.9688C24.5 52.9688 22.9219 54.5469 22.9219 56.4844C22.9219 58.4219 24.5 60 26.4375 60Z" fill="url(#kpkn6ylv3__paint9_linear_6946_492)"/>
+  <defs>
+    <linearGradient id="kpkn6ylv3__paint0_linear_6946_492" x1="49.2891" y1="47.0312" x2="49.2891" y2="57.6562" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint1_linear_6946_492" x1="31.7109" y1="47.0312" x2="31.7109" y2="57.6562" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint2_linear_6946_492" x1="40.5" y1="57.1503" x2="40.5" y2="38.7465" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#273A9B"/>
+      <stop offset="1" stop-color="#88CDFF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint3_linear_6946_492" x1="40.5" y1="20" x2="40.5" y2="48.2031" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint4_linear_6946_492" x1="36.7659" y1="38.115" x2="36.7659" y2="41.5937" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint5_linear_6946_492" x1="34.6429" y1="34.1595" x2="34.6429" y2="39.1562" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint6_linear_6946_492" x1="38.9016" y1="42.1414" x2="38.9016" y2="44.0547" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint7_linear_6946_492" x1="38.6267" y1="55.2563" x2="38.6267" y2="60" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint8_linear_6946_492" x1="54.5625" y1="64.2562" x2="54.5625" y2="51.0094" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#F3C57A"/>
+      <stop offset=".49" stop-color="#F39369"/>
+      <stop offset="1" stop-color="#E94867"/>
+    </linearGradient>
+    <linearGradient id="kpkn6ylv3__paint9_linear_6946_492" x1="26.4375" y1="64.2562" x2="26.4375" y2="51.0094" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#F3C57A"/>
+      <stop offset=".49" stop-color="#F39369"/>
+      <stop offset="1" stop-color="#E94867"/>
+    </linearGradient>
+  </defs>
+</svg>

BIN
src/assets/imgs/product.png


+ 66 - 0
src/assets/imgs/product.svg

@@ -0,0 +1,66 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81 80" class="design-iconfont">
+  <path d="M56.328 56.0937L55.4999 60H50.4999L49.6718 56.0937L52.9999 54.6016L56.328 56.0937Z" fill="url(#kiuy5gxed__paint0_linear_6946_358)"/>
+  <path d="M31.328 56.0937L30.4999 60H25.4999L24.6717 56.0937L27.9999 54.6016L31.328 56.0937Z" fill="url(#kiuy5gxed__paint1_linear_6946_358)"/>
+  <path d="M58 41.0937L57.5 20H53.4922L53 41.0937L55.5 42.2031L58 41.0937Z" fill="url(#kiuy5gxed__paint2_linear_6946_358)"/>
+  <path d="M28 41.0937L27.5 20H23.4922L22.9922 41.0937L25.4922 42.2031L28 41.0937Z" fill="url(#kiuy5gxed__paint3_linear_6946_358)"/>
+  <path d="M20.5 41.0938V56.0937H60.5V41.0938H55.5L40.5 43.5937L25.5 41.0938H20.5Z" fill="url(#kiuy5gxed__paint4_linear_6946_358)"/>
+  <path d="M25.5 41.0938L26.3281 46.0937H54.6719L55.5 41.0938H25.5Z" fill="url(#kiuy5gxed__paint5_linear_6946_358)"/>
+  <path d="M55.7969 49.8047H53.4531V52.1484H55.7969V49.8047Z" fill="url(#kiuy5gxed__paint6_linear_6946_358)"/>
+  <path d="M51.0859 49.8047H48.7422V52.1484H51.0859V49.8047Z" fill="#fff"/>
+  <path d="M46.375 49.8047H44.0312V52.1484H46.375V49.8047Z" fill="url(#kiuy5gxed__paint7_linear_6946_358)"/>
+  <path d="M32.2578 49.8047H25.2109V52.1484H32.2578V49.8047Z" fill="url(#kiuy5gxed__paint8_linear_6946_358)"/>
+  <path d="M38.0079 36.0942L39.6642 37.7505C39.8986 37.5239 40.1954 37.4067 40.5001 37.4067C40.8048 37.4067 41.1095 37.5239 41.3361 37.7505L42.9923 36.0942C42.3048 35.4067 41.3986 35.063 40.5001 35.063C39.6017 35.063 38.6954 35.4067 38.0079 36.0942Z" fill="url(#kiuy5gxed__paint9_linear_6946_358)"/>
+  <path d="M34.6797 32.7656L36.3359 34.4219C37.4453 33.3125 38.9297 32.6953 40.5 32.6953C42.0703 32.6953 43.5547 33.3047 44.6641 34.4219L46.3203 32.7656C44.7656 31.2109 42.6953 30.3516 40.5 30.3516C38.3047 30.3516 36.2344 31.2109 34.6797 32.7656Z" fill="url(#kiuy5gxed__paint10_linear_6946_358)"/>
+  <path d="M31.3514 29.438L33.0077 31.0942C35.0077 29.0942 37.6718 27.9927 40.4999 27.9927C43.328 27.9927 45.9921 29.0942 47.9921 31.0942L49.6483 29.438C47.203 26.9927 43.953 25.6489 40.4999 25.6489C37.0468 25.6489 33.7968 26.9927 31.3514 29.438Z" fill="url(#kiuy5gxed__paint11_linear_6946_358)"/>
+  <defs>
+    <linearGradient id="kiuy5gxed__paint0_linear_6946_358" x1="51.2265" y1="56.3579" x2="51.2265" y2="60" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint1_linear_6946_358" x1="26.2265" y1="56.3579" x2="26.2265" y2="60" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint2_linear_6946_358" x1="55.5" y1="20" x2="55.5" y2="42.2031" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint3_linear_6946_358" x1="25.4961" y1="20" x2="25.4961" y2="42.2031" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint4_linear_6946_358" x1="40.5" y1="41.0938" x2="40.5" y2="56.0937" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint5_linear_6946_358" x1="32.5073" y1="42.7204" x2="32.5073" y2="46.0937" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint6_linear_6946_358" x1="54.625" y1="53.5672" x2="54.625" y2="49.1516" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#F3C57A"/>
+      <stop offset=".49" stop-color="#F39369"/>
+      <stop offset="1" stop-color="#E94867"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint7_linear_6946_358" x1="45.2031" y1="53.1406" x2="45.2031" y2="47.0734" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#273A9B"/>
+      <stop offset="1" stop-color="#88CDFF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint8_linear_6946_358" x1="28.7344" y1="53.1406" x2="28.7344" y2="47.0734" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#273A9B"/>
+      <stop offset="1" stop-color="#88CDFF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint9_linear_6946_358" x1="39.1722" y1="35.9373" x2="39.1722" y2="37.7505" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint10_linear_6946_358" x1="37.3987" y1="31.6758" x2="37.3987" y2="34.4219" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="kiuy5gxed__paint11_linear_6946_358" x1="35.6252" y1="27.4205" x2="35.6252" y2="31.0942" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+  </defs>
+</svg>

BIN
src/assets/imgs/video.png


+ 45 - 0
src/assets/imgs/video.svg

@@ -0,0 +1,45 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 81 80" class="design-iconfont">
+  <path d="M45.5324 52.8665H58.4946V46.7275H45.5324V52.8665Z" fill="url(#itvt2lb2h__paint0_linear_6946_3241)"/>
+  <path d="M59.6665 39.5947H57.3228V60.0003H59.6665V39.5947Z" fill="url(#itvt2lb2h__paint1_linear_6946_3241)"/>
+  <path d="M23.9452 32.4814L29.2003 35.2454L26.0313 40.2667L20.6858 39.5655L19.6665 35.7614L23.9452 32.4814Z" fill="url(#itvt2lb2h__paint2_linear_6946_3241)"/>
+  <path d="M44.8031 45.9931L43.4914 41.0977L41.7935 41.5527L41.794 41.554L41.2222 41.7073L42.5348 46.6031L43.6669 46.2996L43.6553 46.2563L43.6711 46.2964L44.8031 45.9931Z" fill="url(#itvt2lb2h__paint3_linear_6946_3241)"/>
+  <path d="M42.6343 42.429L47.3103 41.1761L45.3749 33.9531L36.0171 36.4612L37.9538 43.6847L42.6301 42.4309L42.624 42.4079L42.6343 42.429Z" fill="url(#itvt2lb2h__paint4_linear_6946_3241)"/>
+  <path d="M53.4557 34.8325L50.4121 23.4736L38.4107 26.6894L38.41 26.6867L23.4588 30.6953L26.5045 42.055L41.4557 38.0464L41.4535 38.0382L41.46 38.0467L53.4557 34.8325Z" fill="url(#itvt2lb2h__paint5_linear_6946_3241)"/>
+  <path d="M55.043 28.9391L52.6477 20L25.4228 27.2949L30.0171 32.313L40.377 29.5371L42.7652 32.229L55.043 28.9391Z" fill="url(#itvt2lb2h__paint6_linear_6946_3241)"/>
+  <path d="M45.6256 53.5943C43.4942 54.1654 41.2956 52.896 40.7244 50.7646C40.1533 48.6331 41.4228 46.4345 43.5542 45.8634C45.6856 45.2923 47.8843 46.5617 48.4554 48.6932C49.0265 50.8246 47.757 53.0232 45.6256 53.5943Z" fill="url(#itvt2lb2h__paint7_linear_6946_3241)"/>
+  <defs>
+    <linearGradient id="itvt2lb2h__paint0_linear_6946_3241" x1="52.0135" y1="46.7275" x2="52.0135" y2="52.8665" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint1_linear_6946_3241" x1="58.4946" y1="39.5947" x2="58.4946" y2="60.0003" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint2_linear_6946_3241" x1="24.4334" y1="44.9793" x2="24.4334" y2="30.312" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#F3C57A"/>
+      <stop offset=".49" stop-color="#F39369"/>
+      <stop offset="1" stop-color="#E94867"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint3_linear_6946_3241" x1="43.0126" y1="48.9337" x2="43.0126" y2="34.682" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#273A9B"/>
+      <stop offset="1" stop-color="#88CDFF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint4_linear_6946_3241" x1="41.6637" y1="33.9531" x2="41.6637" y2="43.6847" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint5_linear_6946_3241" x1="39.9328" y1="32.3665" x2="24.9817" y2="36.3752" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#7AB2FF"/>
+      <stop offset="1" stop-color="#E6F2FF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint6_linear_6946_3241" x1="32.3414" y1="24.0059" x2="32.3414" y2="32.313" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+    <linearGradient id="itvt2lb2h__paint7_linear_6946_3241" x1="44.5899" y1="45.7266" x2="44.5899" y2="53.7311" gradientUnits="userSpaceOnUse">
+      <stop stop-color="#004DF3"/>
+      <stop offset="1" stop-color="#5294FF"/>
+    </linearGradient>
+  </defs>
+</svg>

BIN
src/assets/imgs/zlm-logo.png


+ 57 - 35
src/components/SvgIcon/index.vue

@@ -1,47 +1,69 @@
 <template>
-  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
-  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
+  <div 
+    v-if="isExternalIcon" 
+    :style="styleExternalIcon" 
+    class="svg-external-icon svg-icon"
+    v-bind="$attrs"
+  ></div>
+  <svg 
+    v-else 
+    :class="svgClass" 
+    aria-hidden="true"
+    v-bind="$attrs"
+  >
     <use :xlink:href="iconName" />
   </svg>
 </template>
 
-<script>
-import { isExternal } from '@/utils/validate'
+<script setup>
+import { computed } from 'vue'
 
-export default {
+
+// 启用 inheritAttrs 选项
+defineOptions({
   name: 'SvgIcon',
-  props: {
-    iconClass: {
-      type: String,
-      required: true
-    },
-    className: {
-      type: String,
-      default: ''
-    }
+  inheritAttrs: false
+})
+
+// 定义 props
+const props = defineProps({
+  iconClass: {
+    type: String,
+    required: true
   },
-  computed: {
-    isExternal() {
-      return isExternal(this.iconClass)
-    },
-    iconName() {
-      return `#icon-${this.iconClass}`
-    },
-    svgClass() {
-      if (this.className) {
-        return 'svg-icon ' + this.className
-      } else {
-        return 'svg-icon'
-      }
-    },
-    styleExternalIcon() {
-      return {
-        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
-        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
-      }
-    }
+  className: {
+    type: String,
+    default: ''
   }
+})
+
+ function isExternal(path) {
+    return /^(https?:|mailto:|tel:)/.test(path)
 }
+
+// 计算属性
+const isExternalIcon = computed(() => {
+  return isExternal(props.iconClass)
+})
+
+const iconName = computed(() => {
+  return `#icon-${props.iconClass}`
+})
+
+const svgClass = computed(() => {
+  if (props.className) {
+    return 'svg-icon ' + props.className
+  } else {
+    return 'svg-icon'
+  }
+})
+
+const styleExternalIcon = computed(() => {
+  return {
+    mask: `url(${props.iconClass}) no-repeat 50% 50%`,
+    '-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
+  }
+})
 </script>
 
 <style scoped>
@@ -58,4 +80,4 @@ export default {
   mask-size: cover!important;
   display: inline-block;
 }
-</style>
+</style>

+ 67 - 10
src/config/axios/service.ts

@@ -1,11 +1,11 @@
 import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
 
-import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import { ElMessage, ElMessageBox, ElNotification, ElLoading  } from 'element-plus'
 import qs from 'qs'
 import { config } from '@/config/axios/config'
 import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
 import errorCode from './errorCode'
-
+import {saveAs} from 'file-saver'
 import { resetRouter } from '@/router'
 import { deleteUserCache } from '@/hooks/web/useCache'
 import {langHelper} from '@/utils/langHelper'
@@ -229,14 +229,7 @@ service.interceptors.response.use(
   }
 )
 
-const isSystemPagePath = (path: string): boolean=> {
-  // 正则说明:
-  // ^.*system/ 匹配开头任意字符直到system/
-  // (?:[^/]+/)* 匹配零个或多个非斜杠字符组成的路径段
-  // page(?:\?.*)?$ 匹配page结尾,可带查询参数
-  const pattern = /^.*system\/(?:[^/]+\/)*page(?:\?.*)?$/i
-  return pattern.test(path)
-}
+
 const refreshToken = async () => {
   axios.defaults.headers.common['tenant-id'] = getTenantId()
   return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
@@ -267,4 +260,68 @@ const handleAuthorized = () => {
   }
   return Promise.reject(t('sys.api.timeoutMessage'))
 }
+
+
+let downloadLoadingInstance;
+function tansParams(params) {
+  let result = ''
+  for (const propName of Object.keys(params)) {
+    const value = params[propName];
+    const part = encodeURIComponent(propName) + "=";
+    if (value !== null && value !== "" && typeof (value) !== "undefined") {
+      if (typeof value === 'object') {
+        for (const key of Object.keys(value)) {
+          if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+            const params = propName + '[' + key + ']';
+            const subPart = encodeURIComponent(params) + "=";
+            result += subPart + encodeURIComponent(value[key]) + "&";
+          }
+        }
+      } else {
+        result += part + encodeURIComponent(value) + "&";
+      }
+    }
+  }
+  return result
+}
+async function blobValidate(data) {
+  try {
+    const text = await data.text();
+    JSON.parse(text);
+    return false;
+  } catch (error) {
+    return true;
+  }
+}
+export function download(url, params, filename, config) {
+    downloadLoadingInstance = ElLoading.service({
+        text: "正在下载数据,请稍候",
+        spinner: "el-icon-loading",
+        background: "rgba(0, 0, 0, 0.7)",
+    })
+    return service.post(url, params, {
+        transformRequest: [(params) => {
+            return tansParams(params)
+        }],
+        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
+        responseType: 'blob',
+        ...config
+    }).then(async (data) => {
+        const isLogin = await blobValidate(data);
+        if (isLogin) {
+            const blob = new Blob([data.data])
+            saveAs(blob, filename)
+        } else {
+            const resText = await data.data.text();
+            const rspObj = JSON.parse(resText);
+            const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+            ElMessage.error(errMsg);
+        }
+        downloadLoadingInstance.close();
+    }).catch((r) => {
+        console.error(r)
+        ElMessage.error('下载文件出现错误,请联系管理员!')
+        downloadLoadingInstance.close();
+    })
+}
 export { service }

+ 344 - 0
src/config/axios/service2.ts

@@ -0,0 +1,344 @@
+import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
+
+import { ElMessage, ElMessageBox, ElNotification } from 'element-plus'
+import qs from 'qs'
+import { getAccessToken, getRefreshToken, getTenantId, removeToken, setToken } from '@/utils/auth'
+import errorCode from './errorCode'
+
+import { resetRouter } from '@/router'
+import { deleteUserCache } from '@/hooks/web/useCache'
+import {langHelper} from '@/utils/langHelper'
+import { useLocaleStore } from '@/store/modules/locale'
+
+
+
+
+const config: {
+  base_url: string
+  result_code: number | string
+  default_headers: AxiosHeaders
+  request_timeout: number
+} = {
+  /**
+   * api请求基础路径
+   */
+  base_url: 'http://192.168.188.190:48080/admin-api',
+  /**
+   * 接口成功返回状态码
+   */
+  result_code: 200,
+
+  /**
+   * 接口请求超时时间
+   */
+  request_timeout: 120000,
+
+  /**
+   * 默认接口请求类型
+   * 可选值:application/x-www-form-urlencoded multipart/form-data
+   */
+  default_headers: 'application/json'
+}
+
+
+
+
+// servive
+const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
+const { result_code, base_url, request_timeout } = config
+// const localeStore = useLocaleStore()
+// 需要忽略的提示。忽略后,自动 Promise.reject('error')
+const ignoreMsgs = [
+  '无效的刷新令牌', // 刷新令牌被删除时,不用提示
+  '刷新令牌已过期' // 使用刷新令牌,刷新获取新的访问令牌时,结果因为过期失败,此时需要忽略。否则,会导致继续 401,无法跳转到登出界面
+]
+// 是否显示重新登录
+export const isRelogin = { show: false }
+// Axios 无感知刷新令牌,参考 https://www.dashingdog.cn/article/11 与 https://segmentfault.com/a/1190000020210980 实现
+// 请求队列
+let requestList: any[] = []
+// 是否正在刷新中
+let isRefreshToken = false
+// 请求白名单,无须token的接口
+const whiteList: string[] = ['/login', '/refresh-token']
+
+// 创建axios实例
+const service2: AxiosInstance = axios.create({
+  baseURL: base_url, // api 的 base_url
+  timeout: request_timeout, // 请求超时时间
+  withCredentials: false, // 禁用 Cookie 等信息
+  // 自定义参数序列化函数
+  paramsSerializer: (params) => {
+    return qs.stringify(params, { allowDots: true })
+  }
+})
+
+// request拦截器
+service2.interceptors.request.use(
+  (config: InternalAxiosRequestConfig) => {
+    // 是否需要设置 token
+    let isToken = (config!.headers || {}).isToken === false
+    whiteList.some((v) => {
+      if (config.url && config.url.indexOf(v) > -1) {
+        return (isToken = false)
+      }
+    })
+    if (getAccessToken() && !isToken) {
+      config.headers.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token
+    }
+    // 设置租户
+    if (tenantEnable && tenantEnable === 'true') {
+      const tenantId = getTenantId()
+      if (tenantId) config.headers['tenant-id'] = tenantId
+    }
+    const method = config.method?.toUpperCase()
+    // 防止 GET 请求缓存
+    if (method === 'GET') {
+      config.headers['Cache-Control'] = 'no-cache'
+      config.headers['Pragma'] = 'no-cache'
+    }
+    // 自定义参数序列化函数
+    else if (method === 'POST') {
+      const contentType = config.headers['Content-Type'] || config.headers['content-type']
+      if (contentType === 'application/x-www-form-urlencoded') {
+        if (config.data && typeof config.data !== 'string') {
+          config.data = qs.stringify(config.data)
+        }
+      }
+    }
+    return config
+  },
+  (error: AxiosError) => {
+    // Do something with request error
+    console.log(error) // for debug
+    return Promise.reject(error)
+  }
+)
+
+// response 拦截器
+service2.interceptors.response.use(
+  async (response: AxiosResponse<any>) => {
+    let { data } = response
+    const config = response.config
+    if (!data) {
+      // 返回“[HTTP]请求没有返回值”;
+      throw new Error()
+    }
+    const { t } = useI18n()
+    // 未设置状态码则默认成功状态
+    // 二进制数据则直接返回,例如说 Excel 导出
+    if (
+      response.request.responseType === 'blob' ||
+      response.request.responseType === 'arraybuffer'
+    ) {
+      // 注意:如果导出的响应为 json,说明可能失败了,不直接返回进行下载
+      if (response.data.type !== 'application/json') {
+        return response.data
+      }
+      data = await new Response(response.data).json()
+    }
+    const code = data.code || result_code
+    // 获取错误信息
+    const msg = data.msg || errorCode[code] || errorCode['default']
+    if (ignoreMsgs.indexOf(msg) !== -1) {
+      // 如果是忽略的错误码,直接返回 msg 异常
+      return Promise.reject(msg)
+    } else if (code === 401) {
+      // 如果未认证,并且未进行刷新令牌,说明可能是访问令牌过期了
+      if (!isRefreshToken) {
+        isRefreshToken = true
+        // 1. 如果获取不到刷新令牌,则只能执行登出操作
+        if (!getRefreshToken()) {
+          return handleAuthorized()
+        }
+        // 2. 进行刷新访问令牌
+        try {
+          const refreshTokenRes = await refreshToken()
+          // 2.1 刷新成功,则回放队列的请求 + 当前请求
+          setToken((await refreshTokenRes).data.data)
+          config.headers!.Authorization = 'Bearer ' + getAccessToken()
+          requestList.forEach((cb: any) => {
+            cb()
+          })
+          requestList = []
+          return service2(config)
+        } catch (e) {
+          // 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
+          // 2.2 刷新失败,只回放队列的请求
+          requestList.forEach((cb: any) => {
+            cb()
+          })
+          // 提示是否要登出。即不回放当前请求!不然会形成递归
+          return handleAuthorized()
+        } finally {
+          requestList = []
+          isRefreshToken = false
+        }
+      } else {
+        // 添加到队列,等待刷新获取到新的令牌
+        return new Promise((resolve) => {
+          requestList.push(() => {
+            config.headers!.Authorization = 'Bearer ' + getAccessToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+            resolve(service2(config))
+          })
+        })
+      }
+    } else if (code === 500) {
+      ElMessage.error(t('sys.api.errMsg500'))
+      return Promise.reject(new Error(msg))
+    } else if (code === 901) {
+      ElMessage.error({
+        offset: 300,
+        dangerouslyUseHTMLString: true,
+        message:
+          '<div>' +
+          t('sys.api.errMsg901') +
+          '</div>' +
+          '<div> &nbsp; </div>' +
+          '<div>参考 https://doc.iocoder.cn/ 教程</div>' +
+          '<div> &nbsp; </div>' +
+          '<div>5 分钟搭建本地环境</div>'
+      })
+      return Promise.reject(new Error(msg))
+    } else if (code !== 200) {
+      if (msg === '无效的刷新令牌') {
+        // hard coding:忽略这个提示,直接登出
+        console.log(msg)
+        return handleAuthorized()
+      } else {
+        ElNotification.error({ title: msg })
+      }
+      return Promise.reject('error')
+    } else {
+      // const lang = localeStore.getCurrentLocale.lang
+
+      const requestUrl = response.config.url || ''
+      // 判断是否包含rq/iot路径
+      if (requestUrl.includes('rq/')||requestUrl.includes('system/dict')||requestUrl.includes('system/auth/get-permission-info')||requestUrl.includes('system/dept/list')
+        ||requestUrl.includes('system/menu/simple-list')||requestUrl.includes('system/menu/list')||requestUrl.includes('system/dept/simple-list')
+        ||requestUrl.includes('pms/')||requestUrl.includes('system/user/page')||requestUrl.includes('supplier/base/page')||requestUrl.includes('system/dept/get')
+        ||requestUrl.includes('system/user/simpleUserList')||requestUrl.includes('system/dept/companyLevelDepts')||requestUrl.includes('system/dept/companyLevelChildrenDepts')
+        ||requestUrl.includes('system/user/companyDeptsEmployee')||requestUrl.includes('system/dept/specifiedSimpleDepts')) {
+        const localeStore = useLocaleStore()
+        const lang = localeStore.getCurrentLocale.lang
+        if (data&& data.data) {
+          if (data.data.list) {
+            if (Array.isArray(data.data.list)) {
+              const list = langHelper.transformArray(data.data.list, lang)
+              data.data.list = list;
+              return data;
+            }
+          }else if (data &&Array.isArray(data.data)) {
+            const list = langHelper.transformArray(data.data, lang)
+            data.data = list;
+            return data;
+          }else if (data && typeof data.data === 'object') {
+            const object =  langHelper.transformObject(data, lang)
+            data = object
+            return data
+          } else {
+            return data
+          }
+        }
+
+      }else {
+        return data
+      }
+      // return data
+    }
+  },
+  (error: AxiosError) => {
+    console.log('err' + error) // for debug
+    let { message } = error
+    const { t } = useI18n()
+    if (message === 'Network Error') {
+      message = t('sys.api.errorMessage')
+    } else if (message.includes('timeout')) {
+      message = t('sys.api.apiTimeoutMessage')
+    } else if (message.includes('Request failed with status code')) {
+      message = t('sys.api.apiRequestFailed') + message.substr(message.length - 3)
+    }
+    ElMessage.error(message)
+    return Promise.reject(error)
+  }
+)
+
+const refreshToken = async () => {
+  axios.defaults.headers.common['tenant-id'] = getTenantId()
+  return await axios.post(base_url + '/system/auth/refresh-token?refreshToken=' + getRefreshToken())
+}
+const handleAuthorized = () => {
+  const { t } = useI18n()
+  if (!isRelogin.show) {
+    // 如果已经到登录页面则不进行弹窗提示
+    if (window.location.href.includes('login')) {
+      return
+    }
+    isRelogin.show = true
+    ElMessageBox.confirm(t('sys.api.timeoutMessage'), t('common.confirmTitle'), {
+      showCancelButton: false,
+      closeOnClickModal: false,
+      showClose: false,
+      closeOnPressEscape: false,
+      confirmButtonText: t('login.relogin'),
+      type: 'warning'
+    }).then(() => {
+      resetRouter() // 重置静态路由表
+      deleteUserCache() // 删除用户缓存
+      removeToken()
+      isRelogin.show = false
+      // 干掉token后再走一次路由让它过router.beforeEach的校验
+      window.location.href = window.location.href
+    })
+  }
+  return Promise.reject(t('sys.api.timeoutMessage'))
+}
+
+
+
+
+
+
+// index.ts
+const request = (option: any) => {
+  const { headersType, headers, ...otherOption } = option
+  return service2({
+    ...otherOption,
+    headers: {
+      'Content-Type': headersType || config.default_headers,
+      ...headers
+    }
+  })
+}
+export default {
+  get: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', ...option })
+    return res.data as unknown as T
+  },
+  post: async <T = any>(option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res.data as unknown as T
+  },
+  postOriginal: async (option: any) => {
+    const res = await request({ method: 'POST', ...option })
+    return res
+  },
+  delete: async <T = any>(option: any) => {
+    const res = await request({ method: 'DELETE', ...option })
+    return res.data as unknown as T
+  },
+  put: async <T = any>(option: any) => {
+    const res = await request({ method: 'PUT', ...option })
+    return res.data as unknown as T
+  },
+  download: async <T = any>(option: any) => {
+    const res = await request({ method: 'GET', responseType: 'blob', ...option })
+    return res as unknown as Promise<T>
+  },
+  upload: async <T = any>(option: any) => {
+    option.headersType = 'multipart/form-data'
+    const res = await request({ method: 'POST', ...option })
+    return res as unknown as Promise<T>
+  }
+}
+

Datei-Diff unterdrückt, da er zu groß ist
+ 576 - 577
src/locales/zh-CN.ts


+ 4 - 0
src/main.ts

@@ -35,10 +35,12 @@ import { createApp } from 'vue'
 
 import App from './App.vue'
 
+
 import './permission'
 
 import '@/plugins/tongji' // 百度统计
 import Logger from '@/utils/Logger'
+import mqttTool from '@/utils/mqttTool' // Mqtt工具
 
 import VueDOMPurifyHTML from 'vue-dompurify-html' // 解决v-html 的安全隐患
 
@@ -65,6 +67,8 @@ const setupAll = async () => {
   await router.isReady()
 
   app.use(VueDOMPurifyHTML)
+  app.config.globalProperties.$mqttTool = mqttTool
+
 
   app.mount('#app')
 }

+ 125 - 66
src/router/modules/remaining.ts

@@ -74,8 +74,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
     path: '/dingding',
     name: 'dingtalk',
     component: () => import('@/views/pms/dingding.vue'),
-    meta:{
-      hidden: true,
+    meta: {
+      hidden: true
     }
   },
   {
@@ -137,7 +137,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
 
@@ -340,8 +340,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     children: [
       {
         path: 'template/detail/:deptId/:isFill/:createTime*',
-        component: () => import('@/views/pms/iotopeationfill/StatisticsForm.vue' +
-        ''),
+        component: () => import('@/views/pms/iotopeationfill/StatisticsForm.vue' + ''),
         name: 'FillOrderInfo2',
         meta: {
           title: t('rem.FillInInformation'),
@@ -350,7 +349,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -364,7 +363,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     children: [
       {
         path: 'template/detail/:id',
-        component: () => import('@/views/pms/iotopeationfill/index1.vue'+''),
+        component: () => import('@/views/pms/iotopeationfill/index1.vue' + ''),
         name: 'FillOrderInfo',
         meta: {
           title: t('rem.FillInInformation'),
@@ -374,7 +373,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           keepAlive: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -397,7 +396,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           keepAlive: true,
           activeMenu: '/template/info'
         }
-      },
+      }
     ]
   },
   {
@@ -446,7 +445,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.OperationPlanEdit'),
           activeMenu: '/iotopeationfill/plan/add'
         }
-      },
+      }
     ]
   },
   {
@@ -502,7 +501,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           canTo: true,
           icon: 'ep:add',
-          title:  t('rem.AddEquipment'),
+          title: t('rem.AddEquipment'),
           activeMenu: '/device/base'
         }
       },
@@ -518,7 +517,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.EquipmentEditing'),
           activeMenu: '/device/base'
         }
-      },{
+      },
+      {
         path: 'device/detail/:id',
         component: () => import('@/views/pms/device/DeviceInfo.vue'),
         name: 'DeviceDetailInfo',
@@ -530,7 +530,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.EquipmentDetails'),
           activeMenu: '/device/info'
         }
-      },{
+      },
+      {
         path: 'tddevice/detail/:id/:ifInline/:time/:name/:code/:dept/:vehicle?',
         component: () => import('@/views/pms/device/monitor/TdDeviceInfo.vue'),
         name: 'TdDeviceDetail',
@@ -542,7 +543,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.MonitoringDetails'),
           activeMenu: '/device/info'
         }
-      },{
+      },
+      {
         path: 'device/upload/:id',
         component: () => import('@/views/pms/device/DeviceUpload.vue'),
         name: 'DeviceUpload',
@@ -554,7 +556,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.UploadFile'),
           activeMenu: '/device/upload'
         }
-      },{
+      },
+      {
         path: 'device/bom/:id',
         component: () => import('@/views/pms/device/bom/BomInfo.vue'),
         name: 'DeviceBom',
@@ -709,7 +712,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.ConfigureSafetyStock'),
           activeMenu: '/sapstock/safe'
         }
-      },
+      }
     ]
   },
 
@@ -786,7 +789,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
     name: 'PmsMainWorkOrderCenter',
     meta: {
       hidden: true,
-      keepAlive: true,
+      keepAlive: true
     },
     children: [
       {
@@ -945,7 +948,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.CollectionFormDetails'),
           activeMenu: '/materialreq/detail'
         }
-      },
+      }
     ]
   },
 
@@ -1057,7 +1060,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.RepairOrderEdit'),
           activeMenu: '/maintain/edit'
         }
-      },{
+      },
+      {
         path: 'maintain/detail/:id(\\d+)',
         component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
         name: 'MaintainDetail',
@@ -1069,7 +1073,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.RepairOrderDetail'),
           activeMenu: '/maintain/detail'
         }
-      },
+      }
     ]
   },
   {
@@ -1105,20 +1109,20 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.InspectionRouteEdit'),
           activeMenu: '/route/edit'
         }
-      // }
+        // }
         // ,{
-      //   path: 'route/detail/:id(\\d+)',
-      //   component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
-      //   name: 'InspectRouteDetail',
-      //   meta: {
-      //     noCache: false,
-      //     hidden: true,
-      //     canTo: true,
-      //     icon: 'ep:add',
-      //     title: '巡检路线详情',
-      //     activeMenu: '/route/detail'
-      //   }
-      },
+        //   path: 'route/detail/:id(\\d+)',
+        //   component: () => import('@/views/pms/maintain/IotMaintainDetail.vue'),
+        //   name: 'InspectRouteDetail',
+        //   meta: {
+        //     noCache: false,
+        //     hidden: true,
+        //     canTo: true,
+        //     icon: 'ep:add',
+        //     title: '巡检路线详情',
+        //     activeMenu: '/route/detail'
+        //   }
+      }
     ]
   },
 
@@ -1168,7 +1172,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
         //     title: '巡检路线详情',
         //     activeMenu: '/route/detail'
         //   }
-      },
+      }
     ]
   },
   {
@@ -1243,20 +1247,21 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: t('rem.InspectPlanEdit'),
           activeMenu: '/inspect/order/edit'
         }
+      },
+      {
+        path: '/inspect/order/detail/:id(\\d+)',
+        component: () => import('@/views/pms/inspect/order/InspectOrderDetail.vue'),
+        name: 'InspectOrderDetail',
+        meta: {
+          noCache: false,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:add',
+          title: t('rem.InspectOrderDetail'),
+          activeMenu: '/inspect/order/detail'
         }
-        ,{
-          path: '/inspect/order/detail/:id(\\d+)',
-          component: () => import('@/views/pms/inspect/order/InspectOrderDetail.vue'),
-          name: 'InspectOrderDetail',
-          meta: {
-            noCache: false,
-            hidden: true,
-            canTo: true,
-            icon: 'ep:add',
-            title: t('rem.InspectOrderDetail'),
-            activeMenu: '/inspect/order/detail'
-          }
-      },{
+      },
+      {
         path: '/inspect/order/write/:id(\\d+)',
         component: () => import('@/views/pms/inspect/order/WriteOrder.vue'),
         name: 'InspectOrderWrite',
@@ -2031,7 +2036,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
- 
+
   // 视频中心**********************************
   {
     path: '/videocenter',
@@ -2046,48 +2051,102 @@ const remainingRouter: AppRouteRecordRaw[] = [
     },
     children: [
       {
-        path: 'productTest',
-        component: () => import('@/views/pms/video_center/sip/product-list.vue'),
-        name: 'VideoCenterProductList',
+        path: 'category',
+        component: () => import('@/views/pms/video_center/category/index.vue'),
+        name: 'VideoCenterCategory',
         meta: {
-          title: '产品管理测试',
+          title: '产品分类',
           hidden: false,
-          icon: 'ep:box'
+          icon: 'ep:full-screen'
         }
       },
       {
         path: 'product',
-        component: () => import('@/views/pms/video_center/index.vue'),
+        component: () => import('@/views/pms/video_center/product/index.vue'),
         name: 'VideoCenterProduct',
         meta: {
           title: '产品管理',
           hidden: false,
-          icon: 'ep:box'
+          icon: 'ep:full-screen'
         }
       },
+
+      // 协议管理
+
        {
-        path: 'shebei',
-        component: () => import('@/views/pms/video_center/shebei.vue'),
-        name: 'VideoCenterShebei',
+        path: 'protocol',
+        component: () => import('@/views/pms/video_center/protocol/index.vue'),
+        name: 'VideoCenterProtocol',
+        meta: {
+          title: '协议管理',
+          hidden: false,
+          icon: 'ep:full-screen'
+        }
+      },
+
+      
+
+      {
+        path: 'product/product-edit',
+        component: () => import('@/views/pms/video_center/product/product-edit.vue'),
+        name: 'VideoCenterProductEdit',
+        meta: {
+          title: '产品编辑/新增',
+          hidden: true
+        }
+      },
+      {
+        path: 'device',
+        component: () => import('@/views/pms/video_center/device/index.vue'),
+        name: 'VideoCenterDevice',
         meta: {
-          title: '设备的管理',
+          title: '设备管理',
           hidden: false,
           icon: 'ep:monitor'
         }
       },
-       {
-        path: 'sip',
-        component: () => import('@/views/pms/video_center/ChannelManage.vue'),
-        name: 'VideoCentersip',
+      // 新增/编辑 设备
+      {
+        path: 'device/device-edit',
+        component: () => import('@/views/pms/video_center/device/device-edit.vue'),
+        name: 'VideoCenterDeviceEdit',
+        meta: {
+          title: '设备编辑/新增',
+          hidden: true
+        }
+      },
+      {
+        path: 'splitview',
+        component: () => import('@/views/pms/video_center/sip/splitview.vue'),
+        name: 'VideoCenterSplitview',
+        meta: {
+          title: '分屏显示',
+          hidden: false,
+          icon: 'ep:full-screen'
+        }
+      },
+      {
+        path: 'channelManagement',
+        component: () => import('@/views/pms/video_center/sip/index.vue'),
+        name: 'VideoCenterProductList',
         meta: {
           title: '通道管理',
           hidden: false,
-          icon: 'ep:operation'
+          icon: 'ep:box'
+        }
+      },
+      {
+        path: 'mediaServer',
+        component: () => import('@/views/pms/video_center/sip/mediaServer.vue'),
+        name: 'VideoCenterMediaServer',
+        meta: {
+          title: '视频配置',
+          hidden: false,
+          icon: 'ep:setting'
         }
       }
     ]
-  },
-
+  }
 ]
 
 export default remainingRouter

+ 40 - 0
src/utils/dateUtil.ts

@@ -15,4 +15,44 @@ export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): str
   return dayjs(date).format(format)
 }
 
+export function parseTime(time, pattern) {
+  if (arguments.length === 0 || !time) {
+    return null
+  }
+  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+  let date
+  if (typeof time === 'object') {
+    date = time
+  } else {
+    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+      time = parseInt(time)
+    } else if (typeof time === 'string') {
+      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+    }
+    if ((typeof time === 'number') && (time.toString().length === 10)) {
+      time = time * 1000
+    }
+    date = new Date(time)
+  }
+  const formatObj = {
+    y: date.getFullYear(),
+    m: date.getMonth() + 1,
+    d: date.getDate(),
+    h: date.getHours(),
+    i: date.getMinutes(),
+    s: date.getSeconds(),
+    a: date.getDay()
+  }
+  const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+    let value = formatObj[key]
+    // Note: getDay() returns 0 on Sunday
+    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+    if (result.length > 0 && value < 10) {
+      value = '0' + value
+    }
+    return value || 0
+  })
+  return time_str
+}
+
 export const dateUtil = dayjs

+ 87 - 0
src/utils/download2.js

@@ -0,0 +1,87 @@
+import axios from 'axios'
+import { ElMessage } from 'element-ui'
+import { saveAs } from 'file-saver'
+import { getAccessToken } from '@/utils/auth'
+
+const baseURL = import.meta.env.VITE_BASE_URL
+
+const errorCode = {
+  401: '认证失败,无法访问系统资源',
+  403: '当前操作没有权限',
+  404: '访问资源不存在',
+  default: '系统未知错误,请反馈给管理员'
+}
+
+async function blobValidate(data) {
+  try {
+    const text = await data.text()
+    JSON.parse(text)
+    return false
+  } catch (error) {
+    return true
+  }
+}
+
+export default {
+  name(name, isDelete = true) {
+    var url =
+      baseURL + '/common/download?fileName=' + encodeURIComponent(name) + '&delete=' + isDelete
+    axios({
+      method: 'get',
+      url: url,
+      responseType: 'blob',
+      headers: { Authorization: 'Bearer ' + getAccessToken() }
+    }).then(async (res) => {
+      const isLogin = await blobValidate(res.data)
+      if (isLogin) {
+        const blob = new Blob([res.data])
+        this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
+      } else {
+        this.printErrMsg(res.data)
+      }
+    })
+  },
+  resource(resource) {
+    var url = baseURL + '/common/download/resource?resource=' + encodeURIComponent(resource)
+    axios({
+      method: 'get',
+      url: url,
+      responseType: 'blob',
+      headers: { Authorization: 'Bearer ' + getAccessToken() }
+    }).then(async (res) => {
+      const isLogin = await blobValidate(res.data)
+      if (isLogin) {
+        const blob = new Blob([res.data])
+        this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
+      } else {
+        this.printErrMsg(res.data)
+      }
+    })
+  },
+  zip(url, name) {
+    var url = baseURL + url
+    axios({
+      method: 'get',
+      url: url,
+      responseType: 'blob',
+      headers: { Authorization: 'Bearer ' + getAccessToken() }
+    }).then(async (res) => {
+      const isLogin = await blobValidate(res.data)
+      if (isLogin) {
+        const blob = new Blob([res.data], { type: 'application/zip' })
+        this.saveAs(blob, name)
+      } else {
+        this.printErrMsg(res.data)
+      }
+    })
+  },
+  saveAs(text, name, opts) {
+    saveAs(text, name, opts)
+  },
+  async printErrMsg(data) {
+    const resText = await data.text()
+    const rspObj = JSON.parse(resText)
+    const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+    ElMessage.error(errMsg)
+  }
+}

+ 147 - 0
src/utils/mqttTool.js

@@ -0,0 +1,147 @@
+import mqtt from 'mqtt'
+import { getAccessToken } from '@/utils/auth'
+
+let mqttTool = {
+  client: null
+}
+
+/** 连接Mqtt */
+mqttTool.connect = function () {
+  let options = {
+    username: 'yanfan',
+    password: getAccessToken(),
+    cleanSession: true,
+    keepAlive: 30,
+    clientId: 'web-' + Math.random().toString(16).substr(2),
+    connectTimeout: 60000
+  }
+  // 配置Mqtt地址
+  let url = import.meta.env.VITE_MQTT_SERVER_URL
+  if (url == '') {
+    if (window.location.protocol === 'http:') {
+      url = 'ws://' + window.location.host + '/mqtt'
+    } else {
+      url = 'wss://' + window.location.host + '/mqtt'
+    }
+  }
+
+  mqttTool.client = mqtt.connect(url, options)
+  mqttTool.client.on('connect', (e) => {
+    console.log('mqtt连接成功')
+  })
+  // 重新连接
+  mqttTool.client.on('reconnect', (error) => {
+    console.log('正在重连:', error)
+  })
+  // 发生错误
+  mqttTool.client.on('error', (error) => {
+    console.log('Mqtt客户端连接失败:', error)
+    mqttTool.client.end()
+  })
+  // 断开连接
+  mqttTool.client.on('close', function (res) {
+    console.log('已断开Mqtt连接')
+  })
+}
+/** 断开连接 */
+mqttTool.end = function () {
+  return new Promise((resolve, reject) => {
+    if (mqttTool.client == null) {
+      resolve('未连接')
+      console.log('未连接')
+      return
+    }
+    mqttTool.client.end()
+    mqttTool.client = null
+    console.log('Mqtt服务器已断开连接!')
+    resolve('连接终止')
+  })
+}
+/** 重新连接 */
+mqttTool.reconnect = function () {
+  return new Promise((resolve, reject) => {
+    if (mqttTool.client == null) {
+      // 调用resolve方法,Promise变为操作成功状态(fulfilled)
+      resolve('未连接')
+      console.log('未连接')
+      return
+    }
+    console.log('正在重连...', res)
+    mqttTool.client.reconnect()
+  })
+}
+/** 消息订阅 */
+mqttTool.subscribe = function (topics) {
+  return new Promise((resolve, reject) => {
+    if (mqttTool.client == null) {
+      resolve('未连接')
+      console.log('未连接')
+      return
+    }
+    mqttTool.client.subscribe(
+      topics,
+      {
+        qos: 1
+      },
+      function (err, res) {
+        console.log('订阅主题:', topics)
+        if (!err) {
+          console.log('订阅成功')
+          resolve('订阅成功')
+        } else {
+          console.log('订阅失败,主题可能已经订阅')
+          resolve('订阅失败')
+          return
+        }
+      }
+    )
+  })
+}
+/** 取消订阅 */
+mqttTool.unsubscribe = function (topics) {
+  return new Promise((resolve, reject) => {
+    if (mqttTool.client == null) {
+      resolve('未连接')
+      console.log('未连接')
+      return
+    }
+    mqttTool.client.unsubscribe(topics, function (err) {
+      if (!err) {
+        resolve('取消订阅成功')
+        console.log('取消订阅成功')
+      } else {
+        resolve('取消订阅失败')
+        console.log('取消订阅失败')
+        return
+      }
+    })
+  })
+}
+mqttTool.publish = function (topic, message, name) {
+  return new Promise((resolve, reject) => {
+    if (mqttTool.client == null) {
+      resolve('Mqtt客户端未连接')
+      console.log('Mqtt客户端未连接')
+      return
+    }
+    mqttTool.client.publish(topic, message, { qos: 1 }, function (err) {
+      console.log('发送主题:', topic)
+      console.log('发送内容:', message)
+      if (!err) {
+        if (topic.indexOf('offline') > 0) {
+          console.log('[ ' + name + ' ] 影子指令发送成功')
+          resolve('[ ' + name + ' ] 影子指令发送成功')
+        } else {
+          console.log('[ ' + name + ' ] 指令发送成功')
+          resolve('[ ' + name + ' ] 指令发送成功')
+        }
+      } else {
+        console.log('[ ' + name + ' ] 指令发送失败')
+        reject('[ ' + name + ' ] 指令发送失败')
+        return
+      }
+    })
+  })
+}
+
+export default mqttTool

+ 2 - 0
src/views/pms/device/index.vue

@@ -140,6 +140,8 @@
               {{ scope.$index + 1 }}
             </template>
           </el-table-column>
+
+
           <el-table-column :label="t('iotDevice.yfCode')" sortable align="center" prop="yfDeviceCode" min-width="150"/>
           <el-table-column :label="t('iotDevice.code')" sortable align="center" prop="deviceCode" min-width="150"/>
           <el-table-column :label="t('iotDevice.name')" sortable align="center" prop="deviceName" min-width="250">

+ 4 - 6
src/views/pms/qhse/measure/IotMeasureBookForm.vue

@@ -7,9 +7,7 @@
       label-width="100px"
       v-loading="formLoading"
     >
-      <el-form-item label="计量器具编码" prop="measureCode">
-        <el-input v-model="formData.measureCode" placeholder="请输入计量器具编码" />
-      </el-form-item>
+     
       <el-form-item label="计量器具名称" prop="measureName">
         <el-input v-model="formData.measureName" placeholder="请输入计量器具名称" />
       </el-form-item>
@@ -86,7 +84,7 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
-  measureCode: undefined,
+  // measureCode: undefined,
   measureName: undefined,
   classify: undefined,
   dutyPerson: undefined,
@@ -102,7 +100,7 @@ const formData = ref({
   deptId: undefined,
 })
 const formRules = reactive({
-  measureCode: [{ required: true, message: '计量器具编码不能为空', trigger: 'blur' }],
+  // measureCode: [{ required: true, message: '计量器具编码不能为空', trigger: 'blur' }],
   measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
   classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
   dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
@@ -155,7 +153,7 @@ const submitForm = async () => {
 const resetForm = () => {
   formData.value = {
     id: undefined,
-    measureCode: undefined,
+    // measureCode: undefined,
     measureName: undefined,
     classify: undefined,
     dutyPerson: undefined,

+ 254 - 234
src/views/pms/qhse/measure/index.vue

@@ -1,239 +1,249 @@
 <template>
-  <ContentWrap>
-    <!-- 搜索工作栏 -->
-    <el-form
-      class="-mb-15px"
-      :model="queryParams"
-      ref="queryFormRef"
-      :inline="true"
-      label-width="68px"
-    >
-      <el-form-item label="计量器具编码" prop="measureCode">
-        <el-input
-          v-model="queryParams.measureCode"
-          placeholder="请输入计量器具编码"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="计量器具名称" prop="measureName">
-        <el-input
-          v-model="queryParams.measureName"
-          placeholder="请输入计量器具名称"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="分类" prop="classify">
-        <el-input
-          v-model="queryParams.classify"
-          placeholder="请输入分类"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="责任人" prop="dutyPerson">
-        <el-input
-          v-model="queryParams.dutyPerson"
-          placeholder="请输入责任人"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="采购日期" prop="buyDate">
-        <el-date-picker
-          v-model="queryParams.buyDate"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-220px"
-        />
-      </el-form-item>
-      <el-form-item label="品牌" prop="brand">
-        <el-input
-          v-model="queryParams.brand"
-          placeholder="请输入品牌"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="规格型号" prop="modelName">
-        <el-input
-          v-model="queryParams.modelName"
-          placeholder="请输入规格型号"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="有效期" prop="validity">
-        <el-date-picker
-          v-model="queryParams.validity"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择有效期"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="上次检验/校准日期" prop="lastTime">
-        <el-date-picker
-          v-model="queryParams.lastTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-220px"
-        />
-      </el-form-item>
-      <el-form-item label="单位" prop="measureUnit">
-        <el-input
-          v-model="queryParams.measureUnit"
-          placeholder="请输入单位"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="价格" prop="measurePrice">
-        <el-input
-          v-model="queryParams.measurePrice"
-          placeholder="请输入价格"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="图片" prop="measurePic">
-        <el-input
-          v-model="queryParams.measurePic"
-          placeholder="请输入图片"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input
-          v-model="queryParams.remark"
-          placeholder="请输入备注"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="daterange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-220px"
-        />
-      </el-form-item>
-      <el-form-item label="部门id" prop="deptId">
-        <el-input
-          v-model="queryParams.deptId"
-          placeholder="请输入部门id"
-          clearable
-          @keyup.enter="handleQuery"
-          class="!w-240px"
-        />
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
-        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button
-          type="primary"
-          plain
-          @click="openForm('create')"
-          v-hasPermi="['rq:iot-measure-book:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
-        </el-button>
-        <el-button
-          type="success"
-          plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['rq:iot-measure-book:export']"
-        >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
 
-  <!-- 列表 -->
-  <ContentWrap>
-    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="主键id" align="center" prop="id" />
-      <el-table-column label="计量器具编码" align="center" prop="measureCode" />
-      <el-table-column label="计量器具名称" align="center" prop="measureName" />
-      <el-table-column label="分类" align="center" prop="classify" />
-      <el-table-column label="责任人" align="center" prop="dutyPerson" />
-      <el-table-column label="采购日期" align="center" prop="buyDate" />
-      <el-table-column label="品牌" align="center" prop="brand" />
-      <el-table-column label="规格型号" align="center" prop="modelName" />
-      <el-table-column
-        label="有效期"
-        align="center"
-        prop="validity"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="上次检验/校准日期" align="center" prop="lastTime" />
-      <el-table-column label="单位" align="center" prop="measureUnit" />
-      <el-table-column label="价格" align="center" prop="measurePrice" />
-      <el-table-column label="图片" align="center" prop="measurePic" />
-      <el-table-column label="备注" align="center" prop="remark" />
-      <el-table-column
-        label="创建时间"
-        align="center"
-        prop="createTime"
-        :formatter="dateFormatter"
-        width="180px"
-      />
-      <el-table-column label="部门id" align="center" prop="deptId" />
-      <el-table-column label="操作" align="center" min-width="120px">
-        <template #default="scope">
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['rq:iot-measure-book:update']"
-          >
-            编辑
-          </el-button>
-          <el-button
-            link
-            type="danger"
-            @click="handleDelete(scope.row.id)"
-            v-hasPermi="['rq:iot-measure-book:delete']"
+  <el-row :gutter="20">
+
+     <!-- 左侧部门树 -->
+    <el-col :span="4" :xs="24">
+      <ContentWrap class="h-1/1" v-if="treeShow">
+        <DeptTree @node-click="handleDeptNodeClick" />
+      </ContentWrap>
+    </el-col>
+
+     <el-col :span="contentSpan" :xs="24">
+
+        <ContentWrap>
+          <!-- 搜索工作栏 -->
+          <el-form
+            class="-mb-15px"
+            :model="queryParams"
+            ref="queryFormRef"
+            :inline="true"
+            
           >
-            删除
-          </el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-    <!-- 分页 -->
-    <Pagination
-      :total="total"
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </ContentWrap>
+           
+            <el-form-item label="计量器具名称" prop="measureName">
+              <el-input
+                v-model="queryParams.measureName"
+                placeholder="请输入计量器具名称"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <!-- <el-form-item label="分类" prop="classify">
+              <el-input
+                v-model="queryParams.classify"
+                placeholder="请输入分类"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <el-form-item label="责任人" prop="dutyPerson">
+              <el-input
+                v-model="queryParams.dutyPerson"
+                placeholder="请输入责任人"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <!-- <el-form-item label="采购日期" prop="buyDate">
+              <el-date-picker
+                v-model="queryParams.buyDate"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="品牌" prop="brand">
+              <el-input
+                v-model="queryParams.brand"
+                placeholder="请输入品牌"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="规格型号" prop="modelName">
+              <el-input
+                v-model="queryParams.modelName"
+                placeholder="请输入规格型号"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="有效期" prop="validity">
+              <el-date-picker
+                v-model="queryParams.validity"
+                value-format="YYYY-MM-DD"
+                type="date"
+                placeholder="选择有效期"
+                clearable
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="上次检验/校准日期" prop="lastTime">
+              <el-date-picker
+                v-model="queryParams.lastTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="单位" prop="measureUnit">
+              <el-input
+                v-model="queryParams.measureUnit"
+                placeholder="请输入单位"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="价格" prop="measurePrice">
+              <el-input
+                v-model="queryParams.measurePrice"
+                placeholder="请输入价格"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <!-- <el-form-item label="图片" prop="measurePic">
+              <el-input
+                v-model="queryParams.measurePic"
+                placeholder="请输入图片"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="备注" prop="remark">
+              <el-input
+                v-model="queryParams.remark"
+                placeholder="请输入备注"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item>
+            <el-form-item label="创建时间" prop="createTime">
+              <el-date-picker
+                v-model="queryParams.createTime"
+                value-format="YYYY-MM-DD HH:mm:ss"
+                type="daterange"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                class="!w-220px"
+              />
+            </el-form-item>
+            <el-form-item label="部门id" prop="deptId">
+              <el-input
+                v-model="queryParams.deptId"
+                placeholder="请输入部门id"
+                clearable
+                @keyup.enter="handleQuery"
+                class="!w-240px"
+              />
+            </el-form-item> -->
+            <el-form-item>
+              <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+              <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+              <el-button
+                type="primary"
+                plain
+                @click="openForm('create')"
+                
+              >
+                <Icon icon="ep:plus" class="mr-5px" /> 新增
+              </el-button>
+              <el-button
+                type="success"
+                plain
+                @click="handleExport"
+                :loading="exportLoading"
+                v-hasPermi="['rq:iot-measure-book:export']"
+              >
+                <Icon icon="ep:download" class="mr-5px" /> 导出
+              </el-button>
+            </el-form-item>
+          </el-form>
+        </ContentWrap>
+
+        <!-- 列表 -->
+        <ContentWrap>
+          <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+            <el-table-column label="主键id" align="center" prop="id" />
+            <el-table-column label="计量器具编码" align="center" prop="measureCode" />
+            <el-table-column label="计量器具名称" align="center" prop="measureName" />
+            <el-table-column label="分类" align="center" prop="classify" />
+            <el-table-column label="责任人" align="center" prop="dutyPerson" />
+            <el-table-column label="采购日期" align="center" prop="buyDate" />
+            <el-table-column label="品牌" align="center" prop="brand" />
+            <el-table-column label="规格型号" align="center" prop="modelName" />
+            <el-table-column
+              label="有效期"
+              align="center"
+              prop="validity"
+              :formatter="dateFormatter"
+              width="180px"
+            />
+            <el-table-column label="上次检验/校准日期" align="center" prop="lastTime" />
+            <el-table-column label="单位" align="center" prop="measureUnit" />
+            <el-table-column label="价格" align="center" prop="measurePrice" />
+            <el-table-column label="图片" align="center" prop="measurePic" />
+            <el-table-column label="备注" align="center" prop="remark" />
+            <el-table-column
+              label="创建时间"
+              align="center"
+              prop="createTime"
+              :formatter="dateFormatter"
+              width="180px"
+            />
+            <el-table-column label="部门id" align="center" prop="deptId" />
+            <el-table-column label="操作" align="center" min-width="120px">
+              <template #default="scope">
+                <el-button
+                  link
+                  type="primary"
+                  @click="openForm('update', scope.row.id)"
+                  v-hasPermi="['rq:iot-measure-book:update']"
+                >
+                  编辑
+                </el-button>
+                <el-button
+                  link
+                  type="danger"
+                  @click="handleDelete(scope.row.id)"
+                  v-hasPermi="['rq:iot-measure-book:delete']"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <!-- 分页 -->
+          <Pagination
+            :total="total"
+            v-model:page="queryParams.pageNo"
+            v-model:limit="queryParams.pageSize"
+            @pagination="getList"
+          />
+        </ContentWrap>
+     </el-col>
+
+  
+
+  </el-row>
+  
 
   <!-- 表单弹窗:添加/修改 -->
   <IotMeasureBookForm ref="formRef" @success="getList" />
@@ -244,7 +254,7 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import { IotMeasureBookApi, IotMeasureBookVO } from '@/api/rq/iotmeasurebook'
 import IotMeasureBookForm from './IotMeasureBookForm.vue'
-
+import DeptTree from '@/views/system/user/DeptTree.vue'
 /** 计量器具台账 列表 */
 defineOptions({ name: 'IotMeasureBook' })
 
@@ -339,4 +349,14 @@ const handleExport = async () => {
 onMounted(() => {
   getList()
 })
+
+
+const treeShow = ref(true);
+/** 处理部门被点击 */
+const handleDeptNodeClick = async (row) => {
+  queryParams.deptId = row.id
+  await getList()
+}
+
+const contentSpan = ref(20)
 </script>

+ 0 - 245
src/views/pms/video_center/ChannelManage.vue

@@ -1,245 +0,0 @@
-<template>
-  <div class="channel-manage-page">
-    <el-card class="mb-4" shadow="never">
-      <el-form :inline="true" :model="filters" class="filter-form">
-        <el-row :gutter="20">
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="设备ID">
-              <el-input
-                v-model="filters.deviceId"
-                placeholder="请输入设备编号"
-                clearable
-                style="width: 150px"
-              />
-            </el-form-item>
-          </el-col>
-
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="通道ID">
-              <el-input
-                v-model="filters.channelId"
-                placeholder="请输入通道ID"
-                clearable
-                style="width: 150px"
-              />
-            </el-form-item>
-          </el-col>
-
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="状态">
-              <el-select
-                v-model="filters.status"
-                placeholder="请选择状态"
-                clearable
-                style="width: 150px"
-              >
-                <el-option label="全部" value="" />
-                <el-option label="在线" value="online" />
-                <el-option label="离线" value="offline" />
-              </el-select>
-            </el-form-item>
-          </el-col>
-
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item>
-              <el-button type="primary" @click="search" :icon="Search">搜索</el-button>
-              <el-button @click="reset" type="primary" plain :icon="Refresh">重置</el-button>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-    </el-card>
-
-    <el-card shadow="never">
-      <el-table :data="displayList" style="width: 100%">
-        <el-table-column prop="channelName" label="通道名称" min-width="140" />
-        <el-table-column prop="deviceName" label="关联设备" min-width="140" />
-        <el-table-column prop="channelId" label="通道索引/编号" min-width="120" />
-        <el-table-column prop="type" label="通道类型" min-width="100">
-          <template #default="{ row }">
-            <el-tag type="success" v-if="row.type === 'video'">视频</el-tag>
-            <el-tag type="info" v-else>音频</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="mainStream" label="主码流URL" min-width="200">
-          <template #default="{ row }">
-            <span class="truncate">{{ row.mainStream }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="subStream" label="子码流URL" min-width="200">
-          <template #default="{ row }">
-            <span class="truncate">{{ row.subStream }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="status" label="通道状态" width="120">
-          <template #default="{ row }">
-            <el-tag :type="row.status === 'online' ? 'success' : 'info'">{{
-              row.status === 'online' ? '在线' : '离线'
-            }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" min-width="220" align="center">
-          <template #default="{ row }">
-            <el-button text @click="previewStream(row, 'main')">主码流预览</el-button>
-            <el-button text @click="previewStream(row, 'sub')">子码流预览</el-button>
-            <el-button text type="primary" @click="testStream(row)">测试</el-button>
-            <el-button text type="danger" @click="removeRow(row)">删除</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-card>
-
-    <el-dialog v-model="previewVisible" :title="previewTitle" width="720px">
-      <div v-if="previewUrl">
-        <!-- 使用 video 标签或 iframe 作为预览容器(取决于流地址类型) -->
-        <video
-          v-if="isVideoPreview"
-          :src="previewUrl"
-          controls
-          autoplay
-          muted
-          playsinline
-          style="width: 100%; height: 420px; background: #000"
-        ></video>
-        <iframe
-          v-else
-          :src="previewUrl"
-          frameborder="0"
-          style="width: 100%; height: 420px"
-        ></iframe>
-      </div>
-      <div v-else style="color: #999; padding: 20px">无可用预览地址</div>
-      <template #footer>
-        <el-button @click="previewVisible = false">关闭</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, computed } from 'vue'
-import { ElMessage } from 'element-plus'
-import { Plus, Search,Refresh } from '@element-plus/icons-vue'
-
-interface ChannelItem {
-  id: string
-  channelName: string
-  deviceId: string
-  deviceName: string
-  channelId: string
-  type: 'video' | 'audio'
-  mainStream: string
-  subStream: string
-  status: 'online' | 'offline'
-}
-
-// mock 数据,替换为 API 调用
-const rawList = ref<ChannelItem[]>([
-  {
-    id: '1',
-    channelName: '通道01',
-    deviceId: 'DEV001',
-    deviceName: '设备A',
-    channelId: '1',
-    type: 'video',
-    mainStream: 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8',
-    subStream: '',
-    status: 'online'
-  },
-  {
-    id: '2',
-    channelName: '通道02',
-    deviceId: 'DEV001',
-    deviceName: '设备A',
-    channelId: '2',
-    type: 'audio',
-    mainStream: '',
-    subStream: '',
-    status: 'offline'
-  },
-  {
-    id: '3',
-    channelName: '模拟通道01',
-    deviceId: 'DEV002',
-    deviceName: '设备B',
-    channelId: '模拟通道01',
-    type: 'video',
-    mainStream: 'https://www.w3schools.com/html/mov_bbb.mp4',
-    subStream: '',
-    status: 'online'
-  }
-])
-
-const filters = reactive({ deviceId: '', channelId: '', status: '' })
-
-const displayList = computed(() => {
-  return rawList.value.filter((it) => {
-    if (filters.deviceId && !String(it.deviceId).includes(filters.deviceId)) return false
-    if (filters.channelId && !String(it.channelId).includes(filters.channelId)) return false
-    if (filters.status && it.status !== filters.status) return false
-    return true
-  })
-})
-
-const previewVisible = ref(false)
-const previewUrl = ref('')
-const previewTitle = ref('')
-const isVideoPreview = ref(true)
-
-function search() {
-  // computed displayList 会自动更新
-}
-function reset() {
-  filters.deviceId = ''
-  filters.channelId = ''
-  filters.status = ''
-}
-
-function previewStream(row: ChannelItem, which: 'main' | 'sub') {
-  const url = which === 'main' ? row.mainStream : row.subStream
-  previewUrl.value = url || ''
-  isVideoPreview.value =
-    !!url && (url.endsWith('.mp4') || url.endsWith('.m3u8') || url.startsWith('http'))
-  previewTitle.value = `${row.channelName} - ${which === 'main' ? '主码流' : '子码流'}`
-  previewVisible.value = true
-}
-
-function testStream(row: ChannelItem) {
-  // 简单的可达性测试(只做模拟)
-  if (!row.mainStream && !row.subStream) {
-    ElMessage.warning('无可用流地址进行测试')
-    return
-  }
-  ElMessage.info('开始测试流地址(仅模拟)')
-  // 真实场景应调用后端或尝试 fetch/HEAD 请求验证
-}
-
-function removeRow(row: ChannelItem) {
-  rawList.value = rawList.value.filter((r) => r.id !== row.id)
-  ElMessage.success('已删除')
-}
-</script>
-
-<style scoped>
-.channel-manage-page {
-  padding: 16px;
-}
-.mb-4 {
-  margin-bottom: 16px;
-}
-.filter-form {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-.truncate {
-  display: inline-block;
-  max-width: 400px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.status-legend-name {
-  color: #fff;
-}
-</style>

+ 350 - 0
src/views/pms/video_center/category/index.vue

@@ -0,0 +1,350 @@
+<template>
+  <div style="padding: 6px">
+    <el-card v-show="showSearch" style="margin-bottom: 5px" shadow="never"> 
+
+      <el-form :model="queryParams" ref="queryFormRef" inline style="margin-bottom: -20px">
+        <el-form-item :label="t('product.index091251-2')" prop="categoryName">
+          <el-input 
+            v-model="queryParams.categoryName" 
+            :placeholder="t('product.index091251-3')" 
+            clearable 
+            size="default" 
+            @keyup.enter="handleQuery" 
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" size="default" @click="handleQuery">{{ t('search') }}</el-button>
+          <el-button :icon="Refresh" size="default" @click="resetQuery">{{ t('reset') }}</el-button>
+          <el-checkbox v-model="queryParams.showSenior" style="margin: 0px 10px" @change="handleQuery">{{ t('product.index091251-8') }}</el-checkbox>
+          <el-tooltip :content="t('product.index091251-9')" placement="top">
+            <el-icon><InfoFilled /></el-icon>
+          </el-tooltip>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card class="main-card" shadow="never">
+      <div class="card-toolbar mb8">
+        <el-button type="primary" :icon="Plus" size="default" @click="handleAdd">新增</el-button>
+      </div>
+      
+      <el-table 
+        class="base-table" 
+        v-loading="loading" 
+        :data="categoryList" 
+        @selection-change="handleSelectionChange" 
+        
+      >
+        <el-table-column label="产品分类名称" align="center" prop="categoryName" min-width="150" />
+        <el-table-column label="备注" align="left" header-align="center" prop="remark" min-width="150" />
+        <el-table-column label="系统定义" align="center" prop="isSys" min-width="150">
+          <template #default="scope">
+            <el-icon v-if="scope.row.isSys == 1" style="color: #346cef; font-size: 20px"><Check /></el-icon>
+            <el-icon v-else style="font-size: 20px"><CircleClose /></el-icon>
+          </template>
+        </el-table-column>
+        <el-table-column label="显示顺序" align="center" prop="orderNum" min-width="150" />
+        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
+          <template #default="scope">
+            <el-button
+              size="small"
+              type="primary"
+              plain
+              style="padding: 5px"
+              :icon="View"
+              @click="handleUpdate(scope.row)"
+              v-if="scope.row.isSys == '0' ? true : !isTenant"
+            >
+              查看
+            </el-button>
+            <el-button
+              size="small"
+              type="danger"
+              plain
+              style="padding: 5px"
+              :icon="Delete"
+              @click="handleDelete(scope.row)"
+              v-if="scope.row.isSys == '0' ? true : !isTenant"
+            >
+              删除
+            </el-button>
+            <span style="font-size: 10px; color: #999" v-if="scope.row.isSys == '1' && isTenant">系统定义,不能修改</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination 
+        v-show="total > 0" 
+        :total="total" 
+        v-model:page="queryParams.pageNum" 
+        v-model:limit="queryParams.pageSize" 
+        @pagination="getList" 
+      />
+
+      <!-- 添加或修改产品分类对话框 -->
+      <el-dialog 
+        v-model="open" 
+        title="产品分类" 
+        width="500px" 
+        append-to-body
+      >
+        <el-form 
+          ref="formRef" 
+          :model="form" 
+          :rules="rules" 
+          label-width="80px"
+        >
+          <el-form-item label="分类名称" prop="categoryName">
+            <el-input v-model="form.categoryName" placeholder="请输入产品分类名称" />
+          </el-form-item>
+          <el-form-item label="显示顺序" prop="orderNum">
+            <el-input-number 
+              controls-position="right" 
+              v-model="form.orderNum" 
+              type="number" 
+              style="width: 150px;"
+              placeholder="请输入显示顺序" 
+            />
+          </el-form-item>
+          <el-form-item label="备注" prop="remark">
+            <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button type="primary" @click="submitForm" v-show="form.categoryId">修 改</el-button>
+            <el-button type="primary" @click="submitForm" v-show="!form.categoryId">新 增</el-button>
+            <el-button @click="cancel">取 消</el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox, ElForm } from 'element-plus'
+import { Search, Refresh, InfoFilled, Check, CircleClose, Plus, View, Delete } from '@element-plus/icons-vue'
+import { parseTime } from '@/utils/dateUtil'
+
+// API 导入
+import { addCategory, delCategory, getCategory, listCategory, updateCategory } from '@/api/pms/video/category'
+
+const { t } = useI18n() // 国际化
+// 定义响应式数据
+const isTenant = ref(false)
+const loading = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const showSearch = ref(true)
+const total = ref(0)
+const categoryList = ref([])
+const open = ref(false)
+const queryFormRef = ref(ElForm)
+const formRef = ref()
+
+// 查询参数
+const queryParams = reactive({
+  showSenior: false,
+  pageNum: 1,
+  pageSize: 20,
+  categoryName: null,
+  isSys: null,
+})
+
+// 表单参数
+const form = reactive({
+  categoryId: null,
+  categoryName: null,
+  tenantId: null,
+  tenantName: null,
+  isSys: null,
+  parentId: null,
+  orderNum: null,
+  delFlag: null,
+  createBy: null,
+  createTime: null,
+  updateBy: null,
+  updateTime: null,
+  remark: null,
+})
+
+// 表单校验规则
+const rules = {
+  categoryName: [
+    {
+      required: true,
+      message: t("product.category142342-4"),
+      trigger: 'blur',
+    },
+  ],
+  isSys: [
+    {
+      required: true,
+      message: t("product.category142342-5"),
+      trigger: 'blur',
+    },
+  ],
+}
+
+// 初始化
+const init = () => {
+  // 这里假设 store 可通过 useStore 获取
+  // const store = useStore()
+  // if (store.state.user.roles.indexOf('tenant') !== -1) {
+  //   isTenant.value = true
+  // }
+}
+
+// 获取列表数据
+const getList = () => {
+  loading.value = true
+    listCategory(queryParams).then((response) => {
+    console.log(response);
+    categoryList.value = response.list
+    total.value = response.total
+    loading.value = false 
+  })
+
+    // 测试数据
+    // setTimeout(() => {
+    //     categoryList.value = [
+    //     {
+    //       categoryId: 1,
+    //       categoryName: '分类1',
+    //       tenantId: null,
+    //       tenantName: null,
+    //       isSys: '0',
+    //       parentId: null,
+    //       orderNum: 1,
+    //       delFlag: null,
+    //       createBy: null,
+    //       createTime: '2023-05-05T09:09:09.000+00:00',
+    //     }
+    //   ]
+    //   total.value = 1
+    //   loading.value = false
+    // }, 1000)
+}
+
+// 取消操作
+const cancel = () => {
+  open.value = false
+  reset()
+}
+
+// 重置表单
+const reset = () => {
+  Object.assign(form, {
+    categoryId: null,
+    categoryName: null,
+    tenantId: null,
+    tenantName: null,
+    isSys: null,
+    parentId: null,
+    orderNum: null,
+    delFlag: null,
+    createBy: null,
+    createTime: null,
+    updateBy: null,
+    updateTime: null,
+    remark: null,
+  })
+  
+  // 修复:正确调用表单重置方法
+  if (formRef.value) {
+    formRef.value.resetFields()
+  }
+}
+
+// 查询操作
+const handleQuery = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+// 重置查询
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+// 处理选择变化
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map((item) => item.categoryId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+// 新增操作
+const handleAdd = () => {
+  reset()
+  open.value = true
+  title.value = t("product.category142342-6")
+}
+
+// 修改操作
+const handleUpdate = (row) => {
+  reset()
+  const categoryId = row.categoryId || ids.value
+
+  Object.assign(form, row)
+  open.value = true
+
+}
+
+// 提交表单
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      if (form.categoryId != null) {
+        updateCategory(form).then((response) => {
+          ElMessage.success('更新成功')
+          open.value = false
+          getList()
+        })
+      } else {
+        addCategory(form).then((response) => {
+          ElMessage.success('新增成功')
+          open.value = false
+          getList()
+        })
+      }
+    }
+  })
+}
+
+// 删除操作
+const handleDelete = (row) => {
+  const categoryIds = row.categoryId || ids.value
+  let msg = ''
+  ElMessageBox.confirm(t("product.category142342-8", [categoryIds]), '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning',
+  })
+    .then(() => {
+      return delCategory(categoryIds).then((response) => {
+        msg = response.msg
+      })
+    })
+    .then(() => {
+      getList()
+      ElMessage.success(msg)
+    })
+    .catch(() => {})
+}
+
+// 在挂载时获取数据
+onMounted(() => {
+  getList()
+  init()
+})
+</script>

+ 216 - 0
src/views/pms/video_center/device/alert-user.vue

@@ -0,0 +1,216 @@
+<template>
+    <div style="padding-left: 20px">
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAlertUser" v-hasPermi="['iot:device:alert:user:add']">{{ $t('add') }}</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList">{{ $t('refresh') }}</el-button>
+            </el-col>
+        </el-row>
+
+        <el-table :border="false" v-loading="loading" :data="deviceUserList" size="mini">
+            <el-table-column :label="$t('user.index.098976-30')" align="center" prop="userId" />
+            <el-table-column :label="$t('user.profile.index.894502-1')" align="center" prop="userName" />
+            <el-table-column :label="$t('user.index.098976-3')" align="center" prop="phoneNumber" />
+            <el-table-column :label="$t('opation')" align="center" width="350">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:device:alert:user:remove']">{{ $t('del') }}</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <!--选择告警用户对话框-->
+        <el-dialog :title="$t('alert-user.837395-0')" :visible.sync="open" width="800px">
+            <div style="margin-top: -55px">
+                <el-divider style="margin-top: -30px"></el-divider>
+                <el-form :model="permParams" ref="permForm" :inline="true" label-width="68px">
+                    <el-form-item :label="$t('user.profile.index.894502-1')" prop="userName">
+                        <el-input v-model="permParams.userName" :placeholder="$t('online.093480-2')" clearable size="small" @keyup.enter.native="handleUserQuery" />
+                    </el-form-item>
+                    <el-form-item :label="$t('user.index.098976-3')" prop="phonenumber">
+                        <el-input v-model="permParams.phonenumber" :placeholder="$t('user.index.098976-4')" clearable size="small" @keyup.enter.native="handleUserQuery" />
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleUserQuery">{{ $t('search') }}</el-button>
+                        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('reset') }}</el-button>
+                    </el-form-item>
+                </el-form>
+
+                <el-table :border="false" v-loading="loading" ref="singleTable" :data="UserList" highlight-current-row size="mini" @selection-change="changeCheckBoxValue" :row-key="getRowKeys">
+                    <el-table-column type="selection" width="55" align="center" :reserve-selection="true" />
+                    <el-table-column :label="$t('user.index.098976-30')" align="center" prop="userId" width="100" />
+                    <el-table-column :label="$t('user.profile.index.894502-1')" align="center" prop="userName" />
+                    <el-table-column :label="$t('user.index.098976-3')" align="center" prop="phonenumber" />
+                </el-table>
+
+                <pagination v-show="total > 0" :total="total" :page.sync="permParams.pageNum" :limit.sync="permParams.pageSize" @pagination="getUserList" />
+            </div>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm" v-hasPermi="['iot:device:user:edit']">{{ $t('confirm') }}</el-button>
+                <el-button @click="closeSelectUser">{{ $t('close') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { alertUserList, listUser, addAlertUser, delAlertUser } from '@/api/iot/alertUser';
+
+export default {
+    name: 'alert-user',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: {
+            handler(newVal) {
+                this.deviceInfo = newVal;
+                if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                    this.queryParams.deviceId = this.deviceInfo.deviceId;
+                    this.getList();
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            total: 0,
+            // 是否显示选择用户弹出层
+            open: false,
+            UserList: [],
+            // 查询参数
+            permParams: {
+                pageNum: 1,
+                pageSize: 10,
+                userName: undefined,
+                phonenumber: undefined,
+                deviceId: null,
+            },
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 设备用户表格数据
+            deviceUserList: [],
+            // 设备信息
+            deviceInfo: {},
+            //用户id
+            userIds: [],
+            //选中的数据
+            tableData: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 999,
+            },
+            // 表单参数
+            form: {},
+        };
+    },
+    created() {
+        this.queryParams.deviceId = this.device.deviceId;
+        this.getList();
+    },
+    methods: {
+        /** 查询设备用户列表 */
+        getList() {
+            this.loading = true;
+            listUser(this.queryParams).then((response) => {
+                this.deviceUserList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        getRowKeys(row) {
+            return row.userId;
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                deviceId: null,
+                userId: null,
+                userName: null,
+                phoneNumber: null,
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.getList();
+        },
+        /** 告警用户弹框搜索按钮操作 */
+        handleUserQuery() {
+            this.permParams.pageNum = 1;
+            this.getUserList();
+        },
+        //告警用户查询重置
+        resetQuery() {
+            this.resetForm('permForm');
+            this.handleUserQuery();
+        },
+        changeCheckBoxValue(selection) {
+            this.tableData = selection;
+        },
+        // 选择告警用户弹框
+        handleAlertUser() {
+            this.open = true;
+            this.getUserList();
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            this.$modal
+                .confirm(this.$t('alert-user.837395-1'))
+                .then(function () {
+                    return delAlertUser(row.deviceId, row.userId);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('delSuccess'));
+                })
+                .catch(() => {});
+        },
+        /** 查询用户 */
+        getUserList() {
+            alertUserList(this.permParams).then((response) => {
+                this.UserList = response.rows;
+                this.total = response.total;
+            });
+        },
+        // 重置查询
+        resetUserQuery() {
+            this.resetForm('queryForm');
+            this.reset();
+        },
+        // 关闭选择用户
+        closeSelectUser() {
+            this.open = false;
+            this.resetUserQuery();
+        },
+        /** 确定按钮 */
+        submitForm() {
+            this.userIds = this.tableData.map((item) => item.userId);
+            const useridStr = JSON.parse(JSON.stringify(this.userIds));
+            const params = {
+                userIdList: useridStr,
+                deviceId: this.device.deviceId,
+            };
+            addAlertUser(params).then((response) => {
+                if (response.code == 200) {
+                    this.$modal.msgSuccess(response.msg);
+                    this.resetUserQuery();
+                    this.open = false;
+                    this.getList();
+                    this.$refs.singleTable.clearSelection();
+                } else {
+                    this.$modal.msgError(response.msg);
+                }
+            });
+        },
+    },
+};
+</script>

+ 161 - 0
src/views/pms/video_center/device/allot-import-dialog.vue

@@ -0,0 +1,161 @@
+<template>
+  <!-- 导入分配设备弹窗 -->
+  <el-dialog :title="upload.title" :visible.sync="upload.importAllotDialog" width="550px" append-to-body>
+    <div style="margin-top: -55px">
+      <el-divider style="margin-top: 50px" />
+      <el-form label-position="top" :model="allotForm" ref="allotForm" :rules="allotRules">
+        <el-form-item label="产品" prop="productId">
+          <el-select v-model="allotForm.productId" placeholder="请选择产品" filterable style="width: 100%;">
+            <el-option v-for="item in productList" :key="item.value" :label="item.label"
+                       :value="item.value">
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="目标机构" prop="deptId">
+          <treeselect v-model="allotForm.deptId" :options="deptOptions" :show-count="true"
+                      placeholder="请选择目标机构" />
+        </el-form-item>
+        <el-form-item label="文件上传" prop="fileList">
+          <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers"
+                     :action="upload.url + '?productId=' + allotForm.productId + '&deptId=' + allotForm.deptId"
+                     :disabled="upload.isUploading" :on-progress="handleFileUploadProgress"
+                     :on-success="handleFileSuccess" :auto-upload="false" :on-change="handleChange"
+                     :on-remove="handleRemove" drag v-model="allotForm.fileList">
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">拖拽上传<em>点击上传</em></div>
+            <div class="el-upload__tip" slot="tip">
+              <div style="margin-top: -5px;">
+                <span>1.仅允许导入xls、xlsx格式文件。</span>
+                <div style="margin-top: -10px;">
+                  <span>2.单次最多分配1000个设备,单次设备较多时需要较长的校验、导入时间。</span>
+                </div>
+                <div style="margin-top: -10px;">
+                  <span>3.上传文件并分配后,可到设备列表-更多操作-设备导入记录中查看上传失败的设备详情信息。</span>
+                </div>
+              </div>
+            </div>
+          </el-upload>
+          <el-link :underline="false" style="font-size:14px;vertical-align: baseline;"
+                   type="primary" @click="importAllotTemplate"><i
+          class="el-icon-download"></i>设备分配模板</el-link>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button @click="upload.importAllotDialog = false">取消</el-button>
+      <el-button type="primary" @click="submitImportDevice">确认分配</el-button>
+    </div>
+  </el-dialog>
+</template>
+<script>
+import { listProduct } from "@/api/pms/video/product";
+import {getAccessToken} from '@/utils/auth'
+import { deptsTreeSelect } from "@/api/system/user";
+import Treeselect from "@riophae/vue-treeselect";
+import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+export default {
+  name: 'allotImport',
+  components: {
+    Treeselect
+  },
+  data() {
+    return {
+      type: 1,
+      //导入表单
+      allotForm: {
+        productId: 0,
+        deptId: 0,
+        fileList: [],
+      },
+      productList: [],
+      deptOptions: [],
+      // 分配导入参数
+      upload: {
+        // 弹出层标题
+        title: this.$t('device.allot-import-dialog.060657-13'),
+        importAllotDialog: false,
+        // 是否禁用上传
+        isUploading: false,
+        // 设置上传的请求头部
+        headers: {Authorization: "Bearer " + getAccessToken()},
+        // 上传的地址
+        url: process.env.process.env.VITE_BASE_URL + "/iot/device/importAssignmentData"
+      },
+      isSubDev: false,
+      //导入分配表单校验
+      allotRules: {
+        productId: [{ required: true, message: this.$t('device.allot-import-dialog.060657-14'), trigger: 'change' }],
+        deptId: [{ required: true, message: this.$t('device.allot-import-dialog.060657-15'), trigger: 'change' }],
+        fileList: [
+          { required: true, message: this.$t('plzUploadFile'), trigger: 'change' }
+        ]
+      },
+    };
+  },
+  created() {
+    this.getDeptTree();
+    this.getProductList();
+  },
+  methods: {
+    /** 查询机构下拉树结构 */
+    getDeptTree() {
+      deptsTreeSelect().then(response => {
+        this.deptOptions = response.data;
+      });
+    },
+    /** 下载分配导入模板操作 */
+    importAllotTemplate() {
+      this.type = 2;
+      this.download('/iot/device/uploadTemplate?type=' + this.type, {},
+          `allot_device_${new Date().getTime()}.xlsx`);
+    },
+    // 选择文件后给表单验证的prop字段赋值, 并且清除该字段的校验
+    handleChange(file, fileList) {
+      this.allotForm.fileList = fileList;
+      if (this.allotForm.fileList) {
+        this.$refs.allotForm.clearValidate('fileList');
+      }
+    },
+    // 删除文件后重新校验该字段
+    handleRemove(file, fileList) {
+      this.allotForm.fileList = fileList;
+      this.$refs.allotForm.validateField('fileList');
+    },
+    // 文件上传中处理
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true;
+    },
+    // 文件上传成功处理
+    handleFileSuccess(response, file, fileList) {
+      this.upload.open = false;
+      this.upload.isUploading = false;
+      this.$refs.upload.clearFiles();
+      this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", this.$t('device.allot-import-dialog.060657-17'), { dangerouslyUseHTMLString: true });
+    },
+    /** 查询产品列表 */
+    getProductList() {
+      this.loading = true;
+      const params = {
+        pageSize: 999,
+      }
+      listProduct(params).then(response => {
+        this.productList = response.rows.map((item) => {
+          return {value: item.productId, label: item.productName};
+        });
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    //分配设备导入设备提交按钮
+    submitImportDevice() {
+      this.$refs['allotForm'].validate((valid) => {
+        if (valid) {
+          this.$refs.upload.submit();
+          this.upload.importAllotDialog = false;
+        }
+      });
+    },
+
+  },
+};
+</script>

+ 162 - 0
src/views/pms/video_center/device/allot-record.vue

@@ -0,0 +1,162 @@
+<template>
+    <el-dialog :title="$t('device.allot-record.155854-0')" :visible.sync="open" width="70%">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+                <el-form-item prop="operateDeptId" style="width: 240px">
+                    <treeselect v-model="queryParams.operateDeptId" :options="deptOptions" :show-count="true" :placeholder="$t('device.allot-record.155854-1')" />
+                </el-form-item>
+                <el-form-item prop="productId">
+                    <el-select v-model="queryParams.productId" :placeholder="$t('device.allot-record.155854-2')" filterable style="width: 240px" clearable>
+                        <el-option v-for="item in productList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('search') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('reset') }}</el-button>
+                </el-form-item>
+            </el-form>
+
+            <el-table :border="false" v-loading="loading" ref="singleTable" :data="dataList" size="mini">
+                <el-table-column :label="$t('device.allot-record.155854-2')" align="left" prop="productName" />
+                <el-table-column align="left" prop="operateDeptName">
+                    <template slot="header">
+                        <span>{{ $t('device.allot-record.155854-1') }}</span>
+                        <el-tooltip class="item" effect="dark" :content="$t('device.allot-record.155854-5')" placement="top" style="margin-left: 10px">
+                            <i class="el-icon-warning-outline"></i>
+                        </el-tooltip>
+                    </template>
+                </el-table-column>
+                <el-table-column align="left" prop="targetDeptName">
+                    <template slot="header">
+                        <span>{{ $t('device.allot-import-dialog.060657-2') }}</span>
+                        <el-tooltip class="item" effect="dark" :content="$t('device.allot-record.155854-7')" placement="top" style="margin-left: 10px">
+                            <i class="el-icon-warning-outline"></i>
+                        </el-tooltip>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.allot-record.155854-8')" align="left" prop="total" width="100" />
+                <el-table-column :label="$t('device.allot-record.155854-9')" align="left" prop="successQuantity" width="100" />
+                <el-table-column :label="$t('device.allot-record.155854-10')" align="left" prop="failQuantity" width="100" />
+                <el-table-column :label="$t('device.allot-record.155854-11')" align="left" prop="status">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.common_status_type" :value="scope.row.status" size="small" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.allot-record.155854-12')" align="left" prop="distributeTypeDesc" />
+                <el-table-column :label="$t('device.allot-record.155854-13')" align="left" prop="createTime" width="200" />
+                <el-table-column :label="$t('opation')" align="center">
+                    <template slot-scope="scope">
+                        <el-button type="primary" size="small" style="padding: 5px" icon="el-icon-download" @click="handleDownLoad(scope.row)" v-hasPermi="['iot:device:record:export']">
+                            {{ $t('device.allot-record.155854-15') }}
+                        </el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listProduct } from '@/api/pms/video/product';
+import { listRecycleRecord } from '@/api/pms/video/device';
+import { deptsTreeSelect } from '@/api/system/user';
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+
+export default {
+    name: 'allotRecord',
+    dicts: ['common_status_type'],
+    components: {
+        Treeselect,
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 打开选择产品对话框
+            open: false,
+            // 产品列表
+            productList: [],
+            statusList: [],
+            dataList: [],
+            //时间范围
+            daterangeTime: [],
+            deptOptions: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                type: 3,
+                productName: null,
+                serialNumber: '',
+            },
+        };
+    },
+    created() {
+        this.getProductList();
+        this.getDeptTree();
+    },
+    methods: {
+        /** 查询产品列表 */
+        getProductList() {
+            this.loading = true;
+            const params = {
+                pageSize: 999,
+            };
+            listProduct(params).then((response) => {
+                this.productList = response.rows.map((item) => {
+                    return { value: item.productId, label: item.productName };
+                });
+                this.loading = false;
+            });
+        },
+        //查询分配记录列表
+        getList() {
+            this.loading = true;
+            listRecycleRecord(this.queryParams).then((response) => {
+                this.dataList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        //下载明细
+        handleDownLoad(row) {
+            const params = {
+                parentId: row.id,
+                type: 4,
+            };
+            this.download(
+                'iot/record/export',
+                {
+                    ...params,
+                },
+                `allot_${new Date().getTime()}.xlsx`
+            );
+        },
+        /** 查询机构下拉树结构 */
+        getDeptTree() {
+            deptsTreeSelect().then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /**关闭对话框 */
+        closeDialog() {
+            this.open = false;
+        },
+    },
+};
+</script>

+ 209 - 0
src/views/pms/video_center/device/batch-import-dialog.vue

@@ -0,0 +1,209 @@
+<template>
+  <!-- 批量导入设备 -->
+  <el-dialog v-model="upload.importDeviceDialog" :title="upload.title" width="550px" append-to-body>
+    <div style="margin-top: -55px">
+      <el-divider style="margin-top: 50px;width: 106%;margin-left: -2.9%;" />
+      <el-form label-position="top" :model="importForm" ref="importFormRef" :rules="importRules">
+        <el-form-item label="产品" prop="productId">
+          <el-select
+            v-model="importForm.productId"
+            placeholder="请选择产品"
+            style="width: 100%"
+            filterable
+          >
+            <el-option
+              v-for="item in productList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+
+        <el-form-item label="上传文件" prop="fileList">
+          <el-upload
+            style="width: 100%;"
+            ref="uploadRef"
+            :limit="1"
+            accept=".xlsx, .xls"
+            :headers="upload.headers"
+            v-model:file-list="importForm.fileList"
+            :action="upload.url + '?productId=' + importForm.productId"
+            :disabled="upload.isUploading"
+            :on-progress="handleFileUploadProgress"
+            :on-success="handleFileSuccess"
+            :auto-upload="false"
+            :on-change="handleChange"
+            :on-remove="handleRemove"
+            drag
+          >
+            <el-icon class="el-upload__icon"><upload-filled /></el-icon>
+            <div class="el-upload__text">将文件拖到此处,<em>点击上传</em></div>
+            <template #tip>
+              <div class="el-upload__tip">
+                <div style="margin-top: 10px">
+                  <span>提示:仅允许导入xls、xlsx格式文件。</span>
+                </div>
+              </div>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-link
+          type="primary"
+          :underline="false"
+          style="font-size: 14px; vertical-align: baseline"
+          @click="importTemplate"
+        >
+          <el-icon><Download /></el-icon>
+          下载设备导入模板
+        </el-link>
+      </el-form>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitFileForm">确定</el-button>
+        <el-button @click="upload.importDeviceDialog = false">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { UploadFilled, Download } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { listProduct } from '@/api/pms/video/product'
+import { getAccessToken } from '@/utils/auth'
+import { download } from '@/config/axios/service'
+
+// 国际化
+const { t } = useI18n()
+
+// 定义 emit
+const emit = defineEmits(['save'])
+
+// Refs
+const importFormRef = ref()
+const uploadRef = ref()
+
+// 数据响应式变量
+const type = ref(1)
+
+// 导入表单
+const importForm = reactive({
+  productId: null,
+  fileList: []
+})
+
+const productList = ref([])
+const file = ref(null)
+
+// 批量导入参数
+const upload = reactive({
+  // 是否显示弹出层
+  importDeviceDialog: false,
+  // 弹出层标题
+  title: '批量导入',
+  // 是否禁用上传
+  isUploading: false,
+  // 设置上传的请求头部
+  headers: { Authorization: 'Bearer ' + getAccessToken() },
+  // 上传的地址
+  url: import.meta.env.VITE_BASE_URL + '/iot/device/importData'
+})
+
+// 批量导入表单校验
+const importRules = {
+  productId: [{ required: true, message: '产品不能为空', trigger: 'change' }],
+  fileList: [{ required: true, message: '请上传文件', trigger: 'change' }]
+}
+
+// 生命周期钩子
+onMounted(() => {
+  getProductList()
+})
+
+// 方法定义
+/** 下载模板操作 */
+function importTemplate() {
+  download(
+    '/iot/device/uploadTemplate?type=' + type.value,
+    {},
+    `device_template_${new Date().getTime()}.xlsx`
+  )
+}
+
+// 选择文件后给表单验证的prop字段赋值, 并且清除该字段的校验
+function handleChange(file, fileList) {
+  importForm.fileList = fileList
+  // 防止用户打开了文件选择框之后不选择文件而出现效验失败
+  if (importForm.fileList) {
+    importFormRef.value?.clearValidate('fileList')
+  }
+}
+
+// 删除文件后重新校验该字段
+function handleRemove(file, fileList) {
+  importForm.fileList = fileList
+  importFormRef.value?.validateField('fileList')
+}
+
+// 文件上传中处理
+function handleFileUploadProgress(event, file, fileList) {
+  upload.isUploading = true
+}
+
+// 文件上传成功处理
+function handleFileSuccess(response, file, fileList) {
+  upload.importDeviceDialog = false
+  upload.isUploading = false
+  uploadRef.value?.clearFiles()
+  ElMessageBox.alert(
+    "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
+      response.msg +
+      '</div>',
+    '导入结果',
+    { dangerouslyUseHTMLString: true }
+  )
+  emit('save')
+}
+
+/** 查询产品列表 */
+function getProductList() {
+  const params = {
+    pageSize: 999
+  }
+  listProduct(params).then((response) => {
+    productList.value = response.rows.map((item) => {
+      return { value: item.productId, label: item.productName }
+    })
+  })
+}
+
+// 提交上传文件
+function submitFileForm() {
+  importFormRef.value?.validate((valid) => {
+    if (valid) {
+      uploadRef.value?.submit()
+      upload.importDeviceDialog = false
+    }
+  })
+}
+
+defineExpose({
+  showDialog: () => {
+    upload.importDeviceDialog = true
+  },
+  importForm
+})
+</script>
+
+<style scoped>
+.el-upload__icon {
+  font-size: 67px;
+  color: #c0c4cc;
+  margin: 40px 0 16px;
+  line-height: 50px;
+}
+</style>

+ 78 - 0
src/views/pms/video_center/device/device-abnormal.vue

@@ -0,0 +1,78 @@
+<template>
+    <div class="app-container">
+        <el-card style="margin-bottom: 6px">
+            <el-form v-show="showSearch" ref="queryForm" :inline="true" :model="queryParams" label-width="68px" style="margin-bottom: -20px">
+                <el-form-item label="设备名称" prop="deviceName">
+                    <el-input v-model="queryParams.deviceName" clearable placeholder="设备名称" size="small" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button icon="el-icon-search" size="mini" type="primary" @click="getList">搜索</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+        <el-card class="main-card">
+            <el-table :border="false" :data="tableData" border style="width: 100%">
+                <el-table-column align="center" label="设备名称" prop="deviceName" width="180px"></el-table-column>
+                <el-table-column align="center" label="设备编号" prop="serialNumber" width="180px"></el-table-column>
+                <el-table-column align="center" label="异常数据" prop="abnormalData"></el-table-column>
+                <el-table-column align="center" label="创建时间" width="200px">
+                    <template slot-scope="scope">
+                        <span>{{ parseTime(scope.row.createTime) }}</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+            <pagination v-show="total > 0" :limit.sync="queryParams.pageSize" :page.sync="queryParams.pageNum" :total="total" @pagination="getList" />
+        </el-card>
+    </div>
+</template>
+
+<script>
+import { getDeviceAbnormalList } from '@/api/iot/device';
+
+export default {
+    name: 'Index',
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                deviceName: null,
+            },
+            tableData: [],
+        };
+    },
+    created() {
+        this.getList();
+    },
+    methods: {
+        getList() {
+            getDeviceAbnormalList(this.queryParams).then((rp) => {
+                this.tableData = rp.rows;
+                this.total = rp.total;
+            });
+        },
+        resetQuery() {
+            this.queryParams.deviceName = null;
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+    },
+};
+</script>
+
+<style>
+</style>

+ 233 - 0
src/views/pms/video_center/device/device-alert.vue

@@ -0,0 +1,233 @@
+<template>
+    <div style="padding: 6px">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item :label="$t('device.device-alert.309509-0')" prop="alertName">
+                <el-input v-model="queryParams.alertName" :placeholder="$t('device.device-alert.309509-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('device.device-alert.309509-2')" prop="alertLevel">
+                <el-select v-model="queryParams.alertLevel" :placeholder="$t('device.device-alert.309509-3')" clearable size="small">
+                    <el-option v-for="dict in dict.type.iot_alert_level" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item :label="$t('device.device-alert.309509-4')" prop="status">
+                <el-select v-model="queryParams.status" :placeholder="$t('device.device-alert.309509-5')" clearable size="small">
+                    <el-option v-for="dict in dict.type.iot_process_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.device-alert.309509-6') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.device-alert.309509-7') }}</el-button>
+            </el-form-item>
+        </el-form>
+        <el-table :border="false" v-loading="loading" :data="alertLogList" @selection-change="handleSelectionChange" border>
+            <el-table-column :label="$t('device.device-alert.309509-0')" align="center" prop="alertName" />
+            <el-table-column :label="$t('device.device-alert.309509-2')" align="center" prop="alertLevel" width="170">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.iot_alert_level" :value="scope.row.alertLevel" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-alert.309509-8')" align="center" prop="createBy" width="170" />
+            <el-table-column :label="$t('device.device-alert.309509-9')" align="center" prop="createTime" width="170">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-alert.309509-10')" align="left" header-align="center" prop="detail">
+                <template slot-scope="scope">
+                    <div v-html="formatDetail(scope.row.detail)"></div>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-alert.309509-11')" align="center" prop="status">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.iot_process_status" :value="scope.row.status" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-alert.309509-12')" align="center" width="100">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['iot:alertLog:edit']">{{ $t('device.device-alert.309509-13') }}</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        <!-- 添加或修改设备告警对话框 -->
+        <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+            <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+                <el-form-item :label="$t('device.device-alert.309509-14')" prop="remark">
+                    <el-input v-model="form.remark" type="textarea" :placeholder="$t('device.device-alert.309509-15')" rows="8" />
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">{{ $t('device.device-alert.309509-16') }}</el-button>
+                <el-button @click="cancel">{{ $t('device.device-alert.309509-17') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { addAlertLog, getAlertLog, listAlertLog, updateAlertLog } from '@/api/iot/alertLog';
+
+export default {
+    name: 'DeviceAlertLog',
+    dicts: ['iot_alert_level', 'iot_process_status'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: function (newVal, oldVal) {
+            this.deviceInfo = newVal;
+            if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                this.queryParams.serialNumber = this.deviceInfo.serialNumber;
+                this.getList();
+            }
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 设备告警表格数据
+            alertLogList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                alertName: null,
+                alertLevel: null,
+                status: null,
+                productId: null,
+                productName: null,
+                serialNumber: null,
+                deviceName: null,
+            },
+            // 表单参数
+            form: {},
+            // 表单校验
+            rules: {
+                remark: [
+                    {
+                        required: true,
+                        message: this.$t('device.device-alert.309509-18'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+        };
+    },
+    created() {
+        this.queryParams.serialNumber = this.device.serialNumber;
+        this.getList();
+    },
+    methods: {
+        /** 查询设备告警列表 */
+        getList() {
+            this.loading = true;
+            listAlertLog(this.queryParams).then((response) => {
+                this.alertLogList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                alertLogId: null,
+                alertName: null,
+                alertLevel: null,
+                status: null,
+                productId: null,
+                productName: null,
+                deviceId: null,
+                deviceName: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.alertLogId);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            const alertLogId = row.alertLogId || this.ids;
+            getAlertLog(alertLogId).then((response) => {
+                this.form = response.data;
+                this.open = true;
+                this.title = this.$t('device.device-alert.309509-19');
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    if (this.form.alertLogId != null) {
+                        updateAlertLog(this.form).then((response) => {
+                            this.$modal.msgSuccess(this.$t('device.device-alert.309509-20'));
+                            this.open = false;
+                            this.getList();
+                        });
+                    } else {
+                        addAlertLog(this.form).then((response) => {
+                            this.$modal.msgSuccess(this.$t('device.device-alert.309509-21'));
+                            this.open = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+        /**格式化显示物模型**/
+        formatDetail(json) {
+            if (json == null || json == '') {
+                return;
+            }
+            let item = JSON.parse(json);
+            let result = 'id:<span style="color:#F56C6C">' + item.id + '</span><br />';
+            result = result + 'value:<span style="color:#F56C6C">' + item.value + '</span><br />';
+            result = result + 'remark:<span style="color:#F56C6C">' + item.remark + '</span>';
+            return result;
+        },
+    },
+};
+</script>

+ 1505 - 0
src/views/pms/video_center/device/device-edit.vue

@@ -0,0 +1,1505 @@
+<template>
+  <div class="iot-device-edit">
+    <el-card class="main-card">
+      <el-tabs
+        id="deviceDetailTab"
+        v-model="activeName"
+        tab-position="left"
+        @tab-click="tabChange"
+        style="padding: 10px; min-height: 400px"
+        lazy
+      >
+        <el-tab-pane name="basic">
+          <span slot="label">{{ t('device.device-edit148398-0') }}</span>
+          <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+            <el-row :gutter="100">
+              <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                <el-form-item :label="t('device.device-edit148398-1')" prop="deviceName">
+                  <el-input
+                    v-model="form.deviceName"
+                    :placeholder="t('device.device-edit148398-2')"
+                  >
+                    <el-button slot="append" @click="openSummaryDialog" v-if="form.deviceId != 0">{{
+                      t('device.device-edit148398-3')
+                    }}</el-button>
+                  </el-input>
+                </el-form-item>
+                <el-form-item label="" prop="productName">
+                  <template slot="label">
+                    <span style="color: red">*</span>
+                    {{ t('device.device-edit148398-4') }}
+                  </template>
+                  <el-input
+                    readonly
+                    v-model="form.productName"
+                    :placeholder="t('device.device-edit148398-5')"
+                    :disabled="form.status != 1"
+                  >
+                    <el-button
+                      slot="append"
+                      @click="selectProduct()"
+                      :disabled="form.status != 1"
+                      >{{ t('device.device-edit148398-6') }}</el-button
+                    >
+                  </el-input>
+                </el-form-item>
+                <el-form-item label="" prop="serialNumber">
+                  <template slot="label">
+                    <span style="color: red">*</span>
+                    {{ t('device.device-edit148398-7') }}
+                  </template>
+                  <el-input
+                    v-model="form.serialNumber"
+                    :placeholder="t('device.device-edit148398-8')"
+                    :disabled="form.status != 1"
+                    maxlength="32"
+                  >
+                    <el-button
+                      v-if="form.deviceType !== 3"
+                      slot="append"
+                      @click="generateNum"
+                      :loading="genDisabled"
+                      :disabled="form.status != 1"
+                      v-hasPermi="['iot:device:add']"
+                    >
+                      {{ t('device.device-edit148398-9') }}
+                    </el-button>
+                    <el-button
+                      v-if="form.deviceType === 3"
+                      slot="append"
+                      @click="genSipID()"
+                      :disabled="form.status != 1"
+                      v-hasPermi="['iot:device:add']"
+                    >
+                      {{ t('device.device-edit148398-9') }}
+                    </el-button>
+                  </el-input>
+                </el-form-item>
+                <el-form-item v-if="openServerTip">
+                  <template>
+                    <el-alert
+                      type="info"
+                      show-icon
+                      :description="t('device.device-edit148398-10')"
+                    ></el-alert>
+                  </template>
+                </el-form-item>
+                <el-form-item v-if="openTip">
+                  <template>
+                    <el-alert
+                      type="success"
+                      show-icon
+                      :description="t('device.device-edit148398-11')"
+                    ></el-alert>
+                  </template>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-12')" prop="firmwareVersion">
+                  <el-input
+                    v-model="form.firmwareVersion"
+                    :placeholder="t('device.device-edit148398-13')"
+                    type="number"
+                    step="0.1"
+                    :disabled="form.status != 1 || form.deviceType === 3"
+                  >
+                    <template slot="prepend">Version</template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-15')" prop="isShadow">
+                  <el-switch
+                    v-model="form.isShadow"
+                    active-text=""
+                    inactive-text=""
+                    :active-value="1"
+                    :inactive-value="0"
+                    :disabled="form.deviceType === 3"
+                  ></el-switch>
+                </el-form-item>
+
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('device.device-edit148398-16')"
+                  prop="deviceStatus"
+                >
+                  <el-switch
+                    v-model="deviceStatus"
+                    active-text=""
+                    inactive-text=""
+                    :disabled="form.status == 1 || form.deviceType === 3"
+                    :active-value="1"
+                    :inactive-value="0"
+                    active-color="#F56C6C"
+                  ></el-switch>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-17')" prop="remark">
+                  <el-input
+                    v-model="form.remark"
+                    type="textarea"
+                    :placeholder="t('device.device-edit148398-18')"
+                    rows="1"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                <el-form-item :label="t('device.device-edit148398-19')" prop="locationWay">
+                  <el-select
+                    v-model="form.locationWay"
+                    :placeholder="t('device.device-edit148398-20')"
+                    clearable
+                    size="small"
+                    style="width: 100%"
+                    :disabled="form.deviceType === 3"
+                  >
+                    <el-option
+                      v-for="dict in dict.type.iot_location_way"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="Number(dict.value)"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-21')" prop="longitude">
+                  <el-input
+                    v-model="form.longitude"
+                    :placeholder="t('device.device-edit148398-22')"
+                    type="number"
+                    :disabled="form.locationWay != 3"
+                  >
+                    <el-link
+                      slot="append"
+                      :underline="false"
+                      href="https://api.map.baidu.com/lbsapi/getpoint/index.html"
+                      target="_blank"
+                      :disabled="form.locationWay != 3"
+                    >
+                      {{ t('device.device-edit148398-23') }}
+                    </el-link>
+                  </el-input>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-24')" prop="latitude">
+                  <el-input
+                    v-model="form.latitude"
+                    :placeholder="t('device.device-edit148398-25')"
+                    type="number"
+                    :disabled="form.locationWay != 3"
+                  >
+                    <el-link
+                      slot="append"
+                      :underline="false"
+                      href="https://api.map.baidu.com/lbsapi/getpoint/index.html"
+                      target="_blank"
+                      :disabled="form.locationWay != 3"
+                    >
+                      {{ t('device.device-edit148398-23') }}
+                    </el-link>
+                  </el-input>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-26')" prop="networkAddress">
+                  <el-input
+                    v-model="form.networkAddress"
+                    :placeholder="t('device.device-edit148398-27')"
+                    :disabled="form.locationWay != 3"
+                  />
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-28')" prop="networkIp">
+                  <el-input
+                    v-model="form.networkIp"
+                    :placeholder="t('device.device-edit148398-29')"
+                    disabled
+                  />
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-30')" prop="activeTime">
+                  <el-date-picker
+                    clearable
+                    v-model="form.activeTime"
+                    type="date"
+                    value-format="yyyy-MM-dd"
+                    :placeholder="t('device.device-edit148398-31')"
+                    disabled
+                    style="width: 100%"
+                  ></el-date-picker>
+                </el-form-item>
+                <el-form-item :label="t('device.device-edit148398-32')" prop="rssi">
+                  <el-input
+                    v-model="form.rssi"
+                    :placeholder="t('device.device-edit148398-33')"
+                    disabled
+                  />
+                </el-form-item>
+                <el-form-item
+                  :label="t('device.device-edit148398-34')"
+                  prop="remark"
+                  v-if="form.deviceId != 0"
+                >
+                  <dict-tag
+                    :options="dict.type.iot_device_status"
+                    :value="form.status"
+                    style="display: inline-block; margin-right: 8px"
+                  />
+                  <el-button size="small" @click="handleViewMqtt()">{{
+                    t('device.device-edit148398-35')
+                  }}</el-button>
+                  <el-button size="small" @click="openCodeDialog()">{{
+                    t('device.device-edit148398-36')
+                  }}</el-button>
+                </el-form-item>
+              </el-col>
+              <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8" v-if="form.deviceId != 0">
+                <div
+                  style="
+                    border: 1px solid #dfe4ed;
+                    border-radius: 5px;
+                    padding: 5px;
+                    text-align: center;
+                    line-height: 400px;
+                  "
+                >
+                  <div id="map" style="height: 435px; width: 100%">{{
+                    t('device.device-edit148398-37')
+                  }}</div>
+                </div>
+              </el-col>
+            </el-row>
+          </el-form>
+
+          <el-form label-width="100px" style="margin-top: 50px">
+            <el-form-item style="text-align: center; margin-left: -100px; margin-top: 10px">
+              <el-button
+                type="primary"
+                @click="submitForm"
+                v-hasPermi="['iot:device:edit']"
+                v-show="form.deviceId != 0"
+              >
+                {{ t('device.device-edit148398-38') }} {{ t('device.device-edit148398-39') }}
+              </el-button>
+              <el-button
+                type="primary"
+                @click="submitForm"
+                v-hasPermi="['iot:device:add']"
+                v-show="form.deviceId == 0"
+              >
+                {{ t('device.device-edit148398-40') }} {{ t('device.device-edit148398-41') }}
+              </el-button>
+            </el-form-item>
+          </el-form>
+
+          <!-- 选择产品 -->
+          <product-list
+            ref="productList"
+            :productId="form.productId"
+            @productEvent="getProductData($event)"
+          />
+
+          <sipid ref="sipidGen" :product="form" @addGenEvent="getSipIDData($event)" />
+        </el-tab-pane>
+
+        <el-tab-pane name="runningStatus" v-if="form.deviceType !== 3">
+          <span slot="label">{{ t('device.device-edit148398-42') }}</span>
+          <!-- <real-time-status ref="realTimeStatus" :device="form" @statusEvent="getDeviceStatusData($event)" /> -->
+          <running-status
+            ref="runningStatus"
+            v-if="!isSubDev"
+            :device="form"
+            @statusEvent="getDeviceStatusData($event)"
+          />
+          <real-time-status
+            ref="realTimeStatus"
+            v-else
+            :device="form"
+            @statusEvent="getDeviceStatusData($event)"
+          />
+        </el-tab-pane>
+      
+
+        <el-tab-pane
+          name="sipChannel"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType === 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-44') }}</span>
+          <channel ref="Channel" :device="form" @playerEvent="getPlayerData($event)" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="sipPlayer"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType === 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-45') }}</span>
+          <device-live-stream ref="deviceLiveStream" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="sipVideo"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType === 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-46') }}</span>
+          <deviceVideo ref="deviceVideo" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane name="ossRecord" :disabled="form.deviceId == 0" v-if="form.deviceType === 3">
+          <span slot="label">{{ t('device.device-edit148398-79') }}</span>
+          <OssRecord ref="OssRecord" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceTimer"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-47') }}</span>
+          <device-timer ref="deviceTimer" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane name="deviceUser" :disabled="form.deviceId == 0" lazy>
+          <span slot="label">{{ t('device.device-edit148398-48') }}</span>
+          <device-user ref="deviceUser" :device="form" @userEvent="getUserData($event)" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceLog"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-49') }}</span>
+          <device-log ref="deviceLog" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane name="alertUser" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
+          <span slot="label">{{ t('device.device-edit148398-80') }}</span>
+          <alert-user ref="alertUser" :device="form" />
+        </el-tab-pane>
+        <el-tab-pane name="deviceAlert" :disabled="form.deviceId == 0">
+          <span slot="label">{{ t('device.device-edit148398-81') }}</span>
+          <device-alert ref="deviceAlert" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceFuncLog"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-50') }}</span>
+          <device-func ref="deviceFuncLog" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceMonitor"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+        >
+          <span slot="label">{{ t('device.device-edit148398-51') }}</span>
+          <device-monitor ref="deviceMonitor" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceStastic"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+        >
+          <span slot="label">{{ t('device.device-edit148398-52') }}</span>
+          <device-statistic ref="deviceStatistic" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="deviceModbusTask"
+          :disabled="form.deviceId == 0"
+          v-if="form.transport == 'TCP'"
+        >
+          <span slot="label">{{ t('device.device-edit148398-77') }}</span>
+          <device-modbus-task ref="deviceModbusTask" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="instructionParsing"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-76') }}</span>
+          <instruction-parsing ref="instructionParsing" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="scada"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3 && isShowScada == true"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-73') }}</span>
+          <device-scada ref="deviceScada" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="variable"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-74') }}</span>
+          <device-variable ref="deviceVariable" :device="form" />
+        </el-tab-pane>
+
+        <el-tab-pane
+          name="inlineVideo"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType == 3"
+          lazy
+        >
+          <span slot="label">{{ t('device.device-edit148398-75') }}</span>
+          <device-inline-video ref="deviceInlineVideo" :sipRelationList="form.sipRelationList" />
+        </el-tab-pane>
+
+        <!-- 用于设置间距 -->
+        <el-tab-pane disabled>
+          <span slot="label">
+            <div style="margin-top: 150px"></div>
+          </span>
+        </el-tab-pane>
+
+   
+        <el-tab-pane name="deviceReturn" disabled>
+          <span slot="label">
+            <el-button type="info" size="mini" @click="goBack()">{{
+              t('device.device-edit148398-53')
+            }}</el-button>
+          </span>
+        </el-tab-pane>
+      </el-tabs>
+
+      <!-- 设备配置JSON -->
+      <el-dialog
+        :title="t('device.device-edit148398-54')"
+        :visible.sync="openSummary"
+        width="700px"
+        append-to-body
+      >
+        <el-row :gutter="20">
+          <el-col :span="14">
+            <div
+              style="
+                border: 1px solid #ccc;
+                margin-top: -15px;
+                height: 350px;
+                width: 360px;
+                overflow: scroll;
+              "
+            >
+              <json-viewer :value="summary" :expand-depth="10" copyable>
+                <template v-slot:copy>{{ t('device.device-edit148398-55') }}</template>
+              </json-viewer>
+            </div>
+          </el-col>
+          <el-col :span="10">
+            <div
+              style="
+                border: 1px solid #ccc;
+                width: 200px;
+                text-align: center;
+                margin-left: 20px;
+                margin-top: -10px;
+              "
+            >
+              <vue-qr :text="qrText" :size="200"></vue-qr>
+              <div style="padding-bottom: 10px">{{ t('device.device-edit148398-56') }}</div>
+            </div>
+          </el-col>
+        </el-row>
+        <div slot="footer" class="dialog-footer">
+          <el-button type="info" @click="closeSummaryDialog">{{
+            t('device.device-edit148398-57')
+          }}</el-button>
+        </div>
+      </el-dialog>
+     
+      <el-dialog :visible.sync="openCode" width="300px" append-to-body>
+        <div
+          style="
+            border: 1px solid #ccc;
+            width: 220px;
+            text-align: center;
+            margin: 0 auto;
+            margin-top: -15px;
+          "
+        >
+          <vue-qr :text="qrText" :size="200"></vue-qr>
+          <div style="padding-bottom: 10px">{{ t('device.device-edit148398-56') }}</div>
+        </div>
+      </el-dialog>
+      <el-dialog
+        :title="t('device.device-edit148398-58')"
+        :visible.sync="openViewMqtt"
+        width="600px"
+        append-to-body
+      >
+        <el-form ref="listQuery" :model="listQuery" :rules="rules" label-width="150px">
+          <el-form-item label="clientId" prop="clientId">
+            <el-input
+              v-model="listQuery.clientId"
+              :disabled="listQuery.mqttstats === 0"
+              style="width: 80%"
+            />
+          </el-form-item>
+          <el-form-item label="username" prop="username">
+            <el-input v-model="listQuery.username" disabled style="width: 80%" />
+          </el-form-item>
+          <el-form-item label="passwd" prop="passwd">
+            <el-input clearable v-model="listQuery.passwd" disabled style="width: 80%"></el-input>
+          </el-form-item>
+          <el-form-item label="port" prop="port">
+            <el-input clearable v-model="listQuery.port" disabled style="width: 80%"></el-input>
+          </el-form-item>
+          <el-form-item label="发布" prop="port">
+            <el-input
+              v-model="fb"
+              :disabled="listQuery.mqttstats === 0"
+              clearable
+              style="width: 80%"
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="订阅" prop="port">
+            <el-input
+              v-model="dy"
+              :disabled="listQuery.mqttstats === 0"
+              clearable
+              style="width: 80%"
+            ></el-input>
+          </el-form-item>
+          <el-form-item label="是否自定义">
+            <el-radio-group v-model="listQuery.mqttstats" @change="updateMqttgroup">
+              <el-radio :label="0">初始化</el-radio>
+              <el-radio :label="1">自定义</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button
+            v-if="listQuery.mqttstats === 1"
+            class="btns"
+            type="primary"
+            @click="updateMqtt()"
+            >修改</el-button
+          >
+          <el-button class="btns" type="primary" @click="doCopy(2)">{{
+            t('device.device-edit148398-59')
+          }}</el-button>
+          <el-button @click="closeSummaryDialog">{{
+            t('device.device-edit148398-57')
+          }}</el-button>
+        </div>
+      </el-dialog>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import JsonViewer from 'vue-json-viewer'
+import 'vue-json-viewer/style.css'
+import productList from './product-list'
+import deviceLog from './device-log'
+import deviceAlert from './device-alert'
+import alertUser from './alert-user'
+import deviceUser from './device-user'
+import runningStatus from './running-status'
+import deviceMonitor from './device-monitor'
+import deviceStatistic from './device-statistic'
+import instructionParsing from './instruction-parsing'
+import deviceModbusTask from './device-modbus-task'
+import deviceTimer from './device-timer'
+import channel from '../sip/channel'
+import player from '@/views/components/player/player.vue'
+import deviceVideo from '@/views/components/player/deviceVideo.vue'
+import OssRecord from '@/views/iot/record/record-oss.vue'
+import deviceLiveStream from '@/views/components/player/deviceLiveStream'
+import sipid from '../sip/sipidGen.vue'
+import deviceScada from './device-scada'
+import deviceVariable from './device-variable'
+import deviceInlineVideo from './device-inline-video'
+import deviceFuncLog from './device-functionlog'
+import deviceSub from './device-sub'
+import vueQr from 'vue-qr'
+import { loadBMap } from '@/utils/map.js'
+import {
+  addDevice,
+  deviceSynchronization,
+  generatorDeviceNum,
+  getDevice,
+  getDeviceRunningStatus,
+  getMqttConnect,
+  updateDevice,
+  updateMqttConnectData
+} from '@/api/iot/device'
+import { getDeviceUser } from '@/api/iot/deviceuser'
+import { getUserId } from '@/utils/auth'
+import { cacheJsonThingsModel } from '@/api/iot/model'
+import DeviceFunc from '@/views/iot/device/device-functionlog'
+import RealTimeStatus from '@/views/iot/device/realTime-status'
+import { clientOut } from '@/api/iot/netty'
+import defaultSettings from '@/settings'
+import { use } from 'echarts'
+
+const {t} = useI18n()
+
+export default {
+  name: 'DeviceEdit',
+  dicts: ['iot_device_status', 'iot_location_way'],
+  components: {
+    RealTimeStatus,
+    DeviceFunc,
+    deviceLog,
+    deviceAlert,
+    deviceUser,
+    alertUser,
+    deviceMonitor,
+    deviceStatistic,
+    runningStatus,
+    productList,
+    deviceTimer,
+    deviceFuncLog,
+    deviceVideo,
+    OssRecord,
+    player,
+    deviceLiveStream,
+    deviceSub,
+    JsonViewer,
+    vueQr,
+    channel,
+    sipid,
+    deviceScada,
+    deviceVariable,
+    instructionParsing,
+    deviceModbusTask,
+    deviceInlineVideo
+  },
+  watch: {
+    activeName(val) {
+      if (val == 'deviceStastic') {
+        this.$nextTick(() => {
+          // TODO 重置统计表格的尺寸
+        })
+      }
+    }
+  },
+  computed: {
+    deviceStatus: {
+      set(val) {
+        if (val == 1) {
+          // 1-未激活,2-禁用,3-在线,4-离线
+          this.form.status = 2
+        } else if (val == 0) {
+          this.form.status = 4
+        } else {
+          this.form.status = this.oldDeviceStatus
+        }
+      },
+      get() {
+        if (this.form.status == 2) {
+          return 1
+        }
+        return 0
+      }
+    }
+  },
+  data() {
+    return {
+      // 二维码内容
+      qrText: 'yanfan',
+      // 打开设备配置对话框
+      openSummary: false,
+      //二维码
+      openCode: false,
+      openViewMqtt: false,
+      // 生成设备编码是否禁用
+      genDisabled: false,
+      // 选中选项卡
+      activeName: 'basic',
+      //查看mqtt参数
+      mqttList: [],
+      // 遮罩层
+      loading: true,
+      // 设备开始状态
+      oldDeviceStatus: null,
+      deviceId: '',
+      channelId: '',
+      // 表单参数
+      form: {
+        productId: 0,
+        status: 1,
+        locationWay: 1,
+        firmwareVersion: 1.0,
+        serialNumber: '',
+        deviceType: 1,
+        isSimulate: 0
+      },
+      //mqtt参数查看
+      listQuery: {
+        clientId: 0,
+        username: '',
+        passwd: '',
+        port: '',
+        mqttstats: 0
+      },
+      fb: null,
+      dy: null,
+
+      openTip: false,
+      openServerTip: false,
+      serverType: 1,
+      //用于判断是否是设备组(modbus)
+      isSubDev: false,
+      // 设备摘要
+      summary: [],
+      // 地址
+      baseUrl: process.env.VUE_APP_BASE_API,
+      // 地图相关
+      map: null,
+      mk: null,
+      latitude: '',
+      longitude: '',
+      //组态相关按钮是否显示,true显示,false不显示
+      isShowScada: defaultSettings.isShowScada,
+      // 表单校验
+      rules: {
+        deviceName: [
+          {
+            required: true,
+            message: this.t('device.device-edit148398-60'),
+            trigger: 'blur'
+          },
+          {
+            min: 2,
+            max: 32,
+            message: this.t('device.device-edit148398-61'),
+            trigger: 'blur'
+          }
+        ],
+        firmwareVersion: [
+          {
+            required: true,
+            message: this.t('device.device-edit148398-62'),
+            trigger: 'blur'
+          }
+        ]
+      },
+      isMediaDevice: false
+    }
+  },
+  created() {
+    let activeName = this.$route.query.activeName
+    if (activeName != null && activeName != '') {
+      this.activeName = activeName
+    }
+    // 获取设备信息
+    this.form.deviceId = this.$route.query && this.$route.query.deviceId
+    if (this.form.deviceId != 0) {
+      // this.connectMqtt();
+      this.getDevice(this.form.deviceId)
+    }
+    this.isSubDev = this.$route.query.isSubDev == 1 ? true : false
+    console.log('this.isSubDev', this.isSubDev)
+  },
+  activated() {
+    // 跳转选项卡
+    let activeName = this.$route.query.activeName
+    if (activeName != null && activeName != '') {
+      this.activeName = activeName
+    }
+  },
+  destroyed() {
+    // 取消订阅主题
+    this.mqttUnSubscribe(this.form)
+  },
+  methods: {
+    updateMqttgroup(stater) {
+      if (stater === 1) {
+        this.fb = null
+        this.dy = null
+        this.listQuery.clientId = null
+      }
+      this.updateMqtt()
+    },
+    updateMqtt() {
+      this.listQuery.fpname = this.fb
+      this.listQuery.dyname = this.dy
+      console.log(this.listQuery)
+      updateMqttConnectData(this.listQuery).then((permResponse) => {
+        this.$message.success(permResponse.msg)
+        this.handleViewMqtt()
+      })
+    },
+    /* 连接Mqtt消息服务器 */
+    async connectMqtt() {
+      if (this.$mqttTool.client == null) {
+        await this.$mqttTool.connect(this.vuex_token)
+      }
+      // 删除所有message事件监听器
+      this.$mqttTool.client.removeAllListeners('message')
+      // this.$mqttTool.client.on('connect', (e) => {});
+      this.mqttUnSubscribe(this.form)
+      this.mqttSubscribe(this.form)
+      // 添加message事件监听器
+      this.mqttCallback()
+      if (this.form.deviceType !== 3 && !this.isSubDev) {
+        this.$refs.runningStatus.mqttCallback()
+      }
+    },
+    /* Mqtt回调处理  */
+    mqttCallback() {
+      this.$mqttTool.client.on('message', (topic, message, buffer) => {
+        let topics = topic.split('/')
+        let productId = topics[1]
+        let deviceNum = topics[2]
+        if (message instanceof Uint8Array) {
+          // 创建TextDecoder对象来转换Uint8Array到字符串
+          const decoder = new TextDecoder('utf-8')
+          const str = decoder.decode(message)
+          message = str //转换后的字符串
+        }
+        console.log('🚀 ~ this.$mqttTool.client.on ~ message:', message)
+        console.log('🚀 ~ this.$mqttTool.client.on ~ topics:', topic)
+        message = JSON.parse(message)
+        if (!message) {
+          return
+        }
+        if (topics[3] == 'status' || topics[2] == 'status') {
+          console.log('接收到【设备状态-详情】主题:', topic)
+          console.log('接收到【设备状态-详情】内容:', message)
+          // 更新列表中设备的状态
+          if (this.form.serialNumber == deviceNum) {
+            this.oldDeviceStatus = message.status
+            this.form.status = message.status
+            this.form.isShadow = message.isShadow
+            this.form.rssid = message.rssid
+          }
+        }
+
+        if (topic.endsWith('ws/service')) {
+          this.$busEvent.$emit('updateData', {
+            serialNumber: topics[2],
+            productId: this.form.productId,
+            data: message
+          })
+        }
+        if (topic.endsWith('service/reply')) {
+          this.$busEvent.$emit('updateLog', {
+            serialNumber: topics[2],
+            productId: this.form.productId,
+            data: message
+          })
+        }
+        /**mqtt测试 */
+        if (topic.endsWith('message/post')) {
+          this.$busEvent.$emit('updateMqttMessage', {
+            serialNumber: topics[2],
+            data: message
+          })
+        }
+      })
+    },
+
+    /** Mqtt topics array */
+    getMqttTopics(device) {
+      // 订阅当前设备状态和实时监测
+      const topicService = '/ws/service'
+      const topicStatus = '/status/post'
+      const topicFunction = '/function/post'
+      const topicMonitor = '/monitor/post'
+      const topicReply = '/service/reply'
+      //订阅mqtt测试
+      let messagePost = '/message/post'
+      const topics = [
+        topicService,
+        topicStatus,
+        topicFunction,
+        topicMonitor,
+        topicReply,
+        messagePost
+      ]
+
+      return topics.map((topic) => {
+        return `/${device.productId}/${device.serialNumber}${topic}`
+      })
+    },
+    // 获取子组件订阅的设备状态
+    getDeviceStatusData(status) {
+      this.form.status = status
+    },
+
+    /** Mqtt取消订阅主题 */
+    mqttUnSubscribe(device) {
+      const topics = this.getMqttTopics(device)
+      this.$mqttTool.unsubscribe(topics)
+    },
+
+    /** Mqtt订阅主题 */
+    mqttSubscribe(device) {
+      const topics = this.getMqttTopics(device)
+      this.$mqttTool.subscribe(topics)
+    },
+    // 获取直播子组件传递的激活选项卡名称
+    getPlayerData(data) {
+      this.activeName = data.tabName
+      this.channelId = data.channelId
+      // this.$set(this.form, 'channelId', this.channelId);
+      this.$nextTick(() => {
+        if (this.channelId) {
+          this.$refs.deviceLiveStream.channelId = this.channelId
+          this.$refs.deviceLiveStream.changeChannel()
+        }
+      })
+    },
+    /** 选项卡改变事件*/
+    tabChange(panel) {
+      this.$nextTick(() => {
+        if (this.form.deviceType == 3 && panel.name != 'deviceReturn') {
+          if (panel.name === 'sipPlayer') {
+            if (this.$refs.deviceVideo && this.$refs.deviceVideo.destroy) {
+              this.$refs.deviceVideo.destroy()
+            }
+            if (this.channelId) {
+              if (
+                this.$refs.deviceLiveStream &&
+                this.$refs.deviceLiveStream.channelId !== undefined
+              ) {
+                this.$refs.deviceLiveStream.channelId = this.channelId
+              }
+              this.$refs.deviceLiveStream.changeChannel()
+            }
+            if (this.$refs.deviceLiveStream.channelId) {
+              this.$refs.deviceLiveStream.changeChannel()
+            }
+          } else if (panel.name === 'sipVideo') {
+            if (this.$refs.deviceLiveStream && this.$refs.deviceLiveStream.destroy) {
+              this.$refs.deviceLiveStream.destroy()
+            }
+            if (
+              this.$refs.deviceVideo &&
+              this.$refs.deviceVideo.channelId !== undefined &&
+              this.$refs.deviceVideo.queryDate
+            ) {
+              this.$refs.deviceVideo.loadDevRecord()
+            }
+          } else if (panel.name === 'sipChannel') {
+            this.$nextTick(() => {
+              this.$refs.Channel.getList()
+            })
+          }
+          //关闭直播流
+          if (
+            panel.name !== 'sipPlayer' &&
+            this.$refs.deviceLiveStream &&
+            this.$refs.deviceLiveStream.playing
+          ) {
+            this.$refs.deviceLiveStream.closeDestroy(false)
+          }
+          //关闭录像流
+          if (
+            panel.name !== 'sipVideo' &&
+            this.$refs.deviceVideo &&
+            this.$refs.deviceVideo.playing
+          ) {
+            this.$refs.deviceVideo.closeDestroy()
+          }
+        }
+      })
+
+      this.$nextTick(() => {
+        // 获取监测统计数据
+        if (panel.name === 'deviceStastic') {
+          this.$refs.deviceStatistic.getListHistory()
+        } else if (panel.name === 'deviceTimer') {
+          this.$refs.deviceTimer.getList()
+        } else if (panel.name === 'deviceSub') {
+          if (this.form.serialNumber) {
+            this.$refs.deviceSub.queryParams.gwDeviceId = this.form.deviceId
+            this.$refs.deviceSub.gateway.gwDeviceId = this.form.deviceId
+            this.$refs.deviceSub.getList()
+          }
+        }
+      })
+      if (this.form.deviceType !== 3) {
+        // 用于关闭视频推流(页面切换时候需要关闭推流)
+        if (panel.name !== 'inlineVideo') {
+          this.$refs.deviceInlineVideo && this.$refs.deviceInlineVideo.handleClose()
+        }
+        if (panel.name !== 'scada') {
+          const scadaRef = this.$refs.deviceScada || {}
+          if (scadaRef && scadaRef.$refs && scadaRef.$refs.deviceScada) {
+            const copmRef = scadaRef.$refs.deviceScada
+            if (copmRef.$refs && copmRef.$refs.spirit) {
+              copmRef.$refs.spirit.forEach((item) => {
+                if (item.$vnode.tag.includes('ViewInlineVideo')) {
+                  item.handleCloseJessibuca()
+                }
+              })
+            }
+          }
+        }
+      }
+    },
+    /** 数据同步*/
+    deviceSynchronization() {
+      deviceSynchronization(this.form.serialNumber).then(async (response) => {
+        // 获取缓存物模型
+        response.data.cacheThingsModel = await this.getCacheThingsModdel(response.data.productId)
+        // 获取设备运行状态
+        response.data.thingsModels = await this.getDeviceStatus(this.form)
+        // 格式化物模型,拆分出监测值,数组添加前缀
+        this.formatThingsModel(response.data)
+        this.form = response.data
+        // 选项卡切换
+        this.activeName = 'runningStatus'
+        this.oldDeviceStatus = this.form.status
+        this.loadMap()
+      })
+    },
+    /**获取设备详情*/
+    getDevice(deviceId) {
+      getDevice(deviceId).then(async (response) => {
+        // 分享设备获取用户权限
+        response.data.userPerms = []
+        if (response.data.isOwner == 0) {
+          getDeviceUser(deviceId, getUserId()).then((permResponse) => {
+            if (permResponse.data) {
+              response.data.userPerms = permResponse.data.perms.split(',')
+            }
+            // 获取设备状态和物模型
+            this.getDeviceStatusWitchThingsModel(response)
+          })
+        } else {
+          // 获取设备状态和物模型
+          this.getDeviceStatusWitchThingsModel(response)
+        }
+      })
+    },
+    /**用户是否拥有分享设备权限*/
+    // hasShrarePerm(permission) {
+    //     if (this.form.isOwner == 0) {
+    //         // 分享设备权限
+    //         if (this.form.userPerms.indexOf(permission) == -1) {
+    //             return false;
+    //         }
+    //     }
+    //     return true;
+    // },
+    /** 获取缓存物模型*/
+    getCacheThingsModdel(productId) {
+      return new Promise((resolve, reject) => {
+        cacheJsonThingsModel(productId)
+          .then((response) => {
+            resolve(JSON.parse(response.data))
+          })
+          .catch((error) => {
+            reject(error)
+          })
+      })
+    },
+    /**获取设备运行状态*/
+    getDeviceStatus(data) {
+      const params = {
+        deviceId: data.deviceId,
+        slaveId: data.slaveId
+      }
+      return new Promise((resolve, reject) => {
+        getDeviceRunningStatus(params)
+          .then((response) => {
+            resolve(response.data.thingsModels)
+          })
+          .catch((error) => {
+            reject(error)
+          })
+      })
+    },
+    formatThingsModel(data) {
+      data.chartList = []
+      data.monitorList = []
+      data.staticList = []
+      // 物模型格式化
+      for (let i = 0; i < data.thingsModels.length; i++) {
+        // 数字类型设置默认值并转换未数值
+        if (
+          data.thingsModels[i].datatype.type == 'integer' ||
+          data.thingsModels[i].datatype.type == 'decimal'
+        ) {
+          if (data.thingsModels[i].shadow == '') {
+            data.thingsModels[i].shadow = Number(data.thingsModels[i].datatype.min)
+          } else {
+            data.thingsModels[i].shadow = Number(data.thingsModels[i].shadow)
+          }
+          if (data.thingsModels[i].value == '') {
+            data.thingsModels[i].value = Number(data.thingsModels[i].value)
+          }
+        }
+
+        // 物模型分类放置
+        if (data.thingsModels[i].datatype.type == 'array') {
+          if (data.thingsModels[i].datatype.arrayType == 'object') {
+            for (let k = 0; k < data.thingsModels[i].datatype.arrayParams.length; k++) {
+              for (let j = 0; j < data.thingsModels[i].datatype.arrayParams[k].length; j++) {
+                // 数组元素中参数ID添加前缀,例如:array_00_
+                let index = k > 9 ? String(k) : '0' + k
+                let prefix = 'array_' + index + '_'
+                data.thingsModels[i].datatype.arrayParams[k][j].id =
+                  prefix + data.thingsModels[i].datatype.arrayParams[k][j].id
+                // 图表、实时监测、监测统计分类放置
+                if (data.thingsModels[i].datatype.arrayParams[k][j].isChart == 1) {
+                  // 图表
+                  data.thingsModels[i].datatype.arrayParams[k][j].name =
+                    '[' +
+                    data.thingsModels[i].name +
+                    (k + 1) +
+                    '] ' +
+                    data.thingsModels[i].datatype.arrayParams[k][j].name
+                  data.thingsModels[i].datatype.arrayParams[k][j].datatype.arrayType = 'object'
+                  data.chartList.push(data.thingsModels[i].datatype.arrayParams[k][j])
+                  if (data.thingsModels[i].datatype.arrayParams[k][j].isHistory == 1) {
+                    // 监测统计
+                    data.staticList.push(data.thingsModels[i].datatype.arrayParams[k][j])
+                  }
+                  if (data.thingsModels[i].datatype.arrayParams[k][j].isMonitor == 1) {
+                    // 实时监测
+                    data.monitorList.push(data.thingsModels[i].datatype.arrayParams[k][j])
+                  }
+                  data.thingsModels[i].datatype.arrayParams[k].splice(j--, 1)
+                }
+              }
+            }
+          } else {
+            // 字符串拆分为物模型数组 model=id/name/type/isReadonly/value/shadow
+            let values =
+              data.thingsModels[i].value != '' ? data.thingsModels[i].value.split(',') : []
+            let shadows =
+              data.thingsModels[i].shadow != '' ? data.thingsModels[i].shadow.split(',') : []
+            for (let j = 0; j < data.thingsModels[i].datatype.arrayCount; j++) {
+              if (!data.thingsModels[i].datatype.arrayModel) {
+                data.thingsModels[i].datatype.arrayModel = []
+              }
+              // 数组里面的ID需要添加前缀和索引,例如:array_00_temperature
+              let index = j > 9 ? String(j) : '0' + j
+              let prefix = 'array_' + index + '_'
+              data.thingsModels[i].datatype.arrayModel[j] = {
+                id: prefix + data.thingsModels[i].id,
+                name: data.thingsModels[i].name,
+                type: data.thingsModels[i].type,
+                isReadonly: data.thingsModels[i].isReadonly,
+                value: values[j] ? values[j] : '',
+                shadow: shadows[j] ? shadows[j] : ''
+              }
+            }
+          }
+        } else if (data.thingsModels[i].datatype.type == 'object') {
+          for (let j = 0; j < data.thingsModels[i].datatype.params.length; j++) {
+            // 图表、实时监测、监测统计分类放置
+            if (data.thingsModels[i].datatype.params[j].isChart == 1) {
+              // 图表
+              data.thingsModels[i].datatype.params[j].name =
+                '[' +
+                data.thingsModels[i].name +
+                '] ' +
+                data.thingsModels[i].datatype.params[j].name
+              data.chartList.push(data.thingsModels[i].datatype.params[j])
+              if (data.thingsModels[i].datatype.params[j].isHistory == 1) {
+                // 监测统计
+                data.staticList.push(data.thingsModels[i].datatype.params[j])
+              }
+              if (data.thingsModels[i].datatype.params[j].isMonitor == 1) {
+                // 实时监测
+                data.monitorList.push(data.thingsModels[i].datatype.params[j])
+              }
+              data.thingsModels[i].datatype.params.splice(j--, 1)
+            }
+          }
+        } else if (data.thingsModels[i].isChart == 1) {
+          // // 图表、实时监测、监测统计分类放置
+          data.chartList.push(data.thingsModels[i])
+          if (data.thingsModels[i].isHistory == 1) {
+            // 监测统计
+            data.staticList.push(data.thingsModels[i])
+          }
+          if (data.thingsModels[i].isMonitor == 1) {
+            // 实时监测
+            data.monitorList.push(data.thingsModels[i])
+          }
+          // 使用i--解决索引变更问题
+          data.thingsModels.splice(i--, 1)
+        }
+      }
+    },
+    /**加载地图*/
+    loadMap() {
+      this.$nextTick(() => {
+        loadBMap().then(() => {
+          this.getmap()
+        })
+      })
+    },
+    /** 返回按钮 */
+    goBack() {
+      const obj = {
+        path: '/iotdev/iot/device',
+        query: {
+          t: Date.now(),
+          pageNum: this.$route.query.pageNum
+        }
+      }
+      this.$tab.closeOpenPage(obj)
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        deviceId: 0,
+        deviceName: null,
+        productId: null,
+        productName: null,
+        userId: null,
+        userName: null,
+        tenantId: null,
+        tenantName: null,
+        serialNumber: '',
+        firmwareVersion: 1.0,
+        status: 1,
+        rssi: null,
+        networkAddress: null,
+        networkIp: null,
+        longitude: null,
+        latitude: null,
+        activeTime: null,
+        createBy: null,
+        createTime: null,
+        updateBy: null,
+        updateTime: null,
+        remark: null,
+        locationWay: 1,
+        clientId: 0
+      }
+      this.deviceStatus = 0
+      this.resetForm('form')
+    },
+    /** 提交按钮 */
+    async submitForm() {
+      if (this.form.serialNumber == null || this.form.serialNumber == 0) {
+        this.$modal.alertError(this.t('device.device-edit148398-65'))
+        return
+      }
+      let reg = /^[0-9a-zA-Z]+$/
+      if (!reg.test(this.form.serialNumber)) {
+        this.$modal.alertError(this.t('device.device-edit148398-66'))
+        return
+      }
+      if (this.form.productId == null || this.form.productId == 0) {
+        this.$modal.alertError(this.t('device.device-edit148398-67'))
+        return
+      }
+
+      this.$refs['form'].validate((valid) => {
+        if (valid) {
+          if (this.form.deviceId != 0) {
+            updateDevice(this.form).then((response) => {
+              if (response.data == 0) {
+                this.$modal.alertError(response.msg)
+              } else {
+                this.$modal.alertSuccess(this.t('device.device-edit148398-68'))
+                this.form = JSON.parse(JSON.stringify(this.form))
+                this.loadMap()
+                //是否设备设置为禁用状态,则踢出设备
+                if (this.form.status === 2) {
+                  const params = { clientId: this.form.serialNumber }
+                  clientOut(params).then((res) => {})
+                }
+              }
+            })
+          } else {
+            addDevice(this.form).then(async (response) => {
+              // 获取设备状态
+              await this.getDeviceStatusWitchThingsModel(response)
+              if (this.form.deviceId == null || this.form.deviceId == 0) {
+                this.$modal.alertError(this.t('device.device-edit148398-69'))
+              } else {
+                if (this.form.status == 2) {
+                  this.deviceStatus = 1
+                }
+
+                this.$modal.alertSuccess(this.t('device.device-edit148398-70'))
+                this.loadMap()
+              }
+            })
+          }
+        }
+      })
+    },
+    /** 获取设备状态和物模型 **/
+    async getDeviceStatusWitchThingsModel(response) {
+      // 获取缓存物模型
+      response.data.cacheThingsModel = await this.getCacheThingsModdel(response.data.productId)
+      // 获取设备运行状态
+      response.data.thingsModels = await this.getDeviceStatus(response.data)
+      //分享设备过滤没有权限的物模型
+      // if (response.data.isOwner == 0) {
+      //     for (let i = 0; i < response.data.thingsModels.length; i++) {
+      //         if (response.data.userPerms.indexOf(response.data.thingsModels[i].id) == -1) {
+      //             response.data.thingsModels.splice(i--, 1);
+      //         }
+      //     }
+      // }
+      // 格式化物模型,拆分出监测值,数组添加前缀
+      this.formatThingsModel(response.data)
+      this.form = response.data
+      // 解析设备摘要
+      if (this.form.summary != null && this.form.summary != '') {
+        this.summary = JSON.parse(this.form.summary)
+      }
+      this.oldDeviceStatus = this.form.status
+      this.loadMap()
+      //Mqtt订阅
+      this.connectMqtt()
+    },
+    /**选择产品 */
+    selectProduct() {
+      this.$refs.productList.open = true
+      this.$refs.productList.getList()
+    },
+    genSipID() {
+      this.$refs.sipidGen.open = true
+    },
+    /**获取选中的产品 */
+    getProductData(product) {
+      this.form.productId = product.productId
+      this.form.productName = product.productName
+      this.form.deviceType = product.deviceType
+      this.form.tenantId = product.tenantId
+      this.form.tenantName = product.tenantName
+      if (product.transport === 'TCP') {
+        this.openServerTip = true
+        this.serverType = 3
+      } else {
+        this.openServerTip = false
+        this.serverType = 1
+      }
+    },
+    getSipIDData(devsipid) {
+      this.form.serialNumber = devsipid
+    },
+
+    // 获取选中的用户
+    getUserData(user) {},
+    /**关闭物模型 */
+    openSummaryDialog() {
+      let json = {
+        type: 1, // 1=扫码关联设备
+        deviceNumber: this.form.serialNumber,
+        productId: this.form.productId
+        // productName: this.form.productName,
+      }
+      this.qrText = JSON.stringify(json)
+      this.openSummary = true
+    },
+    /**关闭物模型 */
+    closeSummaryDialog() {
+      this.openSummary = false
+      this.openViewMqtt = false
+    },
+    doCopy(type) {
+      if (type == 2) {
+        const input = document.createElement('input')
+        input.value =
+          '{clientId:' +
+          this.listQuery.clientId +
+          ',username:' +
+          this.listQuery.username +
+          ',passwd:' +
+          this.listQuery.passwd +
+          ',port:' +
+          this.listQuery.port +
+          '}'
+        document.body.appendChild(input)
+        input.select() //选中输入框
+        document.execCommand('Copy') //复制当前选中文本到前切板
+        document.body.removeChild(input)
+        this.$message.success(this.t('device.device-edit148398-71'))
+      }
+    },
+    openCodeDialog() {
+      let json = {
+        type: 1, // 1=扫码关联设备
+        deviceNumber: this.form.serialNumber,
+        productId: this.form.productId,
+        productName: this.form.productName
+      }
+      this.qrText = JSON.stringify(json)
+      this.openCode = true
+    },
+    // 地图定位
+    getmap() {
+      this.map = new BMap.Map('map')
+      let point = null
+      if (
+        this.form.longitude != null &&
+        this.form.longitude != '' &&
+        this.form.latitude != null &&
+        this.form.latitude != ''
+      ) {
+        point = new BMap.Point(this.form.longitude, this.form.latitude)
+      } else {
+        point = new BMap.Point(116.404, 39.915)
+      }
+      this.map.centerAndZoom(point, 19)
+      this.map.enableScrollWheelZoom(true) // 开启鼠标滚轮缩放
+      this.map.addControl(new BMap.NavigationControl())
+
+      // 标注设备位置
+      this.mk = new BMap.Marker(point)
+      this.map.addOverlay(this.mk)
+      this.map.panTo(point)
+    },
+    // 生成随机字母和数字
+    generateNum() {
+      if (!this.form.productId || this.form.productId == 0) {
+        this.$modal.alertError(this.t('device.device-edit148398-72'))
+        return
+      }
+      this.genDisabled = true
+      const params = { type: this.serverType }
+      generatorDeviceNum(params).then((response) => {
+        this.form.serialNumber = response.data
+        this.genDisabled = false
+      })
+    },
+    //mqtt参数查看
+    handleViewMqtt() {
+      this.openViewMqtt = true
+      this.loading = true
+      const params = {
+        deviceId: this.form.deviceId
+      }
+      getMqttConnect(params).then((response) => {
+        if (response.code == 200) {
+          this.listQuery = response.data
+          if (response.data.mqttstats === 1) {
+            if (response.data.fpname == null) {
+              this.fb = '/' + this.form.productId + '/' + this.form.serialNumber + '/property/post'
+            } else {
+              this.fb = response.data.fpname
+            }
+            if (response.data.dyname == null) {
+              this.dy = '/' + this.form.productId + '/' + this.form.serialNumber + '/function/get'
+            } else {
+              this.dy = response.data.dyname
+            }
+          } else {
+            this.fb = '/' + this.form.productId + '/' + this.form.serialNumber + '/property/post'
+            this.dy = '/' + this.form.productId + '/' + this.form.serialNumber + '/function/get'
+          }
+          this.loading = false
+        }
+      })
+    }
+  }
+}
+</script>

+ 232 - 0
src/views/pms/video_center/device/device-functionlog.vue

@@ -0,0 +1,232 @@
+<template>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item :label="$t('device.device-functionlog.399522-0')" label-width="120px" v-if="isSubDev">
+                <el-select v-model="queryParams.slaveId" :placeholder="$t('device.device-functionlog.399522-1')" @change="selectSlave">
+                    <el-option v-for="slave in slaveList" :key="slave.slaveId" :label="`${slave.deviceName}   ({{ $t('device.device-functionlog.399522-2') }}{slave.slaveId})`" :value="slave.slaveId"></el-option>
+                </el-select>
+            </el-form-item>
+            <el-form-item :label="$t('device.device-functionlog.399522-3')" prop="funType">
+                <el-select v-model="queryParams.funType" :placeholder="$t('device.device-functionlog.399522-4')" clearable size="small">
+                    <el-option v-for="dict in dict.type.iot_function_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item :label="$t('device.device-functionlog.399522-5')" prop="identify">
+                <el-input v-model="queryParams.identify" :placeholder="$t('device.device-functionlog.399522-6')" clearable size="small" @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('device.device-functionlog.399522-7')">
+                <el-date-picker
+                    v-model="daterangeTime"
+                    size="small"
+                    style="width: 240px"
+                    value-format="yyyy-MM-dd"
+                    type="daterange"
+                    range-separator="-"
+                    :start-placeholder="$t('device.device-functionlog.399522-8')"
+                    :end-placeholder="$t('device.device-functionlog.399522-9')"
+                ></el-date-picker>
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.device-functionlog.399522-10') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.device-functionlog.399522-11') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table :border="false" v-loading="loading" :data="logList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column :label="showName" align="center" prop="identify" />
+            <el-table-column :label="$t('device.device-functionlog.399522-12')" align="center" prop="funType" width="120px">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.iot_function_type" :value="scope.row.funType" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-functionlog.399522-13')" align="center" prop="funValue" />
+            <el-table-column :label="$t('device.device-edit.148398-7')" align="center" prop="serialNumber" />
+            <el-table-column :label="$t('device.device-functionlog.399522-15')" align="center" prop="createTime" />
+            <el-table-column :label="$t('device.device-functionlog.399522-16')" align="center" prop="resultMsg" />
+            <el-table-column :label="$t('opation')" align="center" width="80">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:log:remove']">{{ $t('device.device-functionlog.399522-18') }}</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+    </div>
+</template>
+
+<script>
+import { listLog, getLog, delLog, addLog, updateLog } from '@/api/iot/functionLog';
+import { cacheJsonThingsModel } from '@/api/iot/model';
+
+export default {
+    name: 'device-func',
+    dicts: ['iot_function_type', 'iot_yes_no'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后
+        device: function (newVal) {
+            this.deviceInfo = newVal;
+            if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                this.isSubDev = this.deviceInfo.subDeviceList && this.deviceInfo.subDeviceList.length > 0;
+                this.showName = this.isSubDev ? this.$t('device.device-functionlog.399522-19') : this.$t('device.device-functionlog.399522-5');
+                this.queryParams.deviceId = this.deviceInfo.deviceId;
+                this.queryParams.slaveId = this.deviceInfo.slaveId;
+                this.queryParams.serialNumber = this.deviceInfo.serialNumber;
+                this.slaveList = newVal.subDeviceList;
+                this.getList();
+            }
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 设备服务下发日志表格数据
+            logList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            //设备数据
+            deviceInfo: {},
+            // 时间范围
+            daterangeTime: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                identify: null,
+                funType: null,
+                funValue: null,
+                messageId: null,
+                deviceName: null,
+                serialNumber: null,
+                mode: null,
+                userId: null,
+                resultMsg: null,
+                resultCode: null,
+                slaveId: null,
+            },
+            // 表单参数
+            form: {},
+            //是否是modbus设备组
+            isSubDev: false,
+            showName: null,
+            slaveList: [],
+            // 表单校验
+            rules: {
+                identify: [{ required: true, message: this.$t('device.device-functionlog.399522-20'), trigger: 'blur' }],
+                funType: [{ required: true, message: this.$t('device.device-functionlog.399522-21'), trigger: 'change' }],
+                funValue: [{ required: true, message: this.$t('device.device-functionlog.399522-22'), trigger: 'blur' }],
+                serialNumber: [{ required: true, message: this.$t('device.device-functionlog.399522-23'), trigger: 'blur' }],
+            },
+        };
+    },
+    created() {
+        this.queryParams.serialNumber = this.device.serialNumber;
+        this.getList();
+    },
+    methods: {
+        /** 查询设备服务下发日志列表 */
+        getList() {
+            this.loading = true;
+            if (null != this.daterangeTime && '' != this.daterangeTime) {
+                this.queryParams.beginTime = this.daterangeTime[0];
+                this.queryParams.endTime = this.daterangeTime[1];
+            }
+            if (this.queryParams.slaveId) {
+                this.queryParams.serialNumber = this.queryParams.serialNumber + '_' + this.queryParams.slaveId;
+            }
+            listLog(this.queryParams).then((response) => {
+                this.logList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                id: null,
+                identify: null,
+                funType: null,
+                funValue: null,
+                messageId: null,
+                deviceName: null,
+                serialNumber: null,
+                mode: null,
+                userId: null,
+                resultMsg: null,
+                resultCode: null,
+                createBy: null,
+                createTime: null,
+                remark: null,
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.id);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const ids = row.id || this.ids;
+            this.$modal
+                .confirm(this.$t('device.device-functionlog.399522-24', [ids]))
+                .then(function () {
+                    return delLog(ids);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('device.device-functionlog.399522-26'));
+                })
+                .catch(() => {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/log/export',
+                {
+                    ...this.queryParams,
+                },
+                `log_${new Date().getTime()}.xlsx`
+            );
+        },
+        //选择从机
+        selectSlave() {},
+    },
+};
+</script>

+ 521 - 0
src/views/pms/video_center/device/device-history.vue

@@ -0,0 +1,521 @@
+<template>
+    <div style="padding: 10px; background-color: #f8f8f8">
+        <el-card class="main-card">
+            <div>
+                <el-form v-show="showSearch" ref="queryForm" :inline="true" :model="queryParams" label-width="68px" size="small">
+                    <el-form-item label="设备名称" prop="serialNumber">
+                        <el-select v-model="queryParams.serialNumber" clearable placeholder="请选择">
+                            <el-option v-for="item in options" :key="item.serialNumber" :label="item.deviceName" :value="item.serialNumber"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="日期范围" prop="daterange">
+                        <el-date-picker v-model="queryParams.daterange" end-placeholder="结束日期" range-separator="至" start-placeholder="开始日期" type="daterange"></el-date-picker>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button icon="el-icon-search" size="mini" type="primary" @click="getlist()">搜索</el-button>
+                        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+                        <el-button icon="el-icon-refresh" size="mini" @click="quanhuan(!open)">切换</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </el-card>
+        <el-card class="main-card">
+            <div v-if="this.open" ref="map" style="height: 650px"></div>
+            <div v-else>
+                <!-- 表格数据-->
+                <el-table :border="false" :data="deviceList" style="width: 100%">
+                    <el-table-column align="center" label="设备名称" prop="deviceName"></el-table-column>
+                    <el-table-column align="center" label="上报时间" prop="reportTime">
+                        <template slot-scope="scope">
+                            <span>{{ parseTime(scope.row.reportTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
+                        </template>
+                    </el-table-column>
+                    <el-table-column align="center" label="纬度" prop="latitude"></el-table-column>
+                    <el-table-column align="center" label="经度" prop="longitude"></el-table-column>
+                </el-table>
+                <pagination :limit.sync="queryParams.pageSize" :page.sync="queryParams.pageNum" :total="total" @pagination="getlist" />
+            </div>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import CountTo from 'vue-count-to';
+import { loadBMap } from '@/utils/map.js';
+//安装的是echarts完整包,里面包含百度地图扩展,路径为 echarts/extension/bmap/bmap.js,将其引入
+//ECharts的百度地图扩展,可以在百度地图上展现点图,线图,热力图等可视化
+require('echarts/extension/bmap/bmap');
+import { listAllDeviceShort, getdevicehis } from '@/api/iot/device';
+import moment from 'moment';
+import Template from '@/views/iot/template/index.vue';
+
+export default {
+    name: 'Index',
+    components: {
+        Template,
+        CountTo,
+    },
+    data() {
+        return {
+            // 总条数
+            total: 0,
+            // 设备列表
+            deviceList: [],
+            // 版本号
+            version: '3.8.0',
+            //搜索条件
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                serialNumber: null,
+                daterange: null,
+            },
+            // 显示搜索条件
+            showSearch: true,
+            options: [],
+            open: true,
+        };
+    },
+    created() {
+        this.getAllDevice();
+        this.loadMap();
+    },
+    methods: {
+        /**查询所有设备 */
+        getAllDevice() {
+            listAllDeviceShort(this.queryParams).then((response) => {
+                this.options = response.rows;
+            });
+        },
+        getlist() {
+            if (this.queryParams.serialNumber == null || this.queryParams.serialNumber == '') {
+                this.$modal.msgWarning('设备不能为空');
+                return;
+            }
+            if (this.queryParams.daterange == null || this.queryParams.serialNumber == '') {
+                this.$modal.msgWarning('日期不能为空');
+                return;
+            }
+            const data = {
+                beginDate: moment(this.queryParams.daterange[0]).format('YYYY-MM-DD'),
+                endDate: moment(this.queryParams.daterange[1]).format('YYYY-MM-DD'),
+                serialNumber: this.queryParams.serialNumber,
+                pageNum: this.queryParams.pageNum,
+                pageSize: this.queryParams.pageSize,
+            };
+            getdevicehis(data).then((response) => {
+                this.deviceList = response.rows;
+                this.total = response.total;
+                this.loadMap();
+            });
+        },
+        /**加载地图*/
+        loadMap() {
+            this.$nextTick(() => {
+                loadBMap().then(() => {
+                    this.getmap();
+                });
+            });
+        },
+        /** 地图 */
+        getmap() {
+            var myChart = this.$echarts.init(this.$refs.map);
+            var option;
+
+            // 格式化数据
+            let convertData = function (data, status) {
+                var res = [];
+                for (var i = 0; i < data.length; i++) {
+                    var geoCoord = [data[i].longitude, data[i].latitude];
+                    if (geoCoord && data[i].status == status) {
+                        res.push({
+                            name: data[i].deviceName,
+                            value: geoCoord,
+                            serialNumber: data[i].serialNumber,
+                            status: data[i].status,
+                            isShadow: data[i].isShadow,
+                            firmwareVersion: data[i].firmwareVersion,
+                            networkAddress: data[i].networkAddress,
+                            productName: data[i].productName,
+                            activeTime: data[i].activeTime == null ? '' : data[i].activeTime,
+                            deviceId: data[i].deviceId,
+                            locationWay: data[i].locationWay,
+                        });
+                    }
+                }
+                return res;
+            };
+            option = {
+                title: {
+                    text: '设备历史轨迹',
+                    subtext: '',
+                    sublink: 'https://iot.yanfankeji.com',
+                    target: '_blank',
+                    textStyle: {
+                        color: '#333',
+                        textBorderColor: '#fff',
+                        textBorderWidth: 10,
+                    },
+                    top: 10,
+                    left: 'center',
+                },
+                tooltip: {
+                    trigger: 'item',
+                    formatter: function (params) {
+                        var htmlStr = '<div style="padding:5px;line-height:28px;">';
+                        htmlStr += "设备名称: <span style='color:#409EFF'>" + params.data.name + '</span><br />';
+                        htmlStr += '设备编号: ' + params.data.serialNumber + '<br />';
+                        htmlStr += '设备状态: ';
+                        if (params.data.status == 1) {
+                            htmlStr += "<span style='color:#E6A23C'>未激活</span>" + '<br />';
+                        } else if (params.data.status == 2) {
+                            htmlStr += "<span style='color:#F56C6C'>禁用</span>" + '<br />';
+                        } else if (params.data.status == 3) {
+                            htmlStr += "<span style='color:#67C23A'>在线</span>" + '<br />';
+                        } else if (params.data.status == 4) {
+                            htmlStr += "<span style='color:#909399'>离线</span>" + '<br />';
+                        }
+                        if (params.data.isShadow == 1) {
+                            htmlStr += '设备影子: ' + "<span style='color:#67C23A'>启用</span>" + '<br />';
+                        } else {
+                            htmlStr += '设备影子: ' + "<span style='color:#909399'>未启用</span>" + '<br />';
+                        }
+                        htmlStr += '产品名称: ' + params.data.productName + '<br />';
+                        htmlStr += '固件版本: Version ' + params.data.firmwareVersion + '<br />';
+                        htmlStr += '激活时间: ' + params.data.activeTime + '<br />';
+                        htmlStr += '定位方式: ';
+                        if (params.data.locationWay == 1) {
+                            htmlStr += '自动定位' + '<br />';
+                        } else if (params.data.locationWay == 2) {
+                            htmlStr += '设备定位' + '<br />';
+                        } else if (params.data.locationWay == 3) {
+                            htmlStr += '自定义位置' + '<br />';
+                        } else {
+                            htmlStr += '未知' + '<br />';
+                        }
+                        htmlStr += '所在地址: ' + params.data.networkAddress + '<br />';
+                        htmlStr += '</div>';
+                        return htmlStr;
+                    },
+                },
+                bmap: {
+                    center: [133, 38],
+                    zoom: 5,
+                    roam: true,
+                    mapStyle: {
+                        styleJson: [
+                            {
+                                featureType: 'water',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#a0cfff',
+                                },
+                            },
+                            {
+                                featureType: 'land',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#fafafa', // #fffff8 淡黄色
+                                },
+                            },
+                            {
+                                featureType: 'railway',
+                                elementType: 'all',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'highway',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#fdfdfd',
+                                },
+                            },
+                            {
+                                featureType: 'highway',
+                                elementType: 'labels',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'arterial',
+                                elementType: 'geometry',
+                                stylers: {
+                                    color: '#fefefe',
+                                },
+                            },
+                            {
+                                featureType: 'arterial',
+                                elementType: 'geometry.fill',
+                                stylers: {
+                                    color: '#fefefe',
+                                },
+                            },
+                            {
+                                featureType: 'poi',
+                                elementType: 'all',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'green',
+                                elementType: 'all',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'subway',
+                                elementType: 'all',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'manmade',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#d1d1d1',
+                                },
+                            },
+                            {
+                                featureType: 'local',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#d1d1d1',
+                                },
+                            },
+                            {
+                                featureType: 'arterial',
+                                elementType: 'labels',
+                                stylers: {
+                                    visibility: 'off',
+                                },
+                            },
+                            {
+                                featureType: 'boundary',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#999999',
+                                },
+                            },
+                            {
+                                featureType: 'building',
+                                elementType: 'all',
+                                stylers: {
+                                    color: '#d1d1d1',
+                                },
+                            },
+                            {
+                                featureType: 'label',
+                                elementType: 'labels.text.fill',
+                                stylers: {
+                                    color: '#999999',
+                                },
+                            },
+                        ],
+                    },
+                },
+                series: [
+                    {
+                        type: 'scatter',
+                        coordinateSystem: 'bmap',
+                        data: convertData(this.deviceList, 1),
+                        symbolSize: 15,
+                        itemStyle: {
+                            color: '#E6A23C',
+                        },
+                    },
+                    {
+                        type: 'scatter',
+                        coordinateSystem: 'bmap',
+                        data: convertData(this.deviceList, 2),
+                        symbolSize: 15,
+                        itemStyle: {
+                            color: '#F56C6C',
+                        },
+                    },
+                    {
+                        type: 'scatter',
+                        coordinateSystem: 'bmap',
+                        data: convertData(this.deviceList, 4),
+                        symbolSize: 15,
+                        itemStyle: {
+                            color: '#909399',
+                        },
+                    },
+                    {
+                        type: 'effectScatter',
+                        coordinateSystem: 'bmap',
+                        data: convertData(this.deviceList, 3),
+                        symbolSize: 15,
+                        showEffectOn: 'render',
+                        rippleEffect: {
+                            brushType: 'stroke',
+                            scale: 5,
+                        },
+                        label: {
+                            formatter: '{b}',
+                            position: 'right',
+                            show: false,
+                        },
+                        itemStyle: {
+                            color: '#67C23A',
+                            shadowBlur: 100,
+                            shadowColor: '#333',
+                        },
+                        zlevel: 1,
+                    },
+                ],
+            };
+
+            option && myChart.setOption(option);
+        },
+        resetQuery() {
+            this.total = 0;
+            this.queryParams = {
+                pageNum: 1,
+                pageSize: 10,
+                serialNumber: null,
+                daterange: null,
+            };
+            this.deviceList = [];
+        },
+        quanhuan(tab) {
+            if (this.total == 0) {
+                this.$modal.msgWarning('无数据');
+                this.loadMap();
+                return;
+            }
+            this.open = tab;
+            if (this.open) {
+                this.getlist();
+            }
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.phone {
+    height: 729px;
+    width: 370px;
+    background-size: cover;
+    margin: 0 auto;
+}
+
+.phone-container {
+    height: 618px;
+    width: 343px;
+    position: relative;
+    top: 46px;
+    left: 12px;
+    background-color: #fff;
+}
+
+.content {
+    line-height: 24px;
+    padding: 10px;
+    border: 1px solid #eee;
+    border-radius: 10px;
+}
+
+.description {
+    font-size: 12px;
+
+    tr {
+        line-height: 20px;
+    }
+}
+
+.panel-group {
+    .card-panel-col {
+        margin-bottom: 10px;
+    }
+
+    .card-panel {
+        height: 68px;
+        cursor: pointer;
+        position: relative;
+        overflow: hidden;
+        color: #666;
+        border: 1px solid #eee;
+        border-radius: 5px;
+        //box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08);
+        background-color: #fff;
+
+        &:hover {
+            .card-panel-icon-wrapper {
+                color: #fff;
+            }
+
+            .icon-blue {
+                background: #36a3f7;
+            }
+
+            .icon-green {
+                background: #34bfa3;
+            }
+
+            .icon-red {
+                background: #f56c6c;
+            }
+
+            .icon-orange {
+                background: #e6a23c;
+            }
+        }
+
+        .icon-blue {
+            color: #36a3f7;
+        }
+
+        .icon-green {
+            color: #34bfa3;
+        }
+
+        .icon-red {
+            color: #f56c6c;
+        }
+
+        .icon-orange {
+            color: #e6a23c;
+        }
+
+        .card-panel-icon-wrapper {
+            float: left;
+            margin: 10px;
+            padding: 10px;
+            transition: all 0.38s ease-out;
+            border-radius: 6px;
+        }
+
+        .card-panel-icon {
+            float: left;
+            font-size: 30px;
+        }
+
+        .card-panel-description {
+            float: right;
+            font-weight: bold;
+            margin: 15px;
+            margin-left: 0px;
+
+            .card-panel-text {
+                line-height: 14px;
+                color: rgba(0, 0, 0, 0.45);
+                font-size: 14px;
+                margin-bottom: 12px;
+                text-align: right;
+            }
+
+            .card-panel-num {
+                font-size: 18px;
+            }
+        }
+    }
+}
+</style>

+ 162 - 0
src/views/pms/video_center/device/device-inline-video.vue

@@ -0,0 +1,162 @@
+<template>
+    <div class="device-inline-video" v-loading="loading" element-loading-background="#ffff">
+        <el-row v-show="total > 0">
+            <el-col id="deviceVideoCol" :span="8" style="padding-left: 10px; padding-right: 10px; padding-top: 15px" v-for="(item, index) in sipList" :key="index">
+                <div class="video">
+                    <player :ref="`player_${index}`" :playerinfo="{ playtype: 'play', deviceId: item.deviceSipId, channelId: item.channelId }"></player>
+                    <span v-if="item.status === 1" class="status" style="color: #ffba00">{{ $t('home.notActive') }}</span>
+                    <span v-if="item.status === 2" class="status" style="color: #ff4949">{{ $t('home.disabled') }}</span>
+                    <span v-if="item.status === 4" class="status" style="color: #909399">{{ $t('home.offline') }}</span>
+                    <i v-if="item.status === 3" class="el-icon-caret-right btn" @click="handlePlay($event, item, index)"></i>
+                </div>
+            </el-col>
+        </el-row>
+        <el-empty v-if="total === 0" :description="$t('device.inline-video.986754-0')"></el-empty>
+        <pagination v-show="total > 0" :total="total" :page.sync="pageNum" :limit.sync="pageSize" :pageSizes="[9, 18, 27, 36]" @pagination="getSipList" />
+    </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import player from '@/views/components/player/player.vue';
+import { startPlay, closeStream } from '@/api/iot/channel';
+
+export default {
+    name: 'DeviceInlineVideo',
+    components: {
+        player,
+    },
+    props: {
+        sipRelationList: {
+            type: Array,
+            default: function () {
+                return [];
+            },
+        },
+    },
+    watch: {
+        // 检测系统菜单栏状态
+        sidebarStatus() {
+            this.calculatePlayerHeight();
+        },
+        sipRelationList: {
+            handler(newVal, oldVal) {
+                if (newVal && newVal.length !== 0) {
+                    this.total = newVal.length;
+                    this.tempSipList = this.sipRelationList;
+                }
+            },
+            deep: true,
+        },
+    },
+    computed: {
+        ...mapState({
+            sidebarStatus: (state) => state.app.sidebar.opened,
+        }),
+        sipList() {
+            const start = (this.pageNum - 1) * this.pageSize;
+            const end = start + this.pageSize;
+            return this.sipRelationList.slice(start, end);
+        },
+    },
+    data() {
+        return {
+            loading: false,
+            pageNum: 1,
+            pageSize: 9,
+            total: 0,
+            tempSipList: [],
+        };
+    },
+    mounted() {
+        this.calculatePlayerHeight();
+        window.addEventListener('resize', this.calculatePlayerHeight, true);
+        this.tempSipList = this.sipRelationList;
+        this.total = this.sipRelationList.length;
+    },
+    methods: {
+        // 获取窗体高度
+        calculatePlayerHeight() {
+            const _this = this;
+            _this.loading = true;
+            setTimeout(() => {
+                _this.sipList &&
+                    _this.sipList.forEach((item, index) => {
+                        let videoContainer = _this.$refs[`player_${index}`][0].$refs.container;
+                        const offsetWidth = document.getElementById('deviceVideoCol').offsetWidth;
+                        videoContainer.style.width = offsetWidth ? offsetWidth - 10 + 'px' : '300px';
+                        videoContainer.style.height = '230px';
+                    });
+                _this.loading = false;
+            }, 100);
+        },
+        getSipList(e) {
+            this.pageNum = e.page;
+            this.pageSize = e.limit;
+        },
+        // 播放
+        handlePlay(event, item, index) {
+            const _this = this;
+            startPlay(item.deviceSipId, item.channelId).then((res) => {
+                if (res.code === 200) {
+                    if (!_this.$refs[`player_${index}`][0].isInit) {
+                        _this.$refs[`player_${index}`][0].init();
+                    }
+                    _this.tempSipList[index].streamId = res.data.streamId;
+                    _this.tempSipList[index].playurl = res.data.playurl;
+                    _this.$refs[`player_${index}`][0].play(res.data.playurl);
+                    event.target.style.visibility = 'hidden';
+                }
+            });
+        },
+        // 关闭播放器
+        handleClose() {
+            if (this.sipList && this.sipList.length !== 0) {
+                const _this = this;
+                this.sipList.forEach((item, index) => {
+                    if (_this.tempSipList[index].streamId) {
+                        closeStream(item.deviceSipId, item.channelId, _this.tempSipList[index].streamId).then((res) => {});
+                    }
+                    _this.$refs[`player_${index}`][0] && _this.$refs[`player_${index}`][0].close();
+                });
+            }
+        },
+    },
+    destroyed() {
+        this.handleClose();
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-inline-video {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    padding-bottom: 15px;
+
+    .video {
+        position: relative;
+        width: 350px;
+        height: 230px;
+
+        .btn {
+            position: absolute;
+            bottom: 5px;
+            left: 5px;
+            border-radius: 50%;
+            background: #fff;
+            padding: 5px;
+            cursor: pointer;
+            color: #606266;
+        }
+
+        .status {
+            position: absolute;
+            top: 5px;
+            left: 8px;
+            font-size: 12px;
+        }
+    }
+}
+</style>

+ 1255 - 0
src/views/pms/video_center/device/device-linkage.vue

@@ -0,0 +1,1255 @@
+<template>
+    <div>
+        <el-card class="main-card">
+            <el-row :gutter="10">
+                <!--机构-设备数据-->
+                <el-col :span="5" :xs="24" style="padding: 0">
+                    <el-card style="margin: 0 20px 20px 0">
+                        <div class="menu-wrap">
+                            <el-input v-model="deptName" :placeholder="$t('device.device-linkage.188958-0')" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px; margin-right: 10px" />
+                            <el-tree
+                                :data="deptOptions"
+                                :props="defaultProps"
+                                :expand-on-click-node="false"
+                                :filter-node-method="filterNode"
+                                ref="tree"
+                                node-key="id"
+                                default-expand-all
+                                highlight-current
+                                @node-click="handleNodeClick"
+                            >
+                                <div class="user-tree-item" slot-scope="{ node, data }" :title="node.label">
+                                    {{ node.label }}
+                                </div>
+                            </el-tree>
+                        </div>
+                    </el-card>
+                    <el-card style="margin-right: 20px">
+                        <div style="height: 445px">
+                            <el-tabs v-model="activeName1" @tab-click="handleClick">
+                                <el-tab-pane :label="$t('device.device-linkage.188958-1')" name="first"></el-tab-pane>
+                                <el-tab-pane :label="$t('device.device-linkage.188958-2')" name="second"></el-tab-pane>
+                                <el-tab-pane :label="$t('device.device-linkage.188958-3')" name="third"></el-tab-pane>
+                            </el-tabs>
+                            <el-input
+                                v-model="queryParams.deviceName"
+                                :placeholder="$t('device.device-edit.148398-2')"
+                                clearable
+                                size="small"
+                                prefix-icon="el-icon-search"
+                                style="margin-bottom: 20px; margin-right: 10px"
+                                @keydown.enter.native="getDeviceList"
+                            />
+                            <el-menu class="menu-wrap" ref="menu" :default-active="deviceId" :default-openeds="defaultOpeneds" @open="handleOpen" style="margin-right: 10px">
+                                <el-submenu index="1" ref="submenu" style="margin-left: -5px">
+                                    <template slot="title">
+                                        <i class="el-icon-menu"></i>
+                                        <span style="font-size: 14px; padding: 0">{{ $t('device.device-linkage.188958-5') }}</span>
+                                    </template>
+                                    <el-empty v-if="deviceList.length == 0" description="暂无设备"></el-empty>
+                                    <el-menu-item
+                                        v-for="item in deviceList"
+                                        :key="item.deviceId"
+                                        :index="item.deviceId"
+                                        class="custom-menu-item"
+                                        :label="item.devcieName"
+                                        :value="item.deviceId"
+                                        @click="handleTypeClick(item.deviceId)"
+                                        :style="`${deviceId == item.deviceId ? 'background-color: #e8f4ff;' : ''} `"
+                                    >
+                                        {{ item.deviceName }}
+                                    </el-menu-item>
+                                </el-submenu>
+                            </el-menu>
+                        </div>
+                    </el-card>
+                </el-col>
+                <el-col :span="19" :xs="24" style="padding: 0" v-loading="loading">
+                    <el-empty v-if="deviceList.length == 0" description="暂无数据"></el-empty>
+
+                    <el-card v-else style="margin: 6px">
+                        <div class="head-container">
+                            <el-tabs v-model="activeName" tab-position="top" @tab-click="tabChange" style="min-height: 400px">
+                                <el-tab-pane name="basic">
+                                    <span slot="label">{{ $t('device.device-edit.148398-0') }}</span>
+                                    <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+                                        <el-row :gutter="30">
+                                            <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                                                <el-form-item :label="$t('device.device-edit.148398-1')" prop="deviceName">
+                                                    <el-input v-model="form.deviceName" :placeholder="$t('device.device-edit.148398-2')">
+                                                        <el-button slot="append" @click="openSummaryDialog" v-if="form.deviceId != 0">{{ $t('device.device-edit.148398-3') }}</el-button>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item label="" prop="productName">
+                                                    <template slot="label">
+                                                        <span style="color: red">*</span>
+                                                        {{ $t('device.device-edit.148398-4') }}
+                                                    </template>
+                                                    <el-input readonly v-model="form.productName" :placeholder="$t('device.allot-import-dialog.060657-1')" disabled>
+                                                        <el-button slot="append" @click="selectProduct()" disabled>{{ $t('device.device-edit.148398-6') }}</el-button>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item label="" prop="serialNumber">
+                                                    <template slot="label">
+                                                        <span style="color: red">*</span>
+                                                        {{ $t('device.device-edit.148398-7') }}
+                                                    </template>
+                                                    <el-input v-model="form.serialNumber" :placeholder="$t('device.device-edit.148398-8')" disabled maxlength="32">
+                                                        <el-button v-if="form.deviceType !== 3" slot="append" @click="generateNum" :loading="genDisabled" disabled v-hasPermi="['iot:device:add']">
+                                                            {{ $t('device.device-edit.148398-9') }}
+                                                        </el-button>
+                                                        <el-button v-if="form.deviceType === 3" slot="append" @click="genSipID()" disabled v-hasPermi="['iot:device:add']">
+                                                            {{ $t('device.device-edit.148398-9') }}
+                                                        </el-button>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item v-if="openServerTip">
+                                                    <template>
+                                                        <el-alert type="info" show-icon :description="$t('device.device-edit.148398-10')"></el-alert>
+                                                    </template>
+                                                </el-form-item>
+                                                <el-form-item v-if="openTip">
+                                                    <template>
+                                                        <el-alert type="success" show-icon :description="$t('device.device-edit.148398-11')"></el-alert>
+                                                    </template>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-edit.148398-12')" prop="firmwareVersion">
+                                                    <el-input
+                                                        v-model="form.firmwareVersion"
+                                                        :placeholder="$t('device.device-edit.148398-13')"
+                                                        type="number"
+                                                        step="0.1"
+                                                        :disabled="form.status != 1 || form.deviceType === 3"
+                                                    >
+                                                        <template slot="prepend">Version</template>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-edit.148398-14')" prop="isSimulate">
+                                                    <el-switch v-model="form.isSimulate" active-text="" inactive-text="" :active-value="1" :inactive-value="0" disabled></el-switch>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-edit.148398-15')" prop="isShadow">
+                                                    <el-switch v-model="form.isShadow" active-text="" inactive-text="" :active-value="1" :inactive-value="0" disabled></el-switch>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-edit.148398-16')" prop="deviceStatus">
+                                                    <el-switch v-model="deviceStatus" active-text="" inactive-text="" disabled :active-value="1" :inactive-value="0" active-color="#F56C6C"></el-switch>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-edit.148398-17')" prop="remark">
+                                                    <el-input v-model="form.remark" type="textarea" :placeholder="$t('plzInput')" rows="1" />
+                                                </el-form-item>
+                                            </el-col>
+                                            <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                                                <el-form-item :label="$t('device.device-edit.148398-19')" prop="locationWay">
+                                                    <el-select v-model="form.locationWay" :placeholder="$t('device.device-linkage.188958-25')" clearable size="small" style="width: 100%" :disabled="form.deviceType === 3">
+                                                        <el-option v-for="dict in dict.type.iot_location_way" :key="dict.value" :label="dict.label" :value="Number(dict.value)" />
+                                                    </el-select>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-26')" prop="longitude">
+                                                    <el-input v-model="form.longitude" :placeholder="$t('device.device-linkage.188958-27')" type="number" disabled>
+                                                        <el-link slot="append" :underline="false" href="https://api.map.baidu.com/lbsapi/getpoint/index.html" target="_blank" :disabled="form.locationWay != 3">
+                                                            {{ $t('device.device-linkage.188958-28') }}
+                                                        </el-link>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-29')" prop="latitude">
+                                                    <el-input v-model="form.latitude" :placeholder="$t('device.device-linkage.188958-30')" type="number" disabled>
+                                                        <el-link slot="append" :underline="false" href="https://api.map.baidu.com/lbsapi/getpoint/index.html" target="_blank" :disabled="form.locationWay != 3">
+                                                            {{ $t('device.device-linkage.188958-28') }}
+                                                        </el-link>
+                                                    </el-input>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-31')" prop="networkAddress">
+                                                    <el-input v-model="form.networkAddress" :placeholder="$t('device.device-linkage.188958-32')" disabled />
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-33')" prop="networkIp">
+                                                    <el-input v-model="form.networkIp" :placeholder="$t('device.device-linkage.188958-34')" disabled />
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-35')" prop="activeTime">
+                                                    <el-date-picker
+                                                        clearable
+                                                        v-model="form.activeTime"
+                                                        type="date"
+                                                        value-format="yyyy-MM-dd"
+                                                        :placeholder="$t('device.device-linkage.188958-36')"
+                                                        disabled
+                                                        style="width: 100%"
+                                                    ></el-date-picker>
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-37')" prop="rssi">
+                                                    <el-input v-model="form.rssi" :placeholder="$t('device.device-linkage.188958-38')" disabled />
+                                                </el-form-item>
+                                                <el-form-item :label="$t('device.device-linkage.188958-39')" prop="remark" v-if="form.deviceId != 0">
+                                                    <dict-tag :options="dict.type.iot_device_status" :value="form.status" style="display: inline-block; margin-right: 8px" />
+                                                    <el-button size="small" @click="handleViewMqtt()">{{ $t('device.device-linkage.188958-40') }}</el-button>
+                                                    <el-button size="small" @click="openCodeDialog()">{{ $t('device.device-linkage.188958-41') }}</el-button>
+                                                </el-form-item>
+                                            </el-col>
+                                            <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8" v-if="form.deviceId != 0">
+                                                <div style="border: 1px solid #dfe4ed; border-radius: 5px; padding: 5px; text-align: center; line-height: 400px">
+                                                    <div id="map" style="height: 435px; width: 100%">{{ $t('device.device-linkage.188958-42') }}</div>
+                                                </div>
+                                            </el-col>
+                                        </el-row>
+                                    </el-form>
+                                    <!-- 选择产品 -->
+                                    <product-list ref="productList" :productId="form.productId" @productEvent="getProductData($event)" />
+
+                                    <sipid ref="sipidGen" :product="form" @addGenEvent="getSipIDData($event)" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="runningStatus" v-if="form.deviceType !== 3">
+                                    <span slot="label">{{ $t('device.device-edit.148398-42') }}</span>
+                                    <!-- <real-time-status ref="realTimeStatus" :device="form" @statusEvent="getDeviceStatusData($event)" /> -->
+                                    <running-status ref="runningStatus" :device="form" @statusEvent="getDeviceStatusData($event)" />
+                                </el-tab-pane>
+                                <el-tab-pane name="deviceSub" :disabled="form.deviceId == 0" v-if="form.deviceType == 2" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-43') }}</span>
+                                    <device-sub ref="deviceSub" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="sipChannel" :disabled="form.deviceId == 0" v-if="form.deviceType === 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-44') }}</span>
+                                    <channel ref="deviceChannel" :device="form" @playerEvent="getPlayerData($event)" />
+                                </el-tab-pane>
+
+                                <el-tab-pane :disabled="form.deviceId == 0" v-if="form.deviceType === 3" name="sipPlayer" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-45') }}</span>
+                                    <device-live-stream ref="deviceLiveStream" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane :disabled="form.deviceId == 0" v-if="form.deviceType === 3" name="sipVideo" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-46') }}</span>
+                                    <deviceVideo ref="deviceVideo" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="ossRecord" :disabled="form.deviceId == 0" v-if="form.deviceType === 3">
+                                    <span slot="label">{{ $t('device.device-edit.148398-79') }}</span>
+                                    <OssRecord ref="OssRecord" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceTimer" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-47') }}</span>
+                                    <device-timer ref="deviceTimer" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceUser" :disabled="form.deviceId == 0" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-48') }}</span>
+                                    <device-user ref="deviceUser" :device="form" @userEvent="getUserData($event)" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceLog" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-49') }}</span>
+                                    <device-log ref="deviceLog" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="alertUser" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
+                                    <span slot="label">{{ $t('device.device-edit.148398-80') }}</span>
+                                    <alert-user ref="alertUser" :device="form" />
+                                </el-tab-pane>
+                                <el-tab-pane name="deviceAlert" :disabled="form.deviceId == 0">
+                                    <span slot="label">{{ $t('device.device-edit.148398-81') }}</span>
+                                    <device-alert ref="deviceAlert" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceFuncLog" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-50') }}</span>
+                                    <device-func ref="deviceFuncLog" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceMonitor" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
+                                    <span slot="label">{{ $t('device.device-edit.148398-51') }}</span>
+                                    <device-monitor ref="deviceMonitor" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceStastic" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3">
+                                    <span slot="label">{{ $t('device.device-edit.148398-52') }}</span>
+                                    <device-statistic ref="deviceStatistic" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="deviceModbusTask" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-77') }}</span>
+                                    <device-modbus-task ref="deviceModbusTask" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="instructionParsing" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-76') }}</span>
+                                    <instruction-parsing ref="instructionParsing" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="scada" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3 && isShowScada == true" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-73') }}</span>
+                                    <device-scada ref="deviceScada" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="variable" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-74') }}</span>
+                                    <device-variable ref="deviceVariable" :device="form" />
+                                </el-tab-pane>
+
+                                <el-tab-pane name="inlineVideo" :disabled="form.deviceId == 0" v-if="form.deviceType !== 3" lazy>
+                                    <span slot="label">{{ $t('device.device-edit.148398-75') }}</span>
+                                    <device-inline-video ref="deviceInlineVideo" :sipRelationList="form.sipRelationList" />
+                                </el-tab-pane>
+
+                                <!-- 用于设置间距 -->
+                                <el-tab-pane disabled>
+                                    <span slot="label">
+                                        <div style="margin-top: 350px"></div>
+                                    </span>
+                                </el-tab-pane>
+
+                                <!-- <el-tab-pane name="device04" v-if="form.deviceType !== 3" disabled>
+                <span slot="label">
+                    <el-tooltip class="item" effect="dark" content="用于查看发送的指令,设备是否已经响应" placement="right-start">
+                        <el-button type="warning" size="mini" @click="deviceSynchronization()" :disabled="form.deviceId == 0">数据同步</el-button>
+                    </el-tooltip>
+                </span>
+            </el-tab-pane> -->
+                            </el-tabs>
+
+                            <!-- 设备配置JSON -->
+                            <el-dialog :title="$t('device.device-linkage.188958-54')" :visible.sync="openSummary" width="700px" append-to-body>
+                                <el-row :gutter="20">
+                                    <el-col :span="14">
+                                        <div style="border: 1px solid #ccc; margin-top: -15px; height: 350px; width: 360px; overflow: scroll">
+                                            <json-viewer :value="summary" :expand-depth="10" copyable>
+                                                <template v-slot:copy>{{ $t('device.device-linkage.188958-55') }}</template>
+                                            </json-viewer>
+                                        </div>
+                                    </el-col>
+                                    <el-col :span="10">
+                                        <div style="border: 1px solid #ccc; width: 200px; text-align: center; margin-left: 20px; margin-top: -10px">
+                                            <vue-qr :text="qrText" :size="200"></vue-qr>
+                                            <div style="padding-bottom: 10px">{{ $t('device.device-linkage.188958-56') }}</div>
+                                        </div>
+                                    </el-col>
+                                </el-row>
+                                <div slot="footer" class="dialog-footer">
+                                    <el-button type="info" @click="closeSummaryDialog">{{ $t('device.device-linkage.188958-57') }}</el-button>
+                                </div>
+                            </el-dialog>
+                            <!-- {{ $t('device.device-linkage.188958-41') }} -->
+                            <el-dialog :visible.sync="openCode" width="300px" append-to-body>
+                                <div style="border: 1px solid #ccc; width: 220px; text-align: center; margin: 0 auto; margin-top: -15px">
+                                    <vue-qr :text="qrText" :size="200"></vue-qr>
+                                    <div style="padding-bottom: 10px">{{ $t('device.device-linkage.188958-56') }}</div>
+                                </div>
+                            </el-dialog>
+                            <el-dialog :title="$t('device.device-linkage.188958-58')" :visible.sync="openViewMqtt" width="600px" append-to-body>
+                                <el-form ref="listQuery" :model="listQuery" :rules="rules" label-width="150px">
+                                    <el-form-item label="clientId" prop="clientId">
+                                        <el-input v-model="listQuery.clientId" disabled style="width: 80%" />
+                                    </el-form-item>
+                                    <el-form-item label="username" prop="username">
+                                        <el-input v-model="listQuery.username" disabled style="width: 80%" />
+                                    </el-form-item>
+                                    <el-form-item label="passwd" prop="passwd">
+                                        <el-input clearable v-model="listQuery.passwd" disabled style="width: 80%"></el-input>
+                                    </el-form-item>
+                                    <el-form-item label="port" prop="port">
+                                        <el-input clearable v-model="listQuery.port" disabled style="width: 80%"></el-input>
+                                    </el-form-item>
+                                </el-form>
+                                <div slot="footer" class="dialog-footer">
+                                    <el-button class="btns" type="primary" @click="doCopy(2)">{{ $t('device.device-linkage.188958-59') }}</el-button>
+                                    <el-button @click="closeSummaryDialog">{{ $t('device.device-linkage.188958-57') }}</el-button>
+                                </div>
+                            </el-dialog>
+                        </div>
+                    </el-card>
+                </el-col>
+            </el-row>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import JsonViewer from 'vue-json-viewer';
+import 'vue-json-viewer/style.css';
+import productList from './product-list';
+import deviceLog from './device-log';
+import deviceUser from './device-user';
+import runningStatus from './running-status';
+import deviceMonitor from './device-monitor';
+import deviceStatistic from './device-statistic';
+import deviceTimer from './device-timer';
+import channel from '../sip/channel';
+import player from '@/views/components/player/player.vue';
+import deviceVideo from '@/views/components/player/deviceVideo.vue';
+import deviceLiveStream from '@/views/components/player/deviceLiveStream';
+import sipid from '../sip/sipidGen.vue';
+import deviceFuncLog from './device-functionlog';
+import deviceSub from './device-sub';
+import alertUser from './alert-user';
+import vueQr from 'vue-qr';
+import { loadBMap } from '@/utils/map.js';
+import { deviceSynchronization, getDevice, addDevice, updateDevice, generatorDeviceNum, getMqttConnect } from '@/api/iot/device';
+import { getDeviceUser } from '@/api/iot/deviceuser';
+import { getUserId } from '@/utils/auth';
+import { getDeviceRunningStatus } from '@/api/iot/device';
+import { cacheJsonThingsModel } from '@/api/iot/model';
+import DeviceFunc from '@/views/iot/device/device-functionlog';
+import { getDeviceTemp } from '@/api/iot/temp';
+import RealTimeStatus from '@/views/iot/device/realTime-status';
+import { clientOut } from '@/api/iot/netty';
+import { deptsTreeSelect } from '@/api/system/user';
+import { listDeviceShort } from '@/api/iot/device';
+import OssRecord from '@/views/iot/record/record-oss.vue';
+import defaultSettings from '@/settings';
+import instructionParsing from './instruction-parsing';
+import deviceModbusTask from './device-modbus-task';
+import deviceScada from './device-scada';
+import deviceVariable from './device-variable';
+import deviceInlineVideo from './device-inline-video';
+import deviceAlert from './device-alert';
+
+export default {
+    name: 'DeviceEdit',
+    dicts: ['iot_device_status', 'iot_location_way'],
+    components: {
+        OssRecord,
+        RealTimeStatus,
+        DeviceFunc,
+        deviceLog,
+        deviceUser,
+        deviceMonitor,
+        deviceStatistic,
+        runningStatus,
+        productList,
+        deviceTimer,
+        deviceFuncLog,
+        deviceVideo,
+        player,
+        deviceLiveStream,
+        deviceSub,
+        alertUser,
+        JsonViewer,
+        vueQr,
+        channel,
+        sipid,
+        deviceModbusTask,
+        instructionParsing,
+        deviceScada,
+        deviceVariable,
+        deviceInlineVideo,
+        deviceAlert,
+    },
+    watch: {
+        // 根据名称筛选机构树
+        deptName(val) {
+            this.$refs.tree.filter(val);
+        },
+        activeName(val) {
+            if (val == 'deviceStastic') {
+                this.$nextTick(() => {
+                    // TODO 重置统计表格的尺寸
+                });
+            }
+        },
+    },
+    computed: {
+        deviceStatus: {
+            set(val) {
+                if (val == 1) {
+                    // 1-未激活,2-禁用,3-在线,4-离线
+                    this.form.status = 2;
+                } else if (val == 0) {
+                    this.form.status = 4;
+                } else {
+                    this.form.status = this.oldDeviceStatus;
+                }
+            },
+            get() {
+                if (this.form.status == 2) {
+                    return 1;
+                }
+                return 0;
+            },
+        },
+    },
+    data() {
+        return {
+            // 二维码内容
+            qrText: 'yanfan',
+            // 打开设备配置对话框
+            openSummary: false,
+            //二维码
+            openCode: false,
+            deptName: '',
+            deviceName1: '',
+            openViewMqtt: false,
+            // 生成设备编码是否禁用
+            genDisabled: false,
+            // 选中选项卡
+            activeName: 'basic',
+            //查看mqtt参数
+            mqttList: [],
+            activeName1: 'first', // 当前激活的标签页
+            // 遮罩层
+            loading: true,
+            // 设备开始状态
+            oldDeviceStatus: null,
+            deviceId: 0,
+            channelId: '',
+            // 表单参数
+            form: {
+                productId: 0,
+                status: 1,
+                locationWay: 1,
+                firmwareVersion: 1.0,
+                serialNumber: '',
+                deviceType: 1,
+                isSimulate: 0,
+            },
+            //默认展开菜单项
+            defaultOpeneds: ['1'],
+            defaultProps: {
+                children: 'children',
+                label: 'label',
+            },
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 999,
+                deptId: undefined,
+                status: '',
+                deviceName: '',
+            },
+            deviceList: [],
+            // 机构树选项
+            deptOptions: undefined,
+            //mqtt参数查看
+            listQuery: {
+                clientId: 0,
+                username: '',
+                passwd: '',
+                port: '',
+            },
+            openTip: false,
+            openServerTip: false,
+            serverType: 1,
+            //用于判断是否是设备组(modbus)
+            isSubDev: false,
+            // 设备摘要
+            summary: [],
+            // 地址
+            baseUrl: process.env.VUE_APP_BASE_API,
+            // 地图相关
+            map: null,
+            mk: null,
+            latitude: '',
+            longitude: '',
+            //组态相关按钮是否显示,true显示,false不显示
+            isShowScada: defaultSettings.isShowScada,
+            // 表单校验
+            rules: {
+                deviceName: [
+                    {
+                        required: true,
+                        message: this.$t('device.device-linkage.188958-60'),
+                        trigger: 'blur',
+                    },
+                    {
+                        min: 2,
+                        max: 32,
+                        message: this.$t('device.device-linkage.188958-61'),
+                        trigger: 'blur',
+                    },
+                ],
+                firmwareVersion: [
+                    {
+                        required: true,
+                        message: this.$t('device.device-linkage.188958-62'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+            isMediaDevice: false,
+        };
+    },
+    created() {
+        this.isSubDev = this.$route.query.isSubDev == 1 ? true : false;
+        this.getDeptTree();
+        this.$nextTick(() => {
+            this.getDeviceList();
+        });
+    },
+    activated() {
+        // 跳转选项卡
+        let activeName = this.$route.query.activeName;
+        if (activeName != null && activeName != '') {
+            this.activeName = activeName;
+        }
+    },
+    destroyed() {
+        // 取消订阅主题
+        this.mqttUnSubscribe(this.form);
+    },
+    methods: {
+        /* 连接Mqtt消息服务器 */
+        async connectMqtt() {
+            if (this.$mqttTool.client == null) {
+                await this.$mqttTool.connect(this.vuex_token);
+            }
+            // 删除所有message事件监听器
+            this.$mqttTool.client.removeAllListeners('message');
+            // 添加message事件监听器
+            this.mqttCallback();
+        },
+        //切换设备在线状态的事件
+        handleClick(tab, event) {
+            // 点击标签页的逻辑处理
+            if (tab.name === 'first') {
+                //全部包括在线,离线,未激活和禁用状态
+                this.queryParams.status = '';
+                this.getDeviceList();
+            } else if (tab.name === 'second') {
+                this.queryParams.status = 3;
+                this.getDeviceList();
+            } else if (tab.name === 'third') {
+                this.queryParams.status = 4;
+                this.getDeviceList();
+            }
+        },
+        handleOpen(index, indexPath) {
+            this.defaultOpeneds = [index];
+        },
+        // 筛选节点
+        filterNode(value, data) {
+            if (!value) return true;
+            return data.label.indexOf(value) !== -1;
+        },
+        /** 查询机构下拉树结构 */
+        getDeptTree() {
+            deptsTreeSelect().then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        // 节点单击事件
+        handleNodeClick(data) {
+            this.queryParams.deptId = data.id;
+            this.getDeviceList();
+        },
+        /* Mqtt回调处理  */
+        mqttCallback() {
+            this.$mqttTool.client.on('message', (topic, message, buffer) => {
+                let topics = topic.split('/');
+                let productId = topics[1];
+                let deviceNum = topics[2];
+                message = JSON.parse(message.toString());
+                if (!message) {
+                    return;
+                }
+                if (topics[3] == 'status' || topics[2] == 'status') {
+                    console.log(this.$t('device.device-linkage.188958-63'), topic);
+                    console.log(this.$t('device.device-linkage.188958-64'), message);
+                    // 更新列表中设备的状态
+                    if (this.form.serialNumber == deviceNum) {
+                        this.oldDeviceStatus = message.status;
+                        this.form.status = message.status;
+                        this.form.isShadow = message.isShadow;
+                        this.form.rssid = message.rssid;
+                    }
+                }
+                //不是modbus不转发到子页面,其他设备的页面有回调方法
+                if (this.isSubDev) {
+                    /*发送设备上报到子模块*/
+                    if (topic.endsWith('ws/service')) {
+                        this.$busEvent.$emit('updateData', {
+                            serialNumber: topics[2],
+                            productId: this.form.productId,
+                            data: message,
+                        });
+                    }
+                    if (topic.endsWith('service/reply')) {
+                        this.$busEvent.$emit('updateLog', {
+                            serialNumber: topics[2],
+                            productId: this.form.productId,
+                            data: message,
+                        });
+                    }
+                }
+                /*发送设备上报到子模块*/
+                if (topic.endsWith('ws/post/simulate')) {
+                    this.$busEvent.$emit('logData', {
+                        serialNumber: topics[1],
+                        productId: this.form.productId,
+                        data: message,
+                    });
+                }
+            });
+        },
+        /** Mqtt订阅主题 */
+        mqttSubscribe(device) {
+            // 订阅当前设备状态和实时监测
+            let topicStatus = '/' + device.productId + '/' + device.serialNumber + '/status/post';
+            let topicProperty = '/' + device.productId + '/' + device.serialNumber + '/property/post';
+            let topicFunction = '/' + device.productId + '/' + device.serialNumber + '/function/post';
+            let topicMonitor = '/' + device.productId + '/' + device.serialNumber + '/monitor/post';
+            let topicReply = '/' + device.productId + '/' + device.serialNumber + '/service/reply';
+            let topics = [];
+            let serviceTop = '/' + device.productId + '/' + device.serialNumber + '/ws/service';
+            topics.push(serviceTop);
+            topics.push(topicStatus);
+            topics.push(topicFunction);
+            topics.push(topicMonitor);
+            topics.push(topicReply);
+            /*modbus设备不订阅此topic*/
+            if (!this.isSubDev) {
+                // topics.push(topicProperty);
+            }
+            this.$mqttTool.subscribe(topics);
+        },
+        /** Mqtt取消订阅主题 */
+        mqttUnSubscribe(device) {
+            // 订阅当前设备状态和实时监测
+            let topicStatus = '/' + device.productId + '/' + device.serialNumber + '/status/post';
+            let topicProperty = '/' + device.productId + '/' + device.serialNumber + '/property/post';
+            let topicFunction = '/' + device.productId + '/' + device.serialNumber + '/function/post';
+            let topicMonitor = '/' + device.productId + '/' + device.serialNumber + '/monitor/post';
+            let topicReply = '/' + device.productId + '/' + device.serialNumber + '/service/reply';
+            let topics = [];
+            let serviceTop = '/' + device.productId + '/' + device.serialNumber + '/ws/service';
+            topics.push(serviceTop);
+
+            topics.push(topicStatus);
+            topics.push(topicFunction);
+            topics.push(topicMonitor);
+            topics.push(topicReply);
+            /*modbus设备不订阅此topic*/
+            if (!this.isSubDev) {
+                /*通过网关再转发*/
+                // topics.push(topicProperty);
+            }
+            this.$mqttTool.unsubscribe(topics);
+        },
+        // 获取子组件订阅的设备状态
+        getDeviceStatusData(status) {
+            this.form.status = status;
+        },
+        // 获取直播子组件传递的激活选项卡名称
+        getPlayerData(data) {
+            this.activeName = data.tabName;
+            this.channelId = data.channelId;
+            if (this.channelId) {
+                this.$refs.deviceLiveStream.channelId = this.channelId;
+                this.$refs.deviceLiveStream.changeChannel();
+            }
+        },
+        //获取机构所对应的设备列表
+        getDeviceList() {
+            this.loading = true;
+            if (this.queryParams.deptId == undefined) {
+                //默认机构
+                this.queryParams.deptId = 100;
+            }
+            listDeviceShort(this.queryParams).then((response) => {
+                this.deviceList = response.rows;
+                if (this.deviceList.length == 0) {
+                    return;
+                } else {
+                    this.deviceId = this.deviceList[0].deviceId;
+                    this.getDevice();
+                    // 订阅消息
+                    if (this.deviceList && this.deviceList.length > 0) {
+                        this.mqttSubscribe(this.deviceList);
+                    }
+                }
+            });
+        },
+        /** 选项卡改变事件*/
+        tabChange(panel) {
+            if (panel.name == 'runningStatus') {
+                console.log('this.$refs[runningStatus]', this.$refs['runningStatus']);
+
+                this.$refs['runningStatus'].chartResize();
+            }
+            if (this.form.deviceType == 3 && panel.name != 'deviceReturn') {
+                if (panel.name === 'sipPlayer') {
+                    this.$refs.deviceVideo.destroy();
+                    if (this.channelId) {
+                        this.$refs.deviceLiveStream.channelId = this.channelId;
+                        this.$refs.deviceLiveStream.changeChannel();
+                    }
+                    if (this.$refs.deviceLiveStream.channelId) {
+                        this.$refs.deviceLiveStream.changeChannel();
+                    }
+                } else if (panel.name === 'sipVideo') {
+                    this.$refs.deviceLiveStream.destroy();
+                    if (this.$refs.deviceVideo.channelId && this.$refs.deviceVideo.queryDate) {
+                        this.$refs.deviceVideo.loadDevRecord();
+                    }
+                } else if (panel.name === 'sipChannel') {
+                    this.$refs.deviceChannel.getList();
+                }
+                //关闭直播流
+                if (panel.name !== 'sipPlayer') {
+                    if (this.$refs.deviceLiveStream.playing) {
+                        this.$refs.deviceLiveStream.closeDestroy(false);
+                    }
+                }
+                //关闭录像流
+                if (panel.name !== 'sipVideo') {
+                    if (this.$refs.deviceVideo.playing) {
+                        this.$refs.deviceVideo.closeDestroy();
+                    }
+                }
+            }
+            this.$nextTick(() => {
+                // 获取监测统计数据
+                if (panel.name === 'deviceStastic') {
+                    this.$refs.deviceStatistic.getListHistory();
+                } else if (panel.name === 'deviceTimer') {
+                    this.$refs.deviceTimer.getList();
+                } else if (panel.name === 'deviceSub') {
+                    if (this.form.serialNumber) {
+                        this.$refs.deviceSub.queryParams.gwDevCode = this.form.serialNumber;
+                        this.$refs.deviceSub.getList();
+                    }
+                }
+            });
+        },
+        /** 数据同步*/
+        deviceSynchronization() {
+            deviceSynchronization(this.form.serialNumber).then(async (response) => {
+                // 获取缓存物模型
+                response.data.cacheThingsModel = await this.getCacheThingsModdel(response.data.productId);
+                // 获取设备运行状态
+                response.data.thingsModels = await this.getDeviceStatus(this.form);
+                // 格式化物模型,拆分出监测值,数组添加前缀
+                this.formatThingsModel(response.data);
+                this.form = response.data;
+                // 选项卡切换
+                this.activeName = 'runningStatus';
+                this.oldDeviceStatus = this.form.status;
+                this.loadMap();
+            });
+        },
+        handleTypeClick(deviceId) {
+            this.deviceId = deviceId;
+            this.activeName = 'basic';
+            this.getDevice();
+        },
+        /**获取设备详情*/
+        getDevice(deviceId) {
+            this.loading = true;
+            getDevice(this.deviceId).then(async (response) => {
+                // 获取设备状态和物模型
+                this.getDeviceStatusWitchThingsModel(response);
+            });
+        },
+        /**用户是否拥有分享设备权限*/
+        // hasShrarePerm(permission) {
+        //     if (this.form.isOwner == 0) {
+        //         // 分享设备权限
+        //         if (this.form.userPerms.indexOf(permission) == -1) {
+        //             return false;
+        //         }
+        //     }
+        //     return true;
+        // },
+        /** 获取缓存物模型*/
+        getCacheThingsModdel(productId) {
+            return new Promise((resolve, reject) => {
+                cacheJsonThingsModel(productId)
+                    .then((response) => {
+                        resolve(JSON.parse(response.data));
+                    })
+                    .catch((error) => {
+                        reject(error);
+                    });
+            });
+        },
+        /**获取设备运行状态*/
+        getDeviceStatus(data) {
+            const params = {
+                deviceId: data.deviceId,
+                slaveId: data.slaveId,
+            };
+            return new Promise((resolve, reject) => {
+                getDeviceRunningStatus(params)
+                    .then((response) => {
+                        resolve(response.data.thingsModels);
+                    })
+                    .catch((error) => {
+                        reject(error);
+                    });
+            });
+        },
+        formatThingsModel(data) {
+            data.chartList = [];
+            data.monitorList = [];
+            data.staticList = [];
+            // 物模型格式化
+            for (let i = 0; i < data.thingsModels.length; i++) {
+                // 数字类型设置默认值并转换未数值
+                if (data.thingsModels[i].datatype.type == 'integer' || data.thingsModels[i].datatype.type == 'decimal') {
+                    if (data.thingsModels[i].shadow == '') {
+                        data.thingsModels[i].shadow = Number(data.thingsModels[i].datatype.min);
+                    } else {
+                        data.thingsModels[i].shadow = Number(data.thingsModels[i].shadow);
+                    }
+                }
+
+                // 物模型分类放置
+                if (data.thingsModels[i].datatype.type == 'array') {
+                    if (data.thingsModels[i].datatype.arrayType == 'object') {
+                        for (let k = 0; k < data.thingsModels[i].datatype.arrayParams.length; k++) {
+                            for (let j = 0; j < data.thingsModels[i].datatype.arrayParams[k].length; j++) {
+                                // 数组元素中参数ID添加前缀,例如:array_00_
+                                let index = k > 9 ? String(k) : '0' + k;
+                                let prefix = 'array_' + index + '_';
+                                data.thingsModels[i].datatype.arrayParams[k][j].id = prefix + data.thingsModels[i].datatype.arrayParams[k][j].id;
+                                // 图表、实时监测、监测统计分类放置
+                                if (data.thingsModels[i].datatype.arrayParams[k][j].isChart == 1) {
+                                    // 图表
+                                    data.thingsModels[i].datatype.arrayParams[k][j].name = '[' + data.thingsModels[i].name + (k + 1) + '] ' + data.thingsModels[i].datatype.arrayParams[k][j].name;
+                                    data.thingsModels[i].datatype.arrayParams[k][j].datatype.arrayType = 'object';
+                                    data.chartList.push(data.thingsModels[i].datatype.arrayParams[k][j]);
+                                    if (data.thingsModels[i].datatype.arrayParams[k][j].isHistory == 1) {
+                                        // 监测统计
+                                        data.staticList.push(data.thingsModels[i].datatype.arrayParams[k][j]);
+                                    }
+                                    if (data.thingsModels[i].datatype.arrayParams[k][j].isMonitor == 1) {
+                                        // 实时监测
+                                        data.monitorList.push(data.thingsModels[i].datatype.arrayParams[k][j]);
+                                    }
+                                    data.thingsModels[i].datatype.arrayParams[k].splice(j--, 1);
+                                }
+                            }
+                        }
+                    } else {
+                        // 字符串拆分为物模型数组 model=id/name/type/isReadonly/value/shadow
+                        let values = data.thingsModels[i].value != '' ? data.thingsModels[i].value.split(',') : [];
+                        let shadows = data.thingsModels[i].shadow != '' ? data.thingsModels[i].shadow.split(',') : [];
+                        for (let j = 0; j < data.thingsModels[i].datatype.arrayCount; j++) {
+                            if (!data.thingsModels[i].datatype.arrayModel) {
+                                data.thingsModels[i].datatype.arrayModel = [];
+                            }
+                            // 数组里面的ID需要添加前缀和索引,例如:array_00_temperature
+                            let index = j > 9 ? String(j) : '0' + j;
+                            let prefix = 'array_' + index + '_';
+                            data.thingsModels[i].datatype.arrayModel[j] = {
+                                id: prefix + data.thingsModels[i].id,
+                                name: data.thingsModels[i].name,
+                                type: data.thingsModels[i].type,
+                                isReadonly: data.thingsModels[i].isReadonly,
+                                value: values[j] ? values[j] : '',
+                                shadow: shadows[j] ? shadows[j] : '',
+                            };
+                        }
+                    }
+                } else if (data.thingsModels[i].datatype.type == 'object') {
+                    for (let j = 0; j < data.thingsModels[i].datatype.params.length; j++) {
+                        // 图表、实时监测、监测统计分类放置
+                        if (data.thingsModels[i].datatype.params[j].isChart == 1) {
+                            // 图表
+                            data.thingsModels[i].datatype.params[j].name = '[' + data.thingsModels[i].name + '] ' + data.thingsModels[i].datatype.params[j].name;
+                            data.chartList.push(data.thingsModels[i].datatype.params[j]);
+                            if (data.thingsModels[i].datatype.params[j].isHistory == 1) {
+                                // 监测统计
+                                data.staticList.push(data.thingsModels[i].datatype.params[j]);
+                            }
+                            if (data.thingsModels[i].datatype.params[j].isMonitor == 1) {
+                                // 实时监测
+                                data.monitorList.push(data.thingsModels[i].datatype.params[j]);
+                            }
+                            data.thingsModels[i].datatype.params.splice(j--, 1);
+                        }
+                    }
+                } else if (data.thingsModels[i].isChart == 1) {
+                    // // 图表、实时监测、监测统计分类放置
+                    data.chartList.push(data.thingsModels[i]);
+                    if (data.thingsModels[i].isHistory == 1) {
+                        // 监测统计
+                        data.staticList.push(data.thingsModels[i]);
+                    }
+                    if (data.thingsModels[i].isMonitor == 1) {
+                        // 实时监测
+                        data.monitorList.push(data.thingsModels[i]);
+                    }
+                    // 使用i--解决索引变更问题
+                    data.thingsModels.splice(i--, 1);
+                }
+            }
+        },
+        /**加载地图*/
+        loadMap() {
+            this.$nextTick(() => {
+                loadBMap().then(() => {
+                    this.getmap();
+                });
+            });
+        },
+        /** 返回按钮 */
+        goBack() {
+            const obj = {
+                path: '/1/iot/device',
+                query: {
+                    t: Date.now(),
+                    pageNum: this.$route.query.pageNum,
+                },
+            };
+            this.$tab.closeOpenPage(obj);
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                deviceId: 0,
+                deviceName: null,
+                productId: null,
+                productName: null,
+                userId: null,
+                userName: null,
+                tenantId: null,
+                tenantName: null,
+                serialNumber: '',
+                firmwareVersion: 1.0,
+                status: 1,
+                rssi: null,
+                networkAddress: null,
+                networkIp: null,
+                longitude: null,
+                latitude: null,
+                activeTime: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+                locationWay: 1,
+                clientId: 0,
+            };
+            this.deviceStatus = 0;
+            this.resetForm('form');
+        },
+        /** 提交按钮 */
+        async submitForm() {
+            if (this.form.serialNumber == null || this.form.serialNumber == 0) {
+                this.$modal.alertError(this.$t('device.device-linkage.188958-65'));
+                return;
+            }
+            let reg = /^[0-9a-zA-Z]+$/;
+            if (!reg.test(this.form.serialNumber)) {
+                this.$modal.alertError(this.$t('device.device-linkage.188958-66'));
+                return;
+            }
+            if (this.form.productId == null || this.form.productId == 0) {
+                this.$modal.alertError(this.$t('device.device-linkage.188958-67'));
+                return;
+            }
+
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    if (this.form.deviceId != 0) {
+                        updateDevice(this.form).then((response) => {
+                            if (response.data == 0) {
+                                this.$modal.alertError(response.msg);
+                            } else {
+                                this.$modal.alertSuccess(this.$t('device.device-linkage.188958-68'));
+                                this.form = JSON.parse(JSON.stringify(this.form));
+                                this.loadMap();
+                                //是否设备设置为禁用状态,则踢出设备
+                                if (this.form.status === 2) {
+                                    const params = { clientId: this.form.serialNumber };
+                                    clientOut(params).then((res) => {});
+                                }
+                            }
+                        });
+                    } else {
+                        addDevice(this.form).then(async (response) => {
+                            // 获取设备状态
+                            await this.getDeviceStatusWitchThingsModel(response);
+                            if (this.form.deviceId == null || this.form.deviceId == 0) {
+                                this.$modal.alertError(this.$t('device.device-linkage.188958-69'));
+                            } else {
+                                if (this.form.status == 2) {
+                                    this.deviceStatus = 1;
+                                }
+
+                                this.$modal.alertSuccess(this.$t('device.device-linkage.188958-70'));
+                                this.loadMap();
+                            }
+                        });
+                    }
+                }
+            });
+        },
+        /** 获取设备状态和物模型 **/
+        async getDeviceStatusWitchThingsModel(response) {
+            this.loading = false;
+            // 获取缓存物模型
+            response.data.cacheThingsModel = await this.getCacheThingsModdel(response.data.productId);
+            // 获取设备运行状态
+            response.data.thingsModels = await this.getDeviceStatus(response.data);
+            //分享设备过滤没有权限的物模型
+            // if (response.data.isOwner == 0) {
+            //     for (let i = 0; i < response.data.thingsModels.length; i++) {
+            //         if (response.data.userPerms.indexOf(response.data.thingsModels[i].id) == -1) {
+            //             response.data.thingsModels.splice(i--, 1);
+            //         }
+            //     }
+            // }
+            // 格式化物模型,拆分出监测值,数组添加前缀
+            this.formatThingsModel(response.data);
+            this.form = response.data;
+            // 解析设备摘要
+            if (this.form.summary != null && this.form.summary != '') {
+                this.summary = JSON.parse(this.form.summary);
+            }
+            this.isSubDev = this.form.subDeviceList && this.form.subDeviceList.length > 0;
+            this.oldDeviceStatus = this.form.status;
+            this.loadMap();
+            //Mqtt订阅
+            this.connectMqtt();
+            this.mqttSubscribe(this.form);
+        },
+        /**选择产品 */
+        selectProduct() {
+            this.$refs.productList.open = true;
+            this.$refs.productList.getDeviceList();
+        },
+        genSipID() {
+            this.$refs.sipidGen.open = true;
+        },
+        /**获取选中的产品 */
+        getProductData(product) {
+            this.form.productId = product.productId;
+            this.form.productName = product.productName;
+            this.form.deviceType = product.deviceType;
+            this.getDeviceTemp();
+            this.form.tenantId = product.tenantId;
+            this.form.tenantName = product.tenantName;
+            if (product.transport === 'TCP') {
+                this.openServerTip = true;
+                this.serverType = 3;
+            } else {
+                this.openServerTip = false;
+                this.serverType = 1;
+            }
+        },
+        getSipIDData(devsipid) {
+            this.form.serialNumber = devsipid;
+        },
+        getDeviceTemp(productId) {
+            getDeviceTemp(this.form).then((response) => {
+                if (response.data && this.form.deviceType == 2) {
+                    this.openTip = true;
+                } else {
+                    this.openTip = false;
+                }
+            });
+        },
+        // 获取选中的用户
+        getUserData(user) {},
+        /**关闭物模型 */
+        openSummaryDialog() {
+            let json = {
+                type: 1, // 1=扫码关联设备
+                deviceNumber: this.form.serialNumber,
+                productId: this.form.productId,
+                // productName: this.form.productName,
+            };
+            this.qrText = JSON.stringify(json);
+            this.openSummary = true;
+        },
+        /**关闭物模型 */
+        closeSummaryDialog() {
+            this.openSummary = false;
+            this.openViewMqtt = false;
+        },
+        doCopy(type) {
+            if (type == 2) {
+                const input = document.createElement('input');
+                input.value = '{clientId:' + this.listQuery.clientId + ',username:' + this.listQuery.username + ',passwd:' + this.listQuery.passwd + ',port:' + this.listQuery.port + '}';
+                document.body.appendChild(input);
+                input.select(); //选中输入框
+                document.execCommand('Copy'); //复制当前选中文本到前切板
+                document.body.removeChild(input);
+                this.$message.success(this.$t('device.device-linkage.188958-71'));
+            }
+        },
+        openCodeDialog() {
+            let json = {
+                type: 1, // 1=扫码关联设备
+                deviceNumber: this.form.serialNumber,
+                productId: this.form.productId,
+                productName: this.form.productName,
+            };
+            this.qrText = JSON.stringify(json);
+            this.openCode = true;
+        },
+        // 地图定位
+        getmap() {
+            this.map = new BMap.Map('map');
+            let point = null;
+            if (this.form.longitude != null && this.form.longitude != '' && this.form.latitude != null && this.form.latitude != '') {
+                point = new BMap.Point(this.form.longitude, this.form.latitude);
+            } else {
+                point = new BMap.Point(116.404, 39.915);
+            }
+            this.map.centerAndZoom(point, 19);
+            this.map.enableScrollWheelZoom(true); // 开启鼠标滚轮缩放
+            this.map.addControl(new BMap.NavigationControl());
+            // 标注设备位置
+            this.mk = new BMap.Marker(point);
+            this.map.addOverlay(this.mk);
+            this.map.panTo(point);
+        },
+        // 生成随机字母和数字
+        generateNum() {
+            if (!this.form.productId || this.form.productId == 0) {
+                this.$modal.alertError(this.$t('device.device-linkage.188958-72'));
+                return;
+            }
+            this.genDisabled = true;
+            const params = { type: this.serverType };
+            generatorDeviceNum(params).then((response) => {
+                this.form.serialNumber = response.data;
+                this.genDisabled = false;
+            });
+        },
+        //mqtt参数查看
+        handleViewMqtt() {
+            this.openViewMqtt = true;
+            this.loading = true;
+            const params = {
+                deviceId: this.form.deviceId,
+            };
+            getMqttConnect(params).then((response) => {
+                if (response.code == 200) {
+                    this.listQuery = response.data;
+                    this.loading = false;
+                }
+            });
+        },
+    },
+};
+</script>
+<style lang="scss" scoped>
+.head-container {
+    height: 830px;
+    overflow-x: hidden;
+    overflow-y: auto;
+}
+
+.menu-wrap {
+    margin-top: -5px;
+    height: 345px;
+    overflow-x: auto;
+    overflow-y: auto;
+    padding-left: 0 !important;
+}
+
+.menu-wrap .el-submenu__title {
+    padding-left: 0 !important;
+}
+
+.custom-menu-item {
+    font-size: 12px;
+    /* 设置字体大小 */
+    color: #000;
+    margin-top: -10px;
+    margin-left: -15px;
+}
+</style>

+ 330 - 0
src/views/pms/video_center/device/device-log.vue

@@ -0,0 +1,330 @@
+<template>
+    <div class="device-detail-page">
+        <el-card class="main-card" style="padding: 0" body-style="padding-bottom: 0">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+                <el-form-item :label="$t('device.device-log.798283-0')" prop="logType">
+                    <el-select v-model="queryParams.logType" :placeholder="$t('device.device-log.798283-1')" clearable size="small">
+                        <el-option v-for="dict in dict.type.iot_event_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-log.798283-2')" prop="identity">
+                    <el-input v-model="queryParams.identity" :placeholder="$t('device.device-log.798283-3')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item :label="$t('device.device-log.798283-4')">
+                    <el-date-picker
+                        v-model="daterangeTime"
+                        size="small"
+                        style="width: 240px"
+                        value-format="yyyy-MM-dd"
+                        type="daterange"
+                        range-separator="-"
+                        :start-placeholder="$t('device.device-log.798283-5')"
+                        :end-placeholder="$t('device.device-log.798283-6')"
+                    ></el-date-picker>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.device-log.798283-7') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.device-log.798283-8') }}</el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+        <el-card class="main-card" body-style="padding-top: 0">
+            <el-table :border="false" v-loading="loading" :data="deviceLogList" size="mini">
+                <el-table-column :label="$t('device.device-log.798283-9')" align="center" prop="logType" width="120">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_event_type" :value="scope.row.logType" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-log.798283-10')" align="center" prop="logType" width="120">
+                    <template slot-scope="scope">
+                        <el-tag type="primary" v-if="scope.row.mode == 1">{{ $t('device.device-log.798283-11') }}</el-tag>
+                        <el-tag type="success" v-else-if="scope.row.mode == 2">{{ $t('device.device-log.798283-12') }}</el-tag>
+                        <el-tag type="info" v-else>{{ $t('device.device-log.798283-13') }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-log.798283-14')" align="center" prop="createTime" width="150">
+                    <template slot-scope="scope">
+                        <span>{{ scope.row.createTime }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-log.798283-2')" align="center" prop="identity" />
+                <el-table-column :label="$t('device.device-log.798283-15')" align="left" header-align="center" prop="logValue">
+                    <template slot-scope="scope">
+                        <div v-html="formatValueDisplay(scope.row)"></div>
+                    </template>
+                </el-table-column>
+
+                <el-table-column :label="$t('device.device-log.798283-16')" header-align="center" align="left" prop="remark">
+                    <template slot-scope="scope">
+                        {{ scope.row.remark == null ? $t('device.device-log.798283-17') : scope.row.remark }}
+                    </template>
+                </el-table-column>
+            </el-table>
+            <div style="height: 40px">
+                <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+            </div>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import { listEventLog } from '../../../api/iot/eventLog';
+
+export default {
+    name: 'DeviceLog',
+    dicts: ['iot_event_type', 'iot_yes_no'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: function (newVal, oldVal) {
+            this.deviceInfo = newVal;
+            if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                this.queryParams.serialNumber = this.deviceInfo.serialNumber;
+                this.getList();
+                // 解析缓存物模型
+                this.thingsModel = this.deviceInfo.cacheThingsModel;
+            }
+        },
+    },
+    data() {
+        return {
+            // 物模型
+            thingsModel: {},
+            // 遮罩层
+            loading: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 设备日志表格数据
+            deviceLogList: [],
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                logType: null,
+                logValue: null,
+                deviceId: null,
+                serialNumber: null,
+                deviceName: null,
+                identity: null,
+                isMonitor: null,
+            },
+            // 时间范围
+            daterangeTime: [],
+        };
+    },
+    created() {
+        this.queryParams.serialNumber = this.device.serialNumber;
+        this.getList();
+    },
+    methods: {
+        /** 查询设备日志列表 */
+        getList() {
+            this.loading = true;
+            listEventLog(this.addDateRange(this.queryParams, this.daterangeTime)).then((response) => {
+                this.deviceLogList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.daterangeTime = [];
+            this.handleQuery();
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/event/export',
+                {
+                    ...this.queryParams,
+                },
+                `eventLog_${new Date().getTime()}.xlsx`
+            );
+        },
+        /** 格式化显示数据定义 */
+        formatValueDisplay(row) {
+            // 类型(1=属性上报,2=调用功能,3=事件上报,4=设备升级,5=设备上线,6=设备离线)
+            if (row.logType == 1) {
+                let propertyItem = this.getThingsModelItem(1, row.identity);
+                if (propertyItem != '') {
+                    return (
+                        (propertyItem.parentName ? '[' + propertyItem.parentName + (propertyItem.arrayIndex ? propertyItem.arrayIndex : '') + '] ' : '') +
+                        propertyItem.name +
+                        ': <span style="color:#409EFF;">' +
+                        this.getThingsModelItemValue(propertyItem, row.logValue) +
+                        ' ' +
+                        (propertyItem.datatype.unit != undefined ? propertyItem.datatype.unit : '') +
+                        '</span>'
+                    );
+                }
+            } else if (row.logType == 2) {
+                let functionItem = this.getThingsModelItem(2, row.identity);
+                if (functionItem != '') {
+                    return (
+                        (functionItem.parentName ? '[' + functionItem.parentName + (functionItem.arrayIndex ? functionItem.arrayIndex : '') + '] ' : '') +
+                        functionItem.name +
+                        ': <span style="color:#409EFF">' +
+                        this.getThingsModelItemValue(functionItem, row.logValue) +
+                        ' ' +
+                        (functionItem.datatype.unit != undefined ? functionItem.datatype.unit : '') +
+                        '</span>'
+                    );
+                }
+            } else if (row.logType == 3) {
+                let eventItem = this.getThingsModelItem(3, row.identity);
+                if (eventItem != '') {
+                    return (
+                        (eventItem.parentName ? '[' + eventItem.parentName + (eventItem.arrayIndex ? eventItem.arrayIndex : '') + '] ' : '') +
+                        eventItem.name +
+                        ': <span style="color:#409EFF">' +
+                        this.getThingsModelItemValue(eventItem, row.logValue) +
+                        ' ' +
+                        (eventItem.datatype.unit != undefined ? eventItem.datatype.unit : '') +
+                        '</span>'
+                    );
+                } else {
+                    return row.logValue;
+                }
+            } else if (row.logType == 4) {
+                return '<span style="font-weight:bold">设备升级</span>';
+            } else if (row.logType == 5) {
+                return '<span style="font-weight:bold">设备上线</span>';
+            } else if (row.logType == 6) {
+                return '<span style="font-weight:bold">设备离线</span>';
+            }
+            return '';
+        },
+        /** 获取物模型项中的值*/
+        getThingsModelItemValue(item, oldValue) {
+            // 枚举和布尔转换为文字
+            if (item.datatype.type == 'bool') {
+                if (oldValue == '0') {
+                    return item.datatype.falseText;
+                } else if (oldValue == '1') {
+                    return item.datatype.trueText;
+                }
+            } else if (item.datatype.type == 'enum') {
+                for (let i = 0; i < item.datatype.enumList.length; i++) {
+                    if (oldValue == item.datatype.enumList[i].value) {
+                        return item.datatype.enumList[i].text;
+                    }
+                }
+            }
+            return oldValue;
+        },
+        /** 获取物模型中的项*/
+        getThingsModelItem(type, identity) {
+            if (type == 1 && this.thingsModel.properties) {
+                for (let i = 0; i < this.thingsModel.properties.length; i++) {
+                    //普通类型 integer/decimal/string/emum//bool
+                    if (this.thingsModel.properties[i].id == identity) {
+                        return this.thingsModel.properties[i];
+                    }
+                    // 对象 object
+                    if (this.thingsModel.properties[i].datatype.type == 'object') {
+                        for (let j = 0; j < this.thingsModel.properties[i].datatype.params.length; j++) {
+                            if (this.thingsModel.properties[i].datatype.params[j].id == identity) {
+                                this.thingsModel.properties[i].datatype.params[j].parentName = this.thingsModel.properties[i].name;
+                                return this.thingsModel.properties[i].datatype.params[j];
+                            }
+                        }
+                    }
+                    // 数组 array
+                    if (this.thingsModel.properties[i].datatype.type == 'array' && this.thingsModel.properties[i].datatype.arrayType) {
+                        if (this.thingsModel.properties[i].datatype.arrayType == 'object') {
+                            // 数组元素格式:array_01_parentId_humidity,array_01_前缀终端上报时加上,物模型中没有
+                            let realIdentity = identity;
+                            let arrayIndex = 0;
+                            if (identity.indexOf('array_') > -1) {
+                                arrayIndex = identity.substring(6, 8);
+                                realIdentity = identity.substring(9);
+                            }
+                            for (let j = 0; j < this.thingsModel.properties[i].datatype.params.length; j++) {
+                                if (this.thingsModel.properties[i].datatype.params[j].id == realIdentity) {
+                                    // 标注索引和父级名称
+                                    this.thingsModel.properties[i].datatype.params[j].arrayIndex = Number(arrayIndex) + 1;
+                                    this.thingsModel.properties[i].datatype.params[j].parentName = this.thingsModel.properties[i].name;
+                                    return this.thingsModel.properties[i].datatype.params[j];
+                                }
+                            }
+                        } else {
+                            // 普通类型
+                            for (let j = 0; j < this.thingsModel.properties[i].datatype.arrayCount.length; j++) {
+                                if (this.thingsModel.properties[i].id == realIdentity) {
+                                    this.thingsModel.properties[i].arrayIndex = Number(arrayIndex) + 1;
+                                    this.thingsModel.properties[i].parentName = this.$t('device.device-log.798283-21');
+                                    return this.thingsModel.properties[i];
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if (type == 2 && this.thingsModel.functions) {
+                for (let i = 0; i < this.thingsModel.functions.length; i++) {
+                    //普通类型 integer/decimal/string/emum/bool
+                    if (this.thingsModel.functions[i].id == identity) {
+                        return this.thingsModel.functions[i];
+                    }
+                    // 对象 object
+                    if (this.thingsModel.functions[i].datatype.type == 'object') {
+                        for (let j = 0; j < this.thingsModel.functions[i].datatype.params.length; j++) {
+                            if (this.thingsModel.functions[i].datatype.params[j].id == identity) {
+                                this.thingsModel.functions[i].datatype.params[j].parentName = this.thingsModel.functions[i].name;
+                                return this.thingsModel.functions[i].datatype.params[j];
+                            }
+                        }
+                    }
+                    // 数组 array
+                    if (this.thingsModel.functions[i].datatype.type == 'array' && this.thingsModel.functions[i].datatype.arrayType) {
+                        // 数组元素格式:array_01_parentId_humidity,array_01_前缀终端上报时加上,物模型中没有
+                        let realIdentity = identity;
+                        let arrayIndex = 0;
+                        if (identity.indexOf('array_') > -1) {
+                            arrayIndex = identity.substring(6, 8);
+                            realIdentity = identity.substring(9);
+                        }
+                        if (this.thingsModel.functions[i].datatype.arrayType == 'object') {
+                            for (let j = 0; j < this.thingsModel.functions[i].datatype.params.length; j++) {
+                                if (this.thingsModel.functions[i].datatype.params[j].id == realIdentity) {
+                                    // 标注索引和父级名称
+                                    this.thingsModel.functions[i].datatype.params[j].arrayIndex = Number(arrayIndex) + 1;
+                                    this.thingsModel.functions[i].datatype.params[j].parentName = this.thingsModel.functions[i].name;
+                                    return this.thingsModel.functions[i].datatype.params[j];
+                                }
+                            }
+                        } else {
+                            // 普通类型
+                            for (let j = 0; j < this.thingsModel.functions[i].datatype.arrayCount.length; j++) {
+                                if (this.thingsModel.functions[i].id == realIdentity) {
+                                    this.thingsModel.functions[i].arrayIndex = Number(arrayIndex) + 1;
+                                    this.thingsModel.functions[i].parentName = this.$t('device.device-log.798283-21');
+                                    return this.thingsModel.functions[i];
+                                }
+                            }
+                        }
+                    }
+                }
+            } else if (type == 3 && this.thingsModel.events) {
+                for (let i = 0; i < this.thingsModel.events.length; i++) {
+                    if (this.thingsModel.events[i].id == identity) {
+                        return this.thingsModel.events[i];
+                    }
+                }
+            }
+            return '';
+        },
+    },
+};
+</script>

+ 933 - 0
src/views/pms/video_center/device/device-modbus-task.vue

@@ -0,0 +1,933 @@
+<template>
+    <div class="instruction-parsing">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="70px">
+            <el-form-item :label="$t('device.device-modbus-task.384302-0')" prop="jobName">
+                <el-input v-model="queryParams.jobName" :placeholder="$t('device.device-modbus-task.384302-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('device.device-modbus-task.384302-2')" prop="status">
+                <el-select v-model="queryParams.status" :placeholder="$t('device.device-modbus-task.384302-3')" clearable size="small">
+                    <el-option v-for="dict in dict.type.sys_job_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.device-modbus-task.384302-4') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.device-modbus-task.384302-5') }}</el-button>
+            </el-form-item>
+            <el-form-item style="float: right">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openEdit">{{ $t('device.device-modbus.433390-1') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table :border="false" v-loading="loading" :data="jobList">
+            <el-table-column :label="$t('device.device-modbus-task.384302-56')" align="center" prop="taskId" />
+            <el-table-column :label="$t('device.device-timer.433369-7')" align="center" prop="jobName" />
+            <el-table-column :label="$t('device.device-modbus-task.384302-57')" align="center" prop="command" />
+            <el-table-column :label="$t('device.device-modbus-task.384302-58')" align="center" prop="status">
+                <template slot-scope="scope">
+                    <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" :active-text="$t('device.device-timer.433369-12')" @change="handleStatusChange(scope.row)"></el-switch>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" prop="remark">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.variable_operation_interval" :value="scope.row.remark" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.device-modbus-task.384302-60')" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['modbus:job:remove']">{{ $t('device.device-modbus-task.384302-61') }}</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+        <el-dialog :title="editName ? $t('device.device-modbus-task.384302-12') : $t('device.device-modbus-task.384302-13')" :visible.sync="editDialog" :width="editName ? '30%' : '40%'">
+            <div class="dialog-content beautify-scroll-def">
+                <el-form :model="createForm" label-position="top">
+                    <el-form-item :label="$t('device.device-modbus-task.384302-0')" prop="jobName">
+                        <el-input v-model="createForm.jobName" :placeholder="$t('device.device-modbus-task.384302-1')" style="width: 280px" />
+                    </el-form-item>
+                    <el-row :gutter="40">
+                        <!-- 从机地址 -->
+                        <el-col :span="12">
+                            <el-form-item :label="$t('device.device-modbus-task.384302-14')" prop="path">
+                                <el-input v-model="createForm.path"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 功能码 -->
+                        <el-col :span="12">
+                            <el-form-item :label="$t('device.device-modbus-task.384302-15')" prop="functionCode">
+                                <el-select v-model="createForm.functionCode" @change="changeNum">
+                                    <el-option :label="functionCode.label" :value="functionCode.value" v-for="functionCode in functionCodeList" :key="functionCode.value"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <!--起始寄存器地址-->
+                        <el-col :span="12">
+                            <el-form-item prop="startPath">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">{{ $t('device.device-modbus-task.384302-16') }}</div>
+                                    <el-tooltip :content="createForm.startPathSwitch" placement="top">
+                                        <el-switch v-model="createForm.startPathSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949" active-value="Dec" inactive-value="Hex" />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="createForm.startPath"
+                                    type="number"
+                                    v-show="createForm.startPathSwitch == 'Dec'"
+                                    :min="0"
+                                    @change="
+                                        () => {
+                                            createForm.startPath16 = int2hex(createForm.startPath);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            createForm.startPath16 = int2hex(createForm.startPath);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ createForm.startPath16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="createForm.startPath16"
+                                    v-show="createForm.startPathSwitch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            createForm.startPath = hex2int(createForm.startPath16);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ createForm.startPath }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 个数或写值 -->
+                        <el-col :span="12">
+                            <!-- 个数 -->
+                            <el-form-item :label="registerNumTitle" prop="registerNum" v-show="!['05', '06'].includes(createForm.functionCode)">
+                                <el-input-number v-model="createForm.registerNum" controls-position="right" :min="0" @change="changeNum" />
+                            </el-form-item>
+                            <!-- 写值 -->
+                            <el-form-item prop="setValue" v-show="['05', '06'].includes(createForm.functionCode)">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">{{ registerNumTitle }}</div>
+                                    <el-tooltip :content="createForm.setValueSwitch" placement="top">
+                                        <el-switch v-model="createForm.setValueSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949" active-value="Dec" inactive-value="Hex" />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="createForm.setValue"
+                                    type="number"
+                                    v-show="createForm.setValueSwitch == 'Dec'"
+                                    @change="
+                                        () => {
+                                            createForm.setValue16 = int2hex(createForm.setValue);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            createForm.setValue16 = int2hex(createForm.setValue);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ createForm.setValue16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="createForm.setValue16"
+                                    v-show="createForm.setValueSwitch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            createForm.setValue = hex2int(createForm.setValue16);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ createForm.setValue }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 批量写寄存器值  -->
+                        <el-col :span="12" v-for="(item, index) in registerValList" :key="'register' + index" v-show="createForm.functionCode == '16'">
+                            <el-form-item prop="registerValList">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">#{{ index }} {{ $t('device.device-modbus-task.384302-17') }}</div>
+                                    <el-tooltip :content="item.switch" placement="top">
+                                        <el-switch
+                                            v-model="item.switch"
+                                            size="mini"
+                                            active-color="#13ce66"
+                                            @change="
+                                                () => {
+                                                    refreshRegisterInpust(item, index);
+                                                }
+                                            "
+                                            inactive-color="#ff4949"
+                                            active-value="Dec"
+                                            inactive-value="Hex"
+                                        />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="item.value"
+                                    type="number"
+                                    v-show="item.switch == 'Dec'"
+                                    :min="0"
+                                    @change="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ item.value16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="item.value16"
+                                    v-show="item.switch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            item.value = hex2int(item.value16);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ item.value }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 批量写线圈值  -->
+                        <el-col :span="6" v-for="(item, index) in IOValList" :key="'IO' + index" v-show="createForm.functionCode == '15'">
+                            <el-form-item prop="registerValList">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">#{{ index }} {{ $t('device.device-modbus-task.384302-18') }}</div>
+                                </div>
+                                <el-switch
+                                    v-model="item.value"
+                                    active-value="1"
+                                    inactive-value="0"
+                                    @change="
+                                        () => {
+                                            refreshIOInpust(item, index);
+                                        }
+                                    "
+                                />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <!-- 任务状态-->
+                    <el-form-item :label="$t('device.device-timer.433369-2')" prop="status">
+                        <el-radio-group v-model="createForm.status">
+                            <el-radio v-for="dict in dict.type.sys_job_status" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+                    <!-- 时间周期-->
+                    <el-form-item :label="$t('device.device-modbus-task.384302-19')" prop="cycleType">
+                        <div class="timer-wrap">
+                            <el-radio-group v-model="createForm.cycleType" @input="handleCycleTypeInput">
+                                <el-radio :label="1" style="display: block">
+                                    {{ $t('device.device-modbus-task.384302-20') }}
+                                    <el-tooltip placement="right">
+                                        <div slot="content">
+                                            {{ $t('device.device-modbus-task.384302-21') }}
+                                            <br />
+                                            {{ $t('device.device-modbus-task.384302-22') }}
+                                        </div>
+                                        <i class="el-icon-question" style="color: #909399"></i>
+                                    </el-tooltip>
+                                    <div class="timer-period">
+                                        <span>{{ $t('device.device-modbus-task.384302-23') }}</span>
+                                        <el-select style="width: 100px; margin-left: 10px" v-model="cycles1[0].interval" size="mini" :disabled="createForm.cycleType === 2" @change="handleCycleInterval">
+                                            <el-option v-for="dict in dict.type.variable_operation_interval" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                                        </el-select>
+                                        <el-select v-if="cycles1[0].interval === 'week'" style="width: 100px; margin-left: 5px" v-model="cycles1[0].week" size="mini" :disabled="createForm.cycleType === 2">
+                                            <el-option v-for="dict in dict.type.variable_operation_week" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                                        </el-select>
+                                        <el-select v-if="cycles1[0].interval === 'month'" style="width: 100px; margin-left: 5px" v-model="cycles1[0].day" size="mini" :disabled="createForm.cycleType === 2">
+                                            <el-option v-for="dict in dict.type.variable_operation_day" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                                        </el-select>
+                                        <el-select
+                                            v-if="cycles1[0].interval === 'day' || cycles1[0].interval === 'week' || cycles1[0].interval === 'month'"
+                                            style="width: 100px; margin-left: 5px"
+                                            v-model="cycles1[0].time"
+                                            size="mini"
+                                            :disabled="createForm.cycleType === 2"
+                                            @change="handleCycleTime"
+                                        >
+                                            <el-option v-for="dict in dict.type.variable_operation_time" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                                        </el-select>
+                                        <span style="margin-left: 10px">{{ $t('device.device-modbus-task.384302-24') }}</span>
+                                    </div>
+                                </el-radio>
+                            </el-radio-group>
+                        </div>
+                    </el-form-item>
+                </el-form>
+                <div v-loading="createLoading">
+                    <div class="create-title">
+                        <el-button type="text" @click.stop="encode">{{ $t('device.device-modbus-task.384302-25') }}</el-button>
+                        <div class="title-right">
+                            <el-button type="primary" size="mini" @click="copyText(createCode)">{{ $t('device.device-modbus-task.384302-26') }}</el-button>
+                        </div>
+                    </div>
+                    <div class="create-code">{{ createCode }}</div>
+                </div>
+            </div>
+            <div slot="footer" class="dialog-btn">
+                <el-button type="" size="mini" @click="editDialog = false">{{ $t('device.device-modbus-task.384302-27') }}</el-button>
+                <el-button type="primary" size="mini" @click="handleAdd">{{ $t('confirm') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { hex2int, int2hex, copyText } from '@/utils/common';
+import { encode } from '@/api/iot/mqttTest';
+import { listJob, getJob, delJob, addJob, updateJob } from '@/api/iot/modbusJob';
+export default {
+    name: 'device-modbus-task',
+    dicts: ['sys_job_group', 'sys_job_status', 'variable_operation_interval', 'variable_operation_time', 'variable_operation_week', 'variable_operation_day', 'variable_operation_type'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        device: {
+            handler(newVal, oldVal) {
+                if (newVal.deviceId && newVal.deviceId !== oldVal.deviceId) {
+                    this.queryParams.subDeviceId = newVal.deviceId;
+                    this.deviceInfo = newVal;
+                    this.getList();
+                }
+            },
+        },
+    },
+
+    computed: {
+        /**编辑寄存器个数显示的标题 */
+        registerNumTitle() {
+            switch (this.createForm.functionCode) {
+                case '01':
+                case '02':
+                case '15':
+                    return this.$t('device.device-modbus-task.384302-29');
+                case '03':
+                case '04':
+                case '16':
+                    return this.$t('device.device-modbus-task.384302-30');
+                case '05':
+                    return this.$t('device.device-modbus-task.384302-31');
+                case '06':
+                    return this.$t('device.device-modbus-task.384302-32');
+            }
+        },
+    },
+    data() {
+        return {
+            //选中的格式
+            format: 'Hex',
+            // 遮罩层
+            loading: false,
+
+            //编辑框
+            editDialog: false,
+            //生成表单
+            createForm: {
+                cycleType: 1, // 1周期,2自定义
+                status: '0',
+            },
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 总条数
+            total: 0,
+            //功能码列表
+            functionCodeList: [
+                {
+                    label: this.$t('device.device-modbus-task.384302-33'),
+                    value: '01',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-34'),
+                    value: '02',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-35'),
+                    value: '03',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-36'),
+                    value: '04',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-37'),
+                    value: '05',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-38'),
+                    value: '06',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-39'),
+                    value: '15',
+                },
+                {
+                    label: this.$t('device.device-modbus-task.384302-40'),
+                    value: '16',
+                },
+            ],
+            jobList: [],
+            // 显示搜索条件
+            showSearch: true,
+            //生成的指令码
+            createCode: '',
+            //批量写的寄存器值数组
+            registerValList: [],
+            //批量写的线圈个数
+            IOValList: [],
+            //编辑名称
+            editName: false,
+            //编辑名称表单
+            editNameForm: {},
+            //生成刷新
+            createLoading: false,
+            //删除快捷指令
+            delDialog: false,
+            //选择删除的快捷指令
+            delItem: {},
+            //父组件传过来的对象
+            deviceInfo: {},
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                subDeviceId: null,
+                subSerialNumber: null,
+                command: null,
+                jobId: null,
+                status: null,
+            },
+            cycles1: [{ interval: '300', time: '', week: '', day: '' }],
+            cycles2: [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }],
+        };
+    },
+    methods: {
+        /** 查询轮训任务列列表 */
+        getList() {
+            this.loading = true;
+            listJob(this.queryParams).then((response) => {
+                this.jobList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+
+        /** 提交按钮 */
+        submitForm() {
+            if (this.createForm.taskId != null) {
+                updateJob(this.createForm).then((response) => {
+                    this.$modal.msgSuccess(this.$t('device.device-modbus-task.384302-62'));
+                    this.open = false;
+                    this.getList();
+                });
+            } else {
+                addJob(this.createForm).then((response) => {
+                    this.$modal.msgSuccess(this.$t('device.device-modbus-task.384302-63'));
+                    this.open = false;
+                    this.getList();
+                });
+            }
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const taskIds = row.taskId || this.ids;
+            const data = {
+                taskId: taskIds,
+                jobId: row.jobId,
+            };
+            this.$modal
+                .confirm(this.$t('device.device-modbus-task.384302-64', [taskIds]))
+                .then(function () {
+                    return delJob(data);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('device.device-modbus-task.384302-65'));
+                })
+                .catch(() => {});
+        },
+
+        /**新增确认按钮*/
+        handleAdd() {
+            let params = {
+                slaveId: parseInt(this.createForm.path), //从机地址
+                address: this.createForm.startPath, //起始寄存器地址
+                code: parseInt(this.createForm.functionCode), //功能码
+            };
+            switch (this.createForm.functionCode) {
+                case '01':
+                case '02':
+                case '03':
+                case '04':
+                    //线圈个数/寄存器个数
+                    params.count = this.createForm.registerNum;
+                    break;
+                case '05':
+                case '06':
+                    //线圈值/寄存器值
+                    params.writeData = this.createForm.setValue;
+                    break;
+                case '15':
+                    //线圈个数/寄存器个数
+                    params.count = this.createForm.registerNum;
+                    //线圈值数组
+                    const IOValList = this.IOValList.map((item) => {
+                        return item.value;
+                    });
+                    params.bitString = IOValList.join('');
+                    break;
+                case '16':
+                    //线圈个数/寄存器个数
+                    params.count = this.createForm.registerNum;
+                    //寄存器值数组
+                    const registerValList = this.registerValList.map((item) => {
+                        return item.value;
+                    });
+                    params.tenWriteData = registerValList;
+                    break;
+            }
+            const res = encode(params).then((response) => {
+                this.createCode = response.msg;
+                this.handlePush();
+            });
+        },
+
+        handlePush() {
+            /**计算定时时间*/
+            let cycle = '';
+            const c = this.cycles1.map((item) => {
+                if (item.interval === 'hour') {
+                    return { type: 'hour' };
+                } else if (item.interval === 'day') {
+                    return { type: 'day', time: item.time };
+                } else if (item.interval === 'week') {
+                    return { type: 'week', week: item.week, time: item.time };
+                } else if (item.interval === 'month') {
+                    return { type: 'month', day: item.day, time: item.time };
+                } else {
+                    return { interval: item.interval };
+                }
+            });
+            cycle = JSON.stringify(c);
+            this.createForm.subDeviceId = this.device.deviceId;
+            this.createForm.subSerialNumber = this.device.serialNumber;
+            this.createForm.command = this.createCode;
+            this.createForm.remark = cycle;
+            this.submitForm();
+            this.editDialog = false;
+        },
+
+        /**打开编辑框 */
+        openEdit() {
+            this.resetCreateForm();
+            this.editName = false;
+            this.editDialog = true;
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                taskId: null,
+                subDeviceId: null,
+                subSerialNumber: null,
+                command: null,
+                jobId: null,
+                status: null,
+                createBy: null,
+                createTime: null,
+                remark: null,
+            };
+            this.resetForm('form');
+        },
+        /**重置编辑框 */
+        resetCreateForm() {
+            this.createForm = {
+                path: '01',
+                functionCode: '01',
+                startPath: 0,
+                startPath16: '0000',
+                registerNum: 1,
+                startPathSwitch: 'Dec',
+                setValue: 0,
+                setValue16: '0000',
+                setValueSwitch: 'Dec',
+                status: '0',
+                cycleType: 1, // 1周期,2自定义
+            };
+            this.createCode = '';
+        },
+        /**十进制变成十六进制 */
+        int2hex(str) {
+            return int2hex(str);
+        },
+        /**十六进制变成十进制 */
+        hex2int(str) {
+            return hex2int(str);
+        },
+        /**修改批量写的寄存器个数或者线圈个数 */
+        changeNum() {
+            //批量写寄存器
+            if (this.createForm.functionCode == '16') {
+                for (let index = 0; index < this.createForm.registerNum; index++) {
+                    const item = this.registerValList[index];
+                    if (!item) {
+                        this.registerValList[index] = {
+                            value: 0,
+                            value16: '0000',
+                            switch: 'Dec',
+                        };
+                    }
+                }
+                //如果编写数组大于他需要写的数字,就到写的个数
+                if (this.registerValList.length > this.createForm.registerNum) {
+                    //多的个数
+                    const num = this.registerValList.length - this.createForm.registerNum;
+                    this.registerValList.splice(this.createForm.registerNum, num);
+                }
+            }
+            //批量写线圈
+            if (this.createForm.functionCode == '15') {
+                for (let index = 0; index < this.createForm.registerNum; index++) {
+                    const item = this.IOValList[index];
+                    if (!item) {
+                        this.IOValList[index] = {
+                            value: '0',
+                        };
+                    }
+                }
+                //如果编写数组大于他需要写的数字,就到写的个数
+                if (this.IOValList.length > this.createForm.registerNum) {
+                    //多的个数
+                    const num = this.IOValList.length - this.createForm.registerNum;
+                    this.IOValList.splice(this.createForm.registerNum, num);
+                }
+            }
+        },
+        /**刷新寄存器输入框 */
+        refreshRegisterInpust(item, index) {
+            this.$set(this.registerValList, index, item);
+        },
+        /**刷新线圈值 */
+        refreshIOInpust(item, index) {
+            this.$set(this.IOValList, index, item);
+        },
+
+        /**复制 */
+        copyText(code) {
+            const res = copyText(code);
+            this.$message({
+                type: res.type,
+                message: res.message,
+            });
+        },
+        /**编码 */
+        async encode() {
+            try {
+                this.createLoading = true;
+                let params = {
+                    slaveId: parseInt(this.createForm.path), //从机地址
+                    address: this.createForm.startPath, //起始寄存器地址
+                    code: parseInt(this.createForm.functionCode), //功能码
+                };
+                switch (this.createForm.functionCode) {
+                    case '01':
+                    case '02':
+                    case '03':
+                    case '04':
+                        //线圈个数/寄存器个数
+                        params.count = this.createForm.registerNum;
+                        break;
+                    case '05':
+                    case '06':
+                        //线圈值/寄存器值
+                        params.writeData = this.createForm.setValue;
+                        break;
+                    case '15':
+                        //线圈个数/寄存器个数
+                        params.count = this.createForm.registerNum;
+                        //线圈值数组
+                        const IOValList = this.IOValList.map((item) => {
+                            return item.value;
+                        });
+                        params.bitString = IOValList.join('');
+                        break;
+                    case '16':
+                        //线圈个数/寄存器个数
+                        params.count = this.createForm.registerNum;
+                        //寄存器值数组
+                        const registerValList = this.registerValList.map((item) => {
+                            return item.value;
+                        });
+                        params.tenWriteData = registerValList;
+                        break;
+                }
+                const res = await encode(params);
+                this.createCode = res.msg;
+            } catch (err) {
+                this.$message({
+                    type: 'error',
+                    message: err.message || this.$t('device.device-modbus-task.384302-41'),
+                });
+            } finally {
+                this.createLoading = false;
+            }
+        },
+        // 搜索按钮操作
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        // 重置按钮操作
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 定时状态修改
+        handleStatusChange(row) {
+            let text = row.status === '0' ? this.$t('device.device-modbus-task.384302-42') : this.$t('device.device-modbus-task.384302-43');
+            this.$modal
+                .confirm(this.$t('device.device-modbus-task.384302-44', [text + '""' + row.jobName]))
+                .then(function () {
+                    return updateJob(row.taskId, row.status);
+                })
+                .then(() => {
+                    this.$modal.msgSuccess(text + this.$t('device.device-modbus-task.384302-45'));
+                })
+                .catch(function () {
+                    row.status = row.status === '0' ? '1' : '0';
+                });
+        },
+        // 格式化显示CRON描述
+        formatCronDisplay(item) {
+            let result = '';
+            if (item.isAdvance == 0) {
+                let time = '<br /><span style="color:#F56C6C">时间 ' + item.cronExpression.substring(5, 7) + ':' + item.cronExpression.substring(2, 4) + '</span>';
+                let week = item.cronExpression.substring(12);
+                if (week == '1,2,3,4,5,6,7') {
+                    (result = this.$t('device.device-modbus-task.384302-47')), +time;
+                } else {
+                    let weekArray = week.split(',');
+                    for (let i = 0; i < weekArray.length; i++) {
+                        if (weekArray[i] == '1') {
+                            result = result + this.$t('device.device-modbus-task.384302-48');
+                        } else if (weekArray[i] == '2') {
+                            result = result + this.$t('device.device-modbus-task.384302-49');
+                        } else if (weekArray[i] == '3') {
+                            result = result + this.$t('device.device-modbus-task.384302-50');
+                        } else if (weekArray[i] == '4') {
+                            result = result + this.$t('device.device-modbus-task.384302-51');
+                        } else if (weekArray[i] == '5') {
+                            result = result + this.$t('device.device-modbus-task.384302-52');
+                        } else if (weekArray[i] == '6') {
+                            result = result + this.$t('device.device-modbus-task.384302-53');
+                        } else if (weekArray[i] == '7') {
+                            result = result + this.$t('device.device-modbus-task.384302-54');
+                        }
+                    }
+                    result = result.substring(0, result.length - 1) + ' ' + time;
+                }
+            } else {
+                result = this.$t('device.device-modbus-task.384302-55');
+            }
+            return result;
+        },
+
+        // 修改按钮操作
+        handleUpdate(row) {
+            this.reset();
+            const jobId = row.jobId || this.ids;
+            getJob(jobId).then((response) => {
+                this.form = response.data;
+                // actionList赋值
+                this.actionList = JSON.parse(this.form.actions);
+                for (let i = 0; i < this.actionList.length; i++) {
+                    if (this.actionList[i].type == 1) {
+                        this.setParentAndModelData(this.actionList[i], this.thingsModel.properties);
+                    } else if (this.actionList[i].type == 2) {
+                        this.setParentAndModelData(this.actionList[i], this.thingsModel.functions);
+                    }
+                }
+                if (this.form.isAdvance == 0) {
+                    let arrayValue = this.form.cronExpression.substring(12).split(',').map(Number);
+                    this.timerWeekValue = arrayValue;
+                    this.timerTimeValue = this.form.cronExpression.substring(5, 7) + ':' + this.form.cronExpression.substring(2, 4);
+                }
+                this.open = true;
+                this.title = this.$t('device.device-timer.433369-71');
+            });
+        },
+
+        // 时间周期-自定义-添加
+        handleCustomIntervalAdd() {
+            this.cycles2.push({ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' });
+        },
+        // 时间周期类型切换
+        handleCycleTypeInput(val) {
+            if (val === 1) {
+                this.cycles2 = [{ type: 'day', time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' }];
+            } else {
+                this.cycles1 = [{ interval: 'hour', time: '', week: '', day: '' }];
+            }
+        },
+        handleCustomIntervalDelete(index) {
+            this.cycles2.splice(index, 1);
+        },
+        // 时间周期-周期循环
+        handleCycleInterval(val) {
+            if (val === 'hour') {
+                this.$set(this.cycles1, 0, { interval: val, time: '', week: '', day: '' });
+            } else if (val === 'day') {
+                this.$set(this.cycles1, 0, { interval: val, time: '01', week: '', day: '' });
+            } else if (val === 'week') {
+                this.$set(this.cycles1, 0, { interval: val, time: '01', week: '1', day: '' });
+            } else if (val === 'month') {
+                this.$set(this.cycles1, 0, { interval: val, time: '01', week: '', day: '1' });
+            } else {
+                this.$set(this.cycles1, 0, { interval: val, time: '', week: '', day: '' });
+            }
+        },
+        // 时间周期-自定义
+        handleCustomInterval(index, val) {
+            if (val === 'day') {
+                this.$set(this.cycles2, index, { type: val, time: '00', week: '', day: '', toType: '1', toTime: '02', toWeek: '', toDay: '' });
+            } else if (val === 'week') {
+                this.$set(this.cycles2, index, { type: val, time: '00', week: '1', day: '', toType: '3', toTime: '02', toWeek: '2', toDay: '' });
+            } else if (val === 'month') {
+                this.$set(this.cycles2, index, { type: val, time: '00', week: '', day: '1', toType: '4', toTime: '02', toWeek: '', toDay: '2' });
+            }
+        },
+    },
+    mounted() {
+        const { deviceId } = this.device;
+        if (deviceId) {
+            this.queryParams.subDeviceId = deviceId;
+            this.getList();
+        }
+        this.resetCreateForm();
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+$border-color: #dcdfe6;
+$right-btn-color: #1890ff;
+
+::v-deep .el-dialog__body {
+    border-top: 1px solid $border-color;
+    border-bottom: 1px solid $border-color;
+    box-sizing: border-box;
+    padding: 0;
+}
+
+.dialog-content {
+    //max-height:1000px;
+    width: 100%;
+    box-sizing: border-box;
+    padding: 30px 20px;
+    overflow: auto;
+
+    .create-title {
+        display: flex;
+        line-height: 36px;
+        margin-bottom: 16px;
+
+        .title-right {
+            margin-left: auto;
+        }
+    }
+
+    .create-code {
+        font-size: 18px;
+        line-height: 36px;
+        font-weight: 800;
+    }
+
+    .form-item-label {
+        display: flex;
+        align-items: center;
+        width: 100%;
+
+        ::v-deep .el-form-item__label {
+            width: 100%;
+        }
+    }
+
+    .timer-wrap {
+        .timer-period {
+            display: inline-block;
+            margin-left: 30px;
+            color: #000000;
+            font-size: 12px;
+            font-weight: normal;
+        }
+
+        .timer-custom {
+            display: block;
+            margin-top: 12px;
+            color: #000000;
+            font-size: 12px;
+            font-weight: normal;
+        }
+    }
+
+    .comp-add-edit {
+        display: flex;
+        flex-direction: column;
+
+        ::v-deep .el-form-item__content {
+            margin-left: 0 !important;
+        }
+
+        .comput-formula-box {
+            padding: 20px 0;
+            border: 1px solid #e7e9f1;
+            margin-left: 50px;
+            display: flex;
+
+            .title {
+                text-align: right;
+                width: 96px;
+                padding-right: 16px;
+            }
+
+            .content {
+                font-size: 12px;
+                line-height: 32px;
+
+                .alias-wrap {
+                    width: 28px;
+                    height: 28px;
+                    background-image: linear-gradient(180deg, #6fb0ff, #3c78ff);
+                    box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.2);
+                    font-size: 12px;
+                    font-weight: 400;
+                    line-height: 28px;
+                    text-align: center;
+                    margin-top: 2px;
+                    color: #fff;
+                }
+            }
+        }
+    }
+}
+</style>

+ 313 - 0
src/views/pms/video_center/device/device-monitor.vue

@@ -0,0 +1,313 @@
+<template>
+    <div class="device-detail-page">
+        <el-card class="main-card">
+            <el-form :inline="true" label-width="100px">
+                <el-form-item :label="$t('device.device-monitor.817489-0')">
+                    <el-tooltip class="item" effect="light" :content="$t('device.device-monitor.817489-1')" placement="top">
+                        <el-input v-model="monitorInterval" :placeholder="$t('device.device-monitor.817489-2')" type="number" clearable size="small" style="width: 180px" />
+                    </el-tooltip>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-monitor.817489-3')">
+                    <el-tooltip class="item" effect="light" :content="$t('device.device-monitor.817489-4')" placement="top">
+                        <el-input v-model="monitorNumber" :placeholder="$t('device.device-monitor.817489-5')" type="number" clearable size="small" style="width: 180px" />
+                    </el-tooltip>
+                </el-form-item>
+                <el-form-item>
+                    <el-button icon="el-icon-video-play" size="mini" type="primary" @click="beginMonitor()" style="margin-left: 30px" v-hasPermi="['iot:service:invoke ']">
+                        {{ $t('device.device-monitor.817489-6') }}
+                    </el-button>
+                    <el-button icon="el-icon-video-pause" size="mini" type="primary" @click="stopMonitor()" v-hasPermi="['iot:service:invoke ']">{{ $t('device.device-monitor.817489-7') }}</el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+        <el-card v-show="monitorThings.length > 0">
+            <el-row :gutter="20" :element-loading-text="$t('device.device-monitor.817489-8')" element-loading-background="rgba(0, 0, 0, 0.8)" element-loading-spinner="el-icon-loading">
+                <el-col v-for="(item, index) in monitorThings" :key="index" :span="12" style="margin-bottom: 20px">
+                    <el-card :body-style="{ paddingTop: '10px', marginBottom: '-20px' }" shadow="hover">
+                        <div ref="monitor" style="height: 210px; padding: 0"></div>
+                    </el-card>
+                </el-col>
+            </el-row>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import moment from 'moment';
+
+export default {
+    name: 'DeviceMonitor',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: function (newVal, oldVal) {
+            this.deviceInfo = newVal;
+            if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                // 监测数据
+                this.monitorThings = this.deviceInfo.monitorList;
+                // 监测数据集合初始化
+                this.dataList = [];
+                for (let i = 0; i < this.monitorThings.length; i++) {
+                    this.dataList.push({
+                        id: this.monitorThings[i].id,
+                        name: this.monitorThings[i].name,
+                        data: [],
+                    });
+                    // this.dataList[i].data.push(["2022-03-14 23:32:09", "30"]);
+                }
+                // 绘制监测图表
+                this.$nextTick(function () {
+                    this.getMonitorChart();
+                });
+                // 添加message事件监听器
+                this.mqttCallback();
+            }
+        },
+    },
+    data() {
+        return {
+            // 实时监测间隔
+            monitorInterval: 1000,
+            // 实时监测次数
+            monitorNumber: 60,
+            // 图表集合
+            chart: [],
+            // 图表数据集合
+            dataList: [],
+            // 监测物模型
+            monitorThings: [],
+            // 图表遮罩层
+            chartLoading: false,
+            // 设备信息
+            deviceInfo: {},
+        };
+    },
+    created() {},
+    methods: {
+        /**
+         * Mqtt发布消息
+         * @device 设备
+         * @model 物模型 ,type 类型(1=属性,2=功能,3=OTA升级,4=实时监测)
+         * */
+        mqttPublish(device, model) {
+            let topic = '';
+            let message = '';
+            if (model.type == 4) {
+                // 实时监测
+                topic = '/' + device.productId + '/' + device.serialNumber + '/monitor/get';
+                message = '{"count":' + model.value + ',"interval":' + this.monitorInterval + '}';
+            } else {
+                return;
+            }
+            if (topic != '') {
+                // 发布
+                this.$mqttTool
+                    .publish(topic, message, model.name)
+                    .then((res) => {
+                        this.$modal.notifySuccess(res);
+                    })
+                    .catch((res) => {
+                        this.$modal.notifyError(res);
+                    });
+            }
+        },
+        /* Mqtt回调处理  */
+        mqttCallback() {
+            this.$mqttTool.client.on('message', (topic, message, buffer) => {
+                let topics = topic.split('/');
+                let productId = topics[1];
+                let deviceNum = topics[2];
+                message = JSON.parse(message.toString());
+                if (!message) {
+                    return;
+                }
+                if (topics[3] == 'status') {
+                    console.log(this.$t('device.device-monitor.817489-9'), topic);
+                    console.log(this.$t('device.device-monitor.817489-10'), message);
+                    // 更新列表中设备的状态
+                    if (this.deviceInfo.serialNumber == deviceNum) {
+                        this.deviceInfo.status = message.status;
+                        this.deviceInfo.isShadow = message.isShadow;
+                        this.deviceInfo.rssi = message.rssi;
+                    }
+                }
+                if (topics[3] == 'monitor') {
+                    console.log(this.$t('device.device-monitor.817489-11'), topic);
+                    console.log(this.$t('device.device-monitor.817489-12'), message);
+                    // 实时监测
+                    // this.chartLoading = false;
+                    for (let k = 0; k < message.length; k++) {
+                        let value = message[k].value;
+                        let id = message[k].id;
+                        let remark = message[k].remark;
+                        const time = moment(Number(remark)).format('YYYY-MM-DD HH:mm:ss');
+                        // 数据加载到图表
+                        for (let i = 0; i < this.dataList.length; i++) {
+                            if (id == this.dataList[i].id) {
+                                // 普通类型匹配
+                                if (this.dataList[i].length > 50) {
+                                    this.dataList[i].shift();
+                                }
+                                this.dataList[i].data.push([remark ? time : this.getTime(), value]);
+                                // 更新图表
+                                this.chart[i].setOption({
+                                    series: [
+                                        {
+                                            data: this.dataList[i].data,
+                                        },
+                                    ],
+                                });
+                                break;
+                            } else if (this.dataList[i].id.indexOf('array_') == 0) {
+                                // 数组类型匹配,例如:gateway_temperature,图表id去除前缀后匹配
+                                let index = this.dataList[i].id.substring(6, 8);
+                                let identity = this.dataList[i].id.substring(9);
+                                if (identity == id) {
+                                    let values = value.split(',');
+                                    if (this.dataList[i].length > 50) {
+                                        this.dataList[i].shift();
+                                    }
+                                    this.dataList[i].data.push([remark ? time : this.getTime(), values[index]]);
+                                    // 更新图表
+                                    this.chart[i].setOption({
+                                        series: [
+                                            {
+                                                data: this.dataList[i].data,
+                                            },
+                                        ],
+                                    });
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                    // this.chartLoading = true;
+                }
+            });
+        },
+        /** 更新实时监测参数*/
+        beginMonitor() {
+            if (this.deviceInfo.status != 3) {
+                this.$modal.alertError(this.$t('device.device-monitor.817489-13'));
+                return;
+            }
+            // 清空图表数据
+            for (let i = 0; i < this.dataList.length; i++) {
+                this.dataList[i].data = [];
+            }
+            if (this.monitorInterval < 500 || this.monitorInterval > 10000) {
+                this.$modal.alertError(this.$t('device.device-monitor.817489-14'));
+            }
+            if (this.monitorNumber == 0 || this.monitorNumber > 300) {
+                this.$modal.alertError(this.$t('device.device-monitor.817489-15'));
+            }
+            // Mqtt发布实时监测消息
+            let model = {};
+            model.name = this.$t('device.device-monitor.817489-16');
+            model.value = this.monitorNumber;
+            model.type = 4;
+            this.mqttPublish(this.deviceInfo, model);
+            // this.chartLoading = true;
+        },
+        /** 停止实时监测 */
+        stopMonitor() {
+            if (this.deviceInfo.status != 3) {
+                this.$modal.alertError(this.$t('device.device-monitor.817489-13'));
+                return;
+            }
+            // this.chartLoading = false;
+            // Mqtt发布实时监测
+            let model = {};
+            model.name = this.$t('device.device-monitor.817489-17');
+            model.value = 0;
+            model.type = 4;
+            this.mqttPublish(this.deviceInfo, model);
+        },
+        /**监测数据 */
+        getMonitorChart() {
+            let color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
+            for (let i = 0; i < this.monitorThings.length; i++) {
+                // 设置宽度
+                this.$refs.monitor[i].style.width = document.documentElement.clientWidth / 2 - 255 + 'px';
+                this.chart[i] = this.$echarts.init(this.$refs.monitor[i]);
+                var option;
+                option = {
+                    title: {
+                        left: 'center',
+                        text: this.monitorThings[i].name + ' (单位 ' + (this.monitorThings[i].datatype.unit != undefined ? this.monitorThings[i].datatype.unit : this.$t('device.device-monitor.817489-19')) + ')',
+                        textStyle: {
+                            fontSize: 14,
+                        },
+                    },
+                    grid: {
+                        top: '50px',
+                        left: '20px',
+                        right: '20px',
+                        bottom: '10px',
+                        containLabel: true,
+                    },
+                    tooltip: {
+                        trigger: 'axis',
+                        axisPointer: {
+                            animation: true,
+                        },
+                    },
+                    xAxis: {
+                        type: 'time',
+                        show: false,
+                        splitLine: {
+                            show: false,
+                        },
+                    },
+                    yAxis: {
+                        type: 'value',
+                        boundaryGap: [0, '100%'],
+                        splitLine: {
+                            show: true,
+                        },
+                        scale: true, //自适应
+                        axisLabel: {
+                            formatter: function (value, index) {
+                                return value.toFixed(2);
+                            },
+                        }, //可根据实际情况修改y轴保留小数位数
+                    },
+                    series: [
+                        {
+                            name: this.monitorThings[i].name,
+                            type: 'line',
+                            symbol: 'none',
+                            sampling: 'lttb',
+                            itemStyle: {
+                                color: i > 9 ? color[0] : color[i],
+                            },
+                            areaStyle: {},
+                            data: [],
+                        },
+                    ],
+                };
+                option && this.chart[i].setOption(option);
+            }
+        },
+        /* 获取当前时间*/
+        getTime() {
+            let date = new Date();
+            let y = date.getFullYear();
+            let m = date.getMonth() + 1;
+            let d = date.getDate();
+            let H = date.getHours();
+            let mm = date.getMinutes();
+            let s = date.getSeconds();
+            m = m < 10 ? '0' + m : m;
+            d = d < 10 ? '0' + d : d;
+            H = H < 10 ? '0' + H : H;
+            return y + '-' + m + '-' + d + ' ' + H + ':' + mm + ':' + s;
+        },
+    },
+};
+</script>

+ 387 - 0
src/views/pms/video_center/device/device-recycle.vue

@@ -0,0 +1,387 @@
+<template>
+    <div class="recycle-wrap">
+        <el-card class="main-card">
+            <el-row :gutter="10">
+                <i class="el-icon-arrow-left" @click="goBack()"></i>
+                <el-divider direction="vertical"></el-divider>
+                <span>{{ $t('device.device-recycle.864193-0') }}</span>
+            </el-row>
+            <el-divider></el-divider>
+            <div style="width: 40%; margin-top: -20px">
+                <el-form label-position="top" :model="queryParams" ref="allotForm" :rules="allotRules">
+                    <div style="width: 35%">
+                        <el-form-item :label="$t('device.device-recycle.864193-1')" prop="deptId">
+                            <treeselect v-model="queryParams.deptId" :options="deptOptions" :show-count="true" :placeholder="$t('device.device-recycle.864193-2')" @input="getList" />
+                        </el-form-item>
+                    </div>
+                </el-form>
+            </div>
+            <div style="width: 50%">
+                <el-row>
+                    <el-form ref="queryForm" :model="queryParams">
+                        <el-col :span="7">
+                            <el-form-item prop="productId">
+                                <el-select v-model="queryParams.productId" :placeholder="$t('device.allot-import-dialog.060657-1')" style="width: 95%" clearable filterable>
+                                    <el-option v-for="item in productList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="7">
+                            <el-form-item prop="deviceName">
+                                <el-input :placeholder="$t('device.device-edit.148398-2')" clearable v-model="queryParams.deviceName" style="width: 95%"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="1.5">
+                            <el-button type="primary" icon="el-icon-search" @click="handleQuery">{{ $t('device.device-recycle.864193-5') }}</el-button>
+                        </el-col>
+                        <el-button icon="el-icon-refresh-left" @click="resetQuery" style="margin-left: 5px">{{ $t('device.device-recycle.864193-6') }}</el-button>
+                    </el-form>
+                </el-row>
+            </div>
+            <div class="general">
+                <div class="topLeft">
+                    <div style="padding: 10px 10px; background-color: #f8f8f9">
+                        <span style="font-size: 15px; font-weight: bold">{{ $t('device.device-recycle.864193-7') }}</span>
+                        <span style="font-size: 15px; font-weight: bold; float: right">{{ selectedCount }}/{{ this.count }}</span>
+                    </div>
+                    <el-row>
+                        <el-table :border="false" ref="leftTableData" :data="menuTableData" style="width: 100%" max-height="373" @selection-change="changeCheckBoxValueLeft">
+                            <template slot="empty">
+                                <el-empty :image-size="100" :description="$t('device.device-recycle.864193-8')"></el-empty>
+                            </template>
+                            <el-table-column :selectable="checkSelectable" type="selection" width="55"></el-table-column>
+                            <el-table-column fixed label="DeviceKey" prop="deviceId" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="productName" :label="$t('device.allot-record.155854-2')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="serialNumber" :label="$t('device.device-edit.148398-7')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="deviceName" :label="$t('device.device-edit.148398-1')" show-overflow-tooltip></el-table-column>
+                        </el-table>
+                    </el-row>
+                </div>
+                <div class="topCenter">
+                    <el-button type="primary" :disabled="add" @click="rightAdd"><i class="el-icon-arrow-right el-icon--right"></i></el-button>
+                    <el-button type="primary" :disabled="del" @click="leftDelete"><i class="el-icon-arrow-left el-icon--left"></i></el-button>
+                </div>
+                <div class="topRight">
+                    <div style="padding: 10px 10px; background-color: #f8f8f9">
+                        <span style="font-size: 15px; font-weight: bold">{{ $t('device.device-recycle.864193-12') }}</span>
+                        <span style="font-size: 15px; font-weight: bold; float: right">{{ selectedCount1 }}/500</span>
+                    </div>
+                    <el-row>
+                        <el-table :border="false" ref="rightTableData" :data="rightTableData" style="width: 100%" max-height="373" @selection-change="changeCheckBoxValueRight">
+                            <template slot="empty">
+                                <el-empty :image-size="100" :description="$t('device.device-recycle.864193-8')"></el-empty>
+                            </template>
+                            <el-table-column type="selection" width="55"></el-table-column>
+                            <el-table-column prop="deviceId" label="DeviceKey" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="productName" :label="$t('device.allot-record.155854-2')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="serialNumber" :label="$t('device.device-edit.148398-7')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="deviceName" :label="$t('device.device-edit.148398-1')" show-overflow-tooltip></el-table-column>
+                        </el-table>
+                    </el-row>
+                </div>
+            </div>
+            <div class="pagination-container">
+                <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :pager-count="5" :limit.sync="queryParams.pageSize" :pageSizes="[10, 20, 30, 40]" @pagination="getList" />
+            </div>
+        </el-card>
+        <div class="footer">
+            <el-button type="primary" @click="confirmDistribution">{{ $t('device.device-recycle.864193-13') }}</el-button>
+        </div>
+    </div>
+</template>
+<script type="text/javascript">
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+import { deptsTreeSelectSub } from '@/api/system/user';
+import { listDevice, recycleDevice } from '@/api/iot/device';
+import { listProduct } from '@/api/iot/product';
+
+export default {
+    components: {
+        Treeselect,
+    },
+    data() {
+        return {
+            productList: [],
+            total: 0,
+            count: 0,
+            productList: [],
+            selectedCount: 0,
+            queryParams: {
+                productId: null,
+                deviceName: '',
+                pageNum: 1,
+                pageSize: 10,
+                showChild: false,
+                deptId: 0,
+            },
+            //导入表单
+            allotForm: {
+                productId: 0,
+                deptId: 0,
+            },
+            deviceIds: {},
+            // 机构树选项
+            deptOptions: [],
+            add: true,
+            del: true,
+            leftTableData: [],
+            rightTableData: [],
+            selectedListLeft: [], //点击左边选中的
+            selectedListRight: [], //点击右边选中的
+            menuTableData: [],
+            //导入分配表单校验
+            allotRules: {
+                deptId: [{ required: true, message: this.$t('device.device-recycle.864193-14'), trigger: 'change' }],
+            },
+        };
+    },
+    created() {
+        this.getProductList();
+        this.getDeptTree();
+    },
+    computed: {
+        selectedCount1() {
+            return this.rightTableData.length;
+        },
+    },
+    watch: {
+        selectedListLeft: function (val) {
+            if (val.length) {
+                this.add = false;
+            } else {
+                this.add = true;
+            }
+        },
+        selectedListRight: function (val) {
+            if (val.length) {
+                this.del = false;
+            } else {
+                this.del = true;
+            }
+        },
+    },
+    methods: {
+        //查询所有设备列表
+        getList() {
+            this.$refs['allotForm'].validate((valid) => {
+                if (valid) {
+                    this.loading = true;
+                    listDevice(this.queryParams).then((response) => {
+                        this.menuTableData = response.rows;
+                        //isSelect用于判断是否可选
+                        this.menuTableData.map((item) => {
+                            this.leftTableData.push(
+                                Object.assign(item, {
+                                    isSelect: 0,
+                                })
+                            );
+                        });
+                        //分页后保持已选中状态
+                        if (this.rightTableData.length != 0) {
+                            this.menuTableData.forEach((item, index) => {
+                                this.rightTableData.forEach((item1) => {
+                                    if (item1.deviceId == item.deviceId) {
+                                        item.isSelect = 1;
+                                    }
+                                });
+                            });
+                        }
+                        this.total = response.total;
+                        if (this.count === 0) {
+                            this.count = this.total;
+                        }
+                        this.loading = false;
+                    });
+                }
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.queryParams.productId = null;
+            this.rightTableData = [];
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /** 查询产品列表 */
+        getProductList() {
+            this.loading = true;
+            const params = {
+                pageSize: 999,
+            };
+            listProduct(params).then((response) => {
+                this.productList = response.rows.map((item) => {
+                    return { value: item.productId, label: item.productName };
+                });
+            });
+        },
+
+        /** 查询机构下拉树结构 */
+        getDeptTree() {
+            this.queryParams.deptId = null;
+            const showOwner = false;
+            deptsTreeSelectSub(showOwner).then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        /** 返回按钮 */
+        goBack() {
+            const obj = {
+                path: '/iotdev/iot/device',
+            };
+            this.$tab.closeOpenPage(obj);
+        },
+        //右边增加的数据
+        rightAdd() {
+            let leftTableData = JSON.parse(JSON.stringify(this.menuTableData));
+            leftTableData.forEach((item, index) => {
+                this.selectedListLeft.forEach((item1) => {
+                    if (item.deviceId == item1.deviceId) {
+                        this.rightTableData = this.rightTableData.concat(item).sort((a, b) => {
+                            return a.deviceId - b.deviceId;
+                        });
+                        item.isSelect = 1;
+                    }
+                });
+            });
+            if (this.selectedCount1 != 0) {
+                this.count = this.count - this.selectedListLeft.length;
+            }
+            leftTableData = leftTableData.filter((val) => {
+                return val;
+            });
+            this.menuTableData = leftTableData;
+            this.selectedListLeft = [];
+        },
+        //删除的数据
+        leftDelete() {
+            let rightTableData = JSON.parse(JSON.stringify(this.rightTableData));
+            rightTableData.forEach((item, index) => {
+                this.selectedListRight.forEach((item1) => {
+                    this.menuTableData.forEach((item2) => {
+                        if (item2.deviceId == item1.deviceId) {
+                            item2.isSelect = 0;
+                        }
+                        if (item1.deviceId == item.deviceId) {
+                            delete rightTableData[index];
+                        }
+                    });
+                });
+            });
+            if (this.selectedCount1 != 0) {
+                this.count = this.count + this.selectedListRight.length;
+                this.getList();
+            }
+            rightTableData = rightTableData.filter((val) => {
+                return val;
+            });
+            this.rightTableData = rightTableData;
+            this.selectedListRight = [];
+        },
+        checkSelectable(row) {
+            let flag = true;
+            if (row.isSelect === 0) {
+                flag = true;
+            } else {
+                flag = false;
+            }
+            return flag;
+        },
+
+        //左边数据
+        changeCheckBoxValueLeft(val) {
+            this.selectedListLeft = val;
+            this.selectedCount = val.length;
+        },
+        //右边数据
+        changeCheckBoxValueRight(val) {
+            this.selectedListRight = val;
+        },
+        //确定回收操作
+        confirmDistribution() {
+            this.$refs['allotForm'].validate((valid) => {
+                if (valid) {
+                    this.deviceIds = this.rightTableData.map((item) => item.deviceId);
+                    const deviceIds = this.deviceIds.join(',');
+                    const recoveryDeptId = this.queryParams.deptId;
+                    recycleDevice(deviceIds, recoveryDeptId).then((response) => {
+                        if (response.code == 200) {
+                            this.$modal.msgSuccess(response.msg);
+                            this.resetQuery();
+                        } else {
+                            this.$modal.msgError(response.msg);
+                        }
+                    });
+                }
+            });
+        },
+    },
+};
+</script>
+<style lang="scss" scoped>
+.recycle-wrap {
+    height: 100%;
+}
+
+::v-deep {
+    .topCenter {
+        .el-button + .el-button {
+            margin-left: 0px;
+            margin-top: 10px;
+        }
+    }
+}
+
+.changeWords {
+    display: inline-block;
+    margin: 10px 0;
+    font-size: 16px;
+    font-weight: bold;
+}
+
+.general {
+    display: flex;
+    align-items: center;
+}
+
+.topLeft {
+    width: 45%;
+    height: 373px;
+}
+
+.topCenter {
+    width: 10%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.topRight {
+    width: 45%;
+    height: 373px;
+}
+
+.footer {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 999;
+    padding: 20px 50px 20px 360px;
+    margin-bottom: 0 !important;
+    color: #fff;
+    text-align: center;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.08);
+}
+
+.pagination-container {
+    text-align: left;
+    width: 45%;
+    margin: 20px 0 100px 200px;
+}
+</style>

+ 100 - 0
src/views/pms/video_center/device/device-scada.vue

@@ -0,0 +1,100 @@
+<template>
+    <div class="device-scada-wrap">
+        <div v-if="isScada" class="scada" :style="{ height: contentHeight + 'px' }">
+            <component ref="deviceScada" :is="scadaComp" :fullScreemTip="false" :isContextmenu="false"></component>
+        </div>
+        <div v-else>
+            <el-empty :description="$t('device.scada.789543-0')"></el-empty>
+        </div>
+    </div>
+</template>
+
+<script>
+export default {
+    name: 'DeviceScada',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        device: {
+            deep: true,
+            handler(newVal, oldVal) {
+                if (newVal.guid) {
+                    this.getScadaComp();
+                    this.isScada = true;
+                } else {
+                    this.isScada = false;
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            isScada: false,
+            contentHeight: window.innerHeight,
+            scadaComp: null,
+        };
+    },
+    mounted() {
+        const { guid } = this.device;
+        if (guid) {
+            this.getScadaComp();
+            this.isScada = true;
+        } else {
+            this.isScada = false;
+        }
+    },
+    methods: {
+        // 获取窗体高度
+        calculateContentHeight() {
+            let originalHeight = document.getElementById('deviceDetailTab').offsetHeight;
+            this.contentHeight = parseFloat(originalHeight);
+        },
+        // 获取组态
+        getScadaComp() {
+            //以下组态特有
+            // import('../../scada/topo/components/topoRender.vue').then((module) => {
+            //     this.scadaComp = module.default;
+            // });
+            //以上组态特有
+            const { guid, serialNumber } = this.device;
+            this.$router.push({ query: { ...this.$route.query, guid: guid, serialNumber: serialNumber, type: 1 } });
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-scada-wrap {
+    position: relative;
+    width: 100%;
+    height: 100%;
+
+    .scada {
+        position: relative;
+        width: 100%;
+        overflow: auto;
+    }
+
+    ::-webkit-scrollbar-thumb {
+        background-color: #e1e1e1;
+    }
+
+    ::-webkit-scrollbar-thumb:hover {
+        background-color: #a5a2a2;
+    }
+
+    ::-webkit-scrollbar {
+        width: 5px;
+        height: 5px;
+        position: absolute;
+    }
+
+    ::-webkit-scrollbar-track {
+        background-color: #fff;
+    }
+}
+</style>

+ 384 - 0
src/views/pms/video_center/device/device-select-allot.vue

@@ -0,0 +1,384 @@
+<template>
+    <div>
+        <el-card style="margin: 10px">
+            <el-row :gutter="10">
+                <i class="el-icon-arrow-left" @click="goBack()"></i>
+                <el-divider direction="vertical"></el-divider>
+                <span>{{ $t('device.device-select-allot.903153-0') }}</span>
+            </el-row>
+            <el-divider></el-divider>
+            <div style="width: 70%">
+                <el-row>
+                    <el-form ref="queryForm" :model="queryParams">
+                        <el-col :span="6">
+                            <el-form-item prop="productId">
+                                <el-select v-model="queryParams.productId" :placeholder="$t('device.allot-import-dialog.060657-1')" style="width: 95%" filterable clearable>
+                                    <el-option v-for="item in productList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="6">
+                            <el-form-item prop="deviceName">
+                                <el-input :placeholder="$t('device.device-edit.148398-2')" clearable v-model="queryParams.deviceName" style="width: 95%"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="6">
+                            <el-form-item prop="serialNumber">
+                                <el-input :placeholder="$t('device.device-edit.148398-8')" clearable v-model="queryParams.serialNumber" style="width: 95%"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :span="1.5">
+                            <el-button type="primary" icon="el-icon-search" @click="handleQuery">{{ $t('device.device-select-allot.903153-4') }}</el-button>
+                        </el-col>
+                        <el-button icon="el-icon-refresh-left" @click="resetQuery" style="margin-left: 5px">{{ $t('device.device-select-allot.903153-5') }}</el-button>
+                    </el-form>
+                </el-row>
+            </div>
+            <div class="general">
+                <div class="topLeft">
+                    <div style="padding: 10px 10px; background-color: #f8f8f9">
+                        <span style="font-size: 15px; font-weight: bold">{{ $t('device.device-select-allot.903153-6') }}</span>
+                        <span style="font-size: 15px; font-weight: bold; float: right">{{ selectedCount }}/{{ this.count }}</span>
+                    </div>
+                    <el-row>
+                        <el-table :border="false" ref="leftTableData" :data="menuTableData" style="width: 100%" max-height="373" @selection-change="changeCheckBoxValueLeft">
+                            <template slot="empty">
+                                <el-empty :image-size="100" :description="$t('device.device-select-allot.903153-7')"></el-empty>
+                            </template>
+                            <el-table-column type="selection" width="55" :selectable="checkSelectable"></el-table-column>
+                            <el-table-column prop="deviceId" label="DeviceKey" fixed show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="productName" :label="$t('device.allot-record.155854-2')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="serialNumber" :label="$t('device.device-edit.148398-7')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="deviceName" :label="$t('device.device-edit.148398-1')" show-overflow-tooltip></el-table-column>
+                        </el-table>
+                    </el-row>
+                </div>
+                <div class="topCenter">
+                    <el-button type="primary" :disabled="add" @click="rightAdd"><i class="el-icon-arrow-right el-icon--right"></i></el-button>
+                    <el-button type="primary" :disabled="del" @click="leftDelete"><i class="el-icon-arrow-left el-icon--left"></i></el-button>
+                </div>
+                <div class="topRight">
+                    <div style="padding: 10px 10px; background-color: #f8f8f9">
+                        <span style="font-size: 15px; font-weight: bold">{{ $t('device.device-select-allot.903153-11') }}</span>
+                        <span style="font-size: 15px; font-weight: bold; float: right">{{ selectedCount1 }}/500</span>
+                    </div>
+                    <el-row>
+                        <el-table :border="false" ref="rightTableData" :data="rightTableData" style="width: 100%" max-height="373" @selection-change="changeCheckBoxValueRight">
+                            <template slot="empty">
+                                <el-empty :image-size="100" :description="$t('device.device-select-allot.903153-7')"></el-empty>
+                            </template>
+                            <el-table-column type="selection" width="55"></el-table-column>
+                            <el-table-column prop="deviceId" label="DeviceKey" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="productName" :label="$t('device.allot-record.155854-2')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="serialNumber" :label="$t('device.device-edit.148398-7')" show-overflow-tooltip></el-table-column>
+                            <el-table-column prop="deviceName" :label="$t('device.device-edit.148398-1')" show-overflow-tooltip></el-table-column>
+                        </el-table>
+                    </el-row>
+                </div>
+            </div>
+            <div class="pagination-container">
+                <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :pager-count="5" :limit.sync="queryParams.pageSize" :pageSizes="[10, 20, 30, 40]" @pagination="getList" />
+            </div>
+            <div style="width: 100%">
+                <el-form label-position="top" :model="allotForm" ref="allotForm" :rules="allotRules">
+                    <div style="width: 45%; margin: 60px 0">
+                        <el-form-item :label="$t('device.allot-import-dialog.060657-2')" prop="deptId">
+                            <treeselect v-model="allotForm.deptId" :options="deptOptions" :show-count="true" :placeholder="$t('device.allot-import-dialog.060657-3')" :appendToBody="true" z-index="9000" />
+                        </el-form-item>
+                    </div>
+                </el-form>
+            </div>
+        </el-card>
+        <div class="footer">
+            <el-button type="primary" @click="confirmDistribution">{{ $t('device.device-select-allot.903153-14') }}</el-button>
+        </div>
+    </div>
+</template>
+<script type="text/javascript">
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+import { deptsTreeSelect } from '@/api/system/user';
+import { listDevice, distributionDevice } from '@/api/iot/device';
+import { listProduct } from '@/api/iot/product';
+
+export default {
+    components: {
+        Treeselect,
+    },
+    data() {
+        return {
+            total: 0,
+            productList: [],
+            queryParams: {
+                productId: null,
+                deviceName: '',
+                pageNum: 1,
+                pageSize: 10,
+                productName: null,
+                serialNumber: null,
+                showChild: false,
+            },
+            count: 0,
+            //导入表单
+            allotForm: {
+                productId: 0,
+                deptId: 0,
+            },
+            deviceIds: {},
+            selectedCount: 0,
+            // 机构树选项
+            deptOptions: [],
+            selectedRow: null,
+            add: true,
+            del: true,
+            leftTableData: [],
+            rightTableData: [],
+            selectedListLeft: [], //点击左边选中的设备
+            selectedListRight: [], //点击右边选中的设备
+            //设备列表数据
+            menuTableData: [],
+            //导入分配表单校验
+            allotRules: {
+                deptId: [{ required: true, message: this.$t('device.allot-import-dialog.060657-15'), trigger: 'change' }],
+            },
+        };
+    },
+    created() {
+        this.getProductList();
+        this.getDeptTree();
+        this.getList();
+    },
+    computed: {
+        //已选设备数量
+        selectedCount1() {
+            return this.rightTableData.length;
+        },
+    },
+    watch: {
+        selectedListLeft: function (val) {
+            if (val.length) {
+                this.add = false;
+            } else {
+                this.add = true;
+            }
+        },
+        selectedListRight: function (val) {
+            if (val.length) {
+                this.del = false;
+            } else {
+                this.del = true;
+            }
+        },
+    },
+    methods: {
+        //查询所有设备列表
+        getList() {
+            this.loading = true;
+            listDevice(this.queryParams).then((response) => {
+                this.menuTableData = response.rows;
+                //isSelect用于判断是否可选
+                this.menuTableData.map((item) => {
+                    this.leftTableData.push(
+                        Object.assign(item, {
+                            isSelect: 0,
+                        })
+                    );
+                });
+                //分页后保持已选中状态
+                if (this.rightTableData.length != 0) {
+                    this.menuTableData.forEach((item, index) => {
+                        this.rightTableData.forEach((item1) => {
+                            if (item1.deviceId == item.deviceId) {
+                                item.isSelect = 1;
+                            }
+                        });
+                    });
+                }
+                this.total = response.total;
+                if (this.count === 0) {
+                    this.count = this.total;
+                }
+                this.loading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.rightTableData = [];
+            this.queryParams.productId = null;
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /** 查询产品列表 */
+        getProductList() {
+            this.loading = true;
+            const params = {
+                pageSize: 999,
+            };
+            listProduct(params).then((response) => {
+                this.productList = response.rows.map((item) => {
+                    return { value: item.productId, label: item.productName };
+                });
+            });
+        },
+        /** 查询机构下拉树结构 */
+        getDeptTree() {
+            this.allotForm.deptId = null;
+            deptsTreeSelect().then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        /** 返回按钮 */
+        goBack() {
+            const obj = {
+                path: '/iotdev/iot/device',
+            };
+            this.$tab.closeOpenPage(obj);
+        },
+        //右边增加的数据
+        rightAdd() {
+            let leftTableData = JSON.parse(JSON.stringify(this.menuTableData));
+            leftTableData.forEach((item, index) => {
+                this.selectedListLeft.forEach((item1) => {
+                    if (item.deviceId == item1.deviceId) {
+                        this.rightTableData = this.rightTableData.concat(item).sort((a, b) => {
+                            return a.deviceId - b.deviceId;
+                        });
+                        item.isSelect = 1;
+                    }
+                });
+            });
+            if (this.selectedCount1 != 0) {
+                this.count = this.count - this.selectedListLeft.length;
+            }
+            leftTableData = leftTableData.filter((val) => {
+                return val;
+            });
+            this.menuTableData = leftTableData;
+            this.selectedListLeft = [];
+        },
+        //删除的数据
+        leftDelete() {
+            let rightTableData = JSON.parse(JSON.stringify(this.rightTableData));
+            rightTableData.forEach((item, index) => {
+                this.selectedListRight.forEach((item1) => {
+                    this.menuTableData.forEach((item2) => {
+                        if (item2.deviceId == item1.deviceId) {
+                            item2.isSelect = 0;
+                        }
+                        if (item1.deviceId == item.deviceId) {
+                            delete rightTableData[index];
+                        }
+                    });
+                });
+            });
+            if (this.selectedCount1 != 0) {
+                this.count = this.count + this.selectedListRight.length;
+                this.getList();
+            }
+            rightTableData = rightTableData.filter((val) => {
+                return val;
+            });
+            this.rightTableData = rightTableData;
+            this.selectedListRight = [];
+        },
+        checkSelectable(row) {
+            let flag = true;
+            if (row.isSelect === 0) {
+                flag = true;
+            } else {
+                flag = false;
+            }
+            return flag;
+        },
+        //左边数据
+        changeCheckBoxValueLeft(val) {
+            this.selectedListLeft = val;
+            this.selectedCount = val.length;
+        },
+        //右边
+        changeCheckBoxValueRight(val) {
+            this.selectedListRight = val;
+        },
+        //确定分配操作
+        confirmDistribution() {
+            this.$refs['allotForm'].validate((valid) => {
+                if (valid) {
+                    this.deviceIds = this.rightTableData.map((item) => item.deviceId);
+                    const deviceIds = this.deviceIds.join(',');
+                    const deptId = this.allotForm.deptId;
+                    distributionDevice(deptId, deviceIds).then((response) => {
+                        if (response.code == 200) {
+                            this.$modal.msgSuccess(response.msg);
+                            this.resetQuery();
+                        } else {
+                            this.$modal.msgError(response.msg);
+                        }
+                    });
+                }
+            });
+        },
+    },
+};
+</script>
+<style lang="scss">
+.topCenter {
+    .el-button + .el-button {
+        margin-left: 0px;
+        margin-top: 10px;
+    }
+}
+
+.vue-treeselect__label {
+    /* 取消加粗 */
+    font-weight: normal;
+    font-size: 14px;
+    /* 取消加粗 */
+}
+
+.general {
+    display: flex;
+    align-items: center;
+}
+
+.topLeft {
+    width: 45%;
+    height: 373px;
+}
+
+.topCenter {
+    width: 10%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+}
+
+.topRight {
+    width: 45%;
+    height: 373px;
+}
+
+.footer {
+    position: fixed;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 999;
+    padding: 20px 50px 20px 360px;
+    margin-bottom: 0 !important;
+    color: #fff;
+    text-align: center;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.08);
+}
+
+.pagination-container {
+    text-align: left;
+    width: 45%;
+    margin-left: 200px;
+    margin-top: 20px;
+}
+</style>

+ 243 - 0
src/views/pms/video_center/device/device-statistic.vue

@@ -0,0 +1,243 @@
+<template>
+    <div class="device-detail-page">
+        <el-card class="main-card">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="75px">
+                <el-form-item :label="$t('device.device-statistic.932674-0')" label-width="120px" v-if="isSubDev">
+                    <el-select v-model="queryParams.slaveId" :placeholder="$t('device.device-statistic.932674-1')" @change="selectSlave">
+                        <el-option v-for="slave in slaveList" :key="slave.slaveId" :label="`${slave.deviceName} (${slave.slaveId})`" :value="slave.slaveId"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-statistic.932674-2')">
+                    <el-date-picker
+                        v-model="daterangeTime"
+                        size="small"
+                        style="width: 240px"
+                        value-format="yyyy-MM-dd"
+                        type="daterange"
+                        range-separator="-"
+                        :start-placeholder="$t('device.device-statistic.932674-3')"
+                        :end-placeholder="$t('device.device-statistic.932674-4')"
+                    ></el-date-picker>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="getListHistory">{{ $t('device.device-statistic.932674-5') }}</el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+
+        <el-card v-for="(item, index) in staticList" :key="index">
+            <div style="margin-bottom: 30px">
+                <el-card shadow="hover" :body-style="{ padding: '10px 0px', overflow: 'auto' }" v-loading="loading">
+                    <div ref="statisticMap" style="height: 300px; width: 1080px"></div>
+                </el-card>
+            </div>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import { listHistory } from '@/api/iot/deviceLog';
+
+export default {
+    name: 'device-statistic',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后
+        device: {
+            handler(newVal) {
+                this.deviceInfo = newVal;
+                if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                    this.isSubDev = this.deviceInfo.subDeviceList && this.deviceInfo.subDeviceList.length > 0;
+                    this.queryParams.slaveId = this.deviceInfo.slaveId;
+                    this.queryParams.serialNumber = this.deviceInfo.serialNumber;
+                    this.slaveList = newVal.subDeviceList;
+                    // 监测数据
+                    if (this.isSubDev) {
+                        this.staticList = this.deviceInfo.cacheThingsModel['properties'].filter((item) => {
+                            return item.tempSlaveId == this.queryParams.slaveId;
+                        });
+                    } else {
+                        this.staticList = this.deviceInfo.staticList;
+                    }
+                    // 加载图表
+                    this.$nextTick(function () {
+                        // 绘制图表
+                        this.getStatistic();
+                    });
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            loading: true,
+            // 设备信息
+            deviceInfo: {},
+            // 统计物模型
+            staticList: [],
+            // 图表集合
+            chart: [],
+            // 激活时间范围
+            daterangeTime: [this.getTime(), this.getTime()],
+            // 查询参数
+            queryParams: {
+                serialNumber: null,
+                identity: '',
+                slaveId: undefined,
+            },
+            // 对象数组类型物模型暂存数据
+            arrayData: [],
+            // 子设备列表
+            slaveList: [],
+            isSubDev: false,
+        };
+    },
+    mounted() {
+        // 获取统计数据
+        // this.getListHistory();
+    },
+    methods: {
+        /** 获取当前日期 **/
+        getTime() {
+            let date = new Date();
+            let y = date.getFullYear();
+            let m = date.getMonth() + 1;
+            let d = date.getDate();
+            m = m < 10 ? '0' + m : m;
+            d = d < 10 ? '0' + d : d;
+            return y + '-' + m + '-' + d;
+        },
+        /* 获取监测历史数据*/
+        getListHistory() {
+            this.loading = true;
+            this.queryParams.serialNumber = this.queryParams.slaveId ? this.deviceInfo.serialNumber + '_' + this.queryParams.slaveId : this.deviceInfo.serialNumber;
+            if (null != this.daterangeTime && '' != this.daterangeTime) {
+                this.queryParams.beginTime = this.daterangeTime[0] + ' 00:00:00';
+                this.queryParams.endTime = this.daterangeTime[1] + ' 23:59:59';
+            }
+            listHistory(this.queryParams).then((res) => {
+                for (let key in res.data) {
+                    for (let i = 0; i < this.staticList.length; i++) {
+                        if (key == this.staticList[i].id) {
+                            // 对象转数组
+                            let dataList = [];
+                            for (let j = 0; j < res.data[key].length; j++) {
+                                let item = [];
+                                item[0] = res.data[key][j].time;
+                                item[1] = res.data[key][j].value;
+                                dataList.push(item);
+                            }
+                            // 图表显示数据
+                            this.chart[i].setOption({
+                                series: [
+                                    {
+                                        data: dataList,
+                                    },
+                                ],
+                            });
+                        }
+                    }
+                }
+                this.loading = false;
+            });
+        },
+
+        /**监测统计数据 */
+        getStatistic() {
+            let color = ['#1890FF', '#91CB74', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'];
+            for (let i = 0; i < this.staticList.length; i++) {
+                // 设置宽度
+                this.$refs.statisticMap[i].style.width = document.documentElement.clientWidth - 510 + 'px';
+                this.chart[i] = this.$echarts.init(this.$refs.statisticMap[i]);
+                var option;
+                option = {
+                    animationDurationUpdate: 3000,
+                    tooltip: {
+                        trigger: 'axis',
+                    },
+                    title: {
+                        left: 'center',
+                        text:
+                            this.staticList[i].name +
+                            '统计 (单位 ' +
+                            (this.staticList[i].datatype && this.staticList[i].datatype.unit != undefined ? this.staticList[i].datatype.unit : this.$t('device.device-statistic.932674-7')) +
+                            ')',
+                    },
+                    grid: {
+                        top: '80px',
+                        left: '40px',
+                        right: '20px',
+                        bottom: '60px',
+                        containLabel: true,
+                    },
+                    toolbox: {
+                        feature: {
+                            dataZoom: {
+                                yAxisIndex: 'none',
+                            },
+                            restore: {},
+                            saveAsImage: {},
+                        },
+                    },
+                    xAxis: {
+                        type: 'time',
+                    },
+                    yAxis: {
+                        type: 'value',
+                        scale: true, //自适应
+                        axisLabel: {
+                            formatter: function (value, index) {
+                                return value.toFixed(2);
+                            },
+                        }, //可根据实际情况修改y轴保留小数位数
+                    },
+                    dataZoom: [
+                        {
+                            type: 'inside',
+                            start: 0,
+                            end: 100,
+                        },
+                        {
+                            start: 0,
+                            end: 100,
+                        },
+                    ],
+                    series: [
+                        {
+                            name: this.staticList[i].name,
+                            type: 'line',
+                            symbol: 'none',
+                            sampling: 'lttb',
+                            itemStyle: {
+                                color: i > 9 ? color[0] : color[i],
+                            },
+                            areaStyle: {},
+                            data: [],
+                        },
+                    ],
+                };
+                option && this.chart[i].setOption(option);
+            }
+        },
+
+        /*选择从机*/
+        selectSlave() {
+            this.staticList = this.deviceInfo.cacheThingsModel['properties'].filter((item) => {
+                return item.tempSlaveId == this.queryParams.slaveId;
+            });
+            // 加载图表
+            this.$nextTick(function () {
+                // 绘制图表
+                this.getStatistic();
+                // 获取统计数据
+                this.getListHistory();
+            });
+        },
+    },
+};
+</script>

+ 200 - 0
src/views/pms/video_center/device/device-sub.vue

@@ -0,0 +1,200 @@
+<template>
+    <div class="app-container">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item>
+                <el-button plain type="primary" icon="el-icon-plus" size="mini" @click="handleAdd">{{ $t('device.sub.083943-0') }}</el-button>
+            </el-form-item>
+            <el-form-item v-if="this.ids.length > 0">
+                <el-button plain type="danger" icon="el-icon-delete" size="mini" @click="handleDelete">{{ $t('device.sub.083943-1') }}</el-button>
+            </el-form-item>
+            <el-form-item>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('refresh') }}</el-button>
+                <span style="font-size: 12px">{{ $t('device.device-sub.299018-28') }}</span>
+            </el-form-item>
+            <el-form-item v-if="!isSet" style="float: right">
+                <el-button plain type="primary" icon="el-icon-edit" size="mini" @click="setSubDeviceAddress">{{ $t('device.sub.083943-2') }}</el-button>
+            </el-form-item>
+            <el-form-item v-if="isSet" style="float: right">
+                <el-button plain type="primary" icon="el-icon-edit" size="mini" @click="saveSetting">{{ $t('save') }}</el-button>
+            </el-form-item>
+            <el-form-item v-if="isSet" style="float: right">
+                <el-button plain type="info" icon="el-icon-edit" size="mini" @click="cancelSetting">{{ $t('cancel') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table :border="false" v-loading="loading" :data="deviceList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column :label="$t('device.device-edit.148398-1')" align="center" prop="subDeviceName" />
+            <el-table-column :label="$t('device.device-edit.148398-7')" align="center" prop="subDeviceNo" />
+            <el-table-column :label="$t('device.sub.083943-3')" align="center" prop="slaveId" width="180px">
+                <template slot-scope="scope">
+                    <el-input style="width: 120px; text-align: center" :disabled="!isSet" v-model="scope.row.slaveId" size="small" :placeholder="$t('device.sub.083943-2')"></el-input>
+                </template>
+            </el-table-column>
+            <!--            <el-table-column label="在线状态" align="center" prop="status">-->
+            <!--                <template slot-scope="scope">-->
+            <!--                    <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />-->
+            <!--                </template>-->
+            <!--            </el-table-column>-->
+            <el-table-column :label="$t('creatTime')" align="center" prop="createTime" width="180">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+        <!-- 子设备列表 -->
+        <subDeviceList ref="subDeviceList" :gateway="gateway" @addSuccess="addSuccess"></subDeviceList>
+    </div>
+</template>
+
+<script>
+import { listGateway, delGateway, editGatewayBatch } from '@/api/iot/gateway';
+
+import subDeviceList from './sub-device-list';
+
+export default {
+    name: 'device-sub',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    components: {
+        subDeviceList,
+    },
+    dicts: ['iot_device_status'],
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 设备表格数据
+            deviceList: [],
+            deviceInfo: {},
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+            },
+            // 表单参数
+            form: {},
+            gateway: {},
+            //是否可设置子设备地址
+            isSet: false,
+        };
+    },
+
+    watch: {
+        device: {
+            handler(newVal) {
+                this.deviceInfo = newVal;
+                if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
+                    this.gateway.gwDeviceId = this.deviceInfo.deviceId;
+                    this.queryParams.gwDeviceId = this.deviceInfo.deviceId;
+                    this.getList();
+                }
+            },
+        },
+    },
+
+    methods: {
+        /** 查询子设备列表 */
+        getList() {
+            // this.loading = true;
+            listGateway(this.queryParams).then((response) => {
+                this.deviceList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                id: null,
+                gwDeviceId: null,
+                subDeviceId: null,
+                slaveId: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+            };
+            this.resetForm('form');
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.getList();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.id);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+
+        /** 新增按钮操作 */
+        handleAdd() {
+            //刷新子组件
+            this.$refs.subDeviceList.openDeviceList = true;
+            this.$refs.subDeviceList.getList();
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const deviceIds = row.deviceId || this.ids;
+            this.$modal
+                .confirm(this.$t('device.device-sub.299018-26', [deviceIds]))
+                .then(function () {
+                    return delGateway(deviceIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('device.device-sub.299018-27'));
+                })
+                .catch(() => {});
+        },
+        /**设置子设备地址*/
+        setSubDeviceAddress() {
+            this.isSet = !this.isSet;
+        },
+        /**保存子设备地址设置*/
+        saveSetting() {
+            this.isSet = !this.isSet;
+            editGatewayBatch(this.deviceList).then((response) => {
+                this.getList();
+                this.$modal.msgSuccess(this.$t('saveSuccess'));
+            });
+        },
+        cancelSetting() {
+            this.isSet = !this.isSet;
+        },
+        /**添加成功 */
+        addSuccess() {
+            this.getList();
+        },
+    },
+};
+</script>

+ 975 - 0
src/views/pms/video_center/device/device-timer.vue

@@ -0,0 +1,975 @@
+<template>
+    <div class="device-timer-wrap device-detail-page">
+        <el-card class="main-card" style="padding: 0" body-style="padding-bottom: 0">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="70px">
+                <el-form-item :label="$t('device.device-timer.433369-0')" prop="jobName">
+                    <el-input v-model="queryParams.jobName" :placeholder="$t('device.device-timer.433369-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item :label="$t('device.device-timer.433369-2')" prop="status">
+                    <el-select v-model="queryParams.status" :placeholder="$t('device.device-timer.433369-3')" clearable size="small">
+                        <el-option v-for="dict in dict.type.sys_job_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.device-timer.433369-4') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.device-timer.433369-5') }}</el-button>
+                </el-form-item>
+            </el-form>
+        </el-card>
+        <el-card class="main-card" shadow="never" body-style="padding-top: 0">
+            <div class="card-toolbar mb8">
+                <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:device:timer:add']">{{ $t('device.device-timer.433369-6') }}</el-button>
+            </div>
+            <el-table class="base-table" :border="false" v-loading="loading" :data="jobList" @selection-change="handleSelectionChange" size="mini">
+                <el-table-column type="selection" width="55" align="center" />
+                <el-table-column :label="$t('device.device-timer.433369-7')" align="center" prop="jobName" :show-overflow-tooltip="true" />
+                <el-table-column :label="$t('device.device-timer.433369-8')" align="center" prop="cronText">
+                    <template slot-scope="scope">
+                        <div v-html="formatCronDisplay(scope.row)"></div>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-timer.433369-9')" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
+                <el-table-column :label="$t('device.device-timer.433369-10')" align="left" prop="actions" :show-overflow-tooltip="true" width="260">
+                    <template slot-scope="scope">
+                        <div style="overflow: hidden; white-space: nowrap" v-html="formatActionsDisplay(scope.row.actions)"></div>
+                    </template>
+                </el-table-column>
+
+                <el-table-column :label="$t('device.device-timer.433369-11')" align="center">
+                    <template slot-scope="scope">
+                        <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" :active-text="$t('device.device-timer.433369-12')" @change="handleStatusChange(scope.row)"></el-switch>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('opation')" align="center" class-name="small-padding fixed-width">
+                    <template slot-scope="scope">
+                        <el-button size="small" type="primary" plain style="padding: 5px" icon="el-icon-view" @click="handleUpdate(scope.row)" v-hasPermi="['iot:device:timer:query']">
+                            {{ $t('device.device-timer.433369-14') }}
+                        </el-button>
+                        <el-button size="small" type="primary" plain style="padding: 5px" icon="el-icon-caret-right" @click="handleView(scope.row)" v-hasPermi="['iot:device:timer:query']">
+                            {{ $t('device.device-timer.433369-15') }}
+                        </el-button>
+                        <br />
+                        <el-button size="small" type="danger" plain style="padding: 5px" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['iot:device:timer:remove']">
+                            {{ $t('device.device-timer.433369-16') }}
+                        </el-button>
+                        <el-button size="small" type="primary" plain style="padding: 5px" icon="el-icon-caret-right" @click="handleRun(scope.row)" v-hasPermi="['iot:device:timer:execute']">
+                            {{ $t('device.device-timer.433369-17') }}
+                        </el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <pagination v-show="total > 0" :limit.sync="queryParams.pageSize" :page.sync="queryParams.pageNum" :total="total" @pagination="getList" />
+        </el-card>
+        <!-- 添加或修改定时定时对话框 -->
+        <el-dialog :close-on-click-modal="false" :close-on-press-escape="false" :title="title" :visible.sync="open" append-to-body class="device-timer-config-dialog" width="800px">
+            <div class="el-divider el-divider--horizontal" style="margin-top: -25px"></div>
+            <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+                <el-form-item :label="$t('device.device-timer.433369-0')" prop="jobName">
+                    <el-input v-model="form.jobName" :placeholder="$t('device.device-timer.433369-1')" style="width: 280px" />
+                </el-form-item>
+                <el-form-item :label="$t('device.device-timer.433369-18')" prop="timerTimeValue">
+                    <el-time-picker
+                        v-model="timerTimeValue"
+                        value-format="HH:mm"
+                        format="HH:mm"
+                        :placeholder="$t('device.device-timer.433369-19')"
+                        :disabled="form.isAdvance == 1"
+                        style="width: 280px"
+                        @change="timeChange"
+                    ></el-time-picker>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-timer.433369-20')" prop="timerWeek">
+                    <el-row>
+                        <el-col :span="18">
+                            <el-select v-model="timerWeekValue" :placeholder="$t('device.device-timer.433369-21')" multiple style="width: 100%" @change="weekChange" :disabled="form.isAdvance == 1">
+                                <el-option v-for="item in timerWeeks" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                            </el-select>
+                        </el-col>
+                    </el-row>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-timer.433369-22')" prop="cron">
+                    <el-row>
+                        <el-col :span="18">
+                            <el-input v-model="form.cronExpression" :placeholder="$t('device.device-timer.433369-23')" :disabled="form.isAdvance == 0">
+                                <template slot="append">
+                                    <el-button type="primary" @click="handleShowCron" :disabled="form.isAdvance == 0">
+                                        {{ $t('device.device-timer.433369-24') }}
+                                        <i class="el-icon-time el-icon--right"></i>
+                                    </el-button>
+                                </template>
+                            </el-input>
+                        </el-col>
+                        <el-col :span="4" :offset="1">
+                            <el-checkbox v-model="form.isAdvance" :true-label="1" :false-label="0" @change="customerCronChange">{{ $t('device.device-timer.433369-25') }}</el-checkbox>
+                        </el-col>
+                    </el-row>
+                </el-form-item>
+                <el-form-item :label="$t('device.device-timer.433369-2')" prop="status">
+                    <el-radio-group v-model="form.status">
+                        <el-radio v-for="dict in dict.type.sys_job_status" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+
+                <div style="height: 1px; background-color: #ddd; margin: 0 0 20px 0"></div>
+                <el-form-item class="action-wrap" :label="$t('device.device-timer.433369-26')" prop="actions">
+                    <div class="item-wrap" v-for="(actionItem, index) in actionList" :key="index + 'action'">
+                        <el-row :gutter="16">
+                            <el-col :span="5">
+                                <el-select v-model="actionItem.type" :placeholder="$t('device.device-timer.433369-27')" size="small" @change="handleActionTypeChange($event, index)">
+                                    <el-option v-for="(subItem, subIndex) in modelTypes" :key="subIndex + 'type'" :label="subItem.label" :value="subItem.value"></el-option>
+                                </el-select>
+                            </el-col>
+                            <el-col :span="10">
+                                <el-select
+                                    style="width: 100%"
+                                    v-model="actionItem.parentId"
+                                    :placeholder="$t('device.device-timer.433369-28')"
+                                    v-if="actionItem.type == 1"
+                                    size="small"
+                                    @change="handleActionParentModelChange($event, index)"
+                                >
+                                    <el-option v-for="(subItem, subIndex) in thingsModel.properties" :key="subIndex + 'property'" :label="subItem.name" :value="subItem.id"></el-option>
+                                </el-select>
+                                <el-select
+                                    style="width: 100%"
+                                    v-model="actionItem.parentId"
+                                    :placeholder="$t('device.device-timer.433369-28')"
+                                    v-else-if="actionItem.type == 2"
+                                    size="small"
+                                    @change="handleActionParentModelChange($event, index)"
+                                >
+                                    <el-option v-for="(subItem, subIndex) in thingsModel.functions" :key="subIndex + 'func'" :label="subItem.name" :value="subItem.id"></el-option>
+                                </el-select>
+                            </el-col>
+                            <div class="delete-wrap">
+                                <el-button v-if="index !== 0" size="small" plain type="danger" style="padding: 5px" icon="el-icon-delete" @click="handleRemoveActionItem(index)">
+                                    {{ $t('device.device-timer.433369-16') }}
+                                </el-button>
+                            </div>
+                        </el-row>
+
+                        <!--数组索引/物模型/{{ $t('device.device-timer.433369-29') }}-->
+                        <el-row :gutter="16">
+                            <el-col v-if="actionItem.parentModel && actionItem.parentModel.datatype.type === 'array'" :span="5">
+                                <el-select v-model="actionItem.arrayIndex" :placeholder="$t('device.device-timer.433369-21')" size="small" @change="handleActionIndexChange($event, index)">
+                                    <el-option v-for="subItem in actionItem.parentModel.datatype.arrayModel" :key="subItem.id" :label="subItem.name" :value="subItem.id"></el-option>
+                                </el-select>
+                            </el-col>
+                            <el-col v-if="actionItem.parentModel && actionItem.parentModel.datatype.type === 'array' && actionItem.parentModel.datatype.arrayType === 'object'" :span="5">
+                                <el-select v-model="actionItem.id" :placeholder="$t('device.device-timer.433369-21')" size="small" @change="handleActionModelChange($event, index)">
+                                    <el-option v-for="(subItem, subIndex) in actionItem.parentModel.datatype.params" :key="subIndex" :label="subItem.name" :value="subItem.id"></el-option>
+                                </el-select>
+                            </el-col>
+                            <el-col v-if="actionItem.parentModel && actionItem.parentModel.datatype.type === 'object'" :span="5">
+                                <el-select v-model="actionItem.id" :placeholder="$t('device.device-timer.433369-21')" size="small" @change="handleActionModelChange($event, index)">
+                                    <el-option v-for="(subItem, subIndex) in actionItem.parentModel.datatype.params" :key="subIndex" :label="subItem.name" :value="subItem.id"></el-option>
+                                </el-select>
+                            </el-col>
+                            <el-col :span="10" v-if="actionItem.model">
+                                <div v-if="actionItem.model.datatype.type == 'integer' || actionItem.model.datatype.type == 'decimal'">
+                                    <el-input
+                                        v-model="actionItem.value"
+                                        :min="actionItem.model.datatype.min"
+                                        :placeholder="$t('device.device-timer.433369-29')"
+                                        :max="actionItem.model.datatype.max"
+                                        size="small"
+                                        style="vertical-align: baseline"
+                                        type="number"
+                                    >
+                                        <template slot="append">{{ actionItem.model.datatype.unit }}</template>
+                                    </el-input>
+                                </div>
+                                <div v-else-if="actionItem.model.datatype.type == 'bool'">
+                                    <el-switch
+                                        v-model="actionItem.value"
+                                        :active-value="1"
+                                        :active-text="actionItem.model.datatype.trueText"
+                                        :inactive-text="actionItem.model.datatype.falseText"
+                                        :inactive-value="0"
+                                        style="vertical-align: baseline"
+                                    ></el-switch>
+                                </div>
+                                <div v-else-if="actionItem.model.datatype.type == 'enum'">
+                                    <el-select v-model="actionItem.value" :placeholder="$t('device.device-timer.433369-21')" style="width: 100%" size="small">
+                                        <el-option v-for="(subItem, subIndex) in actionItem.model.datatype.enumList" :key="subIndex + 'things'" :label="subItem.text" :value="subItem.value"></el-option>
+                                    </el-select>
+                                </div>
+                                <div v-else-if="actionItem.model.datatype.type == 'string'">
+                                    <el-input v-model="actionItem.value" :placeholder="$t('device.device-timer.433369-30')" :max="actionItem.model.datatype.maxLength" size="small" />
+                                </div>
+                            </el-col>
+                        </el-row>
+                    </div>
+                    <div>
+                        +
+                        <a style="color: #409eff" @click="handleAddActionItem()">{{ $t('device.device-timer.433369-31') }}</a>
+                    </div>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="handleSubmitForm" :loading="submitButtonLoading" v-hasPermi="['iot:device:timer:add']" v-show="!form.jobId">{{ $t('device.device-timer.433369-32') }}</el-button>
+                <el-button type="primary" @click="handleSubmitForm" :loading="submitButtonLoading" v-hasPermi="['iot:device:timer:edit']" v-show="form.jobId">{{ $t('device.device-timer.433369-33') }}</el-button>
+                <el-button @click="handleCancel">{{ $t('cancel') }}</el-button>
+            </div>
+        </el-dialog>
+
+        <el-dialog :title="$t('device.device-timer.433369-35')" :visible.sync="openCron" append-to-body destroy-on-close class="scrollbar">
+            <crontab :expression="expression" style="padding-bottom: 80px" @fill="crontabFill" @hide="openCron = false"></crontab>
+        </el-dialog>
+
+        <!-- 定时日志详细 -->
+        <el-dialog :title="$t('device.device-timer.433369-15')" :visible.sync="openView" width="700px" append-to-body>
+            <el-form ref="form" :model="form" label-width="120px" size="mini">
+                <el-row>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-36')">{{ form.jobId }}</el-form-item>
+                        <el-form-item :label="$t('device.device-timer.433369-37')">{{ form.jobName }}</el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-38')">{{ jobGroupFormat(form) }}</el-form-item>
+                        <el-form-item :label="$t('device.device-timer.433369-39')">{{ form.createTime }}</el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-40')">
+                            <div v-if="form.concurrent == 0">{{ $t('device.device-timer.433369-41') }}</div>
+                            <div v-else-if="form.concurrent == 1">{{ $t('device.device-timer.433369-42') }}</div>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-43')">{{ form.cronExpression }}</el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-44')">
+                            <div v-if="form.misfirePolicy == 0">{{ $t('device.device-timer.433369-45') }}</div>
+                            <div v-else-if="form.misfirePolicy == 1">{{ $t('device.device-timer.433369-46') }}</div>
+                            <div v-else-if="form.misfirePolicy == 2">{{ $t('device.device-timer.433369-17') }}</div>
+                            <div v-else-if="form.misfirePolicy == 3">{{ $t('device.device-timer.433369-47') }}</div>
+                        </el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-48')">{{ parseTime(form.nextValidTime) }}</el-form-item>
+                    </el-col>
+                    <el-col :span="12">
+                        <el-form-item :label="$t('device.device-timer.433369-49')">
+                            <div v-if="form.status == 0">{{ $t('device.device-timer.433369-50') }}</div>
+                            <div v-else-if="form.status == 1">{{ $t('device.device-timer.433369-51') }}</div>
+                        </el-form-item>
+                    </el-col>
+
+                    <el-col :span="24">
+                        <el-form-item :label="$t('device.device-timer.433369-52')">
+                            <div style="border: 1px solid #ddd; padding: 10px; border-radius: 5px; width: 465px" v-html="formatActionsDisplay(form.actions)"></div>
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="openView = false">{{ $t('device.device-timer.433369-53') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from '@/api/iot/deviceJob';
+import Crontab from '@/components/Crontab';
+
+export default {
+    components: {
+        Crontab,
+    },
+    name: 'device-timer',
+    dicts: ['sys_job_group', 'sys_job_status'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后
+        device: function (newVal, oldVal) {
+            this.deviceInfo = newVal;
+            this.initThingsModel();
+        },
+    },
+    data() {
+        return {
+            deviceInfo: {}, // 设备
+            actionList: [], // 动作列表
+            thingsModel: {}, // 物模型JSON
+            // 遮罩层
+            loading: false,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 定时定时表格数据
+            jobList: [],
+            timerWeeks: [
+                {
+                    value: 1,
+                    label: '周一',
+                },
+                {
+                    value: 2,
+                    label: '周二',
+                },
+                {
+                    value: 3,
+                    label: '周三',
+                },
+                {
+                    value: 4,
+                    label: '周四',
+                },
+                {
+                    value: 5,
+                    label: '周五',
+                },
+                {
+                    value: 6,
+                    label: '周六',
+                },
+                {
+                    value: 7,
+                    label: '周日',
+                },
+            ],
+
+            timerWeekValue: [],
+            timerTimeValue: '',
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 是否显示详细弹出层
+            openView: false,
+            // 是否显示Cron表达式弹出层
+            openCron: false,
+            // 传入的表达式
+            expression: '',
+            // 提交按钮加载
+            submitButtonLoading: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                deviceId: 0,
+                jobName: undefined,
+                jobGroup: undefined,
+                status: undefined,
+            },
+            // 物模型类别
+            modelTypes: [
+                {
+                    value: 1,
+                    label: this.$t('device.device-timer.433369-61'),
+                },
+                {
+                    value: 2,
+                    label: this.$t('device.device-timer.433369-62'),
+                },
+            ],
+            // 表单参数
+            form: {},
+            // 表单校验
+            rules: {
+                jobName: [
+                    {
+                        required: true,
+                        message: this.$t('device.device-timer.433369-63'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+        };
+    },
+    created() {
+        this.getList();
+        this.deviceInfo = this.device;
+        this.initThingsModel();
+    },
+    methods: {
+        initThingsModel() {
+            if (this.deviceInfo && this.deviceInfo.deviceId !== 0) {
+                this.thingsModel = this.formatArrayIndex(this.deviceInfo.cacheThingsModel);
+
+                // 过滤监测数据和只读数据
+                if (this.thingsModel.properties && this.thingsModel.properties.length !== 0) {
+                    this.thingsModel.properties = this.thingsModel.properties.filter((item) => {
+                        if (item.datatype.params && item.datatype.params.length !== 0) {
+                            item.datatype.params = item.datatype.params.filter((item) => item.isMonitor == 0 && item.isReadonly == 0);
+                        }
+                        return item.isMonitor == 0 && item.isReadonly == 0;
+                    });
+                }
+                if (this.thingsModel.functions && this.thingsModel.functions.length !== 0) {
+                    this.thingsModel.functions = this.thingsModel.functions.filter((item) => {
+                        if (item.datatype.params && item.datatype.params.length !== 0) {
+                            item.datatype.params = item.datatype.params.filter((item) => item.isMonitor == 0 && item.isReadonly == 0);
+                        }
+                        return item.isMonitor == 0 && item.isReadonly == 0;
+                    });
+                }
+                this.queryParams.deviceId = this.deviceInfo.deviceId;
+            }
+        },
+        // 查询定时定时列表
+        getList() {
+            this.loading = true;
+            listJob(this.queryParams).then((response) => {
+                this.jobList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 定时组名字典翻译
+        jobGroupFormat(row, column) {
+            return this.selectDictLabel(this.dict.type.sys_job_group, row.jobGroup);
+        },
+        // 搜索按钮操作
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        // 重置按钮操作
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.jobId);
+            this.single = selection.length != 1;
+            this.multiple = !selection.length;
+        },
+        // 定时状态修改
+        handleStatusChange(row) {
+            let text = row.status === '0' ? this.$t('device.device-timer.433369-12') : this.$t('device.device-timer.433369-64');
+            this.$modal
+                .confirm(this.$t('device.device-timer.433369-65', [text + '""' + row.jobName]))
+                .then(function () {
+                    return changeJobStatus(row.jobId, row.status);
+                })
+                .then(() => {
+                    this.$modal.msgSuccess(text + this.$t('device.device-timer.433369-67'));
+                })
+                .catch(function () {
+                    row.status = row.status === '0' ? '1' : '0';
+                });
+        },
+        /* 立即执行一次 */
+        handleRun(row) {
+            this.$modal
+                .confirm(this.$t('device.device-timer.433369-68', [row.jobName]))
+                .then(function () {
+                    return runJob(row.jobId, row.jobGroup);
+                })
+                .then(() => {
+                    this.$modal.msgSuccess(this.$t('device.device-timer.433369-69'));
+                })
+                .catch(() => {});
+        },
+        /** 定时详细信息 */
+        handleView(row) {
+            getJob(row.jobId).then((response) => {
+                this.form = response.data;
+                this.openView = true;
+            });
+        },
+        /** cron表达式按钮操作 */
+        handleShowCron() {
+            this.expression = this.form.cronExpression;
+            this.openCron = true;
+        },
+        /** 确定后回传值 */
+        crontabFill(value) {
+            this.form.cronExpression = value;
+        },
+        // 新增按钮操作
+        handleAdd() {
+            this.reset();
+            this.open = true;
+            this.title = this.$t('device.device-timer.433369-70');
+        },
+        // 取消按钮
+        handleCancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                jobId: undefined,
+                jobName: undefined,
+                cronExpression: undefined,
+                status: '0',
+                jobGroup: 'DEFAULT', // 定时分组
+                misfirePolicy: 2, // 1=立即执行,2=执行一次,3=放弃执行
+                concurrent: 1, // 是否并发,1=禁止,0=允许
+                isAdvance: 0, // 是否详细cron表达式
+                jobType: 1, // 任务类型 1=设备定时,2=设备告警,3=场景联动
+                productId: 0,
+                productName: '',
+                sceneId: 0, //场景ID
+                alertId: 0, // 告警ID
+                actions: '',
+            };
+            this.submitButtonLoading = false;
+            this.timerWeekValue = [1, 2, 3, 4, 5, 6, 7];
+            this.timerTimeValue = '';
+            this.actionList = [
+                {
+                    id: '',
+                    name: '',
+                    value: '',
+                    valueName: '',
+                    type: 1,
+                    parentId: '',
+                    parentName: '',
+                    arrayIndex: '',
+                    arrayIndexName: '',
+                    model: null,
+                },
+            ];
+            // 物模型项,对应actions
+            this.resetForm('form');
+        },
+        // 修改按钮操作
+        handleUpdate(row) {
+            this.reset();
+            const jobId = row.jobId || this.ids;
+            getJob(jobId).then((response) => {
+                this.form = response.data;
+                // actionList赋值
+                this.actionList = JSON.parse(this.form.actions);
+                for (let i = 0; i < this.actionList.length; i++) {
+                    if (this.actionList[i].type == 1) {
+                        this.setParentAndModelData(this.actionList[i], this.thingsModel.properties);
+                    } else if (this.actionList[i].type == 2) {
+                        this.setParentAndModelData(this.actionList[i], this.thingsModel.functions);
+                    }
+                }
+                if (this.form.isAdvance == 0) {
+                    let arrayValue = this.form.cronExpression.substring(12).split(',').map(Number);
+                    this.timerWeekValue = arrayValue;
+                    this.timerTimeValue = this.form.cronExpression.substring(5, 7) + ':' + this.form.cronExpression.substring(2, 4);
+                }
+                this.open = true;
+                this.title = this.$t('device.device-timer.433369-71');
+            });
+        },
+        // 设置父级物模型和物模型值
+        setParentAndModelData(sceneScript, sceneScripts) {
+            for (let i = 0; i < sceneScripts.length; i++) {
+                if (sceneScript.parentId == sceneScripts[i].id) {
+                    sceneScript.parentModel = sceneScripts[i];
+                    if (sceneScript.parentModel.datatype.type === 'object') {
+                        // 对象类型,物模型赋值
+                        for (let j = 0; j < sceneScript.parentModel.datatype.params.length; j++) {
+                            if (sceneScript.id == sceneScript.parentModel.datatype.params[j].id) {
+                                sceneScript.model = sceneScript.parentModel.datatype.params[j];
+                            }
+                        }
+                    } else if (sceneScript.parentModel.datatype.arrayType === 'object' && sceneScript.parentModel.datatype.type === 'array') {
+                        // 对象数组类型,物模型集合移除索引前缀
+                        if (sceneScript.id.indexOf('array_') != -1) {
+                            sceneScript.id = sceneScript.id.substring(9);
+                        }
+                        // 物模型赋值
+                        for (let j = 0; j < sceneScript.parentModel.datatype.params.length; j++) {
+                            if (sceneScript.id == sceneScript.parentModel.datatype.params[j].id) {
+                                sceneScript.model = sceneScript.parentModel.datatype.params[j];
+                            }
+                        }
+                    } else if (sceneScript.parentModel.datatype.arrayType !== 'object' && sceneScript.parentModel.datatype.type === 'array') {
+                        // 普通数组类型,物模型集合移除索引前缀
+                        if (sceneScript.id.indexOf('array_') != -1) {
+                            sceneScript.id = sceneScript.id.substring(9);
+                        }
+                        sceneScript.model = {
+                            datatype: {
+                                type: sceneScript.parentModel.datatype.arrayType,
+                                maxLength: -1,
+                                min: -1,
+                                max: -1,
+                                unit: this.$t('device.device-timer.433369-72'),
+                            },
+                        };
+                    } else {
+                        // 普通类型
+                        sceneScript.model = sceneScript.parentModel;
+                    }
+                    break;
+                }
+            }
+        },
+        // 删除按钮操作
+        handleDelete(row) {
+            const jobIds = row.jobId || this.ids;
+            this.$modal
+                .confirm(this.$t('device.device-timer.433369-73', [jobIds]))
+                .then(function () {
+                    return delJob(jobIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('device.device-timer.433369-75'));
+                })
+                .catch(() => {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/job/export',
+                {
+                    ...this.queryParams,
+                },
+                `job_${new Date().getTime()}.xlsx`
+            );
+        },
+        /** 星期改变事件 **/
+        weekChange(data) {
+            this.gentCronExpression();
+        },
+        /** 时间改变事件 **/
+        timeChange(data) {
+            this.gentCronExpression();
+        },
+        /**自定义cron表达式选项改变事件 */
+        customerCronChange(data) {
+            if (data == 0) {
+                this.gentCronExpression();
+            }
+        },
+        /** 生成cron表达式**/
+        gentCronExpression() {
+            let hour = '00';
+            let minute = '00';
+            if (this.timerTimeValue != null && this.timerTimeValue != '') {
+                hour = this.timerTimeValue.substring(0, 2);
+                minute = this.timerTimeValue.substring(3);
+            }
+            let week = '*';
+            if (this.timerWeekValue.length > 0) {
+                let order = this.timerWeekValue.slice().sort();
+                for (let i = 0; i < order.length; i++) {
+                    if (order[i] == 7) {
+                        order[i] = 1;
+                    } else {
+                        order[i] = order[i] + 1;
+                    }
+                }
+                console.log(order);
+                week = order;
+            }
+            this.form.cronExpression = '0 ' + minute + ' ' + hour + ' ? * ' + week;
+        },
+        // 格式化显示CRON描述
+        formatCronDisplay(item) {
+            let result = '';
+            if (item.isAdvance == 0) {
+                let time = '<br /><span style="color:#F56C6C">时间 ' + item.cronExpression.substring(5, 7) + ':' + item.cronExpression.substring(2, 4) + '</span>';
+                let week = item.cronExpression.substring(12);
+                if (week == '1,2,3,4,5,6,7') {
+                    result = '每天 ' + time;
+                } else {
+                    let weekArray = week.split(',');
+                    for (let i = 0; i < weekArray.length; i++) {
+                        if (weekArray[i] == '1') {
+                            result = result + this.$t('device.device-timer.433369-78');
+                        } else if (weekArray[i] == '2') {
+                            result = result + this.$t('device.device-timer.433369-79');
+                        } else if (weekArray[i] == '3') {
+                            result = result + this.$t('device.device-timer.433369-80');
+                        } else if (weekArray[i] == '4') {
+                            result = result + this.$t('device.device-timer.433369-81');
+                        } else if (weekArray[i] == '5') {
+                            result = result + this.$t('device.device-timer.433369-82');
+                        } else if (weekArray[i] == '6') {
+                            result = result + this.$t('device.device-timer.433369-83');
+                        } else if (weekArray[i] == '7') {
+                            result = result + this.$t('device.device-timer.433369-84');
+                        }
+                    }
+                    result = result.substring(0, result.length - 1) + ' ' + time;
+                }
+            } else {
+                result = this.$t('device.device-timer.433369-85');
+            }
+            return result;
+        },
+        // 格式化显示动作
+        formatActionsDisplay(json) {
+            if (json == null || json == '') {
+                return;
+            }
+            let actions = JSON.parse(json);
+            let result = '';
+            for (let i = 0; i < actions.length; i++) {
+                if (actions[i].arrayIndexName) {
+                    result =
+                        result +
+                        `${actions[i].parentName} >> ${actions[i].arrayIndexName} >> ${actions[i].name} <span style="color:#F56C6C"> ${actions[i].valueName ? actions[i].valueName : actions[i].value}</span><br />`;
+                } else {
+                    if (actions[i].parentName !== actions[i].name) {
+                        result = result + `${actions[i].parentName} >> ${actions[i].name} <span style="color:#F56C6C">${actions[i].valueName ? actions[i].valueName : actions[i].value}</span><br />`;
+                    } else {
+                        result = result + `${actions[i].name} <span style="color:#F56C6C">${actions[i].valueName ? actions[i].valueName : actions[i].value}</span><br />`;
+                    }
+                }
+            }
+            return result == '' ? this.$t('device.device-timer.433369-86') : result;
+        },
+        // 物模型格式化处理
+        formatArrayIndex(data) {
+            let obj = { ...data };
+            for (let o in obj) {
+                obj[o] = obj[o].map((item) => {
+                    if (item.datatype.type === 'array') {
+                        let arrayModel = [];
+                        for (let k = 0; k < item.datatype.arrayCount; k++) {
+                            let index = k > 9 ? String(k) : '0' + k;
+                            if (item.datatype.arrayType === 'object') {
+                                arrayModel.push({
+                                    id: index,
+                                    name: item.name + ' ' + (k + 1),
+                                });
+                            } else {
+                                arrayModel.push({
+                                    id: index,
+                                    name: item.name + ' ' + (k + 1),
+                                });
+                            }
+                        }
+                        item.datatype.arrayModel = arrayModel;
+                    }
+                    return item;
+                });
+            }
+            return obj;
+        },
+        // 添加执行动作
+        handleAddActionItem() {
+            this.actionList.push({
+                id: '',
+                name: '',
+                value: '',
+                valueName: '',
+                type: 1, // 1=属性,2=功能,3=事件,5=设备上线,6=设备下线
+                parentId: '',
+                parentName: '',
+                arrayIndex: '',
+                arrayIndexName: '',
+                model: null,
+            });
+        },
+        // 删除执行动作
+        handleRemoveActionItem(index) {
+            this.actionList.splice(index, 1);
+        },
+        // 动作类型改变事件
+        handleActionTypeChange(data, index) {
+            this.actionList[index].id = '';
+            this.actionList[index].name = '';
+            this.actionList[index].value = '';
+            this.actionList[index].valueName = '';
+            this.actionList[index].parentId = '';
+            this.actionList[index].parentName = '';
+            this.actionList[index].arrayIndex = '';
+            this.actionList[index].arrayIndexName = '';
+            this.actionList[index].parentModel = null;
+            this.actionList[index].model = null;
+        },
+        // 执行动作物模型数组索引选择
+        handleActionIndexChange(id, index) {
+            this.actionList[index].arrayIndexName = this.actionList[index].parentModel.datatype.arrayModel.find((x) => x.id == id).name;
+            this.actionList[index].value = '';
+            // 数组类型保留id和name
+            if (this.actionList[index].parentModel.datatype.arrayType === 'object') {
+                this.actionList[index].id = '';
+                this.actionList[index].name = '';
+            }
+        },
+        // 动作物模型项改变事件
+        handleActionParentModelChange(identifier, index) {
+            this.actionList[index].model = null;
+            this.actionList[index].value = '';
+            this.actionList[index].arrayIndex = '';
+            this.actionList[index].arrayIndexName = '';
+
+            let scripts = [];
+            if (this.actionList[index].type == 1) {
+                //属性
+                scripts = this.thingsModel.properties;
+            } else if (this.actionList[index].type == 2) {
+                //功能
+                scripts = this.thingsModel.functions;
+            }
+            for (let i = 0; i < scripts.length; i++) {
+                if (scripts[i].id == identifier) {
+                    this.actionList[index].parentName = scripts[i].name;
+                    this.actionList[index].parentModel = scripts[i];
+                    if (scripts[i].datatype.type === 'object') {
+                        // 对象类型
+                        this.actionList[index].id = '';
+                        this.actionList[index].name = '';
+                    } else if (scripts[i].datatype.type === 'array' && scripts[i].datatype.arrayType === 'object') {
+                        // 对象数组类型
+                        this.actionList[index].id = '';
+                        this.actionList[index].name = '';
+                    } else if (scripts[i].datatype.type === 'array' && scripts[i].datatype.arrayType !== 'object') {
+                        // 普通类型,数组类
+                        this.actionList[index].id = scripts[i].id;
+                        this.actionList[index].name = scripts[i].name;
+                        // 用于减少界面判断
+                        this.actionList[index].model = {
+                            datatype: {
+                                type: scripts[i].datatype.arrayType,
+                                maxLength: -1,
+                                min: -1,
+                                max: -1,
+                                unit: this.$t('device.device-timer.433369-72'),
+                            },
+                        };
+                    } else {
+                        // 普通类型,不包含数组类型
+                        this.actionList[index].id = scripts[i].id;
+                        this.actionList[index].name = scripts[i].name;
+                        this.actionList[index].model = scripts[i];
+                    }
+                    break;
+                }
+            }
+        },
+        // 执行动作物模型选择
+        handleActionModelChange(id, index) {
+            this.actionList[index].value = '';
+            let model = null;
+            if (this.actionList[index].parentModel.datatype.type === 'array' || this.actionList[index].parentModel.datatype.type === 'object') {
+                model = this.actionList[index].parentModel.datatype.params.find((item) => item.id == id);
+                this.actionList[index].name = model.name;
+                this.actionList[index].model = model;
+            }
+        },
+        // 提交按钮
+        handleSubmitForm: function () {
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    let actions = [];
+                    if (this.form.isAdvance == 0) {
+                        if (this.timerTimeValue == '' || this.timerTimeValue == null) {
+                            this.$modal.alertError(this.$t('device.device-timer.433369-87'));
+                            return;
+                        }
+                        if (this.timerWeekValue == null || this.timerWeekValue == '') {
+                            this.$modal.alertError(this.$t('device.device-timer.433369-88'));
+                            return;
+                        }
+                    } else if (this.form.isAdvance == 1) {
+                        if (this.form.cronExpression == '') {
+                            this.$modal.alertError(this.$t('device.device-timer.433369-89'));
+                            return;
+                        }
+                    }
+                    for (let i = 0; i < this.actionList.length; i++) {
+                        if (this.actionList[i].value === '') {
+                            this.$modal.alertError(this.$t('device.device-timer.433369-90'));
+                            return;
+                        }
+                        // 数据重组
+                        let item = this.actionList[i];
+                        // id拼接array索引
+                        let id = '';
+                        if (item.arrayIndex != '') {
+                            id = 'array_' + item.arrayIndex + '_' + item.id;
+                        } else {
+                            id = item.id;
+                        }
+                        // 获取valueName
+                        let valueName = '';
+                        if (item.model.datatype.type === 'bool') {
+                            valueName = item.value === 1 ? item.model.datatype.trueText : item.model.datatype.falseText;
+                        } else if (item.model.datatype.type === 'enum') {
+                            valueName = item.model.datatype.enumList.find((subItem) => subItem.value === item.value).text;
+                        } else {
+                            valueName = '';
+                        }
+                        // 只传需要的数据
+                        actions[i] = {
+                            type: item.type,
+                            id: item.id,
+                            name: item.name,
+                            value: item.value,
+                            valueName: valueName,
+                            parentId: item.parentId,
+                            parentName: item.parentName,
+                            arrayIndex: item.arrayIndex,
+                            arrayIndexName: item.arrayIndexName,
+                            deviceId: this.deviceInfo.deviceId,
+                            deviceName: this.deviceInfo.deviceName,
+                        };
+                    }
+                    this.form.actions = JSON.stringify(actions);
+                    // 设备信息
+                    this.form.deviceId = this.deviceInfo.deviceId;
+                    this.form.deviceName = this.deviceInfo.deviceName;
+                    this.form.serialNumber = this.deviceInfo.serialNumber;
+                    this.form.productId = this.deviceInfo.productId;
+                    this.form.productName = this.deviceInfo.productName;
+                    // 按钮等待后端加载完
+                    this.submitButtonLoading = true;
+                    if (this.form.jobId != undefined) {
+                        updateJob(this.form).then(() => {
+                            this.$modal.msgSuccess(this.$t('device.device-timer.433369-91'));
+                            this.submitButtonLoading = false;
+                            this.open = false;
+                            this.getList();
+                        });
+                    } else {
+                        addJob(this.form).then(() => {
+                            this.$modal.msgSuccess(this.$t('device.device-timer.433369-92'));
+                            this.submitButtonLoading = false;
+                            this.open = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-timer-wrap {
+    /* padding-left: 20px; */
+}
+
+.device-timer-config-dialog {
+    .action-wrap {
+        position: relative;
+
+        .item-wrap {
+            margin-bottom: 15px;
+            padding: 10px;
+            background-color: #d9e5f6;
+            border-radius: 5px;
+
+            .delete-wrap {
+                position: absolute;
+                right: 10px;
+                top: 0;
+            }
+        }
+    }
+}
+</style>

+ 393 - 0
src/views/pms/video_center/device/device-user.vue

@@ -0,0 +1,393 @@
+<template>
+    <div class="device-detail-page">
+        <el-card class="main-card">
+            <el-row :gutter="10" class="mb20">
+                <el-col :span="1.5">
+                    <el-button type="primary" plain icon="el-icon-share" size="mini" @click="shareDevice" v-hasPermi="['iot:device:user:share']" :disabled="deviceInfo.isOwner == 1 || deviceInfo.isOwner != null">
+                        分享设备
+                    </el-button>
+                </el-col>
+                <el-col :span="1.5">
+                    <el-button type="primary" plain icon="el-icon-refresh" size="mini" @click="getList">{{ $t('device.device-user.037521-0') }}</el-button>
+                </el-col>
+            </el-row>
+            <el-table class="base-table" :border="false" v-loading="loading" :data="deviceUserList" size="mini" @selection-change="handleSelectionChange">
+                <el-table-column :label="$t('device.device-user.037521-1')" align="center" prop="userId" width="100" />
+                <el-table-column :label="$t('device.device-user.037521-2')" align="center" prop="userName" />
+                <el-table-column :label="$t('device.device-user.037521-3')" align="center" prop="phonenumber" width="150" />
+                <el-table-column :label="$t('device.device-user.037521-4')" align="center" prop="isOwner" width="150">
+                    <template slot-scope="scope">
+                        <el-tag type="primary" v-if="scope.row.isOwner">{{ $t('device.device-user.037521-5') }}</el-tag>
+                        <el-tag type="success" v-else>{{ $t('device.device-user.037521-6') }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-user.037521-7')" align="center" prop="createTime" width="150">
+                    <template slot-scope="scope">
+                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.device-user.037521-8')" align="left" prop="remark" header-align="center" min-width="150" />
+                <el-table-column :label="$t('opation')" align="center" class-name="small-padding fixed-width" width="180">
+                    <template slot-scope="scope">
+                        <el-button
+                            icon="el-icon-view"
+                            type="primary"
+                            plain
+                            style="padding: 5px; margin: 0"
+                            size="small"
+                            @click="handleUpdate(scope.row)"
+                            v-hasPermi="['iot:device:user:query']"
+                            v-if="scope.row.isOwner == 0 && deviceInfo.isOwner == 1"
+                        >
+                            {{ $t('device.device-user.037521-10') }}
+                        </el-button>
+                        <el-button
+                            v-hasPermi="['iot:device:user:remove']"
+                            icon="el-icon-delete"
+                            type="danger"
+                            plain
+                            style="padding: 5px; margin: 0"
+                            size="small"
+                            @click="handleDelete(scope.row)"
+                            v-if="scope.row.isOwner == 0 && deviceInfo.isOwner == 1"
+                        >
+                            {{ $t('device.device-user.037521-11') }}
+                        </el-button>
+                    </template>
+                </el-table-column>
+            </el-table>
+        </el-card>
+        <!--设备分享对话框-->
+        <el-dialog :title="$t('device.device-user.037521-12')" :visible.sync="open" width="800px">
+            <div style="margin-top: -50px">
+                <el-divider></el-divider>
+            </div>
+            <!--用户查询-->
+            <el-form v-if="type == 1" ref="queryForm" :inline="true" :model="permParams" :rules="rules" label-width="80px">
+                <el-form-item :label="$t('device.device-user.037521-3')" prop="phonenumber">
+                    <el-input
+                        type="text"
+                        :placeholder="$t('device.device-user.037521-13')"
+                        v-model="permParams.phonenumber"
+                        minlength="10"
+                        clearable
+                        size="small"
+                        show-word-limit
+                        style="width: 240px"
+                        @keyup.enter.native="handleQuery"
+                    ></el-input>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="userQuery">{{ $t('device.device-user.037521-14') }}</el-button>
+                </el-form-item>
+            </el-form>
+
+            <!--用户信息和权限设置-->
+            <div v-loading="permsLoading" style="background-color: #f8f8f9; line-height: 28px">
+                <div v-if="message" style="padding: 20px">{{ message }}</div>
+                <div v-if="form.userId" style="padding: 15px">
+                    <div style="font-weight: bold; line-height: 28px">{{ $t('device.device-user.037521-15') }}</div>
+                    <span style="width: 80px; display: inline-block">{{ $t('device.device-user.037521-16') }}</span>
+                    <span>{{ form.userId }}</span>
+                    <br />
+                    <span style="width: 80px; display: inline-block">{{ $t('device.device-user.037521-3') }}:</span>
+                    <span>{{ form.phonenumber }}</span>
+                    <br />
+                    <span style="width: 80px; display: inline-block">{{ $t('device.device-user.037521-2') }}:</span>
+                    <span>{{ form.userName }}</span>
+                    <br />
+                    <!--选择权限-->
+                    <div style="font-weight: bold; margin: 15px 0 10px">{{ $t('device.device-user.037521-19') }}</div>
+                    <el-table :border="false" ref="multipleTable" :data="sharePermissionList" highlight-current-row size="mini" @select="handleSelectionChange" @select-all="handleSelectionAll">
+                        <el-table-column align="center" type="selection" width="55" />
+                        <el-table-column :label="$t('device.device-user.037521-20')" align="center" key="modelName" prop="modelName" />
+                        <el-table-column :label="$t('device.device-user.037521-21')" align="center" key="identifier" prop="identifier" />
+                        <el-table-column :label="$t('device.device-edit.148398-17')" align="left" min-width="100" header-align="center" key="remark" prop="remark" />
+                    </el-table>
+                    <!--选择权限-->
+                    <div style="font-weight: bold; margin: 15px 0 10px">{{ $t('device.device-edit.148398-17') }}</div>
+                    <el-input v-model="form.remark" type="textarea" :placeholder="$t('plzInput')" rows="2" />
+                </div>
+            </div>
+            <div slot="footer" class="dialog-footer">
+                <el-button :disabled="!form.userId || !deviceInfo.deviceId" type="primary" @click="submitForm" v-hasPermi="['iot:device:user:edit']">{{ $t('device.device-user.037521-24') }}</el-button>
+                <el-button @click="closeSelectUser">{{ $t('device.device-user.037521-25') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { permListModel } from '@/api/iot/model';
+import { addShare, delShare, getShare, listShare, shareUser, updateShare } from '@/api/iot/share';
+
+export default {
+    name: 'device-user',
+    dicts: ['iot_yes_no'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: {
+            deep: true,
+            handler(newVal, oldVal) {
+                if (newVal.deviceId && newVal.deviceId !== oldVal.deviceId) {
+                    this.deviceInfo = newVal;
+                    this.queryParams.deviceId = newVal.deviceId;
+                    this.getList();
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            // 类型,1=新增,2=更新
+            type: 1,
+            // 消息提示
+            message: '',
+            // 权限遮罩层
+            permsLoading: false,
+            // 权限列表
+            sharePermissionList: [],
+            // 是否显示选择用户弹出层
+            open: false,
+            // 查询参数
+            permParams: {
+                userName: undefined,
+                phonenumber: undefined,
+                deviceId: null,
+            },
+            // 查询表单验证
+            rules: {
+                phonenumber: [
+                    {
+                        required: true,
+                        message: this.$t('device.device-user.037521-26'),
+                        trigger: 'blur',
+                    },
+                    {
+                        min: 11,
+                        max: 11,
+                        message: this.$t('device.device-user.037521-27'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 设备用户表格数据
+            deviceUserList: [],
+            // 设备信息
+            deviceInfo: {},
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                deviceName: null,
+                userName: null,
+                userId: null,
+                tenantName: null,
+                isOwner: null,
+            },
+            // 表单参数
+            form: {},
+        };
+    },
+    created() {
+        this.queryParams.deviceId = this.device.deviceId;
+        this.getList();
+    },
+    methods: {
+        /** 查询设备用户列表 */
+        getList() {
+            this.loading = true;
+            listShare(this.queryParams).then((response) => {
+                this.deviceUserList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                deviceId: null,
+                userId: null,
+                deviceName: null,
+                userName: null,
+                perms: null,
+                phonenumber: null,
+                remark: null,
+            };
+            this.sharePermissionList = [];
+            this.message = '';
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 查看按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            this.type = 2; //更新
+            getShare(row.deviceId, row.userId).then((response) => {
+                this.form = response.data;
+                // 查询物模型权限列表
+                this.getPermissionList();
+                this.open = true;
+            });
+        },
+        // 设备分享
+        shareDevice() {
+            this.type = 1; // 新增
+            this.open = true;
+            this.form = {};
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const params = {
+                deviceId: row.deviceId,
+                userId: row.userId,
+            };
+            this.$modal
+                .confirm(this.$t('device.device-user.037521-28'))
+                .then(function () {
+                    return delShare(params);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('device.device-user.037521-29'));
+                })
+                .catch(() => {});
+        },
+        /** 用户按钮操作 */
+        userQuery() {
+            this.$refs['queryForm'].validate((valid) => {
+                if (valid) {
+                    this.reset();
+                    this.getShareUser();
+                }
+            });
+        },
+        /** 查询用户 */
+        getShareUser() {
+            this.permsLoading = true;
+            if (!this.deviceInfo.deviceId) {
+                this.$modal.alert(this.$t('device.device-user.037521-30'));
+                return;
+            }
+            this.permParams.deviceId = this.deviceInfo.deviceId;
+            shareUser(this.permParams).then((response) => {
+                if (response.data) {
+                    this.form = response.data;
+                    // 查询物模型权限列表
+                    this.getPermissionList();
+                } else {
+                    this.permsLoading = false;
+                    this.message = this.$t('device.device-user.037521-31');
+                }
+            });
+        },
+        /** 查询产品物模型设备权限列表 */
+        async getPermissionList() {
+            let perms = [];
+            if (this.form.perms) {
+                perms = this.form.perms.split(',');
+            }
+            permListModel(this.deviceInfo.productId).then((response) => {
+                // 固定增加设备系统相关权限
+                this.sharePermissionList = [
+                    {
+                        identifier: 'ota',
+                        modelName: this.$t('device.device-user.037521-32'),
+                        remark: this.$t('device.device-user.037521-33'),
+                    },
+                    {
+                        identifier: 'timer',
+                        modelName: this.$t('device.device-user.037521-34'),
+                        remark: this.$t('device.device-user.037521-35'),
+                    },
+                    {
+                        identifier: 'log',
+                        modelName: this.$t('device.device-user.037521-36'),
+                        remark: this.$t('device.device-user.037521-37'),
+                    },
+                    {
+                        identifier: 'monitor',
+                        modelName: this.$t('device.device-user.037521-38'),
+                        remark: this.$t('device.device-user.037521-39'),
+                    },
+                    {
+                        identifier: 'statistic',
+                        modelName: this.$t('device.device-user.037521-40'),
+                        remark: this.$t('device.device-user.037521-41'),
+                    },
+                ];
+                this.sharePermissionList = this.sharePermissionList.concat(response.data);
+
+                // 设置选中
+                if (perms.length > 0) {
+                    for (let i = 0; i < this.sharePermissionList.length; i++) {
+                        for (let j = 0; j < perms.length; j++) {
+                            if (this.sharePermissionList[i].identifier == perms[j]) {
+                                this.$nextTick(() => {
+                                    this.$refs.multipleTable.toggleRowSelection(this.sharePermissionList[i], true);
+                                });
+                                break;
+                            }
+                        }
+                    }
+                }
+                this.permsLoading = false;
+            });
+        },
+        // 重置查询
+        resetUserQuery() {
+            this.resetForm('queryForm');
+            this.reset();
+        },
+        // 关闭选择用户
+        closeSelectUser() {
+            this.open = false;
+            this.resetUserQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.form.perms = selection.map((x) => x.identifier).join(',');
+        },
+        // 全选事件处理
+        handleSelectionAll(selection) {
+            this.form.perms = selection.map((x) => x.identifier).join(',');
+        },
+        /** 提交按钮 */
+        submitForm() {
+            if (this.type == 2) {
+                // 更新设备用户
+                updateShare(this.form).then((response) => {
+                    this.$modal.msgSuccess(this.$t('device.device-user.037521-42'));
+                    this.resetUserQuery();
+                    this.open = false;
+                    this.getList();
+                });
+            } else if (this.type == 1) {
+                // 添加设备用户
+                this.form.deviceId = this.deviceInfo.deviceId;
+                this.form.deviceName = this.deviceInfo.deviceName;
+                addShare(this.form).then((response) => {
+                    this.$modal.msgSuccess(this.$t('device.device-user.037521-43'));
+                    this.resetUserQuery();
+                    this.open = false;
+                    this.getList();
+                });
+            }
+        },
+    },
+};
+</script>

+ 432 - 0
src/views/pms/video_center/device/device-variable.vue

@@ -0,0 +1,432 @@
+<template>
+    <div class="device-variable">
+        <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" label-width="68px">
+            <el-form-item :label="$t('device.variable-case.347856-0')" prop="type">
+                <el-select v-model="queryParams.type" :placeholder="$t('device.variable-case.347856-1')">
+                    <el-option v-for="dict in dict.type.iot_things_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item :label="$t('device.variable-case.347856-2')" prop="modelName">
+                <el-input v-model="queryParams.modelName" :placeholder="$t('device.variable-case.347856-3')" clearable />
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.variable-case.347856-4') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="handleResetQuery">{{ $t('device.variable-case.347856-5') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="activeCollectionAll">{{ $t('device.variable-case.347856-16') }}</el-button>
+            </el-form-item>
+        </el-form>
+        <el-table :border="false" v-loading="loading" :data="variableList" style="width: 100%; margin-top: 5px">
+            <el-table-column prop="identifier" :label="$t('device.variable-case.347856-6')" width="130"></el-table-column>
+            <el-table-column prop="type" :label="$t('device.variable-case.347856-7')" width="100">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.iot_things_type" :value="scope.row.type" />
+                </template>
+            </el-table-column>
+            <el-table-column prop="modelName" :label="$t('device.variable-case.347856-8')"></el-table-column>
+            <el-table-column prop="ts" :label="$t('device.variable-case.347856-9')"></el-table-column>
+            <el-table-column prop="value" :label="$t('device.variable-case.347856-10')">
+                <template slot-scope="scope">
+                    <span>
+                        {{ scope.row.valueName === '' || scope.row.valueName === null ? '-' : scope.row.valueName }} {{ scope.row.unit }}
+                        <i v-if="scope.row.isReadonly === 0" style="cursor: pointer; color: #1890ff" class="el-icon-edit" @click="editFunc(scope.row)"></i>
+                    </span>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.variable-case.347856-11')" align="center" class-name="small-padding fixed-width" width="200">
+                <template slot-scope="scope">
+                    <el-button size="mini" type="text" @click="activeCollection(scope.row)">{{ $t('device.variable-case.347856-13') }}</el-button>
+                    <el-button size="mini" type="text" @click="handleQueryHistory(scope.row)">{{ $t('device.variable-case.347856-12') }}</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getVariableList" />
+
+        <el-dialog :title="$t('device.variable-case.347856-15')" :visible.sync="centerDialogVisible" width="30%" center>
+            <span>{{ $t('device.variable-case.347856-14') }}</span>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="centerDialogVisible = false">{{ $t('iot.group.device-list.849593-12') }}</el-button>
+                <el-button type="primary" @click="confirmCollection">{{ $t('iot.group.device-list.849593-11') }}</el-button>
+            </span>
+        </el-dialog>
+
+        <el-dialog :title="$t('device.realTime-status.099127-26')" :visible.sync="dialogValue" width="30%">
+            <el-form size="mini" style="height: 100%; padding: 0 20px">
+                <el-form-item v-for="(item, index) in opationList" :label="`${item.label}:`" :key="index" label-width="180px">
+                    <el-input
+                        v-model="funVal[item.key]"
+                        :precision="0"
+                        :controls="false"
+                        @input="justNumber(item)"
+                        type="number"
+                        v-if="item.dataTypeName == 'integer' || item.dataTypeName == 'decimal'"
+                        style="width: 50%"
+                    ></el-input>
+                    <el-select v-if="item.dataTypeName == 'enum' || item.dataTypeName == 'bool'" v-model="funVal[item.key]" @change="changeSelect()">
+                        <el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value"></el-option>
+                    </el-select>
+                    <span v-if="(item.dataTypeName == 'integer' || item.dataTypeName == 'decimal') && item.unit && item.unit != 'un' && item.unit != '/'">({{ item.unit }})</span>
+                    <span class="range" v-if="item.dataTypeName == 'integer' || item.dataTypeName == 'decimal'">({{ item.min }} ~ {{ item.max }})</span>
+                </el-form-item>
+                <el-form-item style="display: none">
+                    <el-input v-model="functionName"></el-input>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="dialogValue = false">{{ $t('cancel') }}</el-button>
+                <el-button type="primary" @click="sendService" :loading="btnLoading" :disabled="!canSend">{{ $t('confirm') }}</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import moment from 'moment';
+import { listThingsModel } from '@/api/iot/device';
+import { serviceInvokeReply } from '@/api/iot/runstatus.js';
+import { propGet } from '@/api/iot/runstatus';
+import { getOrderControl } from '@/api/iot/control';
+
+export default {
+    name: 'DeviceVariable',
+    dicts: ['iot_things_type'],
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    data() {
+        return {
+            loading: false,
+            queryParams: {
+                deviceId: null,
+                type: null,
+                modelName: '',
+                pageNum: 1,
+                pageSize: 10,
+            },
+            dialogValue: false,
+            centerDialogVisible: false,
+            form: {},
+            canSend: false, //是否可以下发,主要判断数值在不在范围
+            btnLoading: false,
+            funVal: {},
+            chooseFun: {},
+            //下发的设备
+            serialNumber: '',
+            opationList: [],
+            functionName: '',
+            variableList: [],
+            total: 0, // 总条数
+        };
+    },
+    watch: {
+        device: {
+            deep: true,
+            handler(newVal, oldVal) {
+                if (newVal.deviceId && newVal.deviceId !== oldVal.deviceId) {
+                    this.queryParams.deviceId = newVal.deviceId;
+                    this.getVariableList();
+                    // this.initData();
+                }
+            },
+        },
+    },
+    mounted() {
+        const { deviceId, serialNumber } = this.device;
+        if (deviceId) {
+            this.queryParams.deviceId = deviceId;
+            this.serialNumber = serialNumber;
+            this.getVariableList();
+        }
+        this.initData();
+    },
+    methods: {
+        initData() {
+            // 监听值的实时更新
+            this.$busEvent.$on('updateData', (params) => {
+                this.updateParam(params);
+            });
+        },
+        // 获取变量情况列表
+        getVariableList() {
+            this.loading = true;
+            listThingsModel(this.queryParams).then((res) => {
+                if (res.code === 200) {
+                    this.variableList = res.rows.map((item) => {
+                        return {
+                            ...item,
+                            valueName: this.getValueName(item),
+                            dataTypeName: item.datatype.type || '',
+                        };
+                    });
+                    // this.variableList = res.rows;
+                    this.total = res.total;
+                }
+                this.loading = false;
+            });
+        },
+        // 搜索按钮操作
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getVariableList();
+        },
+        // 重置按钮操作
+        handleResetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        //指令下发
+        async editFunc(item) {
+            //判断是否有权限
+            const params = {
+                deviceId: this.device.deviceId,
+                modelId: item.modelId,
+            };
+            const response = await getOrderControl(params);
+            if (response.code != 200) {
+                this.$message({
+                    type: 'warning',
+                    message: response.msg,
+                });
+                return;
+            }
+            //这里兼容子设备的下发,在网关设备下发的时候选择实际子设备的编号
+            this.serialNumber = item.serialNumber;
+            let title = '';
+            if (this.device.status !== 3 && this.device.isShadow !== 1) {
+                if (this.device.status === 1) {
+                    title = this.$t('device.device-variable.930930-0');
+                } else if (this.device.status === 2) {
+                    title = this.$t('device.device-variable.930930-1');
+                } else {
+                    title = this.$t('device.device-variable.930930-2');
+                }
+                this.$message({
+                    type: 'warning',
+                    message: title,
+                });
+                return;
+            }
+            this.dialogValue = true;
+            this.canSend = true;
+            this.funVal = {};
+            this.chooseFun = item;
+            this.getOpationList(item);
+        },
+
+        // 封装操作列表
+        getOpationList(item) {
+            this.opationList = [];
+            let options = [];
+            this.funVal = {};
+            const datatype = item.datatype;
+            if (datatype.type == 'enum') {
+                options =
+                    datatype.enumList?.map((option) => {
+                        return {
+                            label: option.text,
+                            value: option.value + '',
+                        };
+                    }) || [];
+            }
+            if (datatype.type == 'bool') {
+                options = [
+                    {
+                        label: datatype.falseText || '',
+                        value: '0',
+                    },
+                    {
+                        label: datatype.trueText || '',
+                        value: '1',
+                    },
+                ];
+            }
+            this.opationList.push({
+                dataTypeName: datatype.type,
+                label: item.modelName,
+                key: item.identifier,
+                max: parseInt(datatype?.max || 100),
+                min: parseInt(datatype?.min || -100),
+                options: options,
+                value: item.value,
+            });
+            this.opationList.forEach((item) => {
+                let value = item.value;
+                if (item.datatype == 'integer' || item.datatype == 'decimal') {
+                    value = parseInt(value);
+                }
+                this.funVal[item.key] = value;
+            });
+        },
+
+        // 发送指令
+        async sendService() {
+            try {
+                let params = this.funVal;
+                const pas = {
+                    serialNumber: this.serialNumber,
+                    identifier: this.chooseFun.identifier,
+                    remoteCommand: params,
+                };
+                this.btnLoading = true;
+                const res = await serviceInvokeReply(pas);
+                if (res.code == 200) {
+                    this.$message.success(this.$t('device.device-variable.930930-3'));
+                } else {
+                    this.$message.error(res.data);
+                }
+            } finally {
+                this.btnLoading = false;
+                this.dialogValue = false;
+            }
+        },
+
+        //下拉选择修改触发
+        changeSelect() {
+            this.$forceUpdate();
+        },
+
+        //判断输入是否超过范围
+        justNumber(val) {
+            this.canSend = true;
+            this.opationList.some((item) => {
+                if (item.max < this.funVal[item.key] || item.min > this.funVal[item.key]) {
+                    this.canSend = false;
+                    return true;
+                }
+            });
+            this.$forceUpdate();
+        },
+        // 编辑变量值
+        handleEditVariable(item) {
+            this.$prompt(this.$t('device.device-variable.930930-4'), this.$t('device.device-variable.930930-5'), {
+                confirmButtonText: this.$t('device.device-variable.930930-6'),
+                cancelButtonText: this.$t('device.device-variable.930930-7'),
+                inputPattern: /\S/,
+                inputErrorMessage: this.$t('device.device-variable.930930-8'),
+                inputPlaceholder: item.value,
+            }).then(({ value }) => {
+                if (this.device.status !== 3 && this.device.isShadow !== 1) {
+                    let title = '';
+                    if (this.device.status === 1) {
+                        title = this.$t('device.device-variable.930930-9');
+                    } else if (this.device.status === 2) {
+                        title = this.$t('device.device-variable.930930-10');
+                    } else {
+                        title = this.$t('device.device-variable.930930-11');
+                    }
+                    this.$message({
+                        type: 'warning',
+                        message: title,
+                    });
+                    return;
+                }
+                const command = {};
+                command[item.identifier] = value;
+                const data = {
+                    serialNumber: item.serialNumber,
+                    remoteCommand: command,
+                    identifier: item.identifier,
+                    isShadow: this.device.status != 3,
+                };
+                serviceInvokeReply(data).then((res) => {
+                    if (res.code === 200) {
+                        item.ts = moment(new Date()).format('YYYY-MM-DD HH:mm:ss');
+                        item.value = value;
+                    } else if (res.code === 204) {
+                        this.$message({
+                            type: 'warning',
+                            message: res.msg,
+                        });
+                    }
+                });
+            });
+        },
+
+        // 查询历史数据
+        handleQueryHistory(item) {
+            this.$router.push({
+                path: '/iotdev/dataCenter/history',
+                query: {
+                    deviceId: this.device.deviceId,
+                    identifier: item.identifier,
+                    activeName: 'device',
+                },
+            });
+        },
+        //更新参数值
+        updateParam(params) {
+            let { serialNumber, productId, data } = params;
+            if (data) {
+                data = data.message;
+                console.log('data:', data);
+                data.forEach((msg) => {
+                    this.variableList.some((item, index) => {
+                        if (msg.id === item.identifier) {
+                            const variable = this.variableList[index];
+                            variable.ts = msg.ts;
+                            variable.value = msg.value;
+                            variable.valueName = this.getValueName(item);
+                            this.$set(this.variableList, index, variable);
+                            return true;
+                        }
+                    });
+                });
+            }
+        },
+        getValueName(item) {
+            //返回的数据
+            let res = item.value || '-';
+            //需要解析的类型
+            const optionsType = ['bool', 'enum'];
+            //如果有datatype并且需要解析就遍历解析
+            if (item.datatype) {
+                switch (item.datatype.type) {
+                    case 'bool':
+                        if (0 == item.value) res = item.datatype.falseText;
+                        if (1 == item.value) res = item.datatype.trueText;
+                        break;
+                    case 'enum':
+                        item.datatype.enumList?.some((enumOpt) => {
+                            if (enumOpt.value == item.value) {
+                                res = enumOpt.text;
+                                return true;
+                            }
+                        });
+                        break;
+                }
+            }
+            return res;
+        },
+        //主动采集数据
+        activeCollection(item) {
+            this.centerDialogVisible = true;
+            this.form.serialNumber = item.serialNumber;
+            this.form.type = 1;
+            this.form.identifier = item.identifier;
+        },
+        //确认采集
+        confirmCollection() {
+            propGet(this.form).then((response) => {
+                if (response.code == 200) {
+                    this.centerDialogVisible = false;
+                }
+            });
+        },
+        //采集所有
+        activeCollectionAll() {
+            this.centerDialogVisible = true;
+            this.form.serialNumber = this.serialNumber;
+            this.form.type = 2;
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.device-variable {
+    width: 100%;
+    padding: 5px 5px 10px 20px;
+}
+</style>

+ 123 - 0
src/views/pms/video_center/device/import-record.vue

@@ -0,0 +1,123 @@
+<template>
+    <el-dialog :title="$t('device.import-record.086254-0')" :visible.sync="open" width="900px">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+                <el-form-item prop="productId">
+                    <el-select v-model="queryParams.productId" :placeholder="$t('device.allot-record.155854-2')" clearable style="width: 180px">
+                        <el-option v-for="item in productList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item prop="status">
+                    <el-select v-model="queryParams.status" :placeholder="$t('device.import-record.086254-2')" clearable style="width: 180px">
+                        <el-option v-for="dict in dict.type.common_status_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-date-picker
+                        v-model="daterangeTime"
+                        size="small"
+                        style="width: 180px"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        :end-placeholder="$t('device.import-record.086254-4')"
+                        range-separator="-"
+                        start-placeholder="$t('device.import-record.086254-')"
+                        type="datetimerange"
+                    ></el-date-picker>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.import-record.086254-5') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.import-record.086254-6') }}</el-button>
+                </el-form-item>
+            </el-form>
+            <el-table :border="false" v-loading="loading" ref="singleTable" :data="dataList">
+                <el-table-column :label="$t('device.import-record.086254-7')" align="left" prop="id" width="80" />
+                <el-table-column :label="$t('device.import-record.086254-8')" align="center" prop="total" />
+                <el-table-column :label="$t('device.import-record.086254-13')" align="center" prop="productName" />
+                <el-table-column :label="$t('device.import-record.086254-9')" align="center" prop="successQuantity" />
+                <el-table-column :label="$t('device.import-record.086254-10')" align="center" prop="failQuantity" />
+                <el-table-column :label="$t('device.import-record.086254-11')" align="center" prop="status">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.common_status_type" :value="scope.row.status" size="small" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.import-record.086254-12')" align="center" prop="createTime" width="200" />
+            </el-table>
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listProduct } from '@/api/iot/product';
+import { listImportRecord } from '@/api/iot/device';
+
+export default {
+    name: 'importRecord',
+    dicts: ['common_status_type'],
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 打开选择产品对话框
+            open: false,
+            // 产品列表
+            productList: [],
+            statusList: [],
+            dataList: [],
+            //时间范围
+            daterangeTime: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                productName: null,
+                type: 1,
+            },
+        };
+    },
+    created() {
+        this.getProductList();
+    },
+    methods: {
+        /** 查询产品列表 */
+        getProductList() {
+            this.loading = true;
+            const params = {
+                pageSize: 999,
+            };
+            listProduct(params).then((response) => {
+                this.productList = response.rows.map((item) => {
+                    return { value: item.productId, label: item.productName };
+                });
+                this.loading = false;
+            });
+        },
+        //查询导入记录列表
+        getList() {
+            this.loading = true;
+            listImportRecord(this.addDateRange(this.queryParams, this.daterangeTime)).then((response) => {
+                this.dataList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /**关闭对话框 */
+        closeDialog() {
+            this.open = false;
+        },
+    },
+};
+</script>

+ 1168 - 0
src/views/pms/video_center/device/index.vue

@@ -0,0 +1,1168 @@
+<template>
+  <div class="device">
+    <el-card shadow="never" style="border: 0" v-show="showSearch">
+      <el-form
+        class="search-form"
+        :model="queryParams"
+        ref="queryFormRef"
+        inline
+        style="margin-bottom: -20px"
+      >
+        <el-form-item label="设备名称" prop="deviceName">
+          <el-input
+            v-model="queryParams.deviceName"
+            placeholder="请输入设备名称"
+            clearable
+            size="default"
+            @keyup.enter="handleQuery"
+            style="width: 150px"
+          />
+        </el-form-item>
+        <el-form-item label="设备编号" prop="serialNumber">
+          <el-input
+            v-model="queryParams.serialNumber"
+            placeholder="请输入设备编号"
+            clearable
+            size="default"
+            @keyup.enter="handleQuery"
+            style="width: 150px"
+          />
+        </el-form-item>
+        <el-form-item label="设备状态" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            placeholder="请选择设备状态"
+            clearable
+            size="default"
+            style="width: 150px"
+          >
+            <el-option
+              v-for="dict in myDict.type.iot_device_status"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" :icon="Search" size="default" @click="handleQuery"
+            >查询</el-button
+          >
+          <el-button :icon="Refresh" size="default" @click="resetQuery">重置</el-button>
+          <el-checkbox
+            v-model="queryParams.showChild"
+            style="margin: 0 10px 0"
+            @change="handleQuery"
+            >显示上级机构数据</el-checkbox
+          >
+          <el-tooltip content="选中后,本级可以看下级的数据" placement="top"
+            ><el-icon><InfoFilled /></el-icon
+          ></el-tooltip>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card
+      class="main-card"
+      shadow="never"
+      v-if="showType == 'list'"
+      :style="showSearch ? '' : 'margin:0'"
+    >
+      <div class="card-toolbar mb8">
+        <div>
+          <el-dropdown class="mr10" @command="handleCommand">
+            <el-button size="default" type="primary" :icon="Plus">
+              新增
+              <el-icon class="el-icon--right"><arrow-down /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleEditDevice">手动添加</el-dropdown-item>
+                <el-dropdown-item command="handleBatchImport">批量导入</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <el-dropdown @command="handleCommand1">
+            <el-button size="default" type="primary">
+              分配设备
+              <el-icon class="el-icon--right"><arrow-down /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleSelectAllot">选择分配</el-dropdown-item>
+                <el-dropdown-item command="handleImportAllot">导入分配</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <el-button type="primary" size="default" @click="recycleDevice" style="margin-left: 10px"
+            >回收设备</el-button
+          >
+        </div>
+
+        <div>
+          <el-radio-group class="fr ml10" plain size="default" v-model="showType">
+            <el-radio-button label="card"
+              ><el-icon><Menu /></el-icon
+            ></el-radio-button>
+            <el-radio-button label="list"
+              ><el-icon><Fold /></el-icon
+            ></el-radio-button>
+          </el-radio-group>
+          <right-toolbar v-model:showSearch="showSearch" @query-table="getList" />
+        </div>
+      </div>
+
+      <el-table class="base-table" v-loading="loading" :data="deviceList" :border="false">
+        <el-table-column
+          label="图标"
+          align="center"
+          header-align="center"
+          prop="deviceId"
+          width="72"
+        >
+          <template #default="{ row }">
+            <el-image
+              style="width: 100%; height: auto"
+              lazy
+              :preview-src-list="[baseUrl + row.imgUrl]"
+              :src="baseUrl + row.imgUrl"
+              fit="cover"
+              v-if="row.imgUrl != null && row.imgUrl != ''"
+            />
+            <el-image
+              style="width: 100%; height: auto"
+              :preview-src-list="[gatewayImage]"
+              :src="gatewayImage"
+              fit="cover"
+              v-else-if="row.deviceType == 2"
+            />
+            <el-image
+              style="width: 100%; height: auto"
+              :preview-src-list="[videoImage]"
+              :src="videoImage"
+              fit="cover"
+              v-else-if="row.deviceType == 3"
+            />
+            <el-image
+              style="width: 100%; height: auto"
+              :preview-src-list="[productImage]"
+              :src="productImage"
+              fit="cover"
+              v-else
+            />
+          </template>
+        </el-table-column>
+
+        <el-table-column
+          label="编号"
+          align="center"
+          header-align="center"
+          prop="deviceId"
+          width="50"
+        />
+
+        <el-table-column label="设备名称" prop="deviceName" min-width="180" />
+        <el-table-column label="设备编号" align="center" prop="serialNumber" min-width="130" />
+        <el-table-column label="所属产品" align="center" prop="productName" min-width="160" />
+        <el-table-column label="协议" align="center" prop="transport" min-width="80" />
+        <el-table-column label="通讯协议" align="center" prop="protocolCode" min-width="140" />
+        <el-table-column label="子设备数" align="center" prop="subDeviceCount" width="80">
+          <template #default="scope">
+            {{ scope.row.subDeviceCount }}
+          </template>
+        </el-table-column>
+        <el-table-column label="设备影子" align="center" prop="isShadow" width="80">
+          <template #default="scope">
+            <el-tag type="success" size="default" v-if="scope.row.isShadow == 1">启用</el-tag>
+            <el-tag type="info" size="default" v-else>禁用</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" prop="status" width="80">
+          <template #default="scope">
+            <dict-tag
+              :options="dict.type.iot_device_status"
+              :value="scope.row.status"
+              size="default"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="信号" align="center" prop="rssi" width="60">
+          <template #default="scope">
+            <svg-icon v-if="scope.row.status == 3 && scope.row.rssi >= '-55'" icon-class="wifi_4" />
+            <svg-icon
+              v-else-if="scope.row.status == 3 && scope.row.rssi >= '-70' && scope.row.rssi < '-55'"
+              icon-class="wifi_3"
+            />
+            <svg-icon
+              v-else-if="scope.row.status == 3 && scope.row.rssi >= '-85' && scope.row.rssi < '-70'"
+              icon-class="wifi_2"
+            />
+            <svg-icon
+              v-else-if="
+                scope.row.status == 3 && scope.row.rssi >= '-100' && scope.row.rssi < '-85'
+              "
+              icon-class="wifi_1"
+            />
+            <svg-icon v-else icon-class="wifi_0" />
+          </template>
+        </el-table-column>
+        <el-table-column label="定位方式" align="center" prop="locationWay" width="100">
+          <template #default="scope">
+            <dict-tag
+              :options="myDict.type.iot_location_way"
+              :value="scope.row.locationWay"
+              size="default"
+            />
+          </template>
+        </el-table-column>
+        <el-table-column label="固件版本" align="center" prop="firmwareVersion" width="100">
+          <template #default="scope">
+            <el-tag size="small" type="info">Ver {{ scope.row.firmwareVersion }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="激活时间" align="center" prop="activeTime" width="100">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.activeTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createTime" width="100">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="操作"
+          align="left"
+          class-name="small-padding fixed-width"
+          fixed="right"
+          width="200"
+        >
+          <template #default="scope">
+            <el-button
+              type="danger"
+              plain
+              style="padding: 5px"
+              :icon="Delete"
+              @click="handleDelete(scope.row)"
+              >删除</el-button
+            >
+            <el-button
+              type="primary"
+              plain
+              style="padding: 5px"
+              :icon="View"
+              @click="handleEditDevice(scope.row)"
+              >查看</el-button
+            >
+            <el-button
+              type="primary"
+              plain
+              style="padding: 5px"
+              @click="openSummaryDialog(scope.row)"
+              v-if="form.deviceId != 0"
+              >二维码</el-button
+            >
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        :page-sizes="[12, 24, 36, 60]"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <el-card
+      class="main-card"
+      shadow="never"
+      v-if="showType == 'card'"
+      :style="showSearch ? '' : 'margin:0'"
+    >
+      <div class="card-toolbar mb8">
+        <div>
+          <el-dropdown class="mr10" @command="handleCommand">
+            <el-button size="default" type="primary" :icon="Plus">
+              新增
+              <el-icon class="el-icon--right"><arrow-down /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleEditDevice">手动添加</el-dropdown-item>
+                <el-dropdown-item command="handleBatchImport">批量导入</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <el-dropdown @command="handleCommand1">
+            <el-button size="default" type="primary">
+              分配设备
+              <el-icon class="el-icon--right"><arrow-down /></el-icon>
+            </el-button>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item command="handleSelectAllot">选择分配</el-dropdown-item>
+                <el-dropdown-item command="handleImportAllot">导入分配</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+
+          <el-button
+            size="default"
+            type="primary"
+           
+            @click="recycleDevice"
+            style="margin-left: 10px"
+            >回收设备</el-button
+          >
+        </div>
+
+        <div>
+          <el-radio-group class="fr ml10" plain size="default" v-model="showType">
+            <el-radio-button label="card"
+              ><el-icon><Menu /></el-icon
+            ></el-radio-button>
+            <el-radio-button label="list"
+              ><el-icon><Fold /></el-icon
+            ></el-radio-button>
+          </el-radio-group>
+          <right-toolbar v-model:showSearch="showSearch" @query-table="getList" />
+        </div>
+      </div>
+
+      <el-row :gutter="30" v-loading="loading" style="flex-wrap: wrap">
+        <el-col
+          :xs="24"
+          :sm="12"
+          :md="8"
+          :lg="6"
+          :xl="6"
+          v-for="(item, index) in deviceList"
+          :key="index"
+          style="margin-bottom: 30px; text-align: center"
+        >
+          <el-card :body-style="{ padding: '20px' }" shadow="hover" class="card-item">
+            <el-row :gutter="10" justify="space-between">
+              <el-col :span="19" style="text-align: left">
+                <el-link
+                  type=""
+                  :underline="false"
+                  style="font-weight: bold; font-size: 16px; line-height: 32px"
+                >
+                  <el-tooltip class="item" effect="dark" content="分享的设备" placement="top-start">
+                    <svg-icon
+                      class="mr5"
+                      icon-class="share"
+                      style="font-size: 20px; vertical-align: -6px"
+                      v-if="item.isOwner != 1"
+                    />
+                  </el-tooltip>
+                  <svg-icon icon-class="device" v-if="item.isOwner == 1" />
+                  <b class="card-item__title" @click="handleDeviceDetail(item)">{{
+                    item.deviceName
+                  }}</b>
+
+                  <dict-tag
+                    :options="myDict.type.iot_device_status"
+                    :value="item.status"
+                    size="default"
+                    style="display: inline-block"
+                  />
+                </el-link>
+              </el-col>
+              <el-col :span="1.5" style="font-size: 20px; padding-top: 5px; cursor: pointer">
+                <svg-icon icon-class="qrcode" @click="openSummaryDialog(item)" />
+              </el-col>
+              <el-col :span="5">
+                <div style="font-size: 28px; color: #ccc">
+                  <svg-icon v-if="item.status == 3 && item.rssi >= '-55'" icon-class="wifi_4" />
+                  <svg-icon
+                    v-else-if="item.status == 3 && item.rssi >= '-70' && item.rssi < '-55'"
+                    icon-class="wifi_3"
+                  />
+                  <svg-icon
+                    v-else-if="item.status == 3 && item.rssi >= '-85' && item.rssi < '-70'"
+                    icon-class="wifi_2"
+                  />
+                  <svg-icon
+                    v-else-if="item.status == 3 && item.rssi >= '-100' && item.rssi < '-85'"
+                    icon-class="wifi_1"
+                  />
+                  <svg-icon v-else icon-class="wifi_0" />
+                </div>
+              </el-col>
+            </el-row>
+
+            <el-row :gutter="10">
+              <el-col :span="17">
+                <div style="text-align: left; line-height: 40px; white-space: nowrap">
+                  <el-tag v-if="item.protocolCode" class="mr5" type="primary" size="default">
+                    {{ item.protocolCode }}
+                  </el-tag>
+                  <el-tag v-if="item.transport" class="mr5" type="primary" size="default">
+                    {{ item.transport }}
+                  </el-tag>
+                </div>
+                <el-descriptions :column="1" size="default" style="white-space: nowrap">
+                  <el-descriptions-item label="编号">
+                    <span class="font-primary">{{ item.serialNumber }}</span>
+                  </el-descriptions-item>
+                  <el-descriptions-item label="产品">
+                    {{ item.productName }}
+                  </el-descriptions-item>
+                  <el-descriptions-item label="激活时间">
+                    {{ parseTime(item.activeTime, '{y}-{m}-{d}') }}
+                  </el-descriptions-item>
+                </el-descriptions>
+              </el-col>
+              <el-col :span="7">
+                <div style="margin-top: 10px">
+                  <el-image
+                    style="width: 80px; height: 80px; border-radius: 10px"
+                    lazy
+                    :preview-src-list="[baseUrl + item.imgUrl]"
+                    :src="baseUrl + item.imgUrl"
+                    fit="cover"
+                    v-if="item.imgUrl != null && item.imgUrl != ''"
+                  />
+                  <el-image
+                    style="width: 80px; height: 80px; border-radius: 10px"
+                    :preview-src-list="[gatewayImage]"
+                    :src="gatewayImage"
+                    fit="cover"
+                    v-else-if="item.deviceType == 2"
+                  />
+                  <el-image
+                    style="width: 80px; height: 80px; border-radius: 10px"
+                    :preview-src-list="[videoImage]"
+                    :src="videoImage"
+                    fit="cover"
+                    v-else-if="item.deviceType == 3"
+                  />
+                  <el-image
+                    style="width: 80px; height: 80px; border-radius: 10px"
+                    :preview-src-list="[productImage]"
+                    :src="productImage"
+                    fit="cover"
+                    v-else
+                  />
+                </div>
+              </el-col>
+            </el-row>
+
+            <div class="card-item__footer">
+              <el-button class="delete-btn" size="default" @click="handleDelete(item)"
+                >删除</el-button
+              >
+              <el-button class="detail-btn" size="default" @click="handleEditDevice(item, 'basic')"
+                >查看详情</el-button
+              >
+              <el-button size="default" @click="handleRunDevice(item)">运行状态</el-button>
+            </div>
+          </el-card>
+        </el-col>
+      </el-row>
+
+      <el-empty description="暂无数据,请添加设备" v-if="total == 0" />
+
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        :page-sizes="[12, 24, 36, 60]"
+        @pagination="getList"
+      />
+    </el-card>
+
+    <!-- 二维码 -->
+    <el-dialog v-model="openSummary" width="300px" append-to-body>
+      <div
+        style="
+          border: 1px solid #ccc;
+          width: 220px;
+          text-align: center;
+          margin: 0 auto;
+          margin-top: -15px;
+        "
+      >
+        <Vue3NextQrcode :text="qrText" :size="200" />
+        <div style="padding-bottom: 10px">设备二维码</div>
+      </div>
+    </el-dialog>
+
+    <!-- 批量导入设备 -->
+    <batchImport ref="batchImportRef" @save="saveDialog" />
+    <!-- 导入分配 -->
+    <!-- <allotImport ref="allotImportRef" @save="saveAllotDialog" /> -->
+    <!-- 导入记录 -->
+    <!-- <importRecord ref="importRecordRef" /> -->
+    <!-- 设备回收记录 -->
+    <!-- <recycleRecord ref="recycleRecordRef" /> -->
+    <!-- 设备分配记录 -->
+    <!-- <allotRecord ref="allotRecordRef" /> -->
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onActivated, computed } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { useStore } from 'vuex'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import {
+  Search,
+  Refresh,
+  InfoFilled,
+  Plus,
+  Delete,
+  View,
+  ArrowDown,
+  Menu,
+  Fold
+} from '@element-plus/icons-vue'
+import { getCurrentInstance } from 'vue'
+import { listDeviceShort, delDevice } from '@/api/pms/video/device'
+import { listGroup } from '@/api/pms/video/group'
+import { delSipDeviceBySipId } from '@/api/pms/video/sipdevice'
+import Treeselect from '@riophae/vue-treeselect'
+import '@riophae/vue-treeselect/dist/vue-treeselect.css'
+import { parseTime } from '@/utils/dateUtil'
+import { Vue3NextQrcode } from 'vue3-next-qrcode'
+import 'vue3-next-qrcode/es/style.css'
+import batchImport from './batch-import-dialog.vue'
+// import allotImport from './allot-import-dialog.vue'
+// import importRecord from './import-record.vue'
+// import recycleRecord from './recycle-record.vue'
+// import allotRecord from './allot-record.vue'
+
+// 图片资源
+import gatewayImage from '@/assets/imgs/gateway.svg'
+import videoImage from '@/assets/imgs/video.svg'
+import productImage from '@/assets/imgs/product.svg'
+
+const { proxy } = getCurrentInstance()
+// Vue Router 和 Store
+const router = useRouter()
+const route = useRoute()
+const store = useStore()
+
+// 定义字典类型
+defineProps({
+  myDict: {
+    type: Object,
+    default: () => ({
+      type: {
+        iot_device_status: [],
+        iot_is_enable: [],
+        iot_location_way: [],
+        iot_transport_type: []
+      }
+    })
+  }
+})
+
+// Refs
+const queryFormRef = ref()
+const batchImportRef = ref()
+const allotImportRef = ref()
+const importRecordRef = ref()
+const recycleRecordRef = ref()
+const allotRecordRef = ref()
+
+// 数据响应式变量
+const qrText = ref('yanfan')
+const openSummary = ref(false)
+const showSearch = ref(true)
+const showType = ref('card')
+const loading = ref(true)
+const total = ref(0)
+const deviceList = ref([])
+const myGroupList = ref([])
+const uniqueId = ref('')
+
+// 基础URL
+const baseUrl = import.meta.env.VITE_BASE_URL
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 12,
+  showChild: true,
+  deviceName: null,
+  productId: null,
+  groupId: null,
+  productName: null,
+  userId: null,
+  userName: null,
+  tenantId: null,
+  tenantName: null,
+  serialNumber: null,
+  status: null,
+  networkAddress: null,
+  activeTime: null
+})
+
+// 表单参数
+const form = reactive({
+  productId: 0,
+  status: 1,
+  locationWay: 1,
+  firmwareVersion: 1.0,
+  serialNumber: '',
+  deviceType: 1,
+  isSimulate: 0,
+  deviceId: 0
+})
+
+// 批量导入参数
+const isSubDev = ref(false)
+
+// 生命周期钩子
+onMounted(() => {
+  // 产品筛选
+  let productId = route.query.productId
+  if (productId != null) {
+    queryParams.productId = Number(productId)
+    queryParams.groupId = null
+    queryParams.serialNumber = null
+  }
+  // 分组筛选
+  let groupId = route.query.groupId
+  if (groupId != null) {
+    queryParams.groupId = Number(groupId)
+    queryParams.productId = null
+    queryParams.serialNumber = null
+  }
+  // 设备编号筛选
+  let sn = route.query.sn
+  if (sn != null) {
+    queryParams.serialNumber = sn
+    queryParams.productId = null
+    queryParams.groupId = null
+  }
+  connectMqtt()
+})
+
+onActivated(() => {
+  const time = route.query.t
+  if (time != null && time != uniqueId.value) {
+    uniqueId.value = time
+    // 页码筛选
+    let pageNum = route.query.pageNum
+    if (pageNum != null) {
+      queryParams.pageNum = Number(pageNum)
+    }
+    // 产品筛选
+    let productId = route.query.productId
+    if (productId != null) {
+      queryParams.productId = Number(productId)
+      queryParams.groupId = null
+      queryParams.serialNumber = null
+    }
+    // 分组筛选
+    let groupId = route.query.groupId
+    if (groupId != null) {
+      queryParams.groupId = Number(groupId)
+      queryParams.productId = null
+      queryParams.serialNumber = null
+    }
+    // 设备编号筛选
+    let sn = route.query.sn
+    if (sn != null) {
+      queryParams.serialNumber = sn
+      queryParams.productId = null
+      queryParams.groupId = null
+    }
+    getList()
+  }
+})
+
+// 方法定义
+/* 连接Mqtt消息服务器 */
+async function connectMqtt() {
+ 
+  if (proxy.$mqttTool.client == null) {
+    await proxy.$mqttTool.connect()
+  }
+  mqttCallback()
+  getList()
+}
+
+/* Mqtt回调处理  */
+function mqttCallback() {
+ 
+  proxy.$mqttTool.client.on('message', (topic, message, buffer) => {
+    let topics = topic.split('/')
+    let productId = topics[1]
+    let deviceNum = topics[2]
+    message = JSON.parse(message.toString())
+    if (!message) {
+      return
+    }
+    if (topics[3] == 'status') {
+      // 更新列表中设备的状态
+      for (let i = 0; i < this.deviceList.length; i++) {
+        if (this.deviceList[i].serialNumber == deviceNum) {
+          this.deviceList[i].status = message.status
+          this.deviceList[i].isShadow = message.isShadow
+          this.deviceList[i].rssi = message.rssi
+          return
+        }
+      }
+    }
+  })
+}
+
+// 新增设备更多操作触发
+function handleCommand(command) {
+  switch (command) {
+    case 'handleEditDevice':
+      handleEditDevice(0)
+      break
+    case 'handleBatchImport':
+      handleBatchImport()
+      break
+    default:
+      break
+  }
+}
+
+//批量导入设备
+function handleBatchImport() {
+  
+  if (batchImportRef.value) {
+    batchImportRef.value.showDialog()
+    batchImportRef.value.importForm.productId = null
+  }
+}
+
+//导入分配设备
+function handleImportAllot() {
+  if (allotImportRef.value) {
+    allotImportRef.value.upload.importAllotDialog = true
+    allotImportRef.value.allotForm.productId = null
+    allotImportRef.value.allotForm.deptId = null
+  }
+}
+
+// dialog 保存响应
+function saveDialog() {
+  getList()
+}
+
+// dialog 保存响应
+function saveAllotDialog() {
+  getList()
+}
+
+// 分配设备更多操作触发
+function handleCommand1(command) {
+  switch (command) {
+    case 'handleSelectAllot':
+      handleSelectAllot()
+      break
+    case 'handleImportAllot':
+      handleImportAllot()
+      break
+    default:
+      break
+  }
+}
+
+//跳转选择分配设备页面
+function handleSelectAllot() {
+  router.push({
+    path: '/iotdev/iot/device-select-allot'
+  })
+}
+
+//跳转回收设备页面
+function recycleDevice() {
+  router.push({
+    path: '/iotdev/iot/device-recycle'
+  })
+}
+
+//更多操作
+function handleCommandMore(command) {
+  switch (command) {
+    case 'importRecord':
+      handleImportRecord()
+      break
+    case 'exportDevice':
+      handleexportDevice()
+      break
+    case 'recycleRecord':
+      handleRecycleRecord()
+      break
+    case 'allotRecord':
+      handleAllotRecord()
+      break
+    default:
+      break
+  }
+}
+
+//导入记录
+function handleImportRecord() {
+  if (importRecordRef.value) {
+    importRecordRef.value.open = true
+  }
+}
+
+//设备回收记录
+function handleRecycleRecord() {
+  if (recycleRecordRef.value) {
+    recycleRecordRef.value.open = true
+  }
+}
+
+//设备分配记录
+function handleAllotRecord() {
+  if (allotRecordRef.value) {
+    allotRecordRef.value.open = true
+  }
+}
+
+function openSummaryDialog(row) {
+  let json = {
+    type: 1, // 1=扫码关联设备
+    deviceNumber: row.serialNumber,
+    productId: row.productId,
+    productName: row.productName
+  }
+  qrText.value = JSON.stringify(json)
+  openSummary.value = true
+}
+
+/* 订阅消息 */
+function mqttSubscribe(list) {
+  // 订阅当前页面设备状态和实时监测
+  let topics = []
+  for (let i = 0; i < list.length; i++) {
+    let topicStatus = '/' + '+' + '/' + list[i].serialNumber + '/status/post'
+    topics.push(topicStatus)
+  }
+
+  proxy.$mqttTool.subscribe(topics)
+}
+
+/** 查询设备分组列表 */
+const userStore = useUserStoreWithOut()
+function getGroupList() {
+  loading.value = true
+  let queryGroupParams = {
+    pageSize: 30,
+    pageNum: 1,
+    userId: userStore.user.id
+  }
+  listGroup(queryGroupParams).then((response) => {
+    myGroupList.value = response.rows
+  })
+}
+
+/** 查询所有简短设备列表 */
+function getList() {
+  loading.value = true
+  queryParams.params = {}
+  getGroupList()
+
+  listDeviceShort(queryParams)
+    .then((response) => {
+      deviceList.value = response.rows
+      total.value = response.total
+      // 订阅消息
+      if (deviceList.value && deviceList.value.length > 0) {
+        mqttSubscribe(deviceList.value)
+      }
+    })
+    .catch((e) => {
+      console.error(e)
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  queryParams.productId = null
+  queryParams.groupId = null
+  queryParams.serialNumber = null
+  if (queryFormRef.value) {
+    queryFormRef.value.resetFields()
+  }
+  handleQuery()
+}
+
+// 点击名称查看
+function handleDeviceDetail(item) {
+  handleEditDevice(item)
+}
+
+/** 修改按钮操作 */
+function handleEditDevice(row, activeName) {
+  let deviceId = 0
+  let isSubDevValue = 0
+  if (row != 0) {
+    deviceId = row.deviceId || 0 // 这里需要根据实际情况调整
+    isSubDevValue = row.subDeviceCount > 0 ? 1 : 0
+  }
+  router.push({
+    path: '/videocenter/device/device-edit',
+    query: {
+      deviceId: deviceId,
+      isSubDev: isSubDevValue,
+      pageNum: queryParams.pageNum,
+      activeName: activeName
+    }
+  })
+}
+
+/** 运行状态按钮操作 */
+function handleRunDevice(row) {
+  let deviceId = 0
+  let isSubDevValue = 0
+  if (row != 0) {
+    deviceId = row.deviceId || 0 // 这里需要根据实际情况调整
+    isSubDevValue = row.subDeviceCount > 0 ? 1 : 0
+  }
+  if (row.deviceType === 3) {
+    router.push({
+      path: '/videocenter/device/device-edit',
+      query: {
+        deviceId: deviceId,
+        isSubDev: isSubDevValue,
+        pageNum: queryParams.pageNum,
+        activeName: 'sipChannel'
+      }
+    })
+  } else {
+    router.push({
+      path: '/videocenter/device/device-edit',
+      query: {
+        deviceId: deviceId,
+        isSubDev: isSubDevValue,
+        pageNum: queryParams.pageNum,
+        activeName: 'runningStatus'
+      }
+    })
+  }
+}
+
+/** 删除按钮操作 */
+function handleDelete(row) {
+  const deviceIds = row.deviceId || 0 // 这里需要根据实际情况调整
+  ElMessageBox.confirm('是否确认删除设备编号为"' + deviceIds + '"的数据项?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  })
+    .then(() => {
+      if (row.deviceType === 3) {
+        return delSipDeviceBySipId(row.serialNumber)
+      }
+      return delDevice(deviceIds)
+    })
+    .then(() => {
+      getList()
+      ElMessage.success('删除成功')
+    })
+    .catch(() => {})
+}
+
+/** 未启用设备影子*/
+function shadowUnEnable(device, thingsModel) {
+  // 1-未激活,2-禁用,3-在线,4-离线
+  if (device.status != 3 && device.isShadow == 0) {
+    return true
+  }
+  if (thingsModel.isReadonly) {
+    return true
+  }
+  return false
+}
+
+// 获取表格图片
+function getImg(row) {
+  switch (row.deviceType) {
+    case 2:
+      return gatewayImage
+    case 3:
+      return videoImage
+    default:
+      return productImage
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+::v-deep(.el-upload-dragger) {
+  width: 510px;
+}
+
+.el-dropdown-menu__item {
+  font-size: 12px;
+  /* 设置字体大小 */
+}
+
+.font-primary {
+  color: #0147eb;
+}
+
+.device {
+  --font-grey: #606266;
+  --success-color: #67c23b;
+  padding: 20px;
+  .search-form {
+    .el-form-item {
+      :deep(.el-form-item__label) {
+        font-weight: normal;
+        color: var(--font-grey);
+      }
+    }
+  }
+
+  .main-card.el-card {
+    margin-top: 20px;
+    padding-bottom: 50px;
+    border: 0;
+
+    .card-toolbar {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 20px;
+
+      .el-radio-button {
+        :deep(.el-radio-button__inner) {
+          background-color: #f0f2f5;
+          border-color: #fff;
+        }
+
+        &.is-active {
+          :deep(.el-radio-button__orig-radio:checked + .el-radio-button__inner) {
+            background-color: #0147eb;
+            border-color: #0147eb;
+          }
+        }
+      }
+    }
+
+    .is-publish {
+      font-size: 12px;
+      white-space: nowrap;
+      color: var(--success-color);
+
+      &:before {
+        margin-right: 4px;
+        display: inline-block;
+        width: 4px;
+        height: 4px;
+        border-radius: 2px;
+        vertical-align: 2px;
+        background: var(--success-color);
+        content: '';
+      }
+    }
+
+    .el-table {
+      :deep(.el-table__header th) {
+        color: #9a9da3;
+        border-color: #f6f8fa;
+        background-color: #f5f7fa;
+      }
+
+      :deep(.el-table__body .el-table__cell) {
+        color: #606266;
+        padding: 8px 0;
+        border-color: #f6f8fa;
+
+        .el-button {
+          border-color: transparent;
+          background: transparent;
+
+          &:hover {
+            &.el-button--danger {
+              color: #ff9999;
+            }
+
+            &.el-button--primary {
+              color: #91a8f8;
+            }
+          }
+
+          &.el-button {
+            margin-left: 2px;
+          }
+        }
+      }
+    }
+
+    .pagination-container {
+      :deep(.el-pagination) {
+        .el-pager li:not(.disabled).active {
+          background-color: #0147eb;
+        }
+      }
+    }
+  }
+
+  .card-item {
+    height: 100%;
+    border-radius: 4px;
+    background-image: linear-gradient(#e9f1fc, #fefefe);
+
+    :deep(.el-card__body) {
+      display: flex;
+      height: 100%;
+      flex-direction: column;
+      justify-content: space-between;
+    }
+
+    .card-item__title {
+      vertical-align: -2px;
+      margin-right: 0.5em;
+    }
+
+    .el-descriptions {
+      :deep(.el-descriptions__body) {
+        background: transparent;
+      }
+    }
+
+    .card-item__footer {
+      margin-top: 1em;
+      text-align: right;
+
+      .el-button {
+        background: #f5f7fa;
+        border-color: #f5f7fa;
+
+        &.detail-btn {
+          color: #0147eb;
+        }
+        &.delete-btn {
+          color: #ff6363;
+        }
+
+        &:hover {
+          border-color: #0147eb;
+          color: #0147eb;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1118 - 0
src/views/pms/video_center/device/instruction-parsing.vue

@@ -0,0 +1,1118 @@
+<template>
+    <div class="instruction-parsing">
+        <el-row>
+            <el-col :span="16" class="left">
+                <div class="head">
+                    <span style="color: #909399">{{ $t('device.instruction-parsing.830424-0') }}</span>
+                    <span>{{ device.serialNumber }}</span>
+                    <el-dropdown style="margin-left: auto">
+                        <span class="el-dropdown-link">
+                            {{ format }}
+                            <i class="el-icon-arrow-down el-icon--right"></i>
+                        </span>
+                        <el-dropdown-menu slot="dropdown">
+                            <el-dropdown-item v-for="(item, index) in formatList" :key="index">{{ item }}</el-dropdown-item>
+                        </el-dropdown-menu>
+                    </el-dropdown>
+                </div>
+                <div class="content">
+                    <div v-for="(item, index) in logList" :key="index" class="item-class" :class="{ 'send-class': item.type == 0, 'receive-class': item.type == 1, 'report-class': item.type == 2 }">
+                        <div class="item-head">
+                            <div>
+                                {{ item.type == 0 ? $t('device.instruction-parsing.830424-1') : item.type == 1 ? $t('device.instruction-parsing.830424-2') : $t('device.instruction-parsing.830424-3') }}
+                            </div>
+                            <div class="head-time">{{ item.time }}</div>
+                            <div class="analysis-btn right-btn" v-if="item.type != 0 && !item.analysis" v-loading="item.loading" @click.stop="analysisData(item, index)">
+                                {{ $t('device.instruction-parsing.830424-4') }}
+                            </div>
+                            <div class="analysised right-btn" v-if="item.type != 0 && item.analysis">
+                                {{ $t('device.instruction-parsing.830424-5') }}
+                            </div>
+                        </div>
+                        <div class="item-content">
+                            <div class="content-value">{{ item.value }}</div>
+                        </div>
+                        <div class="analysis-data" v-if="item.type != 0 && item.analysis">
+                            <div class="data-col left-col">
+                                <div class="label">{{ $t('device.instruction-parsing.830424-6') }}</div>
+                                <div class="value">{{ item.analysisVal.name || '--' }}</div>
+                            </div>
+                            <div class="data-col right-col">
+                                <div class="label">{{ $t('device.instruction-parsing.830424-7') }}</div>
+                                <div class="value">{{ item.analysisVal.id || '--' }}</div>
+                            </div>
+                            <div class="data-col left-col">
+                                <div class="label">{{ $t('device.instruction-parsing.830424-8') }}</div>
+                                <div class="value">{{ item.analysisVal.value || '--' }}</div>
+                            </div>
+                            <div class="data-col right-col" v-if="item.analysisList.length > 1">
+                                <el-button type="text" size="mini" @click.stop="openMore(item)">{{ $t('device.instruction-parsing.830424-9') }}</el-button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="bottom">
+                    <el-input :placeholder="$t('device.instruction-parsing.830424-10')" v-model="sendVal" class="input-with-select" size="medium">
+                        <el-select v-model="topic" slot="prepend" :placeholder="$t('device.instruction-parsing.830424-11')">
+                            <el-option :label="$t('device.instruction-parsing.830424-12')" value="/function/get"></el-option>
+                            <el-option :label="$t('device.instruction-parsing.830424-13')" value="/property/post"></el-option>
+                        </el-select>
+                    </el-input>
+                    <el-button round type="primary" class="send-btn" size="mini" @click.stop="sendMessage">{{ $t('device.instruction-parsing.830424-14') }}</el-button>
+                </div>
+            </el-col>
+            <el-col :span="8" class="right">
+                <div class="head right-head">{{ $t('device.instruction-parsing.830424-15') }}</div>
+                <div class="content beautify-scroll-def">
+                    <div v-for="(item, index) in quickParsing" :key="index" class="quick-item" @click.stop="quickClick(item)" @contextmenu.prevent="onContextmenu($event, item)">
+                        {{ item.name }}
+                    </div>
+                </div>
+                <div class="right-bottom" @click.stop="openEdit">{{ $t('device.instruction-parsing.830424-16') }}</div>
+            </el-col>
+        </el-row>
+        <el-dialog :title="editName ? $t('device.instruction-parsing.830424-17') : $t('device.instruction-parsing.830424-18')" :visible.sync="editDialog" :width="editName ? '30%' : '40%'">
+            <div class="dialog-content beautify-scroll-def" v-show="!editName">
+                <el-form :model="createForm" label-position="top">
+                    <el-row :gutter="40">
+                        <!-- 从机地址 -->
+                        <el-col :span="24">
+                            <el-form-item :label="$t('device.instruction-parsing.830424-19')" prop="path">
+                                <el-input v-model="createForm.path" :disabled="true"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 指令类型 -->
+                        <el-col :span="12">
+                            <el-form-item :label="$t('device.instruction-parsing.830424-20')" prop="start">
+                                <el-select v-model="createForm.start" @change="changeNum">
+                                    <el-option :label="start.label" :value="start.value" v-for="start in startList" :key="start.value"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 功能码 -->
+                        <el-col :span="12">
+                            <el-form-item :label="$t('device.instruction-parsing.830424-21')" prop="functionCode">
+                                <el-select v-model="createForm.functionCode" @change="changeNum" :disabled="createForm.start == '0xFFAA'">
+                                    <el-option :label="functionCode.label" :value="functionCode.value" v-for="functionCode in functionCodeList" :key="functionCode.value"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <!--起始寄存器地址-->
+                        <el-col :span="12">
+                            <el-form-item prop="startPath">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">{{ $t('device.instruction-parsing.830424-22') }}</div>
+                                    <el-tooltip :content="createForm.startPathSwitch" placement="top">
+                                        <el-switch v-model="createForm.startPathSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949" active-value="Dec" inactive-value="Hex" />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="createForm.startPath"
+                                    type="number"
+                                    v-show="createForm.startPathSwitch == 'Dec'"
+                                    :min="0"
+                                    @change="
+                                        () => {
+                                            createForm.startPath16 = int2hex(createForm.startPath);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            createForm.startPath16 = int2hex(createForm.startPath);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ createForm.startPath16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="createForm.startPath16"
+                                    v-show="createForm.startPathSwitch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            createForm.startPath = hex2int(createForm.startPath16);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ createForm.startPath }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 个数或写值 -->
+                        <el-col :span="12">
+                            <!-- 个数 -->
+                            <el-form-item :label="registerNumTitle" prop="registerNum" v-show="!['05', '06'].includes(createForm.functionCode)">
+                                <el-input-number v-model="createForm.registerNum" controls-position="right" :min="0" @change="changeNum" />
+                            </el-form-item>
+                            <!-- 写值 -->
+                            <el-form-item prop="setValue" v-show="['05', '06'].includes(createForm.functionCode)">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">{{ registerNumTitle }}</div>
+                                    <el-tooltip :content="createForm.setValueSwitch" placement="top">
+                                        <el-switch v-model="createForm.setValueSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949" active-value="Dec" inactive-value="Hex" />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="createForm.setValue"
+                                    type="number"
+                                    v-show="createForm.setValueSwitch == 'Dec'"
+                                    @change="
+                                        () => {
+                                            createForm.setValue16 = int2hex(createForm.setValue);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            createForm.setValue16 = int2hex(createForm.setValue);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ createForm.setValue16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="createForm.setValue16"
+                                    v-show="createForm.setValueSwitch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            createForm.setValue = hex2int(createForm.setValue16);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ createForm.setValue }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 批量写寄存器值  -->
+                        <el-col :span="12" v-for="(item, index) in registerValList" :key="'register' + index" v-show="createForm.functionCode == '16'">
+                            <el-form-item prop="registerValList">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">#{{ index }} {{ $t('device.instruction-parsing.830424-23') }}</div>
+                                    <el-tooltip :content="item.switch" placement="top">
+                                        <el-switch
+                                            v-model="item.switch"
+                                            size="mini"
+                                            active-color="#13ce66"
+                                            @change="
+                                                () => {
+                                                    refreshRegisterInpust(item, index);
+                                                }
+                                            "
+                                            inactive-color="#ff4949"
+                                            active-value="Dec"
+                                            inactive-value="Hex"
+                                        />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="item.value"
+                                    type="number"
+                                    v-show="item.switch == 'Dec'"
+                                    :min="0"
+                                    @change="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ item.value16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="item.value16"
+                                    v-show="item.switch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            item.value = hex2int(item.value16);
+                                            refreshRegisterInpust(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ item.value }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 批量上报值  -->
+                        <el-col :span="12" v-for="(item, index) in reportValList" :key="'report' + index" v-show="createForm.start == '0xFFAA'">
+                            <el-form-item prop="reportValList">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">#{{ index }} {{ $t('device.instruction-parsing.830424-24') }}</div>
+                                    <el-tooltip :content="item.switch" placement="top">
+                                        <el-switch
+                                            v-model="item.switch"
+                                            size="mini"
+                                            active-color="#13ce66"
+                                            @change="
+                                                () => {
+                                                    refreshReportValList(item, index);
+                                                }
+                                            "
+                                            inactive-color="#ff4949"
+                                            active-value="Dec"
+                                            inactive-value="Hex"
+                                        />
+                                    </el-tooltip>
+                                </div>
+                                <el-input
+                                    v-model="item.value"
+                                    type="number"
+                                    v-show="item.switch == 'Dec'"
+                                    :min="0"
+                                    @change="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshReportValList(item, index);
+                                        }
+                                    "
+                                    @input="
+                                        () => {
+                                            item.value16 = int2hex(item.value);
+                                            refreshReportValList(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">0x{{ item.value16 }}</div>
+                                </el-input>
+                                <el-input
+                                    v-model="item.value16"
+                                    v-show="item.switch != 'Dec'"
+                                    @input="
+                                        () => {
+                                            item.value = hex2int(item.value16);
+                                            refreshReportValList(item, index);
+                                        }
+                                    "
+                                >
+                                    <div slot="append">{{ item.value }}</div>
+                                </el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 批量写线圈值  -->
+                        <el-col :span="6" v-for="(item, index) in IOValList" :key="'IO' + index" v-show="createForm.functionCode == '15'">
+                            <el-form-item prop="registerValList">
+                                <div slot="label" class="form-item-label">
+                                    <div style="margin-right: auto">#{{ index }} {{ $t('device.instruction-parsing.830424-25') }}</div>
+                                </div>
+                                <el-switch
+                                    v-model="item.value"
+                                    active-value="1"
+                                    inactive-value="0"
+                                    @change="
+                                        () => {
+                                            refreshIOInpust(item, index);
+                                        }
+                                    "
+                                />
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-form>
+                <div v-loading="createLoading">
+                    <div class="create-title">
+                        <el-button type="text" @click.stop="encode">{{ $t('device.instruction-parsing.830424-26') }}</el-button>
+                        <div class="title-right">
+                            <el-button type="" size="mini" :disabled="!canSend" @click="openEditName({ command: createCode })">{{ $t('device.instruction-parsing.830424-27') }}</el-button>
+                            <el-button type="primary" :disabled="!canSend" size="mini" @click="copyText(createCode)">{{ $t('device.instruction-parsing.830424-28') }}</el-button>
+                        </div>
+                    </div>
+                    <div class="create-code">{{ createCode }}</div>
+                </div>
+            </div>
+            <div v-show="editName" class="dialog-content">
+                <el-form :model="editNameForm">
+                    <el-form-item :label="$t('device.instruction-parsing.830424-29')">
+                        <el-input v-model="editNameForm.name" :placeholder="$t('device.instruction-parsing.830424-30')" style="width: 60%"></el-input>
+                    </el-form-item>
+                    <el-form-item :label="$t('device.instruction-parsing.830424-31')">
+                        <el-input v-model="editNameForm.command" :disabled="true" style="width: 60%"></el-input>
+                    </el-form-item>
+                </el-form>
+            </div>
+            <div slot="footer" class="dialog-btn">
+                <el-button type="" size="mini" @click="editDialog = false">{{ $t('device.instruction-parsing.830424-32') }}</el-button>
+                <el-button type="primary" :disabled="!canSend" size="mini" @click="confrimBtn" v-loading="saveLoading">{{ $t('device.instruction-parsing.830424-33') }}</el-button>
+            </div>
+        </el-dialog>
+        <el-dialog :visible.sync="delDialog" :title="$t('device.instruction-parsing.830424-34')" width="30%">
+            <div style="padding: 20px">{{ $t('device.instruction-parsing.830424-35') }}{{ delItem.name }}{{ $t('device.instruction-parsing.830424-35') }}</div>
+            <div slot="footer" class="dialog-btn">
+                <el-button type="" size="mini" @click="delDialog = false">{{ $t('device.instruction-parsing.830424-37') }}</el-button>
+                <el-button type="primary" size="mini" @click="delQuick">{{ $t('device.instruction-parsing.830424-38') }}</el-button>
+            </div>
+        </el-dialog>
+        <el-dialog :title="$t('device.instruction-parsing.830424-39')" width="40%" :visible.sync="moreDialog">
+            <div class="dialog-content beautify-scroll-def">
+                <el-table :border="false" :data="analysisList" height="400" border style="width: 100%">
+                    <el-table-column type="index" :label="$t('device.instruction-parsing.830424-40')" />
+                    <el-table-column prop="name" :label="$t('device.instruction-parsing.830424-41')"></el-table-column>
+                    <el-table-column prop="id" :label="$t('device.instruction-parsing.830424-42')"></el-table-column>
+                    <el-table-column prop="value" :label="$t('device.instruction-parsing.830424-43')"></el-table-column>
+                </el-table>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { hex2int, int2hex, copyText, formatDate, deepClone } from '@/utils/common';
+import { encode, decode, messagePost, addPreferences, editPreferences, delPreferences, preferencesList } from '@/api/iot/mqttTest';
+export default {
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    created() {
+        this.getPreferencesList();
+    },
+    watch: {
+        device() {
+            this.getPreferencesList();
+        },
+    },
+    computed: {
+        /**编辑寄存器个数显示的标题 */
+        registerNumTitle() {
+            switch (this.createForm.functionCode) {
+                case '01':
+                case '02':
+                case '15':
+                    return this.$t('device.instruction-parsing.830424-44');
+                case '03':
+                case '04':
+                case '16':
+                    return this.$t('device.instruction-parsing.830424-45');
+                case '05':
+                    return this.$t('device.instruction-parsing.830424-46');
+                case '06':
+                    return this.$t('device.instruction-parsing.830424-47');
+            }
+        },
+    },
+    data() {
+        return {
+            //选中的格式
+            format: 'Hex',
+            //格式列表
+            formatList: ['Hex', 'JSON', 'Plaintext'],
+            //数据列表
+            logList: [],
+            //发送的命令
+            sendVal: '',
+            //发送的主题
+            topic: '/function/get',
+            //快捷指令数组
+            quickParsing: [],
+            //编辑框
+            editDialog: false,
+            //生成表单
+            createForm: {},
+            //功能码列表
+            functionCodeList: [
+                {
+                    label: this.$t('device.instruction-parsing.830424-48'),
+                    value: '01',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-49'),
+                    value: '02',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-50'),
+                    value: '03',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-51'),
+                    value: '04',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-52'),
+                    value: '05',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-53'),
+                    value: '06',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-54'),
+                    value: '15',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-55'),
+                    value: '16',
+                },
+            ],
+            //上报下发列表
+            startList: [
+                {
+                    label: this.$t('device.instruction-parsing.830424-56'),
+                    value: '0xFFDD',
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-57'),
+                    value: '0xFFAA',
+                },
+            ],
+            //生成的指令码
+            createCode: '',
+            //批量写的寄存器值数组
+            registerValList: [],
+            //批量上报值数组
+            reportValList: [],
+            //批量写的线圈个数
+            IOValList: [],
+            //编辑名称
+            editName: false,
+            //编辑名称表单
+            editNameForm: {},
+            //生成刷新
+            createLoading: false,
+            //删除快捷指令
+            delDialog: false,
+            //选择删除的快捷指令
+            delItem: {},
+            //选择的解析组
+            analysisList: [],
+            //更多解析弹框
+            moreDialog: false,
+            //保存按钮loading
+            saveLoading: false,
+            canSend: false,
+        };
+    },
+    methods: {
+        /**打开编辑框 */
+        openEdit() {
+            this.resetCreateForm();
+            this.editName = false;
+            this.editDialog = true;
+            this.canSend = false;
+        },
+        /**打开更多解析框 */
+        openMore(item) {
+            this.analysisList = item.analysisList || [];
+            this.moreDialog = true;
+        },
+        /**打开编辑名字的框 */
+        openEditName(item) {
+            this.editNameForm = {
+                name: item.name || '',
+                command: item.command,
+            };
+            this.editName = true;
+            this.editDialog = true;
+        },
+        /**重置编辑框 */
+        resetCreateForm() {
+            this.createForm = {
+                path: '01',
+                functionCode: '01',
+                startPath: 0,
+                startPath16: '0000',
+                registerNum: 1,
+                startPathSwitch: 'Dec',
+                setValue: 0,
+                setValue16: '0000',
+                setValueSwitch: 'Dec',
+                start: '0xFFDD',
+            };
+            this.createCode = '';
+        },
+        /**十进制变成十六进制 */
+        int2hex(str) {
+            return int2hex(str);
+        },
+        /**十六进制变成十进制 */
+        hex2int(str) {
+            return hex2int(str);
+        },
+        /**修改批量写的寄存器个数或者线圈个数 */
+        changeNum() {
+            //判断是不是上报
+            if (this.createForm.start == '0xFFAA') {
+                for (let index = 0; index < this.createForm.registerNum; index++) {
+                    const item = this.reportValList[index];
+                    if (!item) {
+                        this.reportValList[index] = {
+                            value: 0,
+                            value16: '0000',
+                            switch: 'Dec',
+                        };
+                    }
+                }
+                //如果编写数组大于他需要写的数字,就到写的个数
+                if (this.registerValList.length > this.createForm.reportValList) {
+                    //多的个数
+                    const num = this.reportValList.length - this.createForm.reportValList;
+                    this.registerValList.splice(this.createForm.reportValList, num);
+                }
+                //上报功能码是03
+                this.createForm.functionCode = '03';
+            } else {
+                //批量写寄存器
+                if (this.createForm.functionCode == '16') {
+                    for (let index = 0; index < this.createForm.registerNum; index++) {
+                        const item = this.registerValList[index];
+                        if (!item) {
+                            this.registerValList[index] = {
+                                value: 0,
+                                value16: '0000',
+                                switch: 'Dec',
+                            };
+                        }
+                    }
+                    //如果编写数组大于他需要写的数字,就到写的个数
+                    if (this.registerValList.length > this.createForm.registerNum) {
+                        //多的个数
+                        const num = this.registerValList.length - this.createForm.registerNum;
+                        this.registerValList.splice(this.createForm.registerNum, num);
+                    }
+                }
+                //批量写线圈
+                if (this.createForm.functionCode == '15') {
+                    for (let index = 0; index < this.createForm.registerNum; index++) {
+                        const item = this.IOValList[index];
+                        if (!item) {
+                            this.IOValList[index] = {
+                                value: '0',
+                            };
+                        }
+                    }
+                    //如果编写数组大于他需要写的数字,就到写的个数
+                    if (this.IOValList.length > this.createForm.registerNum) {
+                        //多的个数
+                        const num = this.IOValList.length - this.createForm.registerNum;
+                        this.IOValList.splice(this.createForm.registerNum, num);
+                    }
+                }
+            }
+        },
+        /**刷新寄存器输入框 */
+        refreshRegisterInpust(item, index) {
+            this.$set(this.registerValList, index, item);
+        },
+        /**刷新上报读取设定的值 */
+        refreshReportValList(item, index) {
+            this.$set(this.reportValList, index, item);
+        },
+        /**刷新线圈值 */
+        refreshIOInpust(item, index) {
+            this.$set(this.IOValList, index, item);
+        },
+        /**确认按钮 */
+        async confrimBtn() {
+            if (this.editName) {
+                try {
+                    this.saveLoading = true;
+                    if (this.editNameForm.id) {
+                        await editPreferences({
+                            command: this.editNameForm.command,
+                            name: this.editNameForm.name,
+                            serialNumber: this.device.serialNumber,
+                            id: this.editNameForm.id,
+                        });
+                    } else {
+                        await addPreferences({
+                            command: this.editNameForm.command,
+                            name: this.editNameForm.name,
+                            serialNumber: this.device.serialNumber,
+                        });
+                    }
+                    this.$message({
+                        type: 'success',
+                        message: `${this.editNameForm.id ? this.$t('device.instruction-parsing.830424-58') : this.$t('device.instruction-parsing.830424-59')}this.$t('device.instruction-parsing.830424-60')`,
+                    });
+                    this.getPreferencesList();
+                } catch (err) {
+                    this.$message({
+                        type: 'error',
+                        message:
+                            err.message || `${this.editNameForm.id ? this.$t('device.instruction-parsing.830424-58') : this.$t('device.instruction-parsing.830424-59')}this.$t('device.instruction-parsing.830424-61')`,
+                    });
+                }
+            } else {
+                this.sendVal = this.createCode;
+            }
+            this.saveLoading = false;
+            this.editDialog = false;
+        },
+        /**获取指令列表 */
+        async getPreferencesList() {
+            try {
+                const { rows } = await preferencesList({ serialNumber: this.device.serialNumber });
+                this.quickParsing = rows;
+            } catch (err) {
+                console.log('🚀 ~ getPreferencesList ~ err:', err);
+            }
+        },
+        /**快捷按钮 */
+        quickClick(item) {
+            this.sendVal = item.command;
+        },
+        /**复制 */
+        copyText(command) {
+            const res = copyText(command);
+            this.$message({
+                type: res.type,
+                message: res.message,
+            });
+        },
+        /**编码 */
+        async encode() {
+            try {
+                this.createLoading = true;
+                let params = {
+                    slaveId: parseInt(this.createForm.path), //从机地址
+                    address: this.createForm.startPath, //起始寄存器地址
+                    code: parseInt(this.createForm.functionCode), //功能码
+                    start: this.createForm.start,
+                };
+                //判断是不是上报
+                if (this.createForm.start == '0xFFAA') {
+                    params.address = this.createForm.startPath;
+                    params.bitCount = this.createForm.registerNum * 2;
+                    const reportValList = this.reportValList.map((item) => {
+                        return item.value;
+                    });
+                    params.data = reportValList;
+                } else {
+                    switch (this.createForm.functionCode) {
+                        case '01':
+                        case '02':
+                        case '03':
+                        case '04':
+                            //线圈个数/寄存器个数
+                            params.count = this.createForm.registerNum;
+                            break;
+                        case '05':
+                        case '06':
+                            //线圈值/寄存器值
+                            params.writeData = this.createForm.setValue;
+                            break;
+                        case '15':
+                            //线圈个数/寄存器个数
+                            params.count = this.createForm.registerNum;
+                            //线圈值数组
+                            const IOValList = this.IOValList.map((item) => {
+                                return item.value;
+                            });
+                            params.bitString = IOValList.join('');
+                            break;
+                        case '16':
+                            //线圈个数/寄存器个数
+                            params.count = this.createForm.registerNum;
+                            //寄存器值数组
+                            const registerValList = this.registerValList.map((item) => {
+                                return item.value;
+                            });
+                            params.tenWriteData = registerValList;
+                            break;
+                    }
+                }
+                const res = await encode(params);
+                this.createCode = res.msg;
+            } catch (err) {
+                this.$message({
+                    type: 'error',
+                    message: err.message || this.$t('device.instruction-parsing.830424-62'),
+                });
+            } finally {
+                this.createLoading = false;
+                this.canSend = true;
+            }
+        },
+        /**发送指令 */
+        sendMessage() {
+            try {
+                if (this.sendVal) {
+                    messagePost({
+                        message: this.sendVal,
+                        serialNumber: this.device.serialNumber,
+                        topicName: this.topic,
+                    });
+                } else {
+                    throw { message: this.$t('device.instruction-parsing.830424-63') };
+                }
+            } catch (err) {
+                this.$message({
+                    type: 'error',
+                    message: err.message || this.$t('device.instruction-parsing.830424-64'),
+                });
+            }
+        },
+        /**解析数据 */
+        analysisData(item, index) {
+            try {
+                item.loading = true;
+                item.analysis = true;
+                if (item.analysisList[0]) {
+                    item.analysisVal = {
+                        name: item.analysisList[0].name,
+                        value: item.analysisList[0].value,
+                        id: item.analysisList[0].id,
+                    };
+                } else {
+                    throw { message: this.$t('device.instruction-parsing.830424-65') };
+                }
+            } catch (err) {
+                this.$message({
+                    type: 'error',
+                    message: err.message || this.$t('device.instruction-parsing.830424-66'),
+                });
+            } finally {
+                item.loading = false;
+            }
+        },
+        /**右击快捷指令 */
+        onContextmenu(event, item) {
+            const contextMenuData = [
+                {
+                    label: this.$t('device.instruction-parsing.830424-67'),
+                    icon: 'iconfont el-icon-edit-outline',
+                    onClick: () => {
+                        this.editNameForm = deepClone(item);
+                        this.editName = true;
+                        this.editDialog = true;
+                    },
+                },
+                {
+                    label: this.$t('device.instruction-parsing.830424-68'),
+                    icon: 'iconfont el-icon-delete',
+                    onClick: () => {
+                        this.delItem = item;
+                        this.delDialog = true;
+                    },
+                },
+            ];
+            this.$contextmenu({
+                items: contextMenuData,
+                event, // 鼠标事件信息
+                zIndex: 3, // 菜单样式 z-index
+                minWidth: 230, // 主菜单最小宽度
+            });
+            return false;
+        },
+        /**删除指令 */
+        async delQuick() {
+            try {
+                await delPreferences(this.delItem);
+                this.$message({
+                    type: 'success',
+                    message: this.$t('device.instruction-parsing.830424-69'),
+                });
+                this.delDialog = false;
+                this.getPreferencesList();
+            } catch (err) {
+                this.$message({
+                    type: 'error',
+                    message: err.message || this.$t('device.instruction-parsing.830424-70'),
+                });
+            }
+        },
+    },
+    mounted() {
+        this.resetCreateForm();
+        this.$busEvent.$on('updateData', (params) => {
+            const { serialNumber, productId, data } = params;
+            if (serialNumber == this.device.serialNumber) {
+                this.logList.push({
+                    //0是下发,1是回复,2是上报
+                    type: 2,
+                    time: formatDate(new Date()),
+                    value: data.sources,
+                    loading: false,
+                    analysis: false,
+                    //解析的数据数组
+                    analysisList: data.message,
+                });
+            }
+        });
+        this.$busEvent.$on('updateMqttMessage', (params) => {
+            console.log('params', params);
+            const { data, serialNumber } = params;
+            if (serialNumber == this.device.serialNumber) {
+                this.logList.push({
+                    //0是下发,1是回复,2是上报
+                    type: data.topicName == '/service/reply' ? 1 : data.topicName == '/property/post' ? 2 : 0,
+                    time: formatDate(data.time || new Date()),
+                    value: data.message,
+                    loading: false,
+                    analysis: false,
+                    //解析的数据数组
+                    analysisList: [],
+                });
+            }
+        });
+    },
+    beforeDestroy() {
+        this.$busEvent.$off('updateMqttMessage');
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+$border-color: #dcdfe6;
+$right-btn-color: #1890ff;
+
+.instruction-parsing {
+    border: 1px solid $border-color;
+    width: 90%;
+    margin: auto;
+
+    .left {
+        border-right: 1px solid $border-color;
+
+        .bottom {
+            display: flex;
+            border-top: 1px solid $border-color;
+            position: relative;
+            height: 64px;
+            align-items: center;
+
+            ::v-deep .el-select .el-input {
+                width: 80px;
+            }
+
+            ::v-deep .el-input-group--prepend .el-input__inner {
+                border: 0px;
+                background-color: #fff;
+            }
+
+            ::v-deep .el-input-group__prepend {
+                border: 0;
+                // border-right: 1px solid $border-color;
+            }
+
+            .send-btn {
+                position: absolute;
+                top: 50%;
+                transform: translateY(-50%);
+                right: 24px;
+            }
+
+            .input-with-select {
+                max-width: calc(100% - 80px);
+            }
+        }
+    }
+
+    .head {
+        line-height: 36px;
+        display: flex;
+        padding: 0 16px;
+        align-items: center;
+        font-size: 12px;
+        border-bottom: 1px solid $border-color;
+
+        .el-dropdown-link {
+            cursor: pointer;
+        }
+
+        .el-icon-arrow-down {
+            font-size: 12px;
+        }
+    }
+
+    .right {
+        .right-head {
+            font-size: 14px;
+            text-align: center;
+            width: 100%;
+            justify-content: center;
+        }
+
+        .quick-item {
+            width: 80%;
+            margin: auto;
+            text-align: center;
+            line-height: 36px;
+            margin-top: 12px;
+            border: 1px solid $right-btn-color;
+            color: $right-btn-color;
+            cursor: pointer;
+            border-radius: 4px;
+
+            &:last-child {
+                margin-bottom: 12px;
+            }
+        }
+
+        .right-bottom {
+            line-height: 64px;
+            background-color: $right-btn-color;
+            border-top: 1px solid $right-btn-color;
+            font-size: 24px;
+            color: #fff;
+            text-align: center;
+            cursor: pointer;
+        }
+    }
+
+    .content {
+        height: 600px;
+        width: 100%;
+        overflow: auto;
+        padding: 20px;
+
+        .send-class {
+            border-left: 4px solid #e6a23c;
+            margin-left: auto;
+            color: #e6a23c;
+
+            .head-time {
+                margin-left: auto !important;
+                color: #c0c4cc;
+            }
+        }
+
+        .receive-class {
+            border-left: 4px solid #67c23a;
+            color: #67c23a;
+        }
+
+        .report-class {
+            border-left: 4px solid #409eff;
+            color: #409eff;
+        }
+
+        .item-class {
+            width: 50%;
+            box-sizing: border-box;
+            border-radius: 4px;
+            padding: 0 8px;
+            background-color: #f2f6fc;
+            margin-bottom: 16px;
+
+            .item-head {
+                display: flex;
+                border-bottom: 1px solid $border-color;
+                font-size: 14px;
+                padding: 8px 0;
+                align-items: center;
+
+                .head-time {
+                    margin-left: 8px;
+                    color: #c0c4cc;
+                    font-size: 12px;
+                    margin-top: 3px;
+                }
+
+                .analysis-btn {
+                    background: #1890ff;
+                    color: #fff;
+                    cursor: pointer;
+                }
+
+                .right-btn {
+                    margin-left: auto;
+                    padding: 8px 12px;
+                    border-radius: 4px;
+                    line-height: 12px;
+                    font-size: 14px;
+                }
+
+                .analysised {
+                    background: #909399;
+                    color: #e4e7ed;
+                }
+            }
+
+            .item-content {
+                padding: 8px 0;
+                color: #303133;
+                font-size: 14px;
+                line-height: 20px;
+                display: flex;
+                align-items: center;
+
+                .content-value {
+                    max-width: 100%;
+                    word-wrap: break-word;
+                }
+            }
+
+            .analysis-data {
+                margin-top: 12px;
+                font-size: 12px;
+                line-height: 18px;
+                width: 100%;
+                color: #303133;
+                font-weight: 600;
+                display: flex;
+                flex-wrap: wrap;
+
+                .data-col {
+                    // width:100%;
+                    display: flex;
+                    margin-bottom: 10px;
+
+                    .label {
+                        text-align: right;
+                        margin-right: 12px;
+                        color: #c0c4cc;
+                        font-weight: 400;
+                    }
+                }
+
+                .left-col {
+                    width: 60%;
+
+                    .label {
+                        width: 64px;
+                    }
+                }
+
+                .right-col {
+                    margin-left: auto;
+                }
+            }
+        }
+    }
+
+    ::v-deep .el-dialog__body {
+        border-top: 1px solid $border-color;
+        border-bottom: 1px solid $border-color;
+        box-sizing: border-box;
+        padding: 0;
+    }
+
+    .dialog-content {
+        max-height: 500px;
+        width: 100%;
+        box-sizing: border-box;
+        padding: 30px 20px;
+        overflow: auto;
+
+        .create-title {
+            display: flex;
+            line-height: 36px;
+            margin-bottom: 16px;
+
+            .title-right {
+                margin-left: auto;
+            }
+        }
+
+        .create-code {
+            font-size: 18px;
+            line-height: 36px;
+            font-weight: 800;
+        }
+
+        .form-item-label {
+            display: flex;
+            align-items: center;
+            width: 100%;
+
+            ::v-deep .el-form-item__label {
+                width: 100%;
+            }
+        }
+    }
+}
+</style>

+ 152 - 0
src/views/pms/video_center/device/product-list.vue

@@ -0,0 +1,152 @@
+<template>
+    <el-dialog :title="$t('device.product-list.058448-0')" :visible.sync="open" width="800px">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+                <el-form-item :label="$t('device.allot-record.155854-2')" prop="productName">
+                    <el-input v-model="queryParams.productName" :placeholder="$t('device.product-list.058448-2')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.product-list.058448-3') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.product-list.058448-4') }}</el-button>
+                </el-form-item>
+            </el-form>
+
+            <el-table :border="false" v-loading="loading" ref="singleTable" :data="productList" @row-click="rowClick" highlight-current-row size="mini">
+                <el-table-column :label="$t('device.device-edit.148398-6')" width="50" align="center">
+                    <template slot-scope="scope">
+                        <input type="radio" :checked="scope.row.isSelect" name="product" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.allot-record.155854-2')" align="center" prop="productName" />
+                <el-table-column :label="$t('device.product-list.058448-6')" align="center" prop="categoryName" />
+                <el-table-column :label="$t('device.product-list.058448-7')" align="center" prop="tenantName" />
+                <el-table-column :label="$t('device.product-list.058448-8')" align="center" prop="status" width="70">
+                    <template slot-scope="scope">
+                        <el-tag type="success" v-if="scope.row.isAuthorize == 1">{{ $t('device.product-list.058448-9') }}</el-tag>
+                        <el-tag type="info" v-if="scope.row.isAuthorize == 0">{{ $t('device.product-list.058448-10') }}</el-tag>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.product-list.058448-11')" align="center" prop="status">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_vertificate_method" :value="scope.row.vertificateMethod" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.product-list.058448-12')" align="center" prop="networkMethod">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_network_method" :value="scope.row.networkMethod" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('device.product-list.058448-13')" align="center" prop="createTime" width="100">
+                    <template slot-scope="scope">
+                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        </div>
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="confirmSelectProduct" type="primary">{{ $t('device.product-list.058448-14') }}</el-button>
+            <el-button @click="closeDialog" type="info">{{ $t('device.product-list.058448-15') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listProduct } from '@/api/iot/product';
+
+export default {
+    name: 'ProductList',
+    dicts: ['iot_vertificate_method', 'iot_network_method'],
+    props: {
+        productId: {
+            type: Number,
+            default: 0,
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 打开选择产品对话框
+            open: false,
+            // 产品列表
+            productList: [],
+            // 选中的产品
+            product: {},
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                productName: null,
+                categoryId: null,
+                categoryName: null,
+                tenantId: null,
+                tenantName: null,
+                isSys: null,
+                status: 2, //已发布
+                deviceType: null,
+                networkMethod: null,
+            },
+        };
+    },
+    created() {},
+    methods: {
+        /** 查询产品列表 */
+        getList() {
+            this.loading = true;
+            listProduct(this.queryParams).then((response) => {
+                //产品列表初始化isSelect值,用于单选
+                for (let i = 0; i < response.rows.length; i++) {
+                    response.rows[i].isSelect = false;
+                }
+                this.productList = response.rows;
+                this.total = response.total;
+                if (this.productId != 0) {
+                    this.setRadioSelected(this.productId);
+                }
+                this.loading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /** 单选数据 */
+        rowClick(product) {
+            if (product != null) {
+                this.setRadioSelected(product.productId);
+                this.product = product;
+            }
+        },
+        /** 设置单选按钮选中 */
+        setRadioSelected(productId) {
+            for (let i = 0; i < this.productList.length; i++) {
+                if (this.productList[i].productId == productId) {
+                    this.productList[i].isSelect = true;
+                } else {
+                    this.productList[i].isSelect = false;
+                }
+            }
+        },
+        /**确定选择产品,产品传递给父组件 */
+        confirmSelectProduct() {
+            this.$emit('productEvent', this.product);
+            this.open = false;
+        },
+        /**关闭对话框 */
+        closeDialog() {
+            this.open = false;
+        },
+    },
+};
+</script>

+ 648 - 0
src/views/pms/video_center/device/realTime-status.bak.vue

@@ -0,0 +1,648 @@
+<template>
+    <div class="running-status beautify-scroll-def" v-loading="loading">
+        <el-main v-loading="loading" style="position: relative" class="H100">
+            <el-row :gutter="12" class="row-list" v-if="!loading && runningData.length > 0">
+                <el-col :span="6" v-for="(item, index) in runningData" :key="index" style="margin-bottom: 10px; height: 110px">
+                    <el-card shadow="hover" class="elcard">
+                        <div class="head">
+                            <div class="title">
+                                {{ item.name || '--' }}
+                                <el-tooltip :content="$t('device.realTime-status.845353-0')" v-if="item.isReadonly == 0" class="title_send">
+                                    <span class="el-icon-s-promotion" @click.stop="editFunc(item)">
+                                        <span class="send_title">{{ $t('device.realTime-status.845353-1') }}</span>
+                                    </span>
+                                </el-tooltip>
+                            </div>
+                            <div class="name">
+                                <span class="value_class">{{ item.valueName || '-' }}</span>
+                                <span v-if="item.datatype.unit && item.datatype.unit != 'un'">
+                                    {{ item.datatype.unit || item.datatype.unitName }}
+                                </span>
+                            </div>
+                        </div>
+                        <div class="card-bottom">{{ $t('device.realTime-status.845353-2') }}{{ item.ts || '--' }}</div>
+                    </el-card>
+                </el-col>
+            </el-row>
+            <el-empty :image-size="200" v-if="!loading && runningData.length === 0"></el-empty>
+        </el-main>
+        <el-dialog :title="$t('device.realTime-status.845353-3')" :visible.sync="dialogValue" width="30%">
+            <el-form size="mini" style="height: 100%; padding: 0 20px">
+                <el-form-item v-for="(item, index) in opationList" :label="`${item.label}:`" :key="index" label-width="180px">
+                    <el-input
+                        v-model="funVal[item.key]"
+                        :precision="0"
+                        :controls="false"
+                        @input="justicNumber(item)"
+                        v-if="item.dataTypeName == 'integer' || item.dataTypeName == 'decimal'"
+                        style="width: 50%"
+                        type="number"
+                    ></el-input>
+                    <el-select v-if="item.dataTypeName == 'enum' || item.dataTypeName == 'singleBoolean' || item.dataTypeName == 'bool'" v-model="funVal[item.key]" @change="changeSelect()">
+                        <el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value"></el-option>
+                    </el-select>
+                    <span v-if="(item.dataTypeName == 'integer' || item.dataTypeName == 'decimal') && item.unit && item.unit != 'un' && item.unit != '/'">({{ item.unit }})</span>
+                    <span class="range" v-if="item.dataTypeName == 'integer' || item.dataTypeName == 'decimal'">({{ item.min }} ~ {{ item.max }})</span>
+                </el-form-item>
+                <el-form-item style="display: none">
+                    <el-input v-model="functionName"></el-input>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="dialogValue = false">{{ $t('device.realTime-status.845353-4') }}</el-button>
+                <el-button type="primary" @click="sendService" :loading="btnLoading" :disabled="!canSend">{{ $t('device.realTime-status.845353-5') }}</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { serviceInvoke, runningStatus } from '@/api/iot/runstatus';
+
+export default {
+    props: {
+        device: {
+            type: Object,
+            default: () => {
+                return {};
+            },
+        },
+    },
+    watch: {
+        device: {
+            handler(newVal) {
+                if (newVal && newVal.serialNumber) {
+                    this.params.serialNumber = newVal.serialNumber;
+                    this.serialNumber = newVal.serialNumber;
+                    this.params.productId = newVal.productId;
+                    this.params.slaveId = newVal.slaveId;
+                    this.params.deviceId = newVal.deviceId;
+                    this.deviceInfo = newVal;
+                    this.updateDeviceStatus(this.deviceInfo);
+                    this.slaveList = newVal.subDeviceList;
+                    this.getSlaveList(this.deviceInfo);
+                    this.$busEvent.$on('updateData', (params) => {
+                        if (params.data && params.data[0].remark) {
+                            this.getDeviceFuncLog();
+                            params.data[0].ts = params.data[0].remark;
+                        }
+                        this.updateData(params);
+                    });
+                    this.$busEvent.$on('updateLog', (params) => {
+                        this.getDeviceFuncLog();
+                    });
+                    // 添加message事件监听器
+                    this.mqttCallback();
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            activeGroup: '',
+            groupList: [],
+            runningData: [],
+            loading: false,
+            dialogValue: false,
+            canSend: false, //是否可以下发,主要判断数值在不在范围
+            btnLoading: false,
+            funVal: {},
+            chooseFun: {},
+            opationList: [],
+            functionName: '',
+        };
+    },
+    methods: {
+        /** qos改变事件 **/
+        qosChange(data) {},
+        /** 载体改变事件 **/
+        payloadTypeChange(data) {},
+        /** 获取当前时间 **/
+        getTime() {
+            let date = new Date();
+            let y = date.getFullYear();
+            let m = date.getMonth() + 1;
+            let d = date.getDate();
+            let H = date.getHours();
+            let mm = date.getMinutes();
+            let s = date.getSeconds();
+            m = m < 10 ? '0' + m : m;
+            d = d < 10 ? '0' + d : d;
+            H = H < 10 ? '0' + H : H;
+            return y + '-' + m + '-' + d + ' ' + H + ':' + mm + ':' + s;
+        },
+
+        /*获取运行状态*/
+        getRuntimeStatus() {
+            console.log('哈哈哈哈哈哈哈');
+
+            getDeviceRunningStatus(this.params).then((response) => {
+                this.runningData = response.data.thingsModels;
+                this.runningData.forEach((item) => {
+                    if (item.datatype.type == 'enum') {
+                        item.datatype.enumList.forEach((val) => {
+                            if (val.value == item.value) {
+                                item.value = val.text;
+                            }
+                        });
+                    } else if (item.datatype.type == 'bool') {
+                        item.value = item.value == 0 ? item.falseText : item.trueText;
+                    }
+                });
+                //筛选读写物模型
+                this.functionData = this.runningData.filter((item) => item.isReadonly == 0);
+            });
+        },
+
+        getGateway() {
+            getDeviceRunningStatus(this.params).then((response) => {
+                this.deviceInfo.thingsModels = response.data.thingsModels;
+            });
+        },
+
+        /**根据产品id获取从机列表*/
+        getSlaveList() {
+            this.getRuntimeStatus();
+            this.getDeviceFuncLog();
+        },
+        /*选择从机*/
+        selectSlave() {
+            this.params.serialNumber = this.serialNumber + '_' + this.params.slaveId;
+            this.getRuntimeStatus();
+        },
+        /*tabs切换*/
+        handleClick() {
+            if (this.thingsType === 'prop') {
+                this.params.type = 1;
+            } else if (this.thingsType === 'function') {
+                this.params.type = 2;
+                //筛选读写物模型
+                this.functionData = this.runningData.filter((item) => item.isReadonly == 0);
+            }
+        },
+        //切换实时状态
+        runtimeClick() {
+            if (this.runtimeName === 'gateway') {
+                this.params.serialNumber = this.serialNumber;
+                this.slaveId = this.params.slaveId;
+                this.params.slaveId = undefined;
+                this.getGateway();
+            } else {
+                this.params.serialNumber = this.serialNumber + '_' + this.slaveId;
+                this.params.slaveId = this.slaveId;
+                this.getRuntimeStatus();
+            }
+        },
+        // 更新参数值
+        updateParam(data) {},
+        //指令下发
+        editFunc(item) {
+            this.dialogValue = true;
+            this.canSend = true;
+            this.funVal = {};
+            this.getValueName(item);
+            this.from = item;
+            console.log(this.runningData);
+        },
+
+        /** 更新设备状态 */
+        updateDeviceStatus(device) {
+            if (device.status == 3) {
+                this.statusColor.background = '#12d09f';
+                this.title = '在线模式';
+            } else {
+                if (device.isShadow == 1) {
+                    this.statusColor.background = '#409EFF';
+                    this.title = '影子模式';
+                } else {
+                    this.statusColor.background = '#909399';
+                    this.title = '离线模式';
+                    this.shadowUnEnable = true;
+                }
+            }
+            this.$emit('statusEvent', this.deviceInfo.status);
+        },
+
+        // 解析值
+        getValueName(item) {
+            this.funVal[item.id] = item.value;
+        },
+        // 发送指令
+        sendService() {
+            console.log('下发指令', this.from.shadow);
+            try {
+                this.funVal[this.from.id] = this.from.shadow;
+                const data = {
+                    serialNumber: this.serialNumber,
+                    productId: this.params.productId,
+                    remoteCommand: this.funVal,
+                    identifier: this.from.id,
+                    slaveId: this.params.slaveId,
+                    modelName: this.from.name,
+                    isShadow: this.device.status != 3,
+                    type: this.from.type,
+                };
+                serviceInvoke(data).then((response) => {
+                    if (response.code == 200) {
+                        this.$message({
+                            type: 'success',
+                            message: '服务调用成功!',
+                        });
+                        this.getDeviceFuncLog();
+                    }
+                });
+            } finally {
+                this.dialogValue = false;
+            }
+        },
+
+        //发送指令
+        mqttPublish(device, model) {
+            const command = {};
+            command[model.id] = model.shadow;
+            const data = {
+                serialNumber: device.serialNumber,
+                productId: device.productId,
+                remoteCommand: command,
+                identifier: model.id,
+                modelName: model.name,
+                isShadow: device.status != 3,
+                type: model.type,
+            };
+            serviceInvoke(data).then((response) => {
+                if (response.code === 200) {
+                    this.$message({
+                        type: 'success',
+                        message: '服务调用成功!',
+                    });
+                }
+            });
+        },
+
+        getShowValue(value) {
+            switch (this.from.datatype.type) {
+                case ENUM:
+                    const list = this.from.datatype.enumList;
+                    list.forEach((m) => {
+                        if (m.value === value) {
+                            this.showValue = m.text;
+                        }
+                    });
+                    break;
+                case INTEGER:
+                case DECIMAL:
+                    this.showValue = value;
+                case BOOL:
+                    this.showValue = value == 1 ? this.from.datatype.trueText : this.from.datatype.falseText;
+                    break;
+            }
+        },
+
+        //下拉选择修改触发
+        changeSelect() {
+            this.$forceUpdate();
+        },
+
+        //判断输入是否超过范围
+        justicNumber() {
+            this.canSend = true;
+            if (this.from.datatype.max < this.funVal[this.from.identity] || this.from.datatype.min > this.funVal[this.from.identity]) {
+                this.canSend = false;
+                return true;
+            }
+            this.$forceUpdate();
+        },
+
+        //  获取设备服务下发日志
+        getDeviceFuncLog() {
+            const params = {
+                serialNumber: this.serialNumber,
+            };
+            console.log('params --', params);
+            funcLog(params).then((response) => {
+                this.logList = response.rows;
+            });
+        },
+        updateData(msg) {
+            if (msg.data) {
+                msg.data.forEach((d) => {
+                    this.runningData.some((old, index) => {
+                        if (d.slaveId === old.slaveId && d.id == old.id) {
+                            const template = this.runningData[index];
+                            template.ts = d.ts;
+                            template.value = d.value;
+                            if (old.datatype.type == 'enum') {
+                                old.datatype.enumList.forEach((val) => {
+                                    if (val.value == template.value) {
+                                        template.value = val.text;
+                                    }
+                                });
+                            } else if (old.datatype.type == 'bool') {
+                                template.value = template.value == 0 ? old.datatype.falseText : old.datatype.trueText;
+                            }
+                            this.$set(this.runningData, index, template);
+                            return true;
+                        }
+                    });
+                });
+            }
+        },
+
+        /* Mqtt回调处理 */
+        mqttCallback() {
+            this.$mqttTool.client.on('message', (topic, message, buffer) => {
+                let topics = topic.split('/');
+                let productId = topics[1];
+                let deviceNum = topics[2];
+                message = JSON.parse(message.toString());
+                if (!message) {
+                    return;
+                }
+                if (topics[3] == 'status') {
+                    console.log('接收到【设备状态-运行】主题:', topic);
+                    console.log('接收到【设备状态-运行】内容:', message);
+                    // 更新列表中设备的状态
+                    if (this.deviceInfo.serialNumber == deviceNum) {
+                        this.deviceInfo.status = message.status;
+                        this.deviceInfo.isShadow = message.isShadow;
+                        this.deviceInfo.rssi = message.rssi;
+                        this.updateDeviceStatus(this.deviceInfo);
+                    }
+                }
+                //兼容设备回复
+                if (topics[4] == 'reply') {
+                    this.$modal.notifySuccess(message);
+                }
+                if (topic.endsWith('ws/service')) {
+                    console.log('接收到【物模型】主题1:', topic);
+                    console.log('接收到【物模型】内容:', message);
+                    // 更新列表中设备的属性
+                    if (this.deviceInfo.serialNumber == deviceNum) {
+                        for (let j = 0; j < message.length; j++) {
+                            let isComplete = false;
+                            // 设备状态
+                            for (let k = 0; k < this.deviceInfo.thingsModels.length && !isComplete; k++) {
+                                if (this.deviceInfo.thingsModels[k].id == message[j].id) {
+                                    // 普通类型(小数/整数/字符串/布尔/枚举)
+                                    if (this.deviceInfo.thingsModels[k].datatype.type == 'decimal' || this.deviceInfo.thingsModels[k].datatype.type == 'integer') {
+                                        this.deviceInfo.thingsModels[k].shadow = Number(message[j].value);
+                                    } else {
+                                        this.deviceInfo.thingsModels[k].shadow = message[j].value;
+                                    }
+                                    isComplete = true;
+                                    break;
+                                } else if (this.deviceInfo.thingsModels[k].datatype.type == 'object') {
+                                    // 对象类型
+                                    for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params.length; n++) {
+                                        if (this.deviceInfo.thingsModels[k].datatype.params[n].id == message[j].id) {
+                                            this.deviceInfo.thingsModels[k].datatype.params[n].shadow = message[j].value;
+                                            isComplete = true;
+                                            break;
+                                        }
+                                    }
+                                } else if (this.deviceInfo.thingsModels[k].datatype.type == 'array') {
+                                    // 数组类型
+                                    if (this.deviceInfo.thingsModels[k].datatype.arrayType == 'object') {
+                                        // 1.对象类型数组,id为数组中一个元素,例如:array_01_gateway_temperature
+                                        if (String(message[j].id).indexOf('array_') == 0) {
+                                            for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
+                                                for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
+                                                    if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == message[j].id) {
+                                                        this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = message[j].value;
+                                                        isComplete = true;
+                                                        break;
+                                                    }
+                                                }
+                                                if (isComplete) {
+                                                    break;
+                                                }
+                                            }
+                                        } else {
+                                            // 2.对象类型数组,例如:gateway_temperature,消息ID添加前缀后匹配
+                                            for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
+                                                for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
+                                                    let index = n > 9 ? String(n) : '0' + k;
+                                                    let prefix = 'array_' + index + '_';
+                                                    if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == prefix + message[j].id) {
+                                                        this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = message[j].value;
+                                                        isComplete = true;
+                                                    }
+                                                }
+                                                if (isComplete) {
+                                                    break;
+                                                }
+                                            }
+                                        }
+                                    } else {
+                                        // 整数、小数和字符串类型数组
+                                        for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel.length; n++) {
+                                            if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id == message[j].id) {
+                                                this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow = message[j].value;
+                                                isComplete = true;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                            // 图表数据
+                            for (let k = 0; k < this.deviceInfo.chartList.length; k++) {
+                                if (this.deviceInfo.chartList[k].id.indexOf('array_') == 0) {
+                                    // 数组类型匹配,例如:array_00_gateway_temperature
+                                    if (this.deviceInfo.chartList[k].id == message[j].id) {
+                                        // let shadows = message[j].value.split(",");
+                                        this.deviceInfo.chartList[k].shadow = message[j].value;
+                                        // 更新图表
+                                        for (let m = 0; m < this.monitorChart.length; m++) {
+                                            if (message[j].id == this.monitorChart[m].data.id) {
+                                                let data = [
+                                                    {
+                                                        value: message[j].value,
+                                                        name: this.monitorChart[m].data.name,
+                                                    },
+                                                ];
+                                                this.monitorChart[m].chart.setOption({
+                                                    series: [
+                                                        {
+                                                            data: data,
+                                                        },
+                                                    ],
+                                                });
+                                                break;
+                                            }
+                                        }
+                                    }
+                                } else {
+                                    // 普通类型匹配
+                                    if (this.deviceInfo.chartList[k].id == message[j].id) {
+                                        this.deviceInfo.chartList[k].shadow = message[j].value;
+                                        // 更新图表
+                                        for (let m = 0; m < this.monitorChart.length; m++) {
+                                            if (message[j].id == this.monitorChart[m].data.id) {
+                                                isComplete = true;
+                                                let data = [
+                                                    {
+                                                        value: message[j].value,
+                                                        name: this.monitorChart[m].data.name,
+                                                    },
+                                                ];
+                                                this.monitorChart[m].chart.setOption({
+                                                    series: [
+                                                        {
+                                                            data: data,
+                                                        },
+                                                    ],
+                                                });
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                                if (isComplete) {
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.phone-main {
+    float: right;
+}
+
+.phone {
+    height: 729px;
+    width: 370px;
+    background-image: url('../../../assets/images/phone.png');
+    background-size: cover;
+}
+
+.phone-container {
+    height: 618px;
+    width: 343px;
+    position: relative;
+    top: 46px;
+    left: 12px;
+    background-color: #fff;
+
+    .phone-title {
+        line-height: 40px;
+        color: #fff;
+        background-color: #007aff;
+        text-align: center;
+    }
+
+    .messageContent {
+        height: 440px;
+        overflow-y: scroll;
+        word-wrap: break-word;
+        padding: 6px 0;
+        color: #fff;
+    }
+
+    .messageBottom {
+        height: 150px;
+        position: absolute;
+        bottom: 0;
+        width: 100%;
+        background-color: #eef3f7;
+        padding: 5px;
+        border-top: 1px solid #d2dae1;
+    }
+
+    .messageReceive {
+        float: left;
+        background-color: #409eff;
+        border-radius: 6px;
+        padding: 10px;
+        width: 70%;
+        font-size: 12px;
+        margin-bottom: 15px;
+        border-style: dotted;
+    }
+
+    .messageSend {
+        float: right;
+        background-color: #13ce66;
+        border-radius: 10px;
+        padding: 10px;
+        width: 70%;
+        font-size: 12px;
+        margin-bottom: 15px;
+        border-right-style: double;
+    }
+}
+
+.log-content {
+    padding: 2px;
+    height: calc(100% - 44px);
+    overflow: auto;
+
+    ul {
+        padding: 0;
+        margin: 4px 0;
+        list-style: none;
+    }
+
+    a {
+        width: 100%;
+        color: #333;
+        //border: 1px solid #fff;
+        flex-wrap: wrap;
+        padding: 5px 5px 1px 5px;
+        text-decoration: none;
+        font-size: 12px;
+
+        .time {
+            font-size: 10px;
+            color: gray;
+        }
+
+        div {
+            color: #1b93e0;
+
+            .lable-s1 {
+                color: gray;
+            }
+        }
+
+        .fail {
+            color: #f56c6c;
+        }
+
+        .wait {
+            color: #909399;
+        }
+    }
+}
+
+.H100 {
+    //overflow: hidden;
+    margin-left: 10px;
+}
+
+.row-list {
+    height: calc(100% - 20px);
+    height: 700px;
+    overflow: auto;
+    margin: -20px -20px -20px -30px !important;
+    font-size: 12px;
+    line-height: 20px;
+}
+
+.running-status {
+    .select {
+        margin-bottom: 15px;
+    }
+
+    .edit-class {
+        margin-top: 10px;
+    }
+}
+</style>

+ 1073 - 0
src/views/pms/video_center/device/realTime-status.vue

@@ -0,0 +1,1073 @@
+<template>
+    <div class="running-status H100">
+        <div>
+            <el-tabs type="border-card" v-model="runtimeName" @tab-click="runtimeClick" style="flex: 1; height: 800px; margin-bottom: 5px">
+                <el-tab-pane label="从机实时状态" name="slave">
+                    <!-- tabs多时,可以自己新建组件,免重复代码   -->
+                    <el-tabs type="card" v-model="thingsType" @tab-click="handleClick" style="margin-top: -1px; height: 800px; margin-bottom: 5px">
+                        <el-tab-pane label="属性上报" name="prop">
+                            <el-main v-loading="loading" style="position: relative" class="H100">
+                                <el-row :gutter="20" class="row-list">
+                                    <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in runningData" :key="index" style="margin-bottom: 10px">
+                                        <el-card style="padding: 0px; height: 90px">
+                                            <div class="head">
+                                                <div class="title">{{ item.name }}({{ item.id }})</div>
+                                                <div class="name">
+                                                    <span style="color: #0f73ee">{{ item.value }}</span>
+                                                    <span v-if="item.datatype.unit">{{ item.datatype.unit || item.datatype.unitName }}</span>
+                                                </div>
+                                            </div>
+                                            <div>时间:{{ item.ts }}</div>
+                                        </el-card>
+                                    </el-col>
+                                </el-row>
+                            </el-main>
+                        </el-tab-pane>
+                        <el-tab-pane label="服务下发" name="function">
+                            <el-main v-loading="loading" style="position: relative" class="H100">
+                                <el-row :gutter="20" class="row-list">
+                                    <el-col ::xs="17" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in functionData" :key="index" style="margin-bottom: 10px">
+                                        <el-card shadow="hover" class="elcard" style="height: 90px">
+                                            <div class="head">
+                                                <div class="title">
+                                                    {{ item.name }}
+                                                </div>
+                                                <div class="name">
+                                                    <span style="color: #0f73ee">{{ item.value }}</span>
+                                                    <span v-if="item.datatype.unit">{{ item.datatype.unit }}</span>
+                                                    <el-button type="primary" plain icon="el-icon-s-promotion" size="mini" style="float: right; margin-right: -5px; padding: 3px 5px" @click.stop="editFunc(item)">
+                                                        发送
+                                                    </el-button>
+                                                </div>
+                                            </div>
+                                            <div>
+                                                <span>时间:{{ item.ts }}</span>
+                                            </div>
+                                        </el-card>
+                                    </el-col>
+                                    <el-col ::xs="7" :sm="12" :md="12" :lg="8" :xl="6" class="phone-main">
+                                        <div class="phone">
+                                            <div class="phone-container">
+                                                <div class="phone-title">设 备 指 令</div>
+                                                <div class="log-content" ref="logContent">
+                                                    <el-scrollbar style="height: 100%" ref="scrollContent">
+                                                        <ul v-for="(item, index) in logList" :key="index">
+                                                            <li>
+                                                                <a href="#" style="float: left; text-align: left">
+                                                                    <div class="time">{{ item.createTime }}</div>
+                                                                    <div class="spa">
+                                                                        <span class="lable-s1">服务下发:</span>
+                                                                        {{ item.modelName }}: {{ item.showValue }}
+                                                                    </div>
+                                                                </a>
+                                                                <a href="#" style="float: right; text-align: right">
+                                                                    <div class="time">{{ item.replyTime }}</div>
+                                                                    <div :class="{ fail: item.resultCode == 201, wait: item.resultCode == 203 }">
+                                                                        <span class="lable-s1">设备应答:</span>
+                                                                        {{ item.resultMsg }}
+                                                                    </div>
+                                                                </a>
+                                                            </li>
+                                                        </ul>
+                                                    </el-scrollbar>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </el-col>
+                                </el-row>
+                                <el-empty description="暂无数据" v-show="runningData.length == 0"></el-empty>
+                            </el-main>
+                        </el-tab-pane>
+                        <el-tab-pane label="遥控指令" name="remote">
+                            <el-main v-loading="loading" style="position: relative" class="H100">
+                                <el-row :gutter="20" class="row-list">
+                                    <el-col ::xs="17" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in controlData" :key="index" style="margin-bottom: 10px">
+                                        <el-card shadow="hover" class="elcard" style="height: 90px">
+                                            <div class="head">
+                                                <div class="title">
+                                                    {{ item.name }}
+                                                </div>
+                                                <div class="name">
+                                                    <span style="color: #0f73ee">{{ item.value }}</span>
+                                                    <span v-if="item.datatype.unit">{{ item.datatype.unit }}</span>
+                                                    <el-button type="primary" plain icon="el-icon-s-promotion" size="mini" style="float: right; margin-right: -5px; padding: 3px 5px" @click.stop="editFunc(item)">
+                                                        发送
+                                                    </el-button>
+                                                </div>
+                                            </div>
+                                            <div>
+                                                <span>时间:{{ item.ts }}</span>
+                                            </div>
+                                        </el-card>
+                                    </el-col>
+                                    <el-col ::xs="7" :sm="12" :md="12" :lg="8" :xl="6" class="phone-main">
+                                        <div class="phone">
+                                            <div class="phone-container">
+                                                <div class="phone-title">设 备 指 令</div>
+                                                <div class="log-content" ref="logContent">
+                                                    <el-scrollbar style="height: 100%" ref="scrollContent">
+                                                        <ul v-for="(item, index) in logList" :key="index">
+                                                            <li>
+                                                                <a href="#" style="float: left; text-align: left">
+                                                                    <div class="time">{{ item.createTime }}</div>
+                                                                    <div class="spa">
+                                                                        <span class="lable-s1">服务下发:</span>
+                                                                        {{ item.modelName }} :{{ item.showValue }}
+                                                                    </div>
+                                                                </a>
+                                                                <a href="#" style="float: right; text-align: right">
+                                                                    <div class="time">{{ item.replyTime }}</div>
+                                                                    <div :class="{ fail: item.resultCode == 201, wait: item.resultCode == 203 }">
+                                                                        <span class="lable-s1">设备应答:</span>
+                                                                        {{ item.resultMsg }}
+                                                                    </div>
+                                                                </a>
+                                                            </li>
+                                                        </ul>
+                                                    </el-scrollbar>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </el-col>
+                                </el-row>
+                                <el-empty description="暂无数据" v-show="runningData.length == 0"></el-empty>
+                            </el-main>
+                        </el-tab-pane>
+
+                        <el-tab-pane disabled name="slave">
+                            <span slot="label" style="margin-left: 50px">
+                                <span ref="statusTitle" style="color: #409eff; margin-right: 30px">{{ title }}</span>
+                                <el-select v-model="params.slaveId" placeholder="请选择设备从机" @change="selectSlave" size="mini">
+                                    <el-option v-for="slave in slaveList" :key="slave.slaveId" :label="`${slave.deviceName}   (${slave.slaveId})`" :value="slave.slaveId"></el-option>
+                                </el-select>
+                            </span>
+                        </el-tab-pane>
+                    </el-tabs>
+                </el-tab-pane>
+                <el-tab-pane label="网关实时状态" name="gateway">
+                    <el-row :gutter="120">
+                        <el-col :xs="24" :sm="24" :md="24" :lg="14" :xl="10" style="margin-bottom: 50px">
+                            <el-descriptions :column="1" border style="margin-bottom: 50px">
+                                <!-- 设备模式-->
+                                <el-descriptions-item :labelStyle="statusColor">
+                                    <template slot="label">
+                                        <i class="el-icon-menu"></i>
+                                        设备模式
+                                    </template>
+                                    <el-link :underline="false" style="line-height: 28px; font-size: 16px; padding-right: 10px">{{ title }}</el-link>
+                                </el-descriptions-item>
+
+                                <!-- 设备物模型-->
+                                <el-descriptions-item v-for="(item, index) in this.deviceInfo.thingsModels" :key="index" :labelStyle="statusColor">
+                                    <template slot="label">
+                                        <i class="el-icon-open"></i>
+                                        {{ item.name }}
+                                    </template>
+                                    <div v-if="item.datatype.type == 'bool'">
+                                        <el-switch
+                                            v-model="item.shadow"
+                                            @change="mqttPublish(deviceInfo, item)"
+                                            active-text=""
+                                            inactive-text=""
+                                            active-value="1"
+                                            inactive-value="0"
+                                            style="min-width: 100px"
+                                            :disabled="shadowUnEnable || item.isReadonly == 1"
+                                        />
+                                    </div>
+                                    <div v-if="item.datatype.type == 'enum'">
+                                        <div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
+                                            <el-button
+                                                style="margin: 5px"
+                                                size="mini"
+                                                @click="enumButtonClick(deviceInfo, item, subItem.value)"
+                                                v-for="subItem in item.datatype.enumList"
+                                                :key="subItem.value"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                            >
+                                                {{ subItem.text }}
+                                            </el-button>
+                                        </div>
+                                        <el-select v-else v-model="item.shadow" placeholder="请选择" @change="mqttPublish(deviceInfo, item)" :disabled="shadowUnEnable || item.isReadonly == 1">
+                                            <el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                        </el-select>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'string'">
+                                        <el-input v-model="item.shadow" :placeholder="'请输入字符串 ' + (item.datatype.unit ? ',单位:' + item.datatype.unit : '')" :disabled="shadowUnEnable || item.isReadonly == 1">
+                                            <el-button
+                                                slot="append"
+                                                icon="el-icon-s-promotion"
+                                                @click="mqttPublish(deviceInfo, item)"
+                                                style="font-size: 20px"
+                                                title="指令发送"
+                                                v-if="!shadowUnEnable && item.isReadonly == 0"
+                                            ></el-button>
+                                        </el-input>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'decimal'">
+                                        <div style="width: 80%; float: left">
+                                            <el-slider
+                                                v-model="item.value"
+                                                :min="item.datatype.min"
+                                                :max="item.datatype.max"
+                                                :step="item.datatype.step"
+                                                :format-tooltip="(x) => x + ' ' + item.datatype.unit"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                            ></el-slider>
+                                        </div>
+                                        <div style="width: 20%; float: left">
+                                            <el-button
+                                                icon="el-icon-s-promotion"
+                                                type="info"
+                                                @click="mqttPublish(deviceInfo, item)"
+                                                style="font-size: 16px; padding: 1px 8px; margin: 2px 0 0 5px; border-radius: 3px"
+                                                title="指令发送"
+                                                v-if="!shadowUnEnable && item.isReadonly == 0"
+                                            ></el-button>
+                                        </div>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'integer'">
+                                        <div style="width: 80%; float: left">
+                                            <el-slider
+                                                v-model="item.value"
+                                                :min="item.datatype.min"
+                                                :max="item.datatype.max"
+                                                :step="item.datatype.step"
+                                                :format-tooltip="(x) => x + ' ' + item.datatype.unit"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                            ></el-slider>
+                                        </div>
+                                        <div style="width: 20%; float: left">
+                                            <el-button
+                                                icon="el-icon-s-promotion"
+                                                type="info"
+                                                @click="mqttPublish(deviceInfo, item)"
+                                                style="font-size: 16px; padding: 1px 8px; margin: 4px 0 0 10px; border-radius: 3px"
+                                                title="指令发送"
+                                                v-if="!shadowUnEnable && item.isReadonly == 0"
+                                            ></el-button>
+                                        </div>
+                                    </div>
+                                </el-descriptions-item>
+                            </el-descriptions>
+
+                            <!---设备状态(影子模式,value值不会更新)-->
+                            <el-descriptions :column="1" border size="mini" v-if="deviceInfo.isShadow == 1 && deviceInfo.status != 3">
+                                <template slot="title">
+                                    <span style="font-size: 14px; color: #606266">设备离线时状态</span>
+                                </template>
+
+                                <!-- 设备物模型-->
+                                <el-descriptions-item v-for="(item, index) in deviceInfo.thingsModels" :key="index">
+                                    <template slot="label">
+                                        <i class="el-icon-open"></i>
+                                        {{ item.name }}
+                                    </template>
+                                    <div v-if="item.datatype.type == 'bool'">
+                                        <el-switch v-model="item.value" @change="mqttPublish(deviceInfo, item)" active-text="" inactive-text="" active-value="1" inactive-value="0" style="min-width: 100px" disabled />
+                                    </div>
+                                    <div v-if="item.datatype.type == 'enum'">
+                                        <div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
+                                            <el-button style="margin: 5px" size="mini" disabled v-for="subItem in item.datatype.enumList" :key="subItem.value">{{ subItem.text }}</el-button>
+                                        </div>
+                                        <el-select v-else v-model="item.value" placeholder="请选择" @change="mqttPublish(deviceInfo, item)" disabled size="mini">
+                                            <el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                        </el-select>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'string'">
+                                        <el-input v-model="item.value" placeholder="请输入字符串" disabled size="mini"></el-input>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'decimal'">
+                                        <el-input v-model="item.value" type="number" placeholder="请输入小数 " disabled size="mini"></el-input>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'integer'">
+                                        <el-input v-model="item.value" type="integer" placeholder="请输入整数 " disabled size="mini"></el-input>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'object'">
+                                        <el-descriptions :column="1" size="mini" border>
+                                            <el-descriptions-item v-for="(param, index) in item.datatype.params" :key="index" :label="param.name">
+                                                <div v-if="param.datatype.type == 'bool'">
+                                                    <el-switch
+                                                        v-model="param.value"
+                                                        size="mini"
+                                                        @change="mqttPublish(deviceInfo, param)"
+                                                        active-text=""
+                                                        inactive-text=""
+                                                        active-value="1"
+                                                        inactive-value="0"
+                                                        style="min-width: 100px"
+                                                        disabled
+                                                    />
+                                                </div>
+                                                <div v-if="param.datatype.type == 'enum'">
+                                                    <el-select v-model="param.value" placeholder="请选择" @change="mqttPublish(deviceInfo, param)" disabled size="mini">
+                                                        <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                                    </el-select>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'string'">
+                                                    <el-input v-model="param.value" placeholder="请输入字符串" disabled size="mini"></el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'decimal'">
+                                                    <el-input v-model="param.value" type="number" placeholder="请输入小数 " disabled size="mini"></el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'integer'">
+                                                    <el-input v-model="param.value" type="integer" placeholder="请输入整数 " disabled size="mini"></el-input>
+                                                </div>
+                                            </el-descriptions-item>
+                                        </el-descriptions>
+                                    </div>
+                                    <div v-if="item.datatype.type == 'array'">
+                                        <el-descriptions :column="1" size="mini" border v-if="item.datatype.arrayType != 'object'">
+                                            <el-descriptions-item v-for="(model, index) in item.datatype.arrayModel" :key="index" :label="item.name + (index + 1)">
+                                                <div v-if="item.datatype.arrayType == 'string'">
+                                                    <el-input v-model="model.value" placeholder="请输入字符串" size="mini" disabled></el-input>
+                                                </div>
+                                                <div v-if="item.datatype.arrayType == 'decimal'">
+                                                    <el-input v-model="model.value" type="number" placeholder="请输入小数 " size="mini" disabled></el-input>
+                                                </div>
+                                                <div v-if="item.datatype.arrayType == 'integer'">
+                                                    <el-input v-model="model.value" type="integer" placeholder="请输入整数 " size="mini" disabled></el-input>
+                                                </div>
+                                            </el-descriptions-item>
+                                        </el-descriptions>
+                                        <el-collapse v-if="item.datatype.arrayType == 'object'">
+                                            <el-collapse-item v-for="(arrayParam, index) in item.datatype.arrayParams" :key="index">
+                                                <template slot="title">
+                                                    <span style="color: #666">
+                                                        <i class="el-icon-tickets"></i>
+                                                        {{ item.name + (index + 1) }}
+                                                    </span>
+                                                </template>
+                                                <el-descriptions :column="1" size="mini" border>
+                                                    <el-descriptions-item v-for="(param, index) in arrayParam" :key="index" :label="param.name">
+                                                        <div v-if="param.datatype.type == 'bool'">
+                                                            <el-switch
+                                                                v-model="param.value"
+                                                                @change="mqttPublish(deviceInfo, param)"
+                                                                active-text=""
+                                                                inactive-text=""
+                                                                active-value="1"
+                                                                inactive-value="0"
+                                                                style="min-width: 100px"
+                                                                disabled
+                                                            />
+                                                        </div>
+                                                        <div v-if="param.datatype.type == 'enum'">
+                                                            <el-select v-model="param.value" placeholder="请选择" @change="mqttPublish(deviceInfo, param)" disabled size="mini">
+                                                                <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                                            </el-select>
+                                                        </div>
+                                                        <div v-if="param.datatype.type == 'string'">
+                                                            <el-input v-model="param.value" placeholder="请输入字符串" disabled size="mini"></el-input>
+                                                        </div>
+                                                        <div v-if="param.datatype.type == 'decimal'">
+                                                            <el-input v-model="param.value" type="number" placeholder="请输入小数 " disabled size="mini"></el-input>
+                                                        </div>
+                                                        <div v-if="param.datatype.type == 'integer'">
+                                                            <el-input v-model="param.value" type="integer" placeholder="请输入整数 " disabled size="mini"></el-input>
+                                                        </div>
+                                                    </el-descriptions-item>
+                                                </el-descriptions>
+                                            </el-collapse-item>
+                                        </el-collapse>
+                                    </div>
+                                </el-descriptions-item>
+                            </el-descriptions>
+                        </el-col>
+
+                        <el-col :xs="24" :sm="24" :md="24" :lg="10" :xl="14">
+                            <!-- 设备监测图表-->
+                            <el-row :gutter="20" style="background-color: #f5f7fa; padding: 20px 10px 20px 10px; border-radius: 15px; margin-right: 5px" v-if="deviceInfo.chartList && deviceInfo.chartList.length > 0">
+                                <el-col :xs="24" :sm="12" :md="12" :lg="24" :xl="8" v-for="(item, index) in deviceInfo.chartList" :key="index">
+                                    <el-card shadow="hover" style="border-radius: 30px; margin-bottom: 20px">
+                                        <div ref="map" style="height: 230px; width: 185px; margin: 0 auto"></div>
+                                    </el-card>
+                                </el-col>
+                            </el-row>
+                        </el-col>
+                    </el-row>
+                </el-tab-pane>
+            </el-tabs>
+        </div>
+
+        <el-dialog title="服务调用" :visible.sync="dialogValue" label-width="200px">
+            <el-form v-model="from" size="mini" style="height: 100%; padding: 0 20px">
+                <el-form-item :label="from.name" label-width="180px">
+                    <el-input
+                        v-model="from.shadow"
+                        type="number"
+                        @input="justicNumber()"
+                        v-if="from.datatype.type == 'integer' || from.datatype.type == 'decimal' || from.datatype.type == 'string'"
+                        style="width: 50%"
+                    ></el-input>
+                    <el-select v-if="from.datatype.type == 'enum'" v-model="from.shadow" @change="changeSelect()">
+                        <el-option v-for="option in from.datatype.enumList" :key="option.value" :label="option.text" :value="option.value"></el-option>
+                    </el-select>
+                    <el-switch v-if="from.datatype.type === 'bool'" v-model="from.shadow" active-value="1" inactive-value="0" inline-prompt />
+                    <span v-if="(from.datatype.type == 'integer' || from.datatype.type == 'decimal') && from.datatype.type.unit && from.datatype.type.unit != 'un' && from.datatype.type.unit != '/'">
+                        ({{ from.unit }})
+                    </span>
+                    <div v-if="from.datatype.type == 'integer' || from.datatype.type == 'decimal'" class="range">
+                        (数据范围:{{ from.datatype.max == 'null' ? (from.datatype.type == 'bool' ? 0 : '') : from.datatype.min }} ~
+                        {{ from.datatype.max == 'null' ? (from.datatype.type == 'bool' ? 1 : '') : from.datatype.max }})
+                    </div>
+                </el-form-item>
+            </el-form>
+            <span slot="footer" class="dialog-footer">
+                <el-button @click="dialogValue = false">取消</el-button>
+                <el-button type="primary" @click="sendService" :loading="btnLoading" :disabled="!canSend">确认</el-button>
+            </span>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { serviceInvoke, funcLog } from '@/api/iot/runstatus';
+// import { formatDate2 } from '@/utils/index';
+// import { listByPid } from '@/api/iot/salve';
+import { getDeviceRunningStatus } from '@/api/iot/device';
+// import { listSimulateLog } from '@/api/iot/simulate';
+
+const INTEGER = 'integer';
+const DECIMAL = 'decimal';
+const BOOL = 'bool';
+const ENUM = 'enum';
+
+export default {
+    name: 'realTime-status',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    data() {
+        return {
+            // 未启用设备影子
+            shadowUnEnable: false,
+            // 控制项标题背景
+            statusColor: {
+                background: '#67C23A',
+                color: '#fff',
+                minWidth: '100px',
+            },
+            /**设备模拟消息列表**/
+            messageList: [],
+            /**设备模拟发送消息表单**/
+            simulateForm: {},
+            deviceInfo: {}, // 设备信息
+            dialogValue: false, // 查看详情弹框
+            gridData: [], // 事件数据
+            groupId: 1,
+            treeData: [],
+            runningData: [], // 实时状态列表
+            gatewayData: [],
+            functionData: [],
+            controlData: [],
+            loading: false,
+            debounceGetRuntime: '',
+            serialNumber: '',
+            isControlled: 2,
+            slaveId: 1,
+            params: {
+                serialNumber: undefined,
+                type: 1,
+            },
+            slaveList: [],
+            queryParams: {},
+            thingsType: 'prop',
+            runtimeName: 'slave',
+            opationList: [], // 指令数值数组
+            funVal: {},
+            canSend: false, //是否可以下发,主要判断数值在不在范围
+            functionName: {},
+            btnLoading: false,
+            logList: [],
+            showValue: '',
+            from: {
+                datatype: {
+                    type: '',
+                },
+            },
+            title: '在线模式',
+        };
+    },
+
+    created() {},
+
+    watch: {
+        device: function (newVal) {
+            if (newVal && newVal.serialNumber) {
+                this.params.serialNumber = newVal.serialNumber;
+                this.serialNumber = newVal.serialNumber;
+                this.params.productId = newVal.productId;
+                this.params.slaveId = newVal.slaveId;
+                this.params.deviceId = newVal.deviceId;
+                this.deviceInfo = newVal;
+                this.updateDeviceStatus(this.deviceInfo);
+                this.slaveList = newVal.subDeviceList;
+                this.getSlaveList(this.deviceInfo);
+                this.$busEvent.$on('updateData', (params) => {
+                    if (params.data && params.data.length > 0) {
+                        this.getDeviceFuncLog();
+                        // params.data[0].ts = params.data[0].remark;
+                    }
+                    this.updateData(params);
+                });
+                this.$busEvent.$on('updateLog', (params) => {
+                    this.getDeviceFuncLog();
+                });
+            }
+        },
+    },
+    methods: {
+        /** qos改变事件 **/
+        qosChange(data) {},
+        /** 载体改变事件 **/
+        payloadTypeChange(data) {},
+        /** 获取当前时间 **/
+        getTime() {
+            let date = new Date();
+            let y = date.getFullYear();
+            let m = date.getMonth() + 1;
+            let d = date.getDate();
+            let H = date.getHours();
+            let mm = date.getMinutes();
+            let s = date.getSeconds();
+            m = m < 10 ? '0' + m : m;
+            d = d < 10 ? '0' + d : d;
+            H = H < 10 ? '0' + H : H;
+            return y + '-' + m + '-' + d + ' ' + H + ':' + mm + ':' + s;
+        },
+
+        /*获取运行状态*/
+        getRuntimeStatus() {
+            getDeviceRunningStatus(this.params).then((response) => {
+                this.runningData.splice(0, this.runningData.length);
+                this.runningData.push(...response.data.thingsModels);
+                this.runningData.forEach((item) => {
+                    if (item.datatype.type == 'enum') {
+                        item.datatype.enumList.forEach((val) => {
+                            if (val.value == item.value) {
+                                item.value = val.text;
+                            }
+                        });
+                    } else if (item.datatype.type == 'bool') {
+                        item.value = item.value == 0 ? item.falseText : item.trueText;
+                    }
+                });
+                //筛选读写物模型
+                this.functionData = this.runningData.filter((item) => item.isReadonly == 0);
+            });
+        },
+
+        getGateway() {
+            getDeviceRunningStatus(this.params).then((response) => {
+                const thingsModels = response.data.thingsModels;
+                thingsModels.forEach((item) => {
+                    if (item.datatype.type == 'enum') {
+                        item.datatype.enumList.forEach((val) => {
+                            if (val.value == item.value) {
+                                item.value = val.text;
+                            }
+                        });
+                    } else if (item.datatype.type == 'bool') {
+                        item.value = item.value == 0 ? item.falseText : item.trueText;
+                    } else if (item.datatype.type == 'integer' || item.datatype.type == 'decimal') {
+                        // 数字类型设置默认值并转换未数值
+                        if (item.value == '') {
+                            item.value = Number(item.datatype.min);
+                        } else {
+                            item.value = Number(item.value);
+                        }
+                    }
+                });
+                this.deviceInfo.thingsModels.splice(0, this.deviceInfo.thingsModels.length);
+                this.deviceInfo.thingsModels.push(...thingsModels);
+            });
+        },
+
+        /**根据产品id获取从机列表*/
+        getSlaveList() {
+            this.getRuntimeStatus();
+            this.getDeviceFuncLog();
+        },
+        /*选择从机*/
+        selectSlave() {
+            this.params.serialNumber = this.serialNumber + '_' + this.params.slaveId;
+            this.getRuntimeStatus();
+        },
+        /*tabs切换*/
+        handleClick() {
+            if (this.thingsType === 'prop') {
+                this.params.type = 1;
+                // this.runningData = this.data && this.data.filter((item) => item.isReadonly == 1) || [];
+            } else if (this.thingsType === 'function') {
+                this.isControlled = 2;
+                this.params.type = 2;
+                //筛选读写物模型
+                // this.functionData = this.runningData.filter((item) => item.isReadonly == 0);
+            } else {
+                this.params.type = 3;
+                this.isControlled = 1;
+                // this.controlData = this.data && this.data.filter((item) => item.isMonitor == 1) || [];
+            }
+        },
+        //切换实时状态
+        runtimeClick() {
+            if (this.runtimeName === 'gateway') {
+                this.params.serialNumber = this.serialNumber;
+                this.slaveId = this.params.slaveId;
+                this.params.slaveId = undefined;
+                this.getGateway();
+            } else {
+                this.params.serialNumber = this.serialNumber + '_' + this.slaveId;
+                this.params.slaveId = this.slaveId;
+                this.getRuntimeStatus();
+            }
+        },
+        // 更新参数值
+        updateParam(data) {},
+        //指令下发
+        editFunc(item) {
+            this.dialogValue = true;
+            this.canSend = true;
+            this.funVal = {};
+            this.getValueName(item);
+            this.from = item;
+        },
+
+        /** 更新设备状态 */
+        updateDeviceStatus(device) {
+            if (device.status == 3) {
+                this.statusColor.background = '#12d09f';
+                this.title = '在线模式';
+            } else {
+                if (device.isShadow == 1) {
+                    this.statusColor.background = '#409EFF';
+                    this.title = '影子模式';
+                } else {
+                    this.statusColor.background = '#909399';
+                    this.title = '离线模式';
+                    this.shadowUnEnable = true;
+                }
+            }
+            this.$emit('statusEvent', this.deviceInfo.status);
+        },
+
+        // 解析值
+        getValueName(item) {
+            this.funVal[item.id] = item.value;
+        },
+        // 发送指令
+        sendService() {
+            console.log('下发指令', this.from.shadow);
+            try {
+                this.funVal[this.from.id] = this.from.shadow;
+                const data = {
+                    serialNumber: this.serialNumber,
+                    productId: this.params.productId,
+                    remoteCommand: this.funVal,
+                    identifier: this.from.id,
+                    slaveId: this.params.slaveId,
+                    modelName: this.from.name,
+                    isShadow: this.device.status != 3,
+                    type: this.from.type,
+                    isControlled: this.isControlled,
+                };
+                serviceInvoke(data).then((response) => {
+                    if (response.code == 200) {
+                        this.$message({
+                            type: 'success',
+                            message: '服务调用成功!',
+                        });
+                        this.getDeviceFuncLog();
+                    }
+                });
+            } finally {
+                this.dialogValue = false;
+            }
+        },
+
+        //发送指令
+        mqttPublish(device, model) {
+            const command = {};
+            command[model.id] = model.shadow;
+            const data = {
+                serialNumber: device.serialNumber,
+                productId: device.productId,
+                remoteCommand: command,
+                identifier: model.id,
+                modelName: model.name,
+                isShadow: device.status != 3,
+                type: model.type,
+            };
+            serviceInvoke(data).then((response) => {
+                if (response.code === 200) {
+                    this.$message({
+                        type: 'success',
+                        message: '服务调用成功!',
+                    });
+                }
+            });
+        },
+
+        getShowValue(value) {
+            switch (this.from.datatype.type) {
+                case ENUM:
+                    const list = this.from.datatype.enumList;
+                    list.forEach((m) => {
+                        if (m.value === value) {
+                            this.showValue = m.text;
+                        }
+                    });
+                    break;
+                case INTEGER:
+                case DECIMAL:
+                    this.showValue = value;
+                case BOOL:
+                    this.showValue = value == 1 ? this.from.datatype.trueText : this.from.datatype.falseText;
+                    break;
+            }
+        },
+
+        //下拉选择修改触发
+        changeSelect() {
+            this.$forceUpdate();
+        },
+
+        //判断输入是否超过范围
+        justicNumber() {
+            this.canSend = true;
+            if (this.from.datatype.max < this.funVal[this.from.identity] || this.from.datatype.min > this.funVal[this.from.identity]) {
+                this.canSend = false;
+                return true;
+            }
+            this.$forceUpdate();
+        },
+
+        //  获取设备服务下发日志
+        getDeviceFuncLog() {
+            const params = {
+                serialNumber: this.serialNumber,
+            };
+            console.log('params --', params);
+            funcLog(params).then((response) => {
+                this.logList = response.rows;
+            });
+        },
+        updateData(msg) {
+            if (msg.data && msg.data.message) {
+                msg.data.message.forEach((d) => {
+                    this.runningData.forEach((old, index) => {
+                        // if (d.slaveId === old.slaveId && d.id == old.id) {
+                        if (d.id == old.id) {
+                            const template = this.runningData[index];
+                            template.ts = d.ts;
+                            template.value = d.value;
+                            if (old.datatype.type == 'enum') {
+                                old.datatype.enumList.forEach((val) => {
+                                    if (val.value == template.value) {
+                                        template.value = val.text;
+                                    }
+                                });
+                            } else if (old.datatype.type == 'bool') {
+                                template.value = template.value == 0 ? old.datatype.falseText : old.datatype.trueText;
+                            } else if (old.datatype.type == 'integer') {
+                                template.value = Number(template.value);
+                            }
+                            this.$set(this.runningData, index, template);
+                        }
+                    });
+
+                });
+                this.handleThingsModelsChange(msg);
+
+            }
+        },
+        /* Mqtt回调处理 */
+        handleThingsModelsChange(msg) {
+            const message = msg.data.message
+            const deviceNum = msg.serialNumber
+            // 更新列表中设备的属性
+            if (this.deviceInfo.serialNumber == deviceNum) {
+                for (let j = 0; j < message.length; j++) {
+                    let isComplete = false;
+                    // 设备状态
+                    for (let k = 0; k < this.deviceInfo.thingsModels.length && !isComplete; k++) {
+                        if (this.deviceInfo.thingsModels[k].id == message[j].id) {
+                            // 普通类型(小数/整数/字符串/布尔/枚举)
+                            if (this.deviceInfo.thingsModels[k].datatype.type == 'decimal' || this.deviceInfo.thingsModels[k].datatype.type == 'integer') {
+                                this.deviceInfo.thingsModels[k].value = Number(message[j].value);
+                                this.deviceInfo.thingsModels[k].shadow = message[j].value;
+                            } else {
+                                this.deviceInfo.thingsModels[k].value = message[j].value;
+                                this.deviceInfo.thingsModels[k].shadow = message[j].value;
+                            }
+                            isComplete = true;
+                            break;
+                        } else if (this.deviceInfo.thingsModels[k].datatype.type == 'object') {
+                            // 对象类型
+                            for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params.length; n++) {
+                                if (this.deviceInfo.thingsModels[k].datatype.params[n].id == message[j].id) {
+                                    this.deviceInfo.thingsModels[k].datatype.params[n].shadow = message[j].value;
+                                    isComplete = true;
+                                    break;
+                                }
+                            }
+                        } else if (this.deviceInfo.thingsModels[k].datatype.type == 'array') {
+                            // 数组类型
+                            if (this.deviceInfo.thingsModels[k].datatype.arrayType == 'object') {
+                                // 1.对象类型数组,id为数组中一个元素,例如:array_01_gateway_temperature
+                                if (String(message[j].id).indexOf('array_') == 0) {
+                                    for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
+                                        for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
+                                            if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == message[j].id) {
+                                                this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = message[j].value;
+                                                isComplete = true;
+                                                break;
+                                            }
+                                        }
+                                        if (isComplete) {
+                                            break;
+                                        }
+                                    }
+                                } else {
+                                    // 2.对象类型数组,例如:gateway_temperature,消息ID添加前缀后匹配
+                                    for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
+                                        for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
+                                            let index = n > 9 ? String(n) : '0' + k;
+                                            let prefix = 'array_' + index + '_';
+                                            if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == prefix + message[j].id) {
+                                                this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = message[j].value;
+                                                isComplete = true;
+                                            }
+                                        }
+                                        if (isComplete) {
+                                            break;
+                                        }
+                                    }
+                                }
+                            } else {
+                                // 整数、小数和字符串类型数组
+                                for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel.length; n++) {
+                                    if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id == message[j].id) {
+                                        this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow = message[j].value;
+                                        isComplete = true;
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    if (this.deviceInfo.chartList && this.deviceInfo.chartList.length > 0) {
+                        // 图表数据
+                        for (let k = 0; k < this.deviceInfo.chartList.length; k++) {
+                            if (this.deviceInfo.chartList[k].id.indexOf('array_') == 0) {
+                                // 数组类型匹配,例如:array_00_gateway_temperature
+                                if (this.deviceInfo.chartList[k].id == message[j].id) {
+                                    // let shadows = message[j].value.split(",");
+                                    this.deviceInfo.chartList[k].shadow = message[j].value;
+                                    // 更新图表
+                                    for (let m = 0; m < this.monitorChart.length; m++) {
+                                        if (message[j].id == this.monitorChart[m].data.id) {
+                                            let data = [
+                                                {
+                                                    value: message[j].value,
+                                                    name: this.monitorChart[m].data.name,
+                                                },
+                                            ];
+                                            this.monitorChart[m].chart.setOption({
+                                                series: [
+                                                    {
+                                                        data: data,
+                                                    },
+                                                ],
+                                            });
+                                            break;
+                                        }
+                                    }
+                                }
+                            } else {
+                                // 普通类型匹配
+                                if (this.deviceInfo.chartList[k].id == message[j].id) {
+                                    this.deviceInfo.chartList[k].shadow = message[j].value;
+                                    // 更新图表
+                                    for (let m = 0; m < this.monitorChart.length; m++) {
+                                        if (message[j].id == this.monitorChart[m].data.id) {
+                                            isComplete = true;
+                                            let data = [
+                                                {
+                                                    value: message[j].value,
+                                                    name: this.monitorChart[m].data.name,
+                                                },
+                                            ];
+                                            this.monitorChart[m].chart.setOption({
+                                                series: [
+                                                    {
+                                                        data: data,
+                                                    },
+                                                ],
+                                            });
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                            if (isComplete) {
+                                break;
+                            }
+                        }
+                    }
+                }
+            }
+            this.$forceUpdate();
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.phone-main {
+    float: right;
+}
+
+.phone {
+    height: 729px;
+    width: 370px;
+    background-image: url('../../../assets/images/phone.png');
+    background-size: cover;
+}
+
+.phone-container {
+    height: 618px;
+    width: 343px;
+    position: relative;
+    top: 46px;
+    left: 12px;
+    background-color: #fff;
+
+    .phone-title {
+        line-height: 40px;
+        color: #fff;
+        background-color: #007aff;
+        text-align: center;
+    }
+
+    .messageContent {
+        height: 440px;
+        overflow-y: scroll;
+        word-wrap: break-word;
+        padding: 6px 0;
+        color: #fff;
+    }
+
+    .messageBottom {
+        height: 150px;
+        position: absolute;
+        bottom: 0;
+        width: 100%;
+        background-color: #eef3f7;
+        padding: 5px;
+        border-top: 1px solid #d2dae1;
+    }
+
+    .messageReceive {
+        float: left;
+        background-color: #409eff;
+        border-radius: 6px;
+        padding: 10px;
+        width: 70%;
+        font-size: 12px;
+        margin-bottom: 15px;
+        border-style: dotted;
+    }
+
+    .messageSend {
+        float: right;
+        background-color: #13ce66;
+        border-radius: 10px;
+        padding: 10px;
+        width: 70%;
+        font-size: 12px;
+        margin-bottom: 15px;
+        border-right-style: double;
+    }
+}
+
+.log-content {
+    padding: 2px;
+    height: calc(100% - 44px);
+    overflow: auto;
+    ul {
+        padding: 0;
+        margin: 4px 0;
+        list-style: none;
+    }
+
+    a {
+        width: 100%;
+        color: #333;
+        //border: 1px solid #fff;
+        flex-wrap: wrap;
+        padding: 5px 5px 1px 5px;
+        text-decoration: none;
+        font-size: 12px;
+
+        .time {
+            font-size: 10px;
+            color: gray;
+        }
+
+        div {
+            color: #1b93e0;
+
+            .lable-s1 {
+                color: gray;
+            }
+        }
+
+        .fail {
+            color: #f56c6c;
+        }
+
+        .wait {
+            color: #909399;
+        }
+    }
+}
+
+.H100 {
+    //overflow: hidden;
+    margin-left: 10px;
+}
+
+.row-list {
+    height: calc(100% - 20px);
+    height: 700px;
+    overflow: auto;
+    margin: -20px -20px -20px -30px !important;
+    font-size: 12px;
+    line-height: 20px;
+}
+
+.running-status {
+    .select {
+        margin-bottom: 15px;
+    }
+
+    .edit-class {
+        margin-top: 10px;
+    }
+}
+.running-status {
+    .select {
+        margin-bottom: 15px;
+    }
+
+    .edit-class {
+        margin-top: 10px;
+    }
+}
+</style>

+ 124 - 0
src/views/pms/video_center/device/recycle-record.vue

@@ -0,0 +1,124 @@
+<template>
+    <el-dialog :title="$t('device.recycle-record.845969-0')" :visible.sync="open" width="900px">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+                <el-form-item prop="operateDeptId">
+                    <treeselect v-model="queryParams.operateDeptId" :options="deptOptions" :show-count="true" style="width: 180px" :placeholder="$t('device.recycle-record.845969-1')" />
+                </el-form-item>
+                <el-form-item prop="productId">
+                    <el-select v-model="queryParams.productId" :placeholder="$t('device.allot-record.155854-2')" clearable style="width: 180px">
+                        <el-option v-for="item in productList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item prop="serialNumber">
+                    <el-input v-model="queryParams.serialNumber" :placeholder="$t('device.device-edit.148398-7')" clearable size="small" style="width: 180px" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.recycle-record.845969-4') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('device.recycle-record.845969-5') }}</el-button>
+                </el-form-item>
+            </el-form>
+
+            <el-table :border="false" v-loading="loading" ref="singleTable" :data="dataList" size="mini">
+                <el-table-column :label="$t('device.recycle-record.845969-1')" align="left" prop="operateDeptName" />
+                <el-table-column :label="$t('device.recycle-record.845969-6')" align="left" min-width="120" prop="targetDeptName" />
+                <el-table-column :label="$t('device.device-edit.148398-1')" align="left" prop="deviceName" />
+                <el-table-column label="DeviceKey" align="left" prop="deviceId" />
+                <el-table-column :label="$t('device.device-edit.148398-7')" align="left" prop="serialNumber" />
+                <el-table-column :label="$t('device.allot-record.155854-2')" align="left" prop="productName" />
+                <el-table-column :label="$t('device.recycle-record.845969-8')" align="left" prop="createTime" />
+            </el-table>
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listProduct } from '@/api/pms/video/product';
+import { listRecycleRecord } from '@/api/pms/video/device';
+import { deptsTreeSelect } from '@/api/system/user';
+import Treeselect from '@riophae/vue-treeselect';
+import '@riophae/vue-treeselect/dist/vue-treeselect.css';
+
+export default {
+    name: 'recycleRecord',
+    dicts: [],
+    components: {
+        Treeselect,
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 总条数
+            total: 0,
+            // 打开选择产品对话框
+            open: false,
+            // 产品列表
+            productList: [],
+            dataList: [],
+            //时间范围
+            daterangeTime: [],
+            deptOptions: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                operateDeptId: null,
+                productId: null,
+                serialNumber: '',
+                type: 2,
+            },
+        };
+    },
+    created() {
+        this.getProductList();
+        this.getDeptTree();
+    },
+    methods: {
+        /** 查询产品列表 */
+        getProductList() {
+            this.loading = true;
+            const params = {
+                pageSize: 999,
+            };
+            listProduct(params).then((response) => {
+                this.productList = response.rows.map((item) => {
+                    return { value: item.productId, label: item.productName };
+                });
+                this.loading = false;
+            });
+        },
+        //查询导入记录列表
+        getList() {
+            this.loading = true;
+            listRecycleRecord(this.queryParams).then((response) => {
+                this.dataList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /** 查询机构下拉树结构 */
+        getDeptTree() {
+            deptsTreeSelect().then((response) => {
+                this.deptOptions = response.data;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        /**关闭对话框 */
+        closeDialog() {
+            this.open = false;
+        },
+    },
+};
+</script>

+ 1036 - 0
src/views/pms/video_center/device/running-status.vue

@@ -0,0 +1,1036 @@
+<template>
+    <div class="device-detail-page">
+        <el-card class="main-card">
+            <el-row :gutter="120">
+                <el-col :xs="24" :sm="24" :md="24" :lg="14" :xl="10" style="margin-bottom: 50px">
+                    <el-descriptions :column="1" border style="margin-bottom: 50px">
+                        <!-- 设备模式-->
+                        <el-descriptions-item :labelStyle="statusColor">
+                            <template slot="label">
+                                <i class="el-icon-menu"></i>
+                                {{ $t('device.running-status.866086-0') }}
+                            </template>
+                            <el-link :underline="false" style="line-height: 28px; font-size: 16px; padding-right: 10px">{{ title }}</el-link>
+                        </el-descriptions-item>
+                        <!-- 设备升级-->
+                        <el-descriptions-item :labelStyle="statusColor">
+                            <template slot="label">
+                                <svg-icon icon-class="ota" />
+                                {{ $t('device.running-status.866086-1') }}
+                            </template>
+                            <el-link :underline="false" style="line-height: 28px; font-size: 16px; padding-right: 10px">Version {{ deviceInfo.firmwareVersion }}</el-link>
+                            <el-button type="success" size="mini" style="float: right" @click="getLatestFirmware(deviceInfo.deviceId)" :disabled="deviceInfo.status != 3">
+                                {{ $t('device.running-status.866086-2') }}
+                            </el-button>
+                        </el-descriptions-item>
+
+                        <!-- 设备物模型-->
+                        <el-descriptions-item v-for="(item, index) in deviceInfo.thingsModels" :key="index" :labelStyle="statusColor">
+                            <template slot="label">
+                                <i class="el-icon-open"></i>
+                                {{ item.name }}
+                            </template>
+                            <div v-if="item.datatype.type == 'bool'">
+                                <el-switch
+                                    v-model="item.shadow"
+                                    @change="mqttPublish(deviceInfo, item)"
+                                    active-text=""
+                                    inactive-text=""
+                                    :disabled="shadowUnEnable || item.isReadonly == 1"
+                                    active-value="1"
+                                    inactive-value="0"
+                                    style="min-width: 100px"
+                                />
+                            </div>
+                            <div v-if="item.datatype.type == 'enum'">
+                                <div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
+                                    <el-button
+                                        style="margin: 5px"
+                                        size="mini"
+                                        @click="enumButtonClick(deviceInfo, item, subItem.value)"
+                                        v-for="subItem in item.datatype.enumList"
+                                        :key="subItem.value"
+                                        :disabled="shadowUnEnable || item.isReadonly == 1"
+                                    >
+                                        {{ subItem.text }}
+                                    </el-button>
+                                </div>
+                                <el-select v-else v-model="item.shadow" :placeholder="$t('device.running-status.866086-3')" :disabled="shadowUnEnable || item.isReadonly == 1" @change="mqttPublish(deviceInfo, item)">
+                                    <el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                </el-select>
+                            </div>
+                            <div v-if="item.datatype.type == 'string'">
+                                <el-input
+                                    v-model="item.shadow"
+                                    :disabled="shadowUnEnable || item.isReadonly == 1"
+                                    :placeholder="item.datatype.unit ? $t('device.running-status.866086-5', [item.datatype.unit]) : $t('device.running-status.866086-4')"
+                                >
+                                    <el-button
+                                        slot="append"
+                                        icon="el-icon-s-promotion"
+                                        @click="mqttPublish(deviceInfo, item)"
+                                        v-if="!shadowUnEnable && item.isReadonly == 0"
+                                        :title="$t('device.running-status.866086-6')"
+                                        style="font-size: 20px"
+                                    ></el-button>
+                                </el-input>
+                            </div>
+                            <div v-if="item.datatype.type == 'decimal'">
+                                <div style="width: 80%; float: left">
+                                    <el-slider
+                                        v-model="item.shadow"
+                                        :min="item.datatype.min"
+                                        :max="item.datatype.max"
+                                        :disabled="shadowUnEnable || item.isReadonly == 1"
+                                        :format-tooltip="(x) => x + ' ' + item.datatype.unit"
+                                        :step="item.datatype.step"
+                                    ></el-slider>
+                                </div>
+                                <div style="width: 20%; float: left">
+                                    <el-button
+                                        icon="el-icon-s-promotion"
+                                        type="info"
+                                        @click="mqttPublish(deviceInfo, item)"
+                                        v-if="!shadowUnEnable && item.isReadonly == 0"
+                                        :title="$t('device.running-status.866086-6')"
+                                        style="font-size: 16px; padding: 1px 8px; margin: 2px 0 0 5px; border-radius: 3px"
+                                    ></el-button>
+                                </div>
+                            </div>
+                            <div v-if="item.datatype.type == 'integer'">
+                                <div style="width: 80%; float: left">
+                                    <el-slider
+                                        v-model="item.shadow"
+                                        :min="item.datatype.min"
+                                        :max="item.datatype.max"
+                                        :disabled="shadowUnEnable || item.isReadonly == 1"
+                                        :format-tooltip="(x) => x + ' ' + item.datatype.unit"
+                                        :step="item.datatype.step"
+                                    ></el-slider>
+                                </div>
+                                <div style="width: 20%; float: left">
+                                    <el-button
+                                        icon="el-icon-s-promotion"
+                                        type="info"
+                                        @click="mqttPublish(deviceInfo, item)"
+                                        v-if="!shadowUnEnable && item.isReadonly == 0"
+                                        :title="$t('device.running-status.866086-6')"
+                                        style="font-size: 16px; padding: 1px 8px; margin: 4px 0 0 10px; border-radius: 3px"
+                                    ></el-button>
+                                </div>
+                            </div>
+                            <div v-if="item.datatype.type == 'object'">
+                                <el-descriptions :column="1" size="mini" border>
+                                    <el-descriptions-item v-for="(param, index) in item.datatype.params" :key="index" :label="param.name">
+                                        <div v-if="param.datatype.type == 'bool'">
+                                            <el-switch
+                                                v-model="param.shadow"
+                                                @change="mqttPublish(deviceInfo, param)"
+                                                active-text=""
+                                                :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                active-value="1"
+                                                inactive-text=""
+                                                inactive-value="0"
+                                                style="min-width: 100px"
+                                            />
+                                        </div>
+                                        <div v-if="param.datatype.type == 'enum'">
+                                            <div v-if="param.datatype.showWay && param.datatype.showWay == 'button'">
+                                                <el-button
+                                                    style="margin: 5px"
+                                                    size="mini"
+                                                    v-for="subItem in param.datatype.enumList"
+                                                    :key="subItem.value"
+                                                    :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                    @click="enumButtonClick(deviceInfo, param, subItem.value)"
+                                                >
+                                                    {{ subItem.text }}
+                                                </el-button>
+                                            </div>
+                                            <el-select
+                                                size="small"
+                                                v-else
+                                                v-model="param.shadow"
+                                                :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                :placeholder="$t('device.running-status.866086-3')"
+                                                @change="mqttPublish(deviceInfo, param)"
+                                            >
+                                                <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                            </el-select>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'string'">
+                                            <el-input v-model="param.shadow" :placeholder="$t('device.running-status.866086-4')" :disabled="shadowUnEnable || param.isReadonly == 1">
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, param)"
+                                                    v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'decimal'">
+                                            <el-input v-model="param.shadow" type="number" :placeholder="$t('device.running-status.866086-7')" :disabled="shadowUnEnable || param.isReadonly == 1">
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, param)"
+                                                    v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'integer'">
+                                            <el-input v-model="param.shadow" type="integer" :placeholder="$t('device.running-status.866086-8')" :disabled="shadowUnEnable || param.isReadonly == 1">
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, param)"
+                                                    v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                            </div>
+                            <div v-if="item.datatype.type == 'array'">
+                                <el-descriptions :column="1" size="mini" border v-if="item.datatype.arrayType != 'object'">
+                                    <el-descriptions-item v-for="(model, index) in item.datatype.arrayModel" :key="index" :label="item.name + (index + 1)">
+                                        <div v-if="item.datatype.arrayType == 'string'">
+                                            <el-input
+                                                :placeholder="$t('device.running-status.866086-4')"
+                                                size="mini"
+                                                v-model="model.shadow"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                                @input="arrayItemChange($event, item)"
+                                            >
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, model)"
+                                                    v-if="!shadowUnEnable || item.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                        <div v-if="item.datatype.arrayType == 'decimal'">
+                                            <el-input
+                                                type="number"
+                                                :placeholder="$t('device.running-status.866086-7')"
+                                                size="mini"
+                                                v-model="model.shadow"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                                @input="arrayItemChange($event, item)"
+                                            >
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, model)"
+                                                    v-if="!shadowUnEnable || item.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                        <div v-if="item.datatype.arrayType == 'integer'">
+                                            <el-input
+                                                type="integer"
+                                                :placeholder="$t('device.running-status.866086-8')"
+                                                size="mini"
+                                                v-model="model.shadow"
+                                                :disabled="shadowUnEnable || item.isReadonly == 1"
+                                                @input="arrayItemChange($event, item)"
+                                            >
+                                                <el-button
+                                                    slot="append"
+                                                    icon="el-icon-s-promotion"
+                                                    @click="mqttPublish(deviceInfo, model)"
+                                                    v-if="!shadowUnEnable || item.isReadonly == 0"
+                                                    :title="$t('device.running-status.866086-6')"
+                                                    style="font-size: 20px"
+                                                ></el-button>
+                                            </el-input>
+                                        </div>
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                                <el-collapse v-if="item.datatype.arrayType == 'object'">
+                                    <el-collapse-item v-for="(arrayParam, index) in item.datatype.arrayParams" :key="index">
+                                        <template slot="title">
+                                            <span style="color: #666">
+                                                <i class="el-icon-tickets"></i>
+                                                {{ item.name + (index + 1) }}
+                                            </span>
+                                        </template>
+                                        <el-descriptions :column="1" size="mini" border>
+                                            <el-descriptions-item v-for="(param, index) in arrayParam" :key="index" :label="param.name">
+                                                <div v-if="param.datatype.type == 'bool'">
+                                                    <el-switch
+                                                        v-model="param.shadow"
+                                                        @change="mqttPublish(deviceInfo, param)"
+                                                        active-text=""
+                                                        :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                        active-value="1"
+                                                        inactive-text=""
+                                                        inactive-value="0"
+                                                        style="min-width: 100px"
+                                                    />
+                                                </div>
+                                                <div v-if="param.datatype.type == 'enum'">
+                                                    <div v-if="param.datatype.showWay && param.datatype.showWay == 'button'">
+                                                        <el-button
+                                                            style="margin: 5px"
+                                                            size="mini"
+                                                            v-for="subItem in param.datatype.enumList"
+                                                            :key="subItem.value"
+                                                            :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                            @click="enumButtonClick(deviceInfo, param, subItem.value)"
+                                                        >
+                                                            {{ subItem.text }}
+                                                        </el-button>
+                                                    </div>
+                                                    <el-select
+                                                        v-else
+                                                        v-model="param.shadow"
+                                                        :placeholder="$t('device.running-status.866086-3')"
+                                                        :disabled="shadowUnEnable || param.isReadonly == 1"
+                                                        size="small"
+                                                        @change="mqttPublish(deviceInfo, param)"
+                                                    >
+                                                        <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                                    </el-select>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'string'">
+                                                    <el-input v-model="param.shadow" :placeholder="$t('device.running-status.866086-4')" :disabled="shadowUnEnable || param.isReadonly == 1">
+                                                        <el-button
+                                                            slot="append"
+                                                            icon="el-icon-s-promotion"
+                                                            @click="mqttPublish(deviceInfo, param)"
+                                                            v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                            :title="$t('device.running-status.866086-6')"
+                                                            style="font-size: 20px"
+                                                        ></el-button>
+                                                    </el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'decimal'">
+                                                    <el-input v-model="param.shadow" type="number" :disabled="shadowUnEnable || param.isReadonly == 1" :placeholder="$t('device.running-status.866086-7')">
+                                                        <el-button
+                                                            slot="append"
+                                                            icon="el-icon-s-promotion"
+                                                            @click="mqttPublish(deviceInfo, param)"
+                                                            v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                            :title="$t('device.running-status.866086-6')"
+                                                            style="font-size: 20px"
+                                                        ></el-button>
+                                                    </el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'integer'">
+                                                    <el-input v-model="param.shadow" type="integer" :disabled="shadowUnEnable || param.isReadonly == 1" :placeholder="$t('device.running-status.866086-8')">
+                                                        <el-button
+                                                            slot="append"
+                                                            icon="el-icon-s-promotion"
+                                                            @click="mqttPublish(deviceInfo, param)"
+                                                            v-if="!shadowUnEnable && param.isReadonly == 0"
+                                                            :title="$t('device.running-status.866086-6')"
+                                                            style="font-size: 20px"
+                                                        ></el-button>
+                                                    </el-input>
+                                                </div>
+                                            </el-descriptions-item>
+                                        </el-descriptions>
+                                    </el-collapse-item>
+                                </el-collapse>
+                            </div>
+                        </el-descriptions-item>
+                    </el-descriptions>
+
+                    <!---设备状态(影子模式,value值不会更新)-->
+                    <el-descriptions :column="1" border size="mini" v-if="deviceInfo.isShadow == 1 && deviceInfo.status != 3">
+                        <template slot="title">
+                            <span style="font-size: 14px; color: #606266">{{ $t('device.running-status.866086-9') }}</span>
+                        </template>
+
+                        <!-- 设备物模型-->
+                        <el-descriptions-item v-for="(item, index) in deviceInfo.thingsModels" :key="index">
+                            <template slot="label">
+                                <i class="el-icon-open"></i>
+                                {{ item.name }}
+                            </template>
+                            <div v-if="item.datatype.type == 'bool'">
+                                <el-switch v-model="item.value" @change="mqttPublish(deviceInfo, item)" active-text="" inactive-text="" active-value="1" disabled inactive-value="0" style="min-width: 100px" />
+                            </div>
+                            <div v-if="item.datatype.type == 'enum'">
+                                <div v-if="item.datatype.showWay && item.datatype.showWay == 'button'">
+                                    <el-button style="margin: 5px" size="mini" disabled v-for="subItem in item.datatype.enumList" :key="subItem.value">{{ subItem.text }}</el-button>
+                                </div>
+                                <el-select v-else v-model="item.value" :placeholder="$t('device.running-status.866086-3')" disabled size="mini" @change="mqttPublish(deviceInfo, item)">
+                                    <el-option v-for="subItem in item.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                </el-select>
+                            </div>
+                            <div v-if="item.datatype.type == 'string'">
+                                <el-input v-model="item.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
+                            </div>
+                            <div v-if="item.datatype.type == 'decimal'">
+                                <el-input v-model="item.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
+                            </div>
+                            <div v-if="item.datatype.type == 'integer'">
+                                <el-input v-model="item.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
+                            </div>
+                            <div v-if="item.datatype.type == 'object'">
+                                <el-descriptions :column="1" size="mini" border>
+                                    <el-descriptions-item v-for="(param, index) in item.datatype.params" :key="index" :label="param.name">
+                                        <div v-if="param.datatype.type == 'bool'">
+                                            <el-switch
+                                                v-model="param.value"
+                                                size="mini"
+                                                @change="mqttPublish(deviceInfo, param)"
+                                                active-text=""
+                                                active-value="1"
+                                                disabled
+                                                inactive-text=""
+                                                inactive-value="0"
+                                                style="min-width: 100px"
+                                            />
+                                        </div>
+                                        <div v-if="param.datatype.type == 'enum'">
+                                            <el-select v-model="param.value" :placeholder="$t('device.running-status.866086-3')" disabled size="mini" @change="mqttPublish(deviceInfo, param)">
+                                                <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                            </el-select>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'string'">
+                                            <el-input v-model="param.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'decimal'">
+                                            <el-input v-model="param.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
+                                        </div>
+                                        <div v-if="param.datatype.type == 'integer'">
+                                            <el-input v-model="param.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
+                                        </div>
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                            </div>
+                            <div v-if="item.datatype.type == 'array'">
+                                <el-descriptions :column="1" size="mini" border v-if="item.datatype.arrayType != 'object'">
+                                    <el-descriptions-item v-for="(model, index) in item.datatype.arrayModel" :key="index" :label="item.name + (index + 1)">
+                                        <div v-if="item.datatype.arrayType == 'string'">
+                                            <el-input v-model="model.value" :placeholder="$t('device.running-status.866086-4')" size="mini" disabled></el-input>
+                                        </div>
+                                        <div v-if="item.datatype.arrayType == 'decimal'">
+                                            <el-input v-model="model.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
+                                        </div>
+                                        <div v-if="item.datatype.arrayType == 'integer'">
+                                            <el-input v-model="model.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
+                                        </div>
+                                    </el-descriptions-item>
+                                </el-descriptions>
+                                <el-collapse v-if="item.datatype.arrayType == 'object'">
+                                    <el-collapse-item v-for="(arrayParam, index) in item.datatype.arrayParams" :key="index">
+                                        <template slot="title">
+                                            <span style="color: #666">
+                                                <i class="el-icon-tickets"></i>
+                                                {{ item.name + (index + 1) }}
+                                            </span>
+                                        </template>
+                                        <el-descriptions :column="1" size="mini" border>
+                                            <el-descriptions-item v-for="(param, index) in arrayParam" :key="index" :label="param.name">
+                                                <div v-if="param.datatype.type == 'bool'">
+                                                    <el-switch
+                                                        v-model="param.value"
+                                                        @change="mqttPublish(deviceInfo, param)"
+                                                        active-text=""
+                                                        active-value="1"
+                                                        disabled
+                                                        inactive-text=""
+                                                        inactive-value="0"
+                                                        style="min-width: 100px"
+                                                    />
+                                                </div>
+                                                <div v-if="param.datatype.type == 'enum'">
+                                                    <el-select v-model="param.value" :placeholder="$t('device.running-status.866086-3')" disabled size="mini" @change="mqttPublish(deviceInfo, param)">
+                                                        <el-option v-for="subItem in param.datatype.enumList" :key="subItem.value" :label="subItem.text" :value="subItem.value" />
+                                                    </el-select>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'string'">
+                                                    <el-input v-model="param.value" :placeholder="$t('device.running-status.866086-4')" disabled size="mini"></el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'decimal'">
+                                                    <el-input v-model="param.value" type="number" :placeholder="$t('device.running-status.866086-7')" disabled size="mini"></el-input>
+                                                </div>
+                                                <div v-if="param.datatype.type == 'integer'">
+                                                    <el-input v-model="param.value" type="integer" :placeholder="$t('device.running-status.866086-8')" disabled size="mini"></el-input>
+                                                </div>
+                                            </el-descriptions-item>
+                                        </el-descriptions>
+                                    </el-collapse-item>
+                                </el-collapse>
+                            </div>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-col>
+                <el-col :xs="24" :sm="24" :md="24" :lg="10" :xl="14" style="padding-left: 0">
+                    <!-- 设备监测图表-->
+                    <el-row :gutter="20" v-if="deviceInfo.chartList.length > 0" style="background-color: #f5f7fa; padding: 20px 10px 20px 10px; border-radius: 15px; margin-right: 5px">
+                        <el-col :xs="24" :sm="12" :md="12" v-for="(item, index) in deviceInfo.chartList" :key="index">
+                            <el-card shadow="hover" style="border-radius: 30px; margin-bottom: 20px">
+                                <div ref="map" style="height: 240px; width: 230px; margin: 0 auto"></div>
+                            </el-card>
+                        </el-col>
+                    </el-row>
+                </el-col>
+            </el-row>
+        </el-card>
+        <!-- 添加或修改产品固件对话框 -->
+        <el-dialog :title="$t('device.running-status.866086-10')" :visible.sync="openFirmware" width="600px" append-to-body>
+            <div v-if="firmware == null" style="text-align: center; font-size: 16px">
+                <i class="el-icon-success" style="color: #67c23a"></i>
+                {{ $t('device.running-status.866086-11') }}
+            </div>
+            <el-descriptions :column="1" border size="large" v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version" :labelStyle="{ width: '150px', 'font-weight': 'bold' }">
+                <template slot="title">
+                    <el-link icon="el-icon-success" type="success" :underline="false">{{ $t('device.running-status.866086-12') }}</el-link>
+                </template>
+                <el-descriptions-item :label="$t('device.running-status.866086-13')">{{ firmware.firmwareName }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('device.device-edit.148398-4')">{{ firmware.productName }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('device.device-edit.148398-12')">Version {{ firmware.version }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('device.running-status.866086-16')">
+                    <el-link :href="getDownloadUrl(firmware.filePath)" :underline="false" type="primary">{{ getDownloadUrl(firmware.filePath) }}</el-link>
+                </el-descriptions-item>
+                <el-descriptions-item :label="$t('device.running-status.866086-17')">{{ firmware.remark }}</el-descriptions-item>
+            </el-descriptions>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="success" @click="otaUpgrade" v-if="firmware != null && deviceInfo.firmwareVersion < firmware.version">{{ $t('device.running-status.866086-18') }}</el-button>
+                <el-button @click="cancel">{{ $t('cancel') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import { getLatestFirmware } from '@/api/iot/firmware';
+
+import { serviceInvoke } from '@/api/iot/runstatus';
+
+export default {
+    name: 'running-status',
+    props: {
+        device: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device后,刷新列表
+        device: {
+            handler(newVal) {
+                if (newVal && newVal.deviceId != 0) {
+                    this.deviceInfo = newVal;
+                    this.updateDeviceStatus(this.deviceInfo);
+                    this.$nextTick(function () {
+                        this.MonitorChart();
+                    });
+                    // 添加message事件监听器 这个方法有时间差,已挪到device-edit中执行
+                    // this.mqttCallback();
+                }
+            },
+        },
+    },
+    data() {
+        return {
+            // 控制模块标题
+            title: '设备控制 ',
+            // 未启用设备影子
+            shadowUnEnable: false,
+            // 控制项标题背景
+            statusColor: {
+                background: '#67C23A',
+                color: '#fff',
+                minWidth: '100px',
+            },
+            // 最新固件信息
+            firmware: {},
+            // 打开固件对话框
+            openFirmware: false,
+            // 遮罩层
+            loading: true,
+            // 设备信息
+            deviceInfo: {
+                boolList: [],
+                enumList: [],
+                stringList: [],
+                integerList: [],
+                decimalList: [],
+                arrayList: [],
+                thingsModels: [],
+                chartList: [],
+            },
+            // 监测图表
+            monitorChart: [
+                {
+                    chart: {},
+                    data: {
+                        id: '',
+                        name: '',
+                        value: '',
+                    },
+                },
+            ],
+            remoteCommand: {},
+        };
+    },
+    created() {},
+    mounted() {
+    },
+    methods: {
+        /* Mqtt回调处理 */
+        mqttCallback() {
+            this.$mqttTool.client.on('message', (topic, message, buffer) => {
+                let topics = topic.split('/');
+                let productId = topics[1];
+                let deviceNum = topics[2];
+                message = JSON.parse(message.toString());
+                if (!message) {
+                    return;
+                }
+                if (topics[3] == 'status') {
+                    console.log('接收到【设备状态-运行】主题:', topic);
+                    console.log('接收到【设备状态-运行】内容:', message);
+                    // 更新列表中设备的状态
+                    if (this.deviceInfo.serialNumber == deviceNum) {
+                        this.deviceInfo.status = message.status;
+                        this.deviceInfo.isShadow = message.isShadow;
+                        this.deviceInfo.rssi = message.rssi;
+                        this.updateDeviceStatus(this.deviceInfo);
+                    }
+                }
+                //兼容设备回复
+                if (topics[4] == 'reply') {
+                    this.$modal.notifySuccess(message);
+                }
+                if (topic.endsWith('ws/service')) {
+                    console.log('接收到【物模型】主题 【/ws/service】:', topic);
+                    console.log('接收到【物模型】内容:', message);
+                    // 更新列表中设备的属性
+                    if (this.deviceInfo.serialNumber == deviceNum) {
+                        for (let j = 0; j < message.message.length; j++) {
+                            let isComplete = false;
+                            // 设备状态
+                            for (let k = 0; k < this.deviceInfo.thingsModels.length; k++) {
+                                if (this.deviceInfo.thingsModels[k].datatype.type == 'object') {
+                                    // 对象类型
+                                    let convertedMessage;
+                                    try {
+                                        convertedMessage = JSON.parse(message.message[j].value);
+                                    } catch (error) {
+                                        console.error('Error parsing JSON:', error);
+                                    }
+                                    let keys = Object.keys(convertedMessage);
+                                    for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.params.length; n++) {
+                                        for (let i = 0; i < keys.length; i++) {
+                                            if(message.message[1]&&message.message[0].value.split(':')[0] == message.message[1].value.split(':')[0]){
+                                                //判断是否为相等的属性字段名
+                                                if (this.deviceInfo.thingsModels[j].datatype.params[n].id.includes(this.deviceInfo.thingsModels[k].id + "_" + keys[i])) {
+                                                    this.deviceInfo.thingsModels[j].datatype.params[n].shadow = convertedMessage[keys[i]];
+                                                    break;
+                                                }
+                                            }else{
+                                                if (this.deviceInfo.thingsModels[k].datatype.params[n].id.includes(this.deviceInfo.thingsModels[k].id + "_" + keys[i])) {
+                                                    this.deviceInfo.thingsModels[k].datatype.params[n].shadow = convertedMessage[keys[i]];
+                                                    break;
+                                                }
+                                            }
+                                        }
+                                    }
+                                } else if (this.deviceInfo.thingsModels[k].datatype.type == 'array') {
+                                    // 数组类型
+                                    if (this.deviceInfo.thingsModels[k].datatype.arrayType == 'object') {
+                                        // 1.对象类型数组,id为数组中一个元素,例如:array_01_gateway_temperature
+                                        if (String(message.message[j].id).indexOf('array_') == 0) {
+                                            for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayParams.length; n++) {
+                                                for (let m = 0; m < this.deviceInfo.thingsModels[k].datatype.arrayParams[n].length; m++) {
+                                                    if (this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].id == message.message[j].id) {
+                                                        this.deviceInfo.thingsModels[k].datatype.arrayParams[n][m].shadow = message.message[j].value;
+                                                        isComplete = true;
+                                                        break;
+                                                    }
+                                                }
+                                                if (isComplete) {
+                                                    break;
+                                                }
+                                            }
+                                        } else {
+                                            // 2.对象类型数组,例如:gateway_temperature,消息ID添加前缀后匹配
+                                            for (let n = 0; n < this.deviceInfo.thingsModels.length; n++) {
+                                                this.deviceInfo.thingsModels[k].datatype.arrayParams = this.deviceInfo.thingsModels[k].datatype.arrayParams.map((item, index2) => {
+                                                    for (let m = 0; m < item.length; m++) {
+                                                        let array = JSON.parse(message.message[j].value);
+                                                        let index = `${n}${index2}`;
+                                                        let prefix = 'array_' + index + '_';
+                                                        let obj = array[index2];
+                                                        // 获取对象的所有键
+                                                        for (let key in obj) {
+                                                            const data = prefix + message.message[0].id + '_' + key;
+                                                            if (item[m].id === data) {
+                                                                item[m].shadow = obj[key];
+                                                            }
+                                                        }
+                                                    }
+                                                    return item;
+                                                });
+                                            }
+                                        }
+                                    } else {
+                                        // 整数、小数和字符串类型数组
+                                        for (let n = 0; n < this.deviceInfo.thingsModels[k].datatype.arrayModel.length; n++) {
+                                            if (this.deviceInfo.thingsModels[k].datatype.arrayModel[n].id == message.message[j].id) {
+                                                this.deviceInfo.thingsModels[k].datatype.arrayModel[n].shadow = message.message[j].value;
+                                                isComplete = true;
+                                                break;
+                                            }
+                                        }
+                                    }
+                                } else if (this.deviceInfo.thingsModels[k].id == message.message[j].id) {
+                                    // 普通类型(小数/整数/字符串/布尔/枚举)
+                                    if (this.deviceInfo.thingsModels[k].datatype.type == 'decimal' || this.deviceInfo.thingsModels[k].datatype.type == 'integer') {
+                                        this.deviceInfo.thingsModels[k].shadow = Number(message.message[j].value);
+                                    } else {
+                                        this.deviceInfo.thingsModels[k].shadow = message.message[j].value;
+                                    }
+                                    isComplete = true;
+                                    break;
+                                }
+                            }
+                            // 图表数据
+                            for (let k = 0; k < this.deviceInfo.chartList.length; k++) {
+                                if (this.deviceInfo.chartList[k].id.indexOf('array_') == 0) {
+                                    // 数组类型匹配,例如:array_00_gateway_temperature
+                                    if (this.deviceInfo.chartList[k].id == message.message[j].id) {
+                                        // let shadows = message.message[j].value.split(",");
+                                        this.deviceInfo.chartList[k].shadow = message.message[j].value;
+                                        // 更新图表
+                                        for (let m = 0; m < this.monitorChart.length; m++) {
+                                            if (message.message[j].id == this.monitorChart[m].data.id) {
+                                                let data = [
+                                                    {
+                                                        value: message.message[j].value,
+                                                        name: this.monitorChart[m].data.name,
+                                                    },
+                                                ];
+                                                this.monitorChart[m].chart.setOption({
+                                                    series: [
+                                                        {
+                                                            data: data,
+                                                        },
+                                                    ],
+                                                });
+                                                break;
+                                            }
+                                        }
+                                    }
+                                } else {
+                                    // 普通类型匹配
+                                    if (this.deviceInfo.chartList[k].id == message.message[j].id) {
+                                        this.deviceInfo.chartList[k].shadow = message.message[j].value;
+                                        // 更新图表
+                                        for (let m = 0; m < this.monitorChart.length; m++) {
+                                            if (message.message[j].id == this.monitorChart[m].data.id) {
+                                                isComplete = true;
+                                                let data = [
+                                                    {
+                                                        value: message.message[j].value,
+                                                        name: this.monitorChart[m].data.name,
+                                                    },
+                                                ];
+                                                this.monitorChart[m].chart.setOption({
+                                                    series: [
+                                                        {
+                                                            data: data,
+                                                        },
+                                                    ],
+                                                });
+                                                break;
+                                            }
+                                        }
+                                    }
+                                }
+                                if (isComplete) {
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+        },
+        deepEqual(obj1, obj2) {
+            if (JSON.stringify(obj1) === JSON.stringify(obj2)) {
+                return true;
+            }
+            return false;
+        },
+        //发送指令
+        mqttPublish(device, model) {
+            const command = {};
+            command[model.id] = model.shadow;
+            const data = {
+                serialNumber: device.serialNumber,
+                productId: device.productId,
+                remoteCommand: command,
+                identifier: model.id,
+                modelName: model.name,
+                isShadow: device.status != 3,
+                type: model.type,
+            };
+            serviceInvoke(data).then((response) => {
+                if (response.code === 200) {
+                    this.$message({
+                        type: 'success',
+                        message: this.$t('device.running-status.866086-25'),
+                    });
+                }
+            });
+        },
+
+        /**
+         * Mqtt发布消息
+         * @device 设备
+         * @model 物模型(id/name/type/name/isReadonly/value/shadow),type 类型(1=属性,2=功能,3=OTA升级,4=实时监测)
+         * */
+        // mqttPublish(device, model) {
+        //     let topic = "";
+        //     let message = ""
+        //     if (model.type == 1) {
+        //         if (device.status == 3) {
+        //             // 属性,在线模式
+        //             topic = "/" + device.productId + "/" + device.serialNumber + "/property-online/get";
+        //         } else if (device.isShadow) {
+        //             // 属性,离线模式
+        //             topic = "/" + device.productId + "/" + device.serialNumber + "/property-offline/post";
+        //         }
+        //         message = '[{"id":"' + model.id + '","value":"' + model.shadow + '"}]';
+        //     } else if (model.type == 2) {
+        //         if (device.status == 3) {
+        //             // 功能,在线模式
+        //             topic = "/" + device.productId + "/" + device.serialNumber + "/function-online/get";
+        //
+        //         } else if (device.isShadow) {
+        //             // 功能,离线模式
+        //             topic = "/" + device.productId + "/" + device.serialNumber + "/function-offline/post";
+        //         }
+        //         message = '[{"id":"' + model.id + '","value":"' + model.shadow + '"}]';
+        //     } else if (model.type == 3) {
+        //         // OTA升级
+        //         topic = "/" + device.productId + "/" + device.serialNumber + "/ota/get";
+        //         message = '{"version":' + this.firmware.version + ',"downloadUrl":"' + this.getDownloadUrl(this.firmware.filePath) + '"}';
+        //     } else {
+        //         return;
+        //     }
+        //     if (topic != "") {
+        //         // 发布
+        //         this.$mqttTool.publish(topic, message, model.name).then(res => {
+        //             this.$modal.notifySuccess(res);
+        //         }).catch(res => {
+        //             this.$modal.notifyError(res);
+        //         });
+        //     }
+        // },
+
+        /** 枚举类型按钮单击 */
+        enumButtonClick(device, model, value) {
+            model.shadow = value;
+            this.mqttPublish(device, model);
+        },
+
+        /** 更新设备状态 */
+        updateDeviceStatus(device) {
+            this.shadowUnEnable = false;
+            if (device.status == 3) {
+                this.statusColor.background = '#12d09f';
+                this.title = this.$t('device.running-status.866086-26');
+            } else {
+                if (device.isShadow == 1) {
+                    this.statusColor.background = '#409EFF';
+                    this.title = this.$t('device.running-status.866086-27');
+                } else {
+                    this.statusColor.background = '#909399';
+                    this.title = this.$t('device.running-status.866086-28');
+                    this.shadowUnEnable = true;
+                }
+            }
+            this.$emit('statusEvent', this.deviceInfo.status);
+        },
+        /** 物模型数组元素值改变事件 */
+        arrayItemChange(value, thingsModel) {
+            let shadow = '';
+            for (let i = 0; i < thingsModel.datatype.arrayCount; i++) {
+                shadow += thingsModel.datatype.arrayModel[i].shadow + ',';
+            }
+            shadow = shadow.substring(0, shadow.length - 1);
+            thingsModel.shadow = shadow;
+        },
+        /** 物模型中数组值改变事件 */
+        arrayInputChange(value, thingsModel) {
+            let arrayModels = value.split(',');
+            if (arrayModels.length != thingsModel.datatype.arrayCount) {
+                this.$modal.alertWarning(this.$t('device.running-status.866086-29') + thingsModel.datatype.arrayCount + this.$t('device.running-status.866086-30'));
+            } else {
+                for (let i = 0; i < thingsModel.datatype.arrayCount; i++) {
+                    thingsModel.datatype.arrayModel[i].shadow = arrayModels[i];
+                }
+            }
+        },
+        /**用户是否拥有分享设备权限*/
+        // hasShrarePerm(permission) {
+        //   if (this.deviceInfo.isOwner == 0) {
+        //     // 分享设备权限
+        //     if (this.deviceInfo.userPerms.indexOf(permission) == -1) {
+        //       return false;
+        //     }
+        //   }
+        //   return true;
+        // },
+        /** 设备升级 */
+        otaUpgrade() {
+            // OTA升级
+            let topic = '/' + this.deviceInfo.productId + '/' + this.deviceInfo.serialNumber + '/ota/get';
+            let message = '{"version":' + this.firmware.version + ',"downloadUrl":"' + this.getDownloadUrl(this.firmware.filePath) + '"}';
+            // 发布
+            this.$mqttTool
+                .publish(topic, message, this.$t('device.running-status.866086-31'))
+                .then((res) => {
+                    this.$modal.notifySuccess(res);
+                })
+                .catch((res) => {
+                    this.$modal.notifyError(res);
+                });
+            this.openFirmware = false;
+        },
+        /** 获取最新固件 */
+        getLatestFirmware(deviceId) {
+            getLatestFirmware(deviceId).then((response) => {
+                this.firmware = response.data;
+                this.openFirmware = true;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.openFirmware = false;
+        },
+        // 获取下载路径前缀
+        getDownloadUrl(path) {
+            return window.location.origin + process.env.VUE_APP_BASE_API + path;
+        },
+        /**图表展示*/
+        MonitorChart() {
+            for (let i = 0; i < this.deviceInfo.chartList.length; i++) {
+                this.monitorChart[i] = {
+                    chart: this.$echarts.init(this.$refs.map[i]),
+                    data: {
+                        id: this.deviceInfo.chartList[i].id,
+                        name: this.deviceInfo.chartList[i].name,
+                        value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
+                    },
+                };
+                var option;
+                option = {
+                    tooltip: {
+                        formatter: ' {b} <br/> {c}' + this.deviceInfo.chartList[i].datatype.unit,
+                    },
+                    series: [
+                        {
+                            name: this.deviceInfo.chartList[i].datatype.type,
+                            type: 'gauge',
+                            min: this.deviceInfo.chartList[i].datatype.min,
+                            max: this.deviceInfo.chartList[i].datatype.max,
+                            colorBy: 'data',
+                            splitNumber: 10,
+                            radius: '100%',
+                            // 分割线
+                            splitLine: {
+                                distance: 4,
+                            },
+                            axisLabel: {
+                                fontSize: 10,
+                                distance: 10,
+                            },
+                            // 刻度线
+                            axisTick: {
+                                distance: 4,
+                            },
+                            // 仪表盘轴线
+                            axisLine: {
+                                lineStyle: {
+                                    width: 8,
+                                    color: [
+                                        [0.2, '#409EFF'], // 0~20%
+                                        [0.8, '#12d09f'], // 40~60%
+                                        [1, '#F56C6C'], // 80~100%
+                                    ],
+                                    opacity: 0.3,
+                                },
+                            },
+                            pointer: {
+                                icon: 'triangle',
+                                length: '60%',
+                                width: 7,
+                            },
+                            progress: {
+                                show: true,
+                                width: 8,
+                            },
+                            detail: {
+                                valueAnimation: true,
+                                formatter: '{value}' + ' ' + this.deviceInfo.chartList[i].datatype.unit,
+                                offsetCenter: [0, '72%'],
+                                fontSize: 16,
+                            },
+                            data: [
+                                {
+                                    value: this.deviceInfo.chartList[i].shadow ? this.deviceInfo.chartList[i].shadow : this.deviceInfo.chartList[i].datatype.min,
+                                    name: this.deviceInfo.chartList[i].name,
+                                },
+                            ],
+                            title: {
+                                offsetCenter: [0, '95%'],
+                                fontSize: 16,
+                                text: this.deviceInfo.chartList[i].name,
+                            },
+                        },
+                    ],
+                };
+                option && this.monitorChart[i].chart.setOption(option);
+            }
+        },
+        chartResize() {
+            for (let i = 0; i < this.monitorChart.length; i++) {
+                this.monitorChart[i].chart.resize();
+            }
+        },
+    },
+};
+</script>
+
+<style>
+/* 重写滑动块样式 */
+.el-slider__bar {
+    height: 18px;
+}
+
+.el-slider__runway {
+    height: 18px;
+    margin: 5px 0;
+}
+
+.el-slider__button {
+    height: 18px;
+    width: 18px;
+    border-radius: 10%;
+}
+
+.el-slider__button-wrapper {
+    top: -9px;
+}
+</style>

+ 143 - 0
src/views/pms/video_center/device/sub-device-list.vue

@@ -0,0 +1,143 @@
+<template>
+    <el-dialog :title="$t('device.sub-device-list.323213-0')" :visible.sync="openDeviceList" width="800px" append-to-body>
+        <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item :label="$t('device.index.105953-20')" prop="serialNumber">
+                <el-input v-model="queryParams.serialNumber" :placeholder="$t('device.sub-device-list.323213-1')" clearable @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('device.sub-device-list.323213-2')" prop="deviceName">
+                <el-input v-model="queryParams.deviceName" :placeholder="$t('device.sub-device-list.323213-3')" clearable @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item style="float: right">
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('refresh') }}</el-button>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('search') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table :border="false" v-loading="loading" :data="gatewayList" @selection-change="handleSelectionChange">
+            <el-table-column type="selection" width="55" align="center" />
+            <el-table-column label="ID" align="center" prop="deviceId" />
+            <el-table-column :label="$t('device.device-edit.148398-7')" align="center" prop="serialNumber" />
+            <el-table-column :label="$t('device.device-edit.148398-1')" align="center" prop="deviceName" />
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+        <div slot="footer" class="dialog-footer">
+            <el-button type="primary" @click="handleDeviceSelected">{{ $t('confirm') }}</el-button>
+            <el-button @click="closeSelectDeviceList">{{ $t('cancel') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listSubGateway, addGatewayBatch } from '@/api/iot/gateway';
+
+export default {
+    name: 'sub-device-list',
+    props: {
+        gateway: {
+            type: Object,
+            default: null,
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 网关与子设备关联表格数据
+            gatewayList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            openDeviceList: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 15,
+                deviceName: null,
+                serialNumber: null,
+            },
+            // 表单参数
+            form: {},
+            // 表单校验
+        };
+    },
+    created() {},
+
+    watch: {
+        gateway: {
+            handler() {
+                this.queryParams.pageNum = 1;
+                this.getList();
+            },
+            immediate: true,
+        },
+    },
+    methods: {
+        /** 查询网关与子设备关联列表 */
+        getList() {
+            this.loading = true;
+            listSubGateway(this.queryParams).then((response) => {
+                this.gatewayList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                deviceId: null,
+                deviceName: null,
+                serialNumber: null,
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.deviceId);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+
+        // 关闭选择设备列表
+        closeSelectDeviceList() {
+            this.openDeviceList = false;
+        },
+        // 批量新增子设备
+        handleDeviceSelected() {
+            this.gateway.subDeviceIds = this.ids;
+            addGatewayBatch(this.gateway).then((response) => {
+                this.$modal.msgSuccess(this.$t('device.sub-device-list.323213-4'));
+                this.openDeviceList = false;
+                this.$emit('addSuccess');
+            });
+        },
+    },
+};
+</script>

+ 199 - 0
src/views/pms/video_center/device/user-list.vue

@@ -0,0 +1,199 @@
+<template>
+    <el-dialog :title="$t('device.user-list.041943-0')" :visible.sync="openSelectUser" width="800px">
+        <div style="margin-top: -50px">
+            <el-divider></el-divider>
+        </div>
+        <!--用户数据-->
+        <el-form :model="queryParams" ref="queryForm" :rules="rules" :inline="true" label-width="80px">
+            <el-form-item :label="$t('device.user-list.041943-1')" prop="phonenumber">
+                <el-input
+                    type="text"
+                    :placeholder="$t('device.user-list.041943-2')"
+                    v-model="queryParams.phonenumber"
+                    minlength="10"
+                    clearable
+                    size="small"
+                    show-word-limit
+                    style="width: 240px"
+                    @keyup.enter.native="handleQuery"
+                ></el-input>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('device.user-list.041943-3') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table :border="false" v-loading="loading" :data="userList" highlight-current-row size="mini" @current-change="handleCurrentChange" border>
+            <el-table-column :label="$t('device.device-edit.148398-6')" width="50" align="center">
+                <template slot-scope="scope">
+                    <input type="radio" :checked="scope.row.isSelect" name="user" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('device.user-list.041943-5')" align="center" key="userId" prop="userId" width="120" />
+            <el-table-column :label="$t('device.user-list.041943-6')" align="center" key="userName" prop="userName" />
+            <el-table-column :label="$t('device.user-list.041943-7')" align="center" key="nickName" prop="nickName" />
+            <el-table-column :label="$t('device.user-list.041943-1')" align="center" key="phonenumber" prop="phonenumber" width="120" />
+            <el-table-column :label="$t('device.user-list.041943-8')" align="center" prop="createTime" width="160">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div slot="footer" class="dialog-footer">
+            <el-button type="primary" @click="addDeviceUser">{{ $t('device.user-list.041943-9') }}</el-button>
+            <el-button @click="closeSelectUser">{{ $t('device.user-list.041943-10') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listUser } from '@/api/iot/tool';
+import { addDeviceUser, addDeviceUsers } from '@/api/iot/deviceuser';
+
+export default {
+    name: 'user-list',
+    props: {
+        device: {
+            type: Array,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的device
+        device: function (newVal, oldVal) {
+            this.deviceInfo = newVal;
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: false,
+            // 选中数组
+            ids: [],
+            // 弹出层标题
+            title: '',
+            // 用户列表
+            userList: [],
+            // 选中的用户
+            user: {},
+            // 设备信息
+            deviceInfo: {},
+            // 是否显示选择用户弹出层
+            openSelectUser: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                userName: undefined,
+                phonenumber: undefined,
+                status: 0,
+                deptId: undefined,
+            },
+            // 表单校验
+            rules: {
+                phonenumber: [
+                    {
+                        required: true,
+                        message: this.$t('device.user-list.041943-11'),
+                        trigger: 'blur',
+                    },
+                    {
+                        min: 11,
+                        max: 11,
+                        message: this.$t('device.user-list.041943-12'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+        };
+    },
+    created() {},
+    methods: {
+        /** 查询用户列表 */
+        getList() {
+            this.loading = true;
+            listUser(this.addDateRange(this.queryParams, this.dateRange)).then((response) => {
+                this.userList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.$refs['queryForm'].validate((valid) => {
+                if (valid) {
+                    this.queryParams.pageNum = 1;
+                    this.getList();
+                }
+            });
+        },
+        // 重置查询
+        resetQuery() {
+            this.$refs['queryForm'].resetFields();
+            this.userList = [];
+        },
+        //设置单选按钮选中
+        setRadioSelected(userId) {
+            for (let i = 0; i < this.userList.length; i++) {
+                if (this.userList[i].userId == userId) {
+                    this.userList[i].isSelect = true;
+                    this.user = this.userList[i];
+                } else {
+                    this.userList[i].isSelect = false;
+                }
+            }
+        },
+        // 单选数据
+        handleCurrentChange(user) {
+            if (user != null) {
+                this.setRadioSelected(user.userId);
+                this.user = user;
+            }
+        },
+        // 关闭选择用户
+        closeSelectUser() {
+            this.openSelectUser = false;
+            this.resetQuery();
+        },
+        // 添加设备用户
+        addDeviceUser() {
+            if (this.deviceInfo != null && this.deviceInfo.length > 0 && this.user != null) {
+                if (this.deviceInfo.length == 1) {
+                    var form = {};
+                    form.deviceId = this.deviceInfo[0].deviceId;
+                    form.deviceName = this.deviceInfo[0].deviceName;
+                    form.userId = this.user.userId;
+                    form.userName = this.user.userName;
+                    form.phonenumber = this.user.phonenumber;
+                    addDeviceUser(form).then((response) => {
+                        this.$modal.msgSuccess(this.$t('device.user-list.041943-13'));
+                        this.resetQuery();
+                        this.openSelectUser = false;
+                        this.$parent.getList();
+                    });
+                } else {
+                    var form = [];
+                    this.deviceInfo.forEach((device) => {
+                        let data = {};
+                        data.deviceId = device.deviceId;
+                        data.deviceName = device.deviceName;
+                        data.userId = this.user.userId;
+                        data.userName = this.user.userName;
+                        data.phonenumber = this.user.phonenumber;
+                        form.push(data);
+                    });
+
+                    addDeviceUsers(form).then((response) => {
+                        this.$modal.msgSuccess(this.$t('device.user-list.041943-13'));
+                        this.resetQuery();
+                        this.openSelectUser = false;
+                        this.$parent.getList();
+                    });
+                }
+            } else {
+                this.openSelectUser = false;
+            }
+        },
+    },
+};
+</script>

+ 0 - 351
src/views/pms/video_center/index.vue

@@ -1,351 +0,0 @@
-<template>
-  <div class="product-management-container">
-    <el-card shadow="never" class="mb-4">
-     <el-form
-        :model="queryParams"
-        ref="queryFormRef"
-        :inline="true"
-        
-        class="mb-4"
-      >
-        <el-row :gutter="20">
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="产品名称" prop="productName">
-              <el-input
-                v-model="queryParams.productName"
-                placeholder="请输入产品名称"
-                clearable
-                 style="width: 160px"
-                @keyup.enter="handleQuery"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="协议类型" prop="protocolType">
-              <el-select
-                v-model="queryParams.protocolType"
-                placeholder="请选择协议类型"
-                clearable
-                style="width: 160px"
-              >
-                <el-option
-                  v-for="(value, key) in protocolDict"
-                  :key="key"
-                  :label="value"
-                  :value="key"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item label="厂商名称" prop="manufacturer">
-              <el-input
-                v-model="queryParams.manufacturer"
-                placeholder="请输入厂商名称"
-                clearable
-                 style="width: 160px"
-                @keyup.enter="handleQuery"
-              />
-            </el-form-item>
-          </el-col>
-          <el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6">
-            <el-form-item>
-              <div class="query-buttons">
-                <el-button type="primary" @click="handleQuery" :icon="Search">搜索</el-button>
-                <el-button @click="resetQuery" type="primary" plain :icon="Refresh">重置</el-button>
-              </div>
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-form>
-    </el-card>
-
-    <!-- 标题 -->
-    <el-card shadow="never" class="mb-4">
-      <!-- 操作按钮 -->
-      <div class="flex justify-between items-center mb-4">
-        <el-button type="primary" @click="handleAdd" :icon="Plus">新增产品</el-button>
-      </div>
-
-      <!-- 表格 -->
-      <el-table :data="productList" style="width: 100%" :cell-style="{ padding: '12px 16px' }">
-        <el-table-column
-          prop="productName"
-          label="产品名称"
-          width="180"
-          fixed="left"
-          align="center"
-        />
-        <el-table-column prop="protocolType" label="协议类型" width="180" align="center">
-          <template #default="{ row }">
-            <span>{{ protocolDict[row.protocolType] || row.protocolType }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="model" label="产品型号" width="180" align="center" />
-        <el-table-column prop="manufacturer" label="厂商名称" width="180" align="center" />
-        <el-table-column prop="videoCodec" label="视频编码格式" width="180" align="center" />
-        <el-table-column prop="maxChannels" label="最大通道数" width="120" align="center" />
-        <el-table-column prop="supportRecording" label="是否支持录像" width="120" align="center">
-          <template #default="{ row }">
-            <el-tag size="small" :type="row.supportRecording ? 'success' : 'info'">
-              {{ row.supportRecording ? '是' : '否' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column prop="supportPtz" label="是否支持云台" width="120" align="center">
-          <template #default="{ row }">
-            <el-tag size="small" :type="row.supportPtz ? 'success' : 'info'">
-              {{ row.supportPtz ? '是' : '否' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" width="180" align="center">
-          <template #default="{ row }">
-            <el-button text size="small" type="primary" @click="handleEdit(row)">编辑</el-button>
-            <el-button text size="small" type="danger" @click="handleDelete(row.id)"
-              >删除</el-button
-            >
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <!-- 分页 -->
-      <div class="mt-4 flex justify-center">
-        <el-pagination
-          v-model:current-page="currentPage"
-          v-model:page-size="pageSize"
-          :total="total"
-          layout="total, sizes, prev, pager, next, jumper"
-          @size-change="handleSizeChange"
-          @current-change="handleCurrentChange"
-        />
-      </div>
-    </el-card>
-
-    <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
-      <el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
-        <el-form-item label="产品名称" prop="productName">
-          <el-input v-model="formData.productName" placeholder="请输入产品名称" />
-        </el-form-item>
-        <el-form-item label="协议类型" prop="protocolType">
-          <el-select v-model="formData.protocolType" placeholder="请选择协议类型">
-            <el-option
-              v-for="(value, key) in protocolDict"
-              :key="key"
-              :label="value"
-              :value="key"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="产品型号" prop="model">
-          <el-input v-model="formData.model" placeholder="请输入产品型号" />
-        </el-form-item>
-        <el-form-item label="厂商名称" prop="manufacturer">
-          <el-input v-model="formData.manufacturer" placeholder="请输入厂商名称" />
-        </el-form-item>
-        <el-form-item label="视频编码格式" prop="videoCodec">
-          <el-input v-model="formData.videoCodec" placeholder="请输入视频编码格式" />
-        </el-form-item>
-        <el-form-item label="最大通道数" prop="maxChannels">
-          <el-input-number
-            v-model="formData.maxChannels"
-            :min="1"
-            :max="1000"
-            controls-position="right"
-          />
-        </el-form-item>
-        <el-form-item label="是否支持录像" prop="supportRecording">
-          <el-switch v-model="formData.supportRecording" active-text="是" inactive-text="否" />
-        </el-form-item>
-        <el-form-item label="是否支持云台" prop="supportPtz">
-          <el-switch v-model="formData.supportPtz" active-text="是" inactive-text="否" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <span class="dialog-footer">
-          <el-button @click="dialogVisible = false">取消</el-button>
-          <el-button type="primary" @click="submitForm">确定</el-button>
-        </span>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive, computed } from 'vue'
-import { ElMessage, ElMessageBox } from 'element-plus'
-import { Plus,Search,Refresh } from '@element-plus/icons-vue'
-
-// 查询参数
-const queryParams = reactive({
-  productName: '',
-  protocolType: '',
-  manufacturer: ''
-})
-
-// 查询表单引用
-const queryFormRef = ref()
-
-// 搜索处理
-const handleQuery = () => {
-  // 这里应该调用API获取数据,目前只是演示效果
-  console.log('查询参数:', queryParams)
-  ElMessage.info('执行查询操作')
-}
-
-// 重置查询
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  handleQuery()
-}
-
-// 协议类型字典
-const protocolDict = {
-  GB28181: 'GB28181',
-  ONVIF: 'ONVIF',
-  RTSP: 'RTSP',
-  HIK: '海康私有协议',
-  DAHUA: '大华私有协议'
-}
-
-// 初始数据
-const productList = ref([
-  {
-    id: 1,
-    productName: '海康DS-2CD2945G2-I',
-    protocolType: 'HIK',
-    model: 'DS-2CD2945G2-I',
-    manufacturer: '海康威视',
-    videoCodec: 'H.265',
-    maxChannels: 4,
-    supportRecording: true,
-    supportPtz: true
-  },
-  {
-    id: 2,
-    productName: '大华DH-IPC-HFW5831E-Z',
-    protocolType: 'DAHUA',
-    model: 'DH-IPC-HFW5831E-Z',
-    manufacturer: '大华股份',
-    videoCodec: 'H.265',
-    maxChannels: 8,
-    supportRecording: true,
-    supportPtz: true
-  }
-])
-
-// 分页
-const currentPage = ref(1)
-const pageSize = ref(10)
-const total = computed(() => productList.value.length)
-
-// 弹窗控制
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增产品')
-const formData = reactive({
-  id: null,
-  productName: '',
-  protocolType: '',
-  model: '',
-  manufacturer: '',
-  videoCodec: '',
-  maxChannels: 1,
-  supportRecording: false,
-  supportPtz: false
-})
-
-// 表单校验规则
-const rules = {
-  productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
-  protocolType: [{ required: true, message: '请选择协议类型', trigger: 'change' }],
-  model: [{ required: true, message: '请输入产品型号', trigger: 'blur' }],
-  manufacturer: [{ required: true, message: '请输入厂商名称', trigger: 'blur' }],
-  videoCodec: [{ required: true, message: '请输入视频编码格式', trigger: 'blur' }],
-  maxChannels: [{ required: true, message: '请输入最大通道数', trigger: 'blur' }]
-}
-
-// 新增
-const handleAdd = () => {
-  dialogTitle.value = '新增产品'
-  Object.assign(formData, {
-    id: null,
-    productName: '',
-    protocolType: '',
-    model: '',
-    manufacturer: '',
-    videoCodec: '',
-    maxChannels: 1,
-    supportRecording: false,
-    supportPtz: false
-  })
-  dialogVisible.value = true
-}
-
-// 编辑
-const handleEdit = (row: any) => {
-  dialogTitle.value = '编辑产品'
-  Object.assign(formData, row)
-  dialogVisible.value = true
-}
-
-// 删除
-const handleDelete = (id: number) => {
-  ElMessageBox.confirm('确定要删除该产品吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  })
-    .then(() => {
-      productList.value = productList.value.filter((item) => item.id !== id)
-      ElMessage.success('删除成功')
-    })
-    .catch(() => {})
-}
-
-// 提交表单
-const submitForm = () => {
-  const formRef = useFormRef()
-  formRef.validate((valid: boolean) => {
-    if (valid) {
-      if (formData.id) {
-        // 编辑
-        const index = productList.value.findIndex((item) => item.id === formData.id)
-        if (index !== -1) {
-          productList.value.splice(index, 1, { ...formData } as any)
-        }
-      } else {
-        // 新增
-        formData.id = Date.now()
-        productList.value.push({ ...formData })
-      }
-      ElMessage.success('保存成功')
-      dialogVisible.value = false
-    }
-  })
-}
-
-// 分页事件
-const handleSizeChange = (val: number) => {
-  pageSize.value = val
-}
-const handleCurrentChange = (val: number) => {
-  currentPage.value = val
-}
-
-// 使用 Form Ref
-const formRef = ref()
-const useFormRef = () => formRef.value
-</script>
-
-<style scoped>
-.product-management-container {
-  padding: 20px;
-  background-color: #f0f2f5;
-}
-.dialog-footer {
-  text-align: right;
-}
-
-
-</style>

+ 192 - 0
src/views/pms/video_center/product/components/Crontab/day.vue

@@ -0,0 +1,192 @@
+<template>
+  <el-form size="small">
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        日,允许的通配符[, - * ? / L W]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+        不指定
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        周期从
+        <el-input-number v-model='cycle01' :min="1" :max="30" /> -
+        <el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" />日
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        从
+        <el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
+        <el-input-number v-model='average02' :min="1" :max="31 - average01 || 1" />
+        日执行一次
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="5">
+        每月
+        <el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="6">
+        本月最后一天
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="7">
+        指定
+        <el-select 
+          clearable 
+          v-model="checkboxList" 
+          placeholder="可多选"
+          multiple 
+          style="width:100%"
+        >
+          <el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch, getCurrentInstance } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-day'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 获取实例
+const { proxy } = getCurrentInstance()
+
+// 数据定义
+const radioValue = ref(1)
+const workday = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 1, 30)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : 2, 31, 31)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 1, 30)
+  const average02Val = props.check(average02.value, 1, 31 - average01Val || 0)
+  return average01Val + '/' + average02Val
+})
+
+// 计算工作日格式
+const workdayCheck = computed(() => {
+  const workdayVal = props.check(workday.value, 1, 31)
+  return workdayVal
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  console.log('day rachange')
+  if (radioValue.value !== 2 && props.cron.week !== '?') {
+    emit('update', 'week', '?', 'day')
+  }
+
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'day', '*')
+      break
+    case 2:
+      emit('update', 'day', '?')
+      break
+    case 3:
+      emit('update', 'day', cycleTotal.value)
+      break
+    case 4:
+      emit('update', 'day', averageTotal.value)
+      break
+    case 5:
+      emit('update', 'day', workday.value + 'W')
+      break
+    case 6:
+      emit('update', 'day', 'L')
+      break
+    case 7:
+      emit('update', 'day', checkboxString.value)
+      break
+  }
+  console.log('day rachange end')
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'day', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'day', averageTotal.value)
+  }
+}
+
+// 最近工作日值变化时
+const workdayChange = () => {
+  if (radioValue.value == '5') {
+    emit('update', 'day', workdayCheck.value + 'W')
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '7') {
+    emit('update', 'day', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(workdayCheck, workdayChange)
+watch(checkboxString, checkboxChange)
+</script>

+ 143 - 0
src/views/pms/video_center/product/components/Crontab/hour.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-form size="small">
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        小时,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+        周期从
+        <el-input-number v-model='cycle01' :max="22" :min="0"/>
+        -
+        <el-input-number v-model='cycle02' :max="23" :min="cycle01 ? cycle01 + 1 : 1"/>
+       小时
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        从
+        <el-input-number v-model='average01' :max="22" :min="0"/>
+        小时开始,每
+        <el-input-number v-model='average02' :max="23 - average01 || 0" :min="1"/>
+       小时执行一次
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+       指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="可多选" 
+          clearable
+          multiple 
+          style="width:100%"
+        >
+          <el-option v-for="item in 24" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-hour'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 0, 22)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : 1, 23)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 0, 22)
+  const average02Val = props.check(average02.value, 1, 23 - average01Val || 0)
+  return average01Val + '/' + average02Val
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'hour', '*')
+      break
+    case 2:
+      emit('update', 'hour', cycleTotal.value)
+      break
+    case 3:
+      emit('update', 'hour', averageTotal.value)
+      break
+    case 4:
+      emit('update', 'hour', checkboxString.value)
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '2') {
+    emit('update', 'hour', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'hour', averageTotal.value)
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'hour', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(checkboxString, checkboxChange)
+</script>

+ 446 - 0
src/views/pms/video_center/product/components/Crontab/index.vue

@@ -0,0 +1,446 @@
+<template>
+  <div>
+    <el-tabs type="border-card">
+      <el-tab-pane label="秒" v-if="shouldHide('second')">
+        <CrontabSecond @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronsecondRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="分钟" v-if="shouldHide('min')">
+        <CrontabMin @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronminRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="小时" v-if="shouldHide('hour')">
+        <CrontabHour @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronhourRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="日" v-if="shouldHide('day')">
+        <CrontabDay @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="crondayRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="月" v-if="shouldHide('month')">
+        <CrontabMonth @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronmonthRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="周" v-if="shouldHide('week')">
+        <CrontabWeek @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronweekRef" />
+      </el-tab-pane>
+
+      <el-tab-pane label="年" v-if="shouldHide('year')">
+        <CrontabYear @update="updateCrontabValue" :check="checkNumber" :cron="crontabValueObj" ref="cronyearRef" />
+      </el-tab-pane>
+    </el-tabs>
+
+    <div class="popup-main">
+      <div class="popup-result">
+        <p class="title">时间表达式</p>
+        <table>
+          <thead>
+            <th v-for="item of tabTitles" :key="item" width="40">{{ item }}</th>
+            <th>Cron 表达式</th>
+          </thead>
+          <tbody>
+            <td>
+              <span>{{ crontabValueObj.second }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.min }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.hour }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.day }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.month }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.week }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueObj.year }}</span>
+            </td>
+            <td>
+              <span>{{ crontabValueString }}</span>
+            </td>
+          </tbody>
+        </table>
+      </div>
+      <CrontabResult :ex="crontabValueString" />
+
+      <div class="pop_btn">
+        <el-button size="small" type="primary" @click="submitFill">确定</el-button>
+        <el-button size="small" type="warning" @click="clearCron">重置</el-button>
+        <el-button size="small" @click="hidePopup">取消</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, watch, getCurrentInstance } from 'vue'
+import CrontabSecond from "./second.vue";
+import CrontabMin from "./min.vue";
+import CrontabHour from "./hour.vue";
+import CrontabDay from "./day.vue";
+import CrontabMonth from "./month.vue";
+import CrontabWeek from "./week.vue";
+import CrontabYear from "./year.vue";
+import CrontabResult from "./result.vue";
+
+// 定义组件名称
+defineOptions({
+  name: "vcrontab"
+})
+
+// 定义props
+const props = defineProps({
+  expression: {
+    type: String,
+    default: ""
+  },
+  hideComponent: {
+    type: Array,
+    default: () => []
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['hide', 'fill'])
+
+// 获取实例
+const { proxy } = getCurrentInstance()
+
+// Refs
+const cronsecondRef = ref(null)
+const cronminRef = ref(null)
+const cronhourRef = ref(null)
+const crondayRef = ref(null)
+const cronmonthRef = ref(null)
+const cronweekRef = ref(null)
+const cronyearRef = ref(null)
+
+// 数据定义
+const tabTitles = ref([
+  '秒', 
+  '分钟', 
+  '小时', 
+  '日', 
+  '月', 
+  '周', 
+  '年'
+])
+
+const tabActive = ref(0)
+const myindex = ref(0)
+
+const crontabValueObj = reactive({
+  second: "*",
+  min: "*",
+  hour: "*",
+  day: "*",
+  month: "*",
+  week: "?",
+  year: "",
+})
+
+// 计算属性
+const crontabValueString = computed(() => {
+  let obj = crontabValueObj
+  let str =
+    obj.second +
+    " " +
+    obj.min +
+    " " +
+    obj.hour +
+    " " +
+    obj.day +
+    " " +
+    obj.month +
+    " " +
+    obj.week +
+    (obj.year == "" ? "" : " " + obj.year)
+  return str
+})
+
+// 方法定义
+const shouldHide = (key) => {
+  if (props.hideComponent && props.hideComponent.includes(key)) return false
+  return true
+}
+
+const resolveExp = () => {
+  // 反解析 表达式
+  if (props.expression) {
+    let arr = props.expression.split(" ")
+    if (arr.length >= 6) {
+      //6 位以上是合法表达式
+      let obj = {
+        second: arr[0],
+        min: arr[1],
+        hour: arr[2],
+        day: arr[3],
+        month: arr[4],
+        week: arr[5],
+        year: arr[6] ? arr[6] : "",
+      }
+      Object.assign(crontabValueObj, obj)
+      for (let i in obj) {
+        if (obj[i]) changeRadio(i, obj[i])
+      }
+    }
+  } else {
+    // 没有传入的表达式 则还原
+    clearCron()
+  }
+}
+
+// tab切换值
+const tabCheck = (index) => {
+  tabActive.value = index
+}
+
+// 由子组件触发,更改表达式组成的字段值
+const updateCrontabValue = (name, value, from) => {
+  console.log("updateCrontabValue", name, value, from)
+  crontabValueObj[name] = value
+  if (from && from !== name) {
+    console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
+    changeRadio(name, value)
+  }
+}
+
+// 赋值到组件
+const changeRadio = (name, value) => {
+  let arr = ["second", "min", "hour", "month"]
+  let refName = "cron" + name + "Ref"
+  let insValue
+
+  if (!eval(`typeof ${refName} !== 'undefined' && ${refName}.value`)) return
+
+  if (arr.includes(name)) {
+    if (value === "*") {
+      insValue = 1
+    } else if (value.indexOf("-") > -1) {
+      let indexArr = value.split("-")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.cycle01 = 0`))
+        : (eval(`${refName}.value.cycle01 = indexArr[0]`))
+      eval(`${refName}.value.cycle02 = indexArr[1]`)
+      insValue = 2
+    } else if (value.indexOf("/") > -1) {
+      let indexArr = value.split("/")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.average01 = 0`))
+        : (eval(`${refName}.value.average01 = indexArr[0]`))
+      eval(`${refName}.value.average02 = indexArr[1]`)
+      insValue = 3
+    } else {
+      insValue = 4
+      eval(`${refName}.value.checkboxList = value.split(",")`)
+    }
+  } else if (name == "day") {
+    if (value === "*") {
+      insValue = 1
+    } else if (value == "?") {
+      insValue = 2
+    } else if (value.indexOf("-") > -1) {
+      let indexArr = value.split("-")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.cycle01 = 0`))
+        : (eval(`${refName}.value.cycle01 = indexArr[0]`))
+      eval(`${refName}.value.cycle02 = indexArr[1]`)
+      insValue = 3
+    } else if (value.indexOf("/") > -1) {
+      let indexArr = value.split("/")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.average01 = 0`))
+        : (eval(`${refName}.value.average01 = indexArr[0]`))
+      eval(`${refName}.value.average02 = indexArr[1]`)
+      insValue = 4
+    } else if (value.indexOf("W") > -1) {
+      let indexArr = value.split("W")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.workday = 0`))
+        : (eval(`${refName}.value.workday = indexArr[0]`))
+      insValue = 5
+    } else if (value === "L") {
+      insValue = 6
+    } else {
+      eval(`${refName}.value.checkboxList = value.split(",")`)
+      insValue = 7
+    }
+  } else if (name == "week") {
+    if (value === "*") {
+      insValue = 1
+    } else if (value == "?") {
+      insValue = 2
+    } else if (value.indexOf("-") > -1) {
+      let indexArr = value.split("-")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.cycle01 = 0`))
+        : (eval(`${refName}.value.cycle01 = indexArr[0]`))
+      eval(`${refName}.value.cycle02 = indexArr[1]`)
+      insValue = 3
+    } else if (value.indexOf("#") > -1) {
+      let indexArr = value.split("#")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.average01 = 1`))
+        : (eval(`${refName}.value.average01 = indexArr[0]`))
+      eval(`${refName}.value.average02 = indexArr[1]`)
+      insValue = 4
+    } else if (value.indexOf("L") > -1) {
+      let indexArr = value.split("L")
+      isNaN(indexArr[0])
+        ? (eval(`${refName}.value.weekday = 1`))
+        : (eval(`${refName}.value.weekday = indexArr[0]`))
+      insValue = 5
+    } else {
+      eval(`${refName}.value.checkboxList = value.split(",")`)
+      insValue = 6
+    }
+  } else if (name == "year") {
+    if (value == "") {
+      insValue = 1
+    } else if (value == "*") {
+      insValue = 2
+    } else if (value.indexOf("-") > -1) {
+      insValue = 3
+    } else if (value.indexOf("/") > -1) {
+      insValue = 4
+    } else {
+      eval(`${refName}.value.checkboxList = value.split(",")`)
+      insValue = 5
+    }
+  }
+  eval(`${refName}.value.radioValue = insValue`)
+}
+
+// 表单选项的子组件校验数字格式(通过-props传递)
+const checkNumber = (value, minLimit, maxLimit) => {
+  // 检查必须为整数
+  value = Math.floor(value)
+  if (value < minLimit) {
+    value = minLimit
+  } else if (value > maxLimit) {
+    value = maxLimit
+  }
+  return value
+}
+
+// 隐藏弹窗
+const hidePopup = () => {
+  emit("hide")
+}
+
+// 填充表达式
+const submitFill = () => {
+  emit("fill", crontabValueString.value)
+  hidePopup()
+}
+
+const clearCron = () => {
+  // 还原选择项
+  console.log("准备还原")
+  Object.assign(crontabValueObj, {
+    second: "*",
+    min: "*",
+    hour: "*",
+    day: "*",
+    month: "*",
+    week: "?",
+    year: "",
+  })
+  for (let j in crontabValueObj) {
+    changeRadio(j, crontabValueObj[j])
+  }
+}
+
+// Watchers
+watch(
+  () => props.expression,
+  () => {
+    resolveExp()
+  }
+)
+
+watch(
+  () => props.hideComponent,
+  (value) => {
+    // 隐藏部分组件
+  }
+)
+
+// 生命周期钩子
+onMounted(() => {
+  resolveExp()
+})
+</script>
+
+<style scoped>
+.pop_btn {
+  text-align: center;
+  margin-top: 20px;
+}
+
+.popup-main {
+  position: relative;
+  margin: 10px auto;
+  background: #fff;
+  border-radius: 5px;
+  font-size: 12px;
+  overflow: hidden;
+}
+
+.popup-title {
+  overflow: hidden;
+  line-height: 34px;
+  padding-top: 6px;
+  background: #f2f2f2;
+}
+
+.popup-result {
+  box-sizing: border-box;
+  line-height: 24px;
+  margin: 25px auto;
+  padding: 15px 10px 10px;
+  border: 1px solid #ccc;
+  position: relative;
+}
+
+.popup-result .title {
+  position: absolute;
+  top: -28px;
+  left: 50%;
+  width: 140px;
+  font-size: 14px;
+  margin-left: -70px;
+  text-align: center;
+  line-height: 30px;
+  background: #fff;
+}
+
+.popup-result table {
+  text-align: center;
+  width: 100%;
+  margin: 0 auto;
+}
+
+.popup-result table span {
+  display: block;
+  width: 100%;
+  font-family: arial;
+  line-height: 30px;
+  height: 30px;
+  white-space: nowrap;
+  overflow: hidden;
+  border: 1px solid #e8e8e8;
+}
+
+.popup-result-scroll {
+  font-size: 12px;
+  line-height: 24px;
+  height: 10em;
+  overflow-y: auto;
+}
+</style>

+ 143 - 0
src/views/pms/video_center/product/components/Crontab/min.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-form size="small">
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        分钟,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+       周期从
+        <el-input-number v-model='cycle01' :max="58" :min="0"/>
+        -
+        <el-input-number v-model='cycle02' :max="59" :min="cycle01 ? cycle01 + 1 : 1"/>
+       分钟
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        从
+        <el-input-number v-model='average01' :max="58" :min="0"/>
+        分钟开始,每
+        <el-input-number v-model='average02' :max="59 - average01 || 0" :min="1"/>
+        分钟执行一次
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="可多选" 
+          clearable
+          multiple 
+          style="width:100%"
+        >
+          <el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-min'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 0, 58)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : 1, 59)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 0, 58)
+  const average02Val = props.check(average02.value, 1, 59 - average01Val || 0)
+  return average01Val + '/' + average02Val
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'min', '*', 'min')
+      break
+    case 2:
+      emit('update', 'min', cycleTotal.value, 'min')
+      break
+    case 3:
+      emit('update', 'min', averageTotal.value, 'min')
+      break
+    case 4:
+      emit('update', 'min', checkboxString.value, 'min')
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '2') {
+    emit('update', 'min', cycleTotal.value, 'min')
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'min', averageTotal.value, 'min')
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'min', checkboxString.value, 'min')
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(checkboxString, checkboxChange)
+</script>

+ 143 - 0
src/views/pms/video_center/product/components/Crontab/month.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-form size='small'>
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        月,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+        周期从
+        <el-input-number v-model='cycle01' :max="11" :min="1"/>
+        -
+        <el-input-number v-model='cycle02' :max="12" :min="cycle01 ? cycle01 + 1 : 2"/>
+        月
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        从
+        <el-input-number v-model='average01' :max="11" :min="1"/>
+        月开始,每
+        <el-input-number v-model='average02' :max="12 - average01 || 0" :min="1"/>
+        月执行一次
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="可多选" 
+          clearable
+          multiple 
+          style="width:100%"
+        >
+          <el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-month'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 1, 11)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : 2, 12)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 1, 11)
+  const average02Val = props.check(average02.value, 1, 12 - average01Val || 0)
+  return average01Val + '/' + average02Val
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'month', '*')
+      break
+    case 2:
+      emit('update', 'month', cycleTotal.value)
+      break
+    case 3:
+      emit('update', 'month', averageTotal.value)
+      break
+    case 4:
+      emit('update', 'month', checkboxString.value)
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '2') {
+    emit('update', 'month', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'month', averageTotal.value)
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'month', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(checkboxString, checkboxChange)
+</script>

+ 580 - 0
src/views/pms/video_center/product/components/Crontab/result.vue

@@ -0,0 +1,580 @@
+<template>
+  <div class="popup-result">
+    <p class="title">最近5次运行时间</p>
+    <ul class="popup-result-scroll">
+      <template v-if='isShow'>
+        <li v-for='item in resultList' :key="item">{{ item }}</li>
+      </template>
+      <li v-else>计算结果中...</li>
+    </ul>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, onMounted } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-result'
+})
+
+// 定义props
+const props = defineProps({
+  ex: {
+    type: String,
+    required: true
+  }
+})
+
+// 数据定义
+const dayRule = ref('')
+const dayRuleSup = ref('')
+const dateArr = ref([])
+const resultList = ref([])
+const isShow = ref(false)
+
+// 方法定义
+// 表达式值变化时,开始去计算结果
+const expressionChange = () => {
+  // 计算开始-隐藏结果
+  isShow.value = false
+  // 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
+  let ruleArr = props.ex.split(' ')
+  // 用于记录进入循环的次数
+  let nums = 0
+  // 用于暂时存符号时间规则结果的数组
+  let resultArr = []
+  // 获取当前时间精确至[年、月、日、时、分、秒]
+  let nTime = new Date()
+  let nYear = nTime.getFullYear()
+  let nMonth = nTime.getMonth() + 1
+  let nDay = nTime.getDate()
+  let nHour = nTime.getHours()
+  let nMin = nTime.getMinutes()
+  let nSecond = nTime.getSeconds()
+  // 根据规则获取到近100年可能年数组、月数组等等
+  getSecondArr(ruleArr[0])
+  getMinArr(ruleArr[1])
+  getHourArr(ruleArr[2])
+  getDayArr(ruleArr[3])
+  getMonthArr(ruleArr[4])
+  getWeekArr(ruleArr[5])
+  getYearArr(ruleArr[6], nYear)
+  // 将获取到的数组赋值-方便使用
+  let sDate = dateArr.value[0]
+  let mDate = dateArr.value[1]
+  let hDate = dateArr.value[2]
+  let DDate = dateArr.value[3]
+  let MDate = dateArr.value[4]
+  let YDate = dateArr.value[5]
+  // 获取当前时间在数组中的索引
+  let sIdx = getIndex(sDate, nSecond)
+  let mIdx = getIndex(mDate, nMin)
+  let hIdx = getIndex(hDate, nHour)
+  let DIdx = getIndex(DDate, nDay)
+  let MIdx = getIndex(MDate, nMonth)
+  let YIdx = getIndex(YDate, nYear)
+  // 重置月日时分秒的函数(后面用的比较多)
+  const resetSecond = function () {
+    sIdx = 0
+    nSecond = sDate[sIdx]
+  }
+  const resetMin = function () {
+    mIdx = 0
+    nMin = mDate[mIdx]
+    resetSecond()
+  }
+  const resetHour = function () {
+    hIdx = 0
+    nHour = hDate[hIdx]
+    resetMin()
+  }
+  const resetDay = function () {
+    DIdx = 0
+    nDay = DDate[DIdx]
+    resetHour()
+  }
+  const resetMonth = function () {
+    MIdx = 0
+    nMonth = MDate[MIdx]
+    resetDay()
+  }
+  // 如果当前年份不为数组中当前值
+  if (nYear !== YDate[YIdx]) {
+    resetMonth()
+  }
+  // 如果当前月份不为数组中当前值
+  if (nMonth !== MDate[MIdx]) {
+    resetDay()
+  }
+  // 如果当前"日"不为数组中当前值
+  if (nDay !== DDate[DIdx]) {
+    resetHour()
+  }
+  // 如果当前"时"不为数组中当前值
+  if (nHour !== hDate[hIdx]) {
+    resetMin()
+  }
+  // 如果当前"分"不为数组中当前值
+  if (nMin !== mDate[mIdx]) {
+    resetSecond()
+  }
+
+  // 循环年份数组
+  goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
+    let YY = YDate[Yi]
+    // 如果到达最大值时
+    if (nMonth > MDate[MDate.length - 1]) {
+      resetMonth()
+      continue
+    }
+    // 循环月份数组
+    goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
+      // 赋值、方便后面运算
+      let MM = MDate[Mi]
+      MM = MM < 10 ? '0' + MM : MM
+      // 如果到达最大值时
+      if (nDay > DDate[DDate.length - 1]) {
+        resetDay()
+        if (Mi == MDate.length - 1) {
+          resetMonth()
+          continue goYear
+        }
+        continue
+      }
+      // 循环日期数组
+      goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
+        // 赋值、方便后面运算
+        let DD = DDate[Di]
+        let thisDD = DD < 10 ? '0' + DD : DD
+
+        // 如果到达最大值时
+        if (nHour > hDate[hDate.length - 1]) {
+          resetHour()
+          if (Di == DDate.length - 1) {
+            resetDay()
+            if (Mi == MDate.length - 1) {
+              resetMonth()
+              continue goYear
+            }
+            continue goMonth
+          }
+          continue
+        }
+
+        // 判断日期的合法性,不合法的话也是跳出当前循环
+        if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
+          resetDay()
+          continue goMonth
+        }
+        // 如果日期规则中有值时
+        if (dayRule.value == 'lastDay') {
+          // 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
+
+          if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+            while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+              DD--
+
+              thisDD = DD < 10 ? '0' + DD : DD
+            }
+          }
+        } else if (dayRule.value == 'workDay') {
+          // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
+          if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+            while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+              DD--
+              thisDD = DD < 10 ? '0' + DD : DD
+            }
+          }
+          // 获取达到条件的日期是星期X
+          let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
+          // 当星期日时
+          if (thisWeek == 1) {
+            // 先找下一个日,并判断是否为月底
+            DD++
+            thisDD = DD < 10 ? '0' + DD : DD
+            // 判断下一日已经不是合法日期
+            if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+              DD -= 3
+            }
+          } else if (thisWeek == 7) {
+            // 当星期6时只需判断不是1号就可进行操作
+            if (dayRuleSup.value !== 1) {
+              DD--
+            } else {
+              DD += 2
+            }
+          }
+        } else if (dayRule.value == 'weekDay') {
+          // 如果指定了是星期几
+          // 获取当前日期是属于星期几
+          let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
+          // 校验当前星期是否在星期池(dayRuleSup)中
+          if (dayRuleSup.value.indexOf(thisWeek) < 0) {
+            // 如果到达最大值时
+            if (Di == DDate.length - 1) {
+              resetDay()
+              if (Mi == MDate.length - 1) {
+                resetMonth()
+                continue goYear
+              }
+              continue goMonth
+            }
+            continue
+          }
+        } else if (dayRule.value == 'assWeek') {
+          // 如果指定了是第几周的星期几
+          // 获取每月1号是属于星期几
+          let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
+          if (dayRuleSup.value[1] >= thisWeek) {
+            DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
+          } else {
+            DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
+          }
+        } else if (dayRule.value == 'lastWeek') {
+          // 如果指定了每月最后一个星期几
+          // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
+          if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+            while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+              DD--
+              thisDD = DD < 10 ? '0' + DD : DD
+            }
+          }
+          // 获取月末最后一天是星期几
+          let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
+          // 找到要求中最近的那个星期几
+          if (dayRuleSup.value < thisWeek) {
+            DD -= thisWeek - dayRuleSup.value
+          } else if (dayRuleSup.value > thisWeek) {
+            DD -= 7 - (dayRuleSup.value - thisWeek)
+          }
+        }
+        // 判断时间值是否小于10置换成"05"这种格式
+        DD = DD < 10 ? '0' + DD : DD
+
+        // 循环"时"数组
+        goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
+          let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
+
+          // 如果到达最大值时
+          if (nMin > mDate[mDate.length - 1]) {
+            resetMin()
+            if (hi == hDate.length - 1) {
+              resetHour()
+              if (Di == DDate.length - 1) {
+                resetDay()
+                if (Mi == MDate.length - 1) {
+                  resetMonth()
+                  continue goYear
+                }
+                continue goMonth
+              }
+              continue goDay
+            }
+            continue
+          }
+          // 循环"分"数组
+          goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
+            let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
+
+            // 如果到达最大值时
+            if (nSecond > sDate[sDate.length - 1]) {
+              resetSecond()
+              if (mi == mDate.length - 1) {
+                resetMin()
+                if (hi == hDate.length - 1) {
+                  resetHour()
+                  if (Di == DDate.length - 1) {
+                    resetDay()
+                    if (Mi == MDate.length - 1) {
+                      resetMonth()
+                      continue goYear
+                    }
+                    continue goMonth
+                  }
+                  continue goDay
+                }
+                continue goHour
+              }
+              continue
+            }
+            // 循环"秒"数组
+            goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
+              let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
+              // 添加当前时间(时间合法性在日期循环时已经判断)
+              if (MM !== '00' && DD !== '00') {
+                resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
+                nums++
+              }
+              // 如果条数满了就退出循环
+              if (nums == 5) break goYear
+              // 如果到达最大值时
+              if (si == sDate.length - 1) {
+                resetSecond()
+                if (mi == mDate.length - 1) {
+                  resetMin()
+                  if (hi == hDate.length - 1) {
+                    resetHour()
+                    if (Di == DDate.length - 1) {
+                      resetDay()
+                      if (Mi == MDate.length - 1) {
+                        resetMonth()
+                        continue goYear
+                      }
+                      continue goMonth
+                    }
+                    continue goDay
+                  }
+                  continue goHour
+                }
+                continue goMin
+              }
+            } //goSecond
+          } //goMin
+        }//goHour
+      }//goDay
+    }//goMonth
+  }
+  // 判断100年内的结果条数
+  if (resultArr.length == 0) {
+    resultList.value = ['没有达到条件的结果!']
+  } else {
+    resultList.value = resultArr
+    if (resultArr.length !== 5) {
+      resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
+    }
+  }
+  // 计算完成-显示结果
+  isShow.value = true
+}
+
+// 用于计算某位数字在数组中的索引
+const getIndex = (arr, value) => {
+  if (value <= arr[0] || value > arr[arr.length - 1]) {
+    return 0
+  } else {
+    for (let i = 0; i < arr.length - 1; i++) {
+      if (value > arr[i] && value <= arr[i + 1]) {
+        return i + 1
+      }
+    }
+  }
+}
+
+// 获取"年"数组
+const getYearArr = (rule, year) => {
+  dateArr.value[5] = getOrderArr(year, year + 100)
+  if (rule !== undefined) {
+    if (rule.indexOf('-') >= 0) {
+      dateArr.value[5] = getCycleArr(rule, year + 100, false)
+    } else if (rule.indexOf('/') >= 0) {
+      dateArr.value[5] = getAverageArr(rule, year + 100)
+    } else if (rule !== '*') {
+      dateArr.value[5] = getAssignArr(rule)
+    }
+  }
+}
+
+// 获取"月"数组
+const getMonthArr = (rule) => {
+  dateArr.value[4] = getOrderArr(1, 12)
+  if (rule.indexOf('-') >= 0) {
+    dateArr.value[4] = getCycleArr(rule, 12, false)
+  } else if (rule.indexOf('/') >= 0) {
+    dateArr.value[4] = getAverageArr(rule, 12)
+  } else if (rule !== '*') {
+    dateArr.value[4] = getAssignArr(rule)
+  }
+}
+
+// 获取"日"数组-主要为日期规则
+const getWeekArr = (rule) => {
+  // 只有当日期规则的两个值均为""时则表达日期是有选项的
+  if (dayRule.value == '' && dayRuleSup.value == '') {
+    if (rule.indexOf('-') >= 0) {
+      dayRule.value = 'weekDay'
+      dayRuleSup.value = getCycleArr(rule, 7, false)
+    } else if (rule.indexOf('#') >= 0) {
+      dayRule.value = 'assWeek'
+      let matchRule = rule.match(/[0-9]{1}/g)
+      dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
+      dateArr.value[3] = [1]
+      if (dayRuleSup.value[1] == 7) {
+        dayRuleSup.value[1] = 0
+      }
+    } else if (rule.indexOf('L') >= 0) {
+      dayRule.value = 'lastWeek'
+      dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
+      dateArr.value[3] = [31]
+      if (dayRuleSup.value == 7) {
+        dayRuleSup.value = 0
+      }
+    } else if (rule !== '*' && rule !== '?') {
+      dayRule.value = 'weekDay'
+      dayRuleSup.value = getAssignArr(rule)
+    }
+  }
+}
+
+// 获取"日"数组-少量为日期规则
+const getDayArr = (rule) => {
+  dateArr.value[3] = getOrderArr(1, 31)
+  dayRule.value = ''
+  dayRuleSup.value = ''
+  if (rule.indexOf('-') >= 0) {
+    dateArr.value[3] = getCycleArr(rule, 31, false)
+    dayRuleSup.value = 'null'
+  } else if (rule.indexOf('/') >= 0) {
+    dateArr.value[3] = getAverageArr(rule, 31)
+    dayRuleSup.value = 'null'
+  } else if (rule.indexOf('W') >= 0) {
+    dayRule.value = 'workDay'
+    dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
+    dateArr.value[3] = [dayRuleSup.value]
+  } else if (rule.indexOf('L') >= 0) {
+    dayRule.value = 'lastDay'
+    dayRuleSup.value = 'null'
+    dateArr.value[3] = [31]
+  } else if (rule !== '*' && rule !== '?') {
+    dateArr.value[3] = getAssignArr(rule)
+    dayRuleSup.value = 'null'
+  } else if (rule == '*') {
+    dayRuleSup.value = 'null'
+  }
+}
+
+// 获取"时"数组
+const getHourArr = (rule) => {
+  dateArr.value[2] = getOrderArr(0, 23)
+  if (rule.indexOf('-') >= 0) {
+    dateArr.value[2] = getCycleArr(rule, 24, true)
+  } else if (rule.indexOf('/') >= 0) {
+    dateArr.value[2] = getAverageArr(rule, 23)
+  } else if (rule !== '*') {
+    dateArr.value[2] = getAssignArr(rule)
+  }
+}
+
+// 获取"分"数组
+const getMinArr = (rule) => {
+  dateArr.value[1] = getOrderArr(0, 59)
+  if (rule.indexOf('-') >= 0) {
+    dateArr.value[1] = getCycleArr(rule, 60, true)
+  } else if (rule.indexOf('/') >= 0) {
+    dateArr.value[1] = getAverageArr(rule, 59)
+  } else if (rule !== '*') {
+    dateArr.value[1] = getAssignArr(rule)
+  }
+}
+
+// 获取"秒"数组
+const getSecondArr = (rule) => {
+  dateArr.value[0] = getOrderArr(0, 59)
+  if (rule.indexOf('-') >= 0) {
+    dateArr.value[0] = getCycleArr(rule, 60, true)
+  } else if (rule.indexOf('/') >= 0) {
+    dateArr.value[0] = getAverageArr(rule, 59)
+  } else if (rule !== '*') {
+    dateArr.value[0] = getAssignArr(rule)
+  }
+}
+
+// 根据传进来的min-max返回一个顺序的数组
+const getOrderArr = (min, max) => {
+  let arr = []
+  for (let i = min; i <= max; i++) {
+    arr.push(i)
+  }
+  return arr
+}
+
+// 根据规则中指定的零散值返回一个数组
+const getAssignArr = (rule) => {
+  let arr = []
+  let assiginArr = rule.split(',')
+  for (let i = 0; i < assiginArr.length; i++) {
+    arr[i] = Number(assiginArr[i])
+  }
+  arr.sort(compare)
+  return arr
+}
+
+// 根据一定算术规则计算返回一个数组
+const getAverageArr = (rule, limit) => {
+  let arr = []
+  let agArr = rule.split('/')
+  let min = Number(agArr[0])
+  let step = Number(agArr[1])
+  while (min <= limit) {
+    arr.push(min)
+    min += step
+  }
+  return arr
+}
+
+// 根据规则返回一个具有周期性的数组
+const getCycleArr = (rule, limit, status) => {
+  // status--表示是否从0开始(则从1开始)
+  let arr = []
+  let cycleArr = rule.split('-')
+  let min = Number(cycleArr[0])
+  let max = Number(cycleArr[1])
+  if (min > max) {
+    max += limit
+  }
+  for (let i = min; i <= max; i++) {
+    let add = 0
+    if (status == false && i % limit == 0) {
+      add = limit
+    }
+    arr.push(Math.round(i % limit + add))
+  }
+  arr.sort(compare)
+  return arr
+}
+
+// 比较数字大小(用于Array.sort)
+const compare = (value1, value2) => {
+  if (value2 - value1 > 0) {
+    return -1
+  } else {
+    return 1
+  }
+}
+
+// 格式化日期格式如:2017-9-19 18:04:33
+const formatDate = (value, type) => {
+  // 计算日期相关值
+  let time = typeof value == 'number' ? new Date(value) : value
+  let Y = time.getFullYear()
+  let M = time.getMonth() + 1
+  let D = time.getDate()
+  let h = time.getHours()
+  let m = time.getMinutes()
+  let s = time.getSeconds()
+  let week = time.getDay()
+  // 如果传递了type的话
+  if (type == undefined) {
+    return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
+  } else if (type == 'week') {
+    // 在quartz中 1为星期日
+    return week + 1
+  }
+}
+
+// 检查日期是否存在
+const checkDate = (value) => {
+  let time = new Date(value)
+  let format = formatDate(time)
+  return value === format
+}
+
+// Watchers
+watch(() => props.ex, expressionChange)
+
+// 生命周期钩子
+onMounted(() => {
+  // 初始化 获取一次结果
+  expressionChange()
+})
+</script>

+ 147 - 0
src/views/pms/video_center/product/components/Crontab/second.vue

@@ -0,0 +1,147 @@
+<template>
+  <el-form size="small">
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        秒,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+       周期从
+        <el-input-number v-model='cycle01' :max="58" :min="0"/>
+        -
+        <el-input-number v-model='cycle02' :max="59"
+                         :min="cycle01 ? cycle01 + 1 : 1"/>
+        秒
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+       从
+        <el-input-number v-model='average01' :max="58" :min="0"/>
+        秒开始,每
+        <el-input-number v-model='average02' :max="59 - average01 || 0" :min="1"/>
+        秒执行一次
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="可多选" 
+          clearable
+          multiple 
+          style="width:100%"
+        >
+          <el-option v-for="item in 60" :key="item" :value="item - 1">{{ item - 1 }}</el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-second'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  radioParent: {
+    type: Number,
+    default: 1
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 0, 58)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : 1, 59)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 0, 58)
+  const average02Val = props.check(average02.value, 1, 59 - average01Val || 0)
+  return average01Val + '/' + average02Val
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'second', '*', 'second')
+      break
+    case 2:
+      emit('update', 'second', cycleTotal.value)
+      break
+    case 3:
+      emit('update', 'second', averageTotal.value)
+      break
+    case 4:
+      emit('update', 'second', checkboxString.value)
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '2') {
+    emit('update', 'second', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'second', averageTotal.value)
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'second', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(checkboxString, checkboxChange)
+watch(() => props.radioParent, (newVal) => {
+  radioValue.value = newVal
+})
+</script>

+ 255 - 0
src/views/pms/video_center/product/components/Crontab/week.vue

@@ -0,0 +1,255 @@
+<template>
+  <el-form size='small'>
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+        周,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+        不指定
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        周期从星期
+        <el-select v-model="cycle01" clearable>
+          <el-option 
+            v-for="(item, index) of weekList" 
+            :key="index" 
+            :disabled="item.key === 1" 
+            :label="item.value"
+            :value="item.key"
+          >
+            {{ item.value }}
+          </el-option>
+        </el-select>
+        -
+        <el-select v-model="cycle02" clearable>
+          <el-option 
+            v-for="(item, index) of weekList" 
+            :key="index" 
+            :disabled="item.key < cycle01 && item.key !== 1" 
+            :label="item.value"
+            :value="item.key"
+          >
+            {{ item.value }}
+          </el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        第
+        <el-input-number v-model='average01' :max="4" :min="1"/>
+        周的星期
+        <el-select v-model="average02" clearable>
+          <el-option 
+            v-for="(item, index) of weekList" 
+            :key="index" 
+            :label="item.value" 
+            :value="item.key"
+          >
+            {{ item.value }}
+          </el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="5">
+        本月最后一个星期
+        <el-select v-model="weekday" clearable>
+          <el-option 
+            v-for="(item, index) of weekList" 
+            :key="index" 
+            :label="item.value" 
+            :value="item.key"
+          >
+            {{ item.value }}
+          </el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="6">
+        指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="星期一" 
+          clearable
+          multiple 
+          style="width:100%"
+        >
+          <el-option 
+            v-for="(item, index) of weekList" 
+            :key="index" 
+            :label="item.value"
+            :value="String(item.key)"
+          >
+            {{ item.value }}
+          </el-option>
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-week'
+})
+
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const radioValue = ref(2)
+const weekday = ref(2)
+const cycle01 = ref(2)
+const cycle02 = ref(3)
+const average01 = ref(1)
+const average02 = ref(2)
+const checkboxList = ref([])
+
+const weekList = ref([
+  {
+    key: 2,
+    value: '星期一'
+  },
+  {
+    key: 3,
+    value: '星期二'
+  },
+  {
+    key: 4,
+    value: '星期三'
+  },
+  {
+    key: 5,
+    value: '星期四'
+  },
+  {
+    key: 6,
+    value: '星期五'
+  },
+  {
+    key: 7,
+    value: '星期六'
+  },
+  {
+    key: 1,
+    value: '星期日'
+  }
+])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, 1, 7)
+  const cycle02Val = props.check(cycle02.value, 1, 7)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, 1, 4)
+  const average02Val = props.check(average02.value, 1, 7)
+  return average02Val + '#' + average01Val
+})
+
+// 最近的工作日(格式)
+const weekdayCheck = computed(() => {
+  const weekdayVal = props.check(weekday.value, 1, 7)
+  return weekdayVal
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str == '' ? '*' : str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  if (radioValue.value !== 2 && props.cron.day !== '?') {
+    emit('update', 'day', '?', 'week')
+  }
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'week', '*')
+      break
+    case 2:
+      emit('update', 'week', '?')
+      break
+    case 3:
+      emit('update', 'week', cycleTotal.value)
+      break
+    case 4:
+      emit('update', 'week', averageTotal.value)
+      break
+    case 5:
+      emit('update', 'week', weekday.value + 'L')
+      break
+    case 6:
+      emit('update', 'week', checkboxString.value)
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'week', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'week', averageTotal.value)
+  }
+}
+
+// 最近工作日值变化时
+const weekdayChange = () => {
+  if (radioValue.value == '5') {
+    emit('update', 'week', weekday.value + 'L')
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '6') {
+    emit('update', 'week', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(weekdayCheck, weekdayChange)
+watch(checkboxString, checkboxChange)
+</script>

+ 171 - 0
src/views/pms/video_center/product/components/Crontab/year.vue

@@ -0,0 +1,171 @@
+<template>
+  <el-form size="small">
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="1">
+       不填,允许的通配符[, - * /]
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="2">
+        每年
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="3">
+        周期从
+        <el-input-number v-model='cycle01' :max="2098" :min='fullYear'/>
+        -
+        <el-input-number v-model='cycle02' :max="2099" :min="cycle01 ? cycle01 + 1 : fullYear + 1"/>
+      </el-radio>
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="4">
+        从
+        <el-input-number v-model='average01' :max="2098"
+                         :min='fullYear'/>
+        年开始,每
+        <el-input-number v-model='average02' :max="2099 - average01 || fullYear"
+                         :min="1"/>
+        年执行一次
+      </el-radio>
+
+    </el-form-item>
+
+    <el-form-item>
+      <el-radio v-model='radioValue' :label="5">
+        指定
+        <el-select 
+          v-model="checkboxList" 
+          placeholder="可多选" 
+          clearable
+          multiple
+        >
+          <el-option 
+            v-for="item in 9" 
+            :key="item" 
+            :label="item - 1 + fullYear" 
+            :value="item - 1 + fullYear"
+          />
+        </el-select>
+      </el-radio>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup>
+import { ref, computed, watch, onMounted } from 'vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'crontab-year'
+})
+
+// 定义props
+const props = defineProps({
+  check: {
+    type: Function,
+    required: true
+  },
+  month: {
+    type: [String, Number],
+    default: ''
+  },
+  cron: {
+    type: Object,
+    required: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update'])
+
+// 数据定义
+const fullYear = ref(0)
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(0)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+
+// 计算属性
+// 计算两个周期值
+const cycleTotal = computed(() => {
+  const cycle01Val = props.check(cycle01.value, fullYear.value, 2098)
+  const cycle02Val = props.check(cycle02.value, cycle01Val ? cycle01Val + 1 : fullYear.value + 1, 2099)
+  return cycle01Val + '-' + cycle02Val
+})
+
+// 计算平均用到的值
+const averageTotal = computed(() => {
+  const average01Val = props.check(average01.value, fullYear.value, 2098)
+  const average02Val = props.check(average02.value, 1, 2099 - average01Val || fullYear.value)
+  return average01Val + '/' + average02Val
+})
+
+// 计算勾选的checkbox值合集
+const checkboxString = computed(() => {
+  let str = checkboxList.value.join()
+  return str
+})
+
+// 方法定义
+// 单选按钮值变化时
+const radioChange = () => {
+  switch (radioValue.value) {
+    case 1:
+      emit('update', 'year', '')
+      break
+    case 2:
+      emit('update', 'year', '*')
+      break
+    case 3:
+      emit('update', 'year', cycleTotal.value)
+      break
+    case 4:
+      emit('update', 'year', averageTotal.value)
+      break
+    case 5:
+      emit('update', 'year', checkboxString.value)
+      break
+  }
+}
+
+// 周期两个值变化时
+const cycleChange = () => {
+  if (radioValue.value == '3') {
+    emit('update', 'year', cycleTotal.value)
+  }
+}
+
+// 平均两个值变化时
+const averageChange = () => {
+  if (radioValue.value == '4') {
+    emit('update', 'year', averageTotal.value)
+  }
+}
+
+// checkbox值变化时
+const checkboxChange = () => {
+  if (radioValue.value == '5') {
+    emit('update', 'year', checkboxString.value)
+  }
+}
+
+// Watchers
+watch(radioValue, radioChange)
+watch(cycleTotal, cycleChange)
+watch(averageTotal, averageChange)
+watch(checkboxString, checkboxChange)
+
+// 生命周期钩子
+onMounted(() => {
+  // 仅获取当前年份
+  fullYear.value = Number(new Date().getFullYear())
+  cycle01.value = fullYear.value
+  average01.value = fullYear.value
+})
+</script>

+ 268 - 0
src/views/pms/video_center/product/components/ImageUpload/index.vue

@@ -0,0 +1,268 @@
+<template>
+  <div class="component-upload-image">
+    <el-upload 
+      ref="imageUploadRef" 
+      :action="uploadImgUrl" 
+      :before-upload="handleBeforeUpload"
+      :class="{ hide: fileList.length >= limit }"
+      :file-list="fileList" 
+      :headers="headers" 
+      :limit="limit" 
+      :multiple="multiple"
+      :on-error="handleUploadError" 
+      :on-exceed="handleExceed" 
+      :on-preview="handlePictureCardPreview" 
+      :on-remove="handleDelete"
+      :on-success="handleUploadSuccess" 
+      :show-file-list="true"
+      list-type="picture-card"
+    >
+      <el-icon><Plus /></el-icon>
+    </el-upload>
+
+    <!-- 上传提示 -->
+    <div v-if="showTip" class="el-upload__tip">
+      请上传
+      <template v-if="fileSize">
+        大小不超过
+        <b style="color: #f56c6c">{{ fileSize }}MB</b>
+      </template>
+      <template v-if="fileType">
+        格式为
+        <b style="color: #f56c6c">{{ fileType.join('/') }}</b>
+      </template>
+      的文件
+    </div>
+
+    <el-dialog 
+      title="预览" 
+      v-model="dialogVisible" 
+      append-to-body
+      width="800"
+    >
+      <img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto"/>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, watch } from 'vue'
+import { Plus } from '@element-plus/icons-vue'
+import { getAccessToken } from '@/utils/auth'
+import { ElMessage, ElLoading } from 'element-plus'
+
+
+// 定义组件名称
+defineOptions({
+  name: 'ImageUpload'
+})
+
+
+// 定义props
+const props = defineProps({
+  value: [String, Object, Array],
+  // 图片数量限制
+  limit: {
+    type: Number,
+    default: 5,
+  },
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 5,
+  },
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    default: () => ['png', 'jpg', 'jpeg'],
+  },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true,
+  },
+  // 是否支持多选文件
+  multiple: {
+    type: Boolean,
+    default: true,
+  },
+})
+
+// 定义emits
+const emit = defineEmits(['input'])
+
+// Refs
+const imageUploadRef = ref(null)
+
+// 数据定义
+const number = ref(0)
+const uploadList = ref([])
+const dialogImageUrl = ref('')
+const dialogVisible = ref(false)
+const hideUpload = ref(false)
+const baseUrl = import.meta.env.VITE_BASE_URL
+const uploadImgUrl = import.meta.env.VITE_BASE_URL + '/common/upload' // 上传的图片服务器地址
+const headers = {
+  Authorization: 'Bearer ' + getAccessToken(),
+}
+const fileList = ref([])
+
+// 计算属性
+// 是否显示提示
+const showTip = computed(() => {
+  return props.isShowTip && (props.fileType || props.fileSize)
+})
+
+// Watchers
+watch(
+  () => props.value,
+  (val) => {
+    if (val) {
+      // 首先将值转为数组
+      const list = Array.isArray(val) ? val : props.value.split(',')
+      // 然后将数组转为对象数组
+      fileList.value = list.map((item) => {
+        if (typeof item === 'string') {
+          if (item.indexOf(baseUrl) === -1) {
+            item = { name: baseUrl + item, url: baseUrl + item }
+          } else {
+            item = { name: item, url: item }
+          }
+        }
+        return item
+      })
+    } else {
+      fileList.value = []
+      return []
+    }
+  },
+  {
+    deep: true,
+    immediate: true,
+  }
+)
+
+// 方法定义
+// 上传前loading加载
+const handleBeforeUpload = (file) => {
+  console.log('handleBeforeUpload>>>>>>>>>>>>>>>>>>>', file)
+  let isImg = false
+  if (props.fileType.length) {
+    let fileExtension = ''
+    if (file.name.lastIndexOf('.') > -1) {
+      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1)
+    }
+    isImg = props.fileType.some((type) => {
+      if (file.type.indexOf(type) > -1) return true
+      if (fileExtension && fileExtension.indexOf(type) > -1) return true
+      return false
+    })
+  } else {
+    isImg = file.type.indexOf('image') > -1
+  }
+
+  if (!isImg) {
+    ElMessage.error(
+      '文件格式不正确, 请上传' + 
+      `${props.fileType.join('/')}` + 
+     '格式文件'
+    )
+    return false
+  }
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize
+    if (!isLt) {
+      ElMessage.error('上传头像图片大小不能超过' + `${props.fileSize} MB!`)
+      return false
+    }
+  }
+
+  number.value++
+}
+
+// 文件个数超出
+const handleExceed = () => {
+  ElMessage.error(
+    '上传文件数量不能超过' + 
+    `${props.limit}` + 
+    '个'
+  )
+}
+
+// 上传成功回调
+const handleUploadSuccess = (res, file) => {
+  if (res.code === 200) {
+    uploadList.value.push({ name: res.fileName, url: res.fileName })
+    uploadedSuccessfully()
+  } else {
+    number.value--
+    // ElMessage.closeLoading()
+    ElMessage.error(res.msg)
+    imageUploadRef.value.handleRemove(file)
+    uploadedSuccessfully()
+  }
+}
+
+// 删除图片
+const handleDelete = (file) => {
+  const findex = fileList.value.map((f) => f.name).indexOf(file.name)
+  if (findex > -1) {
+    fileList.value.splice(findex, 1)
+    emit('input', listToString(fileList.value))
+  }
+}
+
+// 上传失败
+const handleUploadError = () => {
+  ElMessage.error('上传图片失败,请重试')
+  // ElMessage.closeLoading()
+}
+
+// 上传结束处理
+const uploadedSuccessfully = () => {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.concat(uploadList.value)
+    uploadList.value = []
+    number.value = 0
+    emit('input', listToString(fileList.value))
+    // ElMessage.closeLoading()
+  }
+}
+
+// 预览
+const handlePictureCardPreview = (file) => {
+  dialogImageUrl.value = file.url
+  dialogVisible.value = true
+}
+
+// 对象转成指定字符串分隔
+const listToString = (list, separator) => {
+  let strs = ''
+  separator = separator || ','
+  for (let i in list) {
+    if (list[i].url) {
+      strs += list[i].url.replace(baseUrl, '') + separator
+    }
+  }
+  return strs != '' ? strs.substr(0, strs.length - 1) : ''
+}
+</script>
+
+<style scoped lang="scss">
+// .el-upload--picture-card 控制加号部分
+::v-deep.hide .el-upload--picture-card {
+  display: none;
+}
+
+// 去掉动画效果
+::v-deep .el-list-enter-active,
+::v-deep .el-list-leave-active {
+  transition: all 0s;
+}
+
+::v-deep .el-list-enter,
+.el-list-leave-active {
+  opacity: 0;
+  transform: translateY(0);
+}
+</style>

+ 129 - 0
src/views/pms/video_center/product/components/RightToolbar/index.vue

@@ -0,0 +1,129 @@
+<template>
+  <div class="top-right-btn" :style="style">
+    <el-row>
+      <el-tooltip 
+        class="item" 
+        effect="dark"
+        v-if="search"
+        :content="showSearch ? '隐藏搜索' : '显示搜索'" 
+        placement="top"
+      >
+        <el-button size="small" circle :icon="Search" @click="toggleSearch()" />
+      </el-tooltip>
+      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
+        <el-button size="small" circle :icon="Refresh" @click="refresh()" />
+      </el-tooltip>
+      <el-tooltip 
+        class="item" 
+        effect="dark" 
+        content="显隐列" 
+        placement="top"
+        v-if="columns"
+      >
+        <el-button size="small" circle :icon="Menu" @click="showColumn()" />
+      </el-tooltip>
+    </el-row>
+    <el-dialog :title="title" v-model="open" append-to-body>
+      <el-transfer 
+        :titles="['显示', '隐藏']"
+        v-model="value" 
+        :data="columns" 
+        @change="dataChange"
+      />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { Search, Refresh, Menu } from '@element-plus/icons-vue'
+
+// 组件名称
+defineOptions({
+  name: "RightToolbar"
+})
+
+// 定义 props
+const props = defineProps({
+  showSearch: {
+    type: Boolean,
+    default: true,
+  },
+  columns: {
+    type: Array,
+  },
+  search: {
+    type: Boolean,
+    default: true,
+  },
+  gutter: {
+    type: Number,
+    default: 10,
+  },
+})
+
+// 定义 emits
+const emit = defineEmits(['update:showSearch', 'queryTable'])
+
+// 数据状态
+const value = ref([])
+const title = ref('显示/隐藏')
+const open = ref(false)
+
+// 计算属性
+const style = computed(() => {
+  const ret = {};
+  if (props.gutter) {
+    ret.marginRight = `${props.gutter / 2}px`;
+  }
+  return ret;
+})
+
+// 组件挂载时执行
+onMounted(() => {
+  // 显隐列初始默认隐藏列
+  for (let item in props.columns) {
+    if (props.columns[item].visible === false) {
+      value.value.push(parseInt(item));
+    }
+  }
+})
+
+// 搜索
+const toggleSearch = () => {
+  emit("update:showSearch", !props.showSearch);
+}
+
+// 刷新
+const refresh = () => {
+  emit("queryTable");
+}
+
+// 右侧列表元素变化
+// 右侧列表元素变化
+const dataChange = (data) => {
+  columnsCopy.value.forEach((item, index) => {
+    item.visible = !data.includes(index);
+  });
+  // 通知父组件更新
+  emit('update:columns', columnsCopy.value)
+}
+
+// 打开显隐列dialog
+const showColumn = () => {
+  open.value = true;
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.el-transfer__button) {
+  border-radius: 50%;
+  padding: 12px;
+  display: block;
+  margin-left: 0px;
+}
+
+:deep(.el-transfer__button:first-child) {
+  margin-bottom: 10px;
+}
+</style>

+ 131 - 0
src/views/pms/video_center/product/components/batchImportModbus.vue

@@ -0,0 +1,131 @@
+<template>
+  <!-- 批量导入寄存器点 -->
+  <el-dialog :title="upload.title" :visible.sync="upload.importDeviceDialog" width="30%" append-to-body
+    v-loading="loading">
+    <div style="margin-top: -55px;text-align:center">
+      <el-divider style="margin-top: -30px"></el-divider>
+      <el-form label-position="top" :model="importForm" ref="importForm" :rules="importRules">
+        <el-form-item :label="$t('uploadFile')" prop="fileList">
+          <el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers"
+                     v-model="importForm.fileList" :action="`${upload.url}?productId=${productId}&type=${justiceSelect=='isSelectData'?2:1}`"
+                     :disabled="upload.isUploading" :on-progress="handleFileUploadProgress"
+                     :on-error="handleError"
+                     :on-success="handleFileSuccess" :auto-upload="false" :on-change="handleChange"
+                     :on-remove="handleRemove" drag>
+            <i class="el-icon-upload"></i>
+            <div class="el-upload__text">{{ $t('dragFileTips') }}<em>{{ $t('clickFileTips') }}</em></div>
+            <div class="el-upload__tip" slot="tip">
+              <div style="margin-top: 10px;">
+                <span>{{ $t('device.batch-import-dialog.850870-5') }}</span>
+              </div>
+            </div>
+          </el-upload>
+          <el-link type="primary" :underline="false" style="font-size:14px;vertical-align: baseline;"
+            @click="importTemplate"><i class="el-icon-download"></i>{{ $t('device.batch-import-dialog.850870-6')
+            }}</el-link>
+        </el-form-item>
+      </el-form>
+    </div>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="submitFileForm">{{ $t('confirm') }}</el-button>
+      <el-button @click="upload.importDeviceDialog = false">{{ $t('cancel') }}</el-button>
+    </div>
+  </el-dialog>
+</template>
+<script>
+import { listProduct } from "@/api/iot/product";
+import { getToken } from "@/utils/auth";
+export default {
+  name: 'batchImport',
+  props: {
+    productId: {
+      type: Number,
+      default: 0
+    },
+    justiceSelect: {
+      type: String,
+      default: 'isSelectData'
+    }
+  },
+  data() {
+    return {
+      type: 1,
+      //导入表单
+      importForm: {
+        productId: null,
+        fileList: [],
+      },
+      file: null,
+      // 批量导入参数
+      upload: {
+        // 是否显示弹出层
+        importDeviceDialog: false,
+        // 弹出层标题
+        title: this.$t('batchImport'),
+        // 是否禁用上传
+        isUploading: false,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + "/modbus/config/importModbus"
+      },
+      // 批量导入表单校验
+      importRules: {
+        fileList: [
+          { required: true, message: this.$t('plzUploadFile'), trigger: 'change' }
+        ]
+      },
+      //加载
+      loading: false
+    };
+  },
+  methods: {
+    /** 下载模板操作 */
+    importTemplate() {
+      const type = this.justiceSelect == 'isSelectData' ? 2 : 1;
+      const name = this.justiceSelect == 'isSelectData' ? this.$t('product.components.batchImportModbus.745343-0') : this.$t('product.components.batchImportModbus.745343-1');
+      this.download('/modbus/config/modbusTemplate?type=' + type, {},
+        `${name}_${new Date().getTime()}.xlsx`);
+    },
+    // 选择文件后给表单验证的prop字段赋值, 并且清除该字段的校验
+    handleChange(file, fileList) {
+      this.importForm.fileList = fileList;
+      // 防止用户打开了文件选择框之后不选择文件而出现效验失败
+      if (this.importForm.fileList) {
+        this.$refs.importForm.clearValidate('fileList');
+      }
+    },
+    // 删除文件后重新校验该字段
+    handleRemove(file, fileList) {
+      this.importForm.fileList = fileList;
+      this.$refs.importForm.validateField('fileList');
+    },
+    // 文件上传中处理
+    handleFileUploadProgress(event, file, fileList) {
+      this.upload.isUploading = true;
+    },
+    //上传失败
+    handleError(err, file, fileList){
+      this.upload.importDeviceDialog = false;
+      this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + err.msg + "</div>", this.$t('device.allot-import-dialog.060657-17'), { dangerouslyUseHTMLString: true });
+    },
+    // 文件上传成功处理
+    handleFileSuccess(response, file, fileList) {
+      this.upload.importDeviceDialog = false;
+      this.upload.isUploading = false;
+      this.loading = false;
+      this.$refs.upload.clearFiles();
+      this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", this.$t('device.allot-import-dialog.060657-17'), { dangerouslyUseHTMLString: true });
+    },
+    // 提交上传文件
+    submitFileForm() {
+      this.$refs['importForm'].validate((valid) => {
+        if (valid) {
+          this.upload.isUploading = true;
+          this.$refs.upload.submit();
+        }
+      });
+    },
+  },
+};
+</script>

+ 128 - 0
src/views/pms/video_center/product/components/batchImportThingsModel.vue

@@ -0,0 +1,128 @@
+<template>
+	<!-- 批量导入寄存器点 -->
+	<el-dialog :title="upload.title" :visible.sync="upload.importDeviceDialog" width="30%" append-to-body v-loading="loading">
+			<div style="margin-top: -55px;text-align:center">
+					<el-divider style="margin-top: -30px"></el-divider>
+					<el-form label-position="top" :model="importForm" ref="importForm" :rules="importRules">
+						<el-form-item :label="$t('uploadFile')" prop="fileList">
+								<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers"
+										v-model="importForm.fileList" :action="`${upload.url}?productId=${productId}`"
+										:disabled="upload.isUploading" :on-progress="handleFileUploadProgress"
+                    :on-error="handleError"
+										:on-success="handleFileSuccess" :auto-upload="false" :on-change="handleChange"
+										:on-remove="handleRemove" drag>
+										<i class="el-icon-upload"></i>
+										<div class="el-upload__text">{{ $t('dragFileTips') }}<em>{{ $t('clickFileTips') }}</em></div>
+										<div class="el-upload__tip" slot="tip">
+												<div style="margin-top: 10px;">
+														<span>{{ $t('device.batch-import-dialog.850870-5') }}</span>
+												</div>
+										</div>
+								</el-upload>
+								<el-link type="primary" :underline="false" style="font-size:14px;vertical-align: baseline;"
+										@click="importTemplate"><i class="el-icon-download"></i>{{ $t('device.batch-import-dialog.850870-6') }}</el-link>
+						</el-form-item>
+					</el-form>
+			</div>
+			<div slot="footer" class="dialog-footer">
+					<el-button type="primary" @click="submitFileForm">{{ $t('confirm') }}</el-button>
+					<el-button @click="upload.importDeviceDialog = false">{{ $t('cancel') }}</el-button>
+			</div>
+	</el-dialog>
+</template>
+<script>
+import { listProduct } from "@/api/iot/product";
+import { getToken } from "@/utils/auth";
+export default {
+	name: 'import-thingModel',
+	props: {
+		productId: {
+			type: Number,
+			default: 0
+		},
+		justiceSelect:{
+			type: String,
+			default:'isSelectData'
+		}
+	},
+	data() {
+		return {
+				type: 1,
+				//导入表单
+				importForm: {
+						productId: null,
+						fileList: [],
+				},
+				file: null,
+				// 批量导入参数
+      // 批量导入参数
+      upload: {
+        // 是否显示弹出层
+        importDeviceDialog: false,
+        // 弹出层标题
+        title: this.$t('batchImport'),
+        // 是否禁用上传
+        isUploading: false,
+        // 设置上传的请求头部
+        headers: { Authorization: "Bearer " + getToken() },
+        // 上传的地址
+        url: process.env.VUE_APP_BASE_API + "/iot/model/importData"
+      },
+				// 批量导入表单校验
+				importRules: {
+						fileList: [
+								{ required: true, message: this.$t('plzUploadFile'), trigger: 'change' }
+						]
+				},
+				//加载
+				loading:false
+		};
+	},
+	methods: {
+			/** 下载模板操作 */
+			importTemplate() {
+				this.download('/iot/model/temp', {},
+						`${new Date().getTime()}.xlsx`);
+			},
+			// 选择文件后给表单验证的prop字段赋值, 并且清除该字段的校验
+			handleChange(file, fileList) {
+					this.importForm.fileList = fileList;
+					// 防止用户打开了文件选择框之后不选择文件而出现效验失败
+					if (this.importForm.fileList) {
+							this.$refs.importForm.clearValidate('fileList');
+					}
+			},
+			// 删除文件后重新校验该字段
+			handleRemove(file, fileList) {
+					this.importForm.fileList = fileList;
+					this.$refs.importForm.validateField('fileList');
+			},
+			// 文件上传中处理
+			handleFileUploadProgress(event, file, fileList) {
+					this.upload.isUploading = true;
+			},
+      //上传失败
+      handleError(err, file, fileList){
+        this.upload.importDeviceDialog = false;
+        this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + err.msg + "</div>", this.$t('device.allot-import-dialog.060657-17'), { dangerouslyUseHTMLString: true });
+      },
+			// 文件上传成功处理
+			handleFileSuccess(response, file, fileList) {
+					this.upload.importDeviceDialog = false;
+					this.upload.isUploading = false;
+					this.loading = false;
+					this.$refs.upload.clearFiles();
+					this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", this.$t('device.allot-import-dialog.060657-17'), { dangerouslyUseHTMLString: true });
+			},
+			// 提交上传文件
+			submitFileForm() {
+					this.$refs['importForm'].validate((valid) => {
+							if (valid) {
+                this.upload.isUploading = true;
+								this.$refs.upload.submit();
+							}
+					});
+			},
+	},
+};
+</script>

+ 737 - 0
src/views/pms/video_center/product/index.vue

@@ -0,0 +1,737 @@
+<template>
+  <div class="product normal-form" style="padding: 6px">
+    <el-card style="margin: 20px">
+      <el-form
+        ref="queryFormRef"
+        :inline="true"
+        :model="queryParams"
+        label-width="68px"
+        style="margin-bottom: -20px"
+      >
+        <el-form-item :label="t('product.index091251-0')" prop="productName">
+          <el-input
+            v-model="queryParams.productName"
+            :placeholder="t('product.index091251-1')"
+            clearable
+            size="small"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('product.index091251-2')" prop="categoryName">
+          <el-input
+            v-model="queryParams.categoryName"
+            :placeholder="t('product.index091251-3')"
+            clearable
+            size="small"
+            @keyup.enter="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item :label="t('product.index091251-4')" prop="status">
+          <el-select
+            v-model="queryParams.status"
+            :placeholder="t('product.index091251-5')"
+            clearable
+            size="small"
+            style="width: 150px;"
+          >
+            <el-option
+              v-for="dict in iot_product_status"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item>
+          <el-button :icon="Search" size="small" type="primary" @click="handleQuery">{{
+            t('product.index091251-6')
+          }}</el-button>
+          <el-button :icon="Refresh" size="small" @click="resetQuery">{{
+            t('product.index091251-7')
+          }}</el-button>
+          <el-checkbox
+            v-model="queryParams.showSenior"
+            style="margin: 0px 10px"
+            @change="handleQuery"
+            >{{ t('product.index091251-8') }}</el-checkbox
+          >
+          <el-tooltip :content="t('product.index091251-9')" placement="top">
+            <el-icon><InfoFilled /></el-icon>
+          </el-tooltip>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <el-card class="main-card" shadow="never" style="margin: 20px">
+      <div class="card-toolbar mb8">
+        <el-button
+         
+          type="primary"
+          size="small"
+          :icon="Plus"
+          @click="handleEditProduct(0)"
+          >新增</el-button
+        >
+        <el-radio-group v-model="showType" class="float-right ml-10" plain size="small">
+          <el-radio-button label="card"
+            ><el-icon><Menu /></el-icon
+          ></el-radio-button>
+          <el-radio-button label="list"
+            ><el-icon><Fold /></el-icon
+          ></el-radio-button>
+        </el-radio-group>
+      </div>
+      <template v-if="total > 0">
+        <el-row
+          v-if="showType == 'card'"
+          v-loading="loading"
+          type="flex"
+          :gutter="30"
+          style="flex-wrap: wrap"
+        >
+          <el-col
+            v-for="(item, index) in productList"
+            :key="index"
+            :xs="24"
+            :sm="12"
+            :md="8"
+            :lg="6"
+            :xl="6"
+            style="margin-bottom: 30px; text-align: center"
+          >
+            <el-card :body-style="{ padding: '20px' }" shadow="hover" class="card-item">
+              <el-row type="flex" :gutter="10" justify="space-between">
+                <el-col :span="16" style="text-align: left">
+                  <el-link
+                    type=""
+                    :underline="false"
+                    style="font-weight: bold; font-size: 16px; line-height: 32px"
+                    @click="handleDeviceDetail(item)"
+                  >
+                    <el-tooltip
+                      class="item"
+                      effect="dark"
+                      content="分享的设备"
+                      placement="top-start"
+                    >
+                      <svg-icon
+                        v-if="item.isOwner != 1"
+                        class="mr-5"
+                        iconClass="share"
+                        className="share"
+                        style="font-size: 20px; vertical-align: -6px"
+                      />
+                    </el-tooltip>
+                    <svg-icon v-if="item.isOwner == 1" icon-class="device" />
+                    <b class="card-item__title" @click="handleDeviceDetail(item)">{{
+                      item.productName
+                    }}</b>
+                    <el-tag type="info">{{ item.tenantName }}</el-tag>
+                  </el-link>
+                </el-col>
+                <el-col type="flex" :span="8">
+                  <li
+                    v-if="item.status == 2"
+                    class="table-status success"
+                    style="text-align: right"
+                  >
+                    <span class="status-content">已发布</span>
+                  </li>
+                  <li v-else class="table-status" style="text-align: right">
+                    <span class="status-content">未发布</span>
+                  </li>
+                </el-col>
+              </el-row>
+              <el-row :gutter="10">
+                <el-col :span="17">
+                  <el-descriptions
+                    :column="1"
+                    size="small"
+                    style="margin-top: 10px; white-space: nowrap"
+                  >
+                    <el-descriptions-item label="所属分类">
+                      <el-link type="primary" :underline="false">{{ item.categoryName }}</el-link>
+                    </el-descriptions-item>
+                    <el-descriptions-item label="产品类型">
+                      <dict-tag :options="iot_device_type" :value="item.deviceType" />
+                    </el-descriptions-item>
+                    <el-descriptions-item label="联网方式">
+                      <dict-tag
+                        :options="iot_network_method"
+                        :value="item.networkMethod"
+                      />
+                    </el-descriptions-item>
+                    <el-descriptions-item label="设备授权">
+                      <el-tag v-if="item.isAuthorize == 1" type="success" size="small"
+                        >已启用</el-tag
+                      >
+                      <el-tag v-else type="info" size="small">未启用</el-tag>
+                    </el-descriptions-item>
+                  </el-descriptions>
+                </el-col>
+                <el-col :span="7">
+                  <div style="margin-top: 10px">
+                    <el-image
+                      v-if="item.imgUrl != null && item.imgUrl != ''"
+                      style="width: 100%; height: auto"
+                      lazy
+                      :preview-src-list="[baseUrl + item.imgUrl]"
+                      :src="baseUrl + item.imgUrl"
+                      fit="cover"
+                    />
+                    <el-image
+                      v-else-if="item.deviceType == 2"
+                      style="width: 100%; height: auto"
+                      :preview-src-list="[gatewaySvg]"
+                      :src="gatewaySvg"
+                      fit="cover"
+                    />
+                    <el-image
+                      v-else-if="item.deviceType == 3"
+                      style="width: 100%; height: auto"
+                      :preview-src-list="[videoSvg]"
+                      :src="videoSvg"
+                      fit="cover"
+                    />
+                    <el-image
+                      v-else
+                      style="width: 100%; height: auto"
+                      :preview-src-list="[productSvg]"
+                      :src="productSvg"
+                      fit="cover"
+                    />
+                  </div>
+                </el-col>
+              </el-row>
+              <div class="card-item__footer">
+                <el-button
+                  
+                  size="small"
+                  class="detail-btn"
+                  @click="handleEditProduct(item)"
+                  >查看详情</el-button
+                >
+                <el-button
+                  v-if="item.status == 1 && item.isOwner != 0"
+                 
+                  size="small"
+                  class="delete-btn"
+                  @click="handleDelete(item)"
+                  >删除</el-button
+                >
+                <el-button
+                  v-if="item.status == 2 && item.isOwner != 0"
+                  
+                  size="small"
+                  class="success-btn"
+                  :disabled="item.isAuthorize != 1"
+                  @click="handleDeviceAuthorize(item)"
+                >
+                  设备授权
+                </el-button>
+                <el-button
+                  
+                  size="small"
+                  type="warning"
+                  @click="handleViewDevice(item.productId)"
+                  >查看设备</el-button
+                >
+              </div>
+            </el-card>
+          </el-col>
+        </el-row>
+        <el-table v-else v-loading="loading" class="base-table" :data="productList" :border="false">
+          <el-table-column
+            label="图标"
+            align="center"
+            header-align="center"
+            prop="productId"
+            width="72"
+          >
+            <template #default="{ row }">
+              <el-image
+                v-if="row.imgUrl != null && row.imgUrl != ''"
+                style="width: 24px; height: 24px"
+                lazy
+                :preview-src-list="[baseUrl + row.imgUrl]"
+                :src="baseUrl + row.imgUrl"
+                fit="cover"
+              />
+              <el-image
+                v-else-if="row.deviceType == 2"
+                style="width: 24px; height: 24px"
+                :preview-src-list="[gatewaySvg]"
+                :src="gatewaySvg"
+                fit="cover"
+              />
+              <el-image
+                v-else-if="row.deviceType == 3"
+                style="width: 24px; height: 24px"
+                :preview-src-list="[videoSvg]"
+                :src="videoSvg"
+                fit="cover"
+              />
+              <el-image
+                v-else
+                style="width: 24px; height: 24px"
+                :preview-src-list="[productSvg]"
+                :src="productSvg"
+                fit="cover"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="名称"
+            align="left"
+            header-align="left"
+            prop="productName"
+            min-width="120"
+          />
+          <el-table-column label="所属分类" align="left" prop="categoryName" min-width="120" />
+          <el-table-column label="所属类型" align="left" prop="deviceType" min-width="120">
+            <template #default="{ row }">
+              <dict-tag :options="iot_device_type" :value="row.deviceType" />
+            </template>
+          </el-table-column>
+          <el-table-column label="联网方式" align="left" prop="networkMethod" min-width="120">
+            <template #default="{ row }">
+              <dict-tag :options="iot_network_method" :value="row.networkMethod" />
+            </template>
+          </el-table-column>
+          <el-table-column label="设备授权" align="center" prop="isAuthorize" min-width="120">
+            <template #default="{ row }">
+              <el-tag v-if="row.isAuthorize == 1" type="success" size="small">已启用</el-tag>
+              <el-tag v-else type="info" size="small">未启用</el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="发布状态" align="center" prop="status" min-width="120">
+            <template #default="{ row }">
+              <li v-if="row.status == 2" class="table-status success"
+                ><span class="status-content">已发布</span></li
+              >
+              <li v-else class="table-status"><span class="status-content">未发布</span></li>
+            </template>
+          </el-table-column>
+          <el-table-column
+            label="操作"
+            align="left"
+            class-name="small-padding fixed-width"
+            fixed="right"
+            width="300"
+          >
+            <template #default="{ row }">
+              <el-button
+                
+                type="primary"
+                plain
+                style="padding: 5px"
+                @click="handleEditProduct(row)"
+                >查看详情</el-button
+              >
+              <el-button
+                
+                type="primary"
+                plain
+                style="padding: 5px"
+                @click="handleViewDevice(row.productId)"
+                >查看设备</el-button
+              >
+              <el-button
+                v-if="row.status == 2 && row.isOwner != 0"
+               
+                type="success"
+                plain
+                style="padding: 5px"
+                @click="handleDeviceAuthorize(row)"
+                >设备授权</el-button
+              >
+              <el-button
+                v-if="row.status == 1 && row.isOwner != 0"
+                
+                type="danger"
+                plain
+                style="padding: 5px"
+                @click="handleDelete(row)"
+                >删除</el-button
+              >
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+      <el-empty v-else description="暂无数据,请添加产品" />
+      <el-pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:current-page="queryParams.pageNum"
+        v-model:page-size="queryParams.pageSize"
+        :page-sizes="[12, 24, 36, 60]"
+        layout="total, sizes, prev, pager, next, jumper"
+        @update:current-page="getList"
+        @update:page-size="getList"
+      />
+
+     
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { listProduct, delProduct, changeProductStatus, deviceCount } from '@/api/pms/video/product'
+import { delSipconfigByProductId } from '@/api/pms/video/sipConfig'
+// import { checkPermi } from '@/utils/permission'
+import { getAccessToken } from '@/utils/auth'
+import { Search, Refresh, InfoFilled, Menu, Fold, Plus } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { download } from '@/config/axios/service'
+import productSvg from '@/assets/imgs/product.svg'
+import videoSvg from '@/assets/imgs/video.svg'
+import gatewaySvg from '@/assets/imgs/gateway.svg'
+
+const { t } = useI18n() // 国际化
+
+// 定义组件名称
+defineOptions({
+  name: 'Product'
+})
+
+// 获取实例和路由
+const router = useRouter()
+const route = useRoute()
+
+// 字典定义
+const iot_product_status = []
+const iot_device_type = []
+const iot_network_method = []
+const iot_vertificate_method = []
+const iot_device_chip = []
+
+// 响应式数据
+const queryFormRef = ref(null)
+const openTemp = ref(false)
+const loading = ref(true)
+const total = ref(0)
+const productList = ref([])
+const title = ref('')
+const open = ref(false)
+const isShowScada = ref(false)
+const showType = ref('card')
+const uniqueId = ref(0)
+const baseUrl = ref(import.meta.VITE_BASE_URL)
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 12,
+  showSenior: true,
+  productName: null,
+  categoryId: null,
+  categoryName: null,
+  tenantId: null,
+  tenantName: null,
+  isSys: null,
+  status: null,
+  deviceType: null,
+  networkMethod: null
+})
+
+const form = reactive({})
+const upload = reactive({
+  open: false,
+  title: '',
+  headers: {
+    Authorization: 'Bearer ' + getAccessToken()
+  },
+  url: import.meta.VITE_BASE_URL + '/iot/product/temp'
+})
+
+// 导入模板
+function importTemplate() {
+  download('iot/product/temp-json', {}, `产品模板${new Date().getTime()}.xlsx`)
+}
+
+
+// 文件上传中处理
+function handleFileUploadProgress(event, file, fileList) {
+  upload.isUploading = true
+}
+
+
+// 查询产品列表
+function getList() {
+  loading.value = true
+  listProduct(queryParams).then((response) => {
+    console.log(response)
+    productList.value = response.list
+    total.value = response.total
+    loading.value = false
+  })
+}
+
+// 同步获取产品下的设备数量
+function getDeviceCountByProductId(productId) {
+  return new Promise((resolve, reject) => {
+    deviceCount(productId)
+      .then((res) => {
+        resolve(res)
+      })
+      .catch((error) => {
+        reject(error)
+      })
+  })
+}
+
+// 更新产品状态
+async function handleChangeProductStatus(productId, status, deviceType) {
+  let message = t('product.index091251-30')
+  if (status == 2) {
+    // 发布
+    // let hasPermission = checkPermi(['iot:product:add'])
+    // if (!hasPermission) {
+    //   ElMessage.error(t('product.index091251-31'))
+    //   return
+    // }
+    message = t('product.index091251-32')
+  } else if (status == 1) {
+    // 取消发布
+    // let hasPermission = checkPermi(['iot:product:edit'])
+    // if (!hasPermission) {
+    //   ElMessage.error(t('product.index091251-31'))
+    //   return
+    // }
+    let result = await getDeviceCountByProductId(productId)
+    if (result.data > 0) {
+      message = t('product.index091251-33', [result.data])
+    }
+  }
+  ElMessageBox.confirm(message, t('product.index091251-34'), {
+      confirmButtonText: t('product.index091251-35'),
+      cancelButtonText: t('product.index091251-36'),
+      type: 'warning'
+    })
+    .then(() => {
+      let data = {}
+      data.productId = productId
+      data.status = status
+      data.deviceType = deviceType
+      changeProductStatus(data)
+        .then((response) => {
+          getList()
+          ElMessage.success(response.msg)
+        })
+        .catch(() => {})
+    })
+    .catch(() => {})
+}
+
+function handleTemp() {
+  openTemp.value = true
+}
+
+// 查看设备按钮操作
+function handleViewDevice(productId) {
+  router.push({
+    path: '/iotdev/iot/device',
+    query: {
+      t: Date.now(),
+      productId: productId
+    }
+  })
+}
+
+
+// 取消按钮
+function cancel() {
+  open.value = false
+  reset()
+}
+
+// 搜索按钮操作
+function handleQuery() {
+  queryParams.pageNum = 1
+  getList()
+}
+
+// 重置按钮操作
+function resetQuery() {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+
+// 删除按钮操作
+function handleDelete(row) {
+  const productIds = row.productId || 0
+  let msg = ''
+   ElMessageBox
+    .confirm(t('product.index091251-39', [productIds]))
+    .then(function () {
+      // 删除SIP配置
+      delSipconfigByProductId(productIds).then((response) => {})
+      return delProduct(productIds).then((response) => {
+        msg = response.msg
+      })
+    })
+    .then(() => {
+      getList()
+      ElMessage.success(msg)
+    })
+    .catch(() => {})
+}
+
+// 修改按钮操作
+function handleEditProduct(row) {
+  let productId = 0
+  if (row != 0) {
+    productId = row.productId || 0
+  }
+  router.push({
+    path: '/videocenter/product/product-edit',
+    query: {
+      productId: productId,
+      pageNum: queryParams.pageNum
+    }
+  })
+}
+
+// 重置表单
+function reset() {
+  form.datatype = undefined
+}
+
+// 查看设备详情
+function handleDeviceDetail(item) {
+  // 实现查看详情逻辑
+}
+
+// 设备授权操作
+function handleDeviceAuthorize(item) {
+  // 实现设备授权逻辑
+}
+
+// 生命周期钩子
+onMounted(() => {
+  getList()
+})
+
+// activated钩子
+if (route.query.t) {
+  const time = route.query.t
+  if (time != null && time != uniqueId.value) {
+    uniqueId.value = time
+    queryParams.pageNum = Number(route.query.pageNum)
+    getList()
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.product {
+  --font-grey: #606266;
+  --success-color: #67c23b;
+  padding: 20px;
+  --primary-color: #0147eb;
+
+  .search-form {
+    .el-form-item {
+      ::v-deep .el-form-item__label {
+        font-weight: normal;
+        color: var(--font-grey);
+      }
+    }
+  }
+
+  .el-table {
+    ::v-deep .el-table__header th {
+      color: #9a9da3;
+      border-color: #f6f8fa;
+      background-color: #f5f7fa;
+    }
+
+    ::v-deep .el-table__body .el-table__cell {
+      color: #606266;
+      padding: 8px 0;
+      border-color: #f6f8fa;
+
+      .el-button {
+        border-color: transparent;
+        background: transparent;
+
+        &:hover {
+          &.el-button--danger {
+            color: #ff9999;
+          }
+
+          &.el-button--primary {
+            color: #91a8f8;
+          }
+        }
+
+        &.el-button {
+          margin-left: 2px;
+        }
+      }
+    }
+  }
+
+  .pagination-container {
+    ::v-deep .el-pagination {
+      .el-pager li:not(.disabled).active {
+        background-color: var(--primary-color);
+      }
+    }
+  }
+
+  .card-item {
+    height: 100%;
+    border-radius: 4px;
+    background-image: linear-gradient(#e9f1fc, #fefefe);
+
+    ::v-deep .el-card__body {
+      display: flex;
+      height: 100%;
+      flex-direction: column;
+      justify-content: space-between;
+    }
+
+    .card-item__title {
+      vertical-align: -2px;
+      margin-right: 0.5em;
+    }
+
+    .el-descriptions {
+      ::v-deep .el-descriptions__body {
+        background: transparent;
+      }
+    }
+
+    .card-item__footer {
+      margin-top: 1em;
+      text-align: right;
+
+      .el-button {
+        background: #f5f7fa;
+        border-color: #f5f7fa;
+        color: #333;
+        font-weight: bold;
+
+        &.detail-btn {
+          color: var(--primary-color);
+        }
+
+        &.delete-btn {
+          color: #ff6363;
+        }
+
+        &.success-btn {
+          color: var(--success-color);
+        }
+
+        &:hover {
+          border-color: var(--primary-color);
+          color: var(--primary-color);
+        }
+      }
+    }
+  }
+}
+</style>

+ 120 - 0
src/views/pms/video_center/product/product-app.vue

@@ -0,0 +1,120 @@
+<template>
+    <div style="padding-left: 20px">
+        <el-row :gutter="10">
+            <el-col :span="14">
+                <el-row :gutter="10" class="mb8">
+                    <el-col :span="1.5">
+                        <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList">{{ $t('product.product-app.045891-0') }}</el-button>
+                    </el-col>
+                    <el-tag type="danger" style="margin-left: 15px">{{ $t('product.product-app.045891-1') }}</el-tag>
+                </el-row>
+                <el-table :border="false" v-loading="loading" :data="modelList" border style="margin-bottom: 60px; margin-top: 20px" size="small">
+                    <el-table-column :label="$t('product.product-app.045891-2')" align="center" prop="modelName" />
+                    <el-table-column :label="$t('product.product-app.045891-3')" align="center" prop="identifier" />
+                    <el-table-column :label="$t('product.product-app.045891-4')" align="center" prop="type">
+                        <template slot-scope="scope">
+                            <dict-tag :options="dict.type.iot_things_type" :value="scope.row.type" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column :label="$t('product.product-app.045891-5')" align="center" prop="datatype">
+                        <template slot-scope="scope">
+                            <dict-tag :options="dict.type.iot_data_type" :value="scope.row.datatype" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column :label="$t('product.product-app.045891-6')" align="center" prop="part">
+                        <template slot-scope="scope">{{ scope.row.part }} {{ $t('product.product-app.045891-7') }}</template>
+                    </el-table-column>
+                </el-table>
+
+                <el-divider>{{ $t('product.product-app.045891-8') }}</el-divider>
+                <el-form ref="form" :model="form" label-width="100px">
+                    <el-form-item :label="$t('product.product-app.045891-9')" prop="page">
+                        <el-input v-model="form.page" :placeholder="$t('product.product-app.045891-10')" />
+                    </el-form-item>
+                </el-form>
+            </el-col>
+            <el-col :span="8" :offset="2">
+                <div class="phone">
+                    <div class="phone-container"></div>
+                </div>
+                <div style="text-align: center; margin-top: 15px; width: 370px">{{ $t('product.product-app.045891-11') }}</div>
+            </el-col>
+        </el-row>
+    </div>
+</template>
+
+<script>
+import { listModel } from '@/api/iot/model';
+export default {
+    name: 'device-log',
+    dicts: ['iot_things_type', 'iot_data_type', 'iot_yes_no'],
+    props: {
+        product: {
+            type: Object,
+            default: null,
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: false,
+            // 产品物模型表格数据
+            modelList: [],
+            // 弹出层标题
+            title: '',
+            // 查询参数
+            queryParams: {
+                productId: 0,
+                // 1-属性,2-功能,3-事件,4-属性和功能
+                type: 4,
+            },
+            form: {},
+            // 产品
+            productInfo: {},
+        };
+    },
+    watch: {
+        // 获取到父组件传递的productId后,刷新列表
+        product: function (newVal, oldVal) {
+            this.productInfo = newVal;
+            if (this.productInfo && this.productInfo.productId != 0) {
+                this.queryParams.productId = this.productInfo.productId;
+                this.getList();
+            }
+        },
+    },
+    created() {},
+    methods: {
+        /** 查询产品物模型列表 */
+        getList() {
+            this.loading = true;
+            listModel(this.queryParams).then((response) => {
+                this.modelList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+    },
+};
+</script>
+
+<style scoped>
+.phone {
+    height: 700px;
+    width: 370px;
+    background-image: url('../../../assets/images/phone.jpg');
+    background-size: cover;
+    top: 0px;
+}
+
+.phone-container {
+    height: 620px;
+    width: 345px;
+    border-radius: 20px;
+    position: relative;
+    top: 45px;
+    left: 12px;
+    border: 1px solid #888;
+    background: linear-gradient(303deg, #b2e9fc 50%, #b5c4f8 50%);
+}
+</style>

+ 534 - 0
src/views/pms/video_center/product/product-authorize.vue

@@ -0,0 +1,534 @@
+<template>
+    <div style="padding-left: 20px">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+            <el-form-item :label="$t('product.product-authorize.314975-0')" prop="serialNumber">
+                <el-input v-model="queryParams.serialNumber" :placeholder="$t('product.product-authorize.314975-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('product.product-authorize.314975-2')" prop="authorizeCode">
+                <el-input v-model="queryParams.authorizeCode" :placeholder="$t('product.product-authorize.314975-3')" clearable size="small" @keyup.enter.native="handleQuery" />
+            </el-form-item>
+            <el-form-item :label="$t('product.product-authorize.314975-4')" prop="status">
+                <el-select v-model="queryParams.status" :placeholder="$t('product.index.091251-5')" clearable size="small">
+                    <el-option v-for="dict in dict.type.iot_auth_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+                </el-select>
+            </el-form-item>
+            <el-form-item>
+                <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('product.product-authorize.314975-6') }}</el-button>
+                <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('product.product-authorize.314975-7') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:authorize:add']">
+                    {{ $t('product.product-authorize.314975-8') }}
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="danger" plain icon="el-icon-delete" size="mini" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:authorize:remove']" :disabled="multiple" @click="handleDelete">
+                    {{ $t('product.product-authorize.314975-9') }}
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['iot:authorize:export']">{{ $t('product.product-authorize.314975-10') }}</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-link type="info" style="padding-top: 5px" :underline="false">{{ $t('product.product-authorize.314975-11') }}</el-link>
+            </el-col>
+        </el-row>
+
+        <el-table :border="false" v-loading="loading" :data="authorizeList" @selection-change="handleSelectionChange" size="small" @cell-dblclick="celldblclick">
+            <el-table-column type="selection" :selectable="selectable" width="55" align="center" />
+            <el-table-column :label="$t('product.product-authorize.314975-2')" width="320" align="center" prop="authorizeCode" />
+            <el-table-column :label="$t('product.product-authorize.314975-4')" align="center" prop="active" width="100">
+                <template slot-scope="scope">
+                    <dict-tag :options="dict.type.iot_auth_status" :value="scope.row.status" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-authorize.314975-0')" width="150" align="center" prop="serialNumber">
+                <template slot-scope="scope">
+                    <el-link type="primary" @click="getDeviceBySerialNumber(scope.row.serialNumber)" :underline="false">{{ scope.row.serialNumber }}</el-link>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-authorize.314975-12')" align="center" prop="updateTime" width="180">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-authorize.314975-13')" align="center" prop="remark" />
+            <el-table-column :label="$t('product.product-authorize.314975-14')" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-s-check"
+                        @click="handleUpdate(scope.row, 'auth')"
+                        v-if="scope.row.status == 1 && !scope.row.deviceId && productInfo.isOwner != 0"
+                        v-hasPermi="['iot:authorize:edit']"
+                    >
+                        {{ $t('product.index.091251-19') }}
+                    </el-button>
+                    <el-button size="mini" type="text" icon="el-icon-notebook-1" @click="handleUpdate(scope.row, 'remark')" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:authorize:edit']">
+                        {{ $t('product.product-authorize.314975-13') }}
+                    </el-button>
+                    <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-if="!scope.row.deviceId && productInfo.isOwner != 0" v-hasPermi="['iot:authorize:remove']">
+                        {{ $t('product.product-authorize.314975-15') }}
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+        <!-- 设备授权和授权备注对话框 -->
+        <el-dialog :title="title" :visible.sync="open" :width="editWidth" append-to-body>
+            <div v-if="editType == 'auth'">
+                <div class="el-divider el-divider--horizontal" style="margin-top: -25px"></div>
+                <el-form :model="deviceParams" ref="queryDeviceForm" :inline="true" label-width="68px">
+                    <el-form-item :label="$t('product.product-authorize.314975-17')" prop="deviceName">
+                        <el-input v-model="deviceParams.deviceName" :placeholder="$t('product.product-authorize.314975-18')" clearable size="small" style="width: 150px" @keyup.enter.native="handleQuery" />
+                    </el-form-item>
+                    <el-form-item :label="$t('product.product-authorize.314975-0')" prop="serialNumber" style="margin: 0 30px">
+                        <el-input v-model="deviceParams.serialNumber" :placeholder="$t('product.product-authorize.314975-1')" clearable size="small" style="width: 150px" @keyup.enter.native="handleQuery" />
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleDeviceQuery">{{ $t('product.product-authorize.314975-6') }}</el-button>
+                        <el-button icon="el-icon-refresh" size="mini" @click="resetDeviceQuery">{{ $t('product.product-authorize.314975-7') }}</el-button>
+                    </el-form-item>
+                </el-form>
+                <el-table :border="false" v-loading="deviceLoading" :data="deviceList" ref="singleTable" size="mini" @row-click="rowClick" highlight-current-row>
+                    <el-table-column :label="$t('product.product-authorize.314975-19')" width="50" align="center">
+                        <template slot-scope="scope">
+                            <input type="radio" :checked="scope.row.isSelect" name="device" />
+                        </template>
+                    </el-table-column>
+                    <el-table-column :label="$t('product.product-authorize.314975-17')" align="center" prop="deviceName" />
+                    <el-table-column :label="$t('product.product-authorize.314975-20')" align="center" prop="deviceId" />
+                    <el-table-column :label="$t('product.product-authorize.314975-0')" align="center" prop="serialNumber" />
+                    <el-table-column :label="$t('product.product-authorize.314975-21')" align="center" prop="userName" />
+                    <el-table-column :label="$t('product.product-authorize.314975-22')" align="center" prop="status">
+                        <template slot-scope="scope">
+                            <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" />
+                        </template>
+                    </el-table-column>
+                </el-table>
+                <pagination v-show="deviceTotal > 0" :total="deviceTotal" :page.sync="deviceParams.pageNum" :limit.sync="deviceParams.pageSize" @pagination="getDeviceList" />
+            </div>
+            <div v-if="editType == 'remark'">
+                <el-input v-model="form.remark" type="textarea" rows="4" :placeholder="$t('product.product-authorize.314975-23')" />
+            </div>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">{{ $t('product.product-authorize.314975-24') }}</el-button>
+                <el-button @click="cancel">{{ $t('product.product-authorize.314975-25') }}</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 设备详情对话框 -->
+        <el-dialog :title="$t('product.product-authorize.314975-26')" :visible.sync="openDevice" width="600px" append-to-body>
+            <div v-if="device == null" style="text-align: center">
+                <i class="el-icon-warning" style="color: #e6a23c"></i>
+                {{ $t('product.product-authorize.314975-27') }}
+            </div>
+            <el-descriptions border :column="2" size="medium" v-if="device != null">
+                <el-descriptions-item :label="$t('product.product-authorize.314975-20')">{{ device.deviceId }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-17')">{{ device.deviceName }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-0')">{{ device.serialNumber }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-22')">
+                    <!-- (1-未激活,2-禁用,3-在线,4-离线) -->
+                    <el-tag v-if="device.status == 1" type="warning">{{ $t('product.product-authorize.314975-28') }}</el-tag>
+                    <el-tag v-else-if="device.status == 2" type="danger">{{ $t('product.product-authorize.314975-29') }}</el-tag>
+                    <el-tag v-else-if="device.status == 3" type="success">{{ $t('product.product-authorize.314975-30') }}</el-tag>
+                    <el-tag v-else-if="device.status == 4" type="info">{{ $t('product.product-authorize.314975-31') }}</el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-32')">
+                    <el-tag v-if="device.isShadow == 1" type="success">{{ $t('product.product-authorize.314975-33') }}</el-tag>
+                    <el-tag v-else type="info">{{ $t('product.index.091251-21') }}</el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-35')">
+                    <!-- (1=自动定位,2=设备定位,3=自定义) -->
+                    <el-tag v-if="device.locationWay == 1" type="success">{{ $t('product.product-authorize.314975-36') }}</el-tag>
+                    <el-tag v-else-if="device.locationWay == 2" type="warning">{{ $t('product.product-authorize.314975-37') }}</el-tag>
+                    <el-tag v-else-if="device.locationWay == 3" type="primary">{{ $t('product.product-authorize.314975-38') }}</el-tag>
+                </el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-39')">{{ device.productName }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-40')">{{ device.userName }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-41')">Version {{ device.firmwareVersion }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-42')">{{ device.networkAddress }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-43')">{{ device.longitude }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-44')">{{ device.latitude }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-45')">{{ device.networkIp }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-46')">{{ device.rssi }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-47')">{{ device.createTime }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-48')">{{ device.activeTime }}</el-descriptions-item>
+                <el-descriptions-item :label="$t('product.product-authorize.314975-49')">{{ device.remark }}</el-descriptions-item>
+            </el-descriptions>
+            <div slot="footer" class="dialog-footer">
+                <el-button @click="goToEditDevice(device.deviceId)" type="primary">{{ $t('product.product-authorize.314975-50') }}</el-button>
+                <el-button @click="closeDevice">{{ $t('product.product-authorize.314975-51') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<style>
+.createNum {
+    width: 300px;
+}
+
+.createNum input {
+    width: 260px;
+}
+</style>
+
+<script>
+import { getDeviceBySerialNumber, listUnAuthDevice } from '@/api/iot/device';
+import { addProductAuthorizeByNum, delAuthorize, getAuthorize, listAuthorize, updateAuthorize } from '@/api/iot/authorize';
+
+export default {
+    name: 'product-authorize',
+    dicts: ['iot_auth_status', 'iot_device_status'],
+    props: {
+        product: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的productId后,刷新列表
+        product: function (newVal, oldVal) {
+            this.productInfo = newVal;
+            if (this.productInfo && this.productInfo.productId != 0) {
+                this.queryParams.productId = this.productInfo.productId;
+                this.deviceParams.productId = this.productInfo.productId;
+                this.getList();
+                this.getDeviceList();
+            }
+        },
+    },
+    data() {
+        return {
+            // 设备信息
+            device: {},
+            // 设备信息对话框
+            openDevice: false,
+            // 设备遮罩层
+            deviceLoading: true,
+            // 总条数
+            deviceTotal: 0,
+            // 设备表格数据
+            deviceList: [],
+            // 查询参数
+            deviceParams: {
+                pageNum: 1,
+                pageSize: 10,
+                userId: null,
+                deviceName: null,
+                productId: 0,
+                productName: null,
+                userId: null,
+                userName: null,
+                tenantId: null,
+                tenantName: null,
+                serialNumber: null,
+                status: null,
+                networkAddress: null,
+                activeTime: null,
+            },
+            // 编辑类型,remark=备注、auth=设备授权
+            editType: '',
+            // 编辑界面宽度
+            editWidth: '500px',
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 产品授权码表格数据
+            authorizeList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 新增授权码个数
+            createNum: 10,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                authorizeCode: null,
+                productId: null,
+                deviceId: null,
+                serialNumber: null,
+                userId: null,
+                userName: null,
+                status: null,
+            },
+            // 表单参数
+            form: {},
+            // 产品
+            productInfo: {},
+        };
+    },
+    created() {},
+    methods: {
+        /**获取设备详情*/
+        getDeviceBySerialNumber(serialNumber) {
+            this.openDevice = true;
+            getDeviceBySerialNumber(serialNumber).then((response) => {
+                this.device = response.data;
+            });
+        },
+        /** 修改按钮操作 */
+        goToEditDevice(deviceId) {
+            this.openDevice = false;
+            this.$router.push({
+                path: '/iot/device-edit',
+                query: {
+                    deviceId: deviceId,
+                },
+            });
+        },
+        /** 查询设备列表 */
+        getDeviceList() {
+            this.deviceLoading = true;
+            this.deviceParams.params = {};
+            listUnAuthDevice(this.deviceParams).then((response) => {
+                //设备列表初始化isSelect值,用于单选
+                for (let i = 0; i < response.rows.length; i++) {
+                    response.rows[i].isSelect = false;
+                }
+                this.deviceList = response.rows;
+                this.deviceTotal = response.total;
+                this.deviceLoading = false;
+            });
+        },
+        /** 搜索按钮操作 */
+        handleDeviceQuery() {
+            this.deviceParams.pageNum = 1;
+            this.getDeviceList();
+        },
+        /** 重置按钮操作 */
+        resetDeviceQuery() {
+            this.resetForm('queryDeviceForm');
+            this.handleDeviceQuery();
+        },
+        /** 单选数据 */
+        rowClick(device) {
+            if (device != null) {
+                this.setRadioSelected(device.deviceId);
+                // 赋值
+                this.form.userId = device.userId;
+                this.form.userName = device.userName;
+                this.form.deviceId = device.deviceId;
+                this.form.serialNumber = device.serialNumber;
+            }
+        },
+        /** 设置单选按钮选中 */
+        setRadioSelected(deviceId) {
+            for (let i = 0; i < this.deviceList.length; i++) {
+                let device = this.deviceList[i];
+                if (this.deviceList[i].deviceId == deviceId) {
+                    device.isSelect = true;
+                    this.$set(this.deviceList, i, device);
+                } else {
+                    device.isSelect = false;
+                    this.$set(this.deviceList, i, device);
+                }
+            }
+        },
+        /** 查询产品授权码列表 */
+        getList() {
+            this.loading = true;
+            listAuthorize(this.queryParams).then((response) => {
+                this.authorizeList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 关闭设备详情
+        closeDevice() {
+            this.openDevice = false;
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                authorizeId: null,
+                authorizeCode: null,
+                productId: '',
+                userId: '',
+                deviceId: null,
+                serialNumber: null,
+                userName: null,
+                delFlag: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+            };
+            this.device = {};
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.authorizeId);
+            this.multiple = !selection.length;
+        },
+        /** 批量新增按钮操作 */
+        handleAdd() {
+            this.$prompt('', this.$t('product.product-authorize.314975-52'), {
+                customClass: 'createNum',
+                confirmButtonText: this.$t('product.product-authorize.314975-53'),
+                cancelButtonText: this.$t('product.product-authorize.314975-54'),
+                inputPattern: /[0-9\-]/,
+                inputErrorMessage: this.$t('product.product-authorize.314975-55'),
+                inputType: 'number',
+                inputValue: this.createNum,
+            })
+                .then(({ value }) => {
+                    this.createNum = value;
+                    if (this.queryParams.productId != null) {
+                        let _addData = {
+                            productId: this.queryParams.productId,
+                            createNum: this.createNum,
+                        };
+                        addProductAuthorizeByNum(_addData).then((response) => {
+                            this.$modal.msgSuccess(this.$t('product.product-authorize.314975-56'));
+                            this.getList();
+                            this.createNum = 10;
+                        });
+                    }
+                })
+                .catch(() => {
+                    this.$message({
+                        type: 'info',
+                        message: this.$t('product.product-authorize.314975-57'),
+                    });
+                });
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row, editType) {
+            this.reset();
+            this.editType = editType;
+            const authorizeId = row.authorizeId || this.ids;
+            getAuthorize(authorizeId).then((response) => {
+                this.form = response.data;
+                this.open = true;
+                if (this.editType == 'auth') {
+                    this.title = this.$t('product.product-authorize.314975-58');
+                    this.editWidth = '800px';
+                } else {
+                    this.title = this.$t('product.product-authorize.314975-49');
+                    this.editWidth = '500px';
+                }
+                // 取消选中
+
+                for (let i = 0; i < this.deviceList.length; i++) {
+                    // this.deviceList[i].isSelect=false;
+                    let device = this.deviceList[i];
+                    device.isSelect = false;
+                    // this.deviceList.splice(i,1,device )
+                    this.$set(this.deviceList, i, device);
+                }
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            if (this.editType == 'auth') {
+                if (this.form.deviceId != null && this.form.deviceId != 0) {
+                    updateAuthorize(this.form).then((response) => {
+                        this.$modal.msgSuccess(this.$t('product.product-authorize.314975-59'));
+                        this.open = false;
+                        this.getList();
+                    });
+                } else {
+                    this.$modal.msg(this.$t('product.product-authorize.314975-60'));
+                }
+            } else if (this.form.authorizeId != null) {
+                updateAuthorize(this.form).then((response) => {
+                    this.$modal.msgSuccess(this.$t('product.product-authorize.314975-61'));
+                    this.open = false;
+                    this.getList();
+                });
+            }
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const authorizeIds = row.authorizeId || this.ids;
+            this.$modal
+                .confirm(this.$t('product.product-authorize.314975-62', [authorizeIds]))
+                .then(function () {
+                    return delAuthorize(authorizeIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('product.product-authorize.314975-63'));
+                })
+                .catch(() => {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/authorize/export',
+                {
+                    ...this.queryParams,
+                },
+                `authorize_${new Date().getTime()}.xlsx`
+            );
+        },
+        //禁用有绑定设备的复选框
+        selectable(row) {
+            return row.deviceId != null ? false : true;
+        },
+        //表格增加复制功能
+        celldblclick(row, column, cell, event) {
+            this.$copyText(row[column.property]).then(
+                (e) => {
+                    this.onCopy();
+                },
+                function (e) {
+                    this.onError();
+                }
+            );
+        },
+        onCopy() {
+            this.$notify({
+                title: this.$t('product.product-authorize.314975-64'),
+                message: this.$t('product.product-authorize.314975-66'),
+                type: 'success',
+                offset: 50,
+                duration: 2000,
+            });
+        },
+        onError() {
+            this.$notify({
+                title: this.$t('product.product-authorize.314975-67'),
+                message: this.$t('product.product-authorize.314975-68'),
+                type: 'error',
+                offset: 50,
+                duration: 2000,
+            });
+        },
+    },
+};
+</script>

+ 1759 - 0
src/views/pms/video_center/product/product-edit.vue

@@ -0,0 +1,1759 @@
+<template>
+  <div>
+    <el-card style="margin: 6px; padding-bottom: 100px" class="main-card">
+      <el-tabs
+        v-model="activeName"
+        style="padding: 10px; min-height: 400px"
+        tab-position="left"
+        @tab-change="tabChange"
+      >
+        <el-tab-pane name="basic">
+          <template #label>
+            <span style="color: red">*</span>
+            {{ t('product.product-edit473153-0') }}
+          </template>
+          <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+            <el-row :gutter="100">
+              <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                <el-form-item :label="t('product.product-edit473153-1')" prop="productName">
+                  <el-input
+                    v-model="form.productName"
+                    :placeholder="t('product.product-edit473153-2')"
+                    :readonly="form.status == 2 || form.isOwner == 0"
+                  />
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-3')" prop="categoryId">
+                  <el-select
+                    v-model="form.categoryId"
+                    :placeholder="t('product.product-edit473153-4')"
+                    @change="selectCategory"
+                    style="width: 100%"
+                    :disabled="form.status == 2 || form.isOwner == 0"
+                  >
+                    <el-option
+                      v-for="category in categoryShortList"
+                      :key="category.categoryId"
+                      :label="category.categoryName"
+                      :value="category.categoryId"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item v-if="form.deviceType !== 3" label="通讯协议" prop="protocolCode">
+                  <template #label>
+                    <span>通讯协议</span>
+                    <el-tooltip effect="light" placement="bottom" style="cursor: pointer">
+                      <template #content>
+                        产品建立后,不允许修改通讯协议,修改会导致该产品下设备脏数据问题
+                        <br />
+                      </template>
+                      <el-icon><QuestionFilled /></el-icon>
+                    </el-tooltip>
+                  </template>
+                  <el-select
+                    v-model="form.protocolCode"
+                    :disabled="form.status == 2 || form.productId != 0 || form.isOwner == 0"
+                    placeholder="请选择协议"
+                    style="width: 100%"
+                    @change="changeProductCode"
+                  >
+                    <el-option
+                      v-for="p in protocolList"
+                      :key="p.protocolCode"
+                      :label="p.protocolName"
+                      :value="p.protocolCode"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item v-if="form.deviceType !== 3 && tempOpen">
+                 
+                    <el-alert
+                      description="当前通讯协议为modbus协议,请选择采集点模板,默认添加设备为网关设备"
+                      type="success"
+                      
+                    />
+                
+                </el-form-item>
+                <el-form-item v-if="tempOpen && selectRowData" prop="templateId">
+                  <template #label>
+                    <span style="color: #f56c6c">*</span>
+                    采集点模板
+                  </template>
+                  <div style="display: inline-block; padding-right: 5px">
+                    <span
+                      style="
+                        font-size: 9px;
+                        padding-right: 5px;
+                        color: #00bb00;
+                        font-size: 12px;
+                        font-weight: bold;
+                      "
+                      >{{ selectRowData.templateName }}</span
+                    >
+                    <el-button
+                      :disabled="form.status == 2 || form.productId != 0 || form.isOwner == 0"
+                      plain
+                      size="small"
+                      @click="deleteData"
+                      >删除</el-button
+                    >
+                  </div>
+                  <el-button
+                    plain
+                    size="small"
+                    type="primary"
+                    :disabled="form.status == 2 || form.productId != 0 || form.isOwner == 0"
+                    @click="selectTemplate"
+                    >选择模板</el-button
+                  >
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-78')" prop="deviceType">
+                  <el-select
+                    v-model="form.deviceType"
+                    :disabled="form.productId != 0"
+                    placeholder="请选择设备类型"
+                    style="width: 100%"
+                  >
+                    <el-option
+                      v-for="dict in iot_device_type"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="parseInt(dict.value)"
+                    />
+                  </el-select>
+                </el-form-item>
+
+                <el-form-item
+                  v-if="form.deviceType !== 4"
+                  :label="t('product.product-edit473153-14')"
+                  prop="transport"
+                >
+                  <el-select
+                    v-model="form.transport"
+                    :placeholder="t('product.product-edit473153-15')"
+                    style="width: 100%"
+                    :disabled="form.productId != 0"
+                    @change="transportChange"
+                  >
+                    <el-option
+                      v-for="dict in iot_transport_type"
+                      :key="dict.value"
+                      :value="dict.value"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-16')" prop="networkMethod">
+                  <el-select
+                    v-model="form.networkMethod"
+                    :placeholder="t('product.product-edit473153-17')"
+                    style="width: 100%"
+                    :disabled="form.productId != 0"
+                  >
+                    <el-option
+                      v-for="dict in networkOptions"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="parseInt(dict.value)"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-18')" prop="isSys">
+                  <template #label>
+                    <span>{{ t('product.product-edit473153-18') }}</span>
+                    <el-tooltip style="cursor: pointer" effect="light" placement="bottom">
+                      <template #content>
+                        {{ t('product.product-edit473153-19') }}
+                        <br />
+                      </template>
+                      <el-icon><QuestionFilled /></el-icon>
+                    </el-tooltip>
+                  </template>
+                  <el-switch
+                    v-model="form.isSys"
+                    :active-value="1"
+                    :disabled="form.productId != 0"
+                    :inactive-value="0"
+                  />
+                </el-form-item>
+              </el-col>
+              <el-col
+                v-show="dialogVisibleInterface === 'MTG'"
+                :xs="24"
+                :sm="24"
+                :md="24"
+                :lg="12"
+                :xl="8"
+              >
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-20')"
+                  prop="networkMethod"
+                >
+                  <el-switch
+                    v-model="form.isAuthorize"
+                    @change="changeIsAuthorize"
+                    :active-value="1"
+                    :inactive-value="0"
+                    :disabled="form.productId != 0"
+                  />
+                </el-form-item>
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-21')"
+                  prop="vertificateMethod"
+                >
+                  <el-select
+                    v-model="form.vertificateMethod"
+                    :placeholder="t('product.product-edit473153-22')"
+                    style="width: 100%"
+                    :disabled="form.productId != 0"
+                  >
+                    <el-option
+                      v-for="dict in iot_vertificate_method"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="parseInt(dict.value)"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-23')"
+                  prop="locationWay"
+                >
+                  <el-select
+                    v-model="form.locationWay"
+                    :placeholder="t('product.product-edit473153-24')"
+                    clearable
+                    size="small"
+                    :disabled="form.productId != 0"
+                    style="width: 100%"
+                  >
+                    <el-option
+                      v-for="dict in iot_location_way"
+                      :key="dict.value"
+                      :label="dict.label"
+                      :value="Number(dict.value)"
+                    />
+                  </el-select>
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-25')" prop="productId">
+                  <el-input
+                    v-model="form.productId"
+                    :placeholder="t('product.product-edit473153-26')"
+                    :disabled="form.productId != 0"
+                    readonly
+                  />
+                </el-form-item>
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-27')"
+                  prop="mqttAccount"
+                >
+                  <el-input
+                    v-model="form.mqttAccount"
+                    :placeholder="t('product.product-edit473153-28')"
+                    :disabled="form.deviceType == 3"
+                    :readonly="accountInputType == 'password' || form.isOwner == 0"
+                    :type="accountInputType"
+                  >
+                    <template #append>
+                      <el-button
+                        :icon="View"
+                        style="font-size: 18px"
+                        @click="changeInputType('account')"
+                      />
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-29')"
+                  prop="mqttPassword"
+                >
+                  <el-input
+                    v-model="form.mqttPassword"
+                    :placeholder="t('product.product-edit473153-30')"
+                    :disabled="form.deviceType == 3"
+                    :readonly="passwordInputType == 'password' || form.isOwner == 0"
+                    :type="passwordInputType"
+                  >
+                    <template #append>
+                      <el-button
+                        :icon="View"
+                        style="font-size: 18px"
+                        @click="changeInputType('password')"
+                      />
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item
+                  v-if="form.deviceType != 4"
+                  :label="t('product.product-edit473153-31')"
+                  prop="mqttSecret"
+                >
+                  <el-input
+                    v-model="form.mqttSecret"
+                    :placeholder="t('product.product-edit473153-26')"
+                    :disabled="form.productId != 0"
+                    readonly
+                    :type="keyInputType"
+                  >
+                    <template #append>
+                      <el-button
+                        :icon="View"
+                        style="font-size: 18px"
+                        @click="changeInputType('key')"
+                      />
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item :label="t('product.product-edit473153-32')" prop="remark">
+                  <el-input
+                    v-model="form.remark"
+                    :placeholder="t('product.product-edit473153-33')"
+                    rows="3"
+                    :readonly="form.status == 2 || form.isOwner == 0"
+                  />
+                </el-form-item>
+              </el-col>
+
+              <el-col
+                :xs="24"
+                :sm="24"
+                :md="24"
+                :lg="12"
+                :xl="8"
+                v-show="dialogVisibleInterface === 'HTTP'"
+              >
+                <el-form-item label="用户登录">
+                  <el-switch
+                    v-model="userParmas.isLogin"
+                    @change="changeIsLogin"
+                    :active-value="true"
+                    :inactive-value="false"
+                  />
+                </el-form-item>
+
+                <el-form-item label="响应数据key">
+                  <el-input
+                    v-model="form.responseKey"
+                    :disabled="form.status == 2 ? true : false"
+                  />
+                </el-form-item>
+                <el-form-item label="请求间隔">
+                  <el-input
+                    v-model="form.cronExpression"
+                    placeholder="请输入cron执行表达式"
+                    :disabled="form.status == 2 ? true : false"
+                  >
+                    <template #append>
+                      <el-button
+                        type="primary"
+                        @click="handleShowCron"
+                        :disabled="form.status == 2 ? true : false"
+                      >
+                        生成表达式
+                        <el-icon class="el-icon--right"><Timer /></el-icon>
+                      </el-button>
+                    </template>
+                  </el-input>
+                </el-form-item>
+
+                <el-form-item label="请求方式">
+                  <el-select
+                    v-model="form.methodType"
+                    placeholder="请选择"
+                    :disabled="form.status == 2 ? true : false"
+                  >
+                    <el-option label="get" value="get" />
+                    <el-option label="post" value="post" />
+                    <el-option label="put" value="put" />
+                  </el-select>
+                </el-form-item>
+
+                <el-form-item label="请求地址">
+                  <el-input
+                    placeholder="例:http:127.0.0.1/"
+                    v-model="form.url"
+                    :disabled="form.status == 2 ? true : false"
+                  />
+                </el-form-item>
+
+                <el-form-item label="请求参数">
+                  <div style="margin-top: 20px; overflow-y: auto">
+                    <el-tabs style="height: 350px" v-model="form.requestType">
+                      <el-tab-pane label="Header" :disabled="form.status == 2 ? true : false">
+                        <el-table :data="form.headersList" style="width: 100%">
+                          <el-table-column type="index" width="50" />
+                          <el-table-column label="key" prop="key">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.key"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="value" prop="value">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.value"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="操作" width="120">
+                            <template #default="scope">
+                              <el-button
+                                size="small"
+                                @click="handleEdit(form.headersList)"
+                                :disabled="form.status == 2 ? true : false"
+                                >+</el-button
+                              >
+                              <el-button
+                                size="small"
+                                type="danger"
+                                @click="handleDelete(scope.$index, form.headersList)"
+                                :disabled="form.status == 2 ? true : false"
+                                >-</el-button
+                              >
+                            </template>
+                          </el-table-column>
+                        </el-table>
+                      </el-tab-pane>
+                      <el-tab-pane label="Params" :disabled="form.status == 2 ? true : false">
+                        <el-table :data="form.paramsDataList" style="width: 100%">
+                          <el-table-column type="index" width="50" />
+                          <el-table-column label="key" prop="key">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.key"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="value" prop="value">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.value"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="操作" width="120">
+                            <template #default="scope">
+                              <el-button
+                                size="small"
+                                @click="handleEdit(form.paramsDataList)"
+                                :disabled="form.status == 2 ? true : false"
+                                >+</el-button
+                              >
+                              <el-button
+                                size="small"
+                                type="danger"
+                                :disabled="form.status == 2 ? true : false"
+                                @click="handleDelete(scope.$index, form.paramsDataList)"
+                                >-</el-button
+                              >
+                            </template>
+                          </el-table-column>
+                        </el-table>
+                      </el-tab-pane>
+                      <el-tab-pane label="Body" :disabled="form.status == 2 ? true : false">
+                        <div style="margin-bottom: 10px">
+                          <el-radio-group
+                            v-model="form.contentTypeNumber"
+                            :disabled="form.status == 2 ? true : false"
+                          >
+                            <el-radio :label="1">none</el-radio>
+                            <el-radio :label="2">form-data</el-radio>
+                            <el-radio :label="3">x-www-form-urlencoded</el-radio>
+                            <el-radio :label="4">json</el-radio>
+                            <el-radio :label="5">xml</el-radio>
+                          </el-radio-group>
+                        </div>
+                        <el-empty
+                          description="该请求没有body体"
+                          v-if="form.contentTypeNumber == 1"
+                          :image-size="100"
+                        />
+                        <el-table
+                          :data="form.bodyDataTable"
+                          style="width: 100%"
+                          v-else-if="form.contentTypeNumber == 2 || form.contentTypeNumber == 3"
+                        >
+                          <el-table-column type="index" width="50" />
+                          <el-table-column label="key" prop="key">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.key"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="value" prop="value">
+                            <template #default="scope">
+                              <el-input
+                                placeholder="请输入"
+                                v-model="scope.row.value"
+                                :disabled="form.status == 2 ? true : false"
+                              />
+                            </template>
+                          </el-table-column>
+                          <el-table-column label="操作" width="120">
+                            <template #default="scope">
+                              <el-button
+                                size="small"
+                                @click="handleEdit(form.bodyDataTable)"
+                                :disabled="form.status == 2 ? true : false"
+                                >+</el-button
+                              >
+                              <el-button
+                                size="small"
+                                type="danger"
+                                @click="handleDelete(scope.$index, form.bodyDataTable)"
+                                :disabled="form.status == 2 ? true : false"
+                                >-</el-button
+                              >
+                            </template>
+                          </el-table-column>
+                        </el-table>
+                        <el-input
+                          type="textarea"
+                          :rows="8"
+                          placeholder="请输入内容"
+                          v-model="form.bodyDataArea"
+                          v-else-if="form.contentTypeNumber == 4 || form.contentTypeNumber == 5"
+                          :disabled="form.status == 2 ? true : false"
+                        />
+                      </el-tab-pane>
+                    </el-tabs>
+                  </div>
+                </el-form-item>
+              </el-col>
+
+              <el-col
+                v-show="dialogVisibleInterface === 'SNMP'"
+                :xs="24"
+                :sm="24"
+                :md="24"
+                :lg="12"
+                :xl="8"
+              >
+                <el-form-item label="请求间隔">
+                  <el-input
+                    v-model="form.cronExpression"
+                    placeholder="请输入cron执行表达式"
+                    :disabled="form.status == 2 ? true : false"
+                  >
+                    <template #append>
+                      <el-button
+                        type="primary"
+                        @click="handleShowCron"
+                        :disabled="form.status == 2 ? true : false"
+                      >
+                        生成表达式
+                        <el-icon class="el-icon--right"><Timer /></el-icon>
+                      </el-button>
+                    </template>
+                  </el-input>
+                </el-form-item>
+                <el-form-item label="设备地址" prop="ipAddress">
+                  <el-input v-model="form.ipAddress" :disabled="form.status == 2 ? true : false" />
+                </el-form-item>
+                <el-form-item label="团体名" prop="community">
+                  <el-input v-model="form.community" :disabled="form.status == 2 ? true : false" />
+                </el-form-item>
+                <div>
+                  <el-form-item
+                    label="OID信息"
+                    label-width="57%"
+                    align="center"
+                    style="text-align: center; background-color: #f5f7fa"
+                    prop="vertificateMethod"
+                  />
+                  <div
+                    class="flex-center mb20"
+                    v-for="(item, index) in form.oidList"
+                    :key="index"
+                    style="border-bottom: 1px solid #dfe6ec; position: relative"
+                  >
+                    <el-form-item label="名称" label-width="60px" prop="oidName">
+                      <el-input
+                        style="width:"
+                        v-model="item.oidName"
+                        :disabled="form.status == 2 ? true : false"
+                      />
+                    </el-form-item>
+                    <el-form-item label="oid" label-width="50px" prop="oid">
+                      <el-input v-model="item.oid" :disabled="form.status == 2 ? true : false" />
+                    </el-form-item>
+                    <el-form-item>
+                      <el-button
+                        v-if="form.oidList.length > 1"
+                        size="small"
+                        type="danger"
+                        @click="deleteOid(item, index)"
+                        :icon="Minus"
+                      />
+                    </el-form-item>
+                  </div>
+                  <div class="flex-center">
+                    <el-button size="small" type="primary" @click="addOid" :icon="Plus" />
+                  </div>
+                </div>
+              </el-col>
+
+              <el-col :xs="24" :sm="24" :md="24" :lg="12" :xl="8">
+                <el-form-item :label="t('product.product-edit473153-34')">
+                  <div v-if="form.status == 2 && form.imgUrl == null && form.isOwner != 0">
+                    <el-image
+                      style="height: 145px; height: 145px; border-radius: 10px"
+                      :preview-src-list="[require('@/assets/imgs/gateway.png')]"
+                      :src="require('@/assets/imgs/gateway.png')"
+                      fit="cover"
+                      v-if="form.deviceType == 2"
+                    />
+                    <el-image
+                      style="height: 145px; height: 145px; border-radius: 10px"
+                      :preview-src-list="[require('@/assets/imgs/video.png')]"
+                      :src="require('@/assets/imgs/video.png')"
+                      fit="cover"
+                      v-else-if="form.deviceType == 3"
+                    />
+                    <el-image
+                      style="height: 145px; height: 145px; border-radius: 10px"
+                      :preview-src-list="[require('@/assets/imgs/product.png')]"
+                      :src="require('@/assets/imgs/product.png')"
+                      fit="cover"
+                      v-else
+                    />
+                  </div>
+                  <div v-else>
+                    <image-upload
+                      ref="imageUploadRef"
+                      :disabled="true"
+                      :value="form.imgUrl"
+                      :limit="form.status == 2 ? 0 : 1"
+                      :file-size="1"
+                      @input="getImagePath"
+                    />
+                  </div>
+                  <div
+                    class="el-upload__tip"
+                    style="color: #f56c6c"
+                    v-if="form.productId == null || form.productId == 0"
+                  >
+                    {{ t('product.product-edit473153-35') }}
+                  </div>
+                </el-form-item>
+              </el-col>
+            </el-row>
+
+            <el-col :span="20">
+              <el-form-item style="text-align: center; margin: 40px 0px">
+                <el-button
+                  type="primary"
+                  @click="submitForm"
+                  v-show="form.productId != 0 && form.status != 2 && form.isOwner != 0"
+                >
+                  {{ t('product.product-edit473153-36') }}
+                </el-button>
+
+                <el-button
+                  type="primary"
+                  @click="submitForm"
+                  v-show="form.productId == 0 && form.status != 2"
+                  >{{ t('product.product-edit473153-37') }}</el-button
+                >
+              </el-form-item>
+            </el-col>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="" name="things" :disabled="form.productId == 0">
+          <template #label>
+            <span style="color: red">*</span>
+            {{ t('product.product-edit473153-38') }}
+          </template>
+          <product-things-model
+            ref="productThingsModelRef"
+            :product="form"
+            @updateModel="updateModel"
+          />
+        </el-tab-pane>
+
+        <el-tab-pane
+          label=""
+          name="sipConfig"
+          :disabled="form.productId == 0"
+          v-if="form.deviceType === 3"
+        >
+          <template #label>{{ t('product.product-edit473153-41') }}</template>
+          <config-sip ref="configSipRef" :product="form" />
+        </el-tab-pane>
+        <el-tab-pane
+          name="scada"
+          :disabled="form.deviceId == 0"
+          v-if="form.deviceType !== 3 && isShowScada == true"
+          lazy
+        >
+          <template #label>{{ t('device.device-edit.148398-73') }}</template>
+          <product-scada ref="productScadaRef" :product="form" />
+        </el-tab-pane>
+        <!-- 用于设置间距 -->
+        <el-tab-pane>
+          <template #label>
+            <div style="margin-top: 200px"></div>
+          </template>
+        </el-tab-pane>
+        <el-tab-pane v-if="form.status == 1" name="product04" disabled>
+          <template #label>
+            <el-button
+              type="success"
+              v-if="form.isOwner != 0"
+              size="small"
+              @click="changeProductStatus(2)"
+              >{{ t('product.product-edit473153-42') }}</el-button
+            >
+          </template>
+        </el-tab-pane>
+        <el-tab-pane v-if="form.status == 2" name="product05" disabled>
+          <template #label>
+            <el-button
+              type="danger"
+              v-if="form.isOwner != 0"
+              size="small"
+              @click="changeProductStatus(1)"
+              >{{ t('product.product-edit473153-43') }}</el-button
+            >
+          </template>
+        </el-tab-pane>
+        <el-tab-pane name="product06" disabled>
+          <template #label>
+            <el-button type="info" size="small" @click="goBack()">{{
+              t('product.product-edit473153-44')
+            }}</el-button>
+          </template>
+        </el-tab-pane>
+      </el-tabs>
+      <!--添加从机对话框-->
+      <el-dialog v-model="open" :title="title" append-to-body width="1000px">
+        <el-row :gutter="30">
+          <el-col :span="11">
+            <el-form ref="tempParamsRef" :inline="true" :model="tempParams">
+              <el-form-item size="small">
+                <el-input v-model="tempParams.templateName" placeholder="模板名称" />
+              </el-form-item>
+              <el-form-item size="small">
+                <el-button :icon="Search" size="small" type="primary" @click="queryTemp"
+                  >搜索</el-button
+                >
+                <el-button :icon="Refresh" size="small" @click="resetQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+            <el-table
+              :border="false"
+              ref="multipleTableRef"
+              v-loading="loading"
+              :data="tempList"
+              highlight-current-row
+              style="width: 100%"
+            >
+              <el-table-column align="left" label="选择采集点模板">
+                <template #default="scope">
+                  <el-radio
+                    v-model="currentRow"
+                    :label="scope.row"
+                    @change="getCurrentRow(scope.row)"
+                    >{{ scope.row.templateName }}</el-radio
+                  >
+                </template>
+              </el-table-column>
+            </el-table>
+            <el-pagination
+              v-show="tempTotal > 0"
+              v-model:page-size="tempParams.pageSize"
+              v-model:current-page="tempParams.pageNum"
+              :total="tempTotal"
+              layout="total, prev, pager, next"
+              small
+              @current-change="getTempList"
+            />
+          </el-col>
+          <el-col :span="13">
+            <el-form :inline="true" :model="pointsParams">
+              <el-form-item size="small">
+                <template #label>
+                  <span style="font-size: 16px; font-weight: 400">物模型列表</span>
+                </template>
+              </el-form-item>
+              <el-form-item size="small">
+                <template #label>
+                  <span style="font-weight: 400; font-size: 12px">从机数量:</span>
+                </template>
+                {{ selectRowData.slaveTotal }}
+              </el-form-item>
+              <el-form-item size="small">
+                <template #label>
+                  <span style="font-weight: 400; font-size: 12px">变量数量:</span>
+                </template>
+                {{ selectRowData.pointTotal }}
+              </el-form-item>
+              <el-form-item size="small">
+                <template #label>
+                  <span style="font-weight: 400; font-size: 12px">采集方式:</span>
+                </template>
+                <dict-tag
+                  :options="data_collect_type"
+                  :value="selectRowData.pollingMethod"
+                  size="small"
+                  style="display: inline-block"
+                />
+              </el-form-item>
+            </el-form>
+            <el-table :border="false" v-loading="loading" :data="pointList" size="small">
+              <el-table-column label="物模型名称" prop="templateName" />
+              <el-table-column label="寄存器" prop="regAddr" />
+              <el-table-column label="数值类型" prop="datatype" />
+            </el-table>
+            <el-pagination
+              v-show="total > 0"
+              v-model:page-size="pointsParams.pageSize"
+              v-model:current-page="pointsParams.pageNum"
+              :total="total"
+              layout="total, prev, pager, next"
+              small
+              @current-change="getList"
+            />
+          </el-col>
+        </el-row>
+
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button @click="cancel">取 消</el-button>
+            <el-button type="primary" @click="submitSelect">确 定</el-button>
+          </div>
+        </template>
+      </el-dialog>
+      <el-dialog v-model="userEdit" title="登录信息" append-to-body @close="userClose">
+        <el-form :inline="true" ref="userFormRef" :model="userParmas">
+          <div class="flex-center">
+            <div>
+              <div>
+                <el-form-item label="账号" prop="user">
+                  <el-input
+                    v-model="userParmas.user"
+                    placeholder="请输入账号"
+                    clearable
+                    size="small"
+                  />
+                </el-form-item>
+              </div>
+              <div>
+                <el-form-item label="密码" prop="password">
+                  <el-input
+                    v-model="userParmas.password"
+                    placeholder="请输入密码"
+                    clearable
+                    size="small"
+                  />
+                </el-form-item>
+              </div>
+              <div>
+                <el-form-item label="登录地址" prop="password">
+                  <el-input
+                    v-model="userParmas.url"
+                    placeholder="请输入登录地址,例:http:127.0.0.1/"
+                    clearable
+                    size="small"
+                  />
+                </el-form-item>
+              </div>
+            </div>
+          </div>
+        </el-form>
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button @click="cancelUser">取 消</el-button>
+            <el-button type="primary" @click="confirmUser">确 定</el-button>
+          </div>
+        </template>
+      </el-dialog>
+      <el-dialog
+        v-model="openCron"
+        title="Cron表达式生成器"
+        append-to-body
+        destroy-on-close
+        class="scrollbar"
+      >
+        <crontab @hide="openCron = false" @fill="crontabFill" :expression="expression" />
+      </el-dialog>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import productThingsModel from './product-things-model.vue'
+import imageUpload from './components/ImageUpload/index.vue'
+import configSip from '../sip/sipconfig.vue'
+import productScada from './product-scada.vue'
+import { listProtocol } from '@/api/pms/video/protocol'
+
+import { listShortCategory,listCategory } from '@/api/pms/video/category'
+import { getDicts } from '@/api/pms/video/dicts'
+import {
+  addProduct,
+  changeProductStatus,
+  deviceCount,
+  getProduct,
+  updateProduct
+} from '@/api/pms/video/product'
+import { getTempByPId, listTemp } from '@/api/pms/video/temp'
+import { getAllPoints } from '@/api/pms/video/template'
+
+import Crontab from './components/Crontab/index.vue'
+import { getAccessToken } from '@/utils/auth'
+import { View, QuestionFilled, Timer, Minus, Plus, Search, Refresh } from '@element-plus/icons-vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 定义组件名称和属性
+defineOptions({
+  name: 'ProductEdit'
+})
+
+// 获取实例、路由和路由器
+const route = useRoute()
+const router = useRouter()
+const { t } = useI18n() // 国际化
+
+// 字典定义
+const iot_device_type = ref([])
+const iot_network_method = ref([])
+const iot_vertificate_method = ref([])
+const iot_transport_type = ref([])
+const data_collect_type = ref([])
+const iot_location_way = ref([])
+const sub_gateway_type = ref([])
+
+// Refs
+const formRef = ref(null)
+const tempParamsRef = ref(null)
+const multipleTableRef = ref(null)
+const userFormRef = ref(null)
+const imageUploadRef = ref(null)
+const productThingsModelRef = ref(null)
+const productFirmwareRef = ref(null)
+const productAuthorizeRef = ref(null)
+const configSipRef = ref(null)
+const productScadaRef = ref(null)
+
+// Reactive 数据
+const dialogVisibleInterface = ref('MTG')
+const expression = ref('')
+const openCron = ref(false)
+
+// 输入框类型
+const keyInputType = ref('password')
+const accountInputType = ref('password')
+const passwordInputType = ref('password')
+
+// 选中选项卡
+const activeName = ref('basic')
+
+// 分类短列表
+const categoryShortList = ref([])
+
+// 协议列表
+const protocolList = ref([])
+
+// 组态相关按钮是否显示,true显示,false不显示
+const isShowScada = ref(false)
+
+// 表单参数
+const userParmas = reactive({
+  isLogin: false
+})
+
+const userStatus = ref(false)
+const userEdit = ref(false)
+
+const userRule = {
+  user: [
+    {
+      required: true,
+      message: '用户id不能为空',
+      trigger: 'blur'
+    }
+  ],
+  password: [
+    {
+      required: true,
+      message: '密码不能为空',
+      trigger: 'blur'
+    }
+  ],
+  url: [
+    {
+      required: true,
+      message: '登录地址不能为空',
+      trigger: 'blur'
+    }
+  ]
+}
+
+const form = reactive({
+  networkMethod: 1,
+  deviceType: 1,
+  vertificateMethod: 3,
+  transport: 'MQTT',
+  imgUrl: '',
+  locationWay: 1,
+  isSys: 0,
+
+  // --------http协议--------
+  responseKey: '',
+  methodType: 'get',
+  url: '',
+  data: {},
+  intervalType: '',
+  interval: '',
+  contentTypeNumber: 1,
+  requestType: '0',
+  paramsData: '',
+  cronExpression: '',
+  paramsDataList: [
+    {
+      key: '',
+      value: ''
+    }
+  ],
+  contentType: 1,
+  bodyDataTable: [
+    {
+      key: '',
+      value: ''
+    }
+  ],
+  bodyDataArea: '',
+  headersList: [
+    {
+      key: 'Authorization',
+      value: 'Bearer ' + getAccessToken()
+    }
+  ],
+  // --------http协议---------
+
+  //SNMP
+  oidListStr: '',
+  ipAddress: null,
+  community: null,
+  oidList: [
+    {
+      oidName: null,
+      oid: null
+    }
+  ]
+})
+
+const tempOpen = ref(false)
+
+// 表单校验
+const rules = {
+  productName: [
+    {
+      required: true,
+      message: t('product.product-edit473153-58'),
+      trigger: 'blur'
+    }
+  ],
+  categoryId: [
+    {
+      required: true,
+      message: t('product.product-edit473153-59'),
+      trigger: 'blur'
+    }
+  ],
+  deviceType: [
+    {
+      required: true,
+      message: t('product.product-edit473153-13'),
+      trigger: 'blur'
+    }
+  ],
+  protocolCode: [
+    {
+      required: true,
+      message: t('product.product-edit473153-60'),
+      trigger: 'blur'
+    }
+  ],
+  transport: [
+    {
+      required: true,
+      message: t('product.product-edit473153-61'),
+      trigger: 'blur'
+    }
+  ],
+  isSys: [
+    {
+      required: true,
+      message: t('product.product-edit473153-61'),
+      trigger: 'blur'
+    }
+  ]
+}
+
+// 查询参数
+const queryParams = reactive({
+  tenantName: null
+})
+
+const pointList = ref([])
+const open = ref(false)
+
+// 弹出层标题
+const title = ref('')
+
+const loading = ref(true)
+const tempList = ref([])
+
+// 总条数
+const total = ref(0)
+const tempTotal = ref(0)
+
+// 查询参数
+const pointsParams = reactive({
+  pageNum: 1,
+  pageSize: 8,
+  templateId: 0
+})
+
+const tempParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  templateName: ''
+})
+
+const currentRow = ref({})
+const selectRowData = ref({})
+const isModbus = ref(false)
+
+// Computed properties
+const networkOptions = computed(() => {
+  return iot_network_method.value
+})
+
+// Methods
+const userClose = () => {
+  if (userParmas.isLogin) {
+    for (let key in userParmas) {
+      if (!userParmas[key]) {
+        userParmas.isLogin = false
+      }
+    }
+  }
+}
+
+const changeIsLogin = (status) => {
+  if (status) {
+    Object.assign(userParmas, {
+      isLogin: true,
+      user: '',
+      password: '',
+      url: ''
+    })
+    userEdit.value = true
+  } else {
+    Object.assign(userParmas, { isLogin: false })
+  }
+}
+
+const cancelUser = () => {
+  Object.assign(userParmas, { isLogin: false })
+  userEdit.value = false
+}
+
+const confirmUser = () => {
+  userFormRef.value.validate((valid) => {
+    if (valid) {
+      userEdit.value = false
+    }
+  })
+}
+
+const deleteOid = (item, index) => {
+  form.oidList.splice(index, 1)
+}
+
+const addOid = (index) => {
+  form.oidList.push({
+    oidName: '',
+    oid: ''
+  })
+}
+
+/** cron表达式按钮操作 */
+const handleShowCron = () => {
+  expression.value = form.cronExpression
+  openCron.value = true
+}
+
+/** 确定后回传值 */
+const crontabFill = (value) => {
+  form.cronExpression = value
+}
+
+const handleEdit = (row) => {
+  let data = {
+    key: '',
+    value: ''
+  }
+  row.push(data)
+}
+
+const handleDelete = (xh, row) => {
+  if (row.length == 1) {
+    return
+  }
+  row.forEach((element, index) => {
+    if (index == xh) {
+      row.splice(index, 1)
+    }
+  })
+}
+
+
+
+// 获取简短分类列表
+const getShortCategory = () => {
+  listCategory().then((response) => {
+    categoryShortList.value = response.list
+  })
+}
+
+/** 返回按钮 */
+const goBack = () => {
+  // 关闭当前tab页签,打开新页签
+  const { delView } = useTagsViewStore()
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'VideoCenterProduct' })
+  reset()
+}
+
+/** 获取产品信息 */
+const getProductInfo = () => {
+  getProduct(form.productId).then((response) => {
+    Object.assign(form, response)
+   
+    if (form.transport === 'HTTP') {
+      Object.assign(userParmas, form.userParmas)
+      form.bodyDataTable = []
+      dialogVisibleInterface.value = 'HTTP'
+      form.headersList = JSON.parse(form.headers)
+      form.paramsDataList = JSON.parse(form.paramsData)
+
+      if (form.contentTypeNumber === 2 || form.contentTypeNumber === 3) {
+        let data1 = JSON.parse(form.data)
+        
+        var dataJSONObj = data1
+        if (Object.prototype.toString.call(data1) !== '[object Object]') {
+          dataJSONObj = JSON.parse(data1)
+        }
+        let data = Object.keys(dataJSONObj)
+        for (let i = 0; i < data.length; i++) {
+          let obj = {
+            key: '',
+            value: ''
+          }
+          obj.key = data[i]
+          obj.value = dataJSONObj[obj.key]
+          form.bodyDataTable.push(obj)
+          form.data = null
+        }
+      } else if (form.contentTypeNumber === 4 || form.contentTypeNumber === 5) {
+        form.bodyDataArea = JSON.parse(form.data)
+        form.bodyDataTable = [
+          {
+            key: '',
+            value: ''
+          }
+        ]
+      } else {
+        form.bodyDataTable = [
+          {
+            key: '',
+            value: ''
+          }
+        ]
+      }
+    }
+    if (form.transport === 'SNMP') {
+      dialogVisibleInterface.value = 'SNMP'
+      form.oidList = JSON.parse(form.oidListStr)
+    }
+    changeProductCode(form.protocolCode)
+  })
+}
+
+// 表单重置
+const reset = () => {
+  Object.assign(form, {
+    // --------http协议--------
+    responseKey: '',
+    methodType: '',
+    url: '',
+    data: {},
+    intervalType: '',
+    interval: '',
+    contentTypeNumber: 1,
+    requestType: '0',
+    paramsData: '',
+    cronExpression: '',
+    paramsDataList: [
+      {
+        key: '',
+        value: ''
+      }
+    ],
+    contentType: 1,
+    bodyDataTable: [
+      {
+        key: '',
+        value: ''
+      }
+    ],
+    bodyDataArea: '',
+    headersList: [
+      {
+        key: 'Authorization',
+        value: 'Bearer ' + getAccessToken()
+      }
+    ],
+    // --------http协议---------
+
+    productId: 0,
+    productName: null,
+    categoryId: null,
+    categoryName: null,
+    status: 0,
+    tslJson: null,
+    isAuthorize: 0,
+    deviceType: 1,
+    networkMethod: 1,
+    vertificateMethod: 3,
+    mqttAccount: null,
+    mqttPassword: null,
+    mqttSecret: null,
+    remark: null,
+    imgUrl: '',
+    locationWay: 1,
+    isSys: 0,
+
+    //SNMP
+    ipAddress: null,
+    community: null,
+    oidList: [
+      {
+        oidName: null,
+        oid: null
+      }
+    ],
+    oidListStr: ''
+  })
+  formRef.value?.resetFields()
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      let data = {}
+      let bodyDataTable = form.bodyDataTable
+
+      if (bodyDataTable) {
+        for (let itemindex = 0; itemindex < bodyDataTable.length; itemindex++) {
+          if (bodyDataTable[itemindex].key) {
+            // 在 Vue 3 中可以直接赋值,响应式系统会自动检测变化
+            data[bodyDataTable[itemindex].key] = bodyDataTable[itemindex].value
+          }
+        }
+      }
+      if (form.contentTypeNumber === 2) {
+        form.contentType = 'multipart/form-data'
+      } else if (form.contentTypeNumber === 3) {
+        form.contentType = 'application/x-www-form-urlencoded;charset=utf-8'
+      } else if (form.contentTypeNumber === 4) {
+        form.contentType = 'application/json;charset=utf-8'
+        data = form.bodyDataArea
+      } else if (form.contentTypeNumber === 5) {
+        form.contentType = 'text/xml'
+        data = form.bodyDataArea
+      } else {
+        form.contentType = 'application/json;charset=utf-8'
+      }
+
+      form.bodyDataTable = bodyDataTable
+      form.data = JSON.stringify(data)
+
+      if (form.transport === 'SNMP') {
+        form.oidListStr = JSON.stringify(form.oidList)
+      } else if (form.transport === 'HTTP') {
+        form.userParmas = userParmas
+      }
+      if (form.productId != null && form.productId != 0) {
+        form.headers = JSON.stringify(form.headersList)
+
+        updateProduct(form).then((response) => {
+          changeProductCode(form.protocolCode)
+          ElMessage.success(t('product.product-edit473153-62'))
+        })
+      } else {
+        if (tempOpen.value && !form.templateId) {
+          ElMessage('请选择采集点模板')
+          return
+        }
+
+        form.headers = JSON.stringify(form.headersList)
+
+        // console.log(form, 'this.form');
+        addProduct(form).then((response) => {
+          if (!form.isModbus) {
+            ElMessage.success(t('product.product-edit473153-64'))
+          } else {
+            ElMessage.success('物模型已经从采集点模板同步至产品')
+          }
+          Object.assign(form, response.data)
+          changeProductCode(form.protocolCode)
+        })
+      }
+    }
+  })
+}
+
+/**同步获取产品下的设备数量**/
+const getDeviceCountByProductId = (productId) => {
+  return new Promise((resolve, reject) => {
+    deviceCount(productId)
+      .then((res) => {
+        resolve(res)
+      })
+      .catch((error) => {
+        reject(error)
+      })
+  })
+}
+
+/** 更新产品状态 */
+const changeProductStatusAsync = async (status) => {
+  let message = t('product.product-edit473153-66')
+  if (status == 2) {
+    message = t('product.product-edit473153-67')
+  } else if (status == 1) {
+    let result = await getDeviceCountByProductId(form.productId)
+    if (result.data > 0) {
+      message = t('product.product-edit473153-68', [result.data])
+    }
+  }
+
+  ElMessageBox.confirm(message, t('product.product-edit473153-69'), {
+    confirmButtonText: t('product.product-edit473153-70'),
+    cancelButtonText: t('product.product-edit473153-71'),
+    type: 'warning'
+  })
+    .then(() => {
+      let data = {}
+      data.productId = form.productId
+      data.status = status
+      data.deviceType = form.deviceType
+      changeProductStatus(data)
+        .then((response) => {
+          ElMessage.success(response.msg)
+          activeName.value = 'basic'
+          getProductInfo()
+        })
+        .catch(() => {
+          if (status == 2) {
+            activeName.value = 'basic'
+          } else {
+            goBack()
+          }
+        })
+    })
+    .catch(() => {
+      activeName.value = 'basic'
+    })
+}
+
+/** 选择分类 */
+const selectCategory = (val) => {
+  for (let i = 0; i < categoryShortList.value.length; i++) {
+   
+    if (categoryShortList.value[i].categoryId == val) {
+      
+      form.categoryName = categoryShortList.value[i].categoryName
+      return
+    }
+  }
+}
+
+/**获取上传图片的路径 */
+const getImagePath = (data) => {
+  form.imgUrl = data
+}
+
+/**改变输入框类型**/
+const changeInputType = (name) => {
+  if (name == 'key') {
+    keyInputType.value = keyInputType.value == 'password' ? 'text' : 'password'
+  } else if (name == 'account') {
+    accountInputType.value = accountInputType.value == 'password' ? 'text' : 'password'
+  } else if (name == 'password') {
+    passwordInputType.value = passwordInputType.value == 'password' ? 'text' : 'password'
+  }
+}
+
+// 授权码状态修改
+const changeIsAuthorize = () => {
+  console.log('changeIsAuthorize>>>>>>>>>>>>>>>')
+  let text =
+    form.isAuthorize == '1'
+      ? t('product.product-edit473153-72')
+      : t('product.product-edit473153-74')
+
+  ElMessageBox.confirm(t('product.product-edit473153-75', [text]), '提示', {
+    confirmButtonText: t('product.product-edit473153-70'),
+    cancelButtonText: t('product.product-edit473153-71'),
+    type: 'warning'
+  })
+    .then(() => {
+      if (form.productId != null && form.productId != 0) {
+        updateProduct(form).then((response) => {
+          ElMessage.success(t('product.product-edit473153-77') + text)
+        })
+      }
+    })
+    .catch(() => {
+      form.isAuthorize = 0
+    })
+}
+
+//获取设备协议
+const getProtocol = () => {
+  const data = {
+    protocolStatus: 1,
+    pageSize: 99
+  }
+  listProtocol(data).then((res) => {
+    protocolList.value = res.list
+  })
+}
+
+/*选择模板*/
+const selectTemplate = () => {
+  // reset();
+  open.value = true
+  title.value = '选择模板'
+  getTempList()
+  // getList()
+}
+
+const getTempDetail = () => {
+  const params = {
+    productId: form.productId
+  }
+  getTempByPId(params).then((response) => {
+    selectRowData.value = response.data
+  })
+}
+
+// 取消按钮
+const cancel = () => {
+  open.value = false
+  // reset();
+}
+
+/** 查询设备采集变量模板列表 */
+const getTempList = () => {
+  loading.value = true
+  listTemp(tempParams).then((response) => {
+    tempList.value = response.list
+    tempTotal.value = response.total
+    currentRow.value = tempList.value[0]
+    // pointsParams.templateId = currentRow.value.templateId;
+    getCurrentRow(tempList.value[0])
+    loading.value = false
+    getList()
+  })
+}
+
+const getList = () => {
+  getAllPoints(pointsParams).then((response) => {
+    
+    pointList.value = response
+    total.value = response.length || 0
+  })
+}
+
+/*确认选择*/
+const submitSelect = () => {
+  open.value = false
+  form.templateId = selectRowData.value.templateId
+}
+
+const getCurrentRow = (val) => {
+  console.log('getCurrentRow>>>>>>>>>>>>>>>', val)
+  if (val != null) {
+    selectRowData.value = val
+  }
+  pointsParams.templateId = val.templateId
+  getList()
+}
+
+const deleteData = () => {
+  selectRowData.value = {}
+  form.templateId = null
+}
+
+const transportChange = (val) => {
+  if (val === 'HTTP') {
+    dialogVisibleInterface.value = 'HTTP'
+  } else if (val === 'SNMP') {
+    dialogVisibleInterface.value = 'SNMP'
+  } else {
+    dialogVisibleInterface.value = 'MTG'
+  }
+}
+
+const changeProductCode = (val) => {
+  if (val && val.startsWith('MODBUS')) {
+    tempOpen.value = true
+    form.deviceType = 2
+    form.isModbus = true
+    if (form.productId != 0 && form.productId != null) {
+      getTempDetail()
+    }
+  } else {
+    form.isModbus = false
+    tempOpen.value = false
+  }
+}
+
+/**选项卡切换事件**/
+const tabChange = (tabItem) => {
+  // 切换到告警配置,获取物模型
+  if (tabItem === 'alert') {
+    productThingsModelRef.value.getCacheThingsModel(form.productId)
+  } else if (tabItem === 'productFirmware') {
+    productFirmwareRef.value.getList()
+  } else if (tabItem === 'productAuthorize') {
+    productAuthorizeRef.value.queryParams.productId = form.productId
+    productAuthorizeRef.value.getList()
+  }
+}
+
+/*按照模板名查询*/
+const queryTemp = () => {
+  getTempList()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  tempParams.pageNum = 1
+  getTempList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  tempParamsRef.value?.resetFields()
+  handleQuery()
+}
+
+/**刷新产品模型 */
+const updateModel = () => {
+  productModbusRef.value.getThingsModelList()
+}
+
+// Lifecycle hooks
+onMounted(() => {
+  // 获取产品信息
+  const productId = route.query && route.query.productId
+  form.productId = productId
+  if (form.productId != 0 && form.productId != null) {
+    getProductInfo()
+  }
+  // 切换选项卡
+  const tabPanelName = route.query && route.query.tabPanelName
+  if (tabPanelName != null && tabPanelName != '') {
+    activeName.value = tabPanelName
+  }
+  // 获取分类信息
+  getShortCategory()
+  // 设置账号密码输入框类型,新增时为text,查看时为password
+  if (!form.productId || form.productId == 0) {
+    accountInputType.value = 'text'
+    passwordInputType.value = 'text'
+  }
+  getProtocol()
+
+  getDicts('iot_device_type').then((response) => {
+    iot_device_type.value = response
+  })
+
+  getDicts('iot_transport_type').then((response) => {
+    iot_transport_type.value = response
+  })
+
+  // iot_network_method
+  getDicts('iot_network_method').then((response) => {
+   
+    iot_network_method.value = response
+  })
+
+  getDicts('iot_vertificate_method').then((response) => {
+    iot_vertificate_method.value = response
+  })
+  getDicts('data_collect_type').then((response) => {
+    data_collect_type.value = response
+  })
+  getDicts('iot_location_way').then((response) => {
+    iot_location_way.value = response
+  })
+  getDicts('sub_gateway_type').then((response) => {
+    sub_gateway_type.value = response
+  })
+})
+</script>
+
+<style lang="scss" scoped>
+.el-aside {
+  margin: 0;
+  padding: 0;
+  background-color: #fff;
+  color: #333;
+}
+
+.el-main {
+  margin: 0;
+  padding: 0 10px;
+  background-color: #fff;
+  color: #333;
+}
+
+.base-table.table-btn {
+  position: absolute;
+  right: -40px;
+  top: 33%;
+  transform: translate(0, -50%);
+}
+::v-deep .el-alert__description {
+ font-size: 12px;
+}
+</style>

+ 332 - 0
src/views/pms/video_center/product/product-firmware.vue

@@ -0,0 +1,332 @@
+<template>
+    <div style="padding-left: 20px">
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:firmware:add']">
+                    {{ $t('product.product-firmware.420545-0') }}
+                </el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList" v-hasPermi="['iot:firmware:list']">{{ $t('product.product-firmware.420545-1') }}</el-button>
+            </el-col>
+        </el-row>
+
+        <el-table :border="false" v-loading="loading" :data="firmwareList" @selection-change="handleSelectionChange" size="small">
+            <el-table-column :label="$t('product.product-firmware.420545-2')" align="center" prop="firmwareName" />
+            <el-table-column :label="$t('product.product-authorize.314975-41')" align="center" prop="version" width="120">
+                <template slot-scope="scope">
+                    <span>Version</span>
+                    {{ scope.row.version }}
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('status')" align="center" prop="isLatest" width="80">
+                <template slot-scope="scope">
+                    <el-tag type="success" v-if="scope.row.isLatest == 1">{{ $t('product.product-firmware.420545-5') }}</el-tag>
+                    <el-tag type="info" v-else>{{ $t('product.product-firmware.420545-6') }}</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-firmware.420545-7')" align="center" prop="createTime" width="100">
+                <template slot-scope="scope">
+                    <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-firmware.420545-8')" align="center" prop="filePath" min-width="100">
+                <template slot-scope="scope">
+                    <el-link :href="getDownloadUrl(scope.row.filePath)" :underline="false" type="success">{{ getDownloadUrl(scope.row.filePath) }}</el-link>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-firmware.420545-9')" align="center" prop="remark" min-width="200" />
+            <el-table-column :label="$t('product.product-firmware.420545-10')" align="center" class-name="small-padding fixed-width" width="200">
+                <template slot-scope="scope">
+                    <el-button size="small" type="primary" style="padding: 5px" icon="el-icon-view" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:firmware:query']" @click="handleUpdate(scope.row)">
+                        {{ $t('product.product-firmware.420545-11') }}
+                    </el-button>
+                    <el-button size="small" type="danger" style="padding: 5px" icon="el-icon-delete" v-if="productInfo.isOwner != 0" v-hasPermi="['iot:firmware:remove']" @click="handleDelete(scope.row)">
+                        {{ $t('product.product-firmware.420545-12') }}
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <!-- 添加或修改产品固件对话框 -->
+        <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+            <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+                <el-form-item :label="$t('product.product-firmware.420545-2')" prop="firmwareName">
+                    <el-input v-model="form.firmwareName" :placeholder="$t('product.product-firmware.420545-13')" />
+                </el-form-item>
+                <el-form-item :label="$t('product.product-firmware.420545-3')" prop="version">
+                    <el-input v-model="form.version" :placeholder="$t('product.product-firmware.420545-14')" type="number" step="0.1" />
+                </el-form-item>
+                <el-form-item :label="$t('product.product-firmware.420545-15')" prop="isLatest">
+                    <el-switch v-model="form.isLatest" active-text="" inactive-text="" :active-value="1" :inactive-value="0"></el-switch>
+                    <el-link type="info" :underline="false" style="font-size: 12px; margin-left: 15px">{{ $t('product.product-firmware.420545-16') }}</el-link>
+                </el-form-item>
+                <el-form-item :label="$t('product.product-firmware.420545-17')" prop="filePath">
+                    <fileUpload ref="file-upload" :value="form.filePath" :limit="1" :fileSize="10" :fileType="['bin', 'zip', 'pdf']" @input="getFilePath($event)"></fileUpload>
+                </el-form-item>
+                <el-form-item :label="$t('product.product-firmware.420545-9')" prop="remark">
+                    <el-input v-model="form.remark" type="textarea" rows="4" :placeholder="$t('product.product-firmware.420545-18')" />
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm" v-hasPermi="['iot:firmware:add']" v-show="!form.firmwareId">{{ $t('product.product-firmware.420545-19') }}</el-button>
+                <el-button type="primary" @click="submitForm" v-hasPermi="['iot:firmware:edit']" v-show="form.firmwareId">{{ $t('product.product-firmware.420545-20') }}</el-button>
+                <el-button @click="cancel">{{ $t('product.product-firmware.420545-21') }}</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<script>
+import fileUpload from '../../../components/FileUpload/index';
+import { listFirmware, getFirmware, delFirmware, addFirmware, updateFirmware } from '@/api/iot/firmware';
+import { getToken } from '@/utils/auth';
+
+export default {
+    name: 'product-firmware',
+    dicts: ['iot_yes_no'],
+    components: {
+        fileUpload,
+    },
+    props: {
+        product: {
+            type: Object,
+            default: null,
+        },
+    },
+    watch: {
+        // 获取到父组件传递的productId后,刷新列表
+        product: function (newVal, oldVal) {
+            this.productInfo = newVal;
+            if (this.productInfo && this.productInfo.productId != 0) {
+                this.queryParams.productId = this.productInfo.productId;
+                this.form.productId = this.productInfo.productId;
+                this.form.productName = this.productInfo.productName;
+                this.getList();
+            }
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 产品固件表格数据
+            firmwareList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 100,
+                firmwareName: null,
+                productName: null,
+                productId: 0,
+                isSys: null,
+            },
+            // 产品
+            productInfo: {},
+            // 表单参数
+            form: {
+                version: 1.0,
+            },
+            // 表单校验
+            rules: {
+                firmwareName: [
+                    {
+                        required: true,
+                        message: this.$t('product.product-firmware.420545-24'),
+                        trigger: 'blur',
+                    },
+                ],
+                version: [
+                    {
+                        required: true,
+                        message: this.$t('product.product-firmware.420545-25'),
+                        trigger: 'blur',
+                    },
+                ],
+                filePath: [
+                    {
+                        required: true,
+                        message: this.$t('product.product-firmware.420545-26'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+            // 上传参数
+            upload: {
+                // 是否禁用上传
+                isUploading: false,
+                // 设置上传的请求头部
+                headers: {
+                    Authorization: 'Bearer ' + getToken(),
+                },
+                // 上传的地址
+                url: process.env.VUE_APP_BASE_API + '/iot/tool/upload',
+                // 上传的文件列表
+                fileList: [],
+            },
+        };
+    },
+    created() {},
+    methods: {
+        getDownloadUrl(path) {
+            return window.location.origin + process.env.VUE_APP_BASE_API + path;
+        },
+        /** 查询产品固件列表 */
+        getList() {
+            this.loading = true;
+            listFirmware(this.queryParams).then((response) => {
+                this.firmwareList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                firmwareId: null,
+                firmwareName: null,
+                tenantId: null,
+                tenantName: null,
+                productId: this.form.productId,
+                productName: this.form.productName,
+                isSys: null,
+                isLatest: 0,
+                version: 1.0,
+                filePath: null,
+                delFlag: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.firmwareId);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /** 新增按钮操作 */
+        handleAdd() {
+            this.reset();
+            this.open = true;
+            this.title = this.$t('product.product-firmware.420545-27');
+            this.upload.fileList = [];
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            const firmwareId = row.firmwareId || this.ids;
+            getFirmware(firmwareId).then((response) => {
+                this.form = response.data;
+                this.open = true;
+                this.title = this.$t('product.product-firmware.420545-28');
+                this.upload.fileList = [
+                    {
+                        name: this.form.firmwareName,
+                        url: this.form.filePath,
+                    },
+                ];
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    if (this.form.firmwareId != null) {
+                        updateFirmware(this.form).then((response) => {
+                            this.$modal.msgSuccess(this.$t('product.product-firmware.420545-29'));
+                            this.open = false;
+                            this.getList();
+                        });
+                    } else {
+                        addFirmware(this.form).then((response) => {
+                            this.$modal.msgSuccess(this.$t('product.product-firmware.420545-30'));
+                            this.open = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const firmwareIds = row.firmwareId || this.ids;
+            this.$modal
+                .confirm(this.$t('product.product-firmware.420545-31', [firmwareIds]))
+                .then(function () {
+                    return delFirmware(firmwareIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('product.product-firmware.420545-32'));
+                })
+                .catch(() => {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/firmware/export',
+                {
+                    ...this.queryParams,
+                },
+                `firmware_${new Date().getTime()}.xlsx`
+            );
+        },
+        // 获取文件路径
+        getFilePath(data) {
+            this.form.filePath = data;
+        },
+
+        // 文件提交处理
+        submitUpload() {
+            this.$refs.upload.submit();
+        },
+        // 文件上传中处理
+        handleFileUploadProgress(event, file, fileList) {
+            this.upload.isUploading = true;
+        },
+        // 文件上传成功处理
+        handleFileSuccess(response, file, fileList) {
+            this.upload.isUploading = false;
+            this.form.filePath = response.url;
+            this.$modal.msgSuccess(response.msg);
+        },
+        // 文件下载处理
+        handleDownload(row) {
+            window.open(process.env.VUE_APP_BASE_API + row.filePath);
+        },
+    },
+};
+</script>

+ 743 - 0
src/views/pms/video_center/product/product-modbus.vue

@@ -0,0 +1,743 @@
+<template>
+    <div class="app-container">
+        <div class="head">
+            <div class="head-title">
+                <h2 class="title">{{ $t('product.product-modbus.562372-0') }}</h2>
+                <span class="tips">{{ $t('product.product-modbus.562372-1') }}</span>
+            </div>
+            <div class="button-group">
+                <el-button type="primary" plain icon="el-icon-edit" size="mini" @click="setSlave" v-if="!enableSetSlave && productInfo.status == 1">{{ $t('product.product-modbus.562372-2') }}</el-button>
+                <el-button type="primary" plain size="mini" @click="saveSlave" v-if="enableSetSlave">{{ $t('product.product-modbus.562372-3') }}</el-button>
+                <el-button type="info" plain size="mini" @click="cancelSlave" v-if="enableSetSlave">{{ $t('product.product-modbus.562372-4') }}</el-button>
+            </div>
+            <el-form ref="form" :model="form" label-width="150px">
+                <el-form-item :label="$t('product.product-modbus.562372-5')">
+                    <el-tooltip placement="top">
+                        <div slot="content" class="test_div">
+                            {{ $t('product.product-modbus.562372-6') }}
+                            <br />
+                            {{ $t('product.product-modbus.562372-7') }}
+                            <br />
+                            {{ $t('product.product-modbus.562372-8') }}
+                        </div>
+                        <!-- elementui图标库:显示黑色的问号图标   -->
+                        <i class="el-icon-question" style="color: dodgerblue" />
+                    </el-tooltip>
+                    <el-radio-group :disabled="!enableSetSlave" v-model="form.statusDeter">
+                        <el-radio v-for="dict in dict.type.device_status_deter" :key="dict.value" :label="Number(dict.value)">
+                            {{ dict.label }}
+                        </el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item v-if="form.statusDeter == '1'" :label="$t('product.product-modbus.562372-9')">
+                    <span>
+                        <el-tooltip placement="top">
+                            <div slot="content" class="test_div">
+                                {{ $t('product.product-modbus.562372-10') }}
+                            </div>
+                            <!-- elementui图标库:显示黑色的问号图标   -->
+                            <i class="el-icon-question" style="color: dodgerblue" />
+                        </el-tooltip>
+                    </span>
+                    <el-select :disabled="!enableSetSlave" v-model="form.deterTimer" :placeholder="$t('product.product-modbus.562372-11')">
+                        <el-option v-for="dict in dict.type.iot_modbus_poll_time" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                    </el-select>
+                    <span style="font-size: small; color: darkgrey">{{ $t('product.product-modbus.562372-12') }}</span>
+                </el-form-item>
+                <el-form-item :label="$t('product.product-modbus.562372-13')">
+                    <el-radio-group :disabled="!enableSetSlave" v-model="form.pollType">
+                        <el-radio v-for="dict in dict.type.data_collect_type" :key="dict.value" :label="Number(dict.value)">{{ dict.label }}</el-radio>
+                    </el-radio-group>
+                </el-form-item>
+                <el-form-item :label="$t('product.product-modbus.562372-14')">
+                    <el-input-number :disabled="!enableSetSlave" v-model="form.slaveId" size="small" :min="1" :max="256" :label="$t('product.product-modbus.562372-15')"></el-input-number>
+                    <span style="font-size: small; color: darkgrey">{{ $t('product.product-modbus.562372-16') }}</span>
+                </el-form-item>
+            </el-form>
+        </div>
+        <div class="head">
+            <div class="head-title">
+                <h2 class="title">{{ $t('product.product-modbus.562372-17') }}</h2>
+                <span class="tips">{{ $t('product.product-modbus.562372-18') }}</span>
+            </div>
+            <div class="button-group">
+                <el-button type="primary" plain icon="el-icon-edit" size="mini" @click="editIOModbus" v-if="!enableEditIO && productInfo.status == 1">{{ $t('product.product-modbus.562372-19') }}</el-button>
+                <el-button type="primary" plain size="mini" @click="submitFormIO" v-if="enableEditIO">{{ $t('product.product-modbus.562372-22') }}</el-button>
+                <el-button type="info" plain size="mini" @click="handleCancelIO" v-if="enableEditIO">{{ $t('product.product-modbus.562372-23') }}</el-button>
+                <div class="right-btns">
+                    <el-button type="primary" plain icon="el-icon-edit" v-if="productInfo.status == 1" size="mini" @click.stop="batchImport('isSelectIo')">{{ $t('product.product-modbus.562372-20') }}</el-button>
+                    <el-button plain icon="el-icon-edit" v-if="productInfo.status == 1" size="mini" @click.stop="exportModbus('isSelectIo')">{{ $t('product.product-modbus.562372-21') }}</el-button>
+                </div>
+            </div>
+        </div>
+
+        <el-table :border="false" style="margin-bottom: 10px" v-loading="loadingIO" :data="configList" :key="configTableKey" data-key="id" ref="IOTable">
+            <el-table-column :label="$t('product.product-modbus.562372-24')" align="center" prop="sort"></el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-25')" align="center" prop="identifier">
+                <template slot-scope="scope">
+                    <el-select
+                        filterable
+                        :disabled="!enableEditIO"
+                        v-model="scope.row.identifier"
+                        :placeholder="$t('product.product-modbus.562372-26')"
+                        :ref="`selectIo${scope.$index}`"
+                        size="mini"
+                        @change="(e) => updateSelectThingsModel({ newVal: e, oldVal: $refs[`selectIo${scope.$index}`].value, justiceSelect: 'isSelectIo' })"
+                    >
+                        <el-option key="0" label="" value="" disabled style="width: 300px">
+                            <span style="float: left">{{ $t('product.product-modbus.562372-27') }}</span>
+                            <span style="float: right; color: #8492a6; font-size: 13px">{{ $t('product.product-modbus.562372-28') }}</span>
+                        </el-option>
+                        <el-option v-for="item in thingsModelList" :key="item.identifier" :label="`${item.modelName} (${item.identifier})`" :value="item.identifier" style="width: 300px" :disabled="!item.isSelectIo">
+                            <span style="float: left">{{ item.modelName }}</span>
+                            <span style="float: right; color: #8492a6; font-size: 13px">{{ item.identifier }}</span>
+                        </el-option>
+                    </el-select>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-29')" align="center" prop="slave">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditIO" v-model="scope.row.slave" size="small" :min="1" :max="256" :label="$t('product.product-modbus.562372-29')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-30')" align="center" prop="address">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditIO" v-model="scope.row.address" size="small" :min="0" :max="400000" :label="$t('product.product-modbus.562372-30')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-31')" align="center" prop="isReadonly">
+                <template slot-scope="scope">
+                    <el-radio-group :disabled="!enableEditIO" size="mini" v-model="scope.row.isReadonly">
+                        <el-radio-button label="0">{{ $t('product.product-modbus.562372-32') }}</el-radio-button>
+                        <el-radio-button label="1">{{ $t('product.product-modbus.562372-33') }}</el-radio-button>
+                    </el-radio-group>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-311')" align="center" prop="bitOrder">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditIO" v-model="scope.row.bitOrder" size="small" :min="0" :max="15" :label="$t('product.product-modbus.562372-311')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-34')" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <el-button v-if="enableEditIO" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row, scope.$index, configList, 'isSelectIo')" v-hasPermi="['iot:config:remove']">
+                        {{ $t('product.product-modbus.562372-35') }}
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="total > 0" :total="total" :page.sync="queryParamsIO.pageNum" :limit.sync="queryParamsIO.pageSize" @pagination="getIOList" />
+
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-if="enableEditIO">{{ $t('product.product-modbus.562372-36') }}</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddBatch('isSelectIo')" v-if="enableEditIO">{{ $t('product.product-modbus.562372-37') }}</el-button>
+            </el-col>
+        </el-row>
+
+        <div class="head">
+            <div class="head-title">
+                <h2 class="title">{{ $t('product.product-modbus.562372-38') }}</h2>
+                <span class="tips">{{ $t('product.product-modbus.562372-39') }}</span>
+            </div>
+            <div class="button-group">
+                <el-button type="primary" plain icon="el-icon-edit" size="mini" @click="editDataModbus" v-if="!enableEditData && productInfo.status == 1">{{ $t('product.product-modbus.562372-19') }}</el-button>
+                <el-button type="primary" plain size="mini" @click="submitFormData" v-if="enableEditData">{{ $t('product.product-modbus.562372-22') }}</el-button>
+                <el-button type="info" plain size="mini" @click="handleCancelData" v-if="enableEditData">{{ $t('product.product-modbus.562372-23') }}</el-button>
+                <div class="right-btns">
+                    <el-button type="primary" plain icon="el-icon-edit" v-if="productInfo.status == 1" size="mini" @click.stop="batchImport('isSelectData')">{{ $t('product.product-modbus.562372-40') }}</el-button>
+                    <el-button plain icon="el-icon-edit" v-if="productInfo.status == 1" size="mini" @click.stop="exportModbus('isSelectIo')">{{ $t('product.product-modbus.562372-41') }}</el-button>
+                </div>
+            </div>
+        </div>
+
+        <el-table :border="false" style="margin-bottom: 10px" v-loading="loadingData" :data="dataModbusList" :key="dataTableKey" ref="Dataable">
+            <el-table-column :label="$t('product.product-modbus.562372-24')" align="center" prop="sort"></el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-25')" align="center" prop="identifier" width="250px">
+                <template slot-scope="scope">
+                    <el-select
+                        filterable
+                        :disabled="!enableEditData"
+                        v-model="scope.row.identifier"
+                        :placeholder="$t('product.product-modbus.562372-26')"
+                        size="mini"
+                        :ref="`selectData${scope.$index}`"
+                        @change="(e) => updateSelectThingsModel({ newVal: e, oldVal: $refs[`selectData${scope.$index}`].value, justiceSelect: 'isSelectData' })"
+                    >
+                        <el-option key="0" label="" value="" disabled style="width: 300px">
+                            <span style="float: left">{{ $t('product.product-modbus.562372-27') }}</span>
+                            <span style="float: right; color: #8492a6; font-size: 13px">{{ $t('product.product-modbus.562372-28') }}</span>
+                        </el-option>
+                        <el-option v-for="item in thingsModelList" :key="item.identifier" :label="`${item.modelName} (${item.identifier})`" :value="item.identifier" style="width: 300px" :disabled="!item.isSelectData">
+                            <!-- 判断是否可选 -->
+                            <span style="float: left">{{ item.modelName }}</span>
+                            <span style="float: right; color: #8492a6; font-size: 13px">{{ item.identifier }}</span>
+                        </el-option>
+                    </el-select>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-29')" align="center" prop="identifier" width="170px">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditData" v-model="scope.row.slave" size="small" :min="1" :max="256" :label="$t('product.product-modbus.562372-29')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-30')" align="center" prop="address" width="170px">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditData" v-model="scope.row.address" size="small" :min="0" :max="400000" :label="$t('product.product-modbus.562372-30')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-31')" align="center" prop="isReadonly" width="170px">
+                <template slot-scope="scope">
+                    <el-radio-group :disabled="!enableEditData" size="mini" v-model="scope.row.isReadonly">
+                        <el-radio-button label="0">{{ $t('product.product-modbus.562372-32') }}</el-radio-button>
+                        <el-radio-button label="1">{{ $t('product.product-modbus.562372-33') }}</el-radio-button>
+                    </el-radio-group>
+                    <!-- <el-tag v-show="!enableEditData">{{scope.row.isReadonly=='0'?'只读':'读写'}}</el-tag> -->
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-42')" align="center" prop="dataType" width="200px">
+                <template slot-scope="scope">
+                    <el-select :disabled="!enableEditData" size="mini" v-model="scope.row.dataType" :placeholder="$t('product.product-modbus.562372-42')" style="display: inline-block; padding-right: 10px">
+                        <el-option v-for="dict in dict.type.iot_modbus_data_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                    </el-select>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-43')" align="center" prop="quantity" width="170px">
+                <template slot-scope="scope">
+                    <el-input-number :disabled="!enableEditData" v-model="scope.row.quantity" size="small" :min="1" :max="256" :label="$t('product.product-modbus.562372-43')"></el-input-number>
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('product.product-modbus.562372-34')" align="center" class-name="small-padding fixed-width">
+                <template slot-scope="scope">
+                    <el-button v-if="enableEditData" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row, scope.$index, dataModbusList, 'isSelectData')" v-hasPermi="['iot:config:remove']">
+                        {{ $t('product.product-modbus.562372-35') }}
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+
+        <pagination v-show="dataTotal > 0" :total="dataTotal" :page.sync="queryParamsData.pageNum" :limit.sync="queryParamsData.pageSize" @pagination="getDataList" />
+
+        <el-row :gutter="10" class="mb8">
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddData" v-if="enableEditData">{{ $t('product.product-modbus.562372-44') }}</el-button>
+            </el-col>
+            <el-col :span="1.5">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAddBatch('isSelectData')" v-if="enableEditData">{{ $t('product.product-modbus.562372-45') }}</el-button>
+            </el-col>
+        </el-row>
+
+        <!-- 选择物模型 -->
+        <things-list ref="thingsListRef" :productId="productId" @productEvent="getThingsData($event)" :justiceSelect="justiceSelect" />
+        <!-- 批量导入 -->
+        <import-batch ref="importBatchRef" :productId="productId" @productEvent="getThingsData($event)" :justiceSelect="justiceSelect" />
+    </div>
+</template>
+
+<script>
+import { listConfig, getConfig, delConfig, addConfig, updateConfig, addBatch } from '@/api/iot/modbusConfig';
+import { getlListModbus } from '@/api/iot/model';
+import thingsList from './things-model-list';
+import importBatch from './components/batchImportModbus';
+import Sortable from 'sortablejs';
+import { addParams, getByProductId, updateParams } from '@/api/iot/params';
+import { addFirmware, updateFirmware } from '@/api/iot/firmware';
+export default {
+    name: 'product-modbus',
+    dicts: ['iot_modbus_data_type', 'iot_yes_no', 'data_collect_type', 'device_status_deter', 'iot_modbus_poll_time'],
+    props: {
+        product: {
+            type: Object,
+            default: null,
+        },
+    },
+    components: {
+        thingsList,
+        importBatch,
+    },
+    watch: {
+        // 获取到父组件传递的productId后,刷新列表
+        product: function (newVal, oldVal) {
+            this.productInfo = newVal;
+            if (this.productInfo && this.productInfo.productId != 0) {
+                this.thingsModelParams.productId = this.productInfo.productId;
+                this.queryParamsIO.productId = this.productInfo.productId;
+                this.queryParamsData.productId = this.productInfo.productId;
+                this.productId = this.productInfo.productId;
+                this.getIOList();
+                this.getDataList();
+                this.getThingsModelList();
+                this.getParams();
+            }
+        },
+        enableEditIO: function (n, old) {
+            //设置行是否拖拽
+            if (this.sortableIo) this.sortableIo.option('disabled', !n);
+            //关闭编辑刷新页面
+            if (!n) {
+                this.getIOList();
+                this.getThingsModelList();
+            }
+            //清空删除数组
+            this.delIoIds = [];
+            this.getThingsModelList();
+        },
+        enableEditData: function (n, old) {
+            //设置行是否拖拽
+            if (this.sortableData) this.sortableData.option('disabled', !n);
+            //关闭编辑刷新页面
+            if (!n) {
+                this.getDataList();
+                this.getThingsModelList();
+            }
+            //清空删除数组
+            this.delDataIds = [];
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loadingIO: true,
+            loadingData: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // modbus配置表格数据
+            configList: [],
+            //io表格是否可以编辑
+            enableEditIO: false,
+            //子设备配置是否可编辑
+            enableSetSlave: false,
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParamsIO: {
+                pageNum: 1,
+                pageSize: 10,
+                identifier: null,
+                address: null,
+                isReadonly: null,
+                dataType: null,
+                quantity: null,
+                type: 1,
+                productId: 0,
+            },
+            // 查询参数
+            queryParamsData: {
+                pageNum: 1,
+                pageSize: 10,
+                identifier: null,
+                address: null,
+                isReadonly: null,
+                dataType: null,
+                quantity: null,
+                type: 2,
+                productId: 0,
+            },
+            //产品id
+            productId: 0,
+            //数据寄存器
+            dataModbusList: [],
+            //io表格是否可以编辑
+            enableEditData: false,
+            //数据寄存器总条数
+            dataTotal: 0,
+            // 数据寄存器遮罩层
+            dataLoading: true,
+
+            //可选属性
+            thingsModelList: [],
+            //物模型遮罩层
+            thingsLoading: true,
+            //thingsModel
+            thingsModelParams: {
+                pageNum: 1,
+                pageSize: 1000,
+                productId: 0,
+            },
+            thingsTotal: 0,
+            // 表单参数
+            form: {
+                statusDeter: 1,
+                slaveId: 1,
+                pollType: 0,
+                deterTimer: '300',
+            },
+            productInfo: {},
+            // 表单校验
+            rules: {
+                identifier: [{ required: true, message: this.$t('product.product-modbus.562372-46'), trigger: 'blur' }],
+                address: [{ required: true, message: this.$t('product.product-modbus.562372-47'), trigger: 'blur' }],
+                isReadonly: [{ required: true, message: this.$t('product.product-modbus.562372-48'), trigger: 'blur' }],
+                dataType: [{ required: true, message: this.$t('product.product-modbus.562372-49'), trigger: 'change' }],
+                quantity: [{ required: true, message: this.$t('product.product-modbus.562372-50'), trigger: 'blur' }],
+                type: [{ required: true, message: this.$t('product.product-modbus.562372-51'), trigger: 'change' }],
+            },
+            //IO行拖拽对象
+            sortableIo: null,
+            //ioTable的key值,主要用于拖拽后重绘
+            configTableKey: 0,
+            //ioTable删除的id
+            delIoIds: [],
+            //数据寄存器行拖拽对象
+            sortableData: null,
+            //dataTable的key值,主要用于拖拽后重绘
+            dataTableKey: 1000,
+            //dataTable删除的id
+            delDataIds: [],
+            //判断多选物模型是io还是data,isSelectIo是判断是否可选io的,isSelectData是判断数据寄存器的,都是false不可选,true可选
+            justiceSelect: 'isSelectData',
+        };
+    },
+    mounted() {
+        this.rowDropIo();
+        this.rowDropData();
+    },
+    methods: {
+        /** 查询modbusIO寄存器列表 */
+        getIOList() {
+            this.loadingIO = true;
+            listConfig(this.queryParamsIO).then((response) => {
+                this.configList = response.rows;
+                this.total = response.total;
+                this.loadingIO = false;
+            });
+        },
+        /** 查询modbus数据寄存器列表 */
+        getDataList() {
+            this.loadingData = true;
+            listConfig(this.queryParamsData).then((response) => {
+                this.dataModbusList = response.rows;
+                this.dataTotal = response.total;
+                this.loadingData = false;
+            });
+            this.$nextTick(() => {
+                this.$refs.Dataable.bodyWrapper.scrollTop = 0;
+            });
+        },
+        /** 查询产品物模型列表 */
+        getThingsModelList() {
+            this.thingsLoading = true;
+            getlListModbus(this.thingsModelParams).then((response) => {
+                this.$refs.thingsListRef.modelList = response.rows;
+                this.thingsModelList = response.rows;
+                this.thingsTotal = response.total;
+                this.thingsLoading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                statusDeter: 1,
+                slaveId: 1,
+                // pollType: '0',
+                deterTime: '300',
+            };
+            this.resetForm('form');
+        },
+
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.id);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /** 新增IO寄存器行操作 */
+        handleAdd() {
+            var d = {
+                identifier: '',
+                slave: 1,
+                address: 1,
+                isReadonly: 1,
+                type: 1,
+                sort: this.configList.length + 1,
+            };
+            this.configList.push(d);
+            setTimeout(() => {
+                this.$refs.IOTable.setCurrentRow(d);
+            }, 10);
+        },
+
+        /** 新增数据寄存器行操作 */
+        handleAddData() {
+            var d = {
+                identifier: '',
+                slave: 1,
+                address: 1,
+                isReadonly: 1,
+                dataType: 'ushort',
+                quantity: 1,
+                type: 2,
+                sort: this.dataModbusList.length + 1,
+            };
+            this.dataModbusList.push(d);
+            setTimeout(() => {
+                this.$refs.Dataable.setCurrentRow(d);
+            }, 10);
+        },
+
+        handleAddBatch(justiceSelect) {
+            this.justiceSelect = justiceSelect;
+            this.$refs.thingsListRef.open = true;
+            this.$refs.thingsListRef.selectedList = justiceSelect == 'isSelectData' ? this.dataModbusList : this.configList;
+            this.$refs.thingsListRef.getList();
+        },
+        /**点击编辑IO控制器*/
+        editIOModbus() {
+            this.enableEditIO = !this.enableEditIO;
+        },
+        /**点击取消IO寄存器编辑按钮*/
+        handleCancelIO() {
+            this.enableEditIO = !this.enableEditIO;
+        },
+        /**点击取消数据寄存器编辑按钮*/
+        handleCancelData() {
+            this.enableEditData = !this.enableEditData;
+        },
+        /**点击编辑数据寄存器*/
+        editDataModbus() {
+            this.enableEditData = !this.enableEditData;
+        },
+
+        /** IO寄存器提交按钮 */
+        submitFormIO() {
+            const configList = [];
+            this.configList.forEach((config, index) => {
+                if (config.identifier) {
+                    config.sort = configList.length + 1;
+                    configList.push(config);
+                }
+            });
+            this.loadingIO = true;
+            const data = { productId: this.productId, configList: configList, delIds: this.delIoIds };
+            addBatch(data)
+                .then((response) => {
+                    this.$modal.msgSuccess('保存成功');
+                    this.open = false;
+                    this.loadingIO = false;
+                    this.enableEditIO = false;
+                })
+                .catch((err) => {
+                    this.loadingIO = false;
+                });
+        },
+        /** 数据寄存器提交按钮 */
+        submitFormData() {
+            const dataModbusList = [];
+            this.dataModbusList.forEach((dataModbus, index) => {
+                if (dataModbus.identifier) {
+                    dataModbus.sort = dataModbusList.length + 1;
+                    dataModbusList.push(dataModbus);
+                }
+            });
+            this.loadingData = true;
+            const data = { productId: this.productId, configList: dataModbusList, delIds: this.delDataIds };
+            addBatch(data)
+                .then((response) => {
+                    this.$modal.msgSuccess(this.$t('product.product-modbus.562372-52'));
+                    this.open = false;
+                    this.enableEditData = false;
+                    this.loadingData = false;
+                })
+                .catch((err) => {
+                    this.loadingData = false;
+                });
+        },
+        /** 删除按钮操作 */
+        handleDelete(row, index, list, justiceSelect) {
+            const item = list.splice(index, 1)[0];
+            //如果是删除数据寄存器,并且有id就存id
+            if (justiceSelect == 'isSelectData' && row.id) this.delDataIds.push(row.id);
+            //如果是删除io,并且有id就存id
+            if (justiceSelect == 'isSelectIo' && row.id) this.delIoIds.push(row.id);
+            //处理选项是否可选
+            this.updateSelectThingsModel({ justiceSelect, oldVal: item.identifier });
+        },
+
+        /*获取选择的物模型*/
+        getThingsData(ids) {
+            const list = this.justiceSelect == 'isSelectData' ? this.dataModbusList : this.configList;
+            ids.forEach((id, index) => {
+                const findIndex = list.findIndex((item) => item.identifier == id);
+                if (findIndex == -1) {
+                    list.push({
+                        identifier: id,
+                        slave: 1,
+                        address: 1,
+                        isReadonly: 1,
+                        dataType: 'ushort',
+                        quantity: 1,
+                        type: this.justiceSelect == 'isSelectData' ? 2 : 1,
+                        sort: list.length + 1,
+                    });
+                    this.updateSelectThingsModel({ justiceSelect: this.justiceSelect, newVal: id });
+                }
+            });
+        },
+
+        /**
+         * io拖拽行
+         */
+        rowDropIo() {
+            const tbodyIo = this.$refs.IOTable.$el.children[2].children[0].children[1];
+            this.sortableIo = new Sortable(tbodyIo, {
+                disabled: true,
+                onEnd: ({ newIndex, oldIndex, to }) => {
+                    this.dealDrop(this.configList, newIndex, oldIndex);
+                    this.configTableKey++;
+                    this.sortableIo.destroy();
+                    this.$nextTick(() => {
+                        this.rowDropIo();
+                        this.sortableIo.option('disabled', false);
+                    });
+                },
+            });
+        },
+        /**
+         * 数据寄存器拖拽
+         */
+        rowDropData() {
+            const tbodyData = this.$refs.Dataable.$el.children[2].children[0].children[1];
+            this.sortableData = new Sortable(tbodyData, {
+                disabled: true,
+                onEnd: ({ newIndex, oldIndex, to }) => {
+                    this.dealDrop(this.dataModbusList, newIndex, oldIndex);
+                    this.dataTableKey++;
+                    this.sortableData.destroy();
+                    this.$nextTick(() => {
+                        this.rowDropData();
+                        this.sortableData.option('disabled', false);
+                    });
+                },
+            });
+        },
+        //处理拖拽后的数据
+        dealDrop(list, newIndex, oldIndex) {
+            if (oldIndex == newIndex) return;
+            //找到选中那列,移除出数组
+            const curRow = list.splice(oldIndex, 1)[0];
+            //将选择中那列查到新位置
+            list.splice(newIndex, 0, curRow);
+        },
+        /**
+         * 更新物模型是否可选
+         */
+        updateSelectThingsModel({ oldVal, newVal, justiceSelect }) {
+            //直接改前后值
+            const oldIndex = oldVal ? this.thingsModelList.findIndex((thingsModel) => thingsModel.identifier == oldVal) : -1;
+            const newIndex = newVal ? this.thingsModelList.findIndex((thingsModel) => thingsModel.identifier == newVal) : -1;
+            if (oldIndex != -1) this.thingsModelList[oldIndex][justiceSelect] = true;
+            if (newIndex != -1) this.thingsModelList[newIndex][justiceSelect] = false;
+        },
+        /**批量导入 */
+        batchImport(justiceSelect) {
+            this.justiceSelect = justiceSelect;
+            this.$refs.importBatchRef.upload.importDeviceDialog = true;
+        },
+        /**导出 */
+        exportModbus(justiceSelect) {
+            const type = justiceSelect == 'isSelectData' ? 2 : 1;
+            const name = justiceSelect == 'isSelectData' ? this.$t('product.product-modbus.562372-17') : this.$t('product.product-modbus.562372-38');
+            this.download('/modbus/config/exportModbus?type=' + type, {}, `${name}_${new Date().getTime()}.xlsx`);
+        },
+        /**获取modbus参数配置*/
+        getParams() {
+            const params = { productId: this.productId };
+            getByProductId(params).then((response) => {
+                if (response.data) {
+                    this.form = response.data;
+                }
+            });
+        },
+        /**编辑modbus配置参数*/
+        setSlave() {
+            this.enableSetSlave = !this.enableSetSlave;
+        },
+
+        /** 保存modbus配置参数 */
+        saveSlave() {
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    this.enableSetSlave = !this.enableSetSlave;
+                    this.form.productId = this.productId;
+                    if (this.form.id != null) {
+                        updateParams(this.form).then((response) => {
+                            this.$modal.msgSuccess(this.$t('product.product-modbus.562372-53'));
+                        });
+                    } else {
+                        addParams(this.form).then((response) => {
+                            this.$modal.msgSuccess('修改成功');
+                        });
+                    }
+                }
+            });
+        },
+        /**取消*/
+        cancelSlave() {
+            this.enableSetSlave = !this.enableSetSlave;
+        },
+    },
+};
+</script>
+
+<style lang="scss">
+.el-input-number.is-disabled .el-input__inner {
+    color: #111315;
+}
+
+.el-select .el-input.is-disabled .el-input__inner {
+    color: #0c0d0e;
+}
+
+.el-radio-button__orig-radio:disabled:checked + .el-radio-button__inner {
+    background-color: #409eff !important;
+}
+
+.head {
+    .head-title {
+        margin-bottom: 12px;
+
+        .title {
+            margin-top: 16px;
+            margin-bottom: 2px;
+        }
+
+        .tips {
+            color: grey;
+            line-height: 36px;
+            font-size: 14px;
+        }
+    }
+
+    .button-group {
+        margin-bottom: 16px;
+        display: flex;
+
+        .right-btns {
+            margin-left: auto;
+        }
+    }
+}
+
+.test_div {
+    width: 300px;
+    padding: 5px;
+}
+</style>

+ 140 - 0
src/views/pms/video_center/product/product-scada.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="product-scada-wrap">
+    <div v-if="isScada" class="scada" :style="{ height: contentHeight + 'px' }">
+      <component :is="scadaComp" :fullScreemTip="false" :isContextmenu="false" />
+    </div>
+    <div v-else>
+      <el-empty :description="t('product.product-scada638785-0')" />
+      <div style="text-align: center">
+        <el-button
+          size="small"
+          type="primary"
+          @click="handleGoToScada()"
+        >{{ t('product.product-scada034908-0') }}</el-button
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, onMounted, onUnmounted } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+
+// 使用 i18n
+const { t } = useI18n()
+
+// 使用路由
+const route = useRoute()
+const router = useRouter()
+
+// 定义组件名称
+defineOptions({
+  name: 'ProductScada'
+})
+
+// 定义 props
+const props = defineProps({
+  product: {
+    type: Object,
+    default: null
+  }
+})
+
+// 数据状态
+const isScada = ref(false)
+const contentHeight = ref(window.innerHeight)
+const scadaComp = ref(null)
+const productInfo = ref({})
+
+// 监听窗口大小变化
+const handleResize = () => {
+  calculateContentHeight()
+}
+
+// 获取窗体高度
+const calculateContentHeight = () => {
+  const element = document.getElementById('productDetailTab')
+  if (element) {
+    contentHeight.value = parseFloat(element.offsetHeight)
+  }
+}
+
+// 组态设计
+const handleGoToScada = () => {
+  router.push({
+    path: '/scada/center/temp',
+    query: {
+      productId: productInfo.value.productId
+    }
+  })
+}
+
+// 获取组态
+const getScadaComp = (guid) => {
+  router.push({ 
+    query: { 
+      ...route.query, 
+      guid: productInfo.value.guid 
+    } 
+  })
+}
+
+// 监听 product 变化
+watch(
+  () => props.product,
+  (newVal, oldVal) => {
+    productInfo.value = newVal
+    if (newVal && newVal.guid) {
+      getScadaComp(newVal)
+      isScada.value = true
+    } else {
+      isScada.value = false
+    }
+  },
+  { deep: true, immediate: true }
+)
+
+// 组件挂载时执行
+onMounted(() => {
+  window.addEventListener('resize', handleResize)
+  calculateContentHeight()
+})
+
+// 组件卸载时清理事件监听器
+onUnmounted(() => {
+  window.removeEventListener('resize', handleResize)
+})
+</script>
+
+<style lang="scss" scoped>
+.product-scada-wrap {
+  position: relative;
+  width: 100%;
+  height: 100%;
+
+  .scada {
+    position: relative;
+    width: 100%;
+    overflow: auto;
+  }
+
+  ::-webkit-scrollbar-thumb {
+    background-color: #e1e1e1;
+  }
+
+  ::-webkit-scrollbar-thumb:hover {
+    background-color: #a5a2a2;
+  }
+
+  ::-webkit-scrollbar {
+    width: 5px;
+    height: 5px;
+    position: absolute;
+  }
+
+  ::-webkit-scrollbar-track {
+    background-color: #fff;
+  }
+}
+</style>

+ 215 - 0
src/views/pms/video_center/product/product-select-template.vue

@@ -0,0 +1,215 @@
+<template>
+  <div style="margin-top: -50px">
+    <el-divider />
+    <el-form :model="queryParams" ref="productSelectTemplateRef" inline>
+      <el-form-item :label="t('product.product-select-template318012-0')" prop="templateName">
+        <el-input 
+          v-model="queryParams.templateName" 
+          :placeholder="t('product.product-select-template318012-1')" 
+          clearable 
+          size="default" 
+          @keyup.enter="handleQuery" 
+        />
+      </el-form-item>
+      <el-form-item :label="t('product.product-select-template.318012-2')" prop="type">
+        <el-select 
+          v-model="queryParams.type" 
+          :placeholder="t('product.product-select-template318012-3')" 
+          clearable 
+          size="default"
+        >
+          <el-option 
+            v-for="dict in dict.type.iot_things_type" 
+            :key="dict.value" 
+            :label="dict.label" 
+            :value="dict.value" 
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" :icon="Search" size="default" @click="handleQuery">
+          {{ t('product.product-select-template318012-4') }}
+        </el-button>
+        <el-button :icon="Refresh" size="default" @click="resetQuery">
+          {{ t('product.product-select-template318012-5') }}
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table 
+      :border="false" 
+      v-loading="loading" 
+      :data="templateList" 
+      @selection-change="handleSelectionChange" 
+      ref="selectTemplateTableRef" 
+      size="default"
+    >
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column 
+        :label="t('product.product-select-template318012-0')" 
+        align="center" 
+        prop="templateName" 
+      />
+      <el-table-column 
+        :label="t('product.product-select-template318012-6')" 
+        align="center" 
+        prop="identifier" 
+      />
+      <el-table-column 
+        :label="t('product.product-select-template318012-7')" 
+        align="center" 
+        prop="type"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_things_type" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column 
+        :label="t('product.product-things-model142341-12')" 
+        align="center" 
+        prop="isChart" 
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isChart" />
+        </template>
+      </el-table-column>
+      <el-table-column 
+        :label="t('product.product-select-template318012-9')" 
+        align="center" 
+        prop="isMonitor" 
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isMonitor" />
+        </template>
+      </el-table-column>
+      <el-table-column 
+        :label="t('product.product-select-template318012-10')" 
+        align="center" 
+        prop="isReadonly" 
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isReadonly" />
+        </template>
+      </el-table-column>
+      <el-table-column 
+        :label="t('product.product-select-template318012-11')" 
+        align="center" 
+        prop="isHistory" 
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isHistory" />
+        </template>
+      </el-table-column>
+      <el-table-column 
+        :label="t('product.product-select-template318012-12')" 
+        align="center" 
+        prop="datatype"
+      >
+        <template #default="scope">
+          <dict-tag :options="dict.type.iot_data_type" :value="scope.row.datatype" />
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination 
+      v-show="total > 0" 
+      :total="total" 
+      v-model:page="queryParams.pageNum" 
+      v-model:limit="queryParams.pageSize" 
+      @pagination="getList" 
+    />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { Search, Refresh } from '@element-plus/icons-vue'
+import { listTemplate } from '@/api/pms/video/template'
+
+// 使用 i18n
+const { t } = useI18n()
+
+// 定义组件名称
+defineOptions({
+  name: 'product-select-template'
+})
+
+// 定义 emits
+const emit = defineEmits(['idsToParentEvent'])
+
+// 引用模板
+const productSelectTemplateRef = ref()
+const selectTemplateTableRef = ref()
+
+// 字典属性(需要从父组件或store获取)
+const dict = defineProps({
+  type: {
+    iot_things_type: Array,
+    iot_data_type: Array,
+    iot_yes_no: Array
+  }
+})
+
+// 数据状态
+const loading = ref(false)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const templateList = ref([])
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  templateName: null,
+  type: null
+})
+
+// 获取列表数据
+const getList = () => {
+  loading.value = true
+  listTemplate(queryParams).then(response => {
+    templateList.value = response.list
+    total.value = response.total
+    loading.value = false
+  })
+}
+
+// 搜索操作
+const handleQuery = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+// 重置操作
+const resetQuery = () => {
+  if (productSelectTemplateRef.value) {
+    productSelectTemplateRef.value.resetFields()
+  }
+  handleQuery()
+}
+
+// 处理选择变化
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map(item => item.templateId)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+  // 向父组件传递ID数组
+  emit('idsToParentEvent', ids.value)
+}
+
+// 组件挂载时获取数据
+onMounted(() => {
+  getList()
+  ids.value = []
+})
+
+defineExpose({
+  selectTemplateTableRef
+})
+</script>

+ 1544 - 0
src/views/pms/video_center/product/product-things-model.vue

@@ -0,0 +1,1544 @@
+<template>
+  <div style="padding-left: 20px">
+    <el-form
+      :model="queryParams"
+      ref="queryFormRef"
+      size="small"
+      :inline="true"
+      v-show="showSearch"
+    >
+      <el-form-item :label="t('product.product-things-model142341-127')" prop="isAPP">
+        <el-select
+          v-model="queryParams.isAPP"
+          :placeholder="t('product.product-things-model142341-128')"
+          clearable
+          size="small"
+          style="width: 150px;"
+        >
+          <el-option
+            v-for="dict in iot_yes_no"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('product.product-things-model142341-129')" prop="type">
+        <el-select
+          v-model="queryParams.type"
+          :placeholder="t('product.product-things-model142341-130')"
+          clearable
+          size="small"
+           style="width: 150px;"
+        >
+          <el-option
+            v-for="dict in iot_things_type"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="t('product.product-things-model142341-131')" prop="isHistory">
+        <el-select
+          v-model="queryParams.isHistory"
+          :placeholder="t('product.product-things-model142341-132')"
+          clearable
+          size="small"
+           style="width: 150px;"
+        >
+          <el-option
+            v-for="dict in iot_yes_no"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" :icon="Search" size="small" @click="handleQuery">{{
+          t('product.product-things-model142341-133')
+        }}</el-button>
+        <el-button :icon="Refresh" size="small" @click="resetQuery">{{
+          t('product.product-things-model142341-134')
+        }}</el-button>
+      </el-form-item>
+      <el-form-item label="设备从机:" v-if="queryParams.isModbus">
+        <el-select v-model="slave.id" placeholder="请选择设备从机" @change="selectSlave">
+          <el-option
+            v-for="slaveItem in slaveList"
+            :key="slaveItem.slaveAddr"
+            :label="`${slaveItem.slaveName}   (${slaveItem.slaveAddr})`"
+            :value="slaveItem.slaveAddr"
+          />
+        </el-select>
+        <el-button style="margin-left: 20px" plain size="small" type="primary" @click="getGateway"
+          >网关物模型</el-button
+        >
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb20">
+      <el-col :span="1.5" v-if="productInfo.status == 1 && productInfo.isOwner != 0">
+        <el-button
+          type="primary"
+          plain
+          :icon="Plus"
+          size="small"
+          @click="handleAdd"
+          v-hasPermi="['iot:model:add']"
+        >
+          {{ t('product.product-things-model142341-0') }}
+        </el-button>
+      </el-col>
+      <!-- <el-col :span="1.5" v-if="productInfo.status == 1 && productInfo.isOwner != 0">
+        <el-button type="primary" plain :icon="Upload" size="small" @click="handleImport" v-hasPermi="['iot:point:import']">
+          {{ t('product.product-things-model142341-126') }}
+        </el-button>
+      </el-col> -->
+      <el-col :span="1.5" v-if="productInfo.status == 1 && productInfo.isOwner != 0">
+        <el-button
+          type="primary"
+          plain
+          :icon="Upload2"
+          size="small"
+          @click="handleSelect"
+         
+        >
+          {{ t('product.product-things-model142341-1') }}
+        </el-button>
+      </el-col>
+      <el-col :span="1.5" class="flex-center">
+        <el-button
+          type="primary"
+          plain
+          :icon="Refresh"
+          size="small"
+          @click="getList"
+          
+          >{{ t('product.product-things-model142341-2') }}</el-button
+        >
+      </el-col>
+      <el-col :span="1.5" class="flex-center">
+        <el-button type="info" plain :icon="View" size="small" @click="handleOpenThingsModel">
+          {{ t('product.product-things-model142341-3') }}
+        </el-button>
+      </el-col>
+      <el-col :span="1.5" class="flex-center">
+        <el-link type="danger" style="padding-top: 5px" :underline="false">
+          {{ t('product.product-things-model142341-4') }}
+        </el-link>
+      </el-col>
+
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" />
+    </el-row>
+    <el-table :border="false" v-loading="loading" :data="modelList" size="small">
+      <el-table-column
+        :label="t('product.product-things-model142341-8')"
+        align="center"
+        prop="modelName"
+        width="230"
+      />
+      <el-table-column
+        :label="t('product.product-things-model142341-9')"
+        align="center"
+        prop="identifier"
+      />
+      <el-table-column
+        v-if="queryParams.isModbus"
+        align="center"
+        label="寄存器地址(10进制)"
+        prop="regAddr"
+      />
+      <el-table-column
+        :label="t('product.product-things-model142341-12')"
+        align="center"
+        prop=""
+        width="80"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_yes_no" :value="scope.row.isChart" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="实时监测"
+        align="center"
+        prop=""
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_yes_no" :value="scope.row.isMonitor" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="APP展示"
+        align="center"
+        prop=""
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_yes_no" :value="scope.row.isApp" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="只读"
+        align="center"
+        prop=""
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_yes_no" :value="scope.row.isReadonly" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('product.product-things-model142341-15')"
+        align="center"
+        prop=""
+        width="75"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_yes_no" :value="scope.row.isHistory" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('product.product-things-model142341-16')"
+        align="center"
+        prop="type"
+        width="100"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_things_type" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('product.product-things-model142341-17')"
+        align="center"
+        prop="datatype"
+        width="80"
+      >
+        <template #default="scope">
+          <dict-tag :options="iot_data_type" :value="scope.row.datatype" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('product.product-things-model142341-18')"
+        align="left"
+        header-align="center"
+        prop="specs"
+        min-width="150"
+        class-name="specsColor"
+      >
+        <template #default="scope">
+          <div v-html="formatSpecsDisplay(scope.row.specs)"></div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        :label="t('product.product-things-model142341-19')"
+        align="center"
+        prop="formula"
+      />
+      <el-table-column
+        :label="t('product.product-things-model142341-20')"
+        align="center"
+        prop="modelOrder"
+        width="80"
+      />
+      <el-table-column
+        :label="t('product.product-things-model142341-21')"
+        align="center"
+        class-name="small-padding fixed-width"
+      >
+        <template #default="scope">
+          <el-button
+            size="small"
+            type="primary"
+            :icon="View"
+            @click="handleUpdate(scope.row)"
+           
+            v-if="productInfo.status != 2 && productInfo.isOwner != 0"
+          >
+            {{ t('product.product-things-model142341-22') }}
+          </el-button>
+          <el-button
+            size="small"
+            type="danger"
+            :icon="Delete"
+            @click="handleDelete(scope.row)"
+          
+            v-if="productInfo.status != 2 && productInfo.isOwner != 0"
+          >
+            {{ t('product.product-things-model142341-23') }}
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <el-pagination
+      v-show="total > 0"
+      v-model:current-page="queryParams.pageNum"
+      v-model:page-size="queryParams.pageSize"
+      :total="total"
+      layout="prev, pager, next"
+      small
+      @current-change="getList"
+    />
+
+    <!-- 添加或修改物模型对话框 -->
+    <el-dialog :title="title" v-model="open" width="600px" append-to-body>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item :label="t('product.product-things-model142341-24')" prop="modelName">
+          <el-input
+            v-model="form.modelName"
+            :placeholder="t('product.product-things-model142341-25')"
+            style="width: 385px"
+          />
+        </el-form-item>
+        <el-form-item :label="t('product.product-things-model142341-26')" prop="identifier">
+          <el-input
+            v-model="form.identifier"
+            :placeholder="t('product.product-things-model142341-27')"
+            style="width: 385px"
+          />
+        </el-form-item>
+        <el-form-item v-if="queryParams.isModbus" label="寄存器地址" prop="regAddr">
+          <el-input v-model="form.regAddr" style="width: 385px" />
+        </el-form-item>
+        <el-form-item :label="t('product.product-things-model142341-28')" prop="modelOrder">
+          <el-input
+            v-model="form.modelOrder"
+            :placeholder="t('product.product-things-model142341-29')"
+            type="number"
+            style="width: 385px"
+          />
+        </el-form-item>
+        <el-form-item :label="t('product.product-things-model142341-30')" prop="type">
+          <el-radio-group v-model="form.type" @change="typeChange">
+            <el-radio-button label="1">{{
+              t('product.product-things-model142341-31')
+            }}</el-radio-button>
+            <el-radio-button label="2">{{
+              t('product.product-things-model142341-32')
+            }}</el-radio-button>
+            <el-radio-button label="3">{{
+              t('product.product-things-model142341-33')
+            }}</el-radio-button>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item :label="t('product.product-things-model142341-34')" prop="property">
+          <el-checkbox
+            name="isChart"
+            :label="t('product.product-things-model142341-12')"
+            @change="isChartChange"
+            v-show="form.type == 1"
+            v-model="form.isChart"
+            :false-label="0"
+            :true-label="1"
+          />
+          <el-checkbox
+            name="isMonitor"
+            :label="t('product.product-select-template.318012-9')"
+            v-show="form.type == 1"
+            v-model="form.isMonitor"
+            :false-label="0"
+            :true-label="1"
+            @change="isMonitorChange"
+          />
+          <el-checkbox
+            name="isReadonly"
+            :label="t('product.product-things-model142341-35')"
+            v-model="form.isReadonly"
+            :disabled="form.type == 3"
+            :false-label="0"
+            :true-label="1"
+            @change="isReadonlyChange"
+          />
+          <el-checkbox
+            name="isHistory"
+            :label="t('product.product-things-model142341-15')"
+            v-model="form.isHistory"
+            :true-label="1"
+            :false-label="0"
+          />
+          <el-checkbox
+            name="isSharePerm"
+            :label="t('product.product-things-model142341-36')"
+            v-model="form.isSharePerm"
+            :true-label="1"
+            :false-label="0"
+          />
+        </el-form-item>
+        <el-divider />
+        <el-form-item :label="t('product.product-app.045891-5')" prop="datatype">
+          <el-select
+            v-model="form.datatype"
+            :placeholder="t('product.product-things-model142341-37')"
+            @change="dataTypeChange"
+            style="width: 175px"
+          >
+            <el-option
+              key="integer"
+              :label="t('product.product-things-model142341-38')"
+              value="integer"
+            />
+            <el-option
+              key="decimal"
+              :label="t('product.product-things-model142341-39')"
+              value="decimal"
+            />
+            <el-option
+              key="bool"
+              :label="t('product.product-things-model142341-40')"
+              value="bool"
+              :disabled="form.isChart == 1"
+            />
+            <el-option
+              key="enum"
+              :label="t('product.product-things-model142341-41')"
+              value="enum"
+              :disabled="form.isChart == 1"
+            />
+            <el-option
+              key="string"
+              :label="t('product.product-things-model142341-42')"
+              value="string"
+              :disabled="form.isChart == 1"
+            />
+            <el-option
+              key="array"
+              :label="t('product.product-things-model142341-43')"
+              value="array"
+              :disabled="form.isChart == 1"
+            />
+            <el-option
+              key="object"
+              :label="t('product.product-things-model142341-44')"
+              value="object"
+              :disabled="form.isChart == 1"
+            />
+          </el-select>
+        </el-form-item>
+
+        <div v-if="form.datatype == 'integer' || form.datatype == 'decimal'">
+          <el-form-item :label="t('product.product-things-model142341-45')">
+            <el-row>
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.min"
+                  :placeholder="t('product.product-things-model142341-46')"
+                  type="number"
+                />
+              </el-col>
+              <el-col :span="2" align="center">{{
+                t('product.product-things-model142341-47')
+              }}</el-col>
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.max"
+                  :placeholder="t('product.product-things-model142341-48')"
+                  type="number"
+                />
+              </el-col>
+            </el-row>
+          </el-form-item>
+          <el-form-item :label="t('product.product-things-model142341-49')">
+            <el-input
+              v-model="form.specs.unit"
+              :placeholder="t('product.product-things-model142341-50')"
+              style="width: 385px"
+            />
+          </el-form-item>
+          <el-form-item :label="t('product.product-things-model142341-51')">
+            <el-input
+              v-model="form.specs.step"
+              :placeholder="t('product.product-things-model142341-52')"
+              type="number"
+              style="width: 385px"
+            />
+          </el-form-item>
+          <el-form-item :label="t('product.product-things-model142341-19')" prop="formula">
+            <template #label>
+              <span>{{ t('product.product-things-model142341-19') }}</span>
+              <el-tooltip style="cursor: pointer" effect="light" placement="top">
+                <template #content>
+                  {{ t('product.product-things-model142341-53') }}
+                  <br />
+                  {{ t('product.product-things-model142341-54') }}
+                  <br />
+                  {{ t('product.product-things-model142341-55') }}
+                  <br />
+                  {{ t('product.product-things-model142341-56') }}
+                  <br />
+                  {{ t('product.product-things-model142341-57') }}
+                  <br />
+                  {{ t('product.product-things-model142341-58') }}
+                  <br />
+                  {{ t('product.product-things-model142341-59') }}
+                  <br />
+                  {{ t('product.product-things-model142341-60') }}({{
+                    t('product.product-things-model142341-61')
+                  }}):%s%10.00
+                  <br />
+                </template>
+                <el-icon><QuestionFilled /></el-icon>
+              </el-tooltip>
+            </template>
+            <el-input v-model="form.formula" style="width: 385px" />
+          </el-form-item>
+          <el-form-item v-if="queryParams.isModbus" label="控制公式" prop="reverseFormula">
+            <el-input v-model="form.reverseFormula" style="width: 385px" />
+          </el-form-item>
+        </div>
+
+        <div v-if="form.datatype == 'bool'">
+          <el-form-item :label="t('product.product-things-model142341-63')" prop="">
+            <el-row style="margin-bottom: 10px">
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.falseText"
+                  :placeholder="t('product.product-things-model142341-64')"
+                />
+              </el-col>
+              <el-col :span="10" :offset="1">{{
+                t('product.product-things-model142341-65')
+              }}</el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.trueText"
+                  :placeholder="t('product.product-things-model142341-66')"
+                />
+              </el-col>
+              <el-col :span="10" :offset="1">{{
+                t('product.product-things-model142341-67')
+              }}</el-col>
+            </el-row>
+          </el-form-item>
+        </div>
+
+        <div v-if="form.datatype == 'enum'">
+          <el-form-item :label="t('product.product-things-model142341-68')">
+            <el-select
+              v-model="form.specs.showWay"
+              :placeholder="t('product.product-things-model142341-69')"
+              style="width: 175px"
+            >
+              <el-option
+                key="select"
+                :label="t('product.product-things-model142341-70')"
+                value="select"
+              />
+              <el-option
+                key="button"
+                :label="t('product.product-things-model142341-71')"
+                value="button"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item :label="t('product.product-things-model142341-72')" prop="">
+            <el-row
+              v-for="(item, index) in form.specs.enumList"
+              :key="'enum' + index"
+              style="margin-bottom: 10px"
+            >
+              <el-col :span="9">
+                <el-input
+                  v-model="item.value"
+                  :placeholder="t('product.product-things-model142341-73')"
+                />
+              </el-col>
+              <el-col :span="11" :offset="1">
+                <el-input
+                  v-model="item.text"
+                  :placeholder="t('product.product-things-model142341-74')"
+                />
+              </el-col>
+              <el-col :span="2" :offset="1" v-if="index != 0">
+                <a style="color: #f56c6c" @click="removeEnumItem(index)">{{ t('del') }}</a>
+              </el-col>
+            </el-row>
+            <div>
+              +
+              <a style="color: #409eff" @click="addEnumItem()">{{
+                t('product.product-things-model142341-75')
+              }}</a>
+            </div>
+          </el-form-item>
+        </div>
+
+        <div v-if="form.datatype == 'string'">
+          <el-form-item :label="t('product.product-things-model142341-76')" prop="">
+            <el-row>
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.maxLength"
+                  :placeholder="t('product.product-things-model142341-77')"
+                  type="number"
+                />
+              </el-col>
+              <el-col :span="14" :offset="1">{{
+                t('product.product-things-model142341-78')
+              }}</el-col>
+            </el-row>
+          </el-form-item>
+        </div>
+
+        <div v-if="form.datatype == 'array'">
+          <el-form-item :label="t('product.product-things-model142341-79')" prop="">
+            <el-row>
+              <el-col :span="9">
+                <el-input
+                  v-model="form.specs.arrayCount"
+                  :placeholder="t('product.product-things-model142341-80')"
+                  type="number"
+                />
+              </el-col>
+            </el-row>
+          </el-form-item>
+          <el-form-item :label="t('product.product-things-model142341-81')" prop="">
+            <el-radio-group v-model="form.specs.arrayType">
+              <el-radio label="integer">{{
+                t('product.product-things-model142341-38')
+              }}</el-radio>
+              <el-radio label="decimal">{{
+                t('product.product-things-model142341-39')
+              }}</el-radio>
+              <el-radio label="string">{{ t('product.product-things-model142341-42') }}</el-radio>
+              <el-radio label="object">{{ t('product.product-things-model142341-44') }}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <el-form-item
+            :label="t('product.product-things-model142341-82')"
+            v-if="form.specs.arrayType == 'object'"
+          >
+            <div style="background-color: #f8f8f8; border-radius: 5px">
+              <el-row
+                style="padding: 0 10px 5px"
+                v-for="(item, index) in form.specs.params"
+                :key="index"
+              >
+                <div style="margin-top: 5px" v-if="index == 0"></div>
+                <el-col :span="18">
+                  <el-input
+                    readonly
+                    v-model="item.name"
+                    size="small"
+                    :placeholder="t('product.product-things-model142341-83')"
+                    style="margin-top: 3px"
+                  >
+                    <template #prepend>
+                      <el-tag
+                        effect="dark"
+                        size="small"
+                        style="margin-left: -21px; height: 26px; line-height: 26px"
+                        >{{ item.order }}</el-tag
+                      >
+                      {{ form.identifier + '_' + item.id }}
+                    </template>
+                    <template #append>
+                      <el-button @click="editParameter(item, index)" size="small">{{
+                        t('edit')
+                      }}</el-button>
+                    </template>
+                  </el-input>
+                </el-col>
+                <el-col :span="2" :offset="2">
+                  <el-button
+                    :icon="Delete"
+                    plain
+                    size="small"
+                    style="padding: 5px"
+                    type="danger"
+                    @click="removeParameter(index)"
+                    >{{ t('del') }}</el-button
+                  >
+                </el-col>
+              </el-row>
+            </div>
+            <div>
+              +
+              <a style="color: #409eff" @click="addParameter()">{{
+                t('product.product-things-model142341-85')
+              }}</a>
+            </div>
+          </el-form-item>
+        </div>
+        <div v-if="form.datatype == 'object'">
+          <el-form-item :label="t('product.product-things-model142341-82')" prop="">
+            <div style="background-color: #f8f8f8; border-radius: 5px">
+              <el-row
+                style="padding: 0 10px 5px"
+                v-for="(item, index) in form.specs.params"
+                :key="index"
+              >
+                <div style="margin-top: 5px" v-if="index == 0"></div>
+                <el-col :span="18">
+                  <el-input
+                    readonly
+                    v-model="item.name"
+                    size="small"
+                    :placeholder="t('product.product-things-model142341-83')"
+                    style="margin-top: 3px"
+                  >
+                    <template #prepend>
+                      <el-tag
+                        effect="dark"
+                        size="small"
+                        style="margin-left: -21px; height: 26px; line-height: 26px"
+                        >{{ item.order }}</el-tag
+                      >
+                      {{ form.identifier + '_' + item.id }}
+                    </template>
+                    <template #append>
+                      <el-button @click="editParameter(item, index)" size="small">{{
+                        t('edit')
+                      }}</el-button>
+                    </template>
+                  </el-input>
+                </el-col>
+                <el-col :span="2" :offset="2">
+                  <el-button
+                    :icon="Delete"
+                    plain
+                    size="small"
+                    style="padding: 5px"
+                    type="danger"
+                    @click="removeParameter(index)"
+                    >{{ t('del') }}</el-button
+                  >
+                </el-col>
+              </el-row>
+            </div>
+            <div>
+              +
+              <a style="color: #409eff" @click="addParameter()">{{
+                t('product.product-things-model142341-85')
+              }}</a>
+            </div>
+          </el-form-item>
+        </div>
+      </el-form>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            type="primary"
+            @click="submitForm"
+          
+            v-show="form.modelId"
+          >
+            更新
+          </el-button>
+          <el-button
+            type="primary"
+            @click="submitForm"
+           
+            v-show="!form.modelId"
+            >添加</el-button
+          >
+          <el-button @click="cancel">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!--物模型参数类型-->
+    <things-parameter :data="paramData" @dataEvent="getParamData" />
+
+    <!-- 导入通用物模型对话框 -->
+    <el-dialog :title="title" v-model="openSelect" width="800px" append-to-body>
+      <product-select-template ref="productSelectTemplateRef" @idsToParentEvent="getChildData" />
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="importSelect">导入</el-button>
+          <el-button @click="cancelSelect">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 物模型JSON -->
+    <el-dialog :title="title" v-model="openThingsModel" width="600px" append-to-body>
+      <div style="border: 1px solid #ccc; margin-top: -15px; height: 600px; overflow: scroll">
+        <json-viewer :value="thingsModel" :expand-depth="10" copyable>
+          <template #copy>{{ t('product.product-things-model142341-92') }}</template>
+        </json-viewer>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="info" @click="handleCloseThingsModel">关闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 批量导入 -->
+    <!--    <import-batch ref="importBatchRef" :productId="productId" />-->
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, getCurrentInstance, onMounted } from 'vue'
+import productSelectTemplate from './product-select-template.vue'
+import JsonViewer from 'vue-json-viewer'
+import RightToolbar from './components/RightToolbar/index.vue'
+import 'vue-json-viewer/style.css'
+import thingsParameter from './template/parameter.vue'
+import { addModel, delModel, getModel, importModel, listModel, updateModel } from '@/api/pms/video/model'
+import { listByPid } from '@/api/pms/video/salve'
+import {
+  Search,
+  Refresh,
+  Plus,
+  Upload,
+  View,
+  Delete,
+  QuestionFilled
+} from '@element-plus/icons-vue'
+
+// 定义组件名称
+defineOptions({
+  name: 'product-things-model'
+})
+
+
+const { t } = useI18n()
+
+// 定义props
+const props = defineProps({
+  product: {
+    type: Object,
+    default: null
+  },
+  isEdit: {
+    type: Boolean,
+    default: true
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['updateModel'])
+
+// 字典定义
+const iot_things_type = []
+const iot_data_type = []
+const iot_yes_no = []
+
+
+// Refs
+const queryFormRef = ref(null)
+const formRef = ref(null)
+const productSelectTemplateRef = ref(null)
+
+// 数据定义
+// 物模型
+const thingsModel = ref({})
+// 父组件接收的产品信息
+const productInfo = ref({})
+// 子组件选中的id数组
+const templateIds = ref([])
+// 遮罩层
+const loading = ref(false)
+// 选中数组
+const ids = ref([])
+// 非单个禁用
+const single = ref(true)
+// 非多个禁用
+const multiple = ref(true)
+// 显示搜索条件
+const showSearch = ref(true)
+// 总条数
+const total = ref(0)
+// 产品物模型表格数据
+const modelList = ref([])
+// 弹出层标题
+const title = ref('')
+// 是否显示弹出层
+const open = ref(false)
+const openSelect = ref(false)
+const openThingsModel = ref(false)
+// 查询参数
+const queryParams = reactive({
+  productId: 0,
+  pageNum: 1,
+  pageSize: 20
+})
+const productId = ref(0)
+// 表单参数
+const form = reactive({
+  templateId: null,
+  templateName: null,
+  userId: null,
+  userName: null,
+  tenantId: null,
+  tenantName: null,
+  identifier: null,
+  modelOrder: 0,
+  type: 1,
+  datatype: 'integer',
+  isSys: null,
+  isChart: 1,
+  isHistory: 1,
+  isSharePerm: 1,
+  isMonitor: 1,
+  isReadonly: 1,
+  delFlag: null,
+  createBy: null,
+  createTime: null,
+  updateBy: null,
+  updateTime: null,
+  remark: null,
+  specs: {
+    enumList: [
+      {
+        value: '',
+        text: ''
+      }
+    ],
+    arrayType: 'integer',
+    arrayCount: 5,
+    showWay: 'select', // 显示方式select=下拉选择框,button=按钮
+    params: []
+  }
+})
+
+// 对象类型参数
+const paramData = reactive({
+  index: -1,
+  parameter: {}
+})
+
+//是否是modbus
+const isModbus = ref(false)
+const slaveList = ref([])
+const slave = reactive({})
+
+// 表单校验
+const rules = {
+  modelName: [
+    {
+      required: true,
+      message: t('product.product-things-model142341-94'),
+      trigger: 'blur'
+    }
+  ],
+  identifier: [
+    {
+      required: true,
+      message: t('product.product-things-model142341-95'),
+      trigger: 'blur'
+    }
+  ],
+  modelOrder: [
+    {
+      required: true,
+      message: t('product.product-things-model142341-96'),
+      trigger: 'blur'
+    }
+  ],
+  type: [
+    {
+      required: true,
+      message: t('product.product-things-model142341-97'),
+      trigger: 'change'
+    }
+  ],
+  datatype: [
+    {
+      required: true,
+      message: t('product.product-things-model142341-98'),
+      trigger: 'change'
+    }
+  ]
+}
+
+// Watchers
+watch(
+  () => props.product,
+  (newVal, oldVal) => {
+    productInfo.value = newVal
+    if (productInfo.value && productInfo.value.productId != 0) {
+      queryParams.productId = productInfo.value.productId
+      productId.value = productInfo.value.productId
+      queryParams.isModbus = productInfo.value.isModbus
+      getList()
+      if (queryParams.isModbus) {
+        getSlaveList()
+      }
+    }
+  }
+)
+
+// 方法定义
+/** 查询产品物模型列表 */
+const getList = () => {
+  loading.value = true
+  listModel(queryParams).then((response) => {
+    modelList.value = response.rows
+    total.value = response.total
+    loading.value = false
+  })
+}
+
+/**根据产品id获取从机列表*/
+const getSlaveList = () => {
+  const params = {
+    productId: queryParams.productId
+  }
+  listByPid(params).then((response) => {
+    slaveList.value = response.data
+    slave.id = slaveList.value[0].slaveAddr
+    queryParams.tempSlaveId = slaveList.value[0].slaveAddr
+    getList()
+  })
+}
+
+const selectSlave = () => {
+  queryParams.tempSlaveId = slave.id
+  getList()
+}
+
+const getGateway = () => {
+  queryParams.tempSlaveId = undefined
+  getList()
+}
+
+// 取消按钮
+const cancel = () => {
+  open.value = false
+  reset()
+}
+
+// 表单重置
+const reset = () => {
+  Object.assign(form, {
+    templateId: null,
+    templateName: null,
+    userId: null,
+    userName: null,
+    tenantId: null,
+    tenantName: null,
+    identifier: null,
+    modelOrder: 0,
+    type: 1,
+    datatype: 'integer',
+    isSys: null,
+    isChart: 1,
+    isHistory: 1,
+    isSharePerm: 1,
+    isMonitor: 1,
+    isReadonly: 1,
+    delFlag: null,
+    createBy: null,
+    createTime: null,
+    updateBy: null,
+    updateTime: null,
+    remark: null,
+    specs: {
+      enumList: [
+        {
+          value: '',
+          text: ''
+        }
+      ],
+      arrayType: 'integer',
+      arrayCount: 5,
+      showWay: 'select', // 显示方式select=下拉选择框,button=按钮
+      params: []
+    }
+  })
+  formRef.value?.resetFields()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset()
+  open.value = true
+  title.value = t('product.product-things-model142341-99')
+}
+
+/** 修改按钮操作 */
+const handleUpdate = (row) => {
+  reset()
+  const modelId = row.modelId
+  getModel(modelId).then((response) => {
+    let tempForm = response.data
+    open.value = true
+    title.value = t('product.product-things-model142341-100')
+    // Json转对象
+    tempForm.specs = JSON.parse(tempForm.specs)
+    if (!tempForm.specs.enumList) {
+      tempForm.specs.showWay = 'select'
+      tempForm.specs.enumList = [
+        {
+          value: '',
+          text: ''
+        }
+      ]
+    }
+    if (!tempForm.specs.arrayType) {
+      tempForm.specs.arrayType = 'integer'
+    }
+    if (!tempForm.specs.arrayCount) {
+      tempForm.specs.arrayCount = 5
+    }
+    if (!tempForm.specs.params) {
+      tempForm.specs.params = []
+    }
+    // 对象和数组中参数删除前缀
+    if (
+      (tempForm.specs.type == 'array' && tempForm.specs.arrayType == 'object') ||
+      tempForm.specs.type == 'object'
+    ) {
+      for (let i = 0; i < tempForm.specs.params.length; i++) {
+        tempForm.specs.params[i].id = String(tempForm.specs.params[i].id).substring(
+          String(tempForm.identifier).length + 1
+        )
+      }
+    }
+    Object.assign(form, tempForm)
+  })
+}
+
+/**查看物模型 */
+const handleOpenThingsModel = () => {
+  title.value = t('product.product-things-model142341-101')
+  // 生成物模型
+  thingsModel.value = {
+    properties: [],
+    functions: [],
+    events: []
+  }
+  for (var i = 0; i < modelList.value.length; i++) {
+    let thingsItem = {}
+    thingsItem.id = modelList.value[i].identifier
+    thingsItem.name = modelList.value[i].modelName
+    if (modelList.value[i].type == 1) {
+      //属性
+      thingsItem.isChart = modelList.value[i].isChart
+      thingsItem.isMonitor = modelList.value[i].isMonitor
+      thingsItem.isHistory = modelList.value[i].isHistory
+      thingsItem.isSharePerm = modelList.value[i].isSharePerm
+      thingsItem.isReadonly = modelList.value[i].isReadonly
+      thingsItem.datatype = JSON.parse(modelList.value[i].specs)
+      thingsModel.value.properties.push(thingsItem)
+    } else if (modelList.value[i].type == 2) {
+      // 功能
+      thingsItem.isHistory = modelList.value[i].isHistory
+      thingsItem.isSharePerm = modelList.value[i].isSharePerm
+      thingsItem.isReadonly = modelList.value[i].isReadonly
+      thingsItem.datatype = JSON.parse(modelList.value[i].specs)
+      thingsModel.value.functions.push(thingsItem)
+    } else if (modelList.value[i].type == 3) {
+      // 事件
+      thingsItem.isHistory = modelList.value[i].isHistory
+      thingsItem.isSharePerm = modelList.value[i].isSharePerm
+      thingsItem.isReadonly = modelList.value[i].isReadonly
+      thingsItem.datatype = JSON.parse(modelList.value[i].specs)
+      thingsModel.value.events.push(thingsItem)
+    }
+  }
+
+  openThingsModel.value = true
+}
+
+/**关闭物模型 */
+const handleCloseThingsModel = () => {
+  openThingsModel.value = false
+}
+
+/** 选择物模型 */
+const handleSelect = () => {
+  openSelect.value = true
+  title.value = t('product.product-things-model142341-1')
+  form.type = 1
+  form.datatype = 'integer'
+  form.specs = {
+    enumList: []
+  }
+}
+
+// 取消导入通用物模型按钮
+const cancelSelect = () => {
+  openSelect.value = false
+  productSelectTemplateRef.value?.selectTemplateTable.clearSelection()
+}
+
+// 获取子组件的值
+const getChildData = (data) => {
+  templateIds.value = data
+}
+
+// 导入通用物模型按钮
+const importSelect = () => {
+  if (templateIds.value != null && templateIds.value.length > 0) {
+    var importData = {
+      productId: productInfo.value.productId,
+      productName: productInfo.value.productName,
+      templateIds: templateIds.value
+    }
+    importModel(importData).then((response) => {
+      ElMessage.success(response.msg)
+      openSelect.value = false
+      productSelectTemplateRef.value?.selectTemplateTable.clearSelection()
+      getList()
+      emit('updateModel')
+    })
+  }
+}
+
+const containsUnderscore = (value) => {
+  // 使用正则表达式检查值中是否包含下划线
+  return /_/.test(value)
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      // 验证对象或对象数组中的参数不能为空
+      if (
+        form.datatype == 'object' ||
+        (form.datatype == 'array' && form.specs.arrayType == 'object')
+      ) {
+        if (!form.specs.params || form.specs.params == 0) {
+          ElMessage.error(t('product.product-things-model142341-102'))
+          return
+        }
+        if (containsUnderscore(form.identifier)) {
+          ElMessage.error(t('product.product-things-model142341-103'))
+          return
+        }
+      }
+      // 验证对象参数标识符不能相同
+      if (form.specs.params && form.specs.params.length > 0) {
+        let arr = form.specs.params.map((item) => item.id).sort()
+        for (let i = 0; i < arr.length; i++) {
+          if (arr[i] == arr[i + 1]) {
+            ElMessage.error('参数标识 ' + arr[i] + ' 重复')
+            return
+          }
+        }
+      }
+      //验证模型特性为图表展示时,数据类型是否为整数或者小数
+      if (
+        form.isChart == 1 &&
+        form.datatype != 'integer' &&
+        form.isChart == 1 &&
+        form.datatype != 'decimal'
+      ) {
+        ElMessage.error(t('product.product-things-model142341-106'))
+      } else if (form.modelId != null) {
+        // 格式化specs
+        let tempForm = JSON.parse(JSON.stringify(form))
+        tempForm.specs = formatThingsSpecs()
+        if (form.type == 2) {
+          tempForm.isMonitor = 0
+          tempForm.isChart = 0
+        } else if (form.type == 3) {
+          tempForm.isMonitor = 0
+          tempForm.isChart = 0
+        }
+        updateModel(tempForm).then((response) => {
+          ElMessage.success(t('product.product-things-model142341-107'))
+          open.value = false
+          getList()
+          emit('updateModel')
+        })
+      } else {
+        // 格式化specs
+        let tempForm = JSON.parse(JSON.stringify(form))
+        tempForm.specs = formatThingsSpecs()
+        tempForm.productId = productInfo.value.productId
+        tempForm.productName = productInfo.value.productName
+        if (form.type == 2) {
+          tempForm.isMonitor = 0
+        } else if (form.type == 3) {
+          tempForm.isMonitor = 0
+          tempForm.isChart = 0
+        }
+        addModel(tempForm).then((response) => {
+          ElMessage.success(t('product.product-things-model142341-108'))
+          open.value = false
+          getList()
+          emit('updateModel')
+        })
+      }
+    }
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = (row) => {
+  const modelIds = row.modelId
+  if (!queryParams.isModbus) {
+    ElMessageBox
+      .confirm(t('product.product-things-model142341-109', modelIds))
+      .then(function () {
+        delModel(modelIds).then(() => {
+          emit('updateModel')
+        })
+      })
+      .then(() => {
+        getList()
+        ElMessage.success(t('product.product-things-model142341-111'))
+      })
+      .catch(() => {})
+  } else {
+    ElMessage.alert('采集点删除请在采集点模板修改')
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  download(
+    'iot/model/export',
+    {
+      ...queryParams
+    },
+    `model_${new Date().getTime()}.xlsx`
+  )
+}
+
+// 类型改变
+const typeChange = (type) => {
+  if (type == 1) {
+    form.isChart = 1
+    form.isHistory = 1
+    form.isSharePerm = 1
+    form.isMonitor = 1
+    form.isReadonly = 1
+    form.datatype = 'integer'
+  } else if (type == 2) {
+    form.isChart = 0
+    form.isHistory = 1
+    form.isSharePerm = 1
+    form.isMonitor = 0
+    form.isReadonly = 0
+  } else if (type == 3) {
+    form.isChart = 0
+    form.isHistory = 1
+    form.isSharePerm = 0
+    form.isMonitor = 0
+    form.isReadonly = 1
+  }
+}
+
+// 是否图表展示改变
+const isChartChange = () => {
+  if (form.isChart == 1) {
+    form.isReadonly = 1
+  } else {
+    form.isMonitor = 0
+  }
+}
+
+// 是否实时监测改变
+const isMonitorChange = () => {
+  if (form.isMonitor == 1) {
+    form.isReadonly = 1
+    form.isChart = 1
+  }
+}
+
+// 是否只读数据改变
+const isReadonlyChange = () => {
+  if (form.isReadonly == 0) {
+    form.isMonitor = 0
+    form.isChart = 0
+  }
+}
+
+// 格式化物模型
+const formatThingsSpecs = () => {
+  var data = {}
+  data.type = form.datatype
+  if (form.datatype == 'integer' || form.datatype == 'decimal') {
+    data.min = Number(form.specs.min ? form.specs.min : 0)
+    data.max = Number(form.specs.max ? form.specs.max : 100)
+    data.unit = form.specs.unit ? form.specs.unit : ''
+    data.step = Number(form.specs.step ? form.specs.step : 1)
+  } else if (form.datatype == 'string') {
+    data.maxLength = Number(form.specs.maxLength ? form.specs.maxLength : 1024)
+  } else if (form.datatype == 'bool') {
+    data.falseText = form.specs.falseText ? form.specs.falseText : t('close')
+    data.trueText = form.specs.trueText ? form.specs.trueText : t('open')
+  } else if (form.datatype == 'enum') {
+    data.showWay = form.specs.showWay
+    if (form.specs.enumList && form.specs.enumList[0].text != '') {
+      data.enumList = form.specs.enumList
+    } else {
+      data.showWay = 'select'
+      data.enumList = [
+        {
+          value: '0',
+          text: t('product.product-things-model142341-115')
+        },
+        {
+          value: '1',
+          text: t('product.product-things-model142341-116')
+        }
+      ]
+    }
+  } else if (form.datatype == 'array') {
+    data.arrayType = form.specs.arrayType
+    data.arrayCount = form.specs.arrayCount ? form.specs.arrayCount : 5
+    if (data.arrayType == 'object') {
+      data.params = form.specs.params
+      // 物模型名称作为参数的标识符前缀
+      for (let i = 0; i < data.params.length; i++) {
+        data.params[i].id = form.identifier + '_' + data.params[i].id
+      }
+    }
+  } else if (form.datatype == 'object') {
+    data.params = form.specs.params
+    // 物模型名称作为参数的标识符前缀
+    for (let i = 0; i < data.params.length; i++) {
+      data.params[i].id = form.identifier + '_' + data.params[i].id
+    }
+  }
+  return JSON.stringify(data)
+}
+
+/** 数据类型改变 */
+const dataTypeChange = (val) => {}
+
+/** 添加枚举项 */
+const addEnumItem = () => {
+  form.specs.enumList.push({
+    value: '',
+    text: ''
+  })
+}
+
+/** 删除枚举项 */
+const removeEnumItem = (index) => {
+  form.specs.enumList.splice(index, 1)
+}
+
+/** 格式化显示数据定义 */
+const formatSpecsDisplay = (json) => {
+  let specs = JSON.parse(json)
+  if (
+    specs.type === 'integer' ||
+    specs.type === 'decimal' ||
+    specs.type === 'INT16' ||
+    specs.type === 'INT'
+  ) {
+    return (
+      `<span style=\'width:50%;display:inline-block;\'>${t('product.product-things-model142341-117')}<span style="color:#F56C6C">` +
+      specs.max +
+      `</span></span>${t('product.product-things-model142341-118')}<span style="color:#F56C6C">` +
+      specs.min +
+      `</span><br /><span style=\'width:50%;display:inline-block;\'>${t('product.product-things-model142341-119')}<span style="color:#F56C6C">` +
+      specs.step +
+      `</span></span>${t('product.product-things-model142341-120')}<span style="color:#F56C6C">` +
+      specs.unit
+    )
+  } else if (specs.type === 'string') {
+    return (
+      `${t('product.product-things-model142341-121')}<span style="color:#F56C6C">` +
+      specs.maxLength +
+      '</span>'
+    )
+  } else if (specs.type === 'array') {
+    return (
+      `<span style=\'width:50%;display:inline-block;\'>${t('product.product-things-model142341-122')}<span style="color:#F56C6C">` +
+      specs.arrayType +
+      `</span></span>${t('product.product-things-model142341-123')}<span style="color:#F56C6C">` +
+      specs.arrayCount
+    )
+  } else if (specs.type === 'enum') {
+    let items = ''
+    for (let i = 0; i < specs.enumList.length; i++) {
+      items =
+        items +
+        "<span style='width:50%;display:inline-block;'>" +
+        specs.enumList[i].value +
+        ":<span style='color:#F56C6C'>" +
+        specs.enumList[i].text +
+        '</span></span>'
+      if (i > 0 && i % 2 != 0) {
+        items = items + '<br />'
+      }
+    }
+    return items
+  } else if (specs.type === 'bool') {
+    return (
+      '<span style=\'width:50%;display:inline-block;\'>0:<span style="color:#F56C6C">' +
+      specs.falseText +
+      '</span></span>1:<span style="color:#F56C6C">' +
+      specs.trueText
+    )
+  } else if (specs.type === 'object') {
+    let items = ''
+    for (let i = 0; i < specs.params.length; i++) {
+      items =
+        items +
+        "<span style='width:50%;display:inline-block;'>" +
+        specs.params[i].name +
+        ":<span style='color:#F56C6C'>" +
+        specs.params[i].datatype.type +
+        '</span></span>'
+      if (i > 0 && i % 2 != 0) {
+        items = items + '<br />'
+      }
+    }
+    return items
+  }
+}
+
+/** 添加参数 */
+const addParameter = () => {
+  paramData.index = -1
+  paramData.parameter = {}
+}
+
+/** 编辑参数*/
+const editParameter = (data, index) => {
+  paramData.index = index
+  paramData.parameter = data
+}
+
+/** 删除动作 */
+const removeParameter = (index) => {
+  form.specs.params.splice(index, 1)
+}
+
+/**获取设置的参数对象*/
+const getParamData = (data) => {
+  if (data.index == -1) {
+    form.specs.params.push(data.parameter)
+  } else {
+    form.specs.params[data.index] = data.parameter
+    // Vue 3 中不需要特殊处理,响应式系统会自动检测数组变化
+  }
+}
+
+const handleImport = () => {
+  // this.$refs.importBatchRef.upload.importDeviceDialog = true;
+}
+
+</script>
+
+<style lang="scss" scoped>
+.specsColor {
+  background-color: #fcfcfc;
+}
+
+.pagination-container {
+  ::v-deep .el-pagination--small .el-pager li:last-child {
+    line-height: 28px;
+    height: auto;
+  }
+}
+</style>

+ 893 - 0
src/views/pms/video_center/product/template/index.vue

@@ -0,0 +1,893 @@
+<template>
+    <div style="padding: 6px">
+        <el-card v-show="showSearch" style="margin-bottom: 5px">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px" style="margin-bottom: -20px">
+                <el-form-item label="名称" prop="templateName">
+                    <el-input v-model="queryParams.templateName" placeholder="请输入物模型名称" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item label="类别" prop="type">
+                    <el-select v-model="queryParams.type" placeholder="请选择模型类别" clearable size="small">
+                        <el-option v-for="dict in dict.type.iot_things_type" :key="dict.value" :label="dict.label" :value="dict.value" />
+                    </el-select>
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+                </el-form-item>
+                <el-form-item style="float: right"></el-form-item>
+            </el-form>
+        </el-card>
+        <el-card class="main-card">
+            <div class="card-toolbar mb8">
+                <div>
+                    <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:template:add']">新增</el-button>
+                    <el-button type="primary" icon="el-icon-plus" size="mini" @click="handleTemp" v-hasPermi="['iot:template:temp']">导入</el-button>
+                </div>
+            </div>
+            <el-table class="base-table" v-loading="loading" :data="templateList" @selection-change="handleSelectionChange" :border="false">
+                <el-table-column label="名称" align="left" prop="templateName" min-width="120" />
+                <el-table-column label="标识符" align="left" prop="identifier" width="80" />
+                <el-table-column label="图表展示" align="center" prop="isMonitor" width="90">
+                    <template slot-scope="scope">
+                        <!-- <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isChart" /> -->
+                        <i style="color: #346cef; font-size: 20px" v-if="scope.row.isChart == 1" class="el-icon el-icon-success"></i>
+                        <i style="font-size: 20px" v-else class="el-icon el-icon-error"></i>
+                    </template>
+                </el-table-column>
+                <el-table-column label="实时监测" align="center" prop="" width="90">
+                    <template slot-scope="scope">
+                        <i style="color: #346cef; font-size: 20px" v-if="scope.row.isMonitor == 1" class="el-icon el-icon-success"></i>
+                        <i style="font-size: 20px" v-else class="el-icon el-icon-error"></i>
+                    </template>
+                </el-table-column>
+                <el-table-column label="只读" align="center" prop="" width="90">
+                    <template slot-scope="scope">
+                        <i style="color: #346cef; font-size: 20px" v-if="scope.row.isReadonly == 1" class="el-icon el-icon-success"></i>
+                        <i style="font-size: 20px" v-else class="el-icon el-icon-error"></i>
+                        <!-- <dict-tag :options="dict.type.iot_yes_no" :value="scope.row.isReadonly" /> -->
+                    </template>
+                </el-table-column>
+                <el-table-column label="历史存储" align="center" prop="" width="90">
+                    <template slot-scope="scope">
+                        <i style="color: #346cef; font-size: 20px" v-if="scope.row.isHistory == 1" class="el-icon el-icon-success"></i>
+                        <i style="font-size: 20px" v-else class="el-icon el-icon-error"></i>
+                    </template>
+                </el-table-column>
+                <el-table-column label="系统定义" align="center" prop="isSys" width="90">
+                    <template slot-scope="scope">
+                        <i style="color: #346cef; font-size: 20px" v-if="scope.row.isSys == 1" class="el-icon el-icon-success"></i>
+                        <i style="font-size: 20px" v-else class="el-icon el-icon-error"></i>
+                    </template>
+                </el-table-column>
+                <el-table-column label="物模型类别" align="center" prop="type" width="90">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_things_type" :value="scope.row.type" />
+                    </template>
+                </el-table-column>
+                <el-table-column label="数据类型" align="center" prop="datatype" width="120">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_data_type" :value="scope.row.datatype" />
+                    </template>
+                </el-table-column>
+                <el-table-column label="数据定义" align="left" header-align="center" width="220" prop="specs" class-name="specsColor">
+                    <template slot-scope="scope">
+                        <div v-html="formatSpecsDisplay(scope.row.specs)"></div>
+                    </template>
+                </el-table-column>
+                <el-table-column label="排序" align="center" prop="modelOrder" width="100" />
+                <el-table-column label="创建时间" align="center" prop="createTime" width="150">
+                    <template slot-scope="scope">
+                        <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+                    </template>
+                </el-table-column>
+                <el-table-column label="操作" align="center" class-name="small-padding fixed-width" min-width="140">
+                    <template slot-scope="scope">
+                        <el-button
+                            size="small"
+                            type="primary"
+                            plain
+                            style="padding: 5px"
+                            icon="el-icon-view"
+                            @click="handleUpdate(scope.row)"
+                            v-hasPermi="['iot:template:query']"
+                            v-if="scope.row.isSys == '0' ? true : !isTenant"
+                        >
+                            查看
+                        </el-button>
+                        <el-button
+                            size="small"
+                            type="danger"
+                            plain
+                            style="padding: 5px"
+                            icon="el-icon-delete"
+                            @click="handleDelete(scope.row)"
+                            v-hasPermi="['iot:template:remove']"
+                            v-if="scope.row.isSys == '0' ? true : !isTenant"
+                        >
+                            删除
+                        </el-button>
+                        <span style="font-size: 10px; color: #999" v-if="scope.row.isSys == '1' && isTenant">系统定义,不能修改</span>
+                    </template>
+                </el-table-column>
+            </el-table>
+
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+
+            <!-- 添加或修改通用物模型对话框 -->
+            <el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
+                <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+                    <el-form-item label="模型名称" prop="templateName">
+                        <el-input v-model="form.templateName" placeholder="请输入物模型名称,例如:温度" style="width: 385px" />
+                    </el-form-item>
+                    <el-form-item label="模型标识" prop="identifier">
+                        <el-input v-model="form.identifier" placeholder="请输入标识符,例如:temperature" style="width: 385px" />
+                    </el-form-item>
+                    <el-form-item label="模型排序" prop="modelOrder">
+                        <el-input-number controls-position="right" v-model="form.modelOrder" placeholder="请输入排序" type="number" style="width: 385px" />
+                    </el-form-item>
+                    <el-form-item label="模型类别" prop="type">
+                        <el-radio-group v-model="form.type" @change="typeChange(form.type)">
+                            <el-radio-button label="1">属性</el-radio-button>
+                            <el-radio-button label="2">功能</el-radio-button>
+                            <el-radio-button label="3">事件</el-radio-button>
+                        </el-radio-group>
+                    </el-form-item>
+
+                    <el-form-item label="模型特性" prop="property">
+                        <el-row>
+                            <el-col :span="6">
+                                <el-tooltip effect="dark" content="设备详情中以图表方式展示" placement="top">
+                                    <el-checkbox name="isChart" label="图表展示" @change="isChartChange" v-show="form.type == 1" v-model="form.isChart" :true-label="1" :false-label="0"></el-checkbox>
+                                </el-tooltip>
+                            </el-col>
+                            <el-col :span="6">
+                                <el-tooltip effect="dark" content="实时显示监测数据,但是不会存储到数据库" placement="top">
+                                    <el-checkbox name="isMonitor" label="实时监测" @change="isMonitorChange" v-show="form.type == 1" v-model="form.isMonitor" :true-label="1" :false-label="0"></el-checkbox>
+                                </el-tooltip>
+                            </el-col>
+                            <el-col :span="6">
+                                <el-tooltip effect="dark" content="设备上报数据,但是平台不能下发指令" placement="top">
+                                    <el-checkbox name="isReadonly" label="只读数据" @change="isReadonlyChange" :disabled="form.type == 3" v-model="form.isReadonly" :true-label="1" :false-label="0"></el-checkbox>
+                                </el-tooltip>
+                            </el-col>
+                            <el-col :span="6">
+                                <el-tooltip effect="dark" content="设备上报的数据会存储到数据库作为历史数据" placement="top">
+                                    <el-checkbox name="isHistory" label="历史存储" v-model="form.isHistory" :true-label="1" :false-label="0"></el-checkbox>
+                                </el-tooltip>
+                            </el-col>
+                            <el-col :span="6">
+                                <el-tooltip effect="dark" content="设备分享时需要指定是否拥有该权限" placement="top">
+                                    <el-checkbox name="isSharePerm" label="分享权限" v-model="form.isSharePerm" :true-label="1" :false-label="0"></el-checkbox>
+                                </el-tooltip>
+                            </el-col>
+                        </el-row>
+                    </el-form-item>
+
+                    <el-divider></el-divider>
+                    <el-form-item label="数据类型" prop="datatype">
+                        <el-select v-model="form.datatype" placeholder="请选择数据类型" @change="dataTypeChange" style="width: 175px">
+                            <el-option key="integer" label="整数" value="integer"></el-option>
+                            <el-option key="decimal" label="小数" value="decimal"></el-option>
+                            <el-option key="bool" label="布尔" value="bool"></el-option>
+                            <el-option key="enum" label="枚举" value="enum"></el-option>
+                            <el-option key="string" label="字符串" value="string"></el-option>
+                            <el-option key="array" label="数组" value="array"></el-option>
+                            <el-option key="object" label="对象" value="object"></el-option>
+<!--                            <el-option key="bool" label="布尔" value="bool" :disabled="form.isChart == 1"></el-option>
+                            <el-option key="enum" label="枚举" value="enum" :disabled="form.isChart == 1"></el-option>
+                            <el-option key="string" label="字符串" value="string" :disabled="form.isChart == 1"></el-option>
+                            <el-option key="array" label="数组" value="array" :disabled="form.isChart == 1"></el-option>
+                            <el-option key="object" label="对象" value="object" :disabled="form.isChart == 1"></el-option>-->
+                        </el-select>
+                    </el-form-item>
+                    <div v-if="form.datatype == 'integer' || form.datatype == 'decimal'">
+                        <el-form-item label="取值范围">
+                            <el-row>
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.min" placeholder="最小值" type="number" />
+                                </el-col>
+                                <el-col :span="2" align="center">到</el-col>
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.max" placeholder="最大值" type="number" />
+                                </el-col>
+                            </el-row>
+                        </el-form-item>
+                        <el-form-item label="单位">
+                            <el-input v-model="form.specs.unit" placeholder="请输入单位,例如:℃" style="width: 385px" />
+                        </el-form-item>
+                        <el-form-item label="步长">
+                            <el-input-number controls-position="right" v-model="form.specs.step" placeholder="请输入步长,例如:1" type="number" style="width: 385px" />
+                        </el-form-item>
+                    </div>
+                    <div v-if="form.datatype == 'bool'">
+                        <el-form-item label="布尔值" prop="">
+                            <el-row style="margin-bottom: 10px">
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.falseText" placeholder="例如:关闭" />
+                                </el-col>
+                                <el-col :span="10" :offset="1">(0 值对应文本)</el-col>
+                            </el-row>
+                            <el-row>
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.trueText" placeholder="例如:打开" />
+                                </el-col>
+                                <el-col :span="10" :offset="1">(1 值对应文本)</el-col>
+                            </el-row>
+                        </el-form-item>
+                    </div>
+                    <div v-if="form.datatype == 'enum'">
+                        <el-form-item label="展示方式">
+                            <el-select v-model="form.specs.showWay" placeholder="请选择展示方式" style="width: 175px">
+                                <el-option key="select" label="下拉框" value="select"></el-option>
+                                <el-option key="button" label="按钮" value="button"></el-option>
+                            </el-select>
+                        </el-form-item>
+                        <el-form-item label="枚举项" prop="">
+                            <el-row v-for="(item, index) in form.specs.enumList" :key="'enum' + index" style="margin-bottom: 10px">
+                                <el-col :span="9">
+                                    <el-input v-model="item.value" placeholder="参数值,例如:0" />
+                                </el-col>
+                                <el-col :span="11" :offset="1">
+                                    <el-input v-model="item.text" placeholder="参数描述,例如:中速档位" />
+                                </el-col>
+                                <el-col :span="2" :offset="1" v-if="index != 0"><a style="color: #f56c6c" @click="removeEnumItem(index)">删除</a></el-col>
+                            </el-row>
+                            <div>
+                                +
+                                <a style="color: #409eff" @click="addEnumItem()">添加枚举项</a>
+                            </div>
+                        </el-form-item>
+                    </div>
+                    <div v-if="form.datatype == 'string'">
+                        <el-form-item label="最大长度" prop="">
+                            <el-row>
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.maxLength" placeholder="例如:1024" type="number" />
+                                </el-col>
+                                <el-col :span="14" :offset="1">(字符串的最大长度)</el-col>
+                            </el-row>
+                        </el-form-item>
+                    </div>
+                    <div v-if="form.datatype == 'array'">
+                        <el-form-item label="元素个数" prop="">
+                            <el-row>
+                                <el-col :span="9">
+                                    <el-input v-model="form.specs.arrayCount" placeholder="例如:5" type="number" />
+                                </el-col>
+                            </el-row>
+                        </el-form-item>
+                        <el-form-item label="数组类型" prop="">
+                            <el-radio-group v-model="form.specs.arrayType">
+                                <el-radio label="integer">整数</el-radio>
+                                <el-radio label="decimal">小数</el-radio>
+                                <el-radio label="string">字符串</el-radio>
+                                <el-radio label="object">对象</el-radio>
+                            </el-radio-group>
+                        </el-form-item>
+                        <el-form-item label="对象参数" v-if="form.specs.arrayType == 'object'">
+                            <div style="background-color: #f8f8f8; border-radius: 5px">
+                                <el-row style="padding: 0 10px 5px" v-for="(item, index) in form.specs.params" :key="index">
+                                    <div style="margin-top: 5px" v-if="index == 0"></div>
+                                    <el-col :span="18">
+                                        <el-input readonly v-model="item.name" size="mini" placeholder="请选择设备" style="margin-top: 3px">
+                                            <template slot="prepend">
+                                                <el-tag size="mini" effect="dark" style="margin-left: -21px; height: 26px; line-height: 26px">{{ item.order }}</el-tag>
+                                                {{ form.identifier + '_' + item.id }}
+                                            </template>
+                                            <el-button slot="append" @click="editParameter(item, index)" size="small">编辑</el-button>
+                                        </el-input>
+                                    </el-col>
+                                    <el-col :span="2" :offset="2">
+                                        <el-button size="small" plain type="danger" style="padding: 5px" icon="el-icon-delete" @click="removeParameter(index)">删除</el-button>
+                                    </el-col>
+                                </el-row>
+                            </div>
+                            <div>
+                                +
+                                <a style="color: #409eff" @click="addParameter()">添加参数</a>
+                            </div>
+                        </el-form-item>
+                    </div>
+                    <div v-if="form.datatype == 'object'">
+                        <el-form-item label="对象参数" prop="">
+                            <div style="background-color: #f8f8f8; border-radius: 5px">
+                                <el-row style="padding: 0 10px 5px" v-for="(item, index) in form.specs.params" :key="index">
+                                    <div style="margin-top: 5px" v-if="index == 0"></div>
+                                    <el-col :span="18">
+                                        <el-input readonly v-model="item.name" size="mini" placeholder="请选择设备" style="margin-top: 3px">
+                                            <template slot="prepend">
+                                                <el-tag size="mini" effect="dark" style="margin-left: -21px; height: 26px; line-height: 26px">{{ item.order }}</el-tag>
+                                                {{ form.identifier + '_' + item.id }}
+                                            </template>
+                                            <el-button slot="append" @click="editParameter(item, index)">编辑</el-button>
+                                        </el-input>
+                                    </el-col>
+                                    <el-col :span="2" :offset="2">
+                                        <el-button size="small" plain type="danger" style="padding: 5px" icon="el-icon-delete" @click="removeParameter(index)">删除</el-button>
+                                    </el-col>
+                                </el-row>
+                            </div>
+                            <div>
+                                +
+                                <a style="color: #409eff" @click="addParameter()">添加参数</a>
+                            </div>
+                        </el-form-item>
+                    </div>
+                </el-form>
+
+                <div slot="footer" class="dialog-footer">
+                    <el-button type="primary" @click="submitForm" v-hasPermi="['iot:template:edit']" v-show="form.templateId">修 改</el-button>
+                    <el-button type="primary" @click="submitForm" v-hasPermi="['iot:template:add']" v-show="!form.templateId">新 增</el-button>
+                    <el-button @click="cancel">取 消</el-button>
+                </div>
+            </el-dialog>
+
+            <!--物模型参数类型-->
+            <things-parameter :data="paramData" @dataEvent="getParamData($event)" />
+        </el-card>
+        <!-- 导入对话框 -->
+        <el-dialog title="导入" :visible.sync="openTemp" append-to-body width="400px">
+            <el-upload
+                ref="upload"
+                :action="upload.url"
+                :auto-upload="false"
+                :data="{ tempSlaveId: this.queryParams.tempSlaveId }"
+                :disabled="upload.isUploading"
+                :headers="upload.headers"
+                :limit="1"
+                :on-progress="handleFileUploadProgress"
+                :on-success="handleFileSuccess"
+                accept=".xlsx, .xls"
+                drag
+            >
+                <i class="el-icon-upload"></i>
+                <div class="el-upload__text">
+                    将文件拖到此处,或
+                    <em>点击上传</em>
+                </div>
+                <div slot="tip" class="el-upload__tip text-center">
+                    <span>仅允许导入xls、xlsx格式文件。</span>
+                    <el-link :underline="false" style="font-size: 12px; vertical-align: baseline" type="primary" @click="importTemplate">下载模板</el-link>
+                </div>
+            </el-upload>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitFileForm">确 定</el-button>
+                <el-button @click="openTemp = false">取 消</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<style>
+.specsColor {
+    background-color: #fcfcfc;
+}
+</style>
+
+<script>
+import { listTemplate, getTemplate, delTemplate, addTemplate, updateTemplate } from '@/api/iot/template';
+import thingsParameter from './parameter';
+import {getToken} from "@/utils/auth";
+export default {
+    name: 'Template',
+    dicts: ['iot_things_type', 'iot_data_type', 'iot_yes_no'],
+    components: {
+        thingsParameter,
+    },
+    data() {
+        return {
+            // 是否为租户
+            isTenant: false,
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 通用物模型表格数据
+            templateList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                templateName: null,
+                type: null,
+            },
+            // 表单参数
+            form: {},
+            // 对象类型参数
+            paramData: {
+                index: -1,
+                parameter: {},
+            },
+            openTemp:false,
+            // 导入参数
+            upload: {
+                // 是否显示弹出层
+                open: false,
+                // 弹出层标题
+                title: '',
+                // 设置上传的请求头部
+                headers: {
+                    Authorization: 'Bearer ' + getToken(),
+                },
+                // 上传的地址
+                url: process.env.VUE_APP_BASE_API + '/iot/template/temp',
+            },
+            // 表单校验
+            rules: {
+                templateName: [
+                    {
+                        required: true,
+                        message: '物模型名称不能为空',
+                        trigger: 'blur',
+                    },
+                ],
+                identifier: [
+                    {
+                        required: true,
+                        message: '标识符,产品下唯一不能为空',
+                        trigger: 'blur',
+                    },
+                ],
+                modelOrder: [
+                    {
+                        required: true,
+                        message: '模型排序不能为空',
+                        trigger: 'blur',
+                    },
+                ],
+                type: [
+                    {
+                        required: true,
+                        message: '模型类别不能为空',
+                        trigger: 'change',
+                    },
+                ],
+                datatype: [
+                    {
+                        required: true,
+                        message: '数据类型不能为空',
+                        trigger: 'change',
+                    },
+                ],
+            },
+        };
+    },
+    created() {
+        this.getList();
+        this.init();
+    },
+    methods: {
+        /*导入模板*/
+        importTemplate() {
+            this.download('iot/template/temp-json', {}, `采集点Json_${new Date().getTime()}.xlsx`);
+        },
+        // 提交上传文件
+        submitFileForm() {
+            this.$refs.upload.submit();
+        },
+        // 文件上传中处理
+        handleFileUploadProgress(event, file, fileList) {
+            this.upload.isUploading = true;
+        },
+        // 文件上传成功处理
+        handleFileSuccess(response, file, fileList) {
+            this.upload.open = false;
+            this.upload.isUploading = false;
+            this.$refs.upload.clearFiles();
+            this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
+                dangerouslyUseHTMLString: true,
+            });
+            this.getList();
+        },
+        init() {
+            if (this.$store.state.user.roles.indexOf('tenant') !== -1) {
+                this.isTenant = true;
+            }
+        },
+        /** 查询通用物模型列表 */
+        getList() {
+            this.loading = true;
+            listTemplate(this.queryParams).then((response) => {
+                this.templateList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.form = {
+                templateId: null,
+                templateName: null,
+                userId: null,
+                userName: null,
+                tenantId: null,
+                tenantName: null,
+                identifier: null,
+                modelOrder: 0,
+                type: 1,
+                datatype: 'integer',
+                isSys: null,
+                isChart: 1,
+                isHistory: 1,
+                isMonitor: 1,
+                isReadonly: 1,
+                isSharePerm: 1,
+                delFlag: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+                specs: {
+                    enumList: [
+                        {
+                            value: '',
+                            text: '',
+                        },
+                    ],
+                    arrayType: 'integer',
+                    arrayCount: 5,
+                    showWay: 'select', // 显示方式select=下拉选择框,button=按钮
+                    params: [],
+                },
+            };
+            this.resetForm('form');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.templateId);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+        /** 新增按钮操作 */
+        handleAdd() {
+            this.reset();
+            this.open = true;
+            this.title = '添加通用物模型';
+        },
+        handleTemp(){
+            this.openTemp=true;
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            const templateId = row.templateId || this.ids;
+            getTemplate(templateId).then((response) => {
+                let tempForm = response.data;
+                this.open = true;
+                this.title = '修改通用物模型';
+                // Json转对象
+                tempForm.specs = JSON.parse(tempForm.specs);
+                if (!tempForm.specs.enumList) {
+                    tempForm.specs.showWay = 'select';
+                    tempForm.specs.enumList = [
+                        {
+                            value: '',
+                            text: '',
+                        },
+                    ];
+                }
+                if (!tempForm.specs.arrayType) {
+                    tempForm.specs.arrayType = 'integer';
+                }
+                if (!tempForm.specs.arrayCount) {
+                    tempForm.specs.arrayCount = 5;
+                }
+                if (!tempForm.specs.params) {
+                    tempForm.specs.params = [];
+                }
+                // 对象和数组中参数删除前缀
+                if ((tempForm.specs.type == 'array' && tempForm.specs.arrayType == 'object') || tempForm.specs.type == 'object') {
+                    for (let i = 0; i < tempForm.specs.params.length; i++) {
+                        tempForm.specs.params[i].id = String(tempForm.specs.params[i].id).substring(String(tempForm.identifier).length + 1);
+                    }
+                }
+                this.form = tempForm;
+            });
+        },
+        containsUnderscore(value) {
+            // 使用正则表达式检查值中是否包含下划线
+            return /_/.test(value);
+        },
+        /** 提交按钮 */
+        submitForm() {
+            this.$refs['form'].validate((valid) => {
+                if (valid) {
+                    // 验证对象或对象数组中的参数不能为空
+                    if (this.form.datatype == 'object' || (this.form.datatype == 'array' && this.form.specs.arrayType == 'object')) {
+                        if (!this.form.specs.params || this.form.specs.params == 0) {
+                            this.$modal.msgError('对象的参数不能为空');
+                            return;
+                        }
+                        if (this.containsUnderscore(this.form.identifier)) {
+                            this.$modal.msgError('对象类型模型标识输入不能包含下划线,请重新填写模型标识!');
+                            return;
+                        }
+                    }
+                    // 验证对象参数标识符不能相同
+                    if (this.form.specs.params && this.form.specs.params.length > 0) {
+                        let arr = this.form.specs.params.map((item) => item.id).sort();
+                        for (let i = 0; i < arr.length; i++) {
+                            if (arr[i] == arr[i + 1]) {
+                                this.$modal.msgError('参数标识 ' + arr[i] + ' 重复');
+                                return;
+                            }
+                        }
+                    }
+                    //验证模型特性为图表展示时,数据类型是否为整数或者小数
+                    if (this.form.isChart == 1 && this.form.datatype != 'integer' && this.form.isChart == 1 && this.form.datatype != 'decimal') {
+                        this.$modal.msgError('请重新选择数据类型!');
+                    } else if (this.form.templateId != null) {
+                        // 格式化specs
+                        let tempForm = JSON.parse(JSON.stringify(this.form));
+                        tempForm.specs = this.formatThingsSpecs();
+                        if (this.form.type == 2) {
+                            tempForm.isMonitor = 0;
+                            tempForm.isChart = 0;
+                        } else if (this.form.type == 3) {
+                            tempForm.isMonitor = 0;
+                            tempForm.isChart = 0;
+                        }
+                        // 添加通用物模型的修改者
+                        tempForm.updateBy = this.$store.state.user.name;
+                        updateTemplate(tempForm).then((response) => {
+                            this.$modal.msgSuccess('修改成功');
+                            this.open = false;
+                            this.getList();
+                        });
+                    } else {
+                        // 格式化specs
+                        let tempForm = JSON.parse(JSON.stringify(this.form));
+                        tempForm.specs = this.formatThingsSpecs();
+                        if (this.form.type == 2) {
+                            tempForm.isMonitor = 0;
+                        } else if (this.form.type == 3) {
+                            tempForm.isMonitor = 0;
+                            tempForm.isChart = 0;
+                        }
+                        // 添加通用物模型的创造者
+                        tempForm.createBy = this.$store.state.user.name;
+                        addTemplate(tempForm).then((response) => {
+                            this.$modal.msgSuccess('新增成功');
+                            this.open = false;
+                            this.getList();
+                        });
+                    }
+                }
+            });
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const templateIds = row.templateId || this.ids;
+            this.$modal
+                .confirm('是否确认删除通用物模型编号为"' + templateIds + '"的数据项?')
+                .then(function () {
+                    return delTemplate(templateIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess('删除成功');
+                })
+                .catch(() => {});
+        },
+        /** 导出按钮操作 */
+        handleExport() {
+            this.download(
+                'iot/template/export',
+                {
+                    ...this.queryParams,
+                },
+                `template_${new Date().getTime()}.xlsx`
+            );
+        },
+        // 类型改变
+        typeChange(type) {
+            console.log(type);
+            if (type == 1) {
+                this.form.isChart = 1;
+                this.form.isHistory = 1;
+                this.form.isMonitor = 1;
+                this.form.isReadonly = 1;
+                this.form.isSharePerm = 1;
+                this.form.datatype = 'integer';
+            } else if (type == 2) {
+                this.form.isChart = 0;
+                this.form.isHistory = 1;
+                this.form.isSharePerm = 1;
+                this.form.isMonitor = 0;
+                this.form.isReadonly = 0;
+            } else if (type == 3) {
+                this.form.isChart = 0;
+                this.form.isHistory = 1;
+                this.form.isMonitor = 0;
+                this.form.isReadonly = 1;
+                this.form.isSharePerm = 0;
+            }
+        },
+        // 是否图表展示改变
+        isChartChange() {
+            if (this.form.isChart == 1) {
+                this.form.isReadonly = 1;
+            } else {
+                this.form.isMonitor = 0;
+            }
+        },
+        // 是否实时监测改变
+        isMonitorChange() {
+            if (this.form.isMonitor == 1) {
+                this.form.isReadonly = 1;
+                this.form.isChart = 1;
+            }
+        },
+        // 是否只读数据改变
+        isReadonlyChange() {
+            if (this.form.isReadonly == 0) {
+                this.form.isMonitor = 0;
+                this.form.isChart = 0;
+            }
+        },
+        // 格式化物模型
+        formatThingsSpecs() {
+            var data = {};
+            data.type = this.form.datatype;
+            if (this.form.datatype == 'integer' || this.form.datatype == 'decimal') {
+                data.min = Number(this.form.specs.min ? this.form.specs.min : 0);
+                data.max = Number(this.form.specs.max ? this.form.specs.max : 100);
+                data.unit = this.form.specs.unit ? this.form.specs.unit : '';
+                data.step = Number(this.form.specs.step ? this.form.specs.step : 1);
+            } else if (this.form.datatype == 'string') {
+                data.maxLength = Number(this.form.specs.maxLength ? this.form.specs.maxLength : 1024);
+            } else if (this.form.datatype == 'bool') {
+                data.falseText = this.form.specs.falseText ? this.form.specs.falseText : '关闭';
+                data.trueText = this.form.specs.trueText ? this.form.specs.trueText : '打开';
+            } else if (this.form.datatype == 'enum') {
+                data.showWay = this.form.specs.showWay;
+                if (this.form.specs.enumList && this.form.specs.enumList[0].text != '') {
+                    data.enumList = this.form.specs.enumList;
+                } else {
+                    data.showWay = 'select';
+                    data.enumList = [
+                        {
+                            value: '0',
+                            text: '低',
+                        },
+                        {
+                            value: '1',
+                            text: '高',
+                        },
+                    ];
+                }
+            } else if (this.form.datatype == 'array') {
+                data.arrayType = this.form.specs.arrayType;
+                data.arrayCount = this.form.specs.arrayCount ? this.form.specs.arrayCount : 5;
+                if (data.arrayType == 'object') {
+                    data.params = this.form.specs.params;
+                    // 物模型名称作为参数的标识符前缀
+                    for (let i = 0; i < data.params.length; i++) {
+                        data.params[i].id = this.form.identifier + '_' + data.params[i].id;
+                    }
+                }
+            } else if (this.form.datatype == 'object') {
+                data.params = this.form.specs.params;
+                // 物模型名称作为参数的标识符前缀
+                for (let i = 0; i < data.params.length; i++) {
+                    data.params[i].id = this.form.identifier + '_' + data.params[i].id;
+                }
+            }
+            return JSON.stringify(data);
+        },
+        /** 数据类型改变 */
+        dataTypeChange(val) {},
+        /** 添加枚举项 */
+        addEnumItem() {
+            this.form.specs.enumList.push({
+                value: '',
+                text: '',
+            });
+        },
+        /** 删除枚举项 */
+        removeEnumItem(index) {
+            this.form.specs.enumList.splice(index, 1);
+        },
+        /** 格式化显示数据定义 */
+        formatSpecsDisplay(json) {
+            if (json == null || json == undefined) {
+                return;
+            }
+            let specs = JSON.parse(json);
+            if (specs.type === 'integer' || specs.type === 'decimal') {
+                return (
+                    '<span style=\'width:50%;display:inline-block;\'>最大值:<span style="color:#F56C6C">' +
+                    specs.max +
+                    '</span></span>最小值:<span style="color:#F56C6C">' +
+                    specs.min +
+                    '</span><br /><span style=\'width:50%;display:inline-block;\'>步长:<span style="color:#F56C6C">' +
+                    specs.step +
+                    '</span></span>单位:<span style="color:#F56C6C">' +
+                    specs.unit
+                );
+            } else if (specs.type === 'string') {
+                return '最大长度:<span style="color:#F56C6C">' + specs.maxLength + '</span>';
+            } else if (specs.type === 'array') {
+                return '<span style=\'width:50%;display:inline-block;\'>数组类型:<span style="color:#F56C6C">' + specs.arrayType + '</span></span>元素个数:<span style="color:#F56C6C">' + specs.arrayCount;
+            } else if (specs.type === 'enum') {
+                let items = '';
+                for (let i = 0; i < specs.enumList.length; i++) {
+                    items = items + "<span style='width:50%;display:inline-block;'>" + specs.enumList[i].value + ":<span style='color:#F56C6C'>" + specs.enumList[i].text + '</span></span>';
+                    if (i > 0 && i % 2 != 0) {
+                        items = items + '<br />';
+                    }
+                }
+                return items;
+            } else if (specs.type === 'bool') {
+                return '<span style=\'width:50%;display:inline-block;\'>0:<span style="color:#F56C6C">' + specs.falseText + '</span></span>1:<span style="color:#F56C6C">' + specs.trueText;
+            } else if (specs.type === 'object') {
+                let items = '';
+                for (let i = 0; i < specs.params.length; i++) {
+                    items = items + "<span style='width:50%;display:inline-block;'>" + specs.params[i].name + ":<span style='color:#F56C6C'>" + specs.params[i].datatype.type + '</span></span>';
+                    if (i > 0 && i % 2 != 0) {
+                        items = items + '<br />';
+                    }
+                }
+                return items;
+            }
+        },
+        /** 添加参数 */
+        addParameter() {
+            this.paramData = {
+                index: -1,
+                parameter: {},
+            };
+        },
+        /** 编辑参数*/
+        editParameter(data, index) {
+            this.paramData = null;
+            this.paramData = {
+                index: index,
+                parameter: data,
+            };
+        },
+        /** 删除动作 */
+        removeParameter(index) {
+            this.form.specs.params.splice(index, 1);
+        },
+        /**获取设置的参数对象*/
+        getParamData(data) {
+            if (data.index == -1) {
+                this.form.specs.params.push(data.parameter);
+            } else {
+                this.form.specs.params[data.index] = data.parameter;
+                // 解决数组在界面中不更新问题
+                this.$set(this.form.specs.params, data.index, this.form.specs.params[data.index]);
+            }
+        },
+    },
+};
+</script>

+ 670 - 0
src/views/pms/video_center/product/template/parameter.vue

@@ -0,0 +1,670 @@
+<template>
+  <div style="padding: 6px">
+    <!-- 添加或修改通用物模型对话框 -->
+    <el-dialog title="编辑参数" v-model="openEdit" append-to-body width="900px">
+      <div style="margin: -30px 0 30px; background-color: #ddd; height: 1px"></div>
+      <el-row>
+        <el-col
+          :span="12"
+          style="border: 1px solid #ddd; border-radius: 5px; padding: 10px; background-color: #eee"
+        >
+          <el-form :inline="true" :model="queryParams" label-width="48px" size="small">
+            <el-form-item label="" prop="templateName">
+              <el-input
+                v-model="queryParams.templateName"
+                placeholder="请输入物模型名称"
+                clearable
+                size="small"
+                style="width: 160px"
+                @keyup.enter="handleQuery"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button
+                :icon="Search"
+                size="small"
+                style="padding: 5px"
+                type="info"
+                @click="handleQuery"
+                >搜索</el-button
+              >
+            </el-form-item>
+            <el-form-item>
+              <el-link
+                :underline="false"
+                :icon="InfoFilled"
+                style="margin-left: 20px"
+                type="primary"
+                >单击应用模板</el-link
+              >
+            </el-form-item>
+          </el-form>
+
+          <el-table
+            :border="false"
+            v-loading="loading"
+            :data="templateList"
+            :row-style="{ backgroundColor: '#eee' }"
+            :show-header="false"
+            highlight-current-row
+            size="small"
+            @row-click="rowClick"
+          >
+            <el-table-column label="选择" align="center" width="30">
+              <template #default="scope">
+                <input
+                  :checked="scope.row.isSelect"
+                  :disabled="scope.row.datatype == 'array' || scope.row.datatype == 'object'"
+                  name="template"
+                  type="radio"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="名称" align="left" prop="templateName" />
+            <el-table-column label="标识符" align="left" prop="identifier" />
+            <el-table-column label="数据类型" align="center" prop="datatype" width="60">
+              <template #default="scope">
+                <dict-tag :options="dict.type.iot_data_type" :value="scope.row.datatype" />
+              </template>
+            </el-table-column>
+          </el-table>
+
+          <pagination
+            v-show="total > 0"
+            v-model:limit="queryParams.pageSize"
+            v-model:page="queryParams.pageNum"
+            :total="total"
+            layout="prev, pager, next"
+            small
+            style="margin: 0 0 10px; background-color: #eee"
+            @pagination="getList"
+          />
+        </el-col>
+
+        <el-col :offset="1" :span="11">
+          <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+            <el-form-item label="参数名称" prop="name">
+              <el-input
+                v-model="form.name"
+                placeholder="例如:温度"
+                size="small"
+                style="width: 270px"
+              />
+            </el-form-item>
+            <el-form-item label="参数标识" prop="id">
+              <el-input
+                v-model="form.id"
+                placeholder="例如:temperature"
+                size="small"
+                style="width: 270px"
+              />
+            </el-form-item>
+            <el-form-item label="参数排序" prop="order">
+              <el-input-number
+                v-model="form.order"
+                placeholder="请输入排序"
+                controls-position="right"
+                size="small"
+                style="width: 270px"
+                type="number"
+              />
+            </el-form-item>
+
+            <el-form-item label="参数特性" prop="property">
+              <el-checkbox
+                v-model="form.isChart"
+                :false-label="0"
+                label="图表展示"
+                :true-label="1"
+                name="isChart"
+                @change="isChartChange"
+              />
+              <el-checkbox
+                v-model="form.isMonitor"
+                :false-label="0"
+                label="实时监测"
+                :true-label="1"
+                name="isMonitor"
+                @change="isMonitorChange"
+              />
+              <el-checkbox
+                v-model="form.isReadonly"
+                :false-label="0"
+                label="只读数据"
+                :true-label="1"
+                name="isReadonly"
+                @change="isReadonlyChange"
+              />
+              <el-checkbox
+                v-model="form.isHistory"
+                :false-label="0"
+                label="历史存储"
+                :true-label="1"
+                name="isHistory"
+              />
+              <el-checkbox
+                v-model="form.isSharePerm"
+                :false-label="0"
+                label="分享权限"
+                :true-label="1"
+                name="isSharePerm"
+              />
+            </el-form-item>
+
+            <div style="margin-bottom: 20px; background-color: #ddd; height: 1px"></div>
+            <el-form-item label="数据类型" prop="datatype">
+              <el-select
+                v-model="form.datatype"
+                placeholder="请选择数据类型"
+                size="small"
+                style="width: 125px"
+              >
+                <el-option key="integer" label="整数" value="integer" />
+                <el-option key="decimal" label="小数" value="decimal" />
+                <el-option
+                  key="bool"
+                  :disabled="form.isChart == 1"
+                  label="布尔"
+                  value="bool"
+                />
+                <el-option
+                  key="enum"
+                  :disabled="form.isChart == 1"
+                  label="枚举"
+                  value="enum"
+                />
+                <el-option
+                  key="string"
+                  :disabled="form.isChart == 1"
+                  label="字符串"
+                  value="string"
+                />
+              </el-select>
+            </el-form-item>
+            <div v-if="form.datatype == 'integer' || form.datatype == 'decimal'">
+              <el-form-item label="取值范围">
+                <el-row>
+                  <el-col :span="10">
+                    <el-input
+                      v-model="form.specs.min"
+                      placeholder="最小值"
+                      size="small"
+                      type="number"
+                    />
+                  </el-col>
+                  <el-col :span="4" align="center">到</el-col>
+                  <el-col :span="10">
+                    <el-input
+                      v-model="form.specs.max"
+                      placeholder="最大值"
+                      size="small"
+                      type="number"
+                    />
+                  </el-col>
+                </el-row>
+              </el-form-item>
+              <el-form-item label="单位">
+                <el-input
+                  v-model="form.specs.unit"
+                  placeholder="例如:℃"
+                  size="small"
+                  style="width: 308px"
+                />
+              </el-form-item>
+              <el-form-item label="步长">
+                <el-input-number
+                  v-model="form.specs.step"
+                  placeholder="例如:1"
+                  controls-position="right"
+                  size="small"
+                  style="width: 308px"
+                  type="number"
+                />
+              </el-form-item>
+            </div>
+            <div v-if="form.datatype == 'bool'">
+              <el-form-item label="布尔值" prop="">
+                <el-row style="margin-bottom: 10px">
+                  <el-col :span="10">
+                    <el-input
+                      v-model="form.specs.falseText"
+                      placeholder="例如:关闭"
+                      size="small"
+                    />
+                  </el-col>
+                  <el-col :offset="1" :span="10">(0 值对应文本)</el-col>
+                </el-row>
+                <el-row>
+                  <el-col :span="10">
+                    <el-input
+                      v-model="form.specs.trueText"
+                      placeholder="例如:打开"
+                      size="small"
+                    />
+                  </el-col>
+                  <el-col :offset="1" :span="10">(1 值对应文本)</el-col>
+                </el-row>
+              </el-form-item>
+            </div>
+            <div v-if="form.datatype == 'enum'">
+              <el-form-item label="展示方式">
+                <el-select
+                  v-model="form.specs.showWay"
+                  placeholder="请选择展示方式"
+                  style="width: 175px"
+                >
+                  <el-option
+                    key="select"
+                    label="下拉框"
+                    value="select"
+                  />
+                  <el-option
+                    key="button"
+                    label="按钮"
+                    value="button"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="枚举项" prop="">
+                <el-row
+                  v-for="(item, index) in form.specs.enumList"
+                  :key="'enum' + index"
+                  style="margin-bottom: 10px"
+                >
+                  <el-col :span="8">
+                    <el-input
+                      v-model="item.value"
+                      placeholder="例如:0"
+                      size="small"
+                    />
+                  </el-col>
+                  <el-col :offset="1" :span="11">
+                    <el-input
+                      v-model="item.text"
+                      placeholder="例如:中速挡位"
+                      size="small"
+                    />
+                  </el-col>
+                  <el-col v-if="index != 0" :offset="1" :span="3">
+                    <a style="color: #f56c6c" @click="removeEnumItem(index)">删除</a>
+                  </el-col>
+                </el-row>
+                <div>
+                  +
+                  <a style="color: #409eff" @click="addEnumItem()">添加枚举项</a>
+                </div>
+              </el-form-item>
+            </div>
+            <div v-if="form.datatype == 'string'">
+              <el-form-item label="最大长度" prop="">
+                <el-row>
+                  <el-col :span="10">
+                    <el-input
+                      v-model="form.specs.maxLength"
+                      placeholder="例如:1024"
+                      size="small"
+                      type="number"
+                    />
+                  </el-col>
+                </el-row>
+              </el-form-item>
+            </div>
+          </el-form>
+        </el-col>
+      </el-row>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">提交</el-button>
+          <el-button @click="cancel">取消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, watch } from 'vue'
+import { Search, InfoFilled } from '@element-plus/icons-vue'
+import { listTemplate } from '@/api/pms/video/template'
+
+// 组件名称
+defineOptions({
+  name: 'things_parameter'
+})
+
+// 定义 props
+const props = defineProps({
+  data: {
+    type: Object,
+    default: null
+  }
+})
+
+// 定义 emits
+const emit = defineEmits(['dataEvent'])
+
+// 字典数据(需要从合适的位置引入)
+const dict = {
+  type: {
+    iot_things_type: [],
+    iot_data_type: [],
+    iot_yes_no: []
+  }
+}
+
+// refs
+const formRef = ref()
+const productId = ref()
+
+// 数据状态
+const loading = ref(true)
+const total = ref(0)
+const templateList = ref([])
+const openEdit = ref(false)
+const index = ref(-1)
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  name: null,
+  type: null
+})
+
+// 表单参数
+const form = reactive({
+  name: null,
+  id: null,
+  order: 0,
+  datatype: 'integer',
+  isChart: 0,
+  isHistory: 1,
+  isSharePerm: 0,
+  isMonitor: 0,
+  isReadonly: 0,
+  specs: {
+    enumList: [
+      {
+        value: '',
+        text: ''
+      }
+    ],
+    showWay: 'select' // 显示方式select=下拉选择框,button=按钮
+  }
+})
+
+// 表单校验规则
+const rules = {
+  name: [
+    {
+      required: true,
+      message: '参数名称不能为空',
+      trigger: 'blur'
+    }
+  ],
+  id: [
+    {
+      required: true,
+      message: '参数标识符不能为空',
+      trigger: 'blur'
+    }
+  ],
+  order: [
+    {
+      required: true,
+      message: '参数顺序不能为空',
+      trigger: 'blur'
+    }
+  ],
+  datatype: [
+    {
+      required: true,
+      message: '数据类型不能为空',
+      trigger: 'change'
+    }
+  ]
+}
+
+// 监听 data 变化
+watch(
+  () => props.data,
+  (newVal, oldVal) => {
+    index.value = newVal.index
+    if (newVal && newVal.parameter.name && newVal.parameter.name != '') {
+      form.name = newVal.parameter.name
+      form.id = newVal.parameter.id
+      form.order = newVal.parameter.order
+      form.isChart = newVal.parameter.isChart ? newVal.parameter.isChart : 0
+      form.isHistory = newVal.parameter.isHistory ? newVal.parameter.isHistory : 1
+      form.isSharePerm = newVal.parameter.isSharePerm ? newVal.parameter.isSharePerm : 0
+      form.isMonitor = newVal.parameter.isMonitor ? newVal.parameter.isMonitor : 0
+      form.isReadonly = newVal.parameter.isReadonly ? newVal.parameter.isReadonly : 0
+      form.specs = newVal.parameter.datatype
+      form.datatype = form.specs.type
+      if (!form.specs.enumList) {
+        form.specs.enumList = [
+          {
+            value: '',
+            text: ''
+          }
+        ]
+      }
+      if (!form.specs.arrayType) {
+        form.specs.arrayType = 'integer'
+      }
+    }
+    openEdit.value = true
+    getList()
+  }
+)
+
+/** 查询通用物模型列表 */
+const getList = () => {
+  loading.value = true
+  listTemplate(queryParams).then((response) => {
+    console.log('模板列表>>>>>>>>>>>>>>>>>>', response)
+    //模板列表初始化isSelect值,用于单选
+    for (let i = 0; i < response.list.length; i++) {
+      response.list[i].isSelect = false
+    }
+    templateList.value = response.list
+    total.value = response.total
+    setRadioSelected(productId.value)
+    loading.value = false
+  })
+}
+
+/** 单选数据 */
+const rowClick = (item) => {
+  if (item != null && item.datatype != 'array' && item.datatype != 'object') {
+    form.name = item.templateName
+    form.id = item.identifier
+    form.order = item.modelOrder
+    form.isChart = item.isChart ? item.isChart : 0
+    form.isHistory = item.isHistory ? item.isHistory : 1
+    form.isSharePerm = item.isSharePerm ? item.isSharePerm : 0
+    form.isReadonly = item.isReadonly ? item.isReadonly : 0
+    form.isMonitor = item.isMonitor ? item.isMonitor : 0
+    form.datatype = item.datatype
+    // Json转对象
+    form.specs = JSON.parse(item.specs)
+    if (!form.specs.enumList) {
+      form.specs.enumList = [
+        {
+          value: '',
+          text: ''
+        }
+      ]
+    }
+    if (!form.specs.arrayType) {
+      form.specs.arrayType = 'integer'
+    }
+    setRadioSelected(item.templateId)
+  }
+}
+
+/** 设置单选按钮选中 */
+const setRadioSelected = (templateId) => {
+  for (let i = 0; i < templateList.value.length; i++) {
+    if (templateList.value[i].templateId == templateId) {
+      templateList.value[i].isSelect = true
+    } else {
+      templateList.value[i].isSelect = false
+    }
+  }
+}
+
+// 取消按钮
+const cancel = () => {
+  openEdit.value = false
+  reset()
+}
+
+// 表单重置
+const reset = () => {
+  index.value = -1
+  Object.assign(form, {
+    name: null,
+    id: null,
+    order: 0,
+    datatype: 'integer',
+    isChart: 0,
+    isHistory: 1,
+    isSharePerm: 0,
+    isMonitor: 0,
+    isReadonly: 0,
+    specs: {
+      enumList: [
+        {
+          value: '',
+          text: ''
+        }
+      ],
+      showWay: 'select' // 显示方式select=下拉选择框,button=按钮
+    }
+  })
+  
+  if (formRef.value) {
+    formRef.value.resetFields()
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      // 格式化datatype
+      const datatype = formatThingsSpecs()
+      // 清空不需要存储数据
+      delete form.specs
+      openEdit.value = false
+      // 返回参数对象
+      let data = {
+        parameter: JSON.parse(JSON.stringify({ ...form, datatype })),
+        index: index.value
+      }
+      console.log('data', data)
+      emit('dataEvent', data)
+      reset()
+    }
+  })
+}
+
+// 是否图表展示改变
+const isChartChange = () => {
+  if (form.isChart == 1) {
+    form.isReadonly = 1
+  } else {
+    form.isMonitor = 0
+  }
+}
+
+// 是否实时监测改变
+const isMonitorChange = () => {
+  if (form.isMonitor == 1) {
+    form.isReadonly = 1
+    form.isChart = 1
+  }
+}
+
+// 是否只读数据改变
+const isReadonlyChange = () => {
+  if (form.isReadonly == 0) {
+    form.isMonitor = 0
+    form.isChart = 0
+  }
+}
+
+// 格式化物模型
+const formatThingsSpecs = () => {
+  var data = {}
+  data.type = form.datatype
+  if (form.datatype == 'integer' || form.datatype == 'decimal') {
+    data.min = Number(form.specs.min ? form.specs.min : 0)
+    data.max = Number(form.specs.max ? form.specs.max : 100)
+    data.unit = form.specs.unit ? form.specs.unit : ''
+    data.step = Number(form.specs.step ? form.specs.step : 1)
+  } else if (form.datatype == 'string') {
+    data.maxLength = Number(form.specs.maxLength ? form.specs.maxLength : 1024)
+  } else if (form.datatype == 'bool') {
+    data.falseText = form.specs.falseText
+      ? form.specs.falseText
+      : '关闭'
+    data.trueText = form.specs.trueText
+      ? form.specs.trueText
+      : '开启'
+  } else if (form.datatype == 'array') {
+    data.arrayType = form.specs.arrayType
+  } else if (form.datatype == 'enum') {
+    data.showWay = form.specs.showWay
+    if (form.specs.enumList && form.specs.enumList[0].text != '') {
+      data.enumList = form.specs.enumList
+    } else {
+      data.showWay = 'select'
+      data.enumList = [
+        {
+          value: '0',
+          text: '低'
+        },
+        {
+          value: '1',
+          text: '高'
+        }
+      ]
+    }
+  }
+  return data
+}
+
+/** 添加枚举项 */
+const addEnumItem = () => {
+  form.specs.enumList.push({
+    value: '',
+    text: ''
+  })
+}
+
+/** 删除枚举项 */
+const removeEnumItem = (index) => {
+  form.specs.enumList.splice(index, 1)
+}
+
+// 组件创建时执行
+onMounted(() => {
+  getList()
+  reset()
+})
+</script>
+
+<style>
+.specsColor {
+  background-color: #fcfcfc;
+}
+</style>

+ 135 - 0
src/views/pms/video_center/product/things-model-list.vue

@@ -0,0 +1,135 @@
+<template>
+    <el-dialog :title="$t('product.thimgs-mopdel-list.738493-0')" :visible.sync="open" width="600px">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="68px">
+                <el-form-item :label="$t('product.thimgs-mopdel-list.738493-1')" prop="productName">
+                    <el-input v-model="queryParams.modelName" :placeholder="$t('product.thimgs-mopdel-list.738493-2')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ $t('product.thimgs-mopdel-list.738493-3') }}</el-button>
+                    <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ $t('product.thimgs-mopdel-list.738493-4') }}</el-button>
+                </el-form-item>
+            </el-form>
+
+            <el-table :border="false" v-loading="loading" :data="modelList" @selection-change="handleSelectionChange" highlight-current-row height="50vh" ref="thingsModelTable" size="mini">
+                <el-table-column type="selection" width="55" align="center" :selectable="selectable" />
+                <el-table-column :label="$t('product.thimgs-mopdel-list.738493-5')" align="center" prop="modelName" />
+                <el-table-column :label="$t('product.thimgs-mopdel-list.738493-6')" align="center" prop="identifier" />
+            </el-table>
+
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        </div>
+        <div slot="footer" class="dialog-footer">
+            <el-button @click="confirmSelectProduct" type="primary">{{ $t('product.thimgs-mopdel-list.738493-7') }}</el-button>
+            <el-button @click="closeDialog" type="info">{{ $t('product.thimgs-mopdel-list.738493-8') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { getlListModbus } from '@/api/iot/model';
+
+export default {
+    name: 'ThingsModelList',
+    props: {
+        productId: {
+            type: Number,
+            default: 0,
+        },
+        justiceSelect: {
+            type: String,
+            default: 'isSelectData',
+        },
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: false,
+            // 选中数组
+            ids: [],
+            // 非单个禁用
+            single: true,
+            // 非多个禁用
+            multiple: true,
+            // 显示搜索条件
+            showSearch: true,
+            // 总条数
+            total: 0,
+            // 产品物模型表格数据
+            modelList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 20,
+            },
+            // 表单参数
+            form: {},
+            //已经选择的列表
+            selectedList: [],
+        };
+    },
+    methods: {
+        /** 查询产品物模型列表 */
+        getList() {
+            this.loading = true;
+            this.queryParams.productId = this.productId;
+            getlListModbus(this.queryParams).then((response) => {
+                this.modelList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+                this.$nextTick(() => {
+                    this.selectedList.forEach((selected) => {
+                        const findIndex = this.modelList.findIndex((model) => model.identifier == selected.identifier);
+                        if (findIndex != -1) {
+                            const model = this.modelList[findIndex];
+                            model.isSelectData = false;
+                            model.isSelectIo = false;
+                            this.$refs.thingsModelTable.toggleRowSelection(model, true);
+                        }
+                    });
+                });
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.identifier);
+            this.single = selection.length !== 1;
+            this.multiple = !selection.length;
+        },
+
+        /**确定选择产品,产品传递给父组件 */
+        confirmSelectProduct() {
+            this.$emit('productEvent', this.ids);
+            this.open = false;
+        },
+        /**关闭对话框 */
+        closeDialog() {
+            this.open = false;
+        },
+        //是否可选
+        selectable(item) {
+            return item[this.justiceSelect];
+        },
+    },
+};
+</script>

+ 339 - 0
src/views/pms/video_center/protocol/index.vue

@@ -0,0 +1,339 @@
+<template>
+  <div style="padding: 6px">
+    <el-card v-show="showSearch" style="margin-bottom: 6px">
+      <el-form 
+        ref="queryFormRef" 
+        :inline="true" 
+        :model="queryParams" 
+        label-width="68px" 
+        style="margin-bottom: -20px"
+      >
+        <el-form-item label="协议名称" prop="protocolName">
+          <el-input 
+            v-model="queryParams.protocolName" 
+            placeholder="协议名称" 
+            clearable 
+            size="small" 
+            @keyup.enter="handleQuery" 
+          />
+        </el-form-item>
+        <el-form-item label="协议编码" prop="protocolCode">
+          <el-input 
+            v-model="queryParams.protocolCode" 
+            placeholder="请输入协议编码" 
+            clearable 
+            size="small" 
+            @keyup.enter="handleQuery" 
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button :icon="Search" size="small" type="primary" @click="handleQuery">搜索</el-button>
+          <el-button :icon="Refresh" size="small" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+
+    <el-card style="padding-bottom: 80px">
+      <el-table 
+        :border="false" 
+        v-loading="loading" 
+        :data="protocolList" 
+        @selection-change="handleSelectionChange"
+      >
+        <el-table-column label="协议名称" align="left" prop="protocolName" />
+        <el-table-column label="协议编码" align="left" prop="protocolCode" />
+        <el-table-column label="协议摘要" align="left" prop="jarSign" />
+        <el-table-column label="上传地址" align="left" prop="protocolFileUrl" />
+        <el-table-column label="协议类型" align="left" prop="protocolType" />
+      </el-table>
+
+      <div class="flex justify-right mt-5">
+         <el-pagination 
+        v-show="total > 0" 
+        v-model:current-page="queryParams.pageNum"
+        v-model:page-size="queryParams.pageSize"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        :page-sizes="[10, 20, 30, 50]"
+        @size-change="handleSizeChange"
+        @current-change="getList"
+      />
+      </div>
+
+      <!-- 添加或修改协议对话框 -->
+      <el-dialog 
+        :title="title" 
+        v-model="open" 
+        append-to-body 
+        width="600px"
+      >
+        <el-form 
+          ref="formRef" 
+          :model="form" 
+          :rules="rules" 
+          label-width="100px"
+        >
+          <el-form-item label="协议名称" prop="protocolName">
+            <el-input v-model="form.protocolName" placeholder="请输入协议名称" />
+          </el-form-item>
+          <el-form-item label="协议编码" prop="protocolCode">
+            <el-input v-model="form.protocolCode" placeholder="请输入协议编码" />
+          </el-form-item>
+          <el-form-item label="上传地址" prop="protocolFileUrl">
+            <el-input v-model="form.protocolFileUrl" placeholder="请输入内容" type="textarea" />
+          </el-form-item>
+          <el-form-item label="协议摘要" prop="jarSign">
+            <el-input v-model="form.jarSign" placeholder="请输入协议文件摘要(文件的md5)" />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <div class="dialog-footer">
+            <el-button type="primary" @click="submitForm">确 定</el-button>
+            <el-button @click="cancel">取 消</el-button>
+          </div>
+        </template>
+      </el-dialog>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import { 
+  listProtocol, 
+  getProtocol, 
+  delProtocol, 
+  addProtocol, 
+  updateProtocol 
+} from '@/api/pms/video/protocol'
+import { Search, Refresh } from '@element-plus/icons-vue'
+import { ElMessageBox } from 'element-plus'
+import { download } from '@/config/axios/service'
+
+// 定义组件名称
+defineOptions({
+  name: 'Protocol'
+})
+
+
+// Refs
+const queryFormRef = ref(null)
+const formRef = ref(null)
+
+// 数据定义
+// 遮罩层
+const loading = ref(true)
+// 选中数组
+const ids = ref([])
+// 非单个禁用
+const single = ref(true)
+// 非多个禁用
+const multiple = ref(true)
+// 显示搜索条件
+const showSearch = ref(true)
+// 总条数
+const total = ref(0)
+// 协议表格数据
+const protocolList = ref([])
+// 弹出层标题
+const title = ref('')
+// 是否显示弹出层
+const open = ref(false)
+
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  protocolCode: null,
+  protocolName: null,
+  protocolFileUrl: null,
+  protocolType: null,
+  jarSign: null,
+  protocolStatus: null,
+})
+
+// 表单参数
+const form = reactive({
+  id: null,
+  protocolCode: null,
+  protocolName: null,
+  protocolFileUrl: null,
+  protocolType: null,
+  jarSign: null,
+  createTime: null,
+  updateTime: null,
+  protocolStatus: 0,
+  delFlag: null,
+})
+
+// 表单校验
+const rules = {
+  protocolCode: [
+    {
+      required: true,
+      message: '协议编码不能为空',
+      trigger: 'blur',
+    },
+  ],
+  protocolName: [
+    {
+      required: true,
+      message: '协议名称不能为空',
+      trigger: 'blur',
+    },
+  ],
+  protocolFileUrl: [
+    {
+      required: true,
+      message: '协议上传地址不能为空',
+      trigger: 'blur',
+    },
+  ],
+  protocolType: [
+    {
+      required: true,
+      message: '协议类型不能为空',
+      trigger: 'change',
+    },
+  ],
+  jarSign: [
+    {
+      required: true,
+      message: '协议摘要不能为空',
+      trigger: 'blur',
+    },
+  ],
+}
+
+// 方法定义
+/** 查询协议列表 */
+const getList = (val) => {
+
+    loading.value = true
+    queryParams.pageNo = val
+    listProtocol(queryParams).then((response) => {
+ 
+    protocolList.value = response.list
+    total.value = response.total
+    loading.value = false
+  })
+}
+
+// 处理分页大小变化
+const handleSizeChange = (val) => {
+  queryParams.pageSize = val
+  getList()
+}
+
+// 取消按钮
+const cancel = () => {
+  open.value = false
+  reset()
+}
+
+// 表单重置
+const reset = () => {
+  Object.assign(form, {
+    id: null,
+    protocolCode: null,
+    protocolName: null,
+    protocolFileUrl: null,
+    protocolType: null,
+    jarSign: null,
+    createTime: null,
+    updateTime: null,
+    protocolStatus: 0,
+    delFlag: null,
+  })
+  formRef.value?.resetFields()
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+// 多选框选中数据
+const handleSelectionChange = (selection) => {
+  ids.value = selection.map((item) => item.id)
+  single.value = selection.length !== 1
+  multiple.value = !selection.length
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset()
+  open.value = true
+  title.value = '添加协议'
+}
+
+/** 修改按钮操作 */
+const handleUpdate = (row) => {
+  reset()
+  const id = row.id || ids.value
+  getProtocol(id).then((response) => {
+    Object.assign(form, response.data)
+    open.value = true
+    title.value = '修改协议'
+  })
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      if (form.id != null) {
+        updateProtocol(form).then((response) => {
+          ElMessage.success('修改成功')
+          open.value = false
+          getList()
+        })
+      } else {
+        addProtocol(form).then((response) => {
+          ElMessage.success('新增成功')
+          open.value = false
+          getList()
+        })
+      }
+    }
+  })
+}
+
+/** 删除按钮操作 */
+const handleDelete = (row) => {
+  const idsVal = row.id || ids.value
+  ElMessageBox
+    .confirm('是否确认删除协议编号为"' + idsVal + '"的数据项?')
+    .then(function () {
+      return delProtocol(idsVal)
+    })
+    .then(() => {
+      getList()
+      ElMessage.success('删除成功')
+    })
+    .catch(() => {})
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  download(
+    'iot/protocol/export',
+    {
+      ...queryParams,
+    },
+    `protocol_${new Date().getTime()}.xlsx`
+  )
+}
+
+// 生命周期钩子
+onMounted(() => {
+  getList()
+})
+</script>

+ 0 - 180
src/views/pms/video_center/shebei.vue

@@ -1,180 +0,0 @@
-<template>
-  <div class="device-manage-page">
-    <el-card class="mb-4" shadow="never">
-      <div class="flex justify-between items-center">
-        
-        <el-button type="primary" @click="openAdd">新增设备</el-button>
-      </div>
-    </el-card>
-    <el-card shadow="never">
-      <el-table :data="deviceList"  style="width: 100%">
-        <el-table-column prop="name" label="设备名称" fixed="left" min-width="120" align="center" />
-        <el-table-column prop="product" label="所属产品" min-width="100" align="center" />
-        <el-table-column prop="type" label="设备类型" min-width="100" align="center" />
-        <el-table-column prop="ip" label="IP/域名" min-width="120" align="center" />
-        <el-table-column prop="port" label="服务端口" min-width="100" align="center" />
-        <el-table-column prop="username" label="用户名" min-width="100" />
-        <el-table-column prop="desc" label="设备描述" min-width="120" align="center" />
-        <el-table-column prop="status" label="设备状态" min-width="90" align="center">
-          <template #default="scope">
-            <el-tag :type="scope.row.status === '在线' ? 'success' : 'info'">{{ scope.row.status }}</el-tag>
-            <el-button @click="checkStatus(scope.row)" type="text">检测</el-button>
-          </template>
-        </el-table-column>
-        <el-table-column prop="dept" label="部门" min-width="100" align="center" />
-        <el-table-column prop="model" label="型号" min-width="100" align="center" />
-        <el-table-column prop="serial" label="序列号" min-width="120" align="center" />
-        <el-table-column prop="firmware" label="固件版本" min-width="100" align="center" />
-        <el-table-column prop="channels" label="通道列表" min-width="120" align="center">
-          <template #default="scope">
-            <el-tag v-for="ch in scope.row.channels" :key="ch" class="mr-1">{{ ch }}</el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" min-width="180" align="center">
-          <template #default="scope">
-            <el-button text type="default" @click="openEdit(scope.row)">编辑</el-button>
-            <el-button text type="danger" @click="removeDevice(scope.row)">删除</el-button>
-            <el-button text type="primary" @click="syncInfo(scope.row)">同步</el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-    </el-card>
-    <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
-      <el-form :model="form" label-width="100px" :rules="rules" ref="formRef">
-        <el-form-item label="设备名称" prop="name">
-          <el-input v-model="form.name" />
-        </el-form-item>
-        <el-form-item label="所属产品" prop="product">
-          <el-input v-model="form.product" />
-        </el-form-item>
-        <el-form-item label="设备类型" prop="type">
-          <el-input v-model="form.type" />
-        </el-form-item>
-        <el-form-item label="IP/域名" prop="ip">
-          <el-input v-model="form.ip" />
-        </el-form-item>
-        <el-form-item label="服务端口" prop="port">
-          <el-input v-model="form.port" />
-        </el-form-item>
-        <el-form-item label="用户名" prop="username">
-          <el-input v-model="form.username" />
-        </el-form-item>
-        <el-form-item label="密码" prop="password">
-          <el-input v-model="form.password" type="password" />
-        </el-form-item>
-        <el-form-item label="设备描述" prop="desc">
-          <el-input v-model="form.desc" />
-        </el-form-item>
-        <el-form-item label="部门" prop="dept">
-          <el-input v-model="form.dept" />
-        </el-form-item>
-        <el-form-item label="型号" prop="model">
-          <el-input v-model="form.model" />
-        </el-form-item>
-        <el-form-item label="序列号" prop="serial">
-          <el-input v-model="form.serial" />
-        </el-form-item>
-        <el-form-item label="固件版本" prop="firmware">
-          <el-input v-model="form.firmware" />
-        </el-form-item>
-        <el-form-item label="通道列表" prop="channels">
-          <el-input v-model="form.channels" placeholder="用逗号分隔" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitForm">保存</el-button>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, reactive } from 'vue'
-import { ElMessage, FormInstance } from 'element-plus'
-
-interface Device {
-  name: string
-  product: string
-  type: string
-  ip: string
-  port: string
-  username: string
-  password: string
-  desc: string
-  status: string
-  dept: string
-  model: string
-  serial: string
-  firmware: string
-  channels: string[]
-}
-
-const deviceList = ref<Device[]>([
-  {
-    name: '设备A', product: '产品1', type: '类型A', ip: '192.168.1.1', port: '8080', username: 'admin', password: '123456', desc: '主控设备', status: '在线', dept: '研发部', model: 'X100', serial: 'SN123456', firmware: 'v1.0.0', channels: ['CH1', 'CH2']
-  },
-  {
-    name: '设备B', product: '产品2', type: '类型B', ip: '192.168.1.2', port: '8081', username: 'user', password: '654321', desc: '备份设备', status: '离线', dept: '运维部', model: 'Y200', serial: 'SN654321', firmware: 'v2.1.0', channels: ['CH1']
-  }
-])
-
-const dialogVisible = ref(false)
-const dialogTitle = ref('')
-const form = reactive<Device>({
-  name: '', product: '', type: '', ip: '', port: '', username: '', password: '', desc: '', status: '离线', dept: '', model: '', serial: '', firmware: '', channels: []
-})
-const formRef = ref<FormInstance>()
-const rules = {
-  name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
-  ip: [{ required: true, message: '请输入IP/域名', trigger: 'blur' }],
-  port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
-}
-
-function openAdd() {
-  dialogTitle.value = '新增设备'
-  Object.assign(form, { name: '', product: '', type: '', ip: '', port: '', username: '', password: '', desc: '', status: '离线', dept: '', model: '', serial: '', firmware: '', channels: [] })
-  dialogVisible.value = true
-}
-function openEdit(row: Device) {
-  dialogTitle.value = '编辑设备'
-  Object.assign(form, row, { channels: [...row.channels] })
-  dialogVisible.value = true
-}
-function removeDevice(row: Device) {
-  deviceList.value = deviceList.value.filter(d => d !== row)
-  ElMessage.success('删除成功')
-}
-function submitForm() {
-  formRef.value?.validate((valid) => {
-    if (!valid) return
-    if (dialogTitle.value === '新增设备') {
-      deviceList.value.push({ ...form, channels: form.channels ? form.channels.split ? form.channels.split(',').map((c:string)=>c.trim()) : form.channels : [] })
-      ElMessage.success('添加成功')
-    } else {
-      // 编辑
-      const idx = deviceList.value.findIndex(d => d.serial === form.serial)
-      if (idx !== -1) deviceList.value[idx] = { ...form, channels: form.channels ? form.channels.split ? form.channels.split(',').map((c:string)=>c.trim()) : form.channels : [] }
-      ElMessage.success('修改成功')
-    }
-    dialogVisible.value = false
-  })
-}
-function checkStatus(row: Device) {
-  // 模拟检测
-  row.status = Math.random() > 0.5 ? '在线' : '离线'
-  ElMessage.info('已检测设备状态')
-}
-function syncInfo(row: Device) {
-  // 模拟同步
-  ElMessage.success('信息同步成功')
-}
-</script>
-
-<style scoped>
-.device-manage-page {
-  padding: 20px;
-}
-.mb-4 { margin-bottom: 16px; }
-</style>

+ 269 - 179
src/views/pms/video_center/sip/channel.vue

@@ -2,205 +2,295 @@
     <div style="padding-left: 20px">
         <el-row :gutter="10" class="mb8">
             <el-col :span="1.5">
-                <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getList">{{ $t('refresh') }}</el-button>
+                <el-button type="warning" plain :icon="Refresh" size="small" @click="getList">{{ t('refresh') }}</el-button>
             </el-col>
         </el-row>
-        <el-table :border="false" v-loading="loading" :data="channelList" size="mini">
-            <el-table-column :label="$t('sip.channel.998532-0')" align="center" prop="deviceSipId" />
-            <el-table-column :label="$t('sip.channel.998532-1')" align="center" prop="channelSipId" />
-            <el-table-column :label="$t('sip.channel.998532-2')" min-width="120">
-                <template v-slot:default="scope">
-                    <el-image v-if="isVideoChannel(scope.row)" :src="getSnap(scope.row)" :preview-src-list="getBigSnap(scope.row)" :fit="'contain'" style="width: 60px">
-                        <div slot="error" class="image-slot">
-                            <i class="el-icon-picture-outline"></i>
-                        </div>
+        <el-table v-loading="loading" :data="channelList" size="small">
+            <el-table-column :label="t('sip.channel.998532-0')" align="center" prop="deviceSipId" />
+            <el-table-column :label="t('sip.channel.998532-1')" align="center" prop="channelSipId" />
+            <el-table-column :label="t('sip.channel.998532-2')" min-width="120">
+                <template #default="scope">
+                    <el-image v-if="isVideoChannel(scope.row)" :src="getSnap(scope.row)" :preview-src-list="getBigSnap(scope.row)" fit="contain" style="width: 60px">
+                        <template #error>
+                            <div class="image-slot">
+                                <i class="el-icon-picture-outline"></i>
+                            </div>
+                        </template>
                     </el-image>
                 </template>
             </el-table-column>
-            <el-table-column :label="$t('sip.channel.998532-3')" align="center" prop="channelName" />
-            <el-table-column :label="$t('sip.channel.998532-4')" align="center" prop="model" />
-            <el-table-column :abel="$t('sip.channel.998532-9')" align="center" prop="streamPush">
-                <template slot-scope="scope">
-                    <el-tag type="warning" v-if="scope.row.streamPush === 0">{{ $t('sip.channel.998532-10') }}</el-tag>
-                    <el-tag type="success" v-if="scope.row.streamPush === 1">{{ $t('sip.channel.998532-11') }}</el-tag>
+            <el-table-column :label="t('sip.channel.998532-3')" align="center" prop="channelName" />
+            <el-table-column :label="t('sip.channel.998532-4')" align="center" prop="model" />
+            <el-table-column :label="t('sip.channel.998532-9')" align="center" prop="streamPush">
+                <template #default="scope">
+                    <el-tag type="warning" v-if="scope.row.streamPush === 0">{{ t('sip.channel.998532-10') }}</el-tag>
+                    <el-tag type="success" v-if="scope.row.streamPush === 1">{{ t('sip.channel.998532-11') }}</el-tag>
                 </template>
             </el-table-column>
-            <el-table-column :label="$t('sip.channel.998532-12')" align="center" prop="streamRecord">
-                <template slot-scope="scope">
-                    <el-tag type="warning" v-if="scope.row.streamRecord === 0">{{ $t('sip.channel.998532-13') }}</el-tag>
-                    <el-tag type="success" v-if="scope.row.streamRecord === 1">{{ $t('sip.channel.998532-14') }}</el-tag>
+            <el-table-column :label="t('sip.channel.998532-12')" align="center" prop="streamRecord">
+                <template #default="scope">
+                    <el-tag type="warning" v-if="scope.row.streamRecord === 0">{{ t('sip.channel.998532-13') }}</el-tag>
+                    <el-tag type="success" v-if="scope.row.streamRecord === 1">{{ t('sip.channel.998532-14') }}</el-tag>
                 </template>
             </el-table-column>
-            <el-table-column :label="$t('sip.channel.998532-15')" align="center" prop="videoRecord">
-                <template slot-scope="scope">
-                    <el-tag type="warning" v-if="scope.row.videoRecord === 0">{{ $t('sip.channel.998532-16') }}</el-tag>
-                    <el-tag type="success" v-if="scope.row.videoRecord === 1">{{ $t('sip.channel.998532-17') }}</el-tag>
+            <el-table-column :label="t('sip.channel.998532-15')" align="center" prop="videoRecord">
+                <template #default="scope">
+                    <el-tag type="warning" v-if="scope.row.videoRecord === 0">{{ t('sip.channel.998532-16') }}</el-tag>
+                    <el-tag type="success" v-if="scope.row.videoRecord === 1">{{ t('sip.channel.998532-17') }}</el-tag>
                 </template>
             </el-table-column>
-            <el-table-column :label="$t('sip.channel.998532-5')" align="center" prop="status" width="80">
-                <template slot-scope="scope">
-                    <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" size="mini" />
+            <el-table-column :label="t('sip.channel.998532-5')" align="center" prop="status" width="80">
+                <template #default="scope">
+                    <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" size="small" />
                 </template>
             </el-table-column>
-            <el-table-column :label="$t('opation')" align="center" width="120" class-name="small-padding fixed-width">
-                <template slot-scope="scope">
-                    <el-button size="small" type="success" icon="el-icon-video-play" style="padding: 5px" :disabled="scope.row.status !== 3" @click="sendDevicePush(scope.row)">{{ $t('sip.channel.998532-6') }}</el-button>
+            <el-table-column :label="t('opation')" align="center" width="120" class-name="small-padding fixed-width">
+                <template #default="scope">
+                    <el-button size="small" type="success" :icon="VideoPlay" style="padding: 5px" :disabled="scope.row.status !== 3" @click="sendDevicePush(scope.row)">
+                        {{ t('sip.channel.998532-6') }}
+                    </el-button>
                 </template>
             </el-table-column>
         </el-table>
-        <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
+        <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
     </div>
 </template>
 
-<script>
-import { listChannel, getChannel, delChannel } from '@/api/pms/video/channel';
-export default {
-    name: 'Channel',
-    dicts: ['iot_device_status', 'video_type', 'channel_type'],
-    props: {
-        device: {
-            type: Object,
-            default: null,
-        },
-    },
-    watch: {
-        // 获取到父组件传递的device后
-        device: function (newVal, oldVal) {
-            this.deviceInfo = newVal;
-            if (this.deviceInfo && this.deviceInfo.deviceId != 0) {
-                this.queryParams.deviceSipId = this.deviceInfo.serialNumber;
-            }
-        },
-    },
-    data() {
-        return {
-            loadSnap: {},
-            // 设备信息
-            deviceInfo: {},
-            // 遮罩层
-            loading: true,
-            // 选中数组
-            ids: [],
-            // 非单个禁用
-            single: true,
-            // 非多个禁用
-            multiple: true,
-            // 显示搜索条件
-            showSearch: true,
-            // 总条数
-            total: 0,
-            // 监控设备通道信息表格数据
-            channelList: [],
-            // 弹出层标题
-            title: '',
-            // 是否显示弹出层
-            open: false,
-            // 查询参数
-            queryParams: {
-                pageNum: 1,
-                pageSize: 10,
-                deviceSipId: null,
-            },
-            // 表单参数
-            form: {},
-        };
-    },
-    created() {
-        this.queryParams.deviceSipId = this.device.serialNumber;
-        this.getList();
+<script setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { Refresh, VideoPlay } from '@element-plus/icons-vue'
+import { listChannel, getChannel, delChannel } from '@/api/pms/video/channel'
+
+const { t } = useI18n() // 国际化
+
+// 定义 props
+const props = defineProps({
+    device: {
+        type: Object,
+        default: null,
     },
-    methods: {
-        //通知设备上传媒体流
-        sendDevicePush: function (itemData) {
-            var data = { tabName: 'sipPlayer', channelId: itemData.channelSipId };
-            this.$emit('playerEvent', data);
-            console.log('通知设备推流:' + itemData.deviceSipId + ' : ' + itemData.channelSipId);
-        },
-        /** 查询监控设备通道信息列表 */
-        getList() {
-            this.loading = true;
-            listChannel(this.queryParams).then((response) => {
-                console.log(response);
-                this.channelList = response.rows;
-                this.total = response.total;
-                this.loading = false;
-            });
-        },
-        // 取消按钮
-        cancel() {
-            this.open = false;
-            this.reset();
-        },
-        // 表单重置
-        reset() {
-            this.form = {
-                channelId: null,
-                channelSipId: null,
-                deviceSipId: null,
-                channelName: null,
-                manufacture: null,
-                model: null,
-                owner: null,
-                civilcode: null,
-                block: null,
-                address: null,
-                parentid: null,
-                ipaddress: null,
-                port: null,
-                password: null,
-                ptztype: null,
-                ptztypetext: null,
-                status: 0,
-                longitude: null,
-                latitude: null,
-                streamid: null,
-                subcount: null,
-                parental: 1,
-                hasaudio: 1,
-            };
-            this.resetForm('form');
-        },
-        /** 搜索按钮操作 */
-        handleQuery() {
-            this.queryParams.pageNum = 1;
-            this.getList();
-        },
-        /** 修改按钮操作 */
-        handleUpdate(row) {
-            this.reset();
-            const channelId = row.channelId || this.ids;
-            getChannel(channelId).then((response) => {
-                this.form = response.data;
-                this.open = true;
-                this.title = this.$t('sip.channel.998532-7');
-            });
-        },
-        /** 删除按钮操作 */
-        handleDelete(row) {
-            const channelIds = row.channelId || this.ids;
-            this.$modal
-                .confirm(this.$t('sip.channel.998532-8', [channelIds]))
-                .then(function () {
-                    return delChannel(channelIds);
-                })
-                .then(() => {
-                    this.getList();
-                    this.$modal.msgSuccess(this.$t('sip.channel.998532-18'));
-                })
-                .catch(() => {});
-        },
-        getSnap: function (row) {
-            console.log('getSnap:' + process.env.VUE_APP_BASE_API + '/profile/snap/' + row.deviceSipId + '_' + row.channelSipId + '.jpg');
-            return process.env.VUE_APP_BASE_API + '/profile/snap/' + row.deviceSipId + '_' + row.channelSipId + '.jpg';
-        },
-        getBigSnap: function (row) {
-            return [this.getSnap(row)];
+})
+
+// 定义 emits
+const emit = defineEmits(['playerEvent'])
+
+// 字典数据(需要根据实际项目情况调整)
+const dict = {
+    type: {
+        iot_device_status: [], // 需要根据实际字典数据填充
+    }
+}
+
+// 响应式数据
+const loadSnap = ref({})
+const deviceInfo = ref({})
+const loading = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const showSearch = ref(true)
+const total = ref(0)
+const channelList = ref([])
+const title = ref('')
+const open = ref(false)
+
+// 查询参数
+const queryParams = reactive({
+    pageNum: 1,
+    pageSize: 10,
+    deviceSipId: null,
+})
+
+// 表单参数
+const form = reactive({
+    channelId: null,
+    channelSipId: null,
+    deviceSipId: null,
+    channelName: null,
+    manufacture: null,
+    model: null,
+    owner: null,
+    civilcode: null,
+    block: null,
+    address: null,
+    parentid: null,
+    ipaddress: null,
+    port: null,
+    password: null,
+    ptztype: null,
+    ptztypetext: null,
+    status: 0,
+    longitude: null,
+    latitude: null,
+    streamid: null,
+    subcount: null,
+    parental: 1,
+    hasaudio: 1,
+})
+
+// 监听 device prop 变化
+watch(
+    () => props.device,
+    (newVal, oldVal) => {
+        deviceInfo.value = newVal
+        if (deviceInfo.value && deviceInfo.value.deviceId != 0) {
+            queryParams.deviceSipId = deviceInfo.value.serialNumber
+        }
+    }
+)
+
+// 通知设备上传媒体流
+const sendDevicePush = (itemData) => {
+    var data = { tabName: 'sipPlayer', channelId: itemData.channelSipId }
+    emit('playerEvent', data)
+    console.log('通知设备推流:' + itemData.deviceSipId + ' : ' + itemData.channelSipId)
+}
+
+// 查询监控设备通道信息列表
+const getList = () => {
+    loading.value = true
+    // listChannel(queryParams).then((response) => {
+    //     console.log(response)
+    //     channelList.value = response.rows
+    //     total.value = response.total
+    //     loading.value = false
+    // })
+
+    // 模拟数据
+    
+    channelList.value = [
+        {
+            channelId: 1,
+            deviceSipId: queryParams.deviceSipId,
+            channelSipId: '34020000002000000001',
+            channelName: '前门摄像头',
+            model: 'IPC-1234',
+            streamPush: 1,
+            streamRecord: 1,
+            videoRecord: 0,
+            status: 3,
         },
-        isVideoChannel: function (row) {
-            let channelType = row.channelSipId.substring(10, 13);
-            // 111-DVR编码;112-视频服务器编码;118-网络视频录像机(NVR)编码;131-摄像机编码;132-网络摄像机(IPC)编码
-            return !(channelType !== '111' && channelType !== '112' && channelType !== '118' && channelType !== '131' && channelType !== '132');
+        {
+            channelId: 2,
+            deviceSipId: queryParams.deviceSipId,
+            channelSipId: '34020000002000000002',
+            channelName: '后门摄像头',
+            model: 'IPC-5678',
+            streamPush: 0,
+            streamRecord: 1,
+            videoRecord: 1,
+            status: 2,
         },
-    },
-};
+    ]
+    total.value = channelList.value.length
+    loading.value = false
+
+}
+
+// 取消按钮
+const cancel = () => {
+    open.value = false
+    reset()
+}
+
+// 表单重置
+const reset = () => {
+    Object.assign(form, {
+        channelId: null,
+        channelSipId: null,
+        deviceSipId: null,
+        channelName: null,
+        manufacture: null,
+        model: null,
+        owner: null,
+        civilcode: null,
+        block: null,
+        address: null,
+        parentid: null,
+        ipaddress: null,
+        port: null,
+        password: null,
+        ptztype: null,
+        ptztypetext: null,
+        status: 0,
+        longitude: null,
+        latitude: null,
+        streamid: null,
+        subcount: null,
+        parental: 1,
+        hasaudio: 1,
+    })
+    // 如果有 resetForm 方法需要根据实际情况实现
+}
+
+// 搜索按钮操作
+const handleQuery = () => {
+    queryParams.pageNum = 1
+    getList()
+}
+
+// 修改按钮操作
+const handleUpdate = (row) => {
+    reset()
+    const channelId = row.channelId || ids.value
+    getChannel(channelId).then((response) => {
+        Object.assign(form, response.data)
+        open.value = true
+        title.value = this.t('sip.channel.998532-7')
+    })
+}
+
+// 删除按钮操作
+const handleDelete = (row) => {
+    const channelIds = row.channelId || ids.value
+    // 这里需要根据实际项目的确认框实现方式调整
+    // $modal.confirm(t('sip.channel.998532-8', [channelIds]))
+    //     .then(function() {
+    //         return delChannel(channelIds);
+    //     })
+    //     .then(() => {
+    //         getList();
+    //         // $modal.msgSuccess(t('sip.channel.998532-18'));
+    //     })
+    //     .catch(() => {});
+}
+
+// 获取快照URL
+const getSnap = (row) => {
+    console.log('getSnap:' + import.meta.env.VITE_APP_BASE_API + '/profile/snap/' + row.deviceSipId + '_' + row.channelSipId + '.jpg')
+    return import.meta.env.VITE_APP_BASE_API + '/profile/snap/' + row.deviceSipId + '_' + row.channelSipId + '.jpg'
+}
+
+// 获取大图URL
+const getBigSnap = (row) => {
+    return [getSnap(row)]
+}
+
+// 判断是否为视频通道
+const isVideoChannel = (row) => {
+    let channelType = row.channelSipId.substring(10, 13)
+    // 111-DVR编码;112-视频服务器编码;118-网络视频录像机(NVR)编码;131-摄像机编码;132-网络摄像机(IPC)编码
+    return !(channelType !== '111' && channelType !== '112' && channelType !== '118' && channelType !== '131' && channelType !== '132')
+}
+
+// 生命周期钩子
+onMounted(() => {
+    if (props.device) {
+        queryParams.deviceSipId = props.device.serialNumber
+        getList()
+    }
+})
 </script>
+
+<style scoped>
+.image-slot {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    background: var(--el-fill-color-light);
+    color: var(--el-text-color-secondary);
+    font-size: 30px;
+}
+</style>

+ 234 - 155
src/views/pms/video_center/sip/components/player/DeviceTree.vue

@@ -1,174 +1,253 @@
 <template>
   <div id="DeviceTree" style="width: 100%; height: 100%; background-color: #ffffff; overflow: auto">
-    <div style="line-height: 3vh;margin-bottom: 10px;font-size: 17px;">设备列表</div>
-    <el-tree ref="tree" :props="defaultProps" :current-node-key="selectchannelId" :default-expanded-keys="expandIds"
-      :highlight-current="true" @node-click="handleNodeClick" :load="loadNode" lazy node-key="id"
-      style="min-width: 100%; display: inline-block !important">
-      <span class="custom-tree-node" slot-scope="{ node, data }" style="width: 100%">
-        <span v-if="node.data.type === 0 && node.data.online" title="在线设备"
-          class="device-online iconfont icon-jiedianleizhukongzhongxin2"></span>
-        <span v-if="node.data.type === 0 && !node.data.online" title="离线设备"
-          class="device-offline iconfont icon-jiedianleizhukongzhongxin2"></span>
-        <span v-if="node.data.type === 3 && node.data.online" title="在线通道"
-          class="device-online iconfont icon-shebeileijiankongdian"></span>
-        <span v-if="node.data.type === 3 && !node.data.online" title="离线通道"
-          class="device-offline iconfont icon-shebeileijiankongdian"></span>
-        <span v-if="node.data.type === 4 && node.data.online" title="在线通道-球机"
-          class="device-online iconfont icon-shebeileiqiuji"></span>
-        <span v-if="node.data.type === 4 && !node.data.online" title="离线通道-球机"
-          class="device-offline iconfont icon-shebeileiqiuji"></span>
-        <span v-if="node.data.type === 5 && node.data.online" title="在线通道-半球"
-          class="device-online iconfont icon-shebeileibanqiu"></span>
-        <span v-if="node.data.type === 5 && !node.data.online" title="离线通道-半球"
-          class="device-offline iconfont icon-shebeileibanqiu"></span>
-        <span v-if="node.data.type === 6 && node.data.online" title="在线通道-枪机"
-          class="device-online iconfont icon-shebeileiqiangjitongdao"></span>
-        <span v-if="node.data.type === 6 && !node.data.online" title="离线通道-枪机"
-          class="device-offline iconfont icon-shebeileiqiangjitongdao"></span>
-        <span v-if="node.data.online" style="padding-left: 1px" class="device-online">{{ node.label }}</span>
-        <span v-if="!node.data.online" style="padding-left: 1px" class="device-offline">{{ node.label }}</span>
-      </span>
+    <div style="line-height: 3vh; margin-bottom: 10px; font-size: 17px;" class="pl-5 mt-5">设备列表</div>
+    <el-tree 
+      ref="treeRef" 
+      :props="defaultProps" 
+      :current-node-key="selectchannelId" 
+      :default-expanded-keys="expandIds"
+      :highlight-current="true" 
+      @node-click="handleNodeClick" 
+      :load="loadNode" 
+      lazy 
+      node-key="id"
+      style="min-width: 100%; display: inline-block !important"
+    >
+      <template #default="{ node, data }">
+        <span class="custom-tree-node" style="width: 100%">
+          <span 
+            v-if="data.type === 0 && data.online" 
+            title="在线设备"
+            class="device-online iconfont icon-jiedianleizhukongzhongxin2"
+          ></span>
+          <span 
+            v-if="data.type === 0 && !data.online" 
+            title="离线设备"
+            class="device-offline iconfont icon-jiedianleizhukongzhongxin2"
+          ></span>
+          <span 
+            v-if="data.type === 3 && data.online" 
+            title="在线通道"
+            class="device-online iconfont icon-shebeileijiankongdian"
+          ></span>
+          <span 
+            v-if="data.type === 3 && !data.online" 
+            title="离线通道"
+            class="device-offline iconfont icon-shebeileijiankongdian"
+          ></span>
+          <span 
+            v-if="data.type === 4 && data.online" 
+            title="在线通道-球机"
+            class="device-online iconfont icon-shebeileiqiuji"
+          ></span>
+          <span 
+            v-if="data.type === 4 && !data.online" 
+            title="离线通道-球机"
+            class="device-offline iconfont icon-shebeileiqiuji"
+          ></span>
+          <span 
+            v-if="data.type === 5 && data.online" 
+            title="在线通道-半球"
+            class="device-online iconfont icon-shebeileibanqiu"
+          ></span>
+          <span 
+            v-if="data.type === 5 && !data.online" 
+            title="离线通道-半球"
+            class="device-offline iconfont icon-shebeileibanqiu"
+          ></span>
+          <span 
+            v-if="data.type === 6 && data.online" 
+            title="在线通道-枪机"
+            class="device-online iconfont icon-shebeileiqiangjitongdao"
+          ></span>
+          <span 
+            v-if="data.type === 6 && !data.online" 
+            title="离线通道-枪机"
+            class="device-offline iconfont icon-shebeileiqiangjitongdao"
+          ></span>
+          <span v-if="data.online" style="padding-left: 1px" class="device-online">{{ node.label }}</span>
+          <span v-if="!data.online" style="padding-left: 1px" class="device-offline">{{ node.label }}</span>
+        </span>
+      </template>
     </el-tree>
   </div>
 </template>
 
-<script>
-import { listSipDeviceChannel } from '@/api/iot/sipdevice';
-import { listDeviceShort } from '@/api/iot/device';
-
-export default {
-  name: 'DeviceTree',
-  data() {
-    return {
-      // 总条数
-      total: 0,
-      // 监控设备通道信息表格数据
-      channelList: [],
-      DeviceData: [],
-      expandIds: [],
-      selectData: {},
-      selectchannelId: '',
-      defaultProps: {
-        children: 'children',
-        label: 'name',
-        isLeaf: 'isLeaf',
-      },
-      queryParams: {
-        pageNum: 1,
-        pageSize: 100,
-        status: 3,
-        deviceType: 3,
-      },
-    };
-  },
-  props: ['onlyCatalog', 'clickEvent'],
-  mounted() {
-    this.selectchannelId = '';
-    this.expandIds = ['0'];
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { listSipDeviceChannel } from '@/api/pms/video/sipdevice'
+import { listDeviceShort } from '@/api/pms/video/device'
+
+// 使用组合式API
+const { t } = useI18n()
+
+// 定义 props
+const props = defineProps({
+  onlyCatalog: {
+    type: Boolean,
+    default: false
   },
-  methods: {
-    handleNodeClick(data, node, element) {
-      this.selectData = node.data;
-      this.selectchannelId = node.data.value;
-      if (node.level !== 0) {
-        let deviceNode = this.$refs.tree.getNode(data.userData.channelSipId);
-        if (typeof this.clickEvent == 'function' && node.level > 1) {
-          this.clickEvent(deviceNode.data.userData);
+  clickEvent: {
+    type: Function,
+    default: null
+  }
+})
+
+// 定义 emits
+const emit = defineEmits([])
+
+// refs
+const treeRef = ref(null)
+
+// 数据状态
+const total = ref(0)
+const channelList = ref([])
+const DeviceData = ref([])
+const expandIds = ref([])
+const selectData = ref({})
+const selectchannelId = ref('')
+
+// 默认属性
+const defaultProps = reactive({
+  children: 'children',
+  label: 'name',
+  isLeaf: 'isLeaf',
+})
+
+// 查询参数
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 100,
+  status: 3,
+  deviceType: 3,
+})
+
+// 生命周期钩子
+onMounted(() => {
+  selectchannelId.value = ''
+  expandIds.value = ['0']
+})
+
+onUnmounted(() => {
+  // 组件销毁时的操作
+})
+
+// 节点点击事件
+const handleNodeClick = (data, node, element) => {
+  selectData.value = node.data
+  selectchannelId.value = node.data.value
+  
+  if (node.level !== 0) {
+    const deviceNode = treeRef.value.getNode(data.userData.channelSipId)
+    if (typeof props.clickEvent === 'function' && node.level > 1) {
+      props.clickEvent(deviceNode.data.userData)
+    }
+  }
+}
+
+// 加载节点
+const loadNode = (node, resolve) => {
+  if (node.level === 0) {
+    listDeviceShort(queryParams).then((response) => {
+      const data = response.rows
+      if (data.length > 0) {
+        let nodeList = []
+        for (let i = 0; i < data.length; i++) {
+          let node = {
+            name: data[i].deviceName,
+            isLeaf: false,
+            id: data[i].serialNumber,
+            type: 0,
+            online: data[i].status === 3,
+            userData: data[i],
+          }
+          nodeList.push(node)
         }
+        resolve(nodeList)
+      } else {
+        resolve([])
       }
-    },
-    loadNode: function (node, resolve) {
-      if (node.level === 0) {
-        listDeviceShort(this.queryParams).then((response) => {
-          const data = response.rows;
-          if (data.length > 0) {
-            let nodeList = [];
-            for (let i = 0; i < data.length; i++) {
-              let node = {
-                name: data[i].deviceName,
-                isLeaf: false,
-                id: data[i].serialNumber,
-                type: 0,
-                online: data[i].status === 3,
-                userData: data[i],
-              };
-              nodeList.push(node);
-            }
-            resolve(nodeList);
-          } else {
-            resolve([]);
-          }
-        });
+    })
+  } else {
+    let channelArray = []
+    listSipDeviceChannel(node.data.userData.serialNumber).then((res) => {
+      if (res.data != null) {
+        channelArray = channelArray.concat(res.data)
+        channelDataHandler(channelArray, resolve)
       } else {
-        let channelArray = [];
-        listSipDeviceChannel(node.data.userData.serialNumber).then((res) => {
-          if (res.data != null) {
-            channelArray = channelArray.concat(res.data);
-            this.channelDataHandler(channelArray, resolve);
-          } else {
-            resolve([]);
-          }
-        });
+        resolve([])
       }
-    },
-    channelDataHandler: function (data, resolve) {
-      if (data.length > 0) {
-        let nodeList = [];
-        for (let i = 0; i < data.length; i++) {
-          let item = data[i];
-          let channelType = item.id.substring(10, 13);
-          console.log('channelType: ' + channelType);
-          let type = 3;
-          if (item.id.length <= 10) {
-            type = 2;
-          } else {
-            if (item.id.length > 14) {
-              let channelType = item.id.substring(10, 13);
-              // 111-DVR编码;112-视频服务器编码;118-网络视频录像机(NVR)编码;131-摄像机编码;132-网络摄像机(IPC)编码
-              if (channelType !== '111' && channelType !== '112' && channelType !== '118' && channelType !== '131' && channelType !== '132') {
-                type = -1;
-                // 1-球机;2-半球;3-固定枪机;4-遥控枪机
-              } else if (item.basicData.ptztype === 1) {
-                type = 4;
-              } else if (item.basicData.ptztype === 2) {
-                type = 5;
-              } else if (item.basicData.ptztype === 3 || item.basicData.ptztype === 4) {
-                type = 6;
-              }
-            } else {
-              if (item.basicData.subCount > 0 || item.basicData.parental === 1) {
-                type = 2;
-              }
-            }
+    })
+  }
+}
+
+// 通道数据处理
+const channelDataHandler = (data, resolve) => {
+  if (data.length > 0) {
+    let nodeList = []
+    for (let i = 0; i < data.length; i++) {
+      let item = data[i]
+      let channelType = item.id.substring(10, 13)
+      console.log('channelType: ' + channelType)
+      let type = 3
+      if (item.id.length <= 10) {
+        type = 2
+      } else {
+        if (item.id.length > 14) {
+          let channelType = item.id.substring(10, 13)
+          // 111-DVR编码;112-视频服务器编码;118-网络视频录像机(NVR)编码;131-摄像机编码;132-网络摄像机(IPC)编码
+          if (channelType !== '111' && channelType !== '112' && channelType !== '118' && channelType !== '131' && channelType !== '132') {
+            type = -1
+            // 1-球机;2-半球;3-固定枪机;4-遥控枪机
+          } else if (item.basicData.ptztype === 1) {
+            type = 4
+          } else if (item.basicData.ptztype === 2) {
+            type = 5
+          } else if (item.basicData.ptztype === 3 || item.basicData.ptztype === 4) {
+            type = 6
           }
-          let node = {
-            name: item.name || item.id,
-            isLeaf: true,
-            id: item.id,
-            deviceId: item.deviceId,
-            type: type,
-            online: item.status === 3,
-            userData: item.basicData,
-          };
-
-          if (channelType === '111' || channelType === '112' || channelType === '118' || channelType === '131' || channelType === '132') {
-            nodeList.push(node);
+        } else {
+          if (item.basicData.subCount > 0 || item.basicData.parental === 1) {
+            type = 2
           }
         }
-        resolve(nodeList);
-      } else {
-        resolve([]);
       }
-    },
-    reset: function () {
-      this.$forceUpdate();
-    },
-  },
-  destroyed() { },
-};
+      let node = {
+        name: item.name || item.id,
+        isLeaf: true,
+        id: item.id,
+        deviceId: item.deviceId,
+        type: type,
+        online: item.status === 3,
+        userData: item.basicData,
+      }
+
+      if (channelType === '111' || channelType === '112' || channelType === '118' || channelType === '131' || channelType === '132') {
+        nodeList.push(node)
+      }
+    }
+    resolve(nodeList)
+  } else {
+    resolve([])
+  }
+}
+
+// 重置
+const reset = () => {
+  // Vue 3 中不再需要 $forceUpdate
+}
+
+// 暴露方法给父组件
+defineExpose({
+  treeRef,
+  handleNodeClick,
+  loadNode,
+  channelDataHandler,
+  reset
+})
 </script>
 
-<style>
+<style scoped>
 .device-tree-main-box {
   text-align: left;
 }
@@ -180,4 +259,4 @@ export default {
 .device-offline {
   color: #727272;
 }
-</style>
+</style>

Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.