|
|
@@ -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 }">
|