861 lines
20 KiB
Vue
861 lines
20 KiB
Vue
<script setup lang="ts">
|
||
import { ref, onUnmounted } from 'vue'
|
||
|
||
const GATEWAY_URL = 'http://localhost:8080'
|
||
|
||
// 查询参数
|
||
const channelId = ref(1)
|
||
const startTime = ref('')
|
||
const endTime = ref('')
|
||
const queryLoading = ref(false)
|
||
const queryError = ref('')
|
||
const recordList = ref<any[]>([])
|
||
const selectedRecord = ref<any>(null)
|
||
|
||
// 初始化时间为今天全天
|
||
const initTime = () => {
|
||
const today = new Date()
|
||
const year = today.getFullYear()
|
||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||
const day = String(today.getDate()).padStart(2, '0')
|
||
|
||
startTime.value = `${year}-${month}-${day} 00:00:00`
|
||
endTime.value = `${year}-${month}-${day} 23:59:59`
|
||
}
|
||
|
||
// 初始化时间
|
||
initTime()
|
||
|
||
// 查询录像列表
|
||
const queryRecordList = async () => {
|
||
if (!channelId.value) {
|
||
queryError.value = '请输入通道ID'
|
||
return
|
||
}
|
||
|
||
if (!startTime.value || !endTime.value) {
|
||
queryError.value = '请选择开始时间和结束时间'
|
||
return
|
||
}
|
||
|
||
queryLoading.value = true
|
||
queryError.value = ''
|
||
recordList.value = []
|
||
selectedRecord.value = null
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
channelId: channelId.value.toString(),
|
||
startTime: startTime.value,
|
||
endTime: endTime.value
|
||
})
|
||
|
||
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback/query?${params}`)
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
if (data.code === 0 && data.data) {
|
||
recordList.value = data.data
|
||
if (recordList.value.length === 0) {
|
||
queryError.value = '未找到录像记录'
|
||
}
|
||
} else {
|
||
queryError.value = data.msg || '查询录像列表失败'
|
||
}
|
||
} catch (error) {
|
||
queryError.value = error instanceof Error ? error.message : '查询录像列表失败'
|
||
} finally {
|
||
queryLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 播放相关
|
||
const playLoading = ref(false)
|
||
const playError = ref('')
|
||
const playbackData = ref<any>(null)
|
||
const currentStreamId = ref<string>('')
|
||
|
||
// Jessibuca 播放器
|
||
let jessibucaPlayer: any = null
|
||
const playerContainerRef = ref<HTMLDivElement | null>(null)
|
||
const isPlaying = ref(false)
|
||
const kBps = ref(0)
|
||
|
||
// 停止当前播放流(调用API)
|
||
const stopPlaybackStream = async () => {
|
||
if (!currentStreamId.value) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
channelId: channelId.value.toString(),
|
||
stream: currentStreamId.value
|
||
})
|
||
|
||
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback/stop?${params}`)
|
||
|
||
if (!response.ok) {
|
||
console.error('停止播放流失败:', response.status)
|
||
}
|
||
|
||
const data = await response.json()
|
||
if (data.code === 0) {
|
||
console.log('停止播放流成功')
|
||
} else {
|
||
console.error('停止播放流失败:', data.msg)
|
||
}
|
||
} catch (error) {
|
||
console.error('停止播放流异常:', error)
|
||
} finally {
|
||
currentStreamId.value = ''
|
||
}
|
||
}
|
||
|
||
// 播放录像
|
||
const playRecord = async (record: any) => {
|
||
selectedRecord.value = record
|
||
playLoading.value = true
|
||
playError.value = ''
|
||
|
||
// 先停止当前播放的流(如果有)
|
||
if (currentStreamId.value) {
|
||
await stopPlaybackStream()
|
||
}
|
||
|
||
// 停止播放器
|
||
stopPlay()
|
||
|
||
try {
|
||
const params = new URLSearchParams({
|
||
channelId: channelId.value.toString(),
|
||
startTime: record.startTime,
|
||
endTime: record.endTime
|
||
})
|
||
|
||
const response = await fetch(`${GATEWAY_URL}/wvp/api/common/channel/playback?${params}`)
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`)
|
||
}
|
||
|
||
const data = await response.json()
|
||
if (data.code === 0 && data.data) {
|
||
playbackData.value = data.data
|
||
playError.value = ''
|
||
|
||
// 保存stream ID用于后续停止
|
||
if (data.data.stream) {
|
||
currentStreamId.value = data.data.stream
|
||
}
|
||
|
||
// 自动播放WebSocket-FLV流
|
||
const playUrl = getPlayUrl(data.data)
|
||
if (playUrl) {
|
||
setTimeout(() => {
|
||
playJessibuca(playUrl)
|
||
}, 500)
|
||
} else {
|
||
playError.value = '未找到可用的播放地址'
|
||
}
|
||
} else {
|
||
playError.value = data.msg || '获取播放地址失败'
|
||
}
|
||
} catch (error) {
|
||
playError.value = error instanceof Error ? error.message : '获取播放地址失败'
|
||
} finally {
|
||
playLoading.value = false
|
||
}
|
||
}
|
||
|
||
// 获取播放地址(优先使用WebSocket-FLV)
|
||
const getPlayUrl = (data: any) => {
|
||
// 根据协议选择合适的流地址
|
||
if (location.protocol === 'https:') {
|
||
return data.wss_flv || data.ws_flv || data.flv
|
||
} else {
|
||
return data.ws_flv || data.wss_flv || data.flv
|
||
}
|
||
}
|
||
|
||
// 使用Jessibuca播放器播放
|
||
const playJessibuca = (url: string) => {
|
||
playError.value = ''
|
||
|
||
if (!playerContainerRef.value) {
|
||
playError.value = '播放器容器未找到'
|
||
return
|
||
}
|
||
|
||
if (jessibucaPlayer) {
|
||
jessibucaPlayer.destroy()
|
||
jessibucaPlayer = null
|
||
}
|
||
|
||
try {
|
||
// @ts-ignore
|
||
jessibucaPlayer = new window.Jessibuca({
|
||
container: playerContainerRef.value,
|
||
videoBuffer: 0,
|
||
isResize: true,
|
||
useMSE: true,
|
||
useWCS: false,
|
||
text: '',
|
||
controlAutoHide: false,
|
||
debug: false,
|
||
hotKey: true,
|
||
// @ts-ignore
|
||
decoder: '/static/js/jessibuca/decoder.js',
|
||
timeout: 10,
|
||
recordType: 'mp4',
|
||
isFlv: false,
|
||
forceNoOffscreen: true,
|
||
hasAudio: true,
|
||
heartTimeout: 5,
|
||
heartTimeoutReplay: true,
|
||
heartTimeoutReplayTimes: 3,
|
||
hiddenAutoPause: false,
|
||
isNotMute: true,
|
||
keepScreenOn: true,
|
||
loadingText: '请稍等, 视频加载中......',
|
||
loadingTimeout: 10,
|
||
loadingTimeoutReplay: true,
|
||
loadingTimeoutReplayTimes: 3,
|
||
operateBtns: {
|
||
fullscreen: false,
|
||
screenshot: false,
|
||
play: false,
|
||
audio: false,
|
||
recorder: false
|
||
},
|
||
showBandwidth: false,
|
||
supportDblclickFullscreen: false,
|
||
useWebFullSreen: true,
|
||
wasmDecodeErrorReplay: true
|
||
})
|
||
|
||
jessibucaPlayer.on('play', () => {
|
||
isPlaying.value = true
|
||
})
|
||
|
||
jessibucaPlayer.on('pause', () => {
|
||
isPlaying.value = false
|
||
})
|
||
|
||
jessibucaPlayer.on('kBps', (val: number) => {
|
||
kBps.value = Math.round(val)
|
||
})
|
||
|
||
jessibucaPlayer.on('error', (error: any) => {
|
||
console.error('Jessibuca error:', error)
|
||
playError.value = '播放错误: ' + (error.message || error)
|
||
isPlaying.value = false
|
||
})
|
||
|
||
jessibucaPlayer.on('timeout', () => {
|
||
playError.value = '播放超时'
|
||
isPlaying.value = false
|
||
})
|
||
|
||
jessibucaPlayer.play(url)
|
||
} catch (error) {
|
||
playError.value = error instanceof Error ? error.message : '播放失败'
|
||
isPlaying.value = false
|
||
}
|
||
}
|
||
|
||
// 停止播放(仅停止播放器,不调用API)
|
||
const stopPlay = () => {
|
||
if (jessibucaPlayer) {
|
||
jessibucaPlayer.pause()
|
||
jessibucaPlayer.destroy()
|
||
jessibucaPlayer = null
|
||
}
|
||
isPlaying.value = false
|
||
kBps.value = 0
|
||
}
|
||
|
||
// 完全停止播放(停止播放器并调用API)
|
||
const stopPlayComplete = async () => {
|
||
// 停止播放器
|
||
stopPlay()
|
||
|
||
// 调用API停止流
|
||
await stopPlaybackStream()
|
||
|
||
// 清空播放数据
|
||
playbackData.value = null
|
||
selectedRecord.value = null
|
||
}
|
||
|
||
// 格式化文件大小
|
||
const formatFileSize = (size: number | null) => {
|
||
if (!size) return '-'
|
||
if (size < 1024) return size + ' B'
|
||
if (size < 1024 * 1024) return (size / 1024).toFixed(2) + ' KB'
|
||
if (size < 1024 * 1024 * 1024) return (size / 1024 / 1024).toFixed(2) + ' MB'
|
||
return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
||
}
|
||
|
||
// 页面关闭前停止播放
|
||
const handleBeforeUnload = async () => {
|
||
stopPlay()
|
||
await stopPlaybackStream()
|
||
}
|
||
|
||
// 页面可见性变化处理(当用户切换标签页或最小化窗口时)
|
||
const handleVisibilityChange = async () => {
|
||
if (document.hidden && isPlaying.value) {
|
||
console.log('页面不可见,停止播放以节省资源')
|
||
// 页面不可见时停止播放
|
||
await stopPlayComplete()
|
||
}
|
||
}
|
||
|
||
// 网络状态变化处理
|
||
const handleOnline = () => {
|
||
console.log('网络已恢复')
|
||
}
|
||
|
||
const handleOffline = async () => {
|
||
console.log('网络已断开,停止播放')
|
||
if (isPlaying.value) {
|
||
// 网络断开时停止播放器(但不调用API,因为网络已断开)
|
||
stopPlay()
|
||
playError.value = '网络连接已断开'
|
||
}
|
||
}
|
||
|
||
// 组件挂载时添加各种事件监听
|
||
if (typeof window !== 'undefined') {
|
||
window.addEventListener('beforeunload', handleBeforeUnload)
|
||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||
window.addEventListener('online', handleOnline)
|
||
window.addEventListener('offline', handleOffline)
|
||
}
|
||
|
||
// 组件卸载时清理播放器和停止流
|
||
onUnmounted(async () => {
|
||
// 移除所有事件监听
|
||
if (typeof window !== 'undefined') {
|
||
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||
window.removeEventListener('online', handleOnline)
|
||
window.removeEventListener('offline', handleOffline)
|
||
}
|
||
|
||
// 停止播放器和流
|
||
stopPlay()
|
||
await stopPlaybackStream()
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="gb28181-record-container">
|
||
<h2>GB28181 国标录像回放</h2>
|
||
|
||
<div class="api-section">
|
||
<h3>1. 查询录像列表</h3>
|
||
<div class="api-description">
|
||
<p>通过网关调用: <code>GET /wvp/api/common/channel/playback/query</code></p>
|
||
<p>实际转发到: <code>http://114.67.89.4:9090/api/common/channel/playback/query</code></p>
|
||
<p>网关会自动注入 access-token 请求头</p>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>通道ID (channelId) - 必填:</label>
|
||
<input
|
||
v-model.number="channelId"
|
||
type="number"
|
||
min="1"
|
||
placeholder="请输入通道ID"
|
||
/>
|
||
<small style="color: #666; display: block; margin-top: 5px;">
|
||
提示:通道ID是数据库主键ID(gbId),不是国标设备ID
|
||
</small>
|
||
</div>
|
||
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label>开始时间 (startTime):</label>
|
||
<input
|
||
v-model="startTime"
|
||
type="text"
|
||
placeholder="2025-12-10 00:00:00"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>结束时间 (endTime):</label>
|
||
<input
|
||
v-model="endTime"
|
||
type="text"
|
||
placeholder="2025-12-10 23:59:59"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button @click="queryRecordList" :disabled="queryLoading">
|
||
{{ queryLoading ? '查询中...' : '搜索' }}
|
||
</button>
|
||
<button @click="initTime" class="secondary-button">
|
||
重置为今天
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="queryError" class="error">
|
||
错误: {{ queryError }}
|
||
</div>
|
||
|
||
<div v-if="recordList.length > 0" class="record-list">
|
||
<h4>录像列表 (共 {{ recordList.length }} 条):</h4>
|
||
<table class="record-table">
|
||
<thead>
|
||
<tr>
|
||
<th>序号</th>
|
||
<th>开始时间</th>
|
||
<th>结束时间</th>
|
||
<th>文件大小</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr
|
||
v-for="(record, index) in recordList"
|
||
:key="index"
|
||
:class="{ 'selected': selectedRecord === record }"
|
||
>
|
||
<td>{{ index + 1 }}</td>
|
||
<td>{{ record.startTime }}</td>
|
||
<td>{{ record.endTime }}</td>
|
||
<td>{{ formatFileSize(record.fileSize) }}</td>
|
||
<td>
|
||
<button
|
||
@click="playRecord(record)"
|
||
class="play-button"
|
||
:disabled="playLoading && selectedRecord === record"
|
||
>
|
||
{{ (playLoading && selectedRecord === record) ? '加载中...' : '播放' }}
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="api-section" v-if="playbackData">
|
||
<h3>2. 录像回放</h3>
|
||
<div class="api-description">
|
||
<p>播放接口: <code>GET /wvp/api/common/channel/playback</code></p>
|
||
<p>实际转发到: <code>http://114.67.89.4:9090/api/common/channel/playback</code></p>
|
||
</div>
|
||
|
||
<div class="selected-record-info">
|
||
<h4>当前播放的录像:</h4>
|
||
<p><strong>开始时间:</strong> {{ selectedRecord?.startTime }}</p>
|
||
<p><strong>结束时间:</strong> {{ selectedRecord?.endTime }}</p>
|
||
<p><strong>文件大小:</strong> {{ formatFileSize(selectedRecord?.fileSize) }}</p>
|
||
</div>
|
||
|
||
<div class="player-section">
|
||
<h4>视频播放器 (Jessibuca):</h4>
|
||
<div class="video-container">
|
||
<div
|
||
ref="playerContainerRef"
|
||
class="video-player"
|
||
style="width: 100%; height: 100%; background-color: #000; position: relative;"
|
||
></div>
|
||
</div>
|
||
|
||
<div class="player-info" v-if="isPlaying">
|
||
<span class="info-text">播放中</span>
|
||
<span class="info-text" style="margin-left: 20px;">码率: {{ kBps }} kb/s</span>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button @click="stopPlayComplete" :disabled="!isPlaying" class="stop-button">
|
||
停止播放
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="playError" class="error">
|
||
播放错误: {{ playError }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="play-urls-info">
|
||
<h4>播放地址:</h4>
|
||
<div class="url-list">
|
||
<div v-if="playbackData.ws_flv" class="url-item">
|
||
<strong>WebSocket-FLV:</strong>
|
||
<code>{{ playbackData.ws_flv }}</code>
|
||
<button @click="playJessibuca(playbackData.ws_flv)" class="small-button">播放</button>
|
||
</div>
|
||
<div v-if="playbackData.wss_flv" class="url-item">
|
||
<strong>WebSocket-FLV (SSL):</strong>
|
||
<code>{{ playbackData.wss_flv }}</code>
|
||
<button @click="playJessibuca(playbackData.wss_flv)" class="small-button">播放</button>
|
||
</div>
|
||
<div v-if="playbackData.flv" class="url-item">
|
||
<strong>HTTP-FLV:</strong>
|
||
<code>{{ playbackData.flv }}</code>
|
||
</div>
|
||
<div v-if="playbackData.hls" class="url-item">
|
||
<strong>HLS:</strong>
|
||
<code>{{ playbackData.hls }}</code>
|
||
</div>
|
||
<div v-if="playbackData.rtmp" class="url-item">
|
||
<strong>RTMP:</strong>
|
||
<code>{{ playbackData.rtmp }}</code>
|
||
</div>
|
||
<div v-if="playbackData.rtsp" class="url-item">
|
||
<strong>RTSP:</strong>
|
||
<code>{{ playbackData.rtsp }}</code>
|
||
</div>
|
||
<div v-if="playbackData.rtc" class="url-item">
|
||
<strong>WebRTC:</strong>
|
||
<code>{{ playbackData.rtc }}</code>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="info-box">
|
||
<h4>使用说明:</h4>
|
||
<p>1. 输入通道ID(数据库主键ID,不是国标设备ID)</p>
|
||
<p>2. 选择开始时间和结束时间,默认为今天全天</p>
|
||
<p>3. 点击"搜索"按钮查询录像列表</p>
|
||
<p>4. 在录像列表中点击"播放"按钮播放对应的录像</p>
|
||
<p>5. 录像会使用Jessibuca播放器自动播放WebSocket-FLV流</p>
|
||
<p><strong>注意:</strong> 时间格式为 YYYY-MM-DD HH:mm:ss,例如:2025-12-10 00:00:00</p>
|
||
<p><strong>播放器:</strong> 使用Jessibuca播放器,支持WebSocket-FLV协议,低延迟高性能</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.gb28181-record-container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
}
|
||
|
||
h2 {
|
||
margin-bottom: 30px;
|
||
color: #333;
|
||
}
|
||
|
||
.api-section {
|
||
background-color: #fff;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.api-section h3 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #2196f3;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.api-description {
|
||
background-color: #f5f5f5;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.api-description p {
|
||
margin: 5px 0;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.api-description code {
|
||
background-color: #e0e0e0;
|
||
padding: 2px 6px;
|
||
border-radius: 3px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 13px;
|
||
color: #d32f2f;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group input[type="text"],
|
||
.form-group input[type="number"] {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-group input:focus {
|
||
outline: none;
|
||
border-color: #2196f3;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 15px;
|
||
}
|
||
|
||
.button-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
background-color: #2196f3;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: background-color 0.3s ease;
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
background-color: #1976d2;
|
||
}
|
||
|
||
button:disabled {
|
||
background-color: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.secondary-button {
|
||
background-color: #757575;
|
||
}
|
||
|
||
.secondary-button:hover:not(:disabled) {
|
||
background-color: #616161;
|
||
}
|
||
|
||
.stop-button {
|
||
background-color: #f44336;
|
||
}
|
||
|
||
.stop-button:hover:not(:disabled) {
|
||
background-color: #d32f2f;
|
||
}
|
||
|
||
.play-button {
|
||
padding: 5px 15px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.small-button {
|
||
padding: 5px 10px;
|
||
font-size: 12px;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.error {
|
||
margin-top: 15px;
|
||
padding: 12px;
|
||
background-color: #ffebee;
|
||
color: #c62828;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.record-list {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.record-list h4 {
|
||
margin-bottom: 10px;
|
||
color: #333;
|
||
}
|
||
|
||
.record-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background-color: white;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.record-table thead {
|
||
background-color: #2196f3;
|
||
color: white;
|
||
}
|
||
|
||
.record-table th,
|
||
.record-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.record-table th {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.record-table td {
|
||
font-size: 13px;
|
||
color: #333;
|
||
}
|
||
|
||
.record-table tbody tr:hover {
|
||
background-color: #f5f5f5;
|
||
}
|
||
|
||
.record-table tbody tr.selected {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.record-table tbody tr:last-child td {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.selected-record-info {
|
||
background-color: #e3f2fd;
|
||
padding: 15px;
|
||
border-radius: 4px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.selected-record-info h4 {
|
||
margin-top: 0;
|
||
margin-bottom: 10px;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.selected-record-info p {
|
||
margin: 5px 0;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.player-section {
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.player-section h4 {
|
||
margin-bottom: 10px;
|
||
color: #333;
|
||
}
|
||
|
||
.video-container {
|
||
background-color: #000;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
min-height: 500px;
|
||
max-height: 600px;
|
||
display: block;
|
||
background-color: #000;
|
||
}
|
||
|
||
.player-info {
|
||
padding: 10px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 4px;
|
||
margin-top: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.info-text {
|
||
color: #2196f3;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.play-urls-info {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #f5f5f5;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.play-urls-info h4 {
|
||
margin-top: 0;
|
||
margin-bottom: 15px;
|
||
color: #333;
|
||
}
|
||
|
||
.url-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.url-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 10px;
|
||
background-color: white;
|
||
border-radius: 4px;
|
||
border: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.url-item strong {
|
||
min-width: 80px;
|
||
color: #2196f3;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.url-item code {
|
||
flex: 1;
|
||
background-color: #f5f5f5;
|
||
padding: 5px 10px;
|
||
border-radius: 3px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 12px;
|
||
color: #333;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.info-box {
|
||
margin-top: 20px;
|
||
padding: 15px;
|
||
background-color: #e3f2fd;
|
||
border-radius: 4px;
|
||
border-left: 4px solid #2196f3;
|
||
}
|
||
|
||
.info-box h4 {
|
||
margin-top: 0;
|
||
margin-bottom: 10px;
|
||
color: #1976d2;
|
||
}
|
||
|
||
.info-box p {
|
||
margin: 8px 0;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
</style>
|