Эх сурвалжийг харах

feat: qhse和视频vue2改造前

yanghao 3 долоо хоног өмнө
parent
commit
329844f82d
44 өөрчлөгдсөн 5877 нэмэгдсэн , 13 устгасан
  1. 13 13
      index.html
  2. 1 0
      package.json
  3. 15 0
      pnpm-lock.yaml
  4. BIN
      public/js/EasyPlayer.swf
  5. 0 0
      public/js/EasyWasmPlayer.js
  6. 114 0
      src/api/pms/video/channel.ts
  7. 333 0
      src/api/pms/video/device.ts
  8. 158 0
      src/api/pms/video/list.ts
  9. 52 0
      src/api/pms/video/mediaServer.ts
  10. 78 0
      src/api/pms/video/product.ts
  11. 34 0
      src/api/pms/video/sipConfig.ts
  12. 186 0
      src/api/pms/video/user.ts
  13. 53 0
      src/api/rq/iotmeasurebook/index.ts
  14. 56 0
      src/router/modules/remaining.ts
  15. 175 0
      src/views/pms/qhse/measure/IotMeasureBookForm.vue
  16. 342 0
      src/views/pms/qhse/measure/index.vue
  17. 245 0
      src/views/pms/video_center/ChannelManage.vue
  18. 351 0
      src/views/pms/video_center/index.vue
  19. 180 0
      src/views/pms/video_center/shebei.vue
  20. 206 0
      src/views/pms/video_center/sip/channel.vue
  21. 183 0
      src/views/pms/video_center/sip/components/player/DeviceTree.vue
  22. 200 0
      src/views/pms/video_center/sip/components/player/deviceLiveStream.vue
  23. 290 0
      src/views/pms/video_center/sip/components/player/deviceVideo.vue
  24. 62 0
      src/views/pms/video_center/sip/components/player/easyplayer.vue
  25. 0 0
      src/views/pms/video_center/sip/components/player/jessibuca-pro-multi.js
  26. 0 0
      src/views/pms/video_center/sip/components/player/jessibuca-pro-talk.js
  27. 280 0
      src/views/pms/video_center/sip/components/player/jessibuca.vue
  28. 351 0
      src/views/pms/video_center/sip/components/player/player.vue
  29. 0 0
      src/views/pms/video_center/sip/components/player/public/jessibuca-pro-multi.js
  30. 0 0
      src/views/pms/video_center/sip/components/player/public/jessibuca-pro-talk.js
  31. 0 0
      src/views/pms/video_center/sip/components/player/public/web-player-pro-multi.js
  32. 0 0
      src/views/pms/video_center/sip/components/player/public/web-player-pro-talk.js
  33. 0 0
      src/views/pms/video_center/sip/components/player/public/web-player-pro.js
  34. 0 0
      src/views/pms/video_center/sip/components/player/web-player-pro-multi.js
  35. 0 0
      src/views/pms/video_center/sip/components/player/web-player-pro-talk.js
  36. 0 0
      src/views/pms/video_center/sip/components/player/web-player-pro.js
  37. 542 0
      src/views/pms/video_center/sip/index.vue
  38. 388 0
      src/views/pms/video_center/sip/mediaServer-edit.vue
  39. 178 0
      src/views/pms/video_center/sip/mediaServer.vue
  40. 141 0
      src/views/pms/video_center/sip/product-list.vue
  41. 159 0
      src/views/pms/video_center/sip/sipconfig.vue
  42. 120 0
      src/views/pms/video_center/sip/sipidGen.vue
  43. 219 0
      src/views/pms/video_center/sip/splitview.vue
  44. 172 0
      src/views/pms/video_center/sip/user-list.vue

+ 13 - 13
index.html

@@ -1,21 +1,21 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
   <head>
-      <script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
-      <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"></script>
-      <script src="https://api.map.baidu.com/api?v=3.0&ak=c0crhdxQ5H7WcqbcazGr7mnHrLa4GmO0"></script>
-      <meta charset="UTF-8" />
+    <script src="https://g.alicdn.com/dingding/dinglogin/0.0.5/ddLogin.js"></script>
+    <script
+      type="text/javascript"
+      src="https://cdn.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"
+    ></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 type="text/javascript" src="/js/EasyWasmPlayer.js"></script>
+    <meta charset="UTF-8" />
     <link rel="icon" href="/favicon.ico" />
     <meta http-equiv="X-UA-Compatible" content="IE=edge" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <meta
-      name="keywords"
-      content="DeepOil"
-    />
-    <meta
-      name="description"
-      content="DeepOil"
-    />
+    <meta name="keywords" content="DeepOil" />
+    <meta name="description" content="DeepOil" />
     <title>%VITE_APP_TITLE%</title>
   </head>
   <body>

+ 1 - 0
package.json

@@ -56,6 +56,7 @@
     "driver.js": "^1.3.1",
     "echarts": "^5.6.0",
     "echarts-wordcloud": "^2.1.0",
+    "element-china-area-data": "^6.1.0",
     "element-plus": "2.9.1",
     "fast-xml-parser": "^4.3.2",
     "highlight.js": "^11.9.0",

+ 15 - 0
pnpm-lock.yaml

@@ -101,6 +101,9 @@ importers:
       echarts-wordcloud:
         specifier: ^2.1.0
         version: 2.1.0(echarts@5.6.0)
+      element-china-area-data:
+        specifier: ^6.1.0
+        version: 6.1.0
       element-plus:
         specifier: 2.9.1
         version: 2.9.1(vue@3.5.12(typescript@5.3.3))
@@ -2611,6 +2614,9 @@ packages:
     resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
     engines: {node: '>= 6'}
 
+  china-division@2.7.0:
+    resolution: {integrity: sha512-4uUPAT+1WfqDh5jytq7omdCmHNk3j+k76zEG/2IqaGcYB90c2SwcixttcypdsZ3T/9tN1TTpBDoeZn+Yw/qBEA==}
+
   chokidar@3.6.0:
     resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
     engines: {node: '>= 8.10.0'}
@@ -3095,6 +3101,9 @@ packages:
   electron-to-chromium@1.5.67:
     resolution: {integrity: sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ==}
 
+  element-china-area-data@6.1.0:
+    resolution: {integrity: sha512-IkpcjwQv2A/2AxFiSoaISZ+oMw1rZCPUSOg5sOCwT5jKc96TaawmKZeY81xfxXsO0QbKxU5LLc6AirhG52hUmg==}
+
   element-plus@2.9.1:
     resolution: {integrity: sha512-9Agqf/jt4Ugk7EZ6C5LME71sgkvauPCsnvJN12Xid2XVobjufxMGpRE4L7pS4luJMOmFAH3J0NgYEGZT5r+NDg==}
     peerDependencies:
@@ -7983,6 +7992,8 @@ snapshots:
       parse5: 7.2.1
       parse5-htmlparser2-tree-adapter: 7.1.0
 
+  china-division@2.7.0: {}
+
   chokidar@3.6.0:
     dependencies:
       anymatch: 3.1.3
@@ -8483,6 +8494,10 @@ snapshots:
 
   electron-to-chromium@1.5.67: {}
 
+  element-china-area-data@6.1.0:
+    dependencies:
+      china-division: 2.7.0
+
   element-plus@2.9.1(vue@3.5.12(typescript@5.3.3)):
     dependencies:
       '@ctrl/tinycolor': 3.6.1

BIN
public/js/EasyPlayer.swf


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
public/js/EasyWasmPlayer.js


+ 114 - 0
src/api/pms/video/channel.ts

@@ -0,0 +1,114 @@
+import request from '@/config/axios'
+
+// 查询监控设备通道信息列表
+export async function listChannel(query) {
+  return await request.get({
+    url: '/sip/channel/list',
+    params: query
+  })
+}
+
+// 查询监控设备通道信息详细
+export function getChannel(channelId) {
+  return request.get({
+    url: '/sip/channel/' + channelId
+  })
+}
+
+// 新增监控设备通道信息
+export function addChannel(createNum, data) {
+  return request.post({
+    url: '/sip/channel/' + createNum,
+
+    data
+  })
+}
+
+// 修改监控设备通道信息
+export function updateChannel(data) {
+  return request.put({
+    url: '/sip/channel',
+
+    data: data
+  })
+}
+
+// 删除监控设备通道信息
+export function delChannel(channelId) {
+  return request.delete({
+    url: '/sip/channel/' + channelId
+  })
+}
+
+// 开始播放
+export function startPlay(deviceId, channelId) {
+  return request.get({
+    url: '/sip/player/play/' + deviceId + '/' + channelId
+  })
+}
+
+// 获取流信息
+export function getStreaminfo(deviceId, channelId) {
+  return request.get({
+    url: '/sip/player/playstream/' + deviceId + '/' + channelId
+  })
+}
+
+export function playback(deviceId, channelId, query) {
+  return request.get({
+    url: '/sip/player/playback/' + deviceId + '/' + channelId,
+
+    params: query
+  })
+}
+
+export function closeStream(deviceId, channelId, streamId) {
+  return request.get({
+    url: '/sip/player/closeStream/' + deviceId + '/' + channelId + '/' + streamId
+  })
+}
+
+export function playbackPause(deviceId, channelId, streamId) {
+  return request.get({
+    url: '/sip/player/playbackPause/' + deviceId + '/' + channelId + '/' + streamId
+  })
+}
+
+export function playbackReplay(deviceId, channelId, streamId) {
+  return request.get({
+    url: '/sip/player/playbackReplay/' + deviceId + '/' + channelId + '/' + streamId
+  })
+}
+
+export function playbackSeek(deviceId, channelId, streamId, query) {
+  return request.get({
+    url: '/sip/player/playbackSeek/' + deviceId + '/' + channelId + '/' + streamId,
+
+    params: query
+  })
+}
+
+export function playbackSpeed(deviceId, channelId, streamId, query) {
+  return request.get({
+    url: '/sip/player/playbackSpeed/' + deviceId + '/' + channelId + '/' + streamId,
+
+    params: query
+  })
+}
+
+// 监控设备绑定设备或场景
+export function binding(data) {
+  return request.post({
+    url: '/iot/relation/addOrUp',
+
+    data
+  })
+}
+// 通过设备或场景查询监控设备通道信息
+export function listRelDeviceOrScene(query) {
+  return request.get({
+    url: '/sip/channel/listRelDeviceOrScene',
+
+    params: query
+  })
+}

+ 333 - 0
src/api/pms/video/device.ts

@@ -0,0 +1,333 @@
+import request from '@/config/axios'
+
+// 查询设备列表
+export function listDevice(query) {
+    return request.get({
+        url: '/iot/device/list',
+       
+        params: query,
+    });
+}
+
+// 查询未授权设备列表
+export function listUnAuthDevice(query) {
+    return request.get({
+        url: '/iot/device/unAuthlist',
+       
+        params: query,
+    });
+}
+
+// 查询分组可添加设备分页列表
+export function listDeviceByGroup(query) {
+    return request.get({
+        url: '/iot/device/listByGroup',
+        
+        params: query,
+    });
+}
+
+// 查询设备简短列表
+export function listDeviceShort(query) {
+    return request.get({
+        url: '/iot/device/shortList',
+       
+        params: query,
+    });
+}
+
+// 查询所有设备简短列表
+export function listAllDeviceShort() {
+    return request.get({
+        url: '/iot/device/all',
+        
+    });
+}
+
+// 查询设备详细
+export function getDevice(deviceId) {
+    return request.get({
+        url: '/iot/device/' + deviceId,
+        
+    });
+}
+
+// 设备数据同步
+export function deviceSynchronization(serialNumber) {
+    return request.get({
+        url: '/iot/device/synchronization/' + serialNumber,
+       
+    });
+}
+
+// 根据设备编号查询设备详细
+export function getDeviceBySerialNumber(serialNumber) {
+    return request.get({
+        url: '/iot/device/getDeviceBySerialNumber/' + serialNumber,
+       
+    });
+}
+
+// 查询设备统计信息
+export function getDeviceStatistic() {
+    return request.get({
+        url: '/iot/device/statistic',
+       
+    });
+}
+
+// 选择分配设备
+export function distributionDevice(deptId, deviceIds) {
+    return request.post({
+        url: '/iot/device/assignment?deptId=' + deptId + '&deviceIds=' + deviceIds,
+       
+    });
+}
+//回收设备
+export function recycleDevice(deviceIds, recoveryDeptId) {
+    return request.post({
+        url: '/iot/device/recovery?deviceIds=' + deviceIds + '&recoveryDeptId=' + recoveryDeptId,
+        
+    });
+}
+//查询设备导入记录
+export function listImportRecord(params) {
+    return request.get({
+        url: '/iot/record/list',
+       
+        params: params,
+    });
+}
+//查询设备回收记录
+export function listRecycleRecord(params) {
+    return request.get({
+        url: '/iot/record/list',
+       
+        params: params,
+    });
+}
+//查询设备分配记录
+export function listAllotRecord(params) {
+    return request.get({
+        url: '/iot/record/list',
+      
+        params: params,
+    });
+}
+
+// 查询设备运行状态详细
+export function getDeviceRunningStatus(params) {
+    return request.get({
+        url: '/iot/device/runningStatus',
+      
+        params: params,
+    });
+}
+
+// 查询设备物模型的值
+export function getDeviceThingsModelValue(deviceId) {
+    return request.get({
+        url: '/iot/device/thingsModelValue/' + deviceId,
+       
+    });
+}
+
+// 新增设备
+export function addDevice(data) {
+    return request.post({
+        url: '/iot/device',
+        
+        data: data,
+    });
+}
+
+// 修改设备
+export function updateDevice(data) {
+    return request.put({
+        url: '/iot/device',
+       
+        data: data,
+    });
+}
+
+// 删除设备
+export function delDevice(deviceId) {
+    return request.delete({
+        url: '/iot/device/' + deviceId,
+       
+    });
+}
+
+// 生成设备编号
+export function generatorDeviceNum(params) {
+    return request.get({
+        url: '/iot/device/generator',
+        
+        params: params,
+    });
+}
+
+export function getGwDevCode(params) {
+    return request.get({
+        url: '/iot/device/gwDevCount',
+        
+        params: params,
+    });
+}
+
+//mqtt连接参数查看
+export function getMqttConnect(params) {
+    return request.get({
+        url: '/iot/device/getMqttConnectData',
+        
+        params: params,
+    });
+}
+
+
+// 查询设备变量概况
+export function listThingsModel(query) {
+    return request.get({
+        url: '/iot/device/listThingsModel',
+       
+        params: query,
+    });
+}
+
+// 查询设备历史轨迹
+export function getdevicehis(params) {
+    return request.get({
+        url: '/iot/device/devicehis',
+       
+        params: params,
+    });
+}
+
+// 测试dtu
+export function getDevDtu(data) {
+    return request.post({
+        url: '/iot/device/getDevDtu',
+       
+        data: data,
+    });
+}
+
+export function getDtuDeviceList(params) {
+    return request.get({
+        url: '/iot/device/getDtuDeviceList',
+       
+        params: params,
+    });
+}
+
+export function addDeviceUsers(data) {
+    return request.post({
+        url: '/iot/device/addDeviceUsers',
+      
+        data: data,
+    });
+}
+
+export function getDtuDeviceGroup() {
+    return request.get({
+        url: '/iot/device/getDtuDeviceGroup',
+       
+    });
+}
+
+export function updateDtuDevice(data) {
+    return request.post({
+        url: '/iot/device/updateDtuDevice',
+      
+        data: data,
+    });
+}
+
+export function deleteDtuDevice(data) {
+    return request.post({
+        url: '/iot/device/deleteDtuDevice',
+      
+        data: data,
+    });
+}
+
+export function getDtuDeviceGroupList(params) {
+    return request.get({
+        url: '/iot/device/getDtuDeviceGroupList',
+        
+        params: params,
+    });
+}
+
+export function addAndUpdateDtuDevGroup(data) {
+    return request.post({
+        url: '/iot/device/addAndUpdateDtuDevGroup',
+       
+        data: data,
+    });
+}
+
+export function deleteDtuDeviceGroup(params) {
+    return request.get({
+        url: '/iot/device/deleteDtuDeviceGroup',
+        
+        params: params,
+    });
+}
+
+export function selectDeviceAisfptList(params) {
+    return request.get({
+        url: '/iot/device/selectDeviceAisfptList',
+       
+        params: params,
+    });
+}
+
+export function addAndUpdateDeviceAisfpt(data) {
+    return request.post({
+        url: '/iot/device/addAndUpdateDeviceAisfpt',
+        
+        data: data,
+    });
+}
+
+export function deleteDeviceAisfpt(params) {
+    return request.get({
+        url: '/iot/device/deleteDeviceAisfpt',
+       
+        params: params,
+    });
+}
+
+export function getDeviceAbnormalList(query) {
+    return request.get({
+        url: `/iot/device/getDeviceAbnormalList`,
+       
+        params: query
+    })
+}
+
+export function updateMqttConnectData(query) {
+    return request.post({
+        url: `/iot/device/updateMqttConnectData`,
+       
+        data: query
+    })
+}
+
+// 查询设备统计信息
+export function getdeviceBoardList() {
+    return request.get({
+        url: '/iot/device/deviceBoardList',
+       
+    });
+}
+
+
+// 根据经纬度来查询地图上的数据
+export function deviceLongitudeAndLatitudeApi() {
+    return request.get({
+        url: '/iot/device/deviceLongitudeAndLatitude',
+       
+    });
+}

