Bladeren bron

✨ feat(pms): 接入海康监控点目录树搜索,支持回车查询及清空重载

Zimo 1 week geleden
bovenliggende
commit
31d3247e88
2 gewijzigde bestanden met toevoegingen van 135 en 105 verwijderingen
  1. 8 0
      src/api/pms/hikvision/index.ts
  2. 127 105
      src/views/pms/video_center/haikang/index.vue

+ 8 - 0
src/api/pms/hikvision/index.ts

@@ -50,6 +50,14 @@ export interface HikvisionPtzControlRequest {
 }
 
 export const HikvisionApi = {
+  /** 按监控点名称搜索,返回匹配监控点及完整祖先目录,children 无需再懒加载。 */
+  searchTree: (keyword: string): Promise<HikvisionTreeNode[]> => {
+    return request.get({
+      url: '/vms/tree/search',
+      params: { keyword }
+    })
+  },
+
   getTreeNodes: (regionIndexCode: string): Promise<HikvisionTreeNode[]> => {
     return request.get({
       url: '/vms/tree',

+ 127 - 105
src/views/pms/video_center/haikang/index.vue

@@ -18,16 +18,21 @@ import {
 } from '@/utils/hikvisionH5Player'
 import { Loading, Location, Search, VideoCamera } from '@element-plus/icons-vue'
 import type { LoadFunction } from 'element-plus/es/components/tree/src/tree.type'
+import {IotDeviceApi} from "@/api/pms/device";
+import {IotOpeationFillApi} from "@/api/pms/iotopeationfill";
 
 defineOptions({ name: 'HikvisionVideoCenter' })
 
-const treeRef = ref()
 const layoutTriggerRef = ref<HTMLElement>()
 const playerContainerRef = ref<HTMLElement>()
 const keyword = ref('')
 const treeData = ref<HikvisionTreeNode[]>([])
 const expandedKeys = ref<string[]>([])
 const treeLoading = ref(false)
+// 搜索结果是完整树;普通目录仍按需加载。每次请求后重建树,避免复用旧的懒加载缓存。
+const treeSearchMode = ref(false)
+const treeVersion = ref(0)
+let treeRequestId = 0
 const playerContainerId = 'hikvision-preview-player'
 const previewLoadingWindowIndexes = ref<number[]>([])
 const previewWindowIndexes = ref<number[]>([])
@@ -560,22 +565,30 @@ const globalSoundTitle = computed(() =>
   soundWindowIndex.value === undefined ? '开启当前监控声音' : '关闭全部监控声音'
 )
 
-async function loadRootTree() {
+async function loadRootTree(searchKeyword = '', requestId = ++treeRequestId) {
   treeLoading.value = true
   try {
-    treeData.value = await HikvisionApi.getTreeNodes('-1')
+    const data = searchKeyword
+      ? await HikvisionApi.searchTree(searchKeyword)
+      : await HikvisionApi.getTreeNodes('-1')
+    // 新搜索或清空会使旧请求失效,防止慢搜索覆盖最新目录。
+    if (componentUnmounted || requestId !== treeRequestId) return
+    treeSearchMode.value = Boolean(searchKeyword)
+    treeData.value = data
     const root = treeData.value[0]
-    expandedKeys.value = root?.nodeType === 'region' ? [root.indexCode] : []
+    expandedKeys.value = !searchKeyword && root?.nodeType === 'region' ? [root.indexCode] : []
+    treeVersion.value += 1
   } catch {
+    if (componentUnmounted || requestId !== treeRequestId) return
     treeData.value = []
   } finally {
-    treeLoading.value = false
+    if (!componentUnmounted && requestId === treeRequestId) treeLoading.value = false
   }
 }
 
 const loadTreeNode: LoadFunction = async (node, resolve, reject) => {
   if (node.level === 0) {
-    resolve([])
+    resolve(treeData.value)
     return
   }
 
@@ -597,11 +610,6 @@ const loadTreeNode: LoadFunction = async (node, resolve, reject) => {
   }
 }
 
-function filterTreeNode(value: string, data: HikvisionTreeNode) {
-  if (!value.trim()) return true
-  return data.name.toLowerCase().includes(value.trim().toLowerCase())
-}
-
 function initPlayer(): Promise<HikvisionPlayerInstance> {
   if (player) return Promise.resolve(player)
   if (playerInitPromise) return playerInitPromise
@@ -628,99 +636,97 @@ function initPlayer(): Promise<HikvisionPlayerInstance> {
       })
 
       // 开发指南要求创建实例后先注册事件,再调用 JS_Play 等播放接口。
-      await Promise.resolve(
-        initializingPlayer.JS_SetWindowControlCallback({
-          windowEventSelect: (windowIndex) => {
-            activeWindowIndex.value = windowIndex
-          },
-          windowEventOver: (windowIndex) => {
-            // 使用 SDK 提供的窗口进入事件控制悬浮工具栏,无需自行计算鼠标坐标。
-            hoveredWindowIndex.value = windowIndex
-          },
-          windowEventOut: (windowIndex) => {
-            if (hoveredWindowIndex.value === windowIndex) hoveredWindowIndex.value = undefined
-          },
-          windowFullCcreenChange: (fullscreen) => {
-            // 双击也能触发全屏,因此以 SDK 回调作为页面全屏状态的最终来源。
-            stageFullscreen.value = fullscreen
-            schedulePlayerResize()
-          },
-          firstFrameDisplay: (windowIndex) => {
-            // JS_Play 成功只代表播放命令已接受;首帧回调才代表用户真正看到了画面。
-            const playingRequestId = previewPlayingWindowRequests.get(windowIndex)
-            if (
-              playingRequestId === undefined ||
-              !isCurrentWindowPreviewRequest(windowIndex, playingRequestId)
-            ) {
-              return
-            }
-            setWindowInterrupted(windowIndex, false)
-            setWindowPreviewSelected(windowIndex, true)
-            cancelWindowPreviewRequest(windowIndex)
-          },
-          InterruptStream: (windowIndex, interruptTime) => {
-            // 获取新地址期间收到的回调可能属于旧流;新流尚未进入 JS_Play 时忽略该旧回调。
-            if (
-              previewWindowRequests.has(windowIndex) &&
-              !previewPlayingWindowRequests.has(windowIndex)
-            ) {
-              return
-            }
-            if (!previewWindowCameras.value[windowIndex]) return
-            cancelWindowPreviewRequest(windowIndex)
-            setWindowInterrupted(windowIndex, true)
-            clearWindowSoundState(windowIndex)
-            void stopWindowTalk(windowIndex)
-            void stopWindowZoom(windowIndex)
-            showPreviewError(`监控画面已中断(${interruptTime} 秒未收到码流)`)
-          },
-          StreamEnd: (windowIndex) => {
-            // 用户主动关闭时由 stopWindowPreview 统一清理,不显示“画面已结束”的异常提示。
-            if (stoppingWindowIndexes.has(windowIndex)) return
-            // 与断流回调相同,避免旧流结束事件清掉正在请求的新监控点。
-            if (
-              previewWindowRequests.has(windowIndex) &&
-              !previewPlayingWindowRequests.has(windowIndex)
-            ) {
-              return
-            }
-            if (!previewWindowCameras.value[windowIndex]) return
+      await initializingPlayer.JS_SetWindowControlCallback({
+        windowEventSelect: (windowIndex) => {
+          activeWindowIndex.value = windowIndex
+        },
+        windowEventOver: (windowIndex) => {
+          // 使用 SDK 提供的窗口进入事件控制悬浮工具栏,无需自行计算鼠标坐标。
+          hoveredWindowIndex.value = windowIndex
+        },
+        windowEventOut: (windowIndex) => {
+          if (hoveredWindowIndex.value === windowIndex) hoveredWindowIndex.value = undefined
+        },
+        windowFullCcreenChange: (fullscreen) => {
+          // 双击也能触发全屏,因此以 SDK 回调作为页面全屏状态的最终来源。
+          stageFullscreen.value = fullscreen
+          schedulePlayerResize()
+        },
+        firstFrameDisplay: (windowIndex) => {
+          // JS_Play 成功只代表播放命令已接受;首帧回调才代表用户真正看到了画面。
+          const playingRequestId = previewPlayingWindowRequests.get(windowIndex)
+          if (
+            playingRequestId === undefined ||
+            !isCurrentWindowPreviewRequest(windowIndex, playingRequestId)
+          ) {
+            return
+          }
+          setWindowInterrupted(windowIndex, false)
+          setWindowPreviewSelected(windowIndex, true)
+          cancelWindowPreviewRequest(windowIndex)
+        },
+        InterruptStream: (windowIndex, interruptTime) => {
+          // 获取新地址期间收到的回调可能属于旧流;新流尚未进入 JS_Play 时忽略该旧回调。
+          if (
+            previewWindowRequests.has(windowIndex) &&
+            !previewPlayingWindowRequests.has(windowIndex)
+          ) {
+            return
+          }
+          if (!previewWindowCameras.value[windowIndex]) return
+          cancelWindowPreviewRequest(windowIndex)
+          setWindowInterrupted(windowIndex, true)
+          clearWindowSoundState(windowIndex)
+          void stopWindowTalk(windowIndex)
+          void stopWindowZoom(windowIndex)
+          showPreviewError(`监控画面已中断(${interruptTime} 秒未收到码流)`)
+        },
+        StreamEnd: (windowIndex) => {
+          // 用户主动关闭时由 stopWindowPreview 统一清理,不显示“画面已结束”的异常提示。
+          if (stoppingWindowIndexes.has(windowIndex)) return
+          // 与断流回调相同,避免旧流结束事件清掉正在请求的新监控点。
+          if (
+            previewWindowRequests.has(windowIndex) &&
+            !previewPlayingWindowRequests.has(windowIndex)
+          ) {
+            return
+          }
+          if (!previewWindowCameras.value[windowIndex]) return
+          void stopWindowTalk(windowIndex)
+          void stopWindowZoom(windowIndex)
+          clearWindowPreview(windowIndex)
+          showPreviewError('监控画面已结束')
+        },
+        performanceLack: () => {
+          // SDK 已经完成性能判断,页面只负责给出短暂提示,不自行推断设备性能。
+          showPreviewError('当前设备性能不足,监控画面可能卡顿')
+        },
+        pluginErrorHandler: (windowIndex, errorCode, error) => {
+          console.error('H5player 播放器错误', errorCode, error)
+          // 新地址仍在请求时,窗口里的 SDK 错误属于即将替换的旧流,不应取消新请求。
+          if (
+            windowIndex >= 0 &&
+            previewWindowRequests.has(windowIndex) &&
+            !previewPlayingWindowRequests.has(windowIndex)
+          ) {
+            return
+          }
+          if (windowIndex >= 0) {
             void stopWindowTalk(windowIndex)
             void stopWindowZoom(windowIndex)
             clearWindowPreview(windowIndex)
-            showPreviewError('监控画面已结束')
-          },
-          performanceLack: () => {
-            // SDK 已经完成性能判断,页面只负责给出短暂提示,不自行推断设备性能。
-            showPreviewError('当前设备性能不足,监控画面可能卡顿')
-          },
-          pluginErrorHandler: (windowIndex, errorCode, error) => {
-            console.error('H5player 播放器错误', errorCode, error)
-            // 新地址仍在请求时,窗口里的 SDK 错误属于即将替换的旧流,不应取消新请求。
-            if (
-              windowIndex >= 0 &&
-              previewWindowRequests.has(windowIndex) &&
-              !previewPlayingWindowRequests.has(windowIndex)
-            ) {
-              return
-            }
-            if (windowIndex >= 0) {
-              void stopWindowTalk(windowIndex)
-              void stopWindowZoom(windowIndex)
-              clearWindowPreview(windowIndex)
-            }
-            showPreviewError(`播放器异常(${errorCode})`)
-          },
-          talkPluginErrorHandler: (errorCode, error) => {
-            console.error('H5player 对讲错误', errorCode, error)
-            talkOperationId += 1
-            talkWindowIndex.value = undefined
-            talkTargetWindowIndex.value = undefined
-            talkControlLoading.value = false
-            showPreviewError(getTalkErrorMessage(errorCode))
           }
-        })
-      )
+          showPreviewError(`播放器异常(${errorCode})`)
+        },
+        talkPluginErrorHandler: (errorCode, error) => {
+          console.error('H5player 对讲错误', errorCode, error)
+          talkOperationId += 1
+          talkWindowIndex.value = undefined
+          talkTargetWindowIndex.value = undefined
+          talkControlLoading.value = false
+          showPreviewError(getTalkErrorMessage(errorCode))
+        }
+      })
       if (componentUnmounted) throw new Error('页面已卸载,取消初始化播放器')
 
       // 回调注册成功且页面仍存在后,播放器实例才正式对其他操作可见。
@@ -1367,7 +1373,16 @@ async function destroyPlayer() {
   await Promise.resolve(currentPlayer.JS_Destroy()).catch(() => undefined)
 }
 
-watch(keyword, (value) => treeRef.value?.filter(value))
+function handleTreeSearch(event: KeyboardEvent) {
+  // 中文输入法确认候选词时的回车不提交搜索。
+  if (event.isComposing || event.keyCode === 229) return
+  void loadRootTree(keyword.value.trim())
+}
+
+watch(keyword, (value, previousValue) => {
+  // 输入文字不请求;点击清空或手动删空时立即恢复初始树。
+  if (!value.trim() && previousValue.trim()) void loadRootTree()
+})
 
 onMounted(() => {
   componentUnmounted = false
@@ -1382,6 +1397,7 @@ onMounted(() => {
 
 onBeforeUnmount(() => {
   componentUnmounted = true
+  treeRequestId += 1
   // 页面离开时补发停止;即使开始请求仍在途中,停止流程也会等待并保持正确顺序。
   void stopActivePtzControl()
   void destroyPlayer()
@@ -1398,21 +1414,27 @@ onBeforeUnmount(() => {
         </div>
       </div>
 
-      <el-input v-model="keyword" :prefix-icon="Search" clearable placeholder="搜索已加载监控点" />
+      <el-input
+        v-model="keyword"
+        :prefix-icon="Search"
+        clearable
+        placeholder="输入监控点名称,回车搜索"
+        @keydown.enter="handleTreeSearch" />
 
       <div v-loading="treeLoading" class="monitor-tree-wrap">
         <el-tree
-          ref="treeRef"
+          v-if="!treeLoading"
+          :key="treeVersion"
           class="monitor-tree"
           :data="treeData"
           :props="treeProps"
           :load="loadTreeNode"
           :default-expanded-keys="expandedKeys"
           node-key="indexCode"
-          lazy
+          :lazy="!treeSearchMode"
+          :default-expand-all="treeSearchMode"
           highlight-current
           :expand-on-click-node="false"
-          :filter-node-method="filterTreeNode"
           empty-text="暂无监控点"
           @node-click="handleNodeClick">
           <template #default="{ data }">