|
|
@ -1,15 +1,27 @@ |
|
|
<template> |
|
|
<template> |
|
|
<div class="video-console live-preview"> |
|
|
<div class="video-console live-preview"> |
|
|
<div class="console-body"> |
|
|
<div class="console-body"> |
|
|
<nvr-device-tree |
|
|
|
|
|
:config="config" |
|
|
|
|
|
:channels="channels" |
|
|
|
|
|
:selected-code="selectedCode" |
|
|
|
|
|
:loading="loading" |
|
|
|
|
|
@channels-loaded="handleTreeChannels" |
|
|
|
|
|
@select="selectChannel" |
|
|
|
|
|
@open="openChannel" |
|
|
|
|
|
/> |
|
|
|
|
|
|
|
|
<div class="tree-panel" :style="{ width: treePanelWidth + 'px' }"> |
|
|
|
|
|
<nvr-device-tree |
|
|
|
|
|
:config="config" |
|
|
|
|
|
:channels="channels" |
|
|
|
|
|
:selected-code="selectedCode" |
|
|
|
|
|
:loading="loading" |
|
|
|
|
|
@channels-loaded="handleTreeChannels" |
|
|
|
|
|
@select="selectChannel" |
|
|
|
|
|
@open="openChannel" |
|
|
|
|
|
/> |
|
|
|
|
|
<div |
|
|
|
|
|
class="tree-resize-handle" |
|
|
|
|
|
:class="{ resizing: isTreeResizing }" |
|
|
|
|
|
title="拖拽调整设备树宽度" |
|
|
|
|
|
@mousedown="startTreeResize" |
|
|
|
|
|
> |
|
|
|
|
|
<span /> |
|
|
|
|
|
<span /> |
|
|
|
|
|
<span /> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
<main class="monitor-workspace"> |
|
|
<main class="monitor-workspace"> |
|
|
<div class="monitor-head"> |
|
|
<div class="monitor-head"> |
|
|
@ -141,7 +153,9 @@ |
|
|
<button type="button" :class="{ active: !activeMuted }" @click="toggleMuted"> |
|
|
<button type="button" :class="{ active: !activeMuted }" @click="toggleMuted"> |
|
|
<i :class="activeMuted ? 'el-icon-turn-off-microphone' : 'el-icon-microphone'" />静音 |
|
|
<i :class="activeMuted ? 'el-icon-turn-off-microphone' : 'el-icon-microphone'" />静音 |
|
|
</button> |
|
|
</button> |
|
|
<button type="button" @click="showPendingOperation('雨刷器')"><i class="el-icon-umbrella" />雨刷器</button> |
|
|
|
|
|
|
|
|
<button type="button" :disabled="wiperRunning" @click="wipeOnce"> |
|
|
|
|
|
<i :class="wiperRunning ? 'el-icon-loading' : 'el-icon-umbrella'" />{{ wiperRunning ? '擦拭中' : '雨刷器' }} |
|
|
|
|
|
</button> |
|
|
<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> |
|
|
@ -155,7 +169,7 @@ import NvrDeviceTree from './components/NvrDeviceTree.vue' |
|
|
import NvrStreamPlayer from './components/NvrStreamPlayer.vue' |
|
|
import NvrStreamPlayer from './components/NvrStreamPlayer.vue' |
|
|
import { |
|
|
import { |
|
|
getNvrConfig, startLive, getLiveStatus, stopLive, controlPtz, |
|
|
getNvrConfig, startLive, getLiveStatus, stopLive, controlPtz, |
|
|
getPresetList, createPreset, createSnapshot, downloadSnapshot |
|
|
|
|
|
|
|
|
getPresetList, createPreset, createSnapshot, downloadSnapshot, controlWiper |
|
|
} from '@/api/nvrVideo' |
|
|
} from '@/api/nvrVideo' |
|
|
|
|
|
|
|
|
const PTZ_CODES = { |
|
|
const PTZ_CODES = { |
|
|
@ -175,7 +189,14 @@ export default { |
|
|
treeChannelsLoaded: false, |
|
|
treeChannelsLoaded: false, |
|
|
treeChannelCodes: new Set(), |
|
|
treeChannelCodes: new Set(), |
|
|
controlPanelExpanded: false, |
|
|
controlPanelExpanded: false, |
|
|
|
|
|
treePanelWidth: 400, |
|
|
|
|
|
isTreeResizing: false, |
|
|
|
|
|
treeResizeStartX: 0, |
|
|
|
|
|
treeResizeStartWidth: 0, |
|
|
streamsInitialized: false, |
|
|
streamsInitialized: false, |
|
|
|
|
|
pageActive: true, |
|
|
|
|
|
lifecycleId: 0, |
|
|
|
|
|
streamCleanupPromise: null, |
|
|
selectedCode: '', |
|
|
selectedCode: '', |
|
|
layout: 9, |
|
|
layout: 9, |
|
|
layouts: [ |
|
|
layouts: [ |
|
|
@ -193,6 +214,7 @@ export default { |
|
|
ptzActive: false, |
|
|
ptzActive: false, |
|
|
ptzCameraCode: '', |
|
|
ptzCameraCode: '', |
|
|
ptzRepeatTimer: null, |
|
|
ptzRepeatTimer: null, |
|
|
|
|
|
wiperRunning: false, |
|
|
presets: [], |
|
|
presets: [], |
|
|
presetId: null, |
|
|
presetId: null, |
|
|
newPresetId: null, |
|
|
newPresetId: null, |
|
|
@ -251,12 +273,54 @@ export default { |
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
this.$root.$emit('video-control-panel-updated', this.controlPanelExpanded) |
|
|
this.loadConfig() |
|
|
this.loadConfig() |
|
|
}, |
|
|
}, |
|
|
|
|
|
async activated() { |
|
|
|
|
|
if (this.pageActive) return |
|
|
|
|
|
const lifecycleId = ++this.lifecycleId |
|
|
|
|
|
if (this.streamCleanupPromise) await this.streamCleanupPromise |
|
|
|
|
|
if (lifecycleId !== this.lifecycleId) return |
|
|
|
|
|
this.pageActive = true |
|
|
|
|
|
this.layout = 9 |
|
|
|
|
|
this.$root.$emit('video-layout-updated', this.layout) |
|
|
|
|
|
if (this.channels.length) { |
|
|
|
|
|
this.streamsInitialized = true |
|
|
|
|
|
this.populateRandomLayout() |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
deactivated() { |
|
|
|
|
|
this.pauseStreams() |
|
|
|
|
|
}, |
|
|
beforeDestroy() { |
|
|
beforeDestroy() { |
|
|
|
|
|
this.stopTreeResize() |
|
|
|
|
|
this.lifecycleId += 1 |
|
|
|
|
|
this.pageActive = false |
|
|
this.$root.$off('video-layout-change', this.changeLayout) |
|
|
this.$root.$off('video-layout-change', this.changeLayout) |
|
|
this.$root.$off('video-control-panel-change', this.changeControlPanel) |
|
|
this.$root.$off('video-control-panel-change', this.changeControlPanel) |
|
|
this.closeAll() |
|
|
this.closeAll() |
|
|
}, |
|
|
}, |
|
|
methods: { |
|
|
methods: { |
|
|
|
|
|
startTreeResize(event) { |
|
|
|
|
|
if (event.button !== 0) return |
|
|
|
|
|
this.isTreeResizing = true |
|
|
|
|
|
this.treeResizeStartX = event.clientX |
|
|
|
|
|
this.treeResizeStartWidth = this.treePanelWidth |
|
|
|
|
|
document.body.style.cursor = 'col-resize' |
|
|
|
|
|
document.body.style.userSelect = 'none' |
|
|
|
|
|
document.addEventListener('mousemove', this.onTreeResize) |
|
|
|
|
|
document.addEventListener('mouseup', this.stopTreeResize) |
|
|
|
|
|
}, |
|
|
|
|
|
onTreeResize(event) { |
|
|
|
|
|
if (!this.isTreeResizing) return |
|
|
|
|
|
const width = this.treeResizeStartWidth + event.clientX - this.treeResizeStartX |
|
|
|
|
|
this.treePanelWidth = Math.max(260, Math.min(620, width)) |
|
|
|
|
|
}, |
|
|
|
|
|
stopTreeResize() { |
|
|
|
|
|
if (!this.isTreeResizing) return |
|
|
|
|
|
this.isTreeResizing = false |
|
|
|
|
|
document.removeEventListener('mousemove', this.onTreeResize) |
|
|
|
|
|
document.removeEventListener('mouseup', this.stopTreeResize) |
|
|
|
|
|
document.body.style.cursor = '' |
|
|
|
|
|
document.body.style.userSelect = '' |
|
|
|
|
|
}, |
|
|
async loadConfig() { |
|
|
async loadConfig() { |
|
|
this.loading = true |
|
|
this.loading = true |
|
|
this.connectionText = '读取配置' |
|
|
this.connectionText = '读取配置' |
|
|
@ -311,7 +375,7 @@ export default { |
|
|
name: channelName || (channel.channelCode && !this.isCameraCodeName(channel.channelCode) ? channel.channelCode : `通道 ${channel.channel || ''}`) |
|
|
name: channelName || (channel.channelCode && !this.isCameraCodeName(channel.channelCode) ? channel.channelCode : `通道 ${channel.channel || ''}`) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
}) |
|
|
if (initializeStreams && !this.streamsInitialized && this.channels.length) { |
|
|
|
|
|
|
|
|
if (initializeStreams && this.pageActive && !this.streamsInitialized && this.channels.length) { |
|
|
this.streamsInitialized = true |
|
|
this.streamsInitialized = true |
|
|
this.populateRandomLayout() |
|
|
this.populateRandomLayout() |
|
|
} |
|
|
} |
|
|
@ -341,12 +405,15 @@ export default { |
|
|
return channels |
|
|
return channels |
|
|
}, |
|
|
}, |
|
|
async populateRandomLayout() { |
|
|
async populateRandomLayout() { |
|
|
|
|
|
if (!this.pageActive) return |
|
|
const channels = this.shuffleChannels().slice(0, this.capacity) |
|
|
const channels = this.shuffleChannels().slice(0, this.capacity) |
|
|
if (!channels.length) return |
|
|
if (!channels.length) return |
|
|
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 |
|
|
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 |
|
|
await this.startChannel(channels[index], index) |
|
|
await this.startChannel(channels[index], index) |
|
|
} |
|
|
} |
|
|
}, |
|
|
}, |
|
|
@ -355,7 +422,7 @@ export default { |
|
|
this.startChannel(channel) |
|
|
this.startChannel(channel) |
|
|
}, |
|
|
}, |
|
|
async startChannel(channel, targetIndex = this.activeIndex, attemptedCameraCodes = []) { |
|
|
async startChannel(channel, targetIndex = this.activeIndex, attemptedCameraCodes = []) { |
|
|
if (!channel || !channel.cameraCode) return |
|
|
|
|
|
|
|
|
if (!this.pageActive || !channel || !channel.cameraCode) return |
|
|
const index = targetIndex |
|
|
const index = targetIndex |
|
|
const current = this.slots[index] |
|
|
const current = this.slots[index] |
|
|
if (current && current.cameraCode === channel.cameraCode) { |
|
|
if (current && current.cameraCode === channel.cameraCode) { |
|
|
@ -363,10 +430,12 @@ export default { |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
if (this.slots[index]) await this.closeSlot(index, false) |
|
|
if (this.slots[index]) await this.closeSlot(index, false) |
|
|
|
|
|
if (!this.pageActive) return |
|
|
const slot = { |
|
|
const slot = { |
|
|
...channel, |
|
|
...channel, |
|
|
loading: true, |
|
|
loading: true, |
|
|
streamSource: { hlsUrl: '', flvUrl: '' }, |
|
|
streamSource: { hlsUrl: '', flvUrl: '' }, |
|
|
|
|
|
startPromise: null, |
|
|
muted: true, |
|
|
muted: true, |
|
|
errorNotified: false, |
|
|
errorNotified: false, |
|
|
attemptedCameraCodes: [...attemptedCameraCodes, channel.cameraCode], |
|
|
attemptedCameraCodes: [...attemptedCameraCodes, channel.cameraCode], |
|
|
@ -380,7 +449,8 @@ export default { |
|
|
this.presets = [] |
|
|
this.presets = [] |
|
|
this.presetId = null |
|
|
this.presetId = null |
|
|
try { |
|
|
try { |
|
|
const result = await startLive(channel) |
|
|
|
|
|
|
|
|
slot.startPromise = startLive(channel) |
|
|
|
|
|
const result = await slot.startPromise |
|
|
if (this.slots[index] !== slot) return |
|
|
if (this.slots[index] !== slot) return |
|
|
this.applyLiveResult(index, slot, result) |
|
|
this.applyLiveResult(index, slot, result) |
|
|
if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) { |
|
|
if (!slot.streamSource.hlsUrl && !slot.streamSource.flvUrl) { |
|
|
@ -409,13 +479,14 @@ export default { |
|
|
return url |
|
|
return url |
|
|
}, |
|
|
}, |
|
|
pollLive(index, slot, attempt) { |
|
|
pollLive(index, slot, attempt) { |
|
|
if (this.slots[index] !== slot) return |
|
|
|
|
|
|
|
|
if (!this.pageActive || this.slots[index] !== slot) return |
|
|
if (attempt >= 20) { |
|
|
if (attempt >= 20) { |
|
|
slot.loading = false |
|
|
slot.loading = false |
|
|
this.$message.error('视频流启动超时,请检查摄像机或流媒体服务') |
|
|
this.$message.error('视频流启动超时,请检查摄像机或流媒体服务') |
|
|
return |
|
|
return |
|
|
} |
|
|
} |
|
|
slot.timer = setTimeout(async() => { |
|
|
slot.timer = setTimeout(async() => { |
|
|
|
|
|
if (!this.pageActive || this.slots[index] !== slot) return |
|
|
try { |
|
|
try { |
|
|
const result = await getLiveStatus(slot.cameraCode) |
|
|
const result = await getLiveStatus(slot.cameraCode) |
|
|
this.applyLiveResult(index, slot, result) |
|
|
this.applyLiveResult(index, slot, result) |
|
|
@ -436,11 +507,23 @@ export default { |
|
|
this.presets = [] |
|
|
this.presets = [] |
|
|
this.presetId = null |
|
|
this.presetId = null |
|
|
} |
|
|
} |
|
|
|
|
|
if (slot.startPromise) { |
|
|
|
|
|
try { await slot.startPromise } catch (_) { /* no stream session was created */ } |
|
|
|
|
|
} |
|
|
try { await stopLive(slot.cameraCode) } catch (_) { /* release the UI slot first */ } |
|
|
try { await stopLive(slot.cameraCode) } catch (_) { /* release the UI slot first */ } |
|
|
if (notify) this.$message.success('视频通道已关闭') |
|
|
if (notify) this.$message.success('视频通道已关闭') |
|
|
}, |
|
|
}, |
|
|
closeAll() { |
|
|
closeAll() { |
|
|
this.slots.forEach((slot, index) => { if (slot) this.closeSlot(index, false) }) |
|
|
|
|
|
|
|
|
return Promise.all(this.slots.map((slot, index) => slot ? this.closeSlot(index, false) : null)) |
|
|
|
|
|
}, |
|
|
|
|
|
pauseStreams() { |
|
|
|
|
|
this.lifecycleId += 1 |
|
|
|
|
|
this.pageActive = false |
|
|
|
|
|
this.streamsInitialized = false |
|
|
|
|
|
if (this.ptzActive) this.stopPtz() |
|
|
|
|
|
clearInterval(this.ptzRepeatTimer) |
|
|
|
|
|
this.ptzRepeatTimer = null |
|
|
|
|
|
this.streamCleanupPromise = this.closeAll() |
|
|
}, |
|
|
}, |
|
|
changeLayout(layout) { |
|
|
changeLayout(layout) { |
|
|
if (layout === this.layout) return |
|
|
if (layout === this.layout) return |
|
|
@ -477,7 +560,7 @@ export default { |
|
|
this.switchToNextChannel(index, slot, error) |
|
|
this.switchToNextChannel(index, slot, error) |
|
|
}, |
|
|
}, |
|
|
async switchToNextChannel(index, failedSlot, error) { |
|
|
async switchToNextChannel(index, failedSlot, error) { |
|
|
if (this.slots[index] !== failedSlot) return |
|
|
|
|
|
|
|
|
if (!this.pageActive || this.slots[index] !== failedSlot) return |
|
|
const attemptedCodes = new Set(failedSlot.attemptedCameraCodes || [failedSlot.cameraCode]) |
|
|
const attemptedCodes = new Set(failedSlot.attemptedCameraCodes || [failedSlot.cameraCode]) |
|
|
const occupiedCodes = new Set(this.slots |
|
|
const occupiedCodes = new Set(this.slots |
|
|
.filter((slot, slotIndex) => slot && slotIndex !== index) |
|
|
.filter((slot, slotIndex) => slot && slotIndex !== index) |
|
|
@ -493,6 +576,7 @@ export default { |
|
|
} |
|
|
} |
|
|
this.$set(this.slots, index, null) |
|
|
this.$set(this.slots, index, null) |
|
|
try { await stopLive(failedSlot.cameraCode) } catch (_) { /* continue switching cameras */ } |
|
|
try { await stopLive(failedSlot.cameraCode) } catch (_) { /* continue switching cameras */ } |
|
|
|
|
|
if (!this.pageActive) return |
|
|
if (nextChannel) { |
|
|
if (nextChannel) { |
|
|
await this.startChannel(nextChannel, index, Array.from(attemptedCodes)) |
|
|
await this.startChannel(nextChannel, index, Array.from(attemptedCodes)) |
|
|
} else { |
|
|
} else { |
|
|
@ -608,6 +692,87 @@ export default { |
|
|
if (!this.controlTarget) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
if (!this.controlTarget) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
this.$set(this.controlTarget, 'muted', !(this.controlTarget.muted !== false)) |
|
|
this.$set(this.controlTarget, 'muted', !(this.controlTarget.muted !== false)) |
|
|
}, |
|
|
}, |
|
|
|
|
|
getIvsIdentity() { |
|
|
|
|
|
const firstValue = value => Array.isArray(value) ? value[0] : value |
|
|
|
|
|
const query = this.$route.query || {} |
|
|
|
|
|
const username = firstValue(query.username) || sessionStorage.getItem('username') || this.$store.getters.name |
|
|
|
|
|
const joinbrightToken = firstValue(query['joinbright-token']) || firstValue(query.token) || sessionStorage.getItem('joinbright-token') |
|
|
|
|
|
return { username: username || undefined, joinbrightToken: joinbrightToken || undefined } |
|
|
|
|
|
}, |
|
|
|
|
|
async sendWiperCommand(cameraCode, controlCode) { |
|
|
|
|
|
try { |
|
|
|
|
|
const result = await controlWiper({ |
|
|
|
|
|
cameraCode, |
|
|
|
|
|
controlCode, |
|
|
|
|
|
controlPara1: '1', |
|
|
|
|
|
controlPara2: '', |
|
|
|
|
|
...this.getIvsIdentity() |
|
|
|
|
|
}) |
|
|
|
|
|
if (!result || typeof result !== 'object') { |
|
|
|
|
|
const error = new Error('雨刷器接口响应无效') |
|
|
|
|
|
error.resultUncertain = true |
|
|
|
|
|
throw error |
|
|
|
|
|
} |
|
|
|
|
|
if (Number(result.resultCode) !== 0) { |
|
|
|
|
|
const error = new Error(result.message || '雨刷器控制失败') |
|
|
|
|
|
error.resultUncertain = false |
|
|
|
|
|
throw error |
|
|
|
|
|
} |
|
|
|
|
|
return result |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
if (typeof error.resultUncertain !== 'boolean') { |
|
|
|
|
|
const status = error.response && error.response.status |
|
|
|
|
|
error.resultUncertain = !status || status >= 500 |
|
|
|
|
|
} |
|
|
|
|
|
throw error |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
wiperErrorMessage(error) { |
|
|
|
|
|
return (error.response && error.response.data && error.response.data.message) || error.message || '未知错误' |
|
|
|
|
|
}, |
|
|
|
|
|
async wipeOnce() { |
|
|
|
|
|
const cameraCode = this.controlTarget && this.controlTarget.cameraCode |
|
|
|
|
|
if (!cameraCode) { this.$message.warning('请先选择有视频的画面'); return } |
|
|
|
|
|
if (this.wiperRunning) return |
|
|
|
|
|
|
|
|
|
|
|
this.wiperRunning = true |
|
|
|
|
|
let opened = false |
|
|
|
|
|
let openError = null |
|
|
|
|
|
let closeError = null |
|
|
|
|
|
try { |
|
|
|
|
|
try { |
|
|
|
|
|
await this.sendWiperCommand(cameraCode, 27) |
|
|
|
|
|
opened = true |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 5000)) |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
openError = error |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (opened || (openError && openError.resultUncertain !== false)) { |
|
|
|
|
|
try { |
|
|
|
|
|
await this.sendWiperCommand(cameraCode, 28) |
|
|
|
|
|
} catch (error) { |
|
|
|
|
|
closeError = error |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (closeError) { |
|
|
|
|
|
this.$message({ |
|
|
|
|
|
type: 'error', |
|
|
|
|
|
message: `雨刷器关闭失败,请立即人工停止:${this.wiperErrorMessage(closeError)}`, |
|
|
|
|
|
duration: 0, |
|
|
|
|
|
showClose: true |
|
|
|
|
|
}) |
|
|
|
|
|
} else if (openError) { |
|
|
|
|
|
const prefix = openError.resultUncertain === false ? '雨刷器启动失败' : '雨刷器启动结果不确定,已尝试发送关闭命令' |
|
|
|
|
|
this.$message.error(`${prefix}:${this.wiperErrorMessage(openError)}`) |
|
|
|
|
|
} else { |
|
|
|
|
|
this.$message.success('雨刷器擦拭完成') |
|
|
|
|
|
} |
|
|
|
|
|
} finally { |
|
|
|
|
|
this.wiperRunning = false |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
showPendingOperation(name) { |
|
|
showPendingOperation(name) { |
|
|
this.$message.info(`${name}控制接口暂未接入`) |
|
|
this.$message.info(`${name}控制接口暂未接入`) |
|
|
} |
|
|
} |
|
|
@ -618,6 +783,11 @@ export default { |
|
|
<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; } |
|
|
.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; } |
|
|
|
|
|
.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:hover span, .tree-resize-handle.resizing span { background: #54ffff; } |
|
|
.monitor-workspace { flex: 1; min-width: 0; height: 100%; display: flex; flex-direction: column; padding: 10px; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; box-sizing: border-box; } |
|
|
.monitor-workspace { flex: 1; min-width: 0; height: 100%; display: flex; flex-direction: column; padding: 10px; border: 1px solid rgba(63,127,216,.72); border-radius: 2px; box-sizing: border-box; } |
|
|
.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; } |
|
|
@ -632,6 +802,7 @@ export default { |
|
|
.preset-quick-actions { display: flex; align-items: center; gap: 7px; margin: 9px 0 0 1px; }.preset-quick-actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid rgba(48, 151, 248, .5); color: #58bfff; background: transparent; cursor: pointer; }.preset-quick-actions button:hover { background: rgba(31, 126, 222, .25); }.preset-quick-actions img { width: 18px; height: 18px; object-fit: contain; } |
|
|
.preset-quick-actions { display: flex; align-items: center; gap: 7px; margin: 9px 0 0 1px; }.preset-quick-actions button { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 1px solid rgba(48, 151, 248, .5); color: #58bfff; background: transparent; cursor: pointer; }.preset-quick-actions button:hover { background: rgba(31, 126, 222, .25); }.preset-quick-actions img { width: 18px; height: 18px; object-fit: contain; } |
|
|
.lens-actions { display: flex; align-items: center; justify-content: center; gap: 11px; min-height: 28px; margin: 0 0 10px; }.lens-tool { position: relative; width: 48px; height: 22px; overflow: hidden; }.lens-tool img { display: block; width: 100%; height: 100%; object-fit: fill; }.lens-tool button { position: absolute; top: 0; bottom: 0; width: 50%; padding: 0; border: 0; background: transparent; cursor: pointer; }.lens-tool button:first-of-type { left: 0; }.lens-tool button:last-of-type { right: 0; }.lens-tool button:active { background: rgba(73, 184, 255, .28); } |
|
|
.lens-actions { display: flex; align-items: center; justify-content: center; gap: 11px; min-height: 28px; margin: 0 0 10px; }.lens-tool { position: relative; width: 48px; height: 22px; overflow: hidden; }.lens-tool img { display: block; width: 100%; height: 100%; object-fit: fill; }.lens-tool button { position: absolute; top: 0; bottom: 0; width: 50%; padding: 0; border: 0; background: transparent; cursor: pointer; }.lens-tool button:first-of-type { left: 0; }.lens-tool button:last-of-type { right: 0; }.lens-tool button:active { background: rgba(73, 184, 255, .28); } |
|
|
.operation-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 9px; }.operation-grid button, .section-actions button { min-height: 29px; border: 1px solid rgba(47, 145, 241, .5); color: #abd4ff; background: #0b2b67; cursor: pointer; }.operation-grid button i { margin-right: 5px; }.operation-grid button.active, .section-actions button.primary { color: #fff; background: #176bc2; } |
|
|
.operation-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 9px; }.operation-grid button, .section-actions button { min-height: 29px; border: 1px solid rgba(47, 145, 241, .5); color: #abd4ff; background: #0b2b67; cursor: pointer; }.operation-grid button i { margin-right: 5px; }.operation-grid button.active, .section-actions button.primary { color: #fff; background: #176bc2; } |
|
|
|
|
|
.operation-grid button:disabled { color: #6385aa; background: #081f4d; cursor: not-allowed; } |
|
|
.operation-grid .wide { grid-column: 1 / -1; } |
|
|
.operation-grid .wide { grid-column: 1 / -1; } |
|
|
::v-deep .el-select .el-input__inner, ::v-deep .preset-create .el-input__inner, ::v-deep .speed-input .el-input__inner { color: #cce6ff; border-color: rgba(52, 147, 241, .5); background: #09265e; } |
|
|
::v-deep .el-select .el-input__inner, ::v-deep .preset-create .el-input__inner, ::v-deep .speed-input .el-input__inner { color: #cce6ff; border-color: rgba(52, 147, 241, .5); background: #09265e; } |
|
|
::v-deep .speed-input .el-input__inner { height: 26px; padding: 0 7px; line-height: 26px; text-align: center; } |
|
|
::v-deep .speed-input .el-input__inner { height: 26px; padding: 0 7px; line-height: 26px; text-align: center; } |
|
|
@ -640,5 +811,5 @@ 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; }.nvr-tree { width: 230px; min-width: 230px; } } |
|
|
|
|
|
|
|
|
@media (max-width: 1100px) { .control-panel { width: 218px; min-width: 218px; }.tree-panel { min-width: 230px; }.tree-panel .nvr-tree { width: 100%; } } |
|
|
</style> |
|
|
</style> |