|
|
@ -1,7 +1,12 @@ |
|
|
<template> |
|
|
<template> |
|
|
<div class="video-console live-preview" :class="{ 'is-embed': embed }"> |
|
|
<div class="video-console live-preview" :class="{ 'is-embed': embed }"> |
|
|
<div class="console-body"> |
|
|
<div class="console-body"> |
|
|
<div v-show="!embed || embedTreeVisible" class="tree-panel" :style="{ width: treePanelWidth + 'px' }"> |
|
|
|
|
|
|
|
|
<div |
|
|
|
|
|
v-show="!embed || embedTreeVisible" |
|
|
|
|
|
class="tree-panel" |
|
|
|
|
|
:style="embedTreeStyle" |
|
|
|
|
|
@mousedown="startEmbedPanelDrag('tree', $event)" |
|
|
|
|
|
> |
|
|
<nvr-device-tree |
|
|
<nvr-device-tree |
|
|
v-if="!embed || sessionReady" |
|
|
v-if="!embed || sessionReady" |
|
|
:config="config" |
|
|
:config="config" |
|
|
@ -26,19 +31,25 @@ |
|
|
<span /> |
|
|
<span /> |
|
|
<span /> |
|
|
<span /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button |
|
|
|
|
|
v-if="embed" |
|
|
|
|
|
type="button" |
|
|
|
|
|
class="embed-toggle embed-toggle-left" |
|
|
|
|
|
:title="embedTreeVisible ? '隐藏设备列表' : '显示设备列表'" |
|
|
|
|
|
@mousedown.stop |
|
|
|
|
|
@click.stop="embedTreeVisible = !embedTreeVisible" |
|
|
|
|
|
> |
|
|
|
|
|
<img :src="treeToggleIcon" alt=""> |
|
|
|
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<button |
|
|
<button |
|
|
v-if="embed" |
|
|
|
|
|
|
|
|
v-if="embed && !embedTreeVisible" |
|
|
type="button" |
|
|
type="button" |
|
|
class="embed-toggle embed-toggle-left" |
|
|
|
|
|
:class="{ open: embedTreeVisible }" |
|
|
|
|
|
:style="embedTreeVisible ? { left: treePanelWidth + 'px' } : null" |
|
|
|
|
|
:title="embedTreeVisible ? '隐藏设备列表' : '显示设备列表'" |
|
|
|
|
|
@click="embedTreeVisible = !embedTreeVisible" |
|
|
|
|
|
style="transform: rotate(180deg) translateY(50%)" |
|
|
|
|
|
|
|
|
class="embed-toggle embed-toggle-left is-collapsed" |
|
|
|
|
|
title="显示设备列表" |
|
|
|
|
|
@click="embedTreeVisible = true" |
|
|
> |
|
|
> |
|
|
<!-- <img :src="embedTreeVisible ? treeCollapseIcon : treeToggleIcon" alt=""> --> |
|
|
|
|
|
<img :src="embedTreeVisible ? treeToggleIcon : treeCollapseIcon" alt=""> |
|
|
|
|
|
|
|
|
<img :src="treeCollapseIcon" alt=""> |
|
|
</button> |
|
|
</button> |
|
|
|
|
|
|
|
|
<main class="monitor-workspace"> |
|
|
<main class="monitor-workspace"> |
|
|
@ -80,7 +91,13 @@ |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
</main> |
|
|
|
|
|
|
|
|
<aside v-show="embed ? embedControlVisible : controlPanelExpanded" class="control-panel"> |
|
|
|
|
|
|
|
|
<aside |
|
|
|
|
|
v-show="embed ? embedControlVisible : controlPanelExpanded" |
|
|
|
|
|
class="control-panel" |
|
|
|
|
|
:style="embedControlStyle" |
|
|
|
|
|
@mousedown="startEmbedPanelDrag('control', $event)" |
|
|
|
|
|
> |
|
|
|
|
|
<div class="control-panel-body"> |
|
|
<section v-if="embed" class="control-section embed-layout-section"> |
|
|
<section v-if="embed" class="control-section embed-layout-section"> |
|
|
<div class="section-title"><span>画面布局</span></div> |
|
|
<div class="section-title"><span>画面布局</span></div> |
|
|
<div class="embed-layout-switch" aria-label="画面布局"> |
|
|
<div class="embed-layout-switch" aria-label="画面布局"> |
|
|
@ -96,7 +113,7 @@ |
|
|
<span |
|
|
<span |
|
|
class="embed-layout-icon" |
|
|
class="embed-layout-icon" |
|
|
:class="`embed-layout-icon-${item.columns}`" |
|
|
:class="`embed-layout-icon-${item.columns}`" |
|
|
:style="{ gridTemplateColumns: `repeat(${item.columns}, auto)` }" |
|
|
|
|
|
|
|
|
:style="{ gridTemplateColumns: `repeat(${item.columns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${item.columns}, minmax(0, 1fr))` }" |
|
|
> |
|
|
> |
|
|
<i v-for="cell in item.value" :key="cell" /> |
|
|
<i v-for="cell in item.value" :key="cell" /> |
|
|
</span> |
|
|
</span> |
|
|
@ -199,16 +216,26 @@ |
|
|
<button type="button" @click="showPendingOperation('3D定位')"><i class="el-icon-aim" />3D定位</button> |
|
|
<button type="button" @click="showPendingOperation('3D定位')"><i class="el-icon-aim" />3D定位</button> |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
|
|
|
</div> |
|
|
|
|
|
<button |
|
|
|
|
|
type="button" |
|
|
|
|
|
class="embed-toggle embed-toggle-right" |
|
|
|
|
|
:class="{ 'is-page': !embed }" |
|
|
|
|
|
title="隐藏云台控制" |
|
|
|
|
|
@mousedown.stop |
|
|
|
|
|
@click.stop="toggleControlPanel(false)" |
|
|
|
|
|
> |
|
|
|
|
|
<img :src="treeToggleIcon" alt=""> |
|
|
|
|
|
</button> |
|
|
</aside> |
|
|
</aside> |
|
|
<button |
|
|
<button |
|
|
v-if="embed" |
|
|
|
|
|
|
|
|
v-if="!(embed ? embedControlVisible : controlPanelExpanded)" |
|
|
type="button" |
|
|
type="button" |
|
|
class="embed-toggle embed-toggle-right" |
|
|
|
|
|
:class="{ open: embedControlVisible }" |
|
|
|
|
|
:title="embedControlVisible ? '隐藏云台控制' : '显示云台控制'" |
|
|
|
|
|
@click="embedControlVisible = !embedControlVisible" |
|
|
|
|
|
|
|
|
class="embed-toggle embed-toggle-right is-collapsed" |
|
|
|
|
|
title="显示云台控制" |
|
|
|
|
|
@click="toggleControlPanel(true)" |
|
|
> |
|
|
> |
|
|
<img :src="embedControlVisible ? treeToggleIcon : treeCollapseIcon" alt=""> |
|
|
|
|
|
|
|
|
<img :src="treeCollapseIcon" alt=""> |
|
|
</button> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@ -257,9 +284,15 @@ const PTZ_CODES = { |
|
|
controlPanelExpanded: false, |
|
|
controlPanelExpanded: false, |
|
|
embedTreeVisible: false, |
|
|
embedTreeVisible: false, |
|
|
embedControlVisible: false, |
|
|
embedControlVisible: false, |
|
|
|
|
|
embedPanelDrag: { |
|
|
|
|
|
tree: { x: 0, y: 0, moved: false }, |
|
|
|
|
|
control: { x: 0, y: 0, moved: false } |
|
|
|
|
|
}, |
|
|
|
|
|
embedDragState: null, |
|
|
treeToggleIcon, |
|
|
treeToggleIcon, |
|
|
treeCollapseIcon, |
|
|
treeCollapseIcon, |
|
|
treePanelWidth: 400, |
|
|
|
|
|
|
|
|
treePanelWidth: 242, |
|
|
|
|
|
controlPanelWidth: 242, |
|
|
isTreeResizing: false, |
|
|
isTreeResizing: false, |
|
|
treeResizeStartX: 0, |
|
|
treeResizeStartX: 0, |
|
|
treeResizeStartWidth: 0, |
|
|
treeResizeStartWidth: 0, |
|
|
@ -275,6 +308,8 @@ const PTZ_CODES = { |
|
|
{ value: 9, columns: 3, label: '九画面' }, { value: 16, columns: 4, label: '十六画面' } |
|
|
{ value: 9, columns: 3, label: '九画面' }, { value: 16, columns: 4, label: '十六画面' } |
|
|
], |
|
|
], |
|
|
slots: Array.from({ length: 16 }, () => null), |
|
|
slots: Array.from({ length: 16 }, () => null), |
|
|
|
|
|
// 实际开过的视频,按开播顺序记录。宫格变少时从末尾往前留。 |
|
|
|
|
|
playbackHistory: [], |
|
|
activeIndex: 0, |
|
|
activeIndex: 0, |
|
|
controlTarget: null, |
|
|
controlTarget: null, |
|
|
placementCursor: 0, |
|
|
placementCursor: 0, |
|
|
@ -343,7 +378,19 @@ const PTZ_CODES = { |
|
|
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${columns}, minmax(0, 1fr))` } |
|
|
return { gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, gridTemplateRows: `repeat(${columns}, minmax(0, 1fr))` } |
|
|
}, |
|
|
}, |
|
|
selectedChannel() { return this.controlTarget }, |
|
|
selectedChannel() { return this.controlTarget }, |
|
|
selectedChannelName() { return this.controlTarget ? (this.controlTarget.name || `通道 ${this.controlTarget.channel}`) : '未选择画面' } |
|
|
|
|
|
|
|
|
selectedChannelName() { return this.controlTarget ? (this.controlTarget.name || `通道 ${this.controlTarget.channel}`) : '未选择画面' }, |
|
|
|
|
|
embedTreeStyle() { |
|
|
|
|
|
if (!this.embed) return { width: this.treePanelWidth + 'px' } |
|
|
|
|
|
const drag = this.embedPanelDrag.tree |
|
|
|
|
|
return drag.moved |
|
|
|
|
|
? { width: this.treePanelWidth + 'px', left: drag.x + 'px' } |
|
|
|
|
|
: { width: this.treePanelWidth + 'px' } |
|
|
|
|
|
}, |
|
|
|
|
|
embedControlStyle() { |
|
|
|
|
|
const width = { width: this.controlPanelWidth + 'px' } |
|
|
|
|
|
const drag = this.embedPanelDrag.control |
|
|
|
|
|
return drag.moved ? { ...width, left: drag.x + 'px', right: 'auto' } : width |
|
|
|
|
|
} |
|
|
}, |
|
|
}, |
|
|
watch: { |
|
|
watch: { |
|
|
sessionReady(ready) { |
|
|
sessionReady(ready) { |
|
|
@ -383,6 +430,7 @@ const PTZ_CODES = { |
|
|
}, |
|
|
}, |
|
|
beforeDestroy() { |
|
|
beforeDestroy() { |
|
|
this.stopTreeResize() |
|
|
this.stopTreeResize() |
|
|
|
|
|
this.stopEmbedPanelDrag() |
|
|
this.lifecycleId += 1 |
|
|
this.lifecycleId += 1 |
|
|
this.pageActive = false |
|
|
this.pageActive = false |
|
|
document.removeEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
document.removeEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
@ -406,7 +454,7 @@ const PTZ_CODES = { |
|
|
onTreeResize(event) { |
|
|
onTreeResize(event) { |
|
|
if (!this.isTreeResizing) return |
|
|
if (!this.isTreeResizing) return |
|
|
const width = this.treeResizeStartWidth + event.clientX - this.treeResizeStartX |
|
|
const width = this.treeResizeStartWidth + event.clientX - this.treeResizeStartX |
|
|
this.treePanelWidth = Math.max(260, Math.min(620, width)) |
|
|
|
|
|
|
|
|
this.treePanelWidth = Math.max(218, Math.min(620, width)) |
|
|
}, |
|
|
}, |
|
|
stopTreeResize() { |
|
|
stopTreeResize() { |
|
|
if (!this.isTreeResizing) return |
|
|
if (!this.isTreeResizing) return |
|
|
@ -416,6 +464,45 @@ const PTZ_CODES = { |
|
|
document.body.style.cursor = '' |
|
|
document.body.style.cursor = '' |
|
|
document.body.style.userSelect = '' |
|
|
document.body.style.userSelect = '' |
|
|
}, |
|
|
}, |
|
|
|
|
|
startEmbedPanelDrag(panel, event) { |
|
|
|
|
|
if (event.button !== 0) return |
|
|
|
|
|
if (!this.embed && panel !== 'control') return |
|
|
|
|
|
if (event.target.closest('button, a, input, textarea, .el-input, .el-select, .el-input-number, .el-tree-node, .tree-content, .embed-toggle')) return |
|
|
|
|
|
const bounds = event.currentTarget.getBoundingClientRect() |
|
|
|
|
|
const parent = event.currentTarget.offsetParent |
|
|
|
|
|
? event.currentTarget.offsetParent.getBoundingClientRect() |
|
|
|
|
|
: { left: 0, top: 0 } |
|
|
|
|
|
this.embedDragState = { |
|
|
|
|
|
panel, |
|
|
|
|
|
startX: event.clientX, |
|
|
|
|
|
startY: event.clientY, |
|
|
|
|
|
originX: bounds.left - parent.left, |
|
|
|
|
|
originY: bounds.top - parent.top |
|
|
|
|
|
} |
|
|
|
|
|
window.getSelection && window.getSelection().removeAllRanges() |
|
|
|
|
|
document.body.style.userSelect = 'none' |
|
|
|
|
|
document.addEventListener('mousemove', this.onEmbedPanelDrag) |
|
|
|
|
|
document.addEventListener('mouseup', this.stopEmbedPanelDrag) |
|
|
|
|
|
}, |
|
|
|
|
|
onEmbedPanelDrag(event) { |
|
|
|
|
|
const drag = this.embedDragState |
|
|
|
|
|
if (!drag) return |
|
|
|
|
|
const panel = this.embedPanelDrag[drag.panel] |
|
|
|
|
|
const parent = this.$el.querySelector('.console-body') |
|
|
|
|
|
const box = parent ? parent.getBoundingClientRect() : { width: window.innerWidth, height: window.innerHeight } |
|
|
|
|
|
const node = parent ? parent.querySelector(drag.panel === 'tree' ? '.tree-panel' : '.control-panel') : null |
|
|
|
|
|
const width = node ? node.offsetWidth : 0 |
|
|
|
|
|
panel.x = Math.max(0, Math.min(box.width - width, drag.originX + event.clientX - drag.startX)) |
|
|
|
|
|
panel.moved = true |
|
|
|
|
|
}, |
|
|
|
|
|
stopEmbedPanelDrag() { |
|
|
|
|
|
if (!this.embedDragState) return |
|
|
|
|
|
this.embedDragState = null |
|
|
|
|
|
document.body.style.userSelect = '' |
|
|
|
|
|
window.getSelection && window.getSelection().removeAllRanges() |
|
|
|
|
|
document.removeEventListener('mousemove', this.onEmbedPanelDrag) |
|
|
|
|
|
document.removeEventListener('mouseup', this.stopEmbedPanelDrag) |
|
|
|
|
|
}, |
|
|
async loadConfig() { |
|
|
async loadConfig() { |
|
|
this.loading = true |
|
|
this.loading = true |
|
|
this.connectionText = '读取配置' |
|
|
this.connectionText = '读取配置' |
|
|
@ -496,19 +583,75 @@ const PTZ_CODES = { |
|
|
if (treeCodes.size) this.channels = this.channels.filter(item => treeCodes.has(item.cameraCode)) |
|
|
if (treeCodes.size) this.channels = this.channels.filter(item => treeCodes.has(item.cameraCode)) |
|
|
this.setAvailableChannels(channels, true) |
|
|
this.setAvailableChannels(channels, true) |
|
|
}, |
|
|
}, |
|
|
async populateDefaultLayout() { |
|
|
|
|
|
if (!this.pageActive) return |
|
|
|
|
|
// 几宫格就按频道列表顺序默认播放前几个,不做随机洗牌 |
|
|
|
|
|
const channels = this.channels.slice(0, this.capacity) |
|
|
|
|
|
if (!channels.length) return |
|
|
|
|
|
|
|
|
isChannelOnline(channel) { |
|
|
|
|
|
if (!channel) return false |
|
|
|
|
|
const status = channel.patroldeviceOnlineStatus |
|
|
|
|
|
if (status === undefined || status === null || status === '') return true |
|
|
|
|
|
return String(status) === '1' |
|
|
|
|
|
}, |
|
|
|
|
|
onlineChannels(channels) { |
|
|
|
|
|
return (channels || []).filter(item => this.isChannelOnline(item)) |
|
|
|
|
|
}, |
|
|
|
|
|
shuffleChannels(channels) { |
|
|
|
|
|
const list = (channels || []).slice() |
|
|
|
|
|
for (let index = list.length - 1; index > 0; index -= 1) { |
|
|
|
|
|
const swapIndex = Math.floor(Math.random() * (index + 1)) |
|
|
|
|
|
const current = list[index] |
|
|
|
|
|
list[index] = list[swapIndex] |
|
|
|
|
|
list[swapIndex] = current |
|
|
|
|
|
} |
|
|
|
|
|
return list |
|
|
|
|
|
}, |
|
|
|
|
|
playbackSnapshot(channel) { |
|
|
|
|
|
if (!channel || !channel.cameraCode) return null |
|
|
|
|
|
return { |
|
|
|
|
|
cameraCode: channel.cameraCode, |
|
|
|
|
|
stationCode: channel.stationCode, |
|
|
|
|
|
name: channel.name || channel.cameraName, |
|
|
|
|
|
channel: channel.channel, |
|
|
|
|
|
channelCode: channel.channelCode, |
|
|
|
|
|
nvrIp: channel.nvrIp || '' |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
rememberPlayback(channel, replacedCode) { |
|
|
|
|
|
const snapshot = this.playbackSnapshot(channel) |
|
|
|
|
|
if (!snapshot) return |
|
|
|
|
|
if (replacedCode && replacedCode !== snapshot.cameraCode) { |
|
|
|
|
|
const stillPlaying = this.slots.some(slot => slot && slot.cameraCode === replacedCode) |
|
|
|
|
|
if (!stillPlaying) { |
|
|
|
|
|
this.playbackHistory = this.playbackHistory.filter(item => item.cameraCode !== replacedCode) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
this.playbackHistory = this.playbackHistory.filter(item => item.cameraCode !== snapshot.cameraCode) |
|
|
|
|
|
this.playbackHistory.push(snapshot) |
|
|
|
|
|
}, |
|
|
|
|
|
forgetPlayback(cameraCode) { |
|
|
|
|
|
if (!cameraCode) return |
|
|
|
|
|
const stillPlaying = this.slots.some(slot => slot && slot.cameraCode === cameraCode) |
|
|
|
|
|
if (stillPlaying) return |
|
|
|
|
|
this.playbackHistory = this.playbackHistory.filter(item => item.cameraCode !== cameraCode) |
|
|
|
|
|
}, |
|
|
|
|
|
async playChannels(channels, highlightCode) { |
|
|
await Promise.all(this.slots.map((slot, index) => slot ? this.closeSlot(index, false) : null)) |
|
|
await Promise.all(this.slots.map((slot, index) => slot ? this.closeSlot(index, false) : null)) |
|
|
if (!this.pageActive) return |
|
|
if (!this.pageActive) return |
|
|
|
|
|
this.playbackHistory = [] |
|
|
this.activeIndex = 0 |
|
|
this.activeIndex = 0 |
|
|
this.controlTarget = null |
|
|
this.controlTarget = null |
|
|
for (let index = 0; index < channels.length; index += 1) { |
|
|
for (let index = 0; index < channels.length; index += 1) { |
|
|
if (!this.pageActive) return |
|
|
if (!this.pageActive) return |
|
|
await this.startChannel(channels[index], index) |
|
|
await this.startChannel(channels[index], index) |
|
|
} |
|
|
} |
|
|
|
|
|
if (highlightCode) { |
|
|
|
|
|
const highlightIndex = channels.findIndex(item => item && item.cameraCode === highlightCode) |
|
|
|
|
|
if (highlightIndex >= 0) this.selectSlot(highlightIndex) |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
async populateDefaultLayout() { |
|
|
|
|
|
if (!this.pageActive) return |
|
|
|
|
|
// 初次打开按宫格数量随机抽在线通道,离线的不进默认画面 |
|
|
|
|
|
const channels = this.shuffleChannels(this.onlineChannels(this.channels)).slice(0, this.capacity) |
|
|
|
|
|
if (!channels.length) return |
|
|
|
|
|
await this.playChannels(channels) |
|
|
}, |
|
|
}, |
|
|
openChannel(channel) { |
|
|
openChannel(channel) { |
|
|
this.selectChannel(channel) |
|
|
this.selectChannel(channel) |
|
|
@ -520,8 +663,10 @@ const PTZ_CODES = { |
|
|
const current = this.slots[index] |
|
|
const current = this.slots[index] |
|
|
if (current && current.cameraCode === channel.cameraCode && current.streamType === streamType) { |
|
|
if (current && current.cameraCode === channel.cameraCode && current.streamType === streamType) { |
|
|
this.controlTarget = current |
|
|
this.controlTarget = current |
|
|
|
|
|
this.rememberPlayback(current) |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
|
|
|
const replacedCode = current && current.cameraCode |
|
|
if (this.slots[index]) await this.closeSlot(index, false) |
|
|
if (this.slots[index]) await this.closeSlot(index, false) |
|
|
if (!this.pageActive) return |
|
|
if (!this.pageActive) return |
|
|
const slot = { |
|
|
const slot = { |
|
|
@ -536,6 +681,7 @@ const PTZ_CODES = { |
|
|
streamType |
|
|
streamType |
|
|
} |
|
|
} |
|
|
this.$set(this.slots, index, slot) |
|
|
this.$set(this.slots, index, slot) |
|
|
|
|
|
this.rememberPlayback(channel, replacedCode) |
|
|
if (index === this.activeIndex) { |
|
|
if (index === this.activeIndex) { |
|
|
this.controlTarget = slot |
|
|
this.controlTarget = slot |
|
|
this.selectedCode = channel.cameraCode |
|
|
this.selectedCode = channel.cameraCode |
|
|
@ -598,9 +744,11 @@ const PTZ_CODES = { |
|
|
async closeSlot(index, notify = true) { |
|
|
async closeSlot(index, notify = true) { |
|
|
const slot = this.slots[index] |
|
|
const slot = this.slots[index] |
|
|
if (!slot) return |
|
|
if (!slot) return |
|
|
|
|
|
const cameraCode = slot.cameraCode |
|
|
if (index === this.activeIndex && this.ptzActive) this.stopPtz() |
|
|
if (index === this.activeIndex && this.ptzActive) this.stopPtz() |
|
|
clearTimeout(slot.timer) |
|
|
clearTimeout(slot.timer) |
|
|
this.$set(this.slots, index, null) |
|
|
this.$set(this.slots, index, null) |
|
|
|
|
|
this.forgetPlayback(cameraCode) |
|
|
if (this.controlTarget === slot) this.controlTarget = null |
|
|
if (this.controlTarget === slot) this.controlTarget = null |
|
|
if (index === this.activeIndex) { |
|
|
if (index === this.activeIndex) { |
|
|
this.presets = [] |
|
|
this.presets = [] |
|
|
@ -643,14 +791,51 @@ const PTZ_CODES = { |
|
|
changeLayout(layout) { |
|
|
changeLayout(layout) { |
|
|
const nextLayout = this.normalizeLayout(layout) |
|
|
const nextLayout = this.normalizeLayout(layout) |
|
|
if (nextLayout === this.layout) return |
|
|
if (nextLayout === this.layout) return |
|
|
|
|
|
const previousCapacity = this.capacity |
|
|
|
|
|
const highlighted = this.playbackSnapshot(this.slots[this.activeIndex]) |
|
|
|
|
|
const history = this.playbackHistory.map(item => this.playbackSnapshot(item)).filter(Boolean) |
|
|
|
|
|
const playing = this.slots.slice(0, previousCapacity).map(slot => this.playbackSnapshot(slot)) |
|
|
this.layout = nextLayout |
|
|
this.layout = nextLayout |
|
|
this.$root.$emit('video-layout-updated', layout) |
|
|
this.$root.$emit('video-layout-updated', layout) |
|
|
this.populateDefaultLayout() |
|
|
|
|
|
|
|
|
if (nextLayout < previousCapacity) { |
|
|
|
|
|
this.playReducedLayout(nextLayout, highlighted, history) |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.expandLayout(playing, nextLayout) |
|
|
|
|
|
}, |
|
|
|
|
|
playReducedLayout(capacity, highlighted, history) { |
|
|
|
|
|
let channels |
|
|
|
|
|
if (capacity === 1) { |
|
|
|
|
|
// 一宫格只保留当前高亮的那一路 |
|
|
|
|
|
channels = highlighted ? [highlighted] : history.slice(-1) |
|
|
|
|
|
} else { |
|
|
|
|
|
// 其余少宫格按开播记录从后往前留,中途换过的画面排在最后 |
|
|
|
|
|
channels = history.slice(-capacity) |
|
|
|
|
|
} |
|
|
|
|
|
if (!channels.length) { |
|
|
|
|
|
this.populateDefaultLayout() |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.playChannels(channels, highlighted && highlighted.cameraCode) |
|
|
|
|
|
}, |
|
|
|
|
|
async expandLayout(playing, capacity) { |
|
|
|
|
|
const kept = playing.filter(Boolean) |
|
|
|
|
|
const used = new Set(kept.map(item => item.cameraCode)) |
|
|
|
|
|
const extras = this.shuffleChannels(this.onlineChannels(this.channels).filter(item => item && !used.has(item.cameraCode))) |
|
|
|
|
|
const channels = kept.concat(extras).slice(0, capacity) |
|
|
|
|
|
await this.playChannels(channels, kept[0] && kept[0].cameraCode) |
|
|
}, |
|
|
}, |
|
|
changeControlPanel(expanded) { |
|
|
changeControlPanel(expanded) { |
|
|
this.controlPanelExpanded = Boolean(expanded) |
|
|
this.controlPanelExpanded = Boolean(expanded) |
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
}, |
|
|
}, |
|
|
|
|
|
toggleControlPanel(expanded) { |
|
|
|
|
|
if (this.embed) { |
|
|
|
|
|
this.embedControlVisible = expanded |
|
|
|
|
|
return |
|
|
|
|
|
} |
|
|
|
|
|
this.changeControlPanel(expanded) |
|
|
|
|
|
}, |
|
|
selectSlot(index) { |
|
|
selectSlot(index) { |
|
|
if (index === this.activeIndex) { |
|
|
if (index === this.activeIndex) { |
|
|
const current = this.slots[index] |
|
|
const current = this.slots[index] |
|
|
@ -963,9 +1148,9 @@ const PTZ_CODES = { |
|
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
<style lang="scss" scoped> |
|
|
.video-console { height: 100%; min-height: 0; display: flex; flex-direction: column; color: #dcecff; background: #0b1248; } |
|
|
.video-console { height: 100%; min-height: 0; display: flex; flex-direction: column; color: #dcecff; background: #0b1248; } |
|
|
.console-body { flex: 1; min-height: 0; display: flex; gap: 10px; } |
|
|
|
|
|
.tree-panel { position: relative; flex: 0 0 auto; min-width: 260px; max-width: 620px; height: 100%; } |
|
|
|
|
|
.tree-panel .nvr-tree { width: 100%; min-width: 0; } |
|
|
|
|
|
|
|
|
.console-body { position: relative; flex: 1; min-height: 0; display: flex; gap: 10px; } |
|
|
|
|
|
.tree-panel { position: relative; flex: 0 0 auto; width: 242px; min-width: 242px; max-width: 620px; height: 100%; } |
|
|
|
|
|
.tree-panel .nvr-tree { width: 100%; min-width: 0; max-width: 100%; } |
|
|
.tree-resize-handle { position: absolute; z-index: 3; top: 0; right: -6px; width: 12px; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; cursor: col-resize; } |
|
|
.tree-resize-handle { position: absolute; z-index: 3; top: 0; right: -6px; width: 12px; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; cursor: col-resize; } |
|
|
.tree-resize-handle span { display: block; width: 3px; height: 3px; border-radius: 50%; background: #4b9eff; box-shadow: 0 0 4px rgba(75,158,255,.8); } |
|
|
.tree-resize-handle span { display: block; width: 3px; height: 3px; border-radius: 50%; background: #4b9eff; box-shadow: 0 0 4px rgba(75,158,255,.8); } |
|
|
.tree-resize-handle:hover span, .tree-resize-handle.resizing span { background: #54ffff; } |
|
|
.tree-resize-handle:hover span, .tree-resize-handle.resizing span { background: #54ffff; } |
|
|
@ -973,10 +1158,11 @@ const PTZ_CODES = { |
|
|
.monitor-head { height: 32px; display: flex; align-items: center; justify-content: space-between; color: #cfe5ff; }.monitor-head small { color: #648fbc; } |
|
|
.monitor-head { height: 32px; display: flex; align-items: center; justify-content: space-between; color: #cfe5ff; }.monitor-head small { color: #648fbc; } |
|
|
.camera-grid { flex: 1; min-height: 0; display: grid; gap: 3px; background: rgba(14, 75, 146, .5); border: 1px solid #176bc2; } |
|
|
.camera-grid { flex: 1; min-height: 0; display: grid; gap: 3px; background: rgba(14, 75, 146, .5); border: 1px solid #176bc2; } |
|
|
.camera-card { position: relative; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid transparent; background: #050b24; cursor: pointer; }.camera-card.active { border-color: #54ffff; box-shadow: inset 0 0 0 1px rgba(84,255,255,.35); } |
|
|
.camera-card { position: relative; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid transparent; background: #050b24; cursor: pointer; }.camera-card.active { border-color: #54ffff; box-shadow: inset 0 0 0 1px rgba(84,255,255,.35); } |
|
|
.camera-label { position: absolute; left: 0; right: 0; top: 0; height: 28px; padding: 0 8px; display: flex; align-items: center; justify-content: space-between; color: #d8eaff; background: linear-gradient(180deg, rgba(0,10,36,.92), transparent); font-size: 11px; pointer-events: none; }.camera-label i { display: inline-block; width: 6px; height: 6px; margin-right: 5px; border-radius: 50%; background: #24e39f; box-shadow: 0 0 6px #24e39f; } |
|
|
|
|
|
|
|
|
.camera-label { position: absolute; left: 0; right: 0; top: 0; height: 28px; padding: 0 8px; display: flex; align-items: center; justify-content: space-between; color: #d8eaff; background: linear-gradient(180deg, rgba(0,10,36,.92), transparent); font-size: 11px; pointer-events: none; user-select: none; }.camera-label i { display: inline-block; width: 6px; height: 6px; margin-right: 5px; border-radius: 50%; background: #24e39f; box-shadow: 0 0 6px #24e39f; } |
|
|
.camera-actions { position: absolute; right: 5px; bottom: 5px; display: none; gap: 3px; }.camera-card:hover .camera-actions { display: flex; }.camera-actions button { width: 26px; height: 25px; border: 1px solid rgba(66, 167, 255, .55); color: #bfe0ff; background: rgba(4,28,78,.88); cursor: pointer; } |
|
|
.camera-actions { position: absolute; right: 5px; bottom: 5px; display: none; gap: 3px; }.camera-card:hover .camera-actions { display: flex; }.camera-actions button { width: 26px; height: 25px; border: 1px solid rgba(66, 167, 255, .55); color: #bfe0ff; background: rgba(4,28,78,.88); cursor: pointer; } |
|
|
.empty-slot { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #466f9c; background: radial-gradient(circle at center, #0b2a61, #050b24 70%); }.empty-slot i { font-size: 28px; }.empty-slot span { font-size: 11px; } |
|
|
|
|
|
.control-panel { width: 242px; min-width: 242px; height: 100%; align-self: stretch; margin: 0; padding: 10px; overflow-y: auto; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; background: rgba(7, 24, 78, .82); box-sizing: border-box; } |
|
|
|
|
|
|
|
|
.empty-slot { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; color: #466f9c; background: radial-gradient(circle at center, #0b2a61, #050b24 70%); user-select: none; }.empty-slot i { font-size: 28px; }.empty-slot span { font-size: 11px; } |
|
|
|
|
|
.control-panel { position: absolute; top: 0; right: 0; bottom: 0; z-index: 4; width: 242px; min-width: 242px; height: auto; margin: 0; overflow: visible; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; background: rgba(7, 24, 78, .82); box-sizing: border-box; cursor: move; user-select: none; } |
|
|
|
|
|
.control-panel-body { height: 100%; padding: 10px; overflow-x: hidden; overflow-y: auto; box-sizing: border-box; } |
|
|
.control-section { padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid rgba(39, 128, 220, .28); }.section-title { height: 30px; display: flex; align-items: center; justify-content: space-between; color: #54ffff; }.section-title small { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #719bc8; }.section-title button { border: 0; color: #75bfff; background: transparent; cursor: pointer; } |
|
|
.control-section { padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid rgba(39, 128, 220, .28); }.section-title { height: 30px; display: flex; align-items: center; justify-content: space-between; color: #54ffff; }.section-title small { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #719bc8; }.section-title button { border: 0; color: #75bfff; background: transparent; cursor: pointer; } |
|
|
.ptz-grid { width: 168px; margin: 6px auto 12px; display: grid; grid-template-columns: repeat(3, 50px); gap: 9px; }.ptz-grid button { width: 50px; height: 50px; border: 1px solid #3988ff; border-radius: 7px; color: #43c0ff; background: #01016b; box-shadow: none; cursor: pointer; transition: color .15s, background .15s, transform .15s; }.ptz-grid button i { font-size: 18px; font-weight: 700; }.ptz-grid button:hover { color: #43c0ff; background: #01016b; }.ptz-grid button:active { color: #ffffff; background: #01016b; transform: scale(.96); } |
|
|
.ptz-grid { width: 168px; margin: 6px auto 12px; display: grid; grid-template-columns: repeat(3, 50px); gap: 9px; }.ptz-grid button { width: 50px; height: 50px; border: 1px solid #3988ff; border-radius: 7px; color: #43c0ff; background: #01016b; box-shadow: none; cursor: pointer; transition: color .15s, background .15s, transform .15s; }.ptz-grid button i { font-size: 18px; font-weight: 700; }.ptz-grid button:hover { color: #43c0ff; background: #01016b; }.ptz-grid button:active { color: #ffffff; background: #01016b; transform: scale(.96); } |
|
|
.speed-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) 48px; align-items: center; gap: 7px; color: #8eb7df; font-size: 12px; }.speed-row > input[type="range"] { min-width: 0; height: 8px; box-sizing: border-box; margin: 0; appearance: none; border: 1px solid #2787ed; border-radius: 4px; outline: none; cursor: pointer; }.speed-row > input[type="range"]::-webkit-slider-runnable-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-webkit-slider-thumb { width: 14px; height: 14px; margin-top: -4px; appearance: none; border: 0; border-radius: 50%; background: #2787ed; }.speed-row > input[type="range"]::-moz-range-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-moz-range-progress { background: transparent; }.speed-row > input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #2787ed; }.speed-input { width: 48px; } |
|
|
.speed-row { display: grid; grid-template-columns: 34px minmax(0, 1fr) 48px; align-items: center; gap: 7px; color: #8eb7df; font-size: 12px; }.speed-row > input[type="range"] { min-width: 0; height: 8px; box-sizing: border-box; margin: 0; appearance: none; border: 1px solid #2787ed; border-radius: 4px; outline: none; cursor: pointer; }.speed-row > input[type="range"]::-webkit-slider-runnable-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-webkit-slider-thumb { width: 14px; height: 14px; margin-top: -4px; appearance: none; border: 0; border-radius: 50%; background: #2787ed; }.speed-row > input[type="range"]::-moz-range-track { height: 6px; border: 0; border-radius: 3px; background: transparent; }.speed-row > input[type="range"]::-moz-range-progress { background: transparent; }.speed-row > input[type="range"]::-moz-range-thumb { width: 14px; height: 14px; border: 0; border-radius: 50%; background: #2787ed; }.speed-input { width: 48px; } |
|
|
@ -992,21 +1178,35 @@ const PTZ_CODES = { |
|
|
::v-deep .preset-create .el-input-number .el-input__inner { text-align: center; } |
|
|
::v-deep .preset-create .el-input-number .el-input__inner { text-align: center; } |
|
|
::v-deep .preset-create .el-input-number--mini { line-height: 26px; } |
|
|
::v-deep .preset-create .el-input-number--mini { line-height: 26px; } |
|
|
.preset-create { display: grid; grid-template-columns: 68px minmax(0, 1fr); align-items: center; gap: 6px; margin-top: 8px; }.preset-create .el-input-number { width: 68px; }.preset-create .el-input { width: 100%; }.section-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; } |
|
|
.preset-create { display: grid; grid-template-columns: 68px minmax(0, 1fr); align-items: center; gap: 6px; margin-top: 8px; }.preset-create .el-input-number { width: 68px; }.preset-create .el-input { width: 100%; }.section-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; } |
|
|
@media (max-width: 1100px) { .control-panel { width: 218px; min-width: 218px; }.tree-panel { min-width: 230px; }.tree-panel .nvr-tree { width: 100%; } .embed-toggle-right.open { right: 218px; } } |
|
|
|
|
|
|
|
|
@media (max-width: 1100px) { .video-console:not(.is-embed) .control-panel { width: 218px; min-width: 218px; } .video-console:not(.is-embed) .tree-panel { width: 218px; min-width: 218px; } .tree-panel .nvr-tree { width: 100%; } } |
|
|
.is-embed .console-body { position: relative; } |
|
|
.is-embed .console-body { position: relative; } |
|
|
|
|
|
.is-embed .monitor-workspace { position: absolute; inset: 0; z-index: 1; } |
|
|
|
|
|
.is-embed .tree-panel, |
|
|
|
|
|
.is-embed .control-panel { position: absolute; top: 0; bottom: 0; z-index: 4; overflow: visible; background: rgba(7, 24, 78, .8); } |
|
|
|
|
|
.is-embed .tree-panel { left: 0; } |
|
|
|
|
|
.is-embed .tree-panel .nvr-tree { background: transparent; } |
|
|
|
|
|
.is-embed .control-panel { right: 0; } |
|
|
|
|
|
.is-embed .tree-panel button, |
|
|
|
|
|
.is-embed .tree-panel input, |
|
|
|
|
|
.is-embed .tree-panel .tree-content, |
|
|
|
|
|
.control-panel button, |
|
|
|
|
|
.control-panel input, |
|
|
|
|
|
.control-panel .el-input, |
|
|
|
|
|
.control-panel .el-select, |
|
|
|
|
|
.is-embed .tree-panel button, |
|
|
|
|
|
.is-embed .tree-panel input, |
|
|
|
|
|
.is-embed .tree-panel .tree-content { cursor: auto; } |
|
|
.embed-toggle { position: absolute; top: 50%; z-index: 6; width: 16px; height: 70px; padding: 0; border: 0; outline: 0; background: transparent; cursor: pointer; transform: translateY(-50%); } |
|
|
.embed-toggle { position: absolute; top: 50%; z-index: 6; width: 16px; height: 70px; padding: 0; border: 0; outline: 0; background: transparent; cursor: pointer; transform: translateY(-50%); } |
|
|
.embed-toggle img { display: block; width: 100%; height: 100%; object-fit: fill; } |
|
|
.embed-toggle img { display: block; width: 100%; height: 100%; object-fit: fill; } |
|
|
.embed-toggle:hover img, .embed-toggle:focus img { filter: brightness(1.2) drop-shadow(0 0 6px rgba(32, 218, 255, .65)); } |
|
|
.embed-toggle:hover img, .embed-toggle:focus img { filter: brightness(1.2) drop-shadow(0 0 6px rgba(32, 218, 255, .65)); } |
|
|
.embed-toggle-left { left: 0; } |
|
|
|
|
|
.embed-toggle-right { right: 0; } |
|
|
|
|
|
.embed-toggle-right.open { right: 242px; } |
|
|
|
|
|
|
|
|
.embed-toggle-left { right: -17px; left: auto; transform: translateY(-50%) rotate(180deg); } |
|
|
|
|
|
.embed-toggle-left.is-collapsed { left: 0; right: auto; transform: translateY(-50%) rotate(180deg); } |
|
|
|
|
|
.embed-toggle-right { left: -17px; right: auto; transform: translateY(-50%); } |
|
|
|
|
|
.embed-toggle-right.is-page { left: -16px; } |
|
|
|
|
|
.embed-toggle-right.is-collapsed { right: 0; left: auto; transform: translateY(-50%); } |
|
|
.embed-layout-switch { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; } |
|
|
.embed-layout-switch { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; } |
|
|
.embed-layout-switch button { height: 36px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(39,135,237,.55); color: #7eb6ef; background: rgba(10,43,103,.55); cursor: pointer; } |
|
|
.embed-layout-switch button { height: 36px; padding: 0; display: flex; align-items: center; justify-content: center; border: 1px solid rgba(39,135,237,.55); color: #7eb6ef; background: rgba(10,43,103,.55); cursor: pointer; } |
|
|
.embed-layout-switch button:hover, .embed-layout-switch button.active { color: #54ffff; border-color: #54ffff; background: rgba(23,107,194,.72); } |
|
|
.embed-layout-switch button:hover, .embed-layout-switch button.active { color: #54ffff; border-color: #54ffff; background: rgba(23,107,194,.72); } |
|
|
.embed-layout-icon { display: grid; gap: 1px; } |
|
|
|
|
|
.embed-layout-icon i { width: 4px; height: 4px; border: 1px solid currentColor; box-sizing: border-box; } |
|
|
|
|
|
.embed-layout-icon-1 i { width: 14px; height: 14px; } |
|
|
|
|
|
.embed-layout-icon-2 i { width: 6px; height: 6px; } |
|
|
|
|
|
.embed-layout-icon-4 { gap: 1px; } |
|
|
|
|
|
.embed-layout-icon-4 i { width: 3px; height: 3px; } |
|
|
|
|
|
|
|
|
.embed-layout-icon { display: grid; width: 16px; height: 16px; gap: 1px; box-sizing: border-box; } |
|
|
|
|
|
.embed-layout-icon i { width: auto; height: auto; border: 1px solid currentColor; box-sizing: border-box; } |
|
|
</style> |
|
|
</style> |