|
|
@ -1,8 +1,9 @@ |
|
|
<template> |
|
|
<template> |
|
|
<div class="video-console live-preview"> |
|
|
|
|
|
|
|
|
<div class="video-console live-preview" :class="{ 'is-embed': embed }"> |
|
|
<div class="console-body"> |
|
|
<div class="console-body"> |
|
|
<div class="tree-panel" :style="{ width: treePanelWidth + 'px' }"> |
|
|
|
|
|
|
|
|
<div v-show="!embed || embedTreeVisible" class="tree-panel" :style="{ width: treePanelWidth + 'px' }"> |
|
|
<nvr-device-tree |
|
|
<nvr-device-tree |
|
|
|
|
|
v-if="!embed || sessionReady" |
|
|
:config="config" |
|
|
:config="config" |
|
|
:channels="channels" |
|
|
:channels="channels" |
|
|
:selected-code="selectedCode" |
|
|
:selected-code="selectedCode" |
|
|
@ -13,6 +14,7 @@ |
|
|
@open="openChannel" |
|
|
@open="openChannel" |
|
|
/> |
|
|
/> |
|
|
<div |
|
|
<div |
|
|
|
|
|
v-if="!embed" |
|
|
class="tree-resize-handle" |
|
|
class="tree-resize-handle" |
|
|
:class="{ resizing: isTreeResizing }" |
|
|
:class="{ resizing: isTreeResizing }" |
|
|
title="拖拽调整设备树宽度" |
|
|
title="拖拽调整设备树宽度" |
|
|
@ -23,9 +25,22 @@ |
|
|
<span /> |
|
|
<span /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<button |
|
|
|
|
|
v-if="embed" |
|
|
|
|
|
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%)" |
|
|
|
|
|
> |
|
|
|
|
|
<!-- <img :src="embedTreeVisible ? treeCollapseIcon : treeToggleIcon" alt=""> --> |
|
|
|
|
|
<img :src="embedTreeVisible ? treeToggleIcon : treeCollapseIcon" alt=""> |
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
|
<main class="monitor-workspace"> |
|
|
<main class="monitor-workspace"> |
|
|
<div class="monitor-head"> |
|
|
|
|
|
|
|
|
<div v-if="!embed" class="monitor-head"> |
|
|
<span>监控画面</span> |
|
|
<span>监控画面</span> |
|
|
<small>双击设备树通道打开,单击画面切换控制对象</small> |
|
|
<small>双击设备树通道打开,单击画面切换控制对象</small> |
|
|
</div> |
|
|
</div> |
|
|
@ -50,7 +65,7 @@ |
|
|
<span><i />{{ slot.name }}</span> |
|
|
<span><i />{{ slot.name }}</span> |
|
|
</div> |
|
|
</div> |
|
|
<div class="camera-actions"> |
|
|
<div class="camera-actions"> |
|
|
<button type="button" title="历史回放" @click.stop="openPlayback(slot)"><i class="el-icon-time" /></button> |
|
|
|
|
|
|
|
|
<button v-if="!embed" type="button" title="历史回放" @click.stop="openPlayback(slot)"><i class="el-icon-time" /></button> |
|
|
<button type="button" title="全屏" @click.stop="fullscreen(index)"><i class="el-icon-full-screen" /></button> |
|
|
<button type="button" title="全屏" @click.stop="fullscreen(index)"><i class="el-icon-full-screen" /></button> |
|
|
<button type="button" title="关闭" @click.stop="closeSlot(index)"><i class="el-icon-close" /></button> |
|
|
<button type="button" title="关闭" @click.stop="closeSlot(index)"><i class="el-icon-close" /></button> |
|
|
</div> |
|
|
</div> |
|
|
@ -63,7 +78,7 @@ |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
</main> |
|
|
|
|
|
|
|
|
<aside v-show="controlPanelExpanded" class="control-panel"> |
|
|
|
|
|
|
|
|
<aside v-show="embed ? embedControlVisible : controlPanelExpanded" class="control-panel"> |
|
|
<section class="control-section"> |
|
|
<section class="control-section"> |
|
|
<div class="section-title"><span>云台控制</span><small>{{ selectedChannelName }}</small></div> |
|
|
<div class="section-title"><span>云台控制</span><small>{{ selectedChannelName }}</small></div> |
|
|
<div class="ptz-grid"> |
|
|
<div class="ptz-grid"> |
|
|
@ -146,7 +161,7 @@ |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
|
|
|
|
|
|
<section class="control-section"> |
|
|
|
|
|
|
|
|
<section v-if="!embed" class="control-section"> |
|
|
<div class="section-title"><span>操作</span></div> |
|
|
<div class="section-title"><span>操作</span></div> |
|
|
<div class="operation-grid"> |
|
|
<div class="operation-grid"> |
|
|
<button type="button" class="wide" @click="capture"><i class="el-icon-camera" />抓图</button> |
|
|
<button type="button" class="wide" @click="capture"><i class="el-icon-camera" />抓图</button> |
|
|
@ -161,6 +176,16 @@ |
|
|
</div> |
|
|
</div> |
|
|
</section> |
|
|
</section> |
|
|
</aside> |
|
|
</aside> |
|
|
|
|
|
<button |
|
|
|
|
|
v-if="embed" |
|
|
|
|
|
type="button" |
|
|
|
|
|
class="embed-toggle embed-toggle-right" |
|
|
|
|
|
:class="{ open: embedControlVisible }" |
|
|
|
|
|
:title="embedControlVisible ? '隐藏云台控制' : '显示云台控制'" |
|
|
|
|
|
@click="embedControlVisible = !embedControlVisible" |
|
|
|
|
|
> |
|
|
|
|
|
<img :src="embedControlVisible ? treeToggleIcon : treeCollapseIcon" alt=""> |
|
|
|
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
</template> |
|
|
@ -168,6 +193,8 @@ |
|
|
<script> |
|
|
<script> |
|
|
import NvrDeviceTree from './components/NvrDeviceTree.vue' |
|
|
import NvrDeviceTree from './components/NvrDeviceTree.vue' |
|
|
import NvrStreamPlayer from './components/NvrStreamPlayer.vue' |
|
|
import NvrStreamPlayer from './components/NvrStreamPlayer.vue' |
|
|
|
|
|
import treeToggleIcon from '@/assets/portal/tree-toggle.png' |
|
|
|
|
|
import treeCollapseIcon from '@/assets/portal/tree-collapse.png' |
|
|
import { |
|
|
import { |
|
|
getNvrConfig, startLive, getLiveStatus, stopLive, controlPtz, |
|
|
getNvrConfig, startLive, getLiveStatus, stopLive, controlPtz, |
|
|
getPresetList, createPreset, createSnapshot, downloadSnapshot, controlWiper |
|
|
getPresetList, createPreset, createSnapshot, downloadSnapshot, controlWiper |
|
|
@ -181,9 +208,14 @@ const PTZ_CODES = { |
|
|
ZOOM_IN: 23, ZOOM_OUT: 24, FOCUS_NEAR: 25, FOCUS_FAR: 26 |
|
|
ZOOM_IN: 23, ZOOM_OUT: 24, FOCUS_NEAR: 25, FOCUS_FAR: 26 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export default { |
|
|
|
|
|
|
|
|
export default { |
|
|
name: 'VideoLivePreview', |
|
|
name: 'VideoLivePreview', |
|
|
components: { NvrDeviceTree, NvrStreamPlayer }, |
|
|
components: { NvrDeviceTree, NvrStreamPlayer }, |
|
|
|
|
|
props: { |
|
|
|
|
|
embed: { type: Boolean, default: false }, |
|
|
|
|
|
// 嵌入页先出画面,token 就绪后再拉设备和视频 |
|
|
|
|
|
sessionReady: { type: Boolean, default: true } |
|
|
|
|
|
}, |
|
|
data() { |
|
|
data() { |
|
|
return { |
|
|
return { |
|
|
config: {}, |
|
|
config: {}, |
|
|
@ -191,6 +223,10 @@ export default { |
|
|
treeChannelsLoaded: false, |
|
|
treeChannelsLoaded: false, |
|
|
treeChannelCodes: new Set(), |
|
|
treeChannelCodes: new Set(), |
|
|
controlPanelExpanded: false, |
|
|
controlPanelExpanded: false, |
|
|
|
|
|
embedTreeVisible: false, |
|
|
|
|
|
embedControlVisible: false, |
|
|
|
|
|
treeToggleIcon, |
|
|
|
|
|
treeCollapseIcon, |
|
|
treePanelWidth: 400, |
|
|
treePanelWidth: 400, |
|
|
isTreeResizing: false, |
|
|
isTreeResizing: false, |
|
|
treeResizeStartX: 0, |
|
|
treeResizeStartX: 0, |
|
|
@ -258,8 +294,13 @@ export default { |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
computed: { |
|
|
computed: { |
|
|
// 汇聚门户进入的会话:设备树与拉流走首页同款聚合接口 |
|
|
|
|
|
isAggMode() { return this.$store.getters.fromPortal }, |
|
|
|
|
|
|
|
|
// 打包模式决定视频接口:build:agg 走汇聚接口,build:htjc6 走单站接口。 |
|
|
|
|
|
// 其它未配置的模式仍按是否从门户进入判断。 |
|
|
|
|
|
isAggMode() { |
|
|
|
|
|
if (process.env.VUE_APP_AGG_VIDEO === 'true') return true |
|
|
|
|
|
if (process.env.VUE_APP_AGG_VIDEO === 'false') return false |
|
|
|
|
|
return this.$store.getters.fromPortal |
|
|
|
|
|
}, |
|
|
capacity() { return this.layout }, |
|
|
capacity() { return this.layout }, |
|
|
visibleSlots() { return this.slots.slice(0, this.capacity) }, |
|
|
visibleSlots() { return this.slots.slice(0, this.capacity) }, |
|
|
activeSlot() { return this.slots[this.activeIndex] || null }, |
|
|
activeSlot() { return this.slots[this.activeIndex] || null }, |
|
|
@ -273,13 +314,20 @@ export default { |
|
|
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}`) : '未选择画面' } |
|
|
}, |
|
|
}, |
|
|
|
|
|
watch: { |
|
|
|
|
|
sessionReady(ready) { |
|
|
|
|
|
if (ready && this.embed) this.loadConfig() |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
created() { |
|
|
created() { |
|
|
this.$root.$on('video-layout-change', this.changeLayout) |
|
|
|
|
|
this.$root.$on('video-control-panel-change', this.changeControlPanel) |
|
|
|
|
|
|
|
|
if (!this.embed) { |
|
|
|
|
|
this.$root.$on('video-layout-change', this.changeLayout) |
|
|
|
|
|
this.$root.$on('video-control-panel-change', this.changeControlPanel) |
|
|
|
|
|
this.$root.$emit('video-layout-updated', this.layout) |
|
|
|
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
|
|
|
} |
|
|
document.addEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
document.addEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
this.$root.$emit('video-layout-updated', this.layout) |
|
|
|
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
|
|
|
this.loadConfig() |
|
|
|
|
|
|
|
|
if (!this.embed || this.sessionReady) this.loadConfig() |
|
|
}, |
|
|
}, |
|
|
async activated() { |
|
|
async activated() { |
|
|
if (this.pageActive) return |
|
|
if (this.pageActive) return |
|
|
@ -302,8 +350,10 @@ export default { |
|
|
this.lifecycleId += 1 |
|
|
this.lifecycleId += 1 |
|
|
this.pageActive = false |
|
|
this.pageActive = false |
|
|
document.removeEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
document.removeEventListener('fullscreenchange', this.handleFullscreenChange) |
|
|
this.$root.$off('video-layout-change', this.changeLayout) |
|
|
|
|
|
this.$root.$off('video-control-panel-change', this.changeControlPanel) |
|
|
|
|
|
|
|
|
if (!this.embed) { |
|
|
|
|
|
this.$root.$off('video-layout-change', this.changeLayout) |
|
|
|
|
|
this.$root.$off('video-control-panel-change', this.changeControlPanel) |
|
|
|
|
|
} |
|
|
this.closeAll() |
|
|
this.closeAll() |
|
|
}, |
|
|
}, |
|
|
methods: { |
|
|
methods: { |
|
|
@ -485,15 +535,10 @@ export default { |
|
|
if (this.slots[index] !== slot || !result) return |
|
|
if (this.slots[index] !== slot || !result) return |
|
|
const payload = result.data || result |
|
|
const payload = result.data || result |
|
|
const mediaUrl = payload.url || payload.playUrl || payload.mediaUrl || payload.address || '' |
|
|
const mediaUrl = payload.url || payload.playUrl || payload.mediaUrl || payload.address || '' |
|
|
const hlsUrl = payload.hlsUrl || payload.hls || (/\.m3u8(?:\?|$)/i.test(mediaUrl) ? mediaUrl : '') |
|
|
|
|
|
const flvUrl = payload.flvUrl || payload.flv || (/\.flv(?:\?|$)/i.test(mediaUrl) ? mediaUrl : '') |
|
|
|
|
|
slot.streamSource.hlsUrl = this.replaceStreamHost(hlsUrl) |
|
|
|
|
|
slot.streamSource.flvUrl = this.replaceStreamHost(flvUrl) |
|
|
|
|
|
|
|
|
slot.streamSource.hlsUrl = payload.hlsUrl || payload.hls || (/\.m3u8(?:\?|$)/i.test(mediaUrl) ? mediaUrl : '') |
|
|
|
|
|
slot.streamSource.flvUrl = payload.flvUrl || payload.flv || (/\.flv(?:\?|$)/i.test(mediaUrl) ? mediaUrl : '') |
|
|
slot.loading = !(slot.streamSource.hlsUrl || slot.streamSource.flvUrl) |
|
|
slot.loading = !(slot.streamSource.hlsUrl || slot.streamSource.flvUrl) |
|
|
}, |
|
|
}, |
|
|
replaceStreamHost(url) { |
|
|
|
|
|
return url |
|
|
|
|
|
}, |
|
|
|
|
|
pollLive(index, slot, attempt) { |
|
|
pollLive(index, slot, attempt) { |
|
|
if (!this.pageActive || this.slots[index] !== slot) return |
|
|
if (!this.pageActive || this.slots[index] !== slot) return |
|
|
if (attempt >= 20) { |
|
|
if (attempt >= 20) { |
|
|
@ -898,5 +943,12 @@ export default { |
|
|
::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%; } } |
|
|
|
|
|
|
|
|
@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; } } |
|
|
|
|
|
.is-embed .console-body { position: relative; } |
|
|
|
|
|
.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: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; } |
|
|
</style> |
|
|
</style> |