正在初始化...
@@ -45,6 +49,7 @@ let isActivated = true
let requestedVisible = true
let lastRealRequest = null
let lastPlaybackRequest = null
+let playbackNeedsStart = false
const realFallbackAttempted = new Set()
const players = new Map()
const isReadyByType = new Map()
@@ -53,6 +58,15 @@ const initResolversByType = new Map()
const readyTimers = new Set()
const closingPlayers = new Map()
let pendingClose = Promise.resolve()
+let cancelCloseWait
+let closeCancellation = new Promise((resolve) => { cancelCloseWait = resolve })
+
+function cancelClosingWaits() {
+ cancelCloseWait()
+ closeCancellation = new Promise((resolve) => { cancelCloseWait = resolve })
+ closingPlayers.clear()
+ pendingClose = Promise.resolve()
+}
let cancelPlaybackWait
let playbackCancellation = new Promise((resolve) => { cancelPlaybackWait = resolve })
@@ -65,6 +79,8 @@ let latestPlaybackToken = 0
const loading = ref(false)
const nativeReady = ref(false)
const initializationError = ref('')
+const connectionStatusText = ref('正在连接视频插件…')
+const CONNECTION_ERROR_MESSAGE = '未能连接本机视频服务,请启动或重新启动视频插件后重试'
const expandedSnum = ref(null)
const REAL_RESTART_GUARD_MS = 800
let lastRealSignature = ""
@@ -128,6 +144,22 @@ function canUsePlayer() {
return !isUnmounted && isActivated && props.active
}
+function hasRequestedPlayback() {
+ return currentWindowType.value === 0 ? !!lastRealRequest?.length
+ : [3, 7].includes(currentWindowType.value) && !!lastPlaybackRequest?.length
+}
+
+function restoreRequestedPlayback() {
+ if (!canUsePlayer() || !requestedVisible || document.hidden || !hasRequestedPlayback()) return
+ if (currentWindowType.value === 0) return startReal(lastRealRequest)
+ return startPlayback(lastPlaybackRequest)
+}
+
+function retryConnection() {
+ if (loading.value || !initializationError.value) return
+ return restoreRequestedPlayback()
+}
+
function getContainerId(type) {
return `video-plugin-${instanceId}-${type}`
}
@@ -207,6 +239,8 @@ function buildPlaybackSignature(list) {
function activatePlayer(type, player) {
VIDEO_PLAYER = player
currentWindowType.value = type
+ initializationError.value = ''
+ nativeReady.value = !!isReadyByType.get(type)
setActiveContainer(type)
players.forEach((item, itemType) => {
const visible = itemType === type && canUsePlayer() && requestedVisible && !document.hidden
@@ -314,8 +348,11 @@ function initVideoPlayer(type = props.windowType) {
const myGeneration = generation
let player = null
let failed = false
+ let resumeAfterReconnect = false
+ let connectionGeneration = 0
+ let readyTimer = null
let resolveInit
- const pending = new Promise((resolve) => { resolveInit = resolve })
+ let pending = new Promise((resolve) => { resolveInit = resolve })
// 必须在查询配置之前占位,避免同时调用时创建出多个原生窗口。
initPromisesByType.set(type, pending)
const settle = (value) => {
@@ -329,6 +366,7 @@ function initVideoPlayer(type = props.windowType) {
const isCurrent = () => canUsePlayer() && generation === myGeneration && !failed
loading.value = true
initializationError.value = ''
+ connectionStatusText.value = '正在连接视频插件…'
const initialize = async () => {
const { data: setting, code: settingCode } = await settingApi.getObj()
if (!isCurrent()) return settle(null)
@@ -370,43 +408,91 @@ function initVideoPlayer(type = props.windowType) {
// 底部操作栏, 选传,【true - 显示, false - 隐藏】
showBar: props.showBar,
// 如果DOM元素被插件挡住了,把DOM元素的类名传入。
- shieldClass: ["el-select__popper", "el-popper", "video-container-page", "yt_info_popup", "video-window-label", "loading-mask", "el-loading-mask"],
+ shieldClass: ["el-select__popper", "el-popper", "video-container-page", "yt_info_popup", "video-window-label", "loading-mask", "el-loading-mask", "connection-error"],
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
coverShieldClass: [],
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
parentIframeShieldClass: [],
+ // 连接探测由 SDK 共享并限制次数,临时断线不按创建失败销毁实例。
+ connectionStateChange: ({ state }) => {
+ if (!isCurrent() || state !== 'reconnecting') return
+ ++connectionGeneration
+ clearTimeout(readyTimer)
+ readyTimers.delete(readyTimer)
+ isReadyByType.set(type, false)
+ resumeAfterReconnect = true
+ if (!initPromisesByType.has(type)) {
+ pending = new Promise((resolve) => { resolveInit = resolve })
+ initPromisesByType.set(type, pending)
+ initResolversByType.set(type, settle)
+ }
+ if (type === 0) lastRealSignature = ''
+ else {
+ lastPlaybackSignature = ''
+ playbackNeedsStart = true
+ }
+ // 断线前的关闭回包已不可达,不能阻塞新连接上的播放。
+ cancelClosingWaits()
+ if (currentWindowType.value === type) {
+ cancelWaitingPlayback()
+ ++latestRealToken
+ ++latestPlaybackToken
+ nativeReady.value = false
+ initializationError.value = ''
+ connectionStatusText.value = '视频插件连接中断,正在重新连接…'
+ loading.value = hasRequestedPlayback()
+ }
+ },
// 创建播放器成功回调
createSuccess: (versionInfo) => {
if (!isCurrent()) return
// 该回调触发后,我们可以进行实时预览/录像回放等操作
console.log(TAG, "创建播放器成功,versionInfo:", versionInfo)
isReadyByType.set(type, true)
- nativeReady.value = true
+ if (currentWindowType.value === type) nativeReady.value = true
+ const readyGeneration = connectionGeneration
const timer = setTimeout(() => {
readyTimers.delete(timer)
if (!isCurrent()) return settle(null)
- if (currentWindowType.value === type) activatePlayer(type, player)
- else player.hide?.()
- loading.value = false
+ if (readyGeneration !== connectionGeneration) return
+ if (currentWindowType.value === type) {
+ activatePlayer(type, player)
+ loading.value = false
+ } else player.hide?.()
settle(player)
+ if (resumeAfterReconnect) {
+ resumeAfterReconnect = false
+ if (currentWindowType.value === type) restoreRequestedPlayback()
+ }
}, 500)
+ readyTimer = timer
readyTimers.add(timer)
},
// 创建播放器失败回调
createError: (err) => {
if (!isCurrent()) return
failed = true
- nativeReady.value = false
- initializationError.value = err?.message || '视频插件未连接,请检查安装及连接'
+ if ([1001, 1003].includes(Number(err?.code))) cancelClosingWaits()
+ ++connectionGeneration
+ clearTimeout(readyTimer)
+ readyTimers.delete(readyTimer)
+ if (type === 0) lastRealSignature = ''
+ else {
+ lastPlaybackSignature = ''
+ playbackNeedsStart = true
+ }
+ if (currentWindowType.value === type) {
+ nativeReady.value = false
+ initializationError.value = hasRequestedPlayback()
+ ? ([1001, 1003].includes(Number(err?.code)) ? CONNECTION_ERROR_MESSAGE : err?.message || '视频插件连接失败,请重试')
+ : ''
+ loading.value = false
+ }
console.log(TAG, "创建播放器失败,err:", err)
isReadyByType.set(type, false)
if (players.get(type) === player) players.delete(type)
if (VIDEO_PLAYER === player) VIDEO_PLAYER = null
disposePlayer(player)
- if ([1001].includes(err.code)) {
- ElMessage.error(err.message)
- }
- loading.value = false
settle(null)
},
// 插件公共回调
@@ -606,6 +692,7 @@ async function startPlayback(items) {
const myToken = ++latestPlaybackToken
++latestRealToken
lastPlaybackRequest = items
+ playbackNeedsStart = true
loading.value = true
const player = await Promise.race([initVideoPlayer(3), cancelled])
if (!player) return
@@ -615,6 +702,7 @@ async function startPlayback(items) {
const now = Date.now()
const signature = buildPlaybackSignature(items)
if (signature && signature === lastPlaybackSignature && now - lastPlaybackStartAt < REAL_RESTART_GUARD_MS) {
+ playbackNeedsStart = false
loading.value = false
return
}
@@ -628,6 +716,7 @@ async function startPlayback(items) {
// console.log(TAG, "开始回放前关闭旧视频失败,err:", e)
// }
player.startPlayback(items)
+ playbackNeedsStart = false
loading.value = false
}
@@ -665,12 +754,18 @@ async function handleVisibilityChange() {
}
VIDEO_PLAYER.hide?.()
} else {
- if (!VIDEO_PLAYER) return
- VIDEO_PLAYER.show?.()
+ updateVisible(true)
+ if (initializationError.value) return
if (currentWindowType.value === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
await startReal(lastRealRequest)
+ return
}
- if ((currentWindowType.value === 3 || currentWindowType.value === 7) && Array.isArray(lastPlaybackRequest)) {
+ if ((currentWindowType.value === 3 || currentWindowType.value === 7) && lastPlaybackRequest?.length) {
+ if (playbackNeedsStart) {
+ await startPlayback(lastPlaybackRequest)
+ return
+ }
+ if (!VIDEO_PLAYER) return
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
@@ -793,8 +888,11 @@ function stopOwnedPlayers() {
const task = Promise.resolve(result).catch((e) => {
console.log(TAG, "关闭播放失败,err:", e)
}).then(() => {
- if (player.setting) player.setting.channelList = []
- if (closingPlayers.get(player) === task) closingPlayers.delete(player)
+ // 旧连接的关闭回包不能清掉重连后或新实例中的播放列表。
+ if (closingPlayers.get(player) === task) {
+ if (player.setting) player.setting.channelList = []
+ closingPlayers.delete(player)
+ }
})
closingPlayers.set(player, task)
return task
@@ -804,21 +902,25 @@ function stopOwnedPlayers() {
}
function closeVideo() {
+ initializationError.value = ''
expandedSnum.value = null
cancelWaitingPlayback()
++latestRealToken
++latestPlaybackToken
lastRealRequest = null
lastPlaybackRequest = null
+ playbackNeedsStart = false
lastRealSignature = ""
lastPlaybackSignature = ""
realFallbackAttempted.clear()
loading.value = false
ytRef.value?.close?.()
- return stopOwnedPlayers()
+ const cancelled = closeCancellation
+ return Promise.race([stopOwnedPlayers(), cancelled])
}
function destroy(preserveRequests = false) {
+ connectionStatusText.value = '正在连接视频插件…'
nativeReady.value = false
initializationError.value = ''
expandedSnum.value = null
@@ -832,8 +934,7 @@ function destroy(preserveRequests = false) {
initResolversByType.clear()
initPromisesByType.clear()
isReadyByType.clear()
- closingPlayers.clear()
- pendingClose = Promise.resolve()
+ cancelClosingWaits()
lastRealSignature = ""
lastPlaybackSignature = ""
realFallbackAttempted.clear()
@@ -846,6 +947,7 @@ function destroy(preserveRequests = false) {
if (!preserveRequests) {
lastRealRequest = null
lastPlaybackRequest = null
+ playbackNeedsStart = false
currentWindowType.value = null
pageTotal.value = 0
}
@@ -998,6 +1100,38 @@ defineExpose({
font-size: 14px;
}
+.connection-error {
+ position: absolute;
+ inset: 0;
+ z-index: 4;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ padding: 16px;
+ background: var(--el-bg-color-overlay, #1f2530);
+ color: var(--el-text-color-primary, #f1f5f9);
+ font-size: 14px;
+ text-align: center;
+ overflow: auto;
+}
+
+.connection-retry {
+ border: 1px solid var(--el-color-primary, #409eff);
+ border-radius: 4px;
+ padding: 8px 16px;
+ background: var(--el-color-primary-dark-2, #2563eb);
+ color: #fff;
+ cursor: pointer;
+ font: inherit;
+}
+
+.connection-retry:focus-visible {
+ outline: 2px solid var(--el-color-primary, #409eff);
+ outline-offset: 3px;
+}
+
.video-container-page {
position: absolute;
pointer-events: auto;
diff --git a/src/theme/device-list.scss b/src/theme/device-list.scss
index 555184a..1f830c3 100644
--- a/src/theme/device-list.scss
+++ b/src/theme/device-list.scss
@@ -179,16 +179,36 @@
}
}
+ .device-status {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 6px;
+ min-width: 0;
+ max-width: 100%;
+ }
+
.device-state-group {
display: flex;
align-items: center;
gap: 8px;
+ min-height: 22px;
}
.device-state {
display: inline-flex;
align-items: center;
- gap: 7px;
+ flex: none;
+ gap: 6px;
+ padding: 1px 7px;
+ border: 1px solid var(--el-color-danger-light-8);
+ border-radius: 4px;
+ background: var(--el-color-danger-light-9);
+ color: var(--el-text-color-primary);
+ color: color-mix(in srgb, var(--el-color-danger) 50%, var(--el-text-color-primary));
+ font-size: 12px;
+ font-weight: 500;
+ line-height: 18px;
white-space: nowrap;
&__dot {
@@ -199,22 +219,54 @@
background: var(--el-color-danger);
}
- &--online .device-state__dot {
- background: var(--el-color-success);
+ &--online {
+ border-color: var(--el-color-success-light-8);
+ background: var(--el-color-success-light-9);
+ color: color-mix(in srgb, var(--el-color-success) 50%, var(--el-text-color-primary));
+
+ .device-state__dot {
+ background: var(--el-color-success);
+ }
}
}
.device-source {
display: inline-block;
- padding: 2px 8px;
- border-radius: 4px;
- background: var(--el-fill-color-light);
- color: var(--el-text-color-primary);
+ flex: none;
+ color: var(--el-text-color-regular);
font-size: 12px;
line-height: 20px;
white-space: nowrap;
}
+ .device-status__reason {
+ display: flex;
+ align-items: baseline;
+ min-width: 0;
+ max-width: 100%;
+ font-size: 12px;
+ line-height: 18px;
+ }
+
+ .device-status__reason-label {
+ flex: none;
+ color: var(--el-text-color-regular);
+ }
+
+ .device-status__reason-value {
+ min-width: 0;
+ overflow: hidden;
+ color: var(--el-text-color-primary);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &:focus-visible {
+ border-radius: 2px;
+ outline: 2px solid var(--el-color-primary);
+ outline-offset: 2px;
+ }
+ }
+
.device-list-pagination {
justify-content: flex-end;
max-width: 100%;
diff --git a/src/views/admin/system/user/components/user-list.vue b/src/views/admin/system/user/components/user-list.vue
index cbcbe3b..e320358 100644
--- a/src/views/admin/system/user/components/user-list.vue
+++ b/src/views/admin/system/user/components/user-list.vue
@@ -1,26 +1,22 @@
-
+
-
+
-
+
- {{ $t('common.queryBtn') }}
+ {{ $t('common.queryBtn')
+ }}
{{ $t('common.resetBtn') }}
@@ -28,68 +24,50 @@
-
- {{ $t('common.addBtn') }}
+
+ 注册
-
+
{{ $t('common.importBtn') }}
-
+
{{ $t('common.delBtn') }}
-
+
-
+
-
+
- {{ item.postName }}
+ {{ item.postName
+ }}
- {{ item.roleName }}
+ {{ item.roleName
+ }}
-
+
@@ -97,55 +75,44 @@
-
+
{{ $t('sysuser.passwordBtn') }}
-
+
{{ $t('common.editBtn') }}
-
+
-
+
{{ $t('common.delBtn') }}
-
+
{{ $t('sysuser.assignRoleBtn') }}
-
+
+
-
-
+
+
diff --git a/src/views/alarm/setting/dhIccSetting/dh-icc-setting.vue b/src/views/alarm/setting/dhIccSetting/dh-icc-setting.vue
index 6009656..f109c82 100644
--- a/src/views/alarm/setting/dhIccSetting/dh-icc-setting.vue
+++ b/src/views/alarm/setting/dhIccSetting/dh-icc-setting.vue
@@ -3,11 +3,12 @@
v-loading="state.loading">
-
大华icc配置
-
+ {{ title }}
+
+
-
+
@@ -53,6 +54,10 @@ import { useMessage } from "/@/hooks/message"
import * as ComDhSettingApi from "/@/api/com/comDhSetting";
import type { DhIccConnectionTestResult } from "/@/api/com/comDhSetting";
+withDefaults(defineProps<{ title?: string }>(), {
+ title: "大华icc配置",
+});
+
const dataFormRef = ref();
const state = reactive({
diff --git a/src/views/base/ss/sbList.vue b/src/views/base/ss/sbList.vue
index 36393d4..111cead 100644
--- a/src/views/base/ss/sbList.vue
+++ b/src/views/base/ss/sbList.vue
@@ -1,5 +1,5 @@
-
+
diff --git a/src/views/collection/video/platform.vue b/src/views/collection/video/platform.vue
index a92cd75..cddc343 100644
--- a/src/views/collection/video/platform.vue
+++ b/src/views/collection/video/platform.vue
@@ -181,12 +181,19 @@
@@ -235,6 +242,8 @@ const emptyStatus = (): PlatformStatus => ({
});
const settingRef = ref();
+// 厂商类型仅用于页面展示,暂不参与配置保存及平台接入。
+const manufacturerType = ref("");
const resourceDialogRef = ref>();
const state = reactive({
diff --git a/src/views/device/components/camera-list.vue b/src/views/device/components/camera-list.vue
index a1f8d96..666ee0f 100644
--- a/src/views/device/components/camera-list.vue
+++ b/src/views/device/components/camera-list.vue
@@ -54,6 +54,7 @@
>批量删除
导入
+
已选 {{ selectObjs.length }} 项