+ 158 - 0
src/api/pms/video/list.ts

@@ -0,0 +1,158 @@
+import request from '@/config/axios'
+
+// 查询场景管理列表
+export function getSceneModelList(query) {
+    return request.get({
+        url: '/scene/model/list',
+      
+        params: query,
+    });
+}
+
+// 新增场景管理
+export function addSceneModel(data) {
+    return request.post({
+        url: '/scene/model',
+        
+        data: data,
+    });
+}
+
+// 修改场景管理
+export function updateSceneModel(data) {
+    return request.put({
+        url: '/scene/model',
+       
+        data: data,
+    });
+}
+
+// 删除场景管理
+export function deleteSceneModel(id) {
+    return request.delete({
+        url: '/scene/model/' + id,
+       
+    });
+}
+
+// 获取场景管理详细信息
+export function getSceneModelDetail(id) {
+    return request.get({
+        url: '/scene/model/' + id,
+       
+    });
+}
+
+// 点击查看进入后查询变量列表
+export function getSceneModelDataList(query) {
+    return request.get({
+        url: '/scene/modelData/list',
+       
+        params: query,
+    });
+}
+
+// 查询场景关联设备列表
+export function getSceneModelDeviceList(query) {
+    return request.get({
+        url: '/scene/modelDevice/list',
+       
+        params: query,
+    });
+}
+
+// 新增场景关联设备
+export function addModelDevice(data) {
+    return request.post({
+        url: '/scene/modelDevice',
+       
+        data: data,
+    });
+}
+
+// 删除场景关联设备
+export function deleteModelDevice(id) {
+    return request.delete({
+        url: '/scene/modelDevice/' + id,
+       
+    });
+}
+
+// 编辑场景关联设备
+export function updateModelDevice(data) {
+    return request.put({
+        url: '/scene/modelDevice',
+       
+        data: data,
+    });
+}
+
+// 点击编辑进入后根据类型查询变量列表
+export function getSceneModelDataListByType(query) {
+    return request.get({
+        url: '/scene/modelData/listByType',
+       
+        params: query,
+    });
+}
+
+// 全部启用
+export function enableModelDevice(data) {
+    return request.post({
+        url: '/scene/modelDevice/editEnable',
+      
+        data: data,
+    });
+}
+
+// 启用变量
+export function enableModelData(data) {
+    return request.post({
+        url: '/scene/modelData/editEnable',
+       
+        data: data,
+    });
+}
+
+// 获取场景录入运算变量列表
+export function getSceneModelTagList(query) {
+    return request.get({
+        url: '/scene/modelTag/list',
+       
+        params: query,
+    });
+}
+
+// 新增场景录入运算变量
+export function addSceneModelTag(data) {
+    return request.post({
+        url: '/scene/modelTag',
+       
+        data: data,
+    });
+}
+
+// 编辑场景录入运算变量
+export function updateSceneModelTag(data) {
+    return request.put({
+        url: '/scene/modelTag',
+        
+        data: data,
+    });
+}
+
+// 删除场景录入运算变量
+export function deleteSceneModelTag(id) {
+    return request.delete({
+        url: '/scene/modelTag/' + id,
+       
+    });
+}
+
+// 获取场景录入运算变量详情
+export function getSceneModelTag(id) {
+    return request.get({
+        url: '/scene/modelTag/' + id,
+        
+    });
+}

+ 52 - 0
src/api/pms/video/mediaServer.ts

@@ -0,0 +1,52 @@
+import request from '@/config/axios'
+
+// 查询流媒体服务器配置列表
+export function listmediaServer(query) {
+  return request.get({
+    url: '/sip/mediaserver/list',
+   
+    params: query
+  })
+}
+
+// 查询流媒体服务器配置详细
+export function getmediaServer() {
+  return request.get({
+    url: '/sip/mediaserver/',
+    
+  })
+}
+
+// 新增流媒体服务器配置
+export function addmediaServer(data) {
+  return request.post({
+    url: '/sip/mediaserver',
+    
+    data: data
+  })
+}
+
+// 修改流媒体服务器配置
+export function updatemediaServer(data) {
+  return request.put({
+    url: '/sip/mediaserver',
+    
+    data: data
+  })
+}
+
+// 删除流媒体服务器配置
+export function delmediaServer(id) {
+  return request.delete({
+    url: '/sip/mediaserver/' + id,
+   
+  })
+}
+
+export function checkmediaServer(query) {
+  return request.get({
+    url: '/sip/mediaserver/check' ,
+    
+    params: query
+  })
+}

+ 78 - 0
src/api/pms/video/product.ts

@@ -0,0 +1,78 @@
+import request from '@/config/axios'
+
+// 查询产品列表
+export function listProduct(query) {
+  return request.get({
+    url: '/iot/product/list',
+   
+    params: query
+  })
+}
+
+// 查询产品列表
+export function listShortProduct() {
+  return request.get({
+    url: '/iot/product/shortList',
+    
+  })
+}
+
+// 查询产品详细
+export function getProduct(productId) {
+  return request.get({
+    url: '/iot/product/' + productId,
+    
+  })
+}
+
+// 新增产品
+export function addProduct(data) {
+  return request.post({
+    url: '/iot/product',
+   
+    data: data
+  })
+}
+
+// 修改产品
+export function updateProduct(data) {
+  return request.put({
+    url: '/iot/product',
+   
+    data: data
+  })
+}
+
+// 获取产品下设备的数量
+export function deviceCount(productId) {
+  return request.get({
+    url: '/iot/product/deviceCount/' + productId,
+    
+  })
+}
+
+// 更新产品状态
+export function changeProductStatus(data) {
+  return request.put({
+    url: '/iot/product/status',
+   
+    data:data
+  })
+}
+
+// 删除产品
+export function delProduct(productId) {
+  return request.delete({
+    url: '/iot/product/' + productId,
+    
+  })
+}
+
+// 根据采集点模板id查询所有产品
+export function selectByTempleId(params) {
+  return request.get({
+    url: '/iot/product/queryByTemplateId',
+    
+    params: params
+  })
+}

+ 34 - 0
src/api/pms/video/sipConfig.ts

@@ -0,0 +1,34 @@
+import request from '@/config/axios'
+
+// 查询sip系统配置详细
+export function getSipconfig(productId) {
+  return request.get({
+    url: '/sip/sipconfig/' + productId,
+  
+  })
+}
+
+// 新增sip系统配置
+export function addSipconfig(data) {
+  return request.post({
+    url: '/sip/sipconfig',
+   
+    data: data
+  })
+}
+
+// 修改sip系统配置
+export function updateSipconfig(data) {
+  return request.put({
+    url: '/sip/sipconfig',
+  
+    data: data
+  })
+}
+
+export function delSipconfigByProductId(productId) {
+  return request.delete({
+    url: '/sip/sipconfig/product/' + productId,
+   
+  })
+}

+ 186 - 0
src/api/pms/video/user.ts

@@ -0,0 +1,186 @@
+import request from '@/config/axios'
+function parseStrEmpty(str) {
+  if (!str || str == "undefined" || str == "null") {
+    return "";
+  }
+  return str;
+}
+
+// 查询用户列表
+export function listUser(query) {
+    return request.get({
+        url: '/system/user/list',
+        
+        params: query,
+    });
+}
+
+// 查询终端用户列表
+export function terminalUserList(query) {
+    return request.get({
+        url: '/system/user/listTerminal',
+       
+        params: query,
+    });
+}
+// 查询用户详细
+export function getUser(userId) {
+    return request.get({
+        url: '/system/user/' + parseStrEmpty(userId),
+       
+    });
+}
+// 查询角色列表
+export function getRole(deptId) {
+    return request.get({
+        url: '/system/dept/getRole?deptId=' + deptId,
+       
+    });
+}
+
+// 新增用户
+export function addUser(data) {
+    return request.post({
+        url: '/system/user',
+       
+        data: data,
+    });
+}
+
+// 修改用户
+export function updateUser(data) {
+    return request.put({
+        url: '/system/user',
+       
+        data: data,
+    });
+}
+
+// 删除用户
+export function delUser(userId) {
+    return request.delete({
+        url: '/system/user/' + userId,
+      
+    });
+}
+
+// 用户密码重置
+export function resetUserPwd(userId, password) {
+    const data = {
+        userId,
+        password,
+    };
+    return request.put({
+        url: '/system/user/resetPwd',
+        
+        data: data,
+    });
+}
+
+// 用户状态修改
+export function changeUserStatus(userId, status) {
+    const data = {
+        userId,
+        status,
+    };
+    return request.put({
+        url: '/system/user/changeStatus',
+       
+        data: data,
+    });
+}
+// 获取微信二维码
+export function getLoginParam() {
+    return request.get({
+        url: '/wechat/getWxBindQr',
+       
+    });
+}
+// 解除绑定
+export function secureBind(data) {
+    return request.post({
+        url: '/wechat/cancelBind',
+       
+        data: data,
+    });
+}
+// 查询用户个人信息
+export function getUserProfile() {
+    return request.get({
+        url: '/system/user/profile',
+       
+    });
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+    return request.put({
+        url: '/system/user/profile',
+       
+        data: data,
+    });
+}
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+    const data = {
+        oldPassword,
+        newPassword,
+    };
+    return request.put({
+        url: '/system/user/profile/updatePwd',
+        
+        params: data,
+    });
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+    return request.post({
+        url: '/system/user/profile/avatar',
+       
+        data: data,
+    });
+}
+
+// 查询授权角色
+export function getAuthRole(userId) {
+    return request.get({
+        url: '/system/user/authRole/' + userId,
+        
+    });
+}
+
+// 保存授权角色
+export function updateAuthRole(data) {
+    return request.put({
+        url: '/system/user/authRole',
+      
+        params: data,
+    });
+}
+
+// 查询机构下拉树结构
+export function deptsTreeSelect() {
+    return request.get({
+        url: '/system/user/deptTree',
+       
+    });
+}
+
+// 查询子机构下拉树结构
+export function deptsTreeSelectSub(showOwner) {
+    return request.get({
+        url: '/system/user/deptTree?showOwner=' + showOwner,
+       
+    });
+}
+
+// 查询终端用户列表
+export function getByDeptId(query) {
+  return request.get({
+    url: '/system/user/getByDeptId',
+   
+    params: query,
+  });
+}

+ 53 - 0
src/api/rq/iotmeasurebook/index.ts

@@ -0,0 +1,53 @@
+import request from '@/config/axios'
+
+// 计量器具台账 VO
+export interface IotMeasureBookVO {
+  id: number // 主键id
+  measureCode: string // 计量器具编码
+  measureName: string // 计量器具名称
+  classify: string // 分类
+  dutyPerson: string // 责任人
+  buyDate: string // 采购日期
+  brand: string // 品牌
+  modelName: string // 规格型号
+  validity: Date // 有效期
+  lastTime: string // 上次检验/校准日期
+  measureUnit: string // 单位
+  measurePrice: number // 价格
+  measurePic: string // 图片
+  remark: string // 备注
+  deptId: number // 部门id
+}
+
+// 计量器具台账 API
+export const IotMeasureBookApi = {
+  // 查询计量器具台账分页
+  getIotMeasureBookPage: async (params: any) => {
+    return await request.get({ url: `/rq/iot-measure-book/page`, params })
+  },
+
+  // 查询计量器具台账详情
+  getIotMeasureBook: async (id: number) => {
+    return await request.get({ url: `/rq/iot-measure-book/get?id=` + id })
+  },
+
+  // 新增计量器具台账
+  createIotMeasureBook: async (data: IotMeasureBookVO) => {
+    return await request.post({ url: `/rq/iot-measure-book/create`, data })
+  },
+
+  // 修改计量器具台账
+  updateIotMeasureBook: async (data: IotMeasureBookVO) => {
+    return await request.put({ url: `/rq/iot-measure-book/update`, data })
+  },
+
+  // 删除计量器具台账
+  deleteIotMeasureBook: async (id: number) => {
+    return await request.delete({ url: `/rq/iot-measure-book/delete?id=` + id })
+  },
+
+  // 导出计量器具台账 Excel
+  exportIotMeasureBook: async (params) => {
+    return await request.download({ url: `/rq/iot-measure-book/export-excel`, params })
+  },
+}

+ 56 - 0
src/router/modules/remaining.ts

@@ -2031,6 +2031,62 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
+ 
+  // 视频中心**********************************
+  {
+    path: '/videocenter',
+    component: Layout,
+    name: 'VideoCenter',
+    meta: {
+      hidden: false,
+      keepAlive: true,
+      alwaysShow: true,
+      title: '视频中心',
+      icon: 'ep:camera'
+    },
+    children: [
+      {
+        path: 'productTest',
+        component: () => import('@/views/pms/video_center/sip/product-list.vue'),
+        name: 'VideoCenterProductList',
+        meta: {
+          title: '产品管理测试',
+          hidden: false,
+          icon: 'ep:box'
+        }
+      },
+      {
+        path: 'product',
+        component: () => import('@/views/pms/video_center/index.vue'),
+        name: 'VideoCenterProduct',
+        meta: {
+          title: '产品管理',
+          hidden: false,
+          icon: 'ep:box'
+        }
+      },
+       {
+        path: 'shebei',
+        component: () => import('@/views/pms/video_center/shebei.vue'),
+        name: 'VideoCenterShebei',
+        meta: {
+          title: '设备的管理',
+          hidden: false,
+          icon: 'ep:monitor'
+        }
+      },
+       {
+        path: 'sip',
+        component: () => import('@/views/pms/video_center/ChannelManage.vue'),
+        name: 'VideoCentersip',
+        meta: {
+          title: '通道管理',
+          hidden: false,
+          icon: 'ep:operation'
+        }
+      }
+    ]
+  },
 
 ]
 

+ 175 - 0
src/views/pms/qhse/measure/IotMeasureBookForm.vue

