|
|
@@ -1,794 +0,0 @@
|
|
|
-<template>
|
|
|
- <div class="page-container">
|
|
|
- <div class="page-header">
|
|
|
- <span class="title">WebRTC 播放</span>
|
|
|
- <!-- <el-tag type="success" size="small" style="margin-left: 10px">延迟 < 2s</el-tag> -->
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 配置区域 -->
|
|
|
- <div class="config-section">
|
|
|
- <el-form label-width="120px">
|
|
|
- <el-form-item label="本地RTC地址">
|
|
|
- <el-input v-model="config.go2rtcUrl" placeholder="服务地址" style="width: 400px">
|
|
|
- <template #prepend>http://</template>
|
|
|
- </el-input>
|
|
|
- <el-text type="info" style="margin-left: 10px">默认端口 1984</el-text>
|
|
|
- </el-form-item>
|
|
|
- <el-form-item label="选择摄像头">
|
|
|
- <el-select
|
|
|
- v-model="config.streamName"
|
|
|
- placeholder="选择摄像头流"
|
|
|
- style="width: 300px"
|
|
|
- @change="handleStreamChange"
|
|
|
- >
|
|
|
- <el-option-group label="ANPVIZ">
|
|
|
- <el-option label="ANPVIZ 主码流" value="anpviz" />
|
|
|
- <el-option label="ANPVIZ 原始流" value="anpviz_raw" />
|
|
|
- <el-option label="ANPVIZ 子码流" value="anpviz_sub" />
|
|
|
- </el-option-group>
|
|
|
- <el-option-group label="CT-IP500">
|
|
|
- <el-option label="CT-IP500 主码流" value="ct-ip500" />
|
|
|
- <el-option label="CT-IP500 子码流" value="ct-ip500_sub" />
|
|
|
- </el-option-group>
|
|
|
- <el-option-group label="HIKVISION 海康威视">
|
|
|
- <el-option label="海康威视 主码流" value="hikvision" />
|
|
|
- <el-option label="海康威视 子码流" value="hikvision_sub" />
|
|
|
- </el-option-group>
|
|
|
- <el-option-group label="SVBC">
|
|
|
- <el-option label="SVBC 主码流" value="svbc" />
|
|
|
- <el-option label="SVBC 原始流" value="svbc_raw" />
|
|
|
- <el-option label="SVBC 子码流" value="svbc_sub" />
|
|
|
- </el-option-group>
|
|
|
- </el-select>
|
|
|
- </el-form-item>
|
|
|
- <!-- http://localhost:1984/api/webrtc?src=camera1 -->
|
|
|
- <el-form-item label="完整URL">
|
|
|
- <el-text>{{ fullGo2rtcUrl }}/api/webrtc?src={{ config.streamName }}</el-text>
|
|
|
- </el-form-item>
|
|
|
- <!-- <el-form-item label="生成的 URL">
|
|
|
- <el-input :value="generatedUrl" readonly style="width: 600px">
|
|
|
- <template #append>
|
|
|
- <el-button @click="copyUrl">复制</el-button>
|
|
|
- </template>
|
|
|
- </el-input>
|
|
|
- </el-form-item> -->
|
|
|
- <el-form-item>
|
|
|
- <el-button type="primary" @click="startPlay">播放</el-button>
|
|
|
- <el-button @click="handleReconnect">重连</el-button>
|
|
|
- <el-button type="danger" @click="handleStop">停止</el-button>
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 播放器和PTZ控制区域 -->
|
|
|
- <div class="player-ptz-container">
|
|
|
- <!-- 播放器区域 -->
|
|
|
- <div class="player-section">
|
|
|
- <div v-if="!isPlaying" class="player-placeholder">
|
|
|
- <el-icon :size="60" color="#ddd">
|
|
|
- <VideoPlay />
|
|
|
- </el-icon>
|
|
|
- <p>请配置 go2rtc 地址和流名称后点击播放</p>
|
|
|
- </div>
|
|
|
- <VideoPlayer
|
|
|
- v-else
|
|
|
- ref="playerRef"
|
|
|
- player-type="webrtc"
|
|
|
- :go2rtc-url="fullGo2rtcUrl"
|
|
|
- :stream-name="config.streamName"
|
|
|
- :autoplay="playConfig.autoplay"
|
|
|
- :muted="playConfig.muted"
|
|
|
- :controls="true"
|
|
|
- @play="onPlay"
|
|
|
- @pause="onPause"
|
|
|
- @error="onError"
|
|
|
- />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- PTZ 云台控制 -->
|
|
|
- <div class="ptz-panel">
|
|
|
- <div class="ptz-header">
|
|
|
- <span>PTZ 云台控制</span>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- PTZ 方向控制 九宫格 -->
|
|
|
- <div class="ptz-controls">
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('UP_LEFT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <TopLeft />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('UP')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Top />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('UP_RIGHT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <TopRight />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('LEFT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Back />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn ptz-center" @click="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Refresh />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('RIGHT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Right />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('DOWN_LEFT')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <BottomLeft />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div class="ptz-btn" @mousedown="handlePTZ('DOWN')" @mouseup="handlePTZStop" @mouseleave="handlePTZStop">
|
|
|
- <el-icon>
|
|
|
- <Bottom />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <div
|
|
|
- class="ptz-btn"
|
|
|
- @mousedown="handlePTZ('DOWN_RIGHT')"
|
|
|
- @mouseup="handlePTZStop"
|
|
|
- @mouseleave="handlePTZStop"
|
|
|
- >
|
|
|
- <el-icon>
|
|
|
- <BottomRight />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 速度控制 -->
|
|
|
- <div class="speed-control">
|
|
|
- <div class="control-label">
|
|
|
- <span>速度: {{ ptzSpeed }}</span>
|
|
|
- </div>
|
|
|
- <el-slider v-model="ptzSpeed" :min="10" :max="100" :step="10" :show-tooltip="true" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 缩放控制 -->
|
|
|
- <div class="zoom-controls">
|
|
|
- <div class="zoom-header">
|
|
|
- <el-icon>
|
|
|
- <ZoomOut />
|
|
|
- </el-icon>
|
|
|
- <span>缩放</span>
|
|
|
- <el-icon>
|
|
|
- <ZoomIn />
|
|
|
- </el-icon>
|
|
|
- </div>
|
|
|
- <el-slider
|
|
|
- v-model="zoomValue"
|
|
|
- :min="-100"
|
|
|
- :max="100"
|
|
|
- :step="10"
|
|
|
- :show-tooltip="true"
|
|
|
- :format-tooltip="formatZoomTooltip"
|
|
|
- @input="handleZoomChange"
|
|
|
- @change="handleZoomRelease"
|
|
|
- />
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 播放控制 -->
|
|
|
- <div class="control-section">
|
|
|
- <el-space wrap>
|
|
|
- <el-button type="primary" @click="handlePlay">播放</el-button>
|
|
|
- <el-button @click="handlePause">暂停</el-button>
|
|
|
- <el-button @click="handleScreenshot">截图</el-button>
|
|
|
- <el-button @click="handleFullscreen">全屏</el-button>
|
|
|
-
|
|
|
- <el-divider direction="vertical" />
|
|
|
-
|
|
|
- <el-switch v-model="playConfig.muted" active-text="静音" inactive-text="有声" />
|
|
|
- <el-switch v-model="playConfig.autoplay" active-text="自动播放" inactive-text="手动" />
|
|
|
- </el-space>
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- 日志区域 -->
|
|
|
- <div class="log-section">
|
|
|
- <div class="log-header">
|
|
|
- <h4>事件日志</h4>
|
|
|
- <el-button size="small" @click="logs = []">清空</el-button>
|
|
|
- </div>
|
|
|
- <div class="log-content">
|
|
|
- <div v-for="(log, index) in logs" :key="index" class="log-item" :class="log.type">
|
|
|
- <span class="time">{{ log.time }}</span>
|
|
|
- <span class="message">{{ log.message }}</span>
|
|
|
- </div>
|
|
|
- <div v-if="logs.length === 0" class="log-empty">暂无日志</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup lang="ts">
|
|
|
-import { ref, reactive, computed } from 'vue'
|
|
|
-import { ElMessage } from 'element-plus'
|
|
|
-import {
|
|
|
- VideoPlay,
|
|
|
- Top,
|
|
|
- Bottom,
|
|
|
- Back,
|
|
|
- Right,
|
|
|
- TopLeft,
|
|
|
- TopRight,
|
|
|
- BottomLeft,
|
|
|
- BottomRight,
|
|
|
- Refresh,
|
|
|
- ZoomIn,
|
|
|
- ZoomOut
|
|
|
-} from '@element-plus/icons-vue'
|
|
|
-import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
|
-import {
|
|
|
- startPTZ,
|
|
|
- stopPTZ,
|
|
|
- PTZ_DIRECTIONS,
|
|
|
- startZoom,
|
|
|
- stopZoom,
|
|
|
- PTZ_ZOOM_DIRECTIONS,
|
|
|
- type CameraVendor
|
|
|
-} from '@/api/ptz'
|
|
|
-
|
|
|
-const playerRef = ref<InstanceType<typeof VideoPlayer>>()
|
|
|
-
|
|
|
-// 配置
|
|
|
-const config = reactive({
|
|
|
- go2rtcUrl: 'localhost:1984',
|
|
|
- streamName: 'hikvision'
|
|
|
-})
|
|
|
-
|
|
|
-// 播放配置
|
|
|
-const playConfig = reactive({
|
|
|
- autoplay: true,
|
|
|
- muted: true
|
|
|
-})
|
|
|
-
|
|
|
-// 摄像头 PTZ 配置映射 (统一使用 viewer 账号)
|
|
|
-const cameraPtzConfigs: Record<string, { vendor: CameraVendor; host: string; username: string; password: string }> = {
|
|
|
- // ANPVIZ
|
|
|
- anpviz: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
|
|
|
- anpviz_raw: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
|
|
|
- anpviz_sub: { vendor: 'ANPVIZ', host: '192.168.0.96', username: 'viewer', password: 'Wxc767718929' },
|
|
|
- // CT-IP500 (无 PTZ)
|
|
|
- 'ct-ip500': { vendor: 'CT-IP500', host: '', username: '', password: '' },
|
|
|
- 'ct-ip500_sub': { vendor: 'CT-IP500', host: '', username: '', password: '' },
|
|
|
- // HIKVISION 海康威视
|
|
|
- hikvision: { vendor: 'HIKVISION', host: '192.168.0.64', username: 'viewer', password: 'Wxc767718929' },
|
|
|
- hikvision_sub: { vendor: 'HIKVISION', host: '192.168.0.64', username: 'viewer', password: 'Wxc767718929' },
|
|
|
- // SVBC (无 PTZ)
|
|
|
- svbc: { vendor: 'SVBC', host: '', username: '', password: '' },
|
|
|
- svbc_raw: { vendor: 'SVBC', host: '', username: '', password: '' },
|
|
|
- svbc_sub: { vendor: 'SVBC', host: '', username: '', password: '' }
|
|
|
-}
|
|
|
-
|
|
|
-// PTZ 配置 (根据选择的摄像头动态更新)
|
|
|
-const ptzConfig = reactive({
|
|
|
- vendor: 'HIKVISION' as CameraVendor,
|
|
|
- host: '192.168.0.64',
|
|
|
- username: 'viewer',
|
|
|
- password: 'Wxc767718929'
|
|
|
-})
|
|
|
-
|
|
|
-// PTZ 速度和缩放
|
|
|
-const ptzSpeed = ref(50)
|
|
|
-const zoomValue = ref(0)
|
|
|
-
|
|
|
-// 播放状态
|
|
|
-const isPlaying = ref(false)
|
|
|
-
|
|
|
-// 完整的 go2rtc URL
|
|
|
-const fullGo2rtcUrl = computed(() => {
|
|
|
- if (!config.go2rtcUrl) return ''
|
|
|
- const url = config.go2rtcUrl.startsWith('http') ? config.go2rtcUrl : `http://${config.go2rtcUrl}`
|
|
|
- return url
|
|
|
-})
|
|
|
-
|
|
|
-// 生成的 WebRTC API URL
|
|
|
-const generatedUrl = computed(() => {
|
|
|
- if (!fullGo2rtcUrl.value || !config.streamName) return ''
|
|
|
- return `${fullGo2rtcUrl.value}/api/webrtc?src=${config.streamName}`
|
|
|
-})
|
|
|
-
|
|
|
-// 连接状态
|
|
|
-const connectionStatus = computed(() => {
|
|
|
- return playerRef.value?.webrtcStatus?.value || 'idle'
|
|
|
-})
|
|
|
-
|
|
|
-const statusText = computed(() => {
|
|
|
- const map: Record<string, string> = {
|
|
|
- idle: '未连接',
|
|
|
- connecting: '连接中',
|
|
|
- connected: '已连接',
|
|
|
- failed: '连接失败'
|
|
|
- }
|
|
|
- return map[connectionStatus.value] || '未知'
|
|
|
-})
|
|
|
-
|
|
|
-const statusTagType = computed(() => {
|
|
|
- const map: Record<string, '' | 'success' | 'warning' | 'danger' | 'info'> = {
|
|
|
- idle: 'info',
|
|
|
- connecting: 'warning',
|
|
|
- connected: 'success',
|
|
|
- failed: 'danger'
|
|
|
- }
|
|
|
- return map[connectionStatus.value] || 'info'
|
|
|
-})
|
|
|
-
|
|
|
-// 日志
|
|
|
-interface LogItem {
|
|
|
- time: string
|
|
|
- type: 'info' | 'success' | 'error'
|
|
|
- message: string
|
|
|
-}
|
|
|
-const logs = ref<LogItem[]>([])
|
|
|
-
|
|
|
-function addLog(message: string, type: LogItem['type'] = 'info') {
|
|
|
- const time = new Date().toLocaleTimeString()
|
|
|
- logs.value.unshift({ time, type, message })
|
|
|
- if (logs.value.length > 100) {
|
|
|
- logs.value.pop()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 开始播放
|
|
|
-function startPlay() {
|
|
|
- if (!config.go2rtcUrl) {
|
|
|
- ElMessage.warning('请输入 go2rtc 地址')
|
|
|
- return
|
|
|
- }
|
|
|
- if (!config.streamName) {
|
|
|
- ElMessage.warning('请选择摄像头')
|
|
|
- return
|
|
|
- }
|
|
|
- isPlaying.value = true
|
|
|
- addLog(`开始 WebRTC 播放: ${config.streamName}`, 'success')
|
|
|
-}
|
|
|
-
|
|
|
-// 切换摄像头流
|
|
|
-function handleStreamChange(streamName: string) {
|
|
|
- addLog(`切换摄像头: ${streamName}`, 'info')
|
|
|
-
|
|
|
- // 更新 PTZ 配置
|
|
|
- const ptzSettings = cameraPtzConfigs[streamName]
|
|
|
- if (ptzSettings) {
|
|
|
- ptzConfig.vendor = ptzSettings.vendor
|
|
|
- ptzConfig.host = ptzSettings.host
|
|
|
- ptzConfig.username = ptzSettings.username
|
|
|
- ptzConfig.password = ptzSettings.password
|
|
|
- addLog(`PTZ 配置已切换到: ${ptzSettings.vendor} (${ptzSettings.host || '无PTZ'})`, 'info')
|
|
|
- }
|
|
|
-
|
|
|
- if (isPlaying.value) {
|
|
|
- // 如果正在播放,重新连接新的流
|
|
|
- playerRef.value?.reconnect()
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 复制 URL
|
|
|
-function copyUrl() {
|
|
|
- if (!generatedUrl.value) {
|
|
|
- ElMessage.warning('请先配置 go2rtc 地址和流名称')
|
|
|
- return
|
|
|
- }
|
|
|
- navigator.clipboard.writeText(generatedUrl.value)
|
|
|
- ElMessage.success('已复制到剪贴板')
|
|
|
-}
|
|
|
-
|
|
|
-// 播放控制
|
|
|
-function handlePlay() {
|
|
|
- playerRef.value?.play()
|
|
|
- addLog('播放', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function handlePause() {
|
|
|
- playerRef.value?.pause()
|
|
|
- addLog('暂停', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function handleStop() {
|
|
|
- isPlaying.value = false
|
|
|
- addLog('停止播放', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function handleReconnect() {
|
|
|
- playerRef.value?.reconnect()
|
|
|
- addLog('重新连接', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function handleScreenshot() {
|
|
|
- playerRef.value?.screenshot()
|
|
|
- addLog('截图', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function handleFullscreen() {
|
|
|
- playerRef.value?.fullscreen()
|
|
|
- addLog('全屏', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-// 事件处理
|
|
|
-function onPlay() {
|
|
|
- addLog('视频开始播放', 'success')
|
|
|
-}
|
|
|
-
|
|
|
-function onPause() {
|
|
|
- addLog('视频已暂停', 'info')
|
|
|
-}
|
|
|
-
|
|
|
-function onError(error: any) {
|
|
|
- addLog(`播放错误: ${error?.message || JSON.stringify(error)}`, 'error')
|
|
|
-}
|
|
|
-
|
|
|
-// PTZ 控制
|
|
|
-async function handlePTZ(direction: keyof typeof PTZ_DIRECTIONS) {
|
|
|
- if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) {
|
|
|
- ElMessage.warning('当前摄像头不支持 PTZ 控制')
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const result = await startPTZ(
|
|
|
- {
|
|
|
- host: ptzConfig.host,
|
|
|
- username: ptzConfig.username,
|
|
|
- password: ptzConfig.password,
|
|
|
- vendor: ptzConfig.vendor
|
|
|
- },
|
|
|
- direction,
|
|
|
- ptzSpeed.value
|
|
|
- )
|
|
|
-
|
|
|
- if (result.success) {
|
|
|
- addLog(`PTZ 移动: ${direction} (${ptzConfig.vendor}, 速度: ${ptzSpeed.value})`, 'info')
|
|
|
- } else {
|
|
|
- addLog(`PTZ 控制失败: ${result.error}`, 'error')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-async function handlePTZStop() {
|
|
|
- if (!ptzConfig.host) return
|
|
|
-
|
|
|
- const result = await stopPTZ({
|
|
|
- host: ptzConfig.host,
|
|
|
- username: ptzConfig.username,
|
|
|
- password: ptzConfig.password,
|
|
|
- vendor: ptzConfig.vendor
|
|
|
- })
|
|
|
-
|
|
|
- if (!result.success) {
|
|
|
- addLog(`PTZ 停止失败: ${result.error}`, 'error')
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 缩放滑块控制
|
|
|
-function formatZoomTooltip(val: number) {
|
|
|
- if (val === 0) return '停止'
|
|
|
- return val > 0 ? `放大 ${val}` : `缩小 ${Math.abs(val)}`
|
|
|
-}
|
|
|
-
|
|
|
-async function handleZoomChange(val: number) {
|
|
|
- if (!ptzConfig.host || !ptzConfig.username || !ptzConfig.password) return
|
|
|
-
|
|
|
- if (val === 0) {
|
|
|
- await stopZoom({
|
|
|
- host: ptzConfig.host,
|
|
|
- username: ptzConfig.username,
|
|
|
- password: ptzConfig.password,
|
|
|
- vendor: ptzConfig.vendor
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const direction = val > 0 ? 'IN' : 'OUT'
|
|
|
- const speed = Math.abs(val)
|
|
|
-
|
|
|
- await startZoom(
|
|
|
- {
|
|
|
- host: ptzConfig.host,
|
|
|
- username: ptzConfig.username,
|
|
|
- password: ptzConfig.password,
|
|
|
- vendor: ptzConfig.vendor
|
|
|
- },
|
|
|
- direction,
|
|
|
- speed
|
|
|
- )
|
|
|
-}
|
|
|
-
|
|
|
-async function handleZoomRelease() {
|
|
|
- // 松开滑块时回到中间并停止
|
|
|
- zoomValue.value = 0
|
|
|
- if (!ptzConfig.host) return
|
|
|
-
|
|
|
- await stopZoom({
|
|
|
- host: ptzConfig.host,
|
|
|
- username: ptzConfig.username,
|
|
|
- password: ptzConfig.password,
|
|
|
- vendor: ptzConfig.vendor
|
|
|
- })
|
|
|
- addLog('缩放停止', 'info')
|
|
|
-}
|
|
|
-</script>
|
|
|
-
|
|
|
-<style lang="scss" scoped>
|
|
|
-.page-container {
|
|
|
-}
|
|
|
-
|
|
|
-.page-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 20px;
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
- color: var(--text-primary);
|
|
|
-
|
|
|
- .title {
|
|
|
- font-size: 18px;
|
|
|
- font-weight: 600;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.config-section {
|
|
|
- padding: 20px;
|
|
|
- margin-bottom: 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
-}
|
|
|
-
|
|
|
-.player-ptz-container {
|
|
|
- padding: 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- display: flex;
|
|
|
- gap: 20px;
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.player-section {
|
|
|
- flex: 1;
|
|
|
- height: 500px;
|
|
|
- border-radius: var(--radius-base);
|
|
|
- overflow: hidden;
|
|
|
- background-color: #000;
|
|
|
-
|
|
|
- .player-placeholder {
|
|
|
- height: 100%;
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- color: var(--text-secondary);
|
|
|
-
|
|
|
- p {
|
|
|
- margin-top: 15px;
|
|
|
- font-size: 14px;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.ptz-panel {
|
|
|
- width: 280px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
- padding: 15px;
|
|
|
-
|
|
|
- .ptz-header {
|
|
|
- font-size: 14px;
|
|
|
- font-weight: 600;
|
|
|
- color: var(--text-primary);
|
|
|
- margin-bottom: 15px;
|
|
|
- padding-bottom: 10px;
|
|
|
- border-bottom: 1px solid var(--border-color);
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-config {
|
|
|
- margin-bottom: 15px;
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-controls {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(3, 1fr);
|
|
|
- gap: 8px;
|
|
|
- margin-bottom: 15px;
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-btn {
|
|
|
- aspect-ratio: 1;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- background-color: var(--bg-hover);
|
|
|
- border: 1px solid var(--border-color);
|
|
|
- border-radius: var(--radius-sm);
|
|
|
- cursor: pointer;
|
|
|
- transition: all 0.2s;
|
|
|
- color: var(--text-regular);
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background-color: var(--color-primary-light-9);
|
|
|
- border-color: var(--color-primary);
|
|
|
- color: var(--color-primary);
|
|
|
- }
|
|
|
-
|
|
|
- &:active {
|
|
|
- background-color: var(--color-primary);
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
-
|
|
|
- .el-icon {
|
|
|
- font-size: 20px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-center {
|
|
|
- background-color: var(--bg-page);
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background-color: var(--color-primary-light-9);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .speed-control {
|
|
|
- margin-top: 15px;
|
|
|
- padding-top: 15px;
|
|
|
- border-top: 1px solid var(--border-color);
|
|
|
-
|
|
|
- .control-label {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary);
|
|
|
- margin-bottom: 8px;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .zoom-controls {
|
|
|
- margin-top: 15px;
|
|
|
- padding-top: 15px;
|
|
|
- border-top: 1px solid var(--border-color);
|
|
|
-
|
|
|
- .zoom-header {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary);
|
|
|
- margin-bottom: 8px;
|
|
|
-
|
|
|
- span {
|
|
|
- flex: 1;
|
|
|
- text-align: center;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .ptz-speed {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 10px;
|
|
|
-
|
|
|
- span {
|
|
|
- font-size: 12px;
|
|
|
- color: var(--text-secondary);
|
|
|
- white-space: nowrap;
|
|
|
- }
|
|
|
-
|
|
|
- .el-slider {
|
|
|
- flex: 1;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.control-section {
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.status-section {
|
|
|
- margin-bottom: 20px;
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
-}
|
|
|
-
|
|
|
-.info-section {
|
|
|
- margin-bottom: 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
-
|
|
|
- .info-content {
|
|
|
- padding: 10px 0;
|
|
|
-
|
|
|
- h4 {
|
|
|
- margin: 15px 0 8px;
|
|
|
- color: var(--text-primary);
|
|
|
- font-size: 14px;
|
|
|
-
|
|
|
- &:first-child {
|
|
|
- margin-top: 0;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- p {
|
|
|
- margin: 0;
|
|
|
- color: var(--text-regular);
|
|
|
- font-size: 13px;
|
|
|
- }
|
|
|
-
|
|
|
- .code-block {
|
|
|
- background-color: var(--bg-hover);
|
|
|
- padding: 12px;
|
|
|
- border-radius: var(--radius-sm);
|
|
|
- font-family: 'Monaco', 'Menlo', monospace;
|
|
|
- font-size: 12px;
|
|
|
- line-height: 1.6;
|
|
|
- overflow-x: auto;
|
|
|
- color: var(--text-primary);
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-.log-section {
|
|
|
- padding: 15px 20px;
|
|
|
- background-color: var(--bg-container);
|
|
|
- border-radius: var(--radius-base);
|
|
|
-
|
|
|
- .log-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 10px;
|
|
|
-
|
|
|
- h4 {
|
|
|
- font-size: 14px;
|
|
|
- margin: 0;
|
|
|
- color: var(--text-primary);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .log-content {
|
|
|
- max-height: 200px;
|
|
|
- overflow-y: auto;
|
|
|
- background-color: var(--bg-hover);
|
|
|
- border-radius: var(--radius-sm);
|
|
|
- padding: 10px;
|
|
|
- }
|
|
|
-
|
|
|
- .log-item {
|
|
|
- font-size: 12px;
|
|
|
- padding: 4px 0;
|
|
|
- border-bottom: 1px solid var(--border-color-light);
|
|
|
- color: var(--text-regular);
|
|
|
-
|
|
|
- &:last-child {
|
|
|
- border-bottom: none;
|
|
|
- }
|
|
|
-
|
|
|
- .time {
|
|
|
- color: var(--text-secondary);
|
|
|
- margin-right: 10px;
|
|
|
- }
|
|
|
-
|
|
|
- &.success .message {
|
|
|
- color: var(--color-success);
|
|
|
- }
|
|
|
-
|
|
|
- &.error .message {
|
|
|
- color: var(--color-danger);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .log-empty {
|
|
|
- text-align: center;
|
|
|
- color: var(--text-secondary);
|
|
|
- font-size: 12px;
|
|
|
- padding: 20px 0;
|
|
|
- }
|
|
|
-}
|
|
|
-</style>
|