This commit is contained in:
dzc committed 2026-03-03 20:20:17 +08:00
1 parent 01508bf7a2
commit 53f209d0d3
8 files changed
+351 -352

No files matched your search

@@ -628,20 +628,15 @@
} else {
data.info.count = data.info.count || 0
data.info.count++
clearTimeout(hwndInfo.notifyrealmonitorTimeout)
hwndInfo.notifyrealmonitorTimeout = setTimeout(() => {
if (Number(data.info.result) === 10704) {
// 拉流失败
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10704, message: '打开视频失败', i18nKey: 'video.player.play.fail' })
} else if (Number(data.info.result) === -405) {
// 拉流超时
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10705, message: '打开视频超时', i18nKey: 'video.player.play.timeout' })
} else {
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 204, message: '流异常断开,请重新连接~', i18nKey: 'video.player.stream.disconnect.reload' })
}
// channelInfo && videoInfo.startReal([channelInfo], { isReOpen: true, count: data.info.count++ })
clearTimeout(hwndInfo.notifyrealmonitorTimeout)
}, 2000)
if (Number(data.info.result) === 10704) {
// 拉流失败
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10704, message: '打开视频失败', i18nKey: 'video.player.play.fail' })
} else if (Number(data.info.result) === -405) {
// 拉流超时
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 10705, message: '打开视频超时', i18nKey: 'video.player.play.timeout' })
} else {
hwndInfo.realError && hwndInfo.realError(channelInfo, { code: 204, message: '流异常断开,请重新连接~', i18nKey: 'video.player.stream.disconnect.reload' })
}
}
videoInfo.setWindowDragEnable()
break
+333 -170
View File
@@ -1,5 +1,6 @@
<template>
<div class="video-container">
<div v-if="loading" class="loading-mask">正在初始化...</div>
<div id="video-plugin-preview-0" class="pane" />
<div id="video-plugin-preview-3" class="pane" />
<div id="video-plugin-preview-7" class="pane" />
@@ -7,7 +8,7 @@
</template>
<script setup>
import { onMounted, onUnmounted } from "vue"
import { onMounted, onUnmounted, ref } from "vue"
import { ElMessage } from "element-plus";
import dayjs from "dayjs"
@@ -19,20 +20,63 @@ import * as settingApi from "/@/api/com/comDhSetting"
const TAG = "|视频预览|"
let VIDEO_PLAYER = null
const divisionSettingList = [1, 2, 4, 6, 8, 9, 13, 16, 20, 25, 36, 64]
let currentWindowType = null
let isReady = false
/** @type {Array<() => void>} */
let readyQueue = []
let lastRealRequest = null
let lastPlaybackRequest = null
let realFallbackAttempted = false
const realFallbackAttempted = new Set()
const players = new Map()
const isReadyByType = new Map()
const readyWaitersByType = new Map()
const initPromisesByType = new Map()
let latestRealToken = 0
let latestPlaybackToken = 0
const loading = ref(false)
const REAL_RESTART_GUARD_MS = 800
let lastRealSignature = ""
let lastRealStartAt = 0
let lastPlaybackSignature = ""
let lastPlaybackStartAt = 0
function waitForReady(type = props.windowType) {
return new Promise((resolve) => {
if (isReadyByType.get(type)) {
resolve()
return
}
const arr = readyWaitersByType.get(type) || []
arr.push(resolve)
readyWaitersByType.set(type, arr)
})
}
function getContainerId(type) {
return `video-plugin-preview-${type}`
}
function getDivisionByCount(count) {
const safeCount = Math.max(1, Number(count) || 1)
const match = divisionSettingList.find((num) => num >= safeCount)
return match || divisionSettingList[divisionSettingList.length - 1] || safeCount
}
function applyDivisionByCount(count) {
if (!VIDEO_PLAYER?.changeDivision) return
const nextDivision = getDivisionByCount(count)
const currentDivision = Number(VIDEO_PLAYER?.setting?.division)
if (currentDivision === nextDivision) return
VIDEO_PLAYER.changeDivision(nextDivision)
}
function getRealFallbackKey(info) {
if (info?.channelId) return `cid:${info.channelId}`
if (typeof info?.snum === "number") return `snum:${info.snum}`
return null
}
function setActiveContainer(type) {
const ids = [0, 3, 7].map(getContainerId)
ids.forEach((id) => {
@@ -43,6 +87,28 @@ function setActiveContainer(type) {
})
}
function buildRealSignature(list) {
if (!Array.isArray(list)) return ""
return list.map((item) => {
const channelId = item?.channelId ?? ""
const snum = typeof item?.snum === "number" ? item.snum : ""
const streamType = item?.streamType ?? ""
return `${channelId}|${snum}|${streamType}`
}).join(";")
}
function buildPlaybackSignature(list) {
if (!Array.isArray(list)) return ""
return list.map((item) => {
const channelId = item?.channelId ?? ""
const snum = typeof item?.snum === "number" ? item.snum : ""
const streamType = item?.streamType ?? ""
const startTime = item?.startTime ?? ""
const endTime = item?.endTime ?? ""
return `${channelId}|${snum}|${streamType}|${startTime}|${endTime}`
}).join(";")
}
/**
* 将任务加入就绪队列;若已就绪则立即执行
* @param {() => void} fn 任务函数
@@ -126,167 +192,195 @@ const props = defineProps({
* @param {0|3|7} [type=props.windowType] 播放器类型(0预览/3回放/7回放支持倒放)
*/
async function initVideoPlayer(type = props.windowType) {
if (players.has(type)) {
VIDEO_PLAYER = players.get(type)
currentWindowType = type
isReady = !!isReadyByType.get(type)
setActiveContainer(type)
if (isReady) flushReadyQueue()
return
}
if (initPromisesByType.get(type)) {
return initPromisesByType.get(type)
}
loading.value = true
// 查询配置
const { data: setting, code: settingCode } = await settingApi.getObj()
if (settingCode != 0) {
ElMessage.error("查询配置失败")
return
loading.value = false
return Promise.resolve()
}
const dhIccSetting = setting.dhIccSetting
try {
// 若已有缓存实例,直接复用并切换容器
if (players.has(type)) {
VIDEO_PLAYER = players.get(type)
currentWindowType = type
isReady = !!isReadyByType.get(type)
setActiveContainer(type)
console.log("缓存-VIDEO_PLAYER:", VIDEO_PLAYER);
console.log(TAG, "复用缓存播放器,windowType:", type)
if (isReady) flushReadyQueue()
return
}
} catch (e) {
console.log(TAG, "旧播放器销毁失败,err:", e)
}
isReady = false
isReadyByType.set(type, false)
setActiveContainer(type)
VIDEO_PLAYER = new window.VideoPlayer({
videoId: getContainerId(type),
// 播放器类型:0-实时预览,3-录像回放,7-录像回放(支持倒放)
windowType: type,
// zh-中文 en-英文
// V1.2.1新增属性,支持语言切换,初始化时设置好语言.
language: 'zh',
// 写死true即可
usePluginLogin: true,
// 必传
pluginLoginInfo: {
// icc平台的ip
host: dhIccSetting.videoHost ?? "",
// icc 平台的端口 https (默认 443)
port: dhIccSetting?.videoPort ?? "",
// 登录 icc 平台的用户名 用户名也可能是 admin/zzz/...
username: dhIccSetting?.videoUsername ?? "",
// 登录 icc 平台的密码
password: dhIccSetting?.videoPassword ?? "",
},
// 默认展示的窗口数量, 必传
division: props.division,
// 窗口拖拽 【暂不支持】
draggable: props.draggable,
// 底部操作栏, 选传,【true - 显示, false - 隐藏】
showBar: props.showBar,
// 如果DOM元素被插件挡住了,把DOM元素的类名传入。
shieldClass: ["el-select__popper"],
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
coverShieldClass: [],
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
parentIframeShieldClass: [],
// 创建播放器成功回调
createSuccess: (versionInfo) => {
// 该回调触发后,我们可以进行实时预览/录像回放等操作
console.log(TAG, "创建播放器成功,versionInfo:", versionInfo)
isReady = true
isReadyByType.set(type, true)
flushReadyQueue()
},
// 创建播放器失败回调
createError: (err) => {
console.log(TAG, "创建播放器失败,err:", err)
isReady = false
isReadyByType.set(type, false)
if ([1001].includes(err.code)) {
ElMessage.error(err.message)
}
},
// 插件公共回调
dhPlayerMessage: (info, err) => {
console.log(TAG, "插件公共消息,info:", info, "err:", err)
},
// 实时预览成功回调
realSuccess: (info) => {
console.log(TAG, "实时预览成功,info:", info)
realFallbackAttempted = false
},
// 实时预览失败回调
realError: (info, err) => {
console.log(TAG, "实时预览失败,info:", info, "err:", err)
if ([408].includes(err.code)) {
try {
const hasSubStream = Array.isArray(lastRealRequest) && lastRealRequest.some(it => (it?.streamType === 2))
if (hasSubStream && !realFallbackAttempted) {
realFallbackAttempted = true
const fallback = lastRealRequest.map(it => ({ ...it, streamType: 1 }))
lastRealRequest = fallback
console.log(TAG, "辅码流不可用,自动切换主码流重试")
VIDEO_PLAYER.startReal(fallback)
}
} catch (e) {
console.log(TAG, "自动切换主码流失败,err:", e)
const p = new Promise((resolve) => {
VIDEO_PLAYER = new window.VideoPlayer({
videoId: getContainerId(type),
// 播放器类型:0-实时预览,3-录像回放,7-录像回放(支持倒放)
windowType: type,
// zh-中文 en-英文
// V1.2.1新增属性,支持语言切换,初始化时设置好语言.
language: 'zh',
// 写死true即可
usePluginLogin: true,
// 必传
pluginLoginInfo: {
// icc平台的ip
host: dhIccSetting.videoHost ?? "",
// icc 平台的端口 https (默认 443)
port: dhIccSetting?.videoPort ?? "",
// 登录 icc 平台的用户名 用户名也可能是 admin/zzz/...
username: dhIccSetting?.videoUsername ?? "",
// 登录 icc 平台的密码
password: dhIccSetting?.videoPassword ?? "",
},
// 默认展示的窗口数量, 必传
division: props.division,
// 窗口拖拽 【暂不支持】
draggable: props.draggable,
// 底部操作栏, 选传,【true - 显示, false - 隐藏】
showBar: props.showBar,
// 如果DOM元素被插件挡住了,把DOM元素的类名传入。
shieldClass: ["el-select__popper", "el-popper"],
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
coverShieldClass: [],
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
parentIframeShieldClass: [],
// 创建播放器成功回调
createSuccess: (versionInfo) => {
// 该回调触发后,我们可以进行实时预览/录像回放等操作
console.log(TAG, "创建播放器成功,versionInfo:", versionInfo)
isReady = true
isReadyByType.set(type, true)
flushReadyQueue()
const waiters = readyWaitersByType.get(type) || []
waiters.forEach((fn) => {
try { fn() } catch (_) { }
})
readyWaitersByType.delete(type)
loading.value = false
resolve(null)
},
// 创建播放器失败回调
createError: (err) => {
console.log(TAG, "创建播放器失败,err:", err)
isReady = false
isReadyByType.set(type, false)
if ([1001].includes(err.code)) {
ElMessage.error(err.message)
}
loading.value = false
resolve(null)
},
// 插件公共回调
dhPlayerMessage: (info, err) => {
console.log(TAG, "插件公共消息,info:", info, "err:", err)
},
// 实时预览成功回调
realSuccess: (info) => {
console.log(TAG, "实时预览成功,info:", info)
const key = getRealFallbackKey(info)
if (key) realFallbackAttempted.delete(key)
},
// 实时预览失败回调
realError: (info, err) => {
console.log(TAG, "实时预览失败,info:", info, "err:", err)
if ([204, 408, 10705].includes(err.code)) {
try {
const key = getRealFallbackKey(info)
if (!key || realFallbackAttempted.has(key)) return
if (info?.streamType !== 2) return
const list = Array.isArray(lastRealRequest) ? lastRealRequest : []
const index = list.findIndex((it) => {
if (info?.channelId && it?.channelId) return it.channelId === info.channelId
if (typeof info?.snum === "number") return it?.snum === info.snum
return false
})
if (index < 0) return
const current = list[index]
if (current?.streamType !== 2) return
realFallbackAttempted.add(key)
const fallbackItem = { ...current, streamType: 1 }
const nextList = [...list]
nextList[index] = fallbackItem
lastRealRequest = nextList
console.log(TAG, "辅码流不可用,自动切换主码流重试", fallbackItem)
VIDEO_PLAYER.startReal([fallbackItem])
} catch (e) {
console.log(TAG, "自动切换主码流失败,err:", e)
}
}
},
// 对讲成功回调
talkSuccess: (info) => {
console.log(TAG, "对讲成功,info:", info)
},
// 对讲失败回调
talkError: (info, err) => {
console.log(TAG, "对讲失败,info:", info, "err:", err)
},
// 录像播放成功回调
playbackSuccess: (info) => {
console.log(TAG, "录像播放成功,info:", info)
},
// 录像播放失败回调
playbackError: (info, err) => {
console.log(TAG, "录像播放失败,info:", info, "err:", err)
},
// 录像播放完成回调
playbackFinish: (info) => {
console.log(TAG, "录像播放完成,info:", info)
},
// 抓图成功回调
snapshotSuccess: ({ base64Url, path }, info) => {
console.log(TAG, "抓图成功,base64Url:", base64Url, "path:", path, "info:", info)
// 可选:保存图片到本地(演示示例)
saveSnapshot(base64Url, `${info.channelName}-抓图-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.jpg`)
},
// 录制视频成功回调
videoDownloadSuccess: (path, info) => {
console.log(TAG, "录制视频成功,path:", path, "info:", info)
// 可选:保存视频到本地(演示示例)
saveVideo(path, `${info.channelName}-录像-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.mp4`)
},
// 关闭视频窗口回调
closeWindowSuccess: ({ isAll, snum, channelList }) => {
console.log(TAG, "关闭视频窗口,isAll:", isAll, "snum:", snum, "channelList:", channelList)
},
// 鼠标单击窗口回调
clickWindow: (snum) => {
console.log(TAG, "鼠标单击窗口,snum:", snum)
},
// 鼠标双击窗口回调
dbClickWindow: (snum) => {
console.log(TAG, "鼠标双击窗口,snum:", snum)
},
// 播放器窗口的数量回调
changeDivision: (division) => {
console.log(TAG, "窗口数量变更,division:", division)
},
// rtsp 流下载录像成功回调
downloadRecordSuccess: (info) => {
console.log(TAG, "RTSP录像下载成功,info:", info)
},
// rtsp 流下载录像失败回调
downloadRecordError: (info, err) => {
console.log(TAG, "RTSP录像下载失败,info:", info, "err:", err)
}
},
// 对讲成功回调
talkSuccess: (info) => {
console.log(TAG, "对讲成功,info:", info)
},
// 对讲失败回调
talkError: (info, err) => {
console.log(TAG, "对讲失败,info:", info, "err:", err)
},
// 录像播放成功回调
playbackSuccess: (info) => {
console.log(TAG, "录像播放成功,info:", info)
},
// 录像播放失败回调
playbackError: (info, err) => {
console.log(TAG, "录像播放失败,info:", info, "err:", err)
},
// 录像播放完成回调
playbackFinish: (info) => {
console.log(TAG, "录像播放完成,info:", info)
},
// 抓图成功回调
snapshotSuccess: ({ base64Url, path }, info) => {
console.log(TAG, "抓图成功,base64Url:", base64Url, "path:", path, "info:", info)
// 可选:保存图片到本地(演示示例)
saveSnapshot(base64Url, `${info.channelName}-抓图-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.jpg`)
},
// 录制视频成功回调
videoDownloadSuccess: (path, info) => {
console.log(TAG, "录制视频成功,path:", path, "info:", info)
// 可选:保存视频到本地(演示示例)
saveVideo(path, `${info.channelName}-录像-${dayjs().format("YYYY-MM-DD HH:mm:ss")}.mp4`)
},
// 关闭视频窗口回调
closeWindowSuccess: ({ isAll, snum, channelList }) => {
console.log(TAG, "关闭视频窗口,isAll:", isAll, "snum:", snum, "channelList:", channelList)
},
// 鼠标单击窗口回调
clickWindow: (snum) => {
console.log(TAG, "鼠标单击窗口,snum:", snum)
},
// 鼠标双击窗口回调
dbClickWindow: (snum) => {
console.log(TAG, "鼠标双击窗口,snum:", snum)
},
// 播放器窗口的数量回调
changeDivision: (division) => {
console.log(TAG, "窗口数量变更,division:", division)
},
// rtsp 流下载录像成功回调
downloadRecordSuccess: (info) => {
console.log(TAG, "RTSP录像下载成功,info:", info)
},
// rtsp 流下载录像失败回调
downloadRecordError: (info, err) => {
console.log(TAG, "RTSP录像下载失败,info:", info, "err:", err)
}
})
currentWindowType = type
players.set(type, VIDEO_PLAYER)
console.log(TAG, "播放器已初始化,windowType:", type)
})
currentWindowType = type
players.set(type, VIDEO_PLAYER)
console.log(TAG, "播放器已初始化,windowType:", type)
initPromisesByType.set(type, p)
await p.finally(() => initPromisesByType.delete(type))
}
/**
@@ -307,40 +401,74 @@ function ensureWindowType(target) {
* 开始实时预览
* @param {RealOptions[]} channels 播放参数数组
*/
function startReal(channels) {
async function startReal(channels) {
const myToken = ++latestRealToken
if (!VIDEO_PLAYER) {
initVideoPlayer(0)
} else {
ensureWindowType(0)
}
lastRealRequest = channels
realFallbackAttempted = false
realFallbackAttempted.clear()
const showBtn = ["BTN_STREAM", "BTN_VOICE", "BTN_TALK", "BTN_RECORD", "BTN_PIC", "BTN_ENLARGE"]
enqueueReadyTask(() => {
console.log(TAG, "开始实时预览,channels:", channels)
VIDEO_PLAYER.startReal(channels)
// 实时预览成功后,设置操作按钮
for (let i = 0; i < channels.length; i++) {
VIDEO_PLAYER.setTabControlBtn(showBtn, i)
}
})
loading.value = true
await waitForReady(0)
if (myToken !== latestRealToken) {
loading.value = false
return
}
const now = Date.now()
const signature = buildRealSignature(channels)
if (signature && signature === lastRealSignature && now - lastRealStartAt < REAL_RESTART_GUARD_MS) {
loading.value = false
return
}
lastRealSignature = signature
lastRealStartAt = now
applyDivisionByCount(Array.isArray(channels) ? channels.length : 1)
console.log(TAG, "开始实时预览,channels:", channels)
VIDEO_PLAYER.startReal(channels)
for (let i = 0; i < channels.length; i++) {
VIDEO_PLAYER.setTabControlBtn(showBtn, i)
}
loading.value = false
}
/**
* 开始录像回放
* @param {PlaybackOptions[]} items 回放参数数组
*/
function startPlayback(items) {
async function startPlayback(items) {
const myToken = ++latestPlaybackToken
if (!VIDEO_PLAYER) {
initVideoPlayer(3)
} else {
ensureWindowType(3)
}
lastPlaybackRequest = items
enqueueReadyTask(() => {
console.log(TAG, "开始录像回放,items:", items)
VIDEO_PLAYER.startPlayback(items)
})
loading.value = true
await waitForReady(3)
if (myToken !== latestPlaybackToken) {
loading.value = false
return
}
const now = Date.now()
const signature = buildPlaybackSignature(items)
if (signature && signature === lastPlaybackSignature && now - lastPlaybackStartAt < REAL_RESTART_GUARD_MS) {
loading.value = false
return
}
lastPlaybackSignature = signature
lastPlaybackStartAt = now
applyDivisionByCount(Array.isArray(items) ? items.length : 1)
console.log(TAG, "开始录像回放,items:", items)
try {
await closeVideo()
} catch (e) {
console.log(TAG, "开始回放前关闭旧视频失败,err:", e)
}
VIDEO_PLAYER.startPlayback(items)
loading.value = false
}
/**
@@ -358,7 +486,7 @@ function updateVisible(visible) {
}
}
function handleVisibilityChange() {
async function handleVisibilityChange() {
if (document.hidden) {
if (!VIDEO_PLAYER) return
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
@@ -376,15 +504,15 @@ function handleVisibilityChange() {
if (!VIDEO_PLAYER) return
VIDEO_PLAYER.show?.()
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
startReal(lastRealRequest)
await waitForReady(0)
await startReal(lastRealRequest)
}
if ((currentWindowType === 3 || currentWindowType === 7) && Array.isArray(lastPlaybackRequest)) {
enqueueReadyTask(() => {
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
}
})
await waitForReady(currentWindowType)
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
}
})
}
}
@@ -447,7 +575,25 @@ function saveVideo(path, filename) {
}
}
async function closeVideo() {
try {
if (VIDEO_PLAYER?.closeVideo) {
await Promise.resolve().then(() => VIDEO_PLAYER.closeVideo())
}
for (const p of players.values()) {
try {
if (p?.closeVideo) {
await Promise.resolve().then(() => p.closeVideo())
}
} catch (e) {
console.log(TAG, "清空播放列表失败,err:", e)
}
}
console.log(TAG, "关闭播放列表成功")
} catch (e) {
console.log(TAG, "关闭播放失败,err:", e)
}
}
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange)
@@ -476,6 +622,7 @@ defineExpose({
startReal,
startPlayback,
updateVisible,
closeVideo,
})
</script>
@@ -488,6 +635,7 @@ defineExpose({
align-items: center;
width: 100%;
height: 100%;
position: relative;
}
.pane {
@@ -499,4 +647,19 @@ defineExpose({
.pane.active {
display: block;
}
.loading-mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.6);
color: #333;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
font-size: 14px;
}
</style>
@@ -1,5 +1,6 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" width="40%" v-model="visible" :close-on-click-modal="false" draggable>
<el-dialog destroy-on-close :title="form.id ? '编辑' : '新增'" width="40%" v-model="visible"
:close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px"
v-loading="loading">
<el-row :gutter="24">
@@ -1,5 +1,5 @@
<template>
<el-dialog title="绑定摄像头" v-model="visible" width="80%" :close-on-click-modal="false" draggable>
<el-dialog destroy-on-close title="绑定摄像头" v-model="visible" width="80%" :close-on-click-modal="false" draggable>
<div class="mb12" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog(
state.queryForm.houseId
@@ -156,6 +156,7 @@ const handleNodeClick = (e: any) => {
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
return
}
@@ -174,6 +175,7 @@ const handleNodeClick = (e: any) => {
recordSource: Number(queryForm.recordSource)
}
]
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
}
@@ -249,6 +251,7 @@ const getDeviceChannelList = async (codeStr: any) => {
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
} catch (error: any) {
console.error(error);
@@ -1,165 +0,0 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<draggable-org-Tree ref="draggableDepTreeRef" placeholder="请输入部门名称" :show-expand="true"
@getSelectNode="handleGetSelectNode" switch-inactive-text="包含" switch-active-text="不包含" :defaultTreeWidth="20"
:nodeClickable="false">
<template #default>
<div class="flex flex-col h-full">
<el-row shadow="hover" v-show="showSearch" class="ml10">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item style="width: 350px;" label="录像时间段" prop="videoDateList">
<el-date-picker v-model="state.queryForm.videoDateList" value-format="YYYY-MM-DD" type="daterange"
:disabled-date="disabledDate" placeholder="请选择日期范围" start-placeholder="开始日期" end-placeholder="结束日期"
@change="handleDateChange" />
</el-form-item>
<el-form-item style="width: 250px;" label="码流" prop="streamType">
<dict-select v-model="state.queryForm.streamType" :options="dh_video_stream_type"
placeholder="请选择码流" />
</el-form-item>
<el-form-item style="width: 250px;" label="录像来源" prop="recordSource">
<dict-select v-model="state.queryForm.recordSource" :options="dh_replay_source"
placeholder="请选择录像来源" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="replayVideoMounted">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<replay-video class="flex-1 overflow-hidden" ref="replayVideoRef" @mounted="replayVideoMounted" />
</div>
</template>
</draggable-org-Tree>
</div>
</div>
</template>
<script setup lang="ts" name="systemDeviceCameraReplay">
import { useMessage } from "/@/hooks/message";
import { useDict } from '/@/hooks/dict';
import * as DeviceCameraReplayApi from "/@/api/third/deviceCameraReplay";
import * as DeviceChannelApi from "/@/api/device/deviceChannel"
// 引入组件
const ReplayVideo = defineAsyncComponent(() => import('../components/replay-video/index.vue'));
// 定义查询字典
const { dh_replay_source, dh_video_stream_type } = useDict("dh_replay_source", "dh_video_stream_type")
// 定义变量内容
const draggableDepTreeRef = ref()
const replayVideoRef = ref()
// 搜索变量
const queryRef = ref()
const showSearch = ref(true)
// 多选变量
const selectObjs = ref([]) as any
const multiple = ref(true)
const state = reactive<any>({
queryForm: {
ownerCode: null,
orgCodes: null,
isInclude: null,
streamType: "1",
recordSource: "1",
videoDateList: getDefaultDateRange()
},
dataList: []
})
const getDataList = async () => {
const { data } = await DeviceChannelApi.getReplayList(state.queryForm)
if (data && data.length) state.dataList = data
}
onMounted(() => {
getDataList()
})
const replayVideoMounted = async () => {
await getDataList()
if (replayVideoRef.value) {
replayVideoRef.value.init(state.dataList, state.queryForm)
}
}
// 获取默认日期范围(前三天)
function getDefaultDateRange() {
const today = new Date()
const threeDaysAgo = new Date(today.getTime() - 2 * 24 * 60 * 60 * 1000) // 前三天(包含今天)
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
return [formatDate(threeDaysAgo), formatDate(today)]
}
// 禁用日期的逻辑
const disabledDate = (time: Date) => {
const today = new Date()
today.setHours(23, 59, 59, 999) // 设置为今天的最后一刻
// 禁用未来日期
if (time.getTime() > today.getTime()) {
return true
}
// 如果已经选择了开始日期,限制结束日期最多3天
const selectedDates = state.queryForm.videoDateList
if (selectedDates && selectedDates.length === 1) {
const startDate = new Date(selectedDates[0])
const maxEndDate = new Date(startDate.getTime() + 2 * 24 * 60 * 60 * 1000) // 最多3天
const minEndDate = new Date(startDate.getTime() - 2 * 24 * 60 * 60 * 1000) // 最少3天前
return time.getTime() > maxEndDate.getTime() || time.getTime() < minEndDate.getTime()
}
return false
}
// 处理日期变化
const handleDateChange = (dates: string[]) => {
if (dates && dates.length === 2) {
const startDate = new Date(dates[0])
const endDate = new Date(dates[1])
const diffTime = Math.abs(endDate.getTime() - startDate.getTime())
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1 // 包含起始日期
if (diffDays > 3) {
useMessage().warning('最多只能选择3天的时间范围')
// 重置为默认值
state.queryForm.videoDateList = getDefaultDateRange()
return
}
}
}
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields()
// 重置日期为默认前三天
state.queryForm.videoDateList = getDefaultDateRange()
// 重置码流为默认值
state.queryForm.streamType = "1"
// 重置录像来源为默认值
state.queryForm.recordSource = "1"
// 清空多选
selectObjs.value = []
draggableDepTreeRef.value.resetData()
getDataList()
}
// 树节点选择时
const handleGetSelectNode = (curClickedNode: any, checkedTreeNodes: any, isDeepTree: boolean) => {
state.queryForm.ownerCode = curClickedNode.id
state.queryForm.orgCodes = checkedTreeNodes.map((item: any) => item.id)
state.queryForm.isInclude = !isDeepTree
getDataList()
}
</script>
@@ -150,6 +150,7 @@ function init(type: string) {
streamType: 2
})
}
previewVideoRef?.value?.closeVideo()
previewVideoRef.value?.startReal([...doorChannelList])
break;
case 'REPLAY':
@@ -169,6 +170,7 @@ function init(type: string) {
recordSource: Number(state.replayForm.recordSource)
})
}
replayVideoRef?.value?.closeVideo()
replayVideoRef.value?.startPlayback([...replayChannelList])
break;
}
+1 -1
View File
@@ -79,7 +79,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
minify: isDev ? 'esbuild' : 'terser',
terserOptions: {
compress: {
drop_console: true, // 删除 console
drop_console: false, // 删除 console
drop_debugger: true, // 删除 debugger
},
format: {