@@ -0,0 +1,175 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      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>
+      <el-form-item label="分类" prop="classify">
+        <el-input v-model="formData.classify" placeholder="请输入分类" />
+      </el-form-item>
+      <el-form-item label="责任人" prop="dutyPerson">
+        <el-input v-model="formData.dutyPerson" placeholder="请输入责任人" />
+      </el-form-item>
+      <el-form-item label="采购日期" prop="buyDate">
+        <el-date-picker
+          v-model="formData.buyDate"
+          type="date"
+          value-format="x"
+          placeholder="选择采购日期"
+        />
+      </el-form-item>
+      <el-form-item label="品牌" prop="brand">
+        <el-input v-model="formData.brand" placeholder="请输入品牌" />
+      </el-form-item>
+      <el-form-item label="规格型号" prop="modelName">
+        <el-input v-model="formData.modelName" placeholder="请输入规格型号" />
+      </el-form-item>
+      <el-form-item label="有效期" prop="validity">
+        <el-date-picker
+          v-model="formData.validity"
+          type="date"
+          value-format="x"
+          placeholder="选择有效期"
+        />
+      </el-form-item>
+      <el-form-item label="上次检验/校准日期" prop="lastTime">
+        <el-date-picker
+          v-model="formData.lastTime"
+          type="date"
+          value-format="x"
+          placeholder="选择上次检验/校准日期"
+        />
+      </el-form-item>
+      <el-form-item label="单位" prop="measureUnit">
+        <el-input v-model="formData.measureUnit" placeholder="请输入单位" />
+      </el-form-item>
+      <el-form-item label="价格" prop="measurePrice">
+        <el-input v-model="formData.measurePrice" placeholder="请输入价格" />
+      </el-form-item>
+      <el-form-item label="图片" prop="measurePic">
+        <el-input v-model="formData.measurePic" placeholder="请输入图片" />
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" placeholder="请输入备注" />
+      </el-form-item>
+      <el-form-item label="部门id" prop="deptId">
+        <el-input v-model="formData.deptId" placeholder="请输入部门id" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { IotMeasureBookApi, IotMeasureBookVO } from '@/api/rq/iotmeasurebook'
+
+/** 计量器具台账 表单 */
+defineOptions({ name: 'IotMeasureBookForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  measureCode: undefined,
+  measureName: undefined,
+  classify: undefined,
+  dutyPerson: undefined,
+  buyDate: undefined,
+  brand: undefined,
+  modelName: undefined,
+  validity: undefined,
+  lastTime: undefined,
+  measureUnit: undefined,
+  measurePrice: undefined,
+  measurePic: undefined,
+  remark: undefined,
+  deptId: undefined,
+})
+const formRules = reactive({
+  measureCode: [{ required: true, message: '计量器具编码不能为空', trigger: 'blur' }],
+  measureName: [{ required: true, message: '计量器具名称不能为空', trigger: 'blur' }],
+  classify: [{ required: true, message: '分类不能为空', trigger: 'blur' }],
+  dutyPerson: [{ required: true, message: '责任人不能为空', trigger: 'blur' }],
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await IotMeasureBookApi.getIotMeasureBook(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as IotMeasureBookVO
+    if (formType.value === 'create') {
+      await IotMeasureBookApi.createIotMeasureBook(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await IotMeasureBookApi.updateIotMeasureBook(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    measureCode: undefined,
+    measureName: undefined,
+    classify: undefined,
+    dutyPerson: undefined,
+    buyDate: undefined,
+    brand: undefined,
+    modelName: undefined,
+    validity: undefined,
+    lastTime: undefined,
+    measureUnit: undefined,
+    measurePrice: undefined,
+    measurePic: undefined,
+    remark: undefined,
+    deptId: undefined,
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 342 - 0
src/views/pms/qhse/measure/index.vue

@@ -0,0 +1,342 @@
+<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-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <IotMeasureBookForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import { IotMeasureBookApi, IotMeasureBookVO } from '@/api/rq/iotmeasurebook'
+import IotMeasureBookForm from './IotMeasureBookForm.vue'
+
+/** 计量器具台账 列表 */
+defineOptions({ name: 'IotMeasureBook' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref<IotMeasureBookVO[]>([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  measureCode: undefined,
+  measureName: undefined,
+  classify: undefined,
+  dutyPerson: undefined,
+  buyDate: [],
+  brand: undefined,
+  modelName: undefined,
+  validity: undefined,
+  validity: [],
+  lastTime: [],
+  measureUnit: undefined,
+  measurePrice: undefined,
+  measurePic: undefined,
+  remark: undefined,
+  createTime: [],
+  deptId: undefined,
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await IotMeasureBookApi.getIotMeasureBookPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await IotMeasureBookApi.deleteIotMeasureBook(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await IotMeasureBookApi.exportIotMeasureBook(queryParams)
+    download.excel(data, '计量器具台账.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

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

@@ -0,0 +1,245 @@
+<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>

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

@@ -0,0 +1,351 @@
+<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>

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

@@ -0,0 +1,180 @@
+<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>

+ 206 - 0
src/views/pms/video_center/sip/channel.vue

@@ -0,0 +1,206 @@
+<template>
+    <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-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-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>
+                </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>
+                </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>
+                </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" />
+                </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>
+                </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 { 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();
+    },
+    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)];
+        },
+        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');
+        },
+    },
+};
+</script>

+ 183 - 0
src/views/pms/video_center/sip/components/player/DeviceTree.vue

@@ -0,0 +1,183 @@
+<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>
+    </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'];
+  },
+  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);
+        }
+      }
+    },
+    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);
+            this.channelDataHandler(channelArray, resolve);
+          } else {
+            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;
+              }
+            }
+          }
+          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([]);
+      }
+    },
+    reset: function () {
+      this.$forceUpdate();
+    },
+  },
+  destroyed() { },
+};
+</script>
+
+<style>
+.device-tree-main-box {
+  text-align: left;
+}
+
+.device-online {
+  color: #252525;
+}
+
+.device-offline {
+  color: #727272;
+}
+</style>

+ 200 - 0
src/views/pms/video_center/sip/components/player/deviceLiveStream.vue

@@ -0,0 +1,200 @@
+<template>
+  <div>
+    <el-row>
+      <span style="margin-left: 10px" prop="channelName">通道名称:</span>
+      <el-select v-model="channelId" placeholder="请选择" @change="changeChannel()" size="small">
+        <el-option v-for="option in channelList" :key="option.value" :label="option.label" :value="option.value"></el-option>
+      </el-select>
+      <span style="margin: 10px 10px 10px 30px">开启拉流:</span>
+      <el-switch v-model="pushStream" active-color="#13ce66" inactive-color="#c4c6c9" style="border-radius: 10px" :disabled="channelId === ''" @change="startPushStream"></el-switch>
+      <span style="margin: 10px 10px 10px 30px">开启直播录像:</span>
+      <el-switch v-model="playrecord" active-color="#13ce66" inactive-color="#c4c6c9" style="border-radius: 10px" :disabled="channelId === ''" @change="startPlayRecord"></el-switch>
+    </el-row>
+    <player ref="player" :playerinfo="playinfo" class="components-container"></player>
+  </div>
+</template>
+<script>
+import player from '@/views/components/player/player.vue';
+import { startPlay, closeStream, listChannel } from '@/api/iot/channel';
+import { startPlayRecord } from '@/api/iot/record';
+
+export default {
+  name: 'device-live-stream',
+  components: {
+    player,
+  },
+  props: {
+    device: {
+      type: Object,
+      default: null,
+    },
+  },
+  watch: {
+    // 获取到父组件传递的device后
+    device: function (newVal, oldVal) {
+      this.deviceInfo = newVal;
+      if (this.deviceInfo.channelId) {
+        this.channelId = this.deviceInfo.channelId;
+        this.changeChannel();
+      }
+      if (this.deviceInfo && this.deviceInfo.deviceId !== 0) {
+        this.queryParams.deviceSipId = this.deviceInfo.serialNumber;
+        this.deviceId = this.device.serialNumber;
+      }
+    },
+  },
+  data() {
+    return {
+      deviceInfo: {},
+      deviceId: '',
+      channelId: '',
+      streamId: '',
+      ssrc: '',
+      playurl: '',
+      playinfo: { playtype: 'play' },
+      playrecord: false,
+      playrecording: false,
+      playing: false,
+      pushStream: false,
+      retrycount: 0,
+      channelList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        deviceSipId: null,
+        channelSipId: null,
+      },
+    };
+  },
+  created() {
+    this.queryParams.deviceSipId = this.device.serialNumber;
+    this.deviceId = this.device.serialNumber;
+    this.getList();
+    this.playinfo = {
+      playtype: 'play',
+      deviceId: this.device.serialNumber,
+    };
+  },
+  beforeDestroy() {
+    console.log("beforeDestroy");
+    this.closeDestroy(false);
+  },
+  methods: {
+    /** 查询监控设备通道信息列表 */
+    getList() {
+      this.loading = true;
+      listChannel(this.queryParams).then((response) => {
+        this.channelList = response.rows.map((item) => {
+          return { value: item.channelSipId, label: item.channelName };
+        });
+        console.log(this.channelList);
+      });
+    },
+    changeChannel() {
+      this.playinfo.channelId = this.channelId;
+      this.startPlayer();
+    },
+    // 直播播放
+    TimeoutCallback() {
+      this.closeDestroy(false);
+      this.retrycount = 0;
+      setTimeout(() => {
+        this.startPlayer();
+      }, 1000);
+    },
+    startPushStream() {
+      if (!this.channelId) {
+        console.log('开始通道: [' + this.channelId + ']');
+        return;
+      }
+      console.log('推流状态: [' + this.pushStream + ']');
+      if (this.pushStream) {
+        this.startPlayer();
+      } else {
+        this.closeDestroy(true);
+      }
+    },
+    startPlayRecord() {
+      console.log('录像状态: [' + this.playrecord + ']');
+      this.closeDestroy(true);
+      setTimeout(() => {
+        this.startPlayer();
+      }, 500);
+    },
+    // 开启直播播放器
+    startPlayer() {
+      if (!this.channelId) {
+        console.log('直播录像通道: [' + this.channelId + ']');
+        return;
+      }
+      this.deviceId = this.queryParams.deviceSipId;
+      if (this.playing) {
+        this.closeDestroy(false);
+      }
+      this.$refs.player.registercallback('loadingTimeout', this.TimeoutCallback);
+      this.$refs.player.registercallback('delayTimeout', this.TimeoutCallback);
+      if (this.playrecord) {
+        startPlayRecord(this.deviceId, this.channelId).then((response) => {
+          console.log('开始录像:' + this.deviceId + ' : ' + this.channelId);
+          const res = response.data;
+          this.streamId = res.streamId;
+          this.playurl = res.playurl;
+          this.$refs.player.play(this.playurl);
+          this.playing = true;
+          this.playrecording = true;
+          this.pushStream = true;
+        });
+      } else {
+        startPlay(this.deviceId, this.channelId).then((response) => {
+          console.log('开始推流: [' + this.channelId + ']');
+          const res = response.data;
+          this.streamId = res.streamId;
+          this.playurl = res.playurl;
+          this.$refs.player.play(this.playurl);
+          this.playing = true;
+          this.playrecording = false;
+          this.pushStream = true;
+        });
+      }
+    },
+    closeStream(force) {
+      if (force) {
+        if (this.playing && this.streamId) {
+          console.log('强制关闭推流: [' + this.streamId + ']');
+          closeStream(this.deviceId, this.channelId, this.streamId).then((res) => {
+            this.streamId = '';
+            this.ssrc = '';
+            this.playurl = '';
+            this.pushStream = false;
+          });
+          this.playing = false;
+          this.playrecording = false;
+        }
+      } else {
+        if (this.playrecording === true) {
+          return;
+        }
+        if (this.playing && this.streamId) {
+          console.log('关闭推流: [' + this.streamId + ']');
+          closeStream(this.deviceId, this.channelId, this.streamId).then((res) => {
+            this.streamId = '';
+            this.ssrc = '';
+            this.playurl = '';
+            this.pushStream = false;
+          });
+          this.playing = false;
+          this.playrecording = false;
+        }
+      }
+    },
+    closeDestroy(force) {
+      this.closeStream(force);
+      this.$refs.player.destroy();
+    },
+    destroy() {
+      this.$refs.player.destroy();
+    },
+  },
+};
+</script>

+ 290 - 0
src/views/pms/video_center/sip/components/player/deviceVideo.vue

@@ -0,0 +1,290 @@
+<template>
+    <div style="display: block; width: 1000px">
+        <div style="display: flex">
+            <el-row>
+                <span style="margin-left: 10px" prop="channelName">通道:</span>
+                <el-select v-model="channelId" placeholder="请选择" @change="changeChannel" style="width: 200px; margin-right: 10px" size="small">
+                    <el-option v-for="option in channelList" :key="option.value" :label="option.label" :value="option.value"></el-option>
+                </el-select>
+                <span style="overflow: auto; margin-left: 10px">日期:</span>
+                <el-date-picker v-model="queryDate" type="date" size="small" value-format="yyyy-MM-dd" clearable placeholder="选择日期" style="width: 180px; margin-right: 10px" />
+                <el-button-group style="margin: 0">
+                    <el-button size="mini" type="success" title="查看录像" @click="loadDevRecord()" :disabled="channelId === '' || !queryDate">
+                        <i class="el-icon-video-camera" />
+                        查看
+                    </el-button>
+                </el-button-group>
+
+                <span style="margin-left: 82px; overflow: auto">时间:</span>
+                <el-button-group>
+                    <el-time-picker
+                        size="small"
+                        is-range
+                        align="left"
+                        v-model="timeRange"
+                        value-format="yyyy-MM-dd HH:mm:ss"
+                        range-separator="至"
+                        start-placeholder="开始时间"
+                        end-placeholder="结束时间"
+                        placeholder="选择时间范围"
+                        @change="timePickerChange"
+                        style="width: 200px"
+                        :disabled="channelId === '' || !queryDate"
+                    ></el-time-picker>
+                </el-button-group>
+                <el-button-group style="margin: 0 0 0 10px">
+                    <el-button size="mini" type="primary" title="下载选定录像" @click="downloadRecord()" :disabled="channelId === '' || !timeRange">
+                        <i class="el-icon-download" />
+                        转存
+                    </el-button>
+                </el-button-group>
+            </el-row>
+        </div>
+        <player ref="playbacker" :playerinfo="playinfo" class="components-container"></player>
+    </div>
+</template>
+<script>
+import player from './player.vue';
+import { listChannel, playback, closeStream, playbackSeek } from '@/api/pms/video/channel';
+import { getDevRecord, startDownloadRecord } from '@/api/iot/record';
+
+export default {
+    name: 'DeviceVideo',
+    components: {
+        player,
+    },
+    data() {
+        return {
+            deviceId: '',
+            channelId: '',
+            streamId: '',
+            ssrc: '',
+            playurl: '',
+            queryDate: '',
+            playing: false,
+            vodData: {},
+            hisData: [],
+            playinfo: {},
+            channelList: [],
+            playbackinfo: {},
+            timeRange: null,
+            startTime: null,
+            endTime: null,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                deviceSipId: null,
+                channelSipId: null,
+            },
+        };
+    },
+    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;
+                this.deviceId = this.device.serialNumber;
+            }
+        },
+    },
+    created() {
+        this.queryParams.deviceSipId = this.device.serialNumber;
+        this.deviceId = this.device.serialNumber;
+        this.getList();
+        this.playinfo = {
+            playtype: 'playback',
+            deviceId: this.device.serialNumber,
+        };
+    },
+    beforeDestroy() {
+      this.closeStream();
+    },
+    methods: {
+        /** 查询监控设备通道信息列表 */
+        getList() {
+            this.loading = true;
+            listChannel(this.queryParams).then((response) => {
+                this.channelList = response.rows.map((item) => {
+                    return { value: item.channelSipId, label: item.channelName };
+                });
+            });
+        },
+        // 改变通道
+        changeChannel() {
+            this.playinfo.channelId = this.channelId;
+        },
+        initUrl(data) {
+            if (data) {
+                this.streamId = data.ssrc;
+                this.ssrc = data.ssrc;
+                this.playurl = data.playurl;
+            } else {
+                this.streamId = '';
+                this.ssrc = '';
+                this.playurl = '';
+            }
+        },
+        getBeijingTime(queryDate) {
+            // 计算与UTC的时区差,对于北京时间来说是8小时
+            let offset = 8 * 60 * 60 * 1000;
+            // 加上时区差,得到北京时间
+            let beijingTime = new Date(new Date(queryDate).getTime() - offset);
+            return beijingTime.getTime();
+        },
+        // 录像控制
+        loadDevRecord() {
+            this.$refs.playbacker.registercallback('playbackSeek', this.seekPlay);
+            if (this.queryDate === '' || this.queryDate === null) {
+                this.$message.error('请选择日期');
+                return;
+            }
+            if (this.deviceId && this.channelId) {
+                const date = this.getBeijingTime(this.queryDate);
+                const start = date / 1000;
+                const end = Math.floor((date + 24 * 60 * 60 * 1000 - 1) / 1000);
+                const query = {
+                    start: start,
+                    end: end,
+                };
+                this.vodData = {
+                    start: start,
+                    end: end,
+                    base: start,
+                };
+                this.setTime(this.queryDate + ' 00:00:00', this.queryDate + ' 23:59:59');
+                getDevRecord(this.deviceId, this.channelId, query).then((res) => {
+                    this.hisData = res.data.recordItems;
+                    if (res.data.recordItems) {
+                        const len = this.hisData.length;
+                        if (len > 0) {
+                            if (this.hisData[0].start < start) {
+                                this.hisData[0].start = start;
+                                this.vodData.start = start;
+                            } else {
+                                this.vodData.start = this.hisData[0].start;
+                            }
+                            if (this.hisData[0].end !== 0 && this.hisData[0].end < end) {
+                                this.vodData.end = this.hisData[0].end;
+                            }
+                            this.playback();
+                        } else {
+                            this.$message({
+                                type: 'warning',
+                                message: '请确认设备是否支持录像,或者设备SD卡是否正确插入!',
+                            });
+                        }
+                    } else {
+                        this.$message({
+                            type: 'warning',
+                            message: '请确认设备是否支持录像,或者设备SD卡是否正确插入!',
+                        });
+                    }
+                });
+            }
+        },
+        /**录像播放*/
+        playback() {
+            const query = {
+                start: this.vodData.start,
+                end: this.vodData.end,
+            };
+            if (this.ssrc) {
+                closeStream(this.deviceId, this.channelId, this.ssrc).then((res) => {
+                    playback(this.deviceId, this.channelId, query)
+                        .then((res) => {
+                            this.initUrl(res.data);
+                        })
+                        .finally(() => {
+                            this.triggerPlay(this.hisData);
+                        });
+                });
+            } else {
+                playback(this.deviceId, this.channelId, query)
+                    .then((res) => {
+                        this.initUrl(res.data);
+                    })
+                    .finally(() => {
+                        this.triggerPlay(this.hisData);
+                    });
+            }
+        },
+        /**触发播放*/
+        triggerPlay(playTimes) {
+            this.$refs.playbacker.playback(this.playurl, playTimes);
+            this.playing = true;
+        },
+        /**录像播放*/
+        seekPlay(s) {
+            const curTime = this.vodData.base + s.hour * 3600 + s.min * 60 + s.second;
+            const seekRange = curTime - this.vodData.start;
+            if (this.ssrc) {
+                const query = {
+                    seek: seekRange,
+                };
+                const _this = this;
+                playbackSeek(this.deviceId, this.channelId, this.streamId, query).then((res) => {
+                    _this.$refs.playbacker.setPlaybackStartTime(curTime);
+                });
+            }
+        },
+        /**关闭播放流*/
+        closeStream() {
+            if (this.playing && this.streamId) {
+                closeStream(this.deviceId, this.channelId, this.streamId).then((res) => {
+                    this.streamId = '';
+                    this.ssrc = '';
+                    this.playurl = '';
+                    this.playing = false;
+                });
+                // this.$refs.playbacker.destroy();
+            }
+        },
+        /**销毁录像播放器*/
+        destroy() {
+            if (this.playing && this.streamId) {
+                this.$refs.playbacker.destroy();
+            }
+        },
+        closeDestroy() {
+            this.closeStream();
+            this.destroy();
+        },
+        /**设置时间*/
+        timePickerChange: function (val) {
+            this.setTime(val[0], val[1]);
+        },
+        setTime: function (startTime, endTime) {
+            this.startTime = startTime;
+            this.endTime = endTime;
+            this.timeRange = [startTime, endTime];
+        },
+        /**下载录像*/
+        downloadRecord: function () {
+            const start = new Date(this.startTime).getTime() / 1000;
+            const end = new Date(this.endTime).getTime() / 1000;
+            const query = {
+                startTime: start,
+                endTime: end,
+                speed: '4',
+            };
+            startDownloadRecord(this.deviceId, this.channelId, query).then((res) => {
+                console.log('开始转存到流服务器:' + this.deviceId + ' : ' + this.channelId);
+                if (res.code === 200) {
+                    this.$message({
+                        type: 'success',
+                        message: '转存到流服务器,请前往视频中心->录像管理查看!',
+                    });
+                }
+            });
+        },
+    },
+};
+</script>

