Browse Source

视频页面增加树形拖拽功能、对接雨刷的功能,调整任务和详情的样式

master
douyage 2 weeks ago
parent
commit
a614677a57
4 changed files with 247 additions and 30 deletions
  1. +11
    -0
      src/api/nvrVideo.js
  2. +13
    -1
      src/views/cards/taskSummary/inspectionTask.vue
  3. +34
    -11
      src/views/cards/taskSummary/newInspectionTaskDetail.vue
  4. +189
    -18
      src/views/video/LivePreview.vue

+ 11
- 0
src/api/nvrVideo.js View File

@ -1,4 +1,11 @@
import request from '@/utils/request' import request from '@/utils/request'
import axios from 'axios'
const ivsRequest = axios.create({
baseURL: process.env.VUE_APP_NVR_API || '/nvr-api',
timeout: 15000,
headers: { 'Content-Type': 'application/json;charset=utf-8' }
})
const unwrap = promise => promise.then(result => { const unwrap = promise => promise.then(result => {
if (result && Object.prototype.hasOwnProperty.call(result, 'data')) return result.data if (result && Object.prototype.hasOwnProperty.call(result, 'data')) return result.data
@ -56,6 +63,10 @@ export function controlPtz(payload) {
})) }))
} }
export function controlWiper(payload) {
return ivsRequest.post('/device/ptzcontrol', payload).then(response => response.data)
}
export function getPresetList(cameraCode) { export function getPresetList(cameraCode) {
const [device, domain] = encodeCameraCodeSegments(cameraCode) const [device, domain] = encodeCameraCodeSegments(cameraCode)
return unwrap(request({ return unwrap(request({


+ 13
- 1
src/views/cards/taskSummary/inspectionTask.vue View File

@ -204,6 +204,7 @@
<template slot-scope="scope"> <template slot-scope="scope">
<span class="table-action-link" @click="lookDetail(scope.row)"> <span class="table-action-link" @click="lookDetail(scope.row)">
<img <img
class="task-detail-icon"
src="../assets/taskDetail.png" src="../assets/taskDetail.png"
style="width: 22px; height: 22px" style="width: 22px; height: 22px"
/> />
@ -232,6 +233,7 @@
@click="lookBasicReport(scope.row)" @click="lookBasicReport(scope.row)"
> >
<img <img
class="task-detail-icon"
style="width: 22px; height: 22px" style="width: 22px; height: 22px"
src="../assets/icon-document.png" src="../assets/icon-document.png"
/> />
@ -722,7 +724,12 @@ export default {
return row.fileStatus == 0 && row.taskState == 1; return row.fileStatus == 0 && row.taskState == 1;
}, },
setTableHeight() { setTableHeight() {
this.tableHeight = this.$refs.tableCardRef.clientHeight - 40;
// Reserve enough space for the pagination bar so it is not clipped
// by the table card's overflow-hidden container.
this.tableHeight = Math.max(
0,
this.$refs.tableCardRef.clientHeight - 52
);
}, },
getList(showLoading = false) { getList(showLoading = false) {
// ID // ID
@ -1147,6 +1154,11 @@ export default {
cursor: pointer; cursor: pointer;
} }
.task-detail-icon {
vertical-align: middle;
transform: translateY(-1px);
}
.task-name-cell { .task-name-cell {
display: block; display: block;
overflow: hidden; overflow: hidden;


+ 34
- 11
src/views/cards/taskSummary/newInspectionTaskDetail.vue View File

@ -1050,12 +1050,12 @@ export default {
width: 189px; width: 189px;
.el-input__inner { .el-input__inner {
background-color: #1a2530 !important;
border-color: #34495e !important;
color: #c6d3ec !important;
background-color: rgba(27, 42, 94, 0.94) !important;
border-color: #6f95de !important;
color: #dcecff !important;
&::placeholder { &::placeholder {
color: #7b8ca8 !important;
color: rgba(220, 236, 255, 0.5) !important;
} }
} }
@ -1098,6 +1098,29 @@ export default {
white-space: nowrap !important; white-space: nowrap !important;
word-break: keep-all; word-break: keep-all;
} }
// Prevent the header's horizontal scrollbar from leaving an extra strip on
// compact laptop viewports while preserving horizontal scrolling for cells.
::v-deep .card-table .el-table__header-wrapper {
overflow-x: hidden !important;
overflow-y: hidden !important;
}
::v-deep .card-table .el-table__header-wrapper col[name="gutter"],
::v-deep .card-table .el-table__header-wrapper th.gutter {
display: none !important;
width: 0 !important;
}
::v-deep .card-table .el-table__header {
width: 100% !important;
table-layout: fixed;
}
::v-deep .card-table .el-table__body {
width: 100% !important;
table-layout: fixed;
}
</style> </style>
<!-- 全局样式用于 el-autocomplete 下拉菜单 --> <!-- 全局样式用于 el-autocomplete 下拉菜单 -->
@ -1128,22 +1151,22 @@ export default {
} }
.custom-autocomplete-popper { .custom-autocomplete-popper {
background-color: #222f3e !important;
border: 1px solid #34495e !important;
background-color: #0b2351 !important;
border: 1px solid #173f89 !important;
border-radius: 4px !important; border-radius: 4px !important;
.el-scrollbar { .el-scrollbar {
.el-scrollbar__view { .el-scrollbar__view {
li { li {
background-color: #222f3e !important;
color: #c6d3ec !important;
background-color: #0b2351 !important;
color: #dcecff !important;
font-size: 14px; font-size: 14px;
padding: 0 20px; padding: 0 20px;
height: 34px; height: 34px;
line-height: 34px; line-height: 34px;
&:hover { &:hover {
background-color: #2c3e50 !important;
background-color: #176bc2 !important;
} }
&.highlighted { &.highlighted {
@ -1155,10 +1178,10 @@ export default {
} }
.popper__arrow { .popper__arrow {
border-bottom-color: #34495e !important;
border-bottom-color: #173f89 !important;
&::after { &::after {
border-bottom-color: #222f3e !important;
border-bottom-color: #0b2351 !important;
} }
} }
} }


+ 189
- 18
src/views/video/LivePreview.vue View File

@ -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>

Loading…
Cancel
Save