feat:修改

This commit is contained in:
dzc committed 2026-03-16 12:22:40 +08:00
1 parent 53f209d0d3
commit 3369a25a9a
17 files changed
+280 -66493

No files matched your search

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-66430
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

@@ -155,6 +155,7 @@ watch(() => [props.windowNum, state.checkedTreeNodes], (newVal, oldVal) => {
if (newVal[0] !== oldVal[0]) {
state.windowNum = newVal[0]
}
console.log("newVal:", newVal);
setCheckedTreeExt()
})
@@ -165,6 +166,8 @@ const setCheckedTreeExt = () => {
}
state.checkedTreeExtMap = {}
const checkedTreeNodes = state.checkedTreeNodes.filter((item: any) => item.deep == 2)
console.log("checkedTreeNodes:", checkedTreeNodes);
const maxLength = checkedTreeNodes.length
let current = 0
for (let i = 1; i <= targetNum; i++) {
+150 -41
View File
@@ -1,14 +1,19 @@
<template>
<div class="video-container">
<div class="video-container" :style="videoStyle" :class="{ 'preview-mode': currentWindowType === 0 }">
<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" />
<div v-if="pageTotal > 1 && showBar && currentWindowType != 0" style="width: 100%; height: 40px;"></div>
<div v-if="pageTotal > 1 && showBar" :style="videoPageStyle" class="video-container-page">
<el-pagination class="video-pager" :current-page="pageCurrent" :page-size="pageSize" :total="pageTotal"
layout="prev, next, total,jumper" small @size-change="onSizeChange" @current-change="onCurrentChange" />
</div>
</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from "vue"
import { onMounted, onUnmounted, ref, computed, defineEmits } from "vue"
import { ElMessage } from "element-plus";
import dayjs from "dayjs"
@@ -16,12 +21,13 @@ import * as VideoPlayer from "./lib/videoPlayer";
import * as settingApi from "/@/api/com/comDhSetting"
const emit = defineEmits(["changePage"])
const TAG = "|视频预览|"
let VIDEO_PLAYER = null
const divisionSettingList = [1, 2, 4, 6, 8, 9, 13, 16, 20, 25, 36, 64]
let currentWindowType = null
const currentWindowType = ref(null)
let isReady = false
/** @type {Array<() => void>} */
let readyQueue = []
@@ -41,6 +47,51 @@ let lastRealStartAt = 0
let lastPlaybackSignature = ""
let lastPlaybackStartAt = 0
const pageCurrent = ref(1)
const pageSize = ref(9)
const pageTotal = ref(0)
const videoPageStyle = computed(() => {
if (currentWindowType.value == 0) {
return {
left: "0px",
bottom: "0px",
maxWidth: "400px",
height: "40px",
zIndex: 3,
padding: "0px 10px",
}
} else {
return {
left: "0px",
right: "0px",
bottom: "0px",
width: "300px",
height: "40px",
zIndex: 3,
padding: "0px 10px",
}
}
})
function onSizeChange(val) {
if (pageSize.value != val) {
pageSize.value = val
emit("changePage", {
current: pageCurrent.value,
size: val,
})
}
}
function onCurrentChange(val) {
pageCurrent.value = val
emit("changePage", {
current: val,
size: pageSize.value,
})
}
function waitForReady(type = props.windowType) {
return new Promise((resolve) => {
if (isReadyByType.get(type)) {
@@ -59,14 +110,33 @@ function getContainerId(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
const list = Array.isArray(props.divisionSettingList) && props.divisionSettingList.length
? props.divisionSettingList
: [safeCount]
const match = list.find((num) => num >= safeCount)
return match || list[list.length - 1] || safeCount
}
function getMaxDivisionCount() {
const list = Array.isArray(props.divisionSettingList) ? props.divisionSettingList : []
const last = list.length ? Number(list[list.length - 1]) : Number(props.division) || 1
return Math.max(1, last || 1)
}
function clampByDivision(list) {
const max = getMaxDivisionCount()
if (Array.isArray(list) && list.length > max) {
console.log(TAG, "输入数量超过分屏上限,已截取", "max:", max, "prev:", list.length)
return list.slice(0, max)
}
return list
}
function applyDivisionByCount(count) {
if (!VIDEO_PLAYER?.changeDivision) return
const nextDivision = getDivisionByCount(count)
const currentDivision = Number(VIDEO_PLAYER?.setting?.division)
pageSize.value = nextDivision
if (currentDivision === nextDivision) return
VIDEO_PLAYER.changeDivision(nextDivision)
}
@@ -109,22 +179,6 @@ function buildPlaybackSignature(list) {
}).join(";")
}
/**
* 将任务加入就绪队列;若已就绪则立即执行
* @param {() => void} fn 任务函数
*/
function enqueueReadyTask(fn) {
if (isReady) {
try {
fn()
} catch (e) {
console.log(TAG, "就绪任务执行失败,err:", e)
}
} else {
readyQueue.push(fn)
}
}
/**
* 清空就绪队列并执行
*/
@@ -181,6 +235,12 @@ const props = defineProps({
windowType: { type: Number, default: 0 },
/** 默认展示的窗口数量 */
division: { type: Number, default: 1 },
/**
* 分屏配置
* 全部配置[1, 2, 4, 6, 8, 9, 13, 16, 20, 25, 36, 64]
*/
divisionSettingList: { type: Array, default: () => [1, 2, 4, 9, 16] },
// divisionSettingList: { type: Array, default: () => [1] },
/** 窗口拖拽(暂不支持,默认 false) */
draggable: { type: Boolean, default: false },
/** 是否显示底部操作栏 */
@@ -194,7 +254,7 @@ const props = defineProps({
async function initVideoPlayer(type = props.windowType) {
if (players.has(type)) {
VIDEO_PLAYER = players.get(type)
currentWindowType = type
currentWindowType.value = type
isReady = !!isReadyByType.get(type)
setActiveContainer(type)
if (isReady) flushReadyQueue()
@@ -248,7 +308,7 @@ async function initVideoPlayer(type = props.windowType) {
// 底部操作栏, 选传,【true - 显示, false - 隐藏】
showBar: props.showBar,
// 如果DOM元素被插件挡住了,把DOM元素的类名传入。
shieldClass: ["el-select__popper", "el-popper"],
shieldClass: ["el-select__popper", "el-popper", "video-container-page"],
// 如果插件要在dom内滚动,需要把DOM元素的类名传入,请查看案例-遮挡
coverShieldClass: [],
// 有 iframe 时,top层 的 dom 元素被插件挡住了,把DOM元素的类名传入。
@@ -259,14 +319,16 @@ async function initVideoPlayer(type = props.windowType) {
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)
setTimeout(() => {
flushReadyQueue()
const waiters = readyWaitersByType.get(type) || []
waiters.forEach((fn) => {
try { fn() } catch (_) { }
})
readyWaitersByType.delete(type)
loading.value = false
resolve(null)
}, 500)
},
// 创建播放器失败回调
createError: (err) => {
@@ -375,7 +437,7 @@ async function initVideoPlayer(type = props.windowType) {
console.log(TAG, "RTSP录像下载失败,info:", info, "err:", err)
}
})
currentWindowType = type
currentWindowType.value = type
players.set(type, VIDEO_PLAYER)
console.log(TAG, "播放器已初始化,windowType:", type)
})
@@ -389,8 +451,8 @@ async function initVideoPlayer(type = props.windowType) {
* @returns {boolean} 是否发生重新创建
*/
function ensureWindowType(target) {
if (currentWindowType !== target) {
console.log(TAG, "检测到窗口类型不一致,当前:", currentWindowType, "目标:", target, "正在重新创建")
if (currentWindowType.value !== target) {
console.log(TAG, "检测到窗口类型不一致,当前:", currentWindowType.value, "目标:", target, "正在重新创建")
initVideoPlayer(target)
return true
}
@@ -402,6 +464,7 @@ function ensureWindowType(target) {
* @param {RealOptions[]} channels 播放参数数组
*/
async function startReal(channels) {
channels = clampByDivision(channels)
const myToken = ++latestRealToken
if (!VIDEO_PLAYER) {
initVideoPlayer(0)
@@ -439,6 +502,7 @@ async function startReal(channels) {
* @param {PlaybackOptions[]} items 回放参数数组
*/
async function startPlayback(items) {
items = clampByDivision(items)
const myToken = ++latestPlaybackToken
if (!VIDEO_PLAYER) {
initVideoPlayer(3)
@@ -489,10 +553,10 @@ function updateVisible(visible) {
async function handleVisibilityChange() {
if (document.hidden) {
if (!VIDEO_PLAYER) return
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
if (currentWindowType.value === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
VIDEO_PLAYER.closeVideo?.()
}
if ((currentWindowType === 3 || currentWindowType === 7) && Array.isArray(lastPlaybackRequest)) {
if ((currentWindowType.value === 3 || currentWindowType.value === 7) && Array.isArray(lastPlaybackRequest)) {
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 0 })
@@ -503,12 +567,12 @@ async function handleVisibilityChange() {
} else {
if (!VIDEO_PLAYER) return
VIDEO_PLAYER.show?.()
if (currentWindowType === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
if (currentWindowType.value === 0 && Array.isArray(lastRealRequest) && lastRealRequest.length) {
await waitForReady(0)
await startReal(lastRealRequest)
}
if ((currentWindowType === 3 || currentWindowType === 7) && Array.isArray(lastPlaybackRequest)) {
await waitForReady(currentWindowType)
if ((currentWindowType.value === 3 || currentWindowType.value === 7) && Array.isArray(lastPlaybackRequest)) {
await waitForReady(currentWindowType.value)
lastPlaybackRequest.forEach((item) => {
if (item && typeof item.snum === "number") {
VIDEO_PLAYER.controlPlayback?.({ snum: item.snum, state: 1 })
@@ -595,6 +659,13 @@ async function closeVideo() {
}
}
function setTotal(total) {
if (total > 0) {
pageTotal.value = total
console.log(TAG, "设置视频总数,total:", total)
}
}
onMounted(() => {
document.addEventListener("visibilitychange", handleVisibilityChange)
})
@@ -623,6 +694,7 @@ defineExpose({
startPlayback,
updateVisible,
closeVideo,
setTotal,
})
</script>
@@ -631,16 +703,18 @@ defineExpose({
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: relative;
background: #2F3235;
}
.pane {
width: 100%;
height: 100%;
flex: 1;
display: none;
}
@@ -662,4 +736,39 @@ defineExpose({
z-index: 2;
font-size: 14px;
}
.video-container-page {
position: absolute;
pointer-events: auto;
}
:deep(.video-pager) {
background: #2F3236;
border-radius: 20px;
padding: 6px 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
:deep(.el-pagination) {
margin: 0 !important;
}
:deep(.el-pagination .btn-prev),
:deep(.el-pagination .btn-next),
:deep(.el-pagination.is-background .el-pager li) {
background-color: transparent;
color: #c9d8e3;
}
:deep(.el-pagination .el-select .el-select__wrapper) {
background-color: transparent;
border-color: #B4BAC2;
color: #c9d8e3;
}
@media (max-width: 1150px) {
.preview-mode .video-container-page {
display: none;
}
}
</style>
@@ -27,9 +27,9 @@
<el-table-column prop="channelName" width="160" label="通道名" />
<el-table-column prop="deviceIp" width="120" label="ip" />
<el-table-column prop="devicePort" width="120" label="端口" />
<el-table-column prop="devicePort" width="120" label="状态">
<el-table-column prop="deviceIsOnline" width="120" label="状态">
<template #default="{ row }">
<tag-online :isOnline="row.isOnline == 1" />
<tag-online :isOnline="row.deviceIsOnline == 1" />
</template>
</el-table-column>
</el-table-column>
@@ -15,7 +15,7 @@
</el-tree>
</div>
<div class="flex-1 overflow-hidden video-container">
<RtspPluginVideo ref="previewVideoRef" style="height: 500px;" />
<RtspPluginVideo ref="previewVideoRef" style="height: 500px;" @changePage="videoChangePage" />
</div>
</div>
<template #footer>
@@ -40,6 +40,8 @@ const state = reactive({
channelList: [],
loading: false,
treeLoading: false,
current: 1,
size: 64,
})
const detail = reactive<any>({})
@@ -63,6 +65,32 @@ const openDialog = async (id: string) => {
await getDeviceChannelList()
};
const videoChangePage = async ({ current, size }: any) => {
state.current = current
state.size = size
console.log("state:", state);
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
// 默认辅码流
streamType: 2
})
}
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end).map((v: any, idx: number) => ({
...v,
snum: idx
}))
await previewVideoRef.value?.closeVideo()
previewVideoRef.value?.setTotal(channelList.length)
previewVideoRef.value?.startReal(pageList)
}
// 点击树
const handleNodeClick = (e: any) => {
if (e.id == -1) {
@@ -78,7 +106,15 @@ const handleNodeClick = (e: any) => {
streamType: 2
})
}
previewVideoRef.value?.startReal(channelList)
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end).map((v: any, idx: number) => ({
...v,
snum: idx
}))
previewVideoRef.value?.setTotal(channelList.length)
previewVideoRef.value?.startReal(pageList)
return
}
const targetChannel = channel.list.find((v: any) => v.channelCode == e.channelCode)
@@ -146,7 +182,12 @@ const getDeviceChannelList = async () => {
streamType: 2
})
}
previewVideoRef.value?.startReal(channelList)
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end)
previewVideoRef.value?.setTotal(channelList.length)
previewVideoRef.value?.startReal(pageList)
} catch (error: any) {
console.error("|查询通道树|,意外的错误", error);
useMessage().error(error?.msg ?? "意外的错误");
@@ -36,7 +36,7 @@
</el-form-item>
</el-form>
<div class="flex-1 overflow-hidden video-container" style="height: 500px;">
<RtspPluginVideo ref="replayVideoRef" style="height: 500px;" />
<RtspPluginVideo ref="replayVideoRef" style="height: 500px;" @changePage="videoChangePage" />
</div>
</div>
</div>
@@ -73,7 +73,9 @@ const state = reactive<any>({
treeLoading: false,
numList: [1, 4, 9, 16, 25],
type: "device",
code: null
code: null,
current: 1,
size: 64,
})
const detail = reactive<any>({})
@@ -84,7 +86,9 @@ const queryForm = reactive({
isInclude: null,
streamType: "0",
recordSource: "2",
videoDateList: getDefaultDateRange()
videoDateList: getDefaultDateRange(),
current: 1,
size: 64,
})
const channel = reactive<any>({
@@ -136,6 +140,37 @@ const handleClose = () => {
state.dialogVisible = false
}
const videoChangePage = async ({ current, size }: any) => {
state.current = current
state.size = size
console.log("state:", state);
const channelList: any[] = []
for (let i = 0; i < channel.list.length; i++) {
const v: any = channel.list[i]
channelList.push({
channelId: v.channelCode,
channelName: v.channelName,
snum: i,
// 默认辅码流
streamType: Number(queryForm.streamType),
// 默认前3天(格式YYYY-MM-DD HH:mm:ss)
startTime: queryForm.videoDateList[0],
endTime: queryForm.videoDateList[1],
// 录像来源:2-设备录像 3-中心录像
recordSource: Number(queryForm.recordSource)
})
}
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end).map((v: any, idx: number) => ({
...v,
snum: idx
}))
await replayVideoRef.value?.closeVideo()
replayVideoRef.value?.setTotal(channelList.length)
replayVideoRef.value?.startPlayback(pageList)
}
// 点击树
const handleNodeClick = (e: any) => {
if (e.id == -1) {
@@ -156,8 +191,15 @@ const handleNodeClick = (e: any) => {
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end).map((v: any, idx: number) => ({
...v,
snum: idx
}))
replayVideoRef.value?.setTotal(channelList.length)
replayVideoRef.value?.startPlayback(pageList)
return
}
const targetChannel = channel.list.find((v: any) => v.channelCode == e.channelCode)
@@ -175,7 +217,6 @@ const handleNodeClick = (e: any) => {
recordSource: Number(queryForm.recordSource)
}
]
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
}
@@ -251,8 +292,17 @@ const getDeviceChannelList = async (codeStr: any) => {
recordSource: Number(queryForm.recordSource)
})
}
replayVideoRef.value?.closeVideo()
replayVideoRef.value?.startPlayback([...channelList])
// 按照分页截取
const start = (state.current - 1) * state.size
const end = start + state.size
const pageList = channelList.slice(start, end).map((v: any, idx: number) => ({
...v,
snum: idx
}))
replayVideoRef.value?.setTotal(channelList.length)
replayVideoRef.value?.startPlayback(pageList)
} catch (error: any) {
console.error(error);
useMessage().error(error?.msg ?? "意外的错误");
+23 -9
View File
@@ -10,7 +10,8 @@
<el-tab-pane v-for="(v, i) in data.tabs" :key="v.name" :label="v.label" :name="v.name" />
</el-tabs>
<div class="w-full h-full flex-1 overflow-hidden" v-loading="videoLoading">
<RtspPluginVideo ref="previewVideoRef" v-if="state.currentTab == 'PREVIEW'" />
<RtspPluginVideo ref="previewVideoRef" @changePage="videoChangePage"
:divisionSettingList="divisionSettingList" v-if="state.currentTab == 'PREVIEW'" />
<div class="w-full h-full flex flex-col" v-if="state.currentTab == 'REPLAY'">
<el-row shadow="hover" class="ml10">
<el-form :model="state.replayForm" ref="queryRef" :inline="true"
@@ -40,7 +41,8 @@
</el-form-item>
</el-form>
</el-row>
<RtspPluginVideo class="flex-1 overflow-hidden" ref="replayVideoRef" />
<RtspPluginVideo class="flex-1 overflow-hidden" ref="replayVideoRef"
:divisionSettingList="divisionSettingList" @changePage="videoChangePage" />
</div>
</div>
</div>
@@ -63,6 +65,8 @@ const queryRef = ref()
const videoLoading = ref(false)
const divisionSettingList = ref([9])
// 定义查询字典
const { dh_replay_source, dh_video_stream_type } = useDict("dh_replay_source", "dh_video_stream_type")
@@ -79,7 +83,9 @@ const data = {
]
}
const state = reactive({
const state = reactive<any>({
current: 1,
size: 64,
currentTab: data.tabs[0].name,
deviceTreeMap: [],
channelCodeList: [],
@@ -96,10 +102,6 @@ onMounted(() => {
})
const windowNumChanged = (num: any) => {
state.windowNum = num
}
// 树节点选择时
const handleGetSelectNode = async (curClickedNode: any, checkedTreeNodes: any, isDeepTree: boolean, selectMap: any) => {
if (!checkedTreeNodes || !checkedTreeNodes.length) return
@@ -117,20 +119,28 @@ function tabClick(e: any) {
init(e.paneName)
}
const videoChangePage = ({ current, size }: any) => {
state.current = current
state.size = size
init(state.currentTab)
}
function init(type: string) {
nextTick(async () => {
videoLoading.value = true
await new Promise(resolve => setTimeout(resolve, 500))
// 查询通道列表
let channelRecords = []
let total = 0
try {
const _params = {
current: 1,
size: 64,
current: state.current,
size: state.size,
deviceTreeMap: state.deviceTreeMap ?? null
}
let { code, data } = await deviceChannelApi.getChannelPage(_params);
if (code != 0) throw new Error("")
total = data?.total ?? 0
if (data.records && data.records.length) {
channelRecords = data.records
}
@@ -150,7 +160,9 @@ function init(type: string) {
streamType: 2
})
}
state.windowNum = doorChannelList.length > 9 ? 9 : doorChannelList.length
previewVideoRef?.value?.closeVideo()
previewVideoRef?.value?.setTotal(total)
previewVideoRef.value?.startReal([...doorChannelList])
break;
case 'REPLAY':
@@ -170,7 +182,9 @@ function init(type: string) {
recordSource: Number(state.replayForm.recordSource)
})
}
state.windowNum = replayChannelList.length > 9 ? 9 : replayChannelList.length
replayVideoRef?.value?.closeVideo()
replayVideoRef?.value?.setTotal(total)
replayVideoRef.value?.startPlayback([...replayChannelList])
break;
}