+ 62 - 0
src/views/pms/video_center/sip/components/player/easyplayer.vue

@@ -0,0 +1,62 @@
+<template>
+  <div id="easyplayer"></div>
+</template>
+
+<script>
+export default {
+  name: 'player',
+  data() {
+    return {
+      easyPlayer: null
+    };
+  },
+  props: ['videoUrl', 'error', 'hasaudio', 'height'],
+  mounted () {
+    let paramUrl = decodeURIComponent(this.$route.params.url)
+    this.$nextTick(() =>{
+      if (typeof (this.videoUrl) == "undefined") {
+        this.videoUrl = paramUrl;
+      }
+      console.log("初始化时的地址为: " + this.videoUrl)
+      this.play(this.videoUrl)
+    })
+  },
+  watch:{
+    videoUrl(newData, oldData){
+      this.play(newData)
+    },
+    immediate:true
+  },
+  methods: {
+    play: function (url) {
+      console.log(this.height)
+      if (this.easyPlayer != null) {
+        this.easyPlayer.destroy();
+      }
+      if (typeof (this.height) == "undefined") {
+        this.height = false
+      }
+      this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK, {Height: this.height})
+      this.easyPlayer.play(url, 1)
+    },
+    pause: function () {
+      this.easyPlayer.destroy();
+      this.easyPlayer = null
+    },
+  },
+  destroyed() {
+    this.easyPlayer.destroy();
+  },
+}
+</script>
+
+<style>
+.LodingTitle {
+  min-width: 70px;
+}
+/* 隐藏logo */
+.iconqingxiLOGO {
+  display: none !important;
+}
+
+</style>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/jessibuca-pro-multi.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/jessibuca-pro-talk.js


+ 280 - 0
src/views/pms/video_center/sip/components/player/jessibuca.vue

@@ -0,0 +1,280 @@
+<template>
+  <div ref="container" @dblclick="fullscreenSwich"
+       style="width:100%;height:100%;background-color: #000000;margin:0 auto;">
+  </div>
+</template>
+
+<script>
+let jessibucaPlayer = {};
+export default {
+  name: 'jessibuca',
+  data() {
+    return {
+      playing: false,
+      isNotMute: false,
+      quieting: false,
+      fullscreen: false,
+      loaded: false, // mute
+      speed: 0,
+      performance: "", // 工作情况
+      kBps: 0,
+      btnDom: null,
+      videoInfo: null,
+      volume: 1,
+      rotate: 0,
+      vod: true, // 点播
+      forceNoOffscreen: false,
+    };
+  },
+  props: ['videoUrl', 'error', 'hasAudio', 'height'],
+  mounted() {
+    window.onerror = (msg) => {
+      // console.error(msg)
+    };
+    console.log(this._uid)
+    let paramUrl = decodeURIComponent(this.$route.params.url)
+    this.$nextTick(() => {
+      this.updatePlayerDomSize()
+      window.onresize = () => {
+        this.updatePlayerDomSize()
+      }
+      if (typeof (this.videoUrl) == "undefined") {
+        this.videoUrl = paramUrl;
+      }
+      this.btnDom = document.getElementById("buttonsBox");
+      console.log("初始化时的地址为: " + this.videoUrl)
+      this.play(this.videoUrl)
+    })
+  },
+  watch: {
+    videoUrl(newData, oldData) {
+      this.play(newData)
+    },
+    immediate: true
+  },
+  methods: {
+    updatePlayerDomSize() {
+      let dom = this.$refs.container;
+      let width = dom.parentNode.clientWidth
+      let height = (9 / 16) * width
+      const clientHeight = Math.min(document.body.clientHeight, document.documentElement.clientHeight)
+      if (height > clientHeight) {
+        height = clientHeight
+        width = (16 / 9) * height
+      }
+      dom.style.width = width + 'px';
+      dom.style.height = height + "px";
+    },
+    create() {
+      let options = {};
+      jessibucaPlayer[this._uid] = new window.JessibucaPro(Object.assign(
+        {
+          container: this.$refs.container,
+          autoWasm: true,
+          background: "",
+          controlAutoHide: false,
+          debug: false,
+          debugLevel: "debug",
+          decoder: "/js/jessibuca-pro/decoder-pro.js",
+          forceNoOffscreen: true,
+          hasAudio: typeof (this.hasAudio) == "undefined" ? true : this.hasAudio,
+          hasVideo: true,
+          heartTimeout: 5,
+          heartTimeoutReplay: true,
+          heartTimeoutReplayTimes: 3,
+          hiddenAutoPause: false,
+          hotKey: false,
+          isFlv: false,
+          isFullResize: false,
+          isNotMute: this.isNotMute,
+          isResize: false,
+          keepScreenOn: false,
+          loadingText: "请稍等, 视频加载中......",
+          loadingTimeout: 10,
+          loadingTimeoutReplay: true,
+          loadingTimeoutReplayTimes: 3,
+          openWebglAlignment: false,
+          operateBtns: {
+            fullscreen: true,
+            zoom: true,
+            ptz: false,
+            play: true
+          },
+          recordType: "webm",
+          rotate: 0,
+          showBandwidth: false,
+          supportDblclickFullscreen: false,
+          timeout: 10,
+          useMSE: location.hostname !== "localhost" && location.protocol !== "https:",
+          useOffscreen: false,
+          useWCS: location.hostname === "localhost" || location.protocol === "https",
+          useWebFullScreen: false,
+          videoBuffer: 0,
+          wasmDecodeAudioSyncVideo: true,
+          wasmDecodeErrorReplay: true,
+          wcsUseVideoRender: true
+        },
+        options
+      ));
+      let jessibuca = jessibucaPlayer[this._uid];
+      let _this = this;
+      jessibuca.on("load", function () {
+        console.log("on load init");
+      });
+
+      jessibuca.on("log", function (msg) {
+        console.log("on log", msg);
+      });
+      jessibuca.on("record", function (msg) {
+        console.log("on record:", msg);
+      });
+      jessibuca.on("pause", function () {
+        _this.playing = false;
+        _this.loaded = true;
+      });
+      jessibuca.on("play", function () {
+        _this.playing = true;
+        _this.loaded = true;
+      });
+      jessibuca.on("fullscreen", function (msg) {
+        console.log("on fullscreen", msg);
+        _this.fullscreen = msg
+      });
+
+      jessibuca.on("mute", function (msg) {
+        console.log("on mute", msg);
+        _this.isNotMute = !msg;
+      });
+      jessibuca.on("audioInfo", function (msg) {
+        console.log("audioInfo", msg);
+      });
+      let _ts = 0;
+      jessibuca.on("timeUpdate", function (ts) {
+        // console.log('timeUpdate,old,new,timestamp', _ts, ts, ts - _ts);
+        _ts = ts;
+      });
+      jessibuca.on("performance", function (performance) {
+        let show = "卡顿";
+        if (performance === 2) {
+          show = "非常流畅";
+        } else if (performance === 1) {
+          show = "流畅";
+        }
+        _this.performance = show;
+      });
+      jessibuca.on('kBps', function (kBps) {
+        _this.kBps = Math.round(kBps);
+      });
+    },
+    play: function (url) {
+      if (jessibucaPlayer[this._uid]) {
+        this.destroy().then(() => {
+          if (jessibucaPlayer[this._uid] && jessibucaPlayer[this._uid].hasLoaded()) {
+            jessibucaPlayer[this._uid].play(url);
+          } else {
+            jessibucaPlayer[this._uid].on("load", () => {
+              console.log("load 播放")
+              jessibucaPlayer[this._uid].play(url);
+            });
+          }
+        });
+      } else {
+        this.create();
+        if (jessibucaPlayer[this._uid] && jessibucaPlayer[this._uid].hasLoaded()) {
+          jessibucaPlayer[this._uid].play(url);
+        } else {
+          jessibucaPlayer[this._uid].on("load", () => {
+            console.log("load 播放")
+            jessibucaPlayer[this._uid].play(url);
+          });
+        }
+      }
+    },
+    pause: function () {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].pause();
+      }
+      this.playing = false;
+      this.err = "";
+      this.performance = "";
+    },
+    screenshot: function () {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].screenshot();
+      }
+    },
+    mute: function () {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].mute();
+      }
+    },
+    cancelMute: function () {
+      if (jessibucaPlayer[this._uid]) {
+        jessibucaPlayer[this._uid].cancelMute();
+      }
+    },
+   destroy: async function () {
+      if (jessibucaPlayer[this._uid]) {
+        await jessibucaPlayer[this._uid].destroy().then(() => {
+          jessibucaPlayer[this._uid] = null;
+          this.playing = false;
+          this.create();
+        });
+      }
+    },
+    fullscreenSwich: function () {
+      let isFull = this.isFullscreen()
+      jessibucaPlayer[this._uid].setFullscreen(!isFull)
+      this.fullscreen = !isFull;
+    },
+    isFullscreen: function () {
+      return document.fullscreenElement ||
+        document.msFullscreenElement ||
+        document.mozFullScreenElement ||
+        document.webkitFullscreenElement || false;
+    }
+  },
+  destroyed() {
+    if (jessibucaPlayer[this._uid]) {
+      jessibucaPlayer[this._uid].destroy();
+    }
+    this.playing = false;
+    this.loaded = false;
+    this.performance = "";
+  },
+}
+</script>
+
+<style>
+@import '../css/iconfont.css';
+.buttons-box {
+  width: 100%;
+  height: 28px;
+  background-color: rgba(43, 51, 63, 0.7);
+  position: absolute;
+  display: -webkit-box;
+  display: -ms-flexbox;
+  display: flex;
+  left: 0;
+  bottom: 0;
+  user-select: none;
+  z-index: 10;
+}
+
+.jessibuca-btn {
+  width: 20px;
+  color: rgb(255, 255, 255);
+  line-height: 27px;
+  margin: 0px 10px;
+  padding: 0px 2px;
+  cursor: pointer;
+  text-align: center;
+  font-size: 0.8rem !important;
+}
+
+.buttons-box-right {
+  position: absolute;
+  right: 0;
+}
+
+</style>

+ 351 - 0
src/views/pms/video_center/sip/components/player/player.vue

