添加GB2818的例子
This commit is contained in:
parent
500d5f688a
commit
6888d46ea2
10
src/App.vue
10
src/App.vue
|
|
@ -4,8 +4,9 @@ import WebSocketDemo from './components/WebSocketDemo.vue'
|
|||
import Zlm from './components/Zlm.vue'
|
||||
import Wvp from './components/Wvp.vue'
|
||||
import GB2818Live from './components/GB2818Live.vue'
|
||||
import GB2818Record from './components/GB2818Record.vue'
|
||||
|
||||
type TabType = 'websocket' | 'zlm' | 'wvp' | 'gb28181'
|
||||
type TabType = 'websocket' | 'zlm' | 'wvp' | 'gb28181' | 'gb28181-record'
|
||||
|
||||
const activeTab = ref<TabType>('websocket')
|
||||
|
||||
|
|
@ -41,6 +42,12 @@ const switchTab = (tab: TabType) => {
|
|||
>
|
||||
GB28181 Live
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: activeTab === 'gb28181-record' }"
|
||||
@click="switchTab('gb28181-record')"
|
||||
>
|
||||
GB28181 Record
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
|
|
@ -48,6 +55,7 @@ const switchTab = (tab: TabType) => {
|
|||
<Zlm v-if="activeTab === 'zlm'" />
|
||||
<Wvp v-if="activeTab === 'wvp'" />
|
||||
<GB2818Live v-if="activeTab === 'gb28181'" />
|
||||
<GB2818Record v-if="activeTab === 'gb28181-record'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,814 @@
|
|||
<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'
|
||||
}
|
||||
|
||||
// 组件卸载时清理播放器和停止流
|
||||
onUnmounted(async () => {
|
||||
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>
|
||||
Loading…
Reference in New Issue