|
|
@@ -4,16 +4,117 @@
|
|
|
class="video-container"
|
|
|
:class="{ 'is-fullscreen': isFullscreen }"
|
|
|
>
|
|
|
+ <!-- PTZ 球机控制面板(左侧) -->
|
|
|
+ <div class="ptz-wrapper">
|
|
|
+ <div class="ptz-panel" :class="{ 'is-collapsed': ptzCollapsed }">
|
|
|
+ <div class="ptz-title">PTZ Control</div>
|
|
|
+ <div class="ptz-grid">
|
|
|
+ <div class="ptz-placeholder" />
|
|
|
+ <button
|
|
|
+ class="ptz-btn"
|
|
|
+ title="Up"
|
|
|
+ @mousedown="handlePtzStart(PTZDirection.Up)"
|
|
|
+ @mouseup="handlePtzStop"
|
|
|
+ >
|
|
|
+ <svg viewBox="0 0 24 24" width="22" height="22">
|
|
|
+ <path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="ptz-placeholder" />
|
|
|
+
|
|
|
+ <button
|
|
|
+ class="ptz-btn"
|
|
|
+ title="Left"
|
|
|
+ @mousedown="handlePtzStart(PTZDirection.Left)"
|
|
|
+ @mouseup="handlePtzStop"
|
|
|
+ >
|
|
|
+ <svg viewBox="0 0 24 24" width="22" height="22">
|
|
|
+ <path fill="currentColor" d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="ptz-btn ptz-btn-center"
|
|
|
+ title="Reset"
|
|
|
+ @click="handlePtzCenter"
|
|
|
+ >
|
|
|
+ <svg viewBox="0 0 24 24" width="22" height="22">
|
|
|
+ <path fill="currentColor" d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8A5.87 5.87 0 0 1 6 12c0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ class="ptz-btn"
|
|
|
+ title="Right"
|
|
|
+ @mousedown="handlePtzStart(PTZDirection.Right)"
|
|
|
+ @mouseup="handlePtzStop"
|
|
|
+ >
|
|
|
+ <svg viewBox="0 0 24 24" width="22" height="22">
|
|
|
+ <path fill="currentColor" d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <div class="ptz-placeholder" />
|
|
|
+ <button
|
|
|
+ class="ptz-btn"
|
|
|
+ title="Down"
|
|
|
+ @mousedown="handlePtzStart(PTZDirection.Down)"
|
|
|
+ @mouseup="handlePtzStop"
|
|
|
+ >
|
|
|
+ <svg viewBox="0 0 24 24" width="22" height="22">
|
|
|
+ <path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <div class="ptz-placeholder" />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 速度控制 -->
|
|
|
+<!-- <div class="ptz-speed">-->
|
|
|
+<!-- <span class="ptz-speed-label">Speed</span>-->
|
|
|
+<!-- <el-slider-->
|
|
|
+<!-- v-model="ptzSpeed"-->
|
|
|
+<!-- :min="1"-->
|
|
|
+<!-- :max="8"-->
|
|
|
+<!-- :step="1"-->
|
|
|
+<!-- :show-tooltip="false"-->
|
|
|
+<!-- size="small"-->
|
|
|
+<!-- />-->
|
|
|
+<!-- <span class="ptz-speed-value">{{ ptzSpeed }}</span>-->
|
|
|
+<!-- </div>-->
|
|
|
+ </div>
|
|
|
+ <!-- 折叠/展开按钮 -->
|
|
|
+ <button class="ptz-toggle" :title="ptzCollapsed ? 'Expand PTZ' : 'Collapse PTZ'" @click="ptzCollapsed = !ptzCollapsed">
|
|
|
+ <svg viewBox="0 0 24 24" width="16" height="16">
|
|
|
+ <path fill="currentColor" :d="ptzCollapsed ? 'M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z' : 'M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'"/>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 视频区域(右侧) -->
|
|
|
<div class="video-wrapper">
|
|
|
<MyVideo
|
|
|
ref="videoRef"
|
|
|
:drag-flag="true"
|
|
|
+ :channel="currentChannel"
|
|
|
@video-error="handleVideoError"
|
|
|
/>
|
|
|
- <!-- 全屏按钮 -->
|
|
|
+ <!-- Channel selector -->
|
|
|
+ <div v-if="channelOptions.length > 1" class="channel-selector">
|
|
|
+ <el-select
|
|
|
+ v-model="currentChannel"
|
|
|
+ size="small"
|
|
|
+ placeholder="Channel"
|
|
|
+ @change="handleChannelChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="ch in channelOptions"
|
|
|
+ :key="ch.value"
|
|
|
+ :label="ch.label"
|
|
|
+ :value="ch.value"
|
|
|
+ />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
<button
|
|
|
class="fullscreen-btn"
|
|
|
- :title="isFullscreen ? '退出全屏 (F)' : '全屏 (F)'"
|
|
|
+ :title="isFullscreen ? 'Exit Fullscreen (F)' : 'Fullscreen (F)'"
|
|
|
@click="toggleFullscreen"
|
|
|
>
|
|
|
<svg
|
|
|
@@ -38,36 +139,64 @@
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import {ref, onMounted, onUnmounted} from 'vue'
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
import MyVideo from '@/components/myVideo.vue'
|
|
|
+import { ptzControl, PTZDirection, getDeviceNum } from '@/api/ptz'
|
|
|
|
|
|
const videoRef = ref()
|
|
|
const isFullscreen = ref(false)
|
|
|
const containerRef = ref<HTMLElement>()
|
|
|
|
|
|
-// 全屏切换
|
|
|
+const ptzSpeed = ref(4)
|
|
|
+const isPtzMoving = ref(false)
|
|
|
+const ptzCollapsed = ref(false)
|
|
|
+
|
|
|
+// Channel
|
|
|
+const currentChannel = ref('ch1')
|
|
|
+const channelOptions = ref<{ label: string; value: string }[]>([
|
|
|
+ { label: 'Channel 1', value: 'ch1' }
|
|
|
+])
|
|
|
+
|
|
|
+// Fetch lens count and build channel options
|
|
|
+const fetchChannelCount = async () => {
|
|
|
+ try {
|
|
|
+ // const res = await getDeviceNum()
|
|
|
+ // const lensNumber = res?.data?.LensNumber ?? 1
|
|
|
+ const lensNumber = 2
|
|
|
+ const options = []
|
|
|
+ for (let i = 1; i <= lensNumber; i++) {
|
|
|
+ options.push({ label: `Channel ${i}`, value: `ch${i}` })
|
|
|
+ }
|
|
|
+ channelOptions.value = options
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to get device channel count:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Switch channel: refresh video stream
|
|
|
+const handleChannelChange = () => {
|
|
|
+ if (videoRef.value?.refreshWebSocket) {
|
|
|
+ videoRef.value.refreshWebSocket()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const toggleFullscreen = () => {
|
|
|
if (!containerRef.value) return
|
|
|
-
|
|
|
if (!isFullscreen.value) {
|
|
|
- // 进入全屏
|
|
|
if (containerRef.value.requestFullscreen) {
|
|
|
containerRef.value.requestFullscreen()
|
|
|
}
|
|
|
} else {
|
|
|
- // 退出全屏
|
|
|
if (document.exitFullscreen) {
|
|
|
document.exitFullscreen()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 监听全屏状态变化
|
|
|
const handleFullscreenChange = () => {
|
|
|
isFullscreen.value = !!document.fullscreenElement
|
|
|
}
|
|
|
|
|
|
-// 键盘快捷键
|
|
|
const handleKeydown = (event: KeyboardEvent) => {
|
|
|
switch (event.code) {
|
|
|
case 'KeyF':
|
|
|
@@ -83,15 +212,42 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 处理视频错误
|
|
|
+const handlePtzStart = async (direction: number) => {
|
|
|
+ if (isPtzMoving.value) return
|
|
|
+ isPtzMoving.value = true
|
|
|
+ try {
|
|
|
+ await ptzControl({ PtzCmd: direction })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('PTZ control failed:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePtzStop = async () => {
|
|
|
+ if (!isPtzMoving.value) return
|
|
|
+ isPtzMoving.value = false
|
|
|
+ try {
|
|
|
+ await ptzControl({ PtzCmd: PTZDirection.Stop })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('PTZ stop failed:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handlePtzCenter = async () => {
|
|
|
+ try {
|
|
|
+ await ptzControl({ PtzCmd: PTZDirection.Center })
|
|
|
+ } catch (error) {
|
|
|
+ console.error('PTZ Center failed:', error)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const handleVideoError = (error: any) => {
|
|
|
- console.error('视频播放错误:', error)
|
|
|
- // 这里可以添加用户友好的错误提示
|
|
|
+ console.error('Video playback error:', error)
|
|
|
}
|
|
|
|
|
|
onMounted(() => {
|
|
|
document.addEventListener('fullscreenchange', handleFullscreenChange)
|
|
|
document.addEventListener('keydown', handleKeydown)
|
|
|
+ fetchChannelCount()
|
|
|
})
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
@@ -108,10 +264,11 @@ onUnmounted(() => {
|
|
|
margin-top: 20px;
|
|
|
justify-content: center;
|
|
|
align-items: center;
|
|
|
+ gap: 16px;
|
|
|
+ padding: 0 16px;
|
|
|
position: relative;
|
|
|
overflow: hidden;
|
|
|
|
|
|
- // 全屏状态
|
|
|
&.is-fullscreen {
|
|
|
position: fixed;
|
|
|
top: 0;
|
|
|
@@ -119,18 +276,38 @@ onUnmounted(() => {
|
|
|
z-index: 9999;
|
|
|
width: 100vw;
|
|
|
height: 100vh;
|
|
|
+ margin-top: 0;
|
|
|
+ padding: 0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.video-wrapper {
|
|
|
position: relative;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- max-width: min(100vw, calc((100vh - 90px) * 16 / 9));
|
|
|
- max-height: min(100vh - 90px, calc(100vw / 16 * 9));
|
|
|
- aspect-ratio: 16/9;
|
|
|
+ /*
|
|
|
+ * 用固定的 calc 尺寸而非 flex:1,
|
|
|
+ * 这样 video.load() 清空内容时容器不会收缩。
|
|
|
+ * 宽度 = 总宽 - PTZ面板(180px) - gap(16px) - 两侧padding(32px) - 留白(20px)
|
|
|
+ * 高度按 16:9 从宽度推算,同时不超过容器高度
|
|
|
+ */
|
|
|
+ width: calc(100vw - 208px);
|
|
|
+ height: calc((100vw - 208px) / 16 * 9);
|
|
|
+ max-height: calc(100vh - 110px);
|
|
|
+ max-width: calc((100vh - 110px) / 9 * 16);
|
|
|
+ flex-shrink: 0;
|
|
|
+
|
|
|
+ :deep(.preview-container) {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 全屏时视频撑满整个屏幕
|
|
|
+ .video-container.is-fullscreen & {
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ max-width: none;
|
|
|
+ max-height: none;
|
|
|
+ }
|
|
|
|
|
|
- // 确保视频组件填满容器
|
|
|
:deep(.preview-container) {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
@@ -142,38 +319,68 @@ onUnmounted(() => {
|
|
|
background: #000;
|
|
|
border-radius: 5px;
|
|
|
overflow: hidden;
|
|
|
- //box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
- // 全屏时移除圆角和阴影
|
|
|
.video-container.is-fullscreen & {
|
|
|
border-radius: 0;
|
|
|
- box-shadow: none;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
:deep(#player) {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
- //object-fit: cover;
|
|
|
object-fit: contain;
|
|
|
}
|
|
|
|
|
|
- // 优化控制条样式
|
|
|
:deep(.video-control) {
|
|
|
opacity: 0;
|
|
|
transition: opacity 0.3s ease;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
+ &:hover { opacity: 1; }
|
|
|
}
|
|
|
|
|
|
- // 鼠标悬停时显示控制条
|
|
|
&:hover :deep(.video-control) {
|
|
|
opacity: 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+/* Channel selector */
|
|
|
+.channel-selector {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 48px;
|
|
|
+ right: 16px;
|
|
|
+ z-index: 10;
|
|
|
+ opacity: 0;
|
|
|
+ transition: opacity 0.3s ease;
|
|
|
+
|
|
|
+ .video-wrapper:hover & {
|
|
|
+ opacity: 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-select) {
|
|
|
+ width: 120px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ border: none;
|
|
|
+ box-shadow: none;
|
|
|
+ backdrop-filter: blur(8px);
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-input__inner) {
|
|
|
+ color: #fff;
|
|
|
+ font-size: 12px;
|
|
|
+
|
|
|
+ &::placeholder {
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-select__caret) {
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
.fullscreen-btn {
|
|
|
position: absolute;
|
|
|
top: 16px;
|
|
|
@@ -197,17 +404,9 @@ onUnmounted(() => {
|
|
|
background: rgba(0, 0, 0, 0.8);
|
|
|
transform: scale(1.05);
|
|
|
}
|
|
|
+ &:active { transform: scale(0.95); }
|
|
|
+ .video-wrapper:hover & { opacity: 1; }
|
|
|
|
|
|
- &:active {
|
|
|
- transform: scale(0.95);
|
|
|
- }
|
|
|
-
|
|
|
- // 鼠标悬停在容器上时显示
|
|
|
- .video-wrapper:hover & {
|
|
|
- opacity: 1;
|
|
|
- }
|
|
|
-
|
|
|
- // 全屏状态下的样式调整
|
|
|
.video-container.is-fullscreen & {
|
|
|
top: 20px;
|
|
|
right: 20px;
|
|
|
@@ -216,66 +415,169 @@ onUnmounted(() => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 响应式设计
|
|
|
-@media (max-width: 768px) {
|
|
|
- .video-container {
|
|
|
- height: 100vh;
|
|
|
+/* PTZ 云台控制面板 */
|
|
|
+.ptz-wrapper {
|
|
|
+ position: relative;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-panel {
|
|
|
+ width: 140px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 12px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ overflow: hidden;
|
|
|
+ transition: width 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
|
|
|
+
|
|
|
+ &.is-collapsed {
|
|
|
+ width: 0;
|
|
|
padding: 0;
|
|
|
+ opacity: 0;
|
|
|
+ pointer-events: none;
|
|
|
}
|
|
|
|
|
|
- .video-wrapper {
|
|
|
- max-width: 100vw;
|
|
|
- max-height: 100vh;
|
|
|
-
|
|
|
- :deep(.video_container) {
|
|
|
- border-radius: 0;
|
|
|
+ .video-container.is-fullscreen & {
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
+
|
|
|
+ .ptz-title { color: #fff; }
|
|
|
+ .ptz-btn {
|
|
|
+ background: rgba(255, 255, 255, 0.1);
|
|
|
+ color: #fff;
|
|
|
+ border-color: rgba(255, 255, 255, 0.15);
|
|
|
+ &:hover { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); }
|
|
|
+ &:active { background: rgba(255, 255, 255, 0.3); }
|
|
|
+ }
|
|
|
+ .ptz-btn-center {
|
|
|
+ background: rgba(64, 158, 255, 0.3) !important;
|
|
|
+ border-color: rgba(64, 158, 255, 0.5) !important;
|
|
|
+ &:hover { background: rgba(64, 158, 255, 0.5) !important; }
|
|
|
}
|
|
|
+ .ptz-speed-label, .ptz-speed-value { color: #fff; }
|
|
|
}
|
|
|
+}
|
|
|
|
|
|
- .fullscreen-btn {
|
|
|
- width: 36px;
|
|
|
- height: 36px;
|
|
|
- top: 12px;
|
|
|
- right: 12px;
|
|
|
+.ptz-toggle {
|
|
|
+ width: 24px;
|
|
|
+ height: 48px;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 0 8px 8px 0;
|
|
|
+ background: #f5f7fa;
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex-shrink: 0;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ padding: 0;
|
|
|
+ margin-left: -1px;
|
|
|
|
|
|
- svg {
|
|
|
- width: 14px;
|
|
|
- height: 14px;
|
|
|
- }
|
|
|
+ &:hover {
|
|
|
+ background: #ecf5ff;
|
|
|
+ color: #409eff;
|
|
|
+ border-color: #b3d8ff;
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-@media (max-width: 480px) {
|
|
|
- .video-wrapper {
|
|
|
- :deep(.video-control) {
|
|
|
- height: 48px;
|
|
|
+ .video-container.is-fullscreen & {
|
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
|
+ border-color: rgba(255, 255, 255, 0.15);
|
|
|
+ color: #fff;
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
|
|
|
- .control-btn {
|
|
|
- width: 40px;
|
|
|
- height: 40px;
|
|
|
- font-size: 20px;
|
|
|
- }
|
|
|
+ &:hover {
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
+ border-color: rgba(255, 255, 255, 0.3);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 减少动画以提升性能
|
|
|
-@media (prefers-reduced-motion: reduce) {
|
|
|
- * {
|
|
|
- transition: none !important;
|
|
|
- animation: none !important;
|
|
|
- }
|
|
|
+.video-container.is-fullscreen .ptz-wrapper {
|
|
|
+ position: absolute;
|
|
|
+ left: 20px;
|
|
|
+ top: 50%;
|
|
|
+ transform: translateY(-50%);
|
|
|
+ z-index: 10;
|
|
|
}
|
|
|
|
|
|
-// 高对比度模式支持
|
|
|
-@media (prefers-contrast: custom) {
|
|
|
- .video-wrapper :deep(.video_container) {
|
|
|
- border: 2px solid white;
|
|
|
- }
|
|
|
+.ptz-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
|
|
|
- .fullscreen-btn {
|
|
|
- border: 1px solid white;
|
|
|
- background: black;
|
|
|
+.ptz-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 4px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-placeholder { aspect-ratio: 1; }
|
|
|
+
|
|
|
+.ptz-btn {
|
|
|
+ width: 100%;
|
|
|
+ aspect-ratio: 1;
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
+ border-radius: 6px;
|
|
|
+ background: #fff;
|
|
|
+ color: #606266;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ padding: 0;
|
|
|
+
|
|
|
+ &:hover { background: #ecf5ff; border-color: #b3d8ff; color: #409eff; }
|
|
|
+ &:active { background: #d9ecff; transform: scale(0.95); }
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-btn-center {
|
|
|
+ background: #ecf5ff !important;
|
|
|
+ border-color: #b3d8ff !important;
|
|
|
+ color: #409eff !important;
|
|
|
+ &:hover { background: #d9ecff !important; }
|
|
|
+}
|
|
|
+
|
|
|
+.ptz-speed {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .ptz-speed-label { font-size: 12px; color: #909399; white-space: nowrap; }
|
|
|
+ .ptz-speed-value { font-size: 12px; color: #606266; font-weight: 600; min-width: 14px; text-align: center; }
|
|
|
+ :deep(.el-slider) { flex: 1; }
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .video-container {
|
|
|
+ flex-direction: column;
|
|
|
+ height: auto;
|
|
|
+ padding: 0;
|
|
|
+ gap: 12px;
|
|
|
+ }
|
|
|
+ .ptz-panel { width: 100%; max-width: 200px; order: 2; }
|
|
|
+ .video-wrapper {
|
|
|
+ order: 1;
|
|
|
+ width: 100vw;
|
|
|
+ height: calc(100vw / 16 * 9);
|
|
|
+ max-height: none;
|
|
|
+ max-width: none;
|
|
|
+ :deep(.video_container) { border-radius: 0; }
|
|
|
}
|
|
|
+ .fullscreen-btn { width: 36px; height: 36px; top: 12px; right: 12px; svg { width: 14px; height: 14px; } }
|
|
|
+}
|
|
|
+
|
|
|
+@media (prefers-reduced-motion: reduce) {
|
|
|
+ * { transition: none !important; animation: none !important; }
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|