@@ -0,0 +1,351 @@
+<template>
+    <div class="root">
+        <div class="container-shell">
+            <div id="container" ref="container" :style="{width: width, height: height}"></div>
+        </div>
+    </div>
+</template>
+
+<script>
+let jessibucaPlayer = {};
+import {ptzdirection, ptzscale} from '@/api/iot/sipdevice';
+
+export default {
+    name: 'player',
+    props: {
+        playerinfo: {
+            type: Object,
+            default: null,
+        },
+        width: {
+            type: String,
+            default: '1000px'
+        },
+        height: {
+            type: String,
+            default: '630px'
+        }
+    },
+    mounted() {
+        console.log("this._uid",this._uid);
+    },
+    watch: {
+        playerinfo: function (newVal, oldVal) {
+            console.log('playerinfo 发生变化');
+            this.playinfo = newVal;
+            if (this.playinfo && this.playinfo.playtype !== '') {
+                this.playtype = this.playinfo.playtype;
+            }
+        },
+    },
+    jessibuca: null,
+    data() {
+        return {
+            isPlaybackPause: false,
+            useWebGPU: false,
+            isInit: false,
+            playinfo: {},
+            playtype: 'play',
+            operateBtns: {
+                fullscreen: true,
+                zoom: true,
+                play: true,
+                audio: true,
+            },
+        };
+    },
+    beforeDestroy() {
+    },
+    created() {
+        this.playinfo = this.playerinfo;
+        if (this.playinfo && this.playinfo.playtype !== '') {
+            this.playtype = this.playinfo.playtype;
+        }
+        this.init();
+    },
+    methods: {
+        init() {
+            var isSupportWebgpu = 'gpu' in navigator;
+            if (isSupportWebgpu) {
+                console.log('支持webGPU');
+                this.useWebGPU = true;
+            } else {
+                console.log('暂不支持webGPU,降级到webgl渲染');
+                this.useWebGPU = false;
+            }
+
+            const useVconsole = this.isMobile() || this.isPad();
+            if (useVconsole && window.VConsole) {
+                new window.VConsole();
+            }
+            this.$nextTick(() => {
+                this.initplayer();
+            });
+        },
+        initplayer() {
+            this.isPlaybackPause = false;
+            this.initconf();
+            jessibucaPlayer[this._uid] = new window.JessibucaPro({
+                container: this.$refs.container,
+                decoder: '/js/jessibuca-pro/decoder-pro.js',
+                videoBuffer: Number(0.2), // 缓存时长
+                isResize: false,
+                useWCS: false,
+                useMSE: false,
+                useSIMD: true,
+                wcsUseVideoRender: false,
+                loadingText: '加载中',
+                debug: false,
+                debugLevel: "debug",
+                showBandwidth: true, // 显示网速
+                showPlaybackOperate: true,
+                operateBtns: this.operateBtns,
+                forceNoOffscreen: true,
+                isNotMute: true, // 默认关闭声音
+                showPerformance: false,
+                // playFailedAndReplay: true,
+                // networkDelayTimeoutReplay: true,
+                playbackForwardMaxRateDecodeIFrame: 4,
+                useWebGPU: this.useWebGPU, // 使用WebGPU
+            });
+            let jessibuca = jessibucaPlayer[this._uid];
+            this.initcallback(jessibuca);
+            this.isInit = true;
+        },
+        initconf() {
+            if (this.playtype === 'play') {
+                //直播按钮配置
+                this.operateBtns.ptz = true;
+            } else {
+                //录像回放按钮配置
+                this.operateBtns.ptz = false;
+            }
+        },
+        initcallback(jessibuca) {
+            const _this = this;
+            jessibuca.on('error', function (error) {
+                console.log('jessibuca error', error);
+                //_this.destroy();
+            });
+            jessibuca.on("playFailedAndPaused", function (reason, lastFrameInfo, msg) {
+                console.log('playFailedAndPaused', reason, msg);
+                // lastFrameInfo 是最后一帧的画面,可以用来重播的时候,显示最后一帧画面。
+                // msg 具体的错误信息。
+            });
+            jessibuca.on('visibilityChange', (value) => {
+                if (value === true) {
+                    // 窗口显示
+                    console.log('visibilityChange true');
+                } else {
+                    // 窗口隐藏
+                    console.log('visibilityChange false');
+                }
+            });
+            jessibuca.on('pause', function (pause) {
+                console.log('pause success!');
+                console.log(pause);
+            });
+            jessibuca.on('play', function (flag) {
+                console.log('play!', flag);
+            });
+            jessibuca.on('loading', function (load) {
+                console.log('loading success!', load);
+            });
+            jessibuca.on('stats', function (s) {
+                // console.log('stats is', s);
+            });
+            jessibuca.on('timeout', function (error) {
+                console.log('timeout:', error);
+            });
+            jessibuca.on('playbackPreRateChange', (rate) => {
+                jessibuca.forward(rate);
+            });
+
+            let pre = 0;
+            let cur = 0;
+            jessibuca.on('timeUpdate', function (ts) {
+                cur = parseInt(ts / 60000);
+                if (pre !== cur) {
+                    pre++;
+                }
+            });
+            jessibuca.on(JessibucaPro.EVENTS.ptz, (arrow) => {
+                console.log('ptz arrow', arrow);
+                _this.handlePtz(arrow);
+            });
+            jessibuca.on('crashLog', (data) => {
+                console.log('crashLog is', data);
+            });
+        },
+        registercallback(events, func) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].on(events, func);
+            }
+        },
+        isMobile() {
+            return /iphone|ipad|android.*mobile|windows.*phone|blackberry.*mobile/i.test(window.navigator.userAgent.toLowerCase());
+        },
+        isPad() {
+            return /ipad|android(?!.*mobile)|tablet|kindle|silk/i.test(window.navigator.userAgent.toLowerCase());
+        },
+        play(url) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].play(url);
+            }
+        },
+        pause() {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].pause();
+            }
+        },
+        replay(url) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].destroy().then(() => {
+                    this.initplayer();
+                    this.play(url);
+                });
+            } else {
+                this.initplayer();
+                this.play(url);
+            }
+        },
+        handlePtz(arrow) {
+            let leftRight = 0;
+            let upDown = 0;
+            if (arrow === 'left') {
+                leftRight = 2;
+            } else if (arrow === 'right') {
+                leftRight = 1;
+            } else if (arrow === 'up') {
+                upDown = 1;
+            } else if (arrow === 'down') {
+                upDown = 2;
+            }
+            var data = {
+                leftRight: leftRight,
+                upDown: upDown,
+                moveSpeed: 125,
+            };
+            if (this.playinfo && this.playinfo.playtype !== '') {
+                ptzdirection(this.playinfo.deviceId, this.playinfo.channelId, data).then(async (response) => {
+                    //console.log("云台方向控制:" + JSON.stringify(response));
+                });
+            }
+        },
+        playback(url, playTimes) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].playback(url, {
+                    playList: playTimes,
+                    fps: 25, //FPS(定频(本地设置)生效)
+                    showControl: true,
+                    showRateBtn: true,
+                    isUseFpsRender: true, // 是否使用固定的fps渲染,如果设置的fps小于流推过来的,会造成内存堆积甚至溢出
+                    isCacheBeforeDecodeForFpsRender: false, // rfs渲染时,是否在解码前缓存数据
+                    supportWheel: true, // 是否支持滚动轴切换精度。
+                    rateConfig: [
+                        {label: '正常', value: 1},
+                        {label: '2倍', value: 2},
+                        {label: '4倍', value: 4},
+                        {label: '8倍', value: 8},
+                    ],
+                });
+                this.isPlaybackPause = false;
+            }
+        },
+        playbackPause() {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].playbackPause();
+                this.isPlaybackPause = true;
+            }
+        },
+        replayback(url, playTimes) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].destroy().then(() => {
+                    this.initplayer();
+                    this.playback(url, playTimes);
+                });
+            } else {
+                this.initplayer();
+                this.playback(url, playTimes);
+            }
+        },
+        setPlaybackStartTime(curTime) {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].setPlaybackStartTime(curTime);
+            }
+        },
+        destroy() {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].destroy().then(() => {
+                    this.initplayer();
+                });
+            }
+        },
+        close() {
+            if (jessibucaPlayer[this._uid]) {
+                jessibucaPlayer[this._uid].close();
+            }
+        },
+    },
+};
+</script>
+
+<style scoped lang="scss">
+.root {
+    display: flex;
+    margin-right: 3rem;
+}
+
+.container-shell {
+    backdrop-filter: blur(5px);
+    // background: hsla(0, 0%, 50%, 0.5);
+    //background: #fff;
+    //padding: 10px 4px 10px 4px;
+    /* border: 2px solid black; */
+    // width: auto;
+    position: relative;
+    border-radius: 10px;
+    // box-shadow: 0 5px 5px;
+}
+
+.container-shell:before {
+    //content: "设备播放器";
+    position: absolute;
+    color: darkgray;
+    //top: 4px;
+    left: 10px;
+    //text-shadow: 1px 1px black;
+}
+
+#container {
+    background: rgba(13, 14, 27, 0.7);
+    border-radius: 5px;
+}
+
+.err {
+    position: absolute;
+    top: 40px;
+    left: 10px;
+    color: red;
+}
+
+.option {
+    position: absolute;
+    top: 4px;
+    right: 10px;
+    display: flex;
+    place-content: center;
+    font-size: 12px;
+}
+
+.option span {
+    color: white;
+}
+
+@media (max-width: 720px) {
+    #container {
+        width: 90vw;
+        height: 52.7vw;
+    }
+}
+</style>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/public/jessibuca-pro-multi.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/public/jessibuca-pro-talk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/public/web-player-pro-multi.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/public/web-player-pro-talk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/public/web-player-pro.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/web-player-pro-multi.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/web-player-pro-talk.js


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/views/pms/video_center/sip/components/player/web-player-pro.js


+ 542 - 0
src/views/pms/video_center/sip/index.vue

@@ -0,0 +1,542 @@
+<template>
+    <div>
+        <el-card class="main-card">
+            <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="60px" style="margin-bottom: -20px">
+                <el-form-item :label="$t('sip.index.998533-0')" prop="deviceSipId">
+                    <el-input v-model="queryParams.deviceSipId" :placeholder="$t('sip.index.998533-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-2')" prop="channelSipId">
+                    <el-input v-model="queryParams.channelSipId" :placeholder="$t('sip.index.998533-3')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-4')" prop="status">
+                    <el-select v-model="queryParams.status" :placeholder="$t('sip.index.998533-5')" clearable size="small">
+                        <el-option v-for="dict in dict.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="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-card>
+        <el-card class="main-card">
+            <div class="mb20">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['iot:video:add']" :disabled="isGeneralUser">{{ $t('sip.index.998533-6') }}</el-button>
+                <el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple || isGeneralUser" v-hasPermi="['iot:video:remove']" @click="handleDelete">
+                    {{ $t('sip.index.998533-7') }}
+                </el-button>
+            </div>
+            <el-table :border="false" v-loading="loading" :data="sipidList" @selection-change="handleSelectionChange" size="" @cell-dblclick="celldblclick">
+                <el-table-column type="selection" :selectable="selectable" width="55" align="center" />
+                <el-table-column :label="$t('device.device-edit.148398-7')" align="center" prop="deviceSipId">
+                    <template slot-scope="scope">
+                        <el-link :underline="false" type="primary" @click="handleViewDevice(scope.row.deviceSipId)">{{ scope.row.deviceSipId }}</el-link>
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('sip.index.998533-2')" align="center" prop="channelSipId" />
+                <el-table-column :label="$t('sip.index.998533-4')" align="center" prop="status" width="80">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.iot_device_status" :value="scope.row.status" size="mini" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('sip.index.998533-8')" align="center" prop="productName" />
+                <el-table-column :label="$t('sip.index.998533-9')" align="center" prop="deviceType">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.video_type" :value="scope.row.deviceType" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('sip.index.998533-15')" align="center" prop="channelType">
+                    <template slot-scope="scope">
+                        <dict-tag :options="dict.type.channel_type" :value="scope.row.channelType" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('sip.index.998533-10')" align="center" prop="citycode" />
+                <el-table-column :label="$t('sip.index.998533-11')" align="center" prop="registerTime" width="180">
+                    <template slot-scope="scope">
+                        <span>{{ parseTime(scope.row.registerTime, '{y}-{m}-{d} {h}:{m}:{s}') }}</span>
+                    </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="mini" type="text" icon="el-icon-edit" @click="handleBinding(scope.row)">{{ $t('sip.index.998533-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="getList" />
+        </el-card>
+
+        <el-dialog :title="title" :visible.sync="open" width="450px" append-to-body>
+            <el-form :model="createForm" label-width="80px" ref="createForm">
+                <el-form-item :label="$t('sip.index.998533-13')">
+                    <el-cascader :options="cityOptions" v-model="createForm.city" @change="changeProvince" change-on-select></el-cascader>
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-9')" prop="deviceType">
+                    <el-select v-model="createForm.deviceType" :placeholder="$t('sip.index.998533-14')">
+                        <el-option v-for="dict in dict.type.video_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-15')" prop="channelType">
+                    <el-select v-model="createForm.channelType" :placeholder="$t('sip.index.998533-16')">
+                        <el-option v-for="dict in dict.type.channel_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-8')" prop="productName">
+                    <el-input readonly v-model="createForm.productName" :placeholder="$t('sip.index.998533-17')">
+                        <el-button slot="append" @click="selectProduct()">{{ $t('sip.index.998533-18') }}</el-button>
+                    </el-input>
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-20')" prop="createNum">
+                    <el-input-number controls-position="right" v-model="createForm.createNum" :max="10" :placeholder="$t('sip.index.998533-19')" style="width: 330px" type="number" />
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitForm">{{ $t('sip.index.998533-21') }}</el-button>
+                <el-button @click="cancel">{{ $t('cancel') }}</el-button>
+            </div>
+        </el-dialog>
+        <el-dialog :title="title" :visible.sync="bindingOpen" width="450px" append-to-body>
+            <el-form :model="form" label-width="80px" ref="form">
+                <el-form-item :label="$t('sip.index.998533-22')" prop="deviceId">
+                    <el-select style="width: 210px" v-model="form.reDeviceId" size="small" :placeholder="$t('sip.index.998533-23')" filterable @change="handleUpdateDeviceItem(scope.row, $event)">
+                        <el-option v-for="(item, index) in deviceList" :key="index" :label="item.deviceName" :value="item.deviceId"></el-option>
+                    </el-select>
+                </el-form-item>
+                <el-form-item :label="$t('sip.index.998533-24')" prop="sceneId">
+                    <el-select style="width: 210px" v-model="form.reSceneModelId" size="small" :placeholder="$t('sip.index.998533-25')" filterable>
+                        <el-option v-for="(item, index) in sceneList" :key="index" :label="item.sceneModelName" :value="item.sceneModelId"></el-option>
+                    </el-select>
+                </el-form-item>
+            </el-form>
+            <div slot="footer" class="dialog-footer">
+                <el-button type="primary" @click="submitFormBinding">{{ $t('confirm') }}</el-button>
+                <el-button @click="cancelBinding">{{ $t('cancel') }}</el-button>
+            </div>
+        </el-dialog>
+
+        <!-- 选择产品 -->
+        <product-list ref="productList" @productEvent="getProductData($event)" />
+    </div>
+</template>
+
+<style>
+.createNum {
+    width: 300px;
+}
+
+.createNum input {
+    width: 260px;
+}
+</style>
+
+<script>
+import { getDeviceBySerialNumber, listDeviceShort } from '@/api/pms/video/device';
+import { regionData, CodeToText } from 'element-china-area-data';
+import { listChannel, getChannel, delChannel, addChannel, binding } from '@/api/pms/video/channel';
+import productList from './product-list.vue';
+import { getSceneModelList } from '@/api/pms/video/list';
+
+export default {
+    name: 'Sip',
+    dicts: ['iot_device_status', 'video_type', 'channel_type'],
+    components: {
+        productList,
+    },
+    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();
+            }
+        },
+    },
+    data() {
+        return {
+            // 是否普通用户
+            isGeneralUser: true,
+            // 遮罩层
+            loading: true,
+            // 选中数组
+            ids: [],
+            // 非多个禁用
+            multiple: true,
+            // 总条数
+            total: 0,
+            // sipid表格数据
+            sipidList: [],
+            // 弹出层标题
+            title: '',
+            // 是否显示弹出层
+            open: false,
+            //绑定弹窗
+            bindingOpen: false,
+            /*选择设备列表*/
+            deviceList: [],
+            //场景列表
+            sceneList: [],
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                deviceSipId: null,
+                deviceChannelId: null,
+                status: null,
+            },
+            // 表单参数
+            createForm: {
+                city: '',
+                deviceType: '',
+                channelType: '',
+                createNum: 1,
+                remark: '',
+                area: '',
+            },
+            form: {},
+            // 产品
+            productInfo: {},
+            // 城市
+            cityOptions: regionData,
+            city: '',
+            // 表单校验
+            rules: {
+                protocol: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-26'),
+                        trigger: 'blur',
+                    },
+                ],
+                ip: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-27'),
+                        trigger: 'blur',
+                    },
+                ],
+                domain: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-28'),
+                        trigger: 'blur',
+                    },
+                ],
+                secret: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-29'),
+                        trigger: 'blur',
+                    },
+                ],
+                portHttp: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-30'),
+                        trigger: 'blur',
+                    },
+                ],
+                portHttps: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-31'),
+                        trigger: 'blur',
+                    },
+                ],
+                portRtmp: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-32'),
+                        trigger: 'blur',
+                    },
+                ],
+                portRtsp: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-33'),
+                        trigger: 'blur',
+                    },
+                ],
+                rtpPortRange: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-34'),
+                        trigger: 'blur',
+                    },
+                ],
+                delFlag: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-35'),
+                        trigger: 'blur',
+                    },
+                ],
+                createBy: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-36'),
+                        trigger: 'blur',
+                    },
+                ],
+                createTime: [
+                    {
+                        required: true,
+                        message: this.$t('sip.index.998533-37'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+        };
+    },
+    created() {
+        // 普通用户只能查看自己的通道
+        if (this.$store.state.user.roles.indexOf('general') === -1) {
+            this.isGeneralUser = false;
+        }
+        this.getList();
+        this.getDeviceList();
+        this.getSceneListDatas();
+    },
+    methods: {
+        /** 查看设备操作 */
+        handleViewDevice(serialNumber) {
+            this.$router.push({
+                path: '/iot/device',
+                query: {
+                    t: Date.now(),
+                    sn: serialNumber,
+                },
+            });
+        },
+        /**选择产品 */
+        selectProduct() {
+            this.open = false;
+            this.$refs.productList.open = true;
+            this.$refs.productList.getList();
+        },
+        /**获取选中的产品 */
+        getProductData(product) {
+            this.open = true;
+            this.createForm.productId = product.productId;
+            this.createForm.productName = product.productName;
+            this.createForm.tenantId = product.tenantId;
+            this.createForm.tenantName = product.tenantName;
+        },
+        /** 行政区划改变 **/
+        changeProvince(data) {
+            if (data && data[0] != null && data[1] != null && data[2] != null) {
+                const str = CodeToText[data[0]] + '/' + CodeToText[data[1]] + '/' + CodeToText[data[2]];
+                this.createForm.citycode = str;
+            }
+        },
+        /**获取设备详情*/
+        getDeviceBySerialNumber(serialNumber) {
+            this.openDevice = true;
+            getDeviceBySerialNumber(serialNumber).then((response) => {
+                this.device = response.data;
+            });
+        },
+        /** 查询通道列表 */
+        getList() {
+            listChannel(this.queryParams).then((response) => {
+                this.sipidList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        // 取消按钮
+        cancel() {
+            this.open = false;
+            this.reset();
+        },
+        // 表单重置
+        reset() {
+            this.createForm = {
+                id: null,
+                deviceSipId: null,
+                channelSipId: null,
+                status: 0,
+                registertime: null,
+                createBy: null,
+                createTime: null,
+                updateBy: null,
+                updateTime: null,
+                remark: null,
+                createNum: 1,
+            };
+            this.resetForm('createForm');
+        },
+        /** 搜索按钮操作 */
+        handleQuery() {
+            this.loading = true;
+            this.queryParams.pageNum = 1;
+            this.getList();
+        },
+        /** 重置按钮操作 */
+        resetQuery() {
+            this.resetForm('queryForm');
+            this.handleQuery();
+        },
+        // 多选框选中数据
+        handleSelectionChange(selection) {
+            this.ids = selection.map((item) => item.id);
+            this.multiple = !selection.length;
+        },
+        /** 批量新增按钮操作 */
+        handleAdd() {
+            this.reset();
+            this.open = true;
+            this.title = this.$t('sip.index.998533-38');
+        },
+        /** 修改按钮操作 */
+        handleUpdate(row) {
+            this.reset();
+            const id = row.id || this.ids;
+            console.log(row);
+            getChannel(id).then((response) => {
+                this.createForm = response.data;
+                this.open = true;
+                this.title = this.$t('sip.index.998533-39');
+            });
+        },
+        handleBinding(row) {
+            this.form = row;
+            this.bindingOpen = true;
+            this.title = this.$t('sip.index.998533-40');
+        },
+
+        // 获取设备列表
+        getDeviceList() {
+            const params = {
+                showChild: true,
+                pageNum: 1,
+                pageSize: 9999,
+            };
+            listDeviceShort(params).then((res) => {
+                if (res.code === 200) {
+                    this.deviceList = res.rows;
+                } else {
+                    this.$message.error(res.msg);
+                }
+            });
+        },
+        // 获取场景列表数据
+        getSceneListDatas() {
+            const params = {
+                pageNum: 1,
+                pageSize: 9999,
+            };
+            getSceneModelList(params).then((res) => {
+                if (res.code === 200) {
+                    this.sceneList = res.rows;
+                } else {
+                    this.$message.error(res.msg);
+                }
+            });
+        },
+        cancelBinding() {
+            this.bindingOpen = false;
+            this.reset();
+        },
+        /*确认绑定设备或场景*/
+        submitFormBinding() {
+            const data = {
+                channelId: this.form.channelSipId,
+                reDeviceId: this.form.reDeviceId,
+                reSceneModelId: this.form.reSceneModelId,
+            };
+            binding(data).then((response) => {
+                if (response.code === 200) {
+                    this.bindingOpen = false;
+                    this.$message.success(this.$t('sip.index.998533-41'));
+                } else {
+                    this.$message.error(response.msg);
+                }
+            });
+        },
+        /** 提交按钮 */
+        submitForm() {
+            if (this.createForm.createNum < 1) {
+                this.$modal.alertError(this.$t('sip.index.998533-42'));
+                return;
+            }
+            if (!this.createForm.productId || this.createForm.productId == 0) {
+                this.$modal.alertError(this.$t('sip.index.998533-43'));
+                return;
+            }
+            this.createForm.deviceSipId = this.createForm.city[2] + '0000' + this.createForm.deviceType + '0';
+            this.createForm.channelSipId = this.createForm.city[2] + '0000' + this.createForm.channelType + '0';
+            if (this.createForm.deviceType !== '' && this.createForm.channelType !== '' && this.createForm.city.length === 3) {
+                console.log(this.createForm);
+                addChannel(this.createForm.createNum, this.createForm).then((response) => {
+                    this.$modal.msgSuccess(this.$t('addSuccess'));
+                    this.open = false;
+                    this.getList();
+                });
+            } else {
+                this.$message({
+                    type: 'error',
+                    message: this.$t('sip.index.998533-44'),
+                });
+            }
+        },
+        /** 删除按钮操作 */
+        handleDelete(row) {
+            const sipIds = row.id || this.ids;
+            this.$modal
+                .confirm(this.$t('sip.index.998533-45') + sipIds + this.$t('notify.channel.index.333541-18'))
+                .then(function () {
+                    return delChannel(sipIds);
+                })
+                .then(() => {
+                    this.getList();
+                    this.$modal.msgSuccess(this.$t('delSuccess'));
+                })
+                .catch(() => {});
+        },
+
+        //禁用有绑定设备的复选框,status:1=未使用,2=已使用
+        selectable(row) {
+            if (row.status == 2 || this.isGeneralUser) {
+                return false;
+            }
+            return 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('success'),
+                message: this.$t('sip.index.998533-46'),
+                type: 'success',
+                offset: 50,
+                duration: 2000,
+            });
+        },
+        onError() {
+            this.$notify({
+                title: this.$t('fail'),
+                message: this.$t('sip.index.998533-47'),
+                type: 'error',
+                offset: 50,
+                duration: 2000,
+            });
+        },
+    },
+};
+</script>

+ 388 - 0
src/views/pms/video_center/sip/mediaServer-edit.vue

@@ -0,0 +1,388 @@
+<template>
+  <div id="mediaServerEdit" v-loading="isLoging">
+    <el-dialog :title="$t('sip.mediaServerEdit.998534-0')" :width="dialogWidth" top="2rem" :close-on-click-modal="false"
+               :destroy-on-close="true" :visible.sync="showDialog" @close="close()">
+      <div id="formStep" style="margin-top: 1rem; margin-right: 20px">
+        <el-form v-if="currentStep == 1" ref="mediaServerForm" :rules="rules" :model="mediaServerForm"
+                 label-width="280px" style="width: 70%">
+          <!--          <el-form-item label="所属租户" prop="productName">-->
+          <!--            <el-input readonly v-model="mediaServerForm.tenantName" placeholder="请选择所属租户">-->
+          <!--              <el-button slot="append" @click="selectUser()">选择</el-button>-->
+          <!--            </el-input>-->
+          <!--          </el-form-item>-->
+          <el-form-item :label="$t('sip.mediaServerEdit.998534-1')" prop="ip">
+            <el-input v-model="mediaServerForm.ip" :placeholder="$t('sip.mediaServerEdit.998534-2')"
+                      clearable></el-input>
+          </el-form-item>
+          <el-form-item :label="$t('sip.mediaServerEdit.998534-3')" prop="portHttp">
+            <el-input v-model="mediaServerForm.portHttp" :placeholder="$t('sip.mediaServerEdit.998534-4')"
+                      clearable></el-input>
+          </el-form-item>
+          <el-form-item :label="$t('sip.mediaServerEdit.998534-5')" prop="secret">
+            <el-input v-model="mediaServerForm.secret" :placeholder="$t('sip.mediaServerEdit.998534-6')"
+                      clearable></el-input>
+          </el-form-item>
+          <el-form-item>
+            <div style="float: right; font-size: 28px">
+              <el-button @click="close">{{ $t('cancel') }}</el-button>
+              <el-button type="success" @click="checkServer"
+                         :loading="btnLoading">{{ $t('notify.template.index.333542-7') }}
+              </el-button>
+              <el-button type="primary" v-if="currentStep === 1 && serverCheck === 1"
+                         @click="next">{{ $t('next') }}
+              </el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+        <el-row :gutter="24">
+          <el-col :span="12">
+            <el-form v-if="currentStep === 2 || currentStep === 3" ref="mediaServerForm1" :rules="rules"
+                     :disabled="!editFlag" :model="mediaServerForm" label-width="140px">
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-7')" prop="serverId">
+                <el-input v-model="mediaServerForm.serverId" :placeholder="$t('sip.mediaServerEdit.998534-7')"
+                          clearable></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-8')" prop="ip">
+                <el-input v-if="currentStep === 2" v-model="mediaServerForm.ip" disabled></el-input>
+                <el-input v-if="currentStep === 3" v-model="mediaServerForm.ip"></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-9')" prop="protocol">
+                <el-select v-model="mediaServerForm.protocol" style="width: 100%">
+                  <el-option key="http" label="http" value="http"></el-option>
+                  <el-option key="https" label="https" value="https"></el-option>
+                  <el-option key="ws" label="ws" value="ws"></el-option>
+                  <el-option key="rtmp" label="rtmp" value="rtmp"></el-option>
+                  <el-option key="rtsp" label="rtsp" value="rtsp"></el-option>
+                </el-select>
+              </el-form-item>
+              <el-form-item label="HookUrl" prop="hookurl">
+                <el-input v-model="mediaServerForm.hookurl" placeholder="HookUrl" clearable></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-10')" prop="portHttp">
+                <el-input v-if="currentStep === 2" v-model="mediaServerForm.portHttp" disabled></el-input>
+                <el-input v-if="currentStep === 3" v-model="mediaServerForm.portHttp"></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-11')" prop="portHttps">
+                <el-input v-model="mediaServerForm.portHttps" placeholder="Https端口" clearable></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-12')" prop="portRtsp">
+                <el-input v-model="mediaServerForm.portRtsp" :placeholder="$t('sip.mediaServerEdit.998534-12')"
+                          clearable></el-input>
+              </el-form-item>
+            </el-form>
+          </el-col>
+          <el-col :span="12">
+            <el-form v-if="currentStep === 2 || currentStep === 3" ref="mediaServerForm2" :rules="rules"
+                     :disabled="!editFlag" :model="mediaServerForm" label-width="180px">
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-13')" prop="secret">
+                <el-input v-if="currentStep === 2" v-model="mediaServerForm.secret" disabled></el-input>
+                <el-input v-if="currentStep === 3" v-model="mediaServerForm.secret"></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-14')" prop="domain">
+                <el-input v-model="mediaServerForm.domain" :placeholder="$t('sip.mediaServerEdit.998534-14')"
+                          clearable></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-15')">
+                <el-switch v-model="mediaServerForm.autoConfig"></el-switch>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-16')">
+                <el-switch :active-text="$t('sip.mediaServerEdit.998534-17')"
+                           v-model="mediaServerForm.rtpEnable" :inactive-text="$t('sip.mediaServerEdit.998534-18')"
+                           @change="portRangeChange"></el-switch>
+              </el-form-item>
+              <el-form-item v-if="!mediaServerForm.rtpEnable" :label="$t('sip.mediaServerEdit.998534-19')"
+                            prop="rtpProxyPort">
+                <el-input v-model.number="mediaServerForm.rtpProxyPort" clearable></el-input>
+              </el-form-item>
+              <el-form-item v-if="mediaServerForm.rtpEnable" :label="$t('sip.mediaServerEdit.998534-19')">
+                <el-input v-model="rtpPortRange1" :placeholder="$t('sip.mediaServerEdit.998534-20')"
+                          clearable prop="rtpPortRange1" style="width: 100px" @change="portRangeChange"></el-input>
+                <el-input v-model="rtpPortRange2" :placeholder="$t('sip.mediaServerEdit.998534-21')"
+                          clearable prop="rtpPortRange2" style="width: 100px" @change="portRangeChange"></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-22')" prop="portRtmp">
+                <el-input v-model="mediaServerForm.portRtmp" :placeholder="$t('sip.mediaServerEdit.998534-22')"
+                          clearable></el-input>
+              </el-form-item>
+              <el-form-item :label="$t('sip.mediaServerEdit.998534-23')" prop="recordPort">
+                <el-input v-model.number="mediaServerForm.recordPort" :disabled="!editFlag">
+                  <el-button v-if="mediaServerForm.recordPort > 0" class="el-icon-check" slot="append" type="primary"
+                             @click="checkRecordServer"></el-button>
+                </el-input>
+                <i v-if="recordServerCheck === 1" class="el-icon-success"
+                   style="color: #3caf36; position: absolute; top: 14px"></i>
+                <i v-if="recordServerCheck === 2" class="el-icon-loading"
+                   style="color: #3caf36; position: absolute; top: 14px"></i>
+                <i v-if="recordServerCheck === -1" class="el-icon-error"
+                   style="color: #c80000; position: absolute; top: 14px"></i>
+              </el-form-item>
+              <el-form-item>
+                <div style="float: right">
+                  <el-button type="primary" @click="onSubmit" v-if="editFlag">{{ $t('submit') }}</el-button>
+                  <el-button @click="close" v-if="editFlag">{{ $t('close') }}</el-button>
+                </div>
+              </el-form-item>
+            </el-form>
+          </el-col>
+        </el-row>
+      </div>
+    </el-dialog>
+    <!-- 选择用户 -->
+    <user-list ref="userList" @userEvent="getUserData($event)" />
+  </div>
+</template>
+
+<script>
+import { addmediaServer, checkmediaServer, updatemediaServer } from '@/api/pms/video/mediaServer';
+import userList from './user-list.vue';
+
+export default {
+  name: 'MediaServerEdit',
+  components: { userList },
+  props: {
+    editFlag: {
+      type: Boolean,
+      default: false,
+
+    },
+  },
+  data() {
+    const isValidIp = (rule, value, callback) => {
+      // 校验IP是否符合规则
+      var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/;
+      if (!reg.test(value)) {
+        return callback(new Error(this.$t('sip.mediaServerEdit.998534-24')));
+      } else {
+        callback();
+      }
+      return true;
+    };
+    const isValidPort = (rule, value, callback) => {
+      // 校验IP是否符合规则
+      var reg = /^(([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5]))$/;
+      if (!reg.test(value)) {
+        return callback(new Error(this.$t('sip.mediaServerEdit.998534-25')));
+      } else {
+        callback();
+      }
+      return true;
+    };
+    return {
+      tempTenantId: '',
+      tempTenantName: '',
+      btnLoading: false,
+      dialogWidth: '',
+      defaultWidth: 1000,
+      listChangeCallback: null,
+      showDialog: false,
+      isLoging: false,
+      dialogLoading: false,
+      currentStep: 1,
+      platformList: [],
+      serverCheck: 0,
+      recordServerCheck: 0,
+      mediaServerForm: {
+        serverId: '',
+        ip: '',
+        domain: '',
+        productId: '',
+        productName: '',
+        tenantId: '',
+        tenantName: '',
+        autoConfig: true,
+        hookurl: '',
+        secret: '',
+        portHttp: '',
+        portHttps: '',
+        recordPort: '',
+        portRtmp: '',
+        portRtsp: '',
+        rtpEnable: true,
+        rtpPortRange: '',
+        rtpProxyPort: '',
+      },
+      rtpPortRange1: 30000,
+      rtpPortRange2: 30100,
+      rules: {
+        ip: [{ required: true, validator: isValidIp, message: this.$t('sip.mediaServerEdit.998534-24'), trigger: 'blur' }],
+        portHttp: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        portHttps: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        recordPort: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        portRtmp: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        portRtsp: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        rtpPortRange1: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        rtpPortRange2: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        rtpProxyPort: [{ required: true, validator: isValidPort, message: this.$t('sip.mediaServerEdit.998534-25'), trigger: 'blur' }],
+        secret: [{ required: true, message: '请输入secret', trigger: 'blur' }],
+      },
+    };
+  },
+  computed: {},
+  created() {
+    this.setDialogWidth();
+  },
+  methods: {
+    setDialogWidth() {
+      let val = document.body.clientWidth;
+      if (val < this.defaultWidth) {
+        this.dialogWidth = '100%';
+      } else {
+        this.dialogWidth = this.defaultWidth + 'px';
+      }
+    },
+    openDialog: function (param, callback) {
+      this.showDialog = true;
+      this.listChangeCallback = callback;
+      if (param != null) {
+        if (param.autoConfig === 1) {
+          param.autoConfig = param.autoConfig === 1;
+        } else if (param.autoConfig === 0) {
+          param.autoConfig = param.autoConfig === 1;
+        }
+
+        if (param.rtpEnable === 1) {
+          param.rtpEnable = param.rtpEnable === 1;
+        } else if (param.rtpEnable === 0) {
+          param.rtpEnable = param.rtpEnable === 1;
+        }
+
+        this.mediaServerForm = param;
+        this.currentStep = 3;
+        if (param.rtpPortRange) {
+          let rtpPortRange = this.mediaServerForm.rtpPortRange.split(',');
+          if (rtpPortRange.length > 0) {
+            this.rtpPortRange1 = rtpPortRange[0];
+            this.rtpPortRange2 = rtpPortRange[1];
+          }
+        }
+      }
+    },
+    checkServer: function () {
+      this.$refs.mediaServerForm.validate((valid) => {
+        if (valid) {
+          this.btnLoading = true;
+          this.serverCheck = 0;
+          let query = {
+            ip: this.mediaServerForm.ip,
+            port: this.mediaServerForm.portHttp,
+            secret: this.mediaServerForm.secret,
+          };
+          checkmediaServer(query)
+              .then((response) => {
+                this.btnLoading = false;
+                if (response.data != null) {
+                  this.mediaServerForm = response.data;
+                  this.mediaServerForm.autoConfig = true;
+                  this.mediaServerForm.rtpEnable = true;
+                  this.mediaServerForm.protocol = 'http';
+                  this.mediaServerForm.domain = 'yanfan.com';
+                  this.mediaServerForm.enabled = 1;
+                  this.mediaServerForm.tenantId = this.tempTenantId;
+                  this.mediaServerForm.tenantName = this.tempTenantName;
+                  this.mediaServerForm.serverId = 'yanfan';
+                  this.mediaServerForm.hookurl = 'java:8080';
+                  this.mediaServerForm.portHttps = 8443;
+                  this.mediaServerForm.recordPort = 18081;
+                  this.mediaServerForm.portRtmp = 1935;
+                  this.mediaServerForm.portRtsp = 554;
+                  this.mediaServerForm.rtpProxyPort = '';
+                  this.rtpPortRange1 = 30000;
+                  this.rtpPortRange2 = 30100;
+                  this.serverCheck = 1;
+                  this.$modal.alertSuccess(this.$t('sip.mediaServerEdit.998534-26'));
+                } else {
+                  this.serverCheck = -1;
+                  this.$modal.alertError(this.$t('sip.mediaServerEdit.998534-27'));
+                }
+              })
+              .catch((error) => {
+                this.serverCheck = -1;
+                this.$message({
+                  showClose: true,
+                  message: error,
+                  type: 'error',
+                });
+              });
+        }
+      });
+    },
+    next: function () {
+      this.currentStep = 2;
+      this.defaultWidth = 900;
+      this.setDialogWidth();
+    },
+    checkRecordServer: function () {
+      let that = this;
+      that.recordServerCheck = 2;
+      if (that.mediaServerForm.recordPort <= 0 || that.mediaServerForm.recordPort > 65535) {
+        that.recordServerCheck = -1;
+        that.$message({
+          showClose: true,
+          message: this.$t('sip.mediaServerEdit.998534-28'),
+          type: 'error',
+        });
+        return;
+      }
+    },
+    onSubmit: function () {
+      this.dialogLoading = true;
+      this.mediaServerForm.rtpEnable = this.mediaServerForm.rtpEnable ? 1 : 0;
+      this.mediaServerForm.autoConfig = this.mediaServerForm.autoConfig ? 1 : 0;
+      if (this.mediaServerForm.id != null) {
+        updatemediaServer(this.mediaServerForm).then((response) => {
+          this.$modal.msgSuccess(this.$t('updateSuccess'));
+          this.showDialog = false;
+        });
+      } else {
+        this.portRangeChange();
+        addmediaServer(this.mediaServerForm).then((response) => {
+          this.$modal.msgSuccess(this.$t('addSuccess'));
+          this.showDialog = false;
+        });
+      }
+      this.$parent.getServerList();
+      this.$parent.delay();
+    },
+    close: function () {
+      this.showDialog = false;
+      this.dialogLoading = false;
+      this.mediaServerForm = {
+        serverId: '',
+        ip: '',
+        domain: '',
+        autoConfig: true,
+        hookurl: '',
+        secret: '',
+        portHttp: '',
+        portHttps: '',
+        recordPort: '',
+        portRtmp: '',
+        portRtsp: '',
+        rtpEnable: true,
+        rtpPortRange: '',
+        rtpProxyPort: '',
+      };
+      this.rtpPortRange1 = 30000;
+      this.rtpPortRange2 = 30100;
+      this.listChangeCallback = null;
+      this.currentStep = 1;
+    },
+    portRangeChange: function () {
+      if (this.mediaServerForm.rtpEnable) {
+        this.mediaServerForm.rtpPortRange = this.rtpPortRange1 + ',' + this.rtpPortRange2;
+        console.log(this.mediaServerForm.rtpPortRange);
+      }
+    },
+    selectUser() {
+      this.$refs.userList.open = true;
+      this.$refs.userList.getList();
+    },
+    getUserData(user) {
+      this.tempTenantId = user.userId;
+      this.tempTenantName = user.userName;
+      this.mediaServerForm.tenantId = user.userId;
+      this.mediaServerForm.tenantName = user.userName;
+    },
+  },
+};
+</script>

+ 178 - 0
src/views/pms/video_center/sip/mediaServer.vue

@@ -0,0 +1,178 @@
+<template>
+    <div id="mediaServer" class="iot-sip-mediaServer">
+        <el-card style="padding-bottom: 100px" v-loading="loading">
+            <div class="mb20">
+                <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="add" v-hasPermi="['iot:video:add']">{{ $t('sip.mediaServer.998535-0') }}</el-button>
+                <el-button type="warning" plain icon="el-icon-refresh" size="mini" @click="getServerList">{{ $t('refresh') }}</el-button>
+            </div>
+            <el-row :gutter="30">
+                <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="6" v-for="(item, index) in mediaServerList" :key="index" style="margin-bottom: 30px; text-align: center">
+                    <div class="card-box-outer">
+                        <el-card shadow="always" class="card-item">
+                            <el-row :gutter="10">
+                                <el-col :span="15">
+                                    <el-descriptions :column="1" size="mini" style="white-space: nowrap">
+                                        <el-descriptions-item :label="$t('sip.mediaServer.998535-1')">
+                                            {{ item.serverId }}
+                                        </el-descriptions-item>
+                                        <el-descriptions-item :label="$t('sip.mediaServer.998535-2')">
+                                            {{ item.ip }}
+                                        </el-descriptions-item>
+                                        <el-descriptions-item :label="$t('sip.mediaServer.998535-3')">
+                                            {{ item.protocol }}
+                                        </el-descriptions-item>
+                                        <el-descriptions-item :label="$t('sip.mediaServer.998535-4')">
+                                            {{ parseTime(item.createTime, '{y}-{m}-{d}') }}
+                                        </el-descriptions-item>
+                                    </el-descriptions>
+                                </el-col>
+                                <el-col :span="8">
+                                    <div style="margin-top: 10px">
+                                        <el-image :src="require('@/assets/images/zlm-logo.png')" fit="fit"></el-image>
+                                    </div>
+                                </el-col>
+                            </el-row>
+                            <el-button-group style="margin-top: 10px">
+                                <el-button class="delete-btn" style="padding: 5px 10px" icon="el-icon-delete" v-hasPermi="['iot:video:remove']" @click="del(item)">{{ $t('del') }}</el-button>
+                                <el-button class="detail-btn" size="mini" style="padding: 5px 15px; margin: 0 10px" icon="el-icon-view" @click="view(item)" v-hasPermi="['iot:video:query']">{{ $t('look') }}</el-button>
+                                <el-button v-if="!istrue" class="success-btn" size="mini" style="padding: 5px 15px" icon="el-icon-odometer" v-hasPermi="['iot:video:edit']" @click.native.prevent="edit(item)">
+                                    {{ $t('edit') }}
+                                </el-button>
+                                <el-button v-else class="success-btn" size="mini" style="padding: 5px 15px" icon="el-icon-odometer" :loading="true" disabled>{{ $t('sip.mediaServer.998535-5') }}</el-button>
+                            </el-button-group>
+                        </el-card>
+                    </div>
+                </el-col>
+            </el-row>
+            <el-empty :description="$t('sip.mediaServer.998535-6')" v-if="total == 0"></el-empty>
+            <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" :pageSizes="[12, 24, 36, 60]" @pagination="getServerList" />
+        </el-card>
+
+        <mediaServerEdit ref="mediaServerEdit" :edit-flag="editFlag"></mediaServerEdit>
+    </div>
+</template>
+
+<script>
+import mediaServerEdit from './mediaServer-edit.vue';
+import { delmediaServer, listmediaServer } from '@/api/pms/video/mediaServer';
+export default {
+    name: 'MediaServer',
+    components: {
+        mediaServerEdit,
+    },
+    data() {
+        return {
+            // 遮罩层
+            loading: true,
+            editFlag: false,
+            istrue: false,
+            mediaServerList: [], //设备列表
+            winHeight: window.innerHeight - 200,
+            updateLooper: false,
+            currentPage: 1,
+            count: 15,
+            num: this.getNumberByWidth(),
+            total: 0,
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+            },
+        };
+    },
+    computed: {},
+    mounted() {
+        this.initData();
+        //this.updateLooper = setInterval(this.initData, 2000);
+    },
+    destroyed() {
+        clearTimeout(this.updateLooper);
+    },
+    methods: {
+        initData: function () {
+            this.getServerList();
+        },
+        pageNumChange: function (val) {
+            this.queryParams.pageNum = val;
+            this.getServerList();
+        },
+        pageSizeChange: function (val) {
+            this.queryParams.pageSize = val;
+            this.getServerList();
+        },
+        getServerList: function () {
+            this.loading = true;
+            listmediaServer(this.queryParams).then((response) => {
+                this.mediaServerList = response.rows;
+                this.total = response.total;
+                this.loading = false;
+            });
+        },
+        add: function () {
+            this.$refs.mediaServerEdit.openDialog(null, this.initData, this.editFlag);
+            this.editFlag = true;
+        },
+        view: function (row) {
+            this.$refs.mediaServerEdit.openDialog(row, this.initData, this.editFlag);
+            this.editFlag = false;
+        },
+        edit: function (row) {
+            this.$refs.mediaServerEdit.openDialog(row, this.initData, this.editFlag);
+            this.editFlag = true;
+        },
+        delay: function () {
+            let n = 5;
+            // 定义定时器time
+            const time = setInterval(() => {
+                this.istrue = true;
+                n--;
+                // 如果n<0,清除定时器,禁用状态取消,文字提示为空(不显示)
+                if (n < 0) {
+                    this.istrue = false;
+                    clearInterval(time);
+                }
+            }, 1000);
+        },
+        del: function (row) {
+            const ids = row.id || this.ids;
+            this.$modal
+                .confirm(this.$t('sip.mediaServer.998535-7', [ids]))
+                .then(function () {
+                    delmediaServer(ids);
+                })
+                .then(() => {
+                    this.getServerList();
+                    this.$modal.msgSuccess(this.$t('delSuccess'));
+                })
+                .catch(() => {});
+        },
+        getNumberByWidth() {
+            let candidateNums = [1, 2, 3, 4, 6, 8, 12, 24];
+            let clientWidth = window.innerWidth - 30;
+            let interval = 20;
+            let itemWidth = 360;
+            let num = (clientWidth + interval) / (itemWidth + interval);
+            let result = Math.ceil(24 / num);
+            let resultVal = 24;
+            for (let i = 0; i < candidateNums.length; i++) {
+                let value = candidateNums[i];
+                if (i + 1 >= candidateNums.length) {
+                    return 24;
+                }
+                if (value <= result && candidateNums[i + 1] > result) {
+                    return value;
+                }
+            }
+            return resultVal;
+        },
+    },
+};
+</script>
+
+<style lang="scss" scoped>
+.card-item {
+    border-radius: 10px;
+    padding: 15px 0px;
+    background-image: linear-gradient(#e9f1fc, #fefefe);
+    border: 0;
+}
+</style>

+ 141 - 0
src/views/pms/video_center/sip/product-list.vue

@@ -0,0 +1,141 @@
+<template>
+    <el-dialog :title="$t('sip.product-list.998536-0')" :visible.sync="open" append-to-body width="600px">
+        <div style="margin-top: -55px">
+            <el-divider style="margin-top: -30px"></el-divider>
+            <el-form ref="queryForm" :inline="true" :model="queryParams" label-width="68px">
+                <el-form-item :label="$t('product.index.091251-0')" prop="productName">
+                    <el-input v-model="queryParams.productName" :placeholder="$t('product.index.091251-1')" clearable size="small" @keyup.enter.native="handleQuery" />
+                </el-form-item>
+                <el-form-item>
+                    <el-button icon="el-icon-search" size="mini" type="primary" @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" ref="singleTable" v-loading="loading" :data="productList" @row-click="rowClick" highlight-current-row size="mini">
+                <el-table-column :label="$t('select')" align="center" width="50">
+                    <template slot-scope="scope">
+                        <input :checked="scope.row.isSelect" name="product" type="radio" />
+                    </template>
+                </el-table-column>
+                <el-table-column :label="$t('product.index.091251-0')" align="center" prop="productName" />
+                <el-table-column :label="$t('product.index.091251-2')" align="center" prop="categoryName" />
+                <el-table-column :label="$t('sip.product-list.998536-1')" align="center" prop="tenantName" />
+                <el-table-column :label="$t('sip.product-list.998536-2')" 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('sip.mediaServer.998535-4')" 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" :limit.sync="queryParams.pageSize" :page.sync="queryParams.pageNum" :total="total" @pagination="getList" />
+        </div>
+        <div slot="footer" class="dialog-footer">
+            <el-button type="primary" @click="confirmSelectProduct">{{ $t('confirm') }}</el-button>
+            <el-button type="info" @click="closeDialog">{{ $t('close') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { listProduct } from '@/api/pms/video/product';
+
+export default {
+    name: 'SipProductList',
+    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: 3, // 监控设备
+                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>

+ 159 - 0
src/views/pms/video_center/sip/sipconfig.vue

@@ -0,0 +1,159 @@
+<template>
+  <div style="padding:6px;">
+    <el-form ref="form" :model="form" label-width="100px">
+      <el-row :gutter="100">
+        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+<!--          <el-form-item :label="$t('sip.sipConfig.998537-0')" prop="isdefault">
+            <el-switch v-model="form.isdefault" :active-value="1" :inactive-value="0" />
+          </el-form-item>-->
+          <el-form-item :label="$t('sip.sipConfig.998537-1')" prop="ip">
+            <el-input v-model="form.ip" disabled />
+          </el-form-item>
+          <el-form-item :label="$t('sip.sipConfig.998537-2')" prop="domain">
+            <el-input v-model="form.domain" />
+          </el-form-item>
+          <el-form-item :label="$t('sip.sipConfig.998537-3')" prop="password">
+            <el-input v-model="form.password" :placeholder="$t('sip.sipConfig.998537-4')" />
+          </el-form-item>
+        </el-col>
+        <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
+          <el-form-item :label="$t('sip.sipConfig.998537-5')">
+            <el-input v-model="accessWay" disabled>
+            </el-input>
+          </el-form-item>
+          <el-form-item :label="$t('sip.sipConfig.998537-6')" prop="port">
+            <el-input v-model="form.port" type="number" disabled />
+          </el-form-item>
+          <el-form-item :label="$t('sip.sipConfig.998537-7')" prop="serverSipid">
+            <el-input v-model="form.serverSipid" />
+          </el-form-item>
+        </el-col>
+        <el-col :xs="23" :sm="23" :md="23" :lg="23" :xl="15">
+          <el-form-item style="text-align:center;margin-top:20px;">
+            <el-button v-show="form.id && productInfo.status != 2" v-hasPermi="['iot:video:edit']" type="primary"
+                       @click="submitForm">{{ $t('update') }}
+            </el-button>
+            <el-button v-show="!form.id && productInfo.status != 2" v-hasPermi="['iot:video:add']" type="primary"
+                       @click="submitForm">{{ $t('add') }}
+            </el-button>
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+  </div>
+</template>
+
+<style>
+.specsColor {
+  background-color: #fcfcfc;
+}
+</style>
+
+<script>
+import {addSipconfig, getSipconfig, updateSipconfig,} from '@/api/pms/video/sipConfig';
+
+export default {
+  name: 'ConfigSip',
+  props: {
+    product: {
+      type: Object,
+      default: null,
+    },
+  },
+  data() {
+    return {
+      // 接入方式
+      accessWay: '国标GB28181',
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 总条数
+      total: 0,
+      // sip系统配置表格数据
+      sipconfigList: [],
+      // 弹出层标题
+      title: '',
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        productId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        domain: [{
+          required: true,
+          message: this.$t('sip.sipConfig.998537-8'),
+          trigger: 'blur',
+        }],
+        serverSipid: [{
+          required: true,
+          message: this.$t('sip.sipConfig.998537-9'),
+          trigger: 'blur',
+        }],
+        password: [{
+          required: true,
+          message: this.$t('sip.sipConfig.998537-10'),
+          trigger: 'blur',
+        }],
+      },
+    };
+  },
+  watch: {
+    // 获取到父组件传递的productId后,刷新列表
+    product: function (newVal, oldVal) {
+      this.productInfo = newVal;
+      if (this.productInfo && this.productInfo.productId != 0) {
+        // 表单没有数据则获取默认配置
+        if (!this.form.id) {
+          this.getSipconfig();
+        }
+      }
+    },
+  },
+  created() {
+    this.productInfo = this.product;
+    if (this.productInfo && this.productInfo.productId != 0) {
+      this.getSipconfig();
+    }
+  },
+  methods: {
+    /** 获取产品下第一条SIP配置 */
+    getSipconfig() {
+      getSipconfig(this.productInfo.productId).then(response => {
+        this.form = response.data;
+          // this.submitForm();
+        /*if (isDefault) {
+          this.submitForm();
+        }*/
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.form.productId = this.product.productId;
+          /*if (this.form.isdefault == null) {
+            this.form.isdefault = 0;
+          }*/
+          if (this.form.id != null) {
+            updateSipconfig(this.form).then(response => {
+              this.$modal.msgSuccess('修改成功');
+            });
+          } else {
+            addSipconfig(this.form).then(response => {
+              this.$modal.msgSuccess('新增成功');
+              this.getSipconfig(false);
+            });
+          }
+        }
+      });
+    },
+  },
+};
+</script>

+ 120 - 0
src/views/pms/video_center/sip/sipidGen.vue

@@ -0,0 +1,120 @@
+<template>
+  <el-dialog :title="title" :visible.sync="open" append-to-body width="400px">
+    <el-form ref="createForm" :model="createForm" label-width="100px">
+      <el-form-item :label="$t('sip.sipidGen.998538-0')">
+        <el-cascader v-model="createForm.city" :options="cityOptions" change-on-select @change="changeProvince">
+        </el-cascader>
+      </el-form-item>
+      <el-form-item :label="$t('sip.index.998533-9')" prop="deviceType">
+        <el-select v-model="createForm.deviceType" :placeholder="$t('sip.index.998533-14')">
+          <el-option v-for="dict in dict.type.video_type" :key="dict.value" :label="dict.label"
+                     :value="dict.value"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('sip.index.998533-15')" prop="channelType">
+        <el-select v-model="createForm.channelType" :placeholder="$t('sip.index.998533-16')">
+          <el-option v-for="dict in dict.type.channel_type" :key="dict.value" :label="dict.label"
+                     :value="dict.value"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item :label="$t('sip.index.998533-20')" prop="createNum">
+        <el-input-number v-model="createForm.createNum" :placeholder="$t('sip.index.998533-19')"
+                         controls-position="right" style="width:220px;" type="number"/>
+      </el-form-item>
+    </el-form>
+    <div slot="footer" class="dialog-footer">
+      <el-button type="primary" @click="submitForm">{{ $t('sip.index.998533-21') }}</el-button>
+      <el-button @click="closeDialog">{{ $t('cancel') }}</el-button>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+import {
+  regionData,
+  CodeToText
+} from 'element-china-area-data'
+
+import { addChannel } from "@/api/pms/video/channel";
+
+export default {
+  name: "SipidDialog",
+  dicts: ['video_type', 'channel_type'],
+  props: {
+    product: {
+      type: Object,
+      default: null
+    }
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      title: this.$t('sip.sipidGen.998538-1'),
+      // 总条数
+      total: 0,
+      // 打开选择产品对话框
+      open: false,
+      devsipid: "",
+      createForm: {
+        city: '',
+        deviceType: '',
+        channelType: '',
+        createNum: 1,
+      },
+      // 城市
+      cityOptions: regionData,
+      city: '',
+      cityCode: '',
+    };
+  },
+  created() {
+
+  },
+  methods: {
+    /** 行政区划改变 **/
+    changeProvince(data) {
+      if (data && data[0] != null && data[1] != null && data[2] != null) {
+        const str = CodeToText[data[0]] + '/' + CodeToText[data[1]] + '/' + CodeToText[data[2]];
+        this.createForm.citycode = str;
+      }
+    },
+    /** 提交按钮 */
+    submitForm() {
+      if (this.createForm.createNum < 1) {
+        this.$modal.alertError(this.$t('sip.index.998533-42'));
+        return;
+      }
+      this.createForm.productId = this.product.productId;
+      this.createForm.productName = this.product.productName;
+      this.createForm.tenantId = this.product.tenantId;
+      this.createForm.tenantName = this.product.tenantName;
+      this.createForm.deviceSipId = this.createForm.city[2] + "0000" + this.createForm.deviceType + "0"
+      this.createForm.channelSipId = this.createForm.city[2] + "0000" + this.createForm.channelType + "0"
+      if (this.createForm.deviceType !== "" && this.createForm.channelType !== "" && this.createForm.city.length === 3) {
+        addChannel(this.createForm.createNum, this.createForm).then(response => {
+          this.$modal.msgSuccess(this.$t('sip.sipidGen.998538-2'));
+          this.devsipid = response.data;
+          this.confirmSelectProduct();
+        });
+      } else {
+        this.$message({
+          type: 'error',
+          message: this.$t('sip.sipidGen.998538-3')
+        });
+      }
+
+    },
+    confirmSelectProduct() {
+      this.open = false;
+      this.$emit('addGenEvent', this.devsipid);
+    },
+    /**关闭对话框 */
+    closeDialog() {
+      this.open = false;
+    }
+  }
+}
+</script>
+
+<style scoped></style>

+ 219 - 0
src/views/pms/video_center/sip/splitview.vue

@@ -0,0 +1,219 @@
+<template>
+    <div style="height: 100%">
+        <el-card id="devicePosition" style="height: calc(100% - 40px)" :body-style="{ padding: '0px' }">
+            <el-container v-loading="loading" style="height: 100%" :element-loading-text="$t('sip.splitview.998531-0')">
+                <el-aside width="250px" style="background-color: #ffffff">
+                    <DeviceTree :clickEvent="clickEvent"></DeviceTree>
+                </el-aside>
+                <el-main style="padding: 0">
+                    <div height="5vh" style="text-align: left; font-size: 17px; line-height: 5vh; margin-bottom: 10px">
+                        {{ $t('sip.splitview.998531-1') }}
+                        <el-button type="success" style="margin-left: 10px" :class="{ active: spilt == 1 }" @click="spilt = 1" icon="el-icon-full-screen" plain size="mini">{{ $t('sip.splitview.998531-2') }}</el-button>
+                        <el-button type="info" style="margin-left: 10px" :class="{ active: spilt == 4 }" @click="spilt = 4" icon="el-icon-menu" plain size="mini">{{ $t('sip.splitview.998531-3') }}</el-button>
+                        <el-button type="warning" style="margin-left: 10px" :class="{ active: spilt == 9 }" @click="spilt = 9" icon="el-icon-s-grid" plain size="mini">{{ $t('sip.splitview.998531-4') }}</el-button>
+                    </div>
+                    <div style="height: 85vh; display: flex; flex-wrap: wrap">
+                        <div v-for="i in spilt" :key="i" class="play-box" :style="liveStyle" :class="{ redborder: playerIdx == i - 1 }" @click="playerIdx = i - 1">
+                            <div v-if="!videoUrl[i - 1]" style="color: #ffffff; font-size: 30px; font-weight: bold">{{ i }}</div>
+                            <player ref="player" v-else :videoUrl="videoUrl[i - 1]" fluent autoplay @screenshot="shot" class="player-wrap" @destroy="destroy" />
+                        </div>
+                    </div>
+                </el-main>
+            </el-container>
+        </el-card>
+    </div>
+</template>
+
+<script>
+import player from './components/player/jessibuca.vue';
+import DeviceTree from './components/player/DeviceTree.vue';
+import { startPlay } from '@/api/pms/video/channel';
+
+export default {
+    name: 'SplitView',
+    components: {
+        player,
+        DeviceTree,
+    },
+    data() {
+        return {
+            videoUrl: [''],
+            spilt: 1, //分屏
+            playerIdx: 0, //激活播放器
+            updateLooper: 0, //数据刷新轮训标志
+            count: 15,
+            total: 0,
+            loading: false,
+        };
+    },
+    mounted() {},
+    created() {
+        this.checkPlayByParam();
+    },
+
+    computed: {
+        liveStyle() {
+            let style = { width: '81%', height: '99%' };
+            switch (this.spilt) {
+                case 4:
+                    style = { width: '40%', height: '49%' };
+                    break;
+                case 9:
+                    style = { width: '27%', height: '32%' };
+                    break;
+            }
+            this.$nextTick(() => {
+                for (let i = 0; i < this.spilt; i++) {
+                    const player = this.$refs.player;
+                    player && player[i] && player[i].updatePlayerDomSize();
+                }
+            });
+            return style;
+        },
+    },
+    watch: {
+        spilt(newValue) {
+            console.log('切换画幅;' + newValue);
+            let that = this;
+            for (let i = 1; i <= newValue; i++) {
+                if (!that.$refs['player' + i]) {
+                    continue;
+                }
+                this.$nextTick(() => {
+                    if (that.$refs['player' + i] instanceof Array) {
+                        that.$refs['player' + i][0].resize();
+                    } else {
+                        that.$refs['player' + i].resize();
+                    }
+                });
+            }
+            window.localStorage.setItem('split', newValue);
+        },
+        '$route.fullPath': 'checkPlayByParam',
+    },
+    destroyed() {
+        clearTimeout(this.updateLooper);
+    },
+    methods: {
+        destroy(idx) {
+            console.log(idx);
+            this.clear(idx.substring(idx.length - 1));
+        },
+        clickEvent: function (data) {
+            if (data.channelSipId) {
+                this.sendDevicePush(data);
+            }
+        },
+        //通知设备上传媒体流
+        sendDevicePush: function (itemData) {
+            this.save(itemData);
+            let deviceId = itemData.deviceSipId;
+            let channelId = itemData.channelSipId;
+            console.log('通知设备推流1:' + deviceId + ' : ' + channelId);
+            let idxTmp = this.playerIdx;
+            let that = this;
+            this.loading = true;
+            startPlay(deviceId, channelId)
+                .then((response) => {
+                    console.log('开始播放:' + this.deviceId + ' : ' + this.channelId);
+                    console.log('流媒体信息:' + response.data);
+                    let res = response.data;
+                    console.log('playurl:' + res.playurl);
+                    console.log('protocol:' + window.location.protocol);
+                    if (window.location.protocol === 'http:') {
+                        itemData.playUrl = res.ws_flv;
+                    } else {
+                        itemData.playUrl = res.wss_flv;
+                    }
+                    itemData.streamId = res.streamId;
+                    that.setPlayUrl(itemData.playUrl, idxTmp);
+                })
+                .finally(() => {
+                    that.loading = false;
+                });
+        },
+        setPlayUrl(url, idx) {
+            this.$set(this.videoUrl, idx, url);
+            let _this = this;
+            setTimeout(() => {
+                window.localStorage.setItem('videoUrl', JSON.stringify(_this.videoUrl));
+            }, 100);
+        },
+        checkPlayByParam() {
+            let { deviceId, channelId } = this.$route.query;
+            if (deviceId && channelId) {
+                this.sendDevicePush({ deviceId, channelId });
+            }
+        },
+        shot(e) {
+            var base64ToBlob = function (code) {
+                let parts = code.split(';base64,');
+                let contentType = parts[0].split(':')[1];
+                let raw = window.atob(parts[1]);
+                let rawLength = raw.length;
+                let uInt8Array = new Uint8Array(rawLength);
+                for (let i = 0; i < rawLength; ++i) {
+                    uInt8Array[i] = raw.charCodeAt(i);
+                }
+                return new Blob([uInt8Array], {
+                    type: contentType,
+                });
+            };
+            let aLink = document.createElement('a');
+            let blob = base64ToBlob(e); //new Blob([content]);
+            let evt = document.createEvent('HTMLEvents');
+            evt.initEvent('click', true, true); //initEvent 不加后两个参数在FF下会报错  事件类型,是否冒泡,是否阻止浏览器的默认行为
+            aLink.download = this.$t('sip.splitview.998531-5');
+            aLink.href = URL.createObjectURL(blob);
+            aLink.click();
+        },
+        save(item) {
+            let dataStr = window.localStorage.getItem('playData') || '[]';
+            let data = JSON.parse(dataStr);
+            data[this.playerIdx] = item;
+            window.localStorage.setItem('playData', JSON.stringify(data));
+        },
+        clear(idx) {
+            let dataStr = window.localStorage.getItem('playData') || '[]';
+            let data = JSON.parse(dataStr);
+            data[idx - 1] = null;
+            console.log(data);
+            window.localStorage.setItem('playData', JSON.stringify(data));
+        },
+    },
+};
+</script>
+<style>
+.btn {
+    margin: 0 10px;
+}
+
+.btn:hover {
+    color: #409eff;
+}
+
+.btn.active {
+    color: #409eff;
+}
+
+.redborder {
+    border: 2px solid red !important;
+}
+
+.play-box {
+    background-color: #000000;
+    border: 1px solid #505050;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-right: 10px;
+    position: relative;
+    border-radius: 5px;
+}
+
+.player-wrap {
+    position: absolute;
+    top: 0px;
+    height: 100% !important;
+}
+</style>

+ 172 - 0
src/views/pms/video_center/sip/user-list.vue

@@ -0,0 +1,172 @@
+<template>
+    <el-dialog :title="$t('sip.user-list.558539-0')" :visible.sync="open" width="800px">
+        <div style="margin-top: -50px">
+            <el-divider></el-divider>
+        </div>
+        <!--用户数据-->
+        <el-form ref="queryForm" :inline="true" :model="queryParams" :rules="rules" label-width="80px">
+            <el-form-item :label="$t('sip.user-list.558539-1')" prop="phonenumber">
+                <el-input
+                    type="text"
+                    :placeholder="$t('sip.user-list.558539-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('query') }}</el-button>
+            </el-form-item>
+        </el-form>
+
+        <el-table ref="user-table" :border="false" v-loading="loading" :data="userList" highlight-current-row size="mini" @current-change="handleCurrentChange">
+            <el-table-column :label="$t('select')" width="50" align="center">
+                <template slot-scope="scope">
+                    <input :checked="scope.row.isSelect" name="user" type="radio" />
+                </template>
+            </el-table-column>
+            <el-table-column :label="$t('sip.user-list.558539-2')" align="center" key="userId" prop="userId" width="120" />
+            <el-table-column :label="$t('sip.user-list.558539-3')" align="center" key="userName" prop="userName" />
+            <el-table-column :label="$t('sip.user-list.558539-4')" align="center" key="nickName" prop="nickName" />
+            <el-table-column :label="$t('sip.user-list.558539-1')" align="center" key="phonenumber" prop="phonenumber" width="120" />
+            <el-table-column :label="$t('sip.user-list.558539-5')" align="center" prop="createTime" width="160">
+                <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" />
+
+        <div slot="footer" class="dialog-footer">
+            <el-button type="primary" @click="confirmSelectUser">{{ $t('confirm') }}</el-button>
+            <el-button @click="closeSelectUser">{{ $t('close') }}</el-button>
+        </div>
+    </el-dialog>
+</template>
+
+<script>
+import { getSimpleUserList } from '@/api/system/user';
+
+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: {},
+            // 是否显示选择用户弹出层
+            open: false,
+            // 查询参数
+            queryParams: {
+                pageNum: 1,
+                pageSize: 10,
+                userName: undefined,
+                phonenumber: undefined,
+                status: 0,
+                deptId: undefined,
+            },
+            total: 0,
+            // 表单校验
+            rules: {
+                phonenumber: [
+                    {
+                        required: true,
+                        message: this.$t('sip.user-list.558539-6'),
+                        trigger: 'blur',
+                    },
+                    {
+                        min: 11,
+                        max: 11,
+                        message: this.$t('sip.user-list.558539-7'),
+                        trigger: 'blur',
+                    },
+                ],
+            },
+        };
+    },
+    created() {},
+    methods: {
+        /** 查询用户列表 */
+        getList(userId) {
+            this.loading = true;
+            getSimpleUserList(this.addDateRange(this.queryParams, this.dateRange)).then((response) => {
+                this.userList = response.rows.map((item) => {
+                    if (userId && userId == item.userId) {
+                        item.isSelect = true;
+                    }
+                    return item;
+                });
+                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;
+            }
+        },
+        confirmSelectUser() {
+            this.$emit('userEvent', this.user);
+            this.open = false;
+        },
+        // 关闭选择用户
+        closeSelectUser() {
+            this.open = false;
+            this.resetQuery();
+        },
+    },
+};
+</script>

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно