完善节目管理与回放播放体验
This commit is contained in:
1 parent
b265518084
commit
a493ef2309
14 files changed
+446
-96
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Select, Button, Typography, Space, Tag, Spin, Empty, message } from 'antd'
|
||||
import { Select, Button, Typography, Tag, Spin, Empty, message } from 'antd'
|
||||
import { ArrowLeftOutlined, WifiOutlined, SignalFilled } from '@ant-design/icons'
|
||||
import Hls from 'hls.js'
|
||||
import { getWatchUrls, type WatchInfo, type LineInfo } from '../../api/streams'
|
||||
@@ -52,7 +52,14 @@ export default function WatchPage() {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!info || !activeLine || !videoRef.current) return
|
||||
if (!info || !videoRef.current) return
|
||||
if (info.replay_url && info.status !== 'live') {
|
||||
hlsRef.current?.destroy()
|
||||
hlsRef.current = null
|
||||
videoRef.current.src = info.replay_url
|
||||
return
|
||||
}
|
||||
if (!activeLine) return
|
||||
const line = info.lines.find(l => l.node_id === activeLine)
|
||||
if (!line) return
|
||||
const quality = activeQuality ? line.qualities.find(q => q.label === activeQuality) : line.qualities[0]
|
||||
@@ -74,6 +81,12 @@ export default function WatchPage() {
|
||||
}
|
||||
}, [activeLine, activeQuality, info])
|
||||
|
||||
function jumpToChapter(time: number) {
|
||||
if (!videoRef.current) return
|
||||
videoRef.current.currentTime = time
|
||||
videoRef.current.play().catch(() => {})
|
||||
}
|
||||
|
||||
useEffect(() => { document.title = info ? `${info.title} - ${brand.brand_name}` : brand.brand_name }, [info, brand])
|
||||
|
||||
if (loading) return (
|
||||
@@ -91,53 +104,59 @@ export default function WatchPage() {
|
||||
|
||||
return (
|
||||
<div className="watch-shell">
|
||||
{/* Top bar */}
|
||||
<header className="watch-header">
|
||||
<Space size={16}>
|
||||
<div className="watch-heading">
|
||||
<Button type="text" icon={<ArrowLeftOutlined />} style={{ color: '#949088' }}
|
||||
onClick={() => navigate('/')}>{brand.brand_name}</Button>
|
||||
<div style={{ width: 1, height: 18, background: 'rgba(235,229,219,0.1)' }} />
|
||||
<Text style={{ color: '#EBE5DB', fontSize: 14, fontWeight: 500 }}>{info.title}</Text>
|
||||
<span className="watch-divider" />
|
||||
<Text className="watch-title">{info.title}</Text>
|
||||
<Tag color={info.status === 'live' ? 'green' : 'default'} style={{ fontWeight: 600, fontSize: 11 }}>
|
||||
{statusLabel}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Space size={16}>
|
||||
</div>
|
||||
<div className="watch-options">
|
||||
<Tag style={{ margin: 0 }}>节点:{currentLine?.node_name || info.selected_node?.name || '不可用'}</Tag>
|
||||
<Tag style={{ margin: 0 }}>清晰度:{activeQuality || '不可用'}</Tag>
|
||||
<Text style={{ color: '#949088', fontSize: 13, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<SignalFilled style={{ color: info.multicast_available ? '#5B8C5A' : '#5E5A52', fontSize: 12 }} />
|
||||
{info.multicast_available ? '组播' : '单播'}
|
||||
</Text>
|
||||
<Select value={activeQuality} onChange={setActiveQuality} style={{ width: 100 }} size="small"
|
||||
<Select value={activeQuality || undefined} onChange={setActiveQuality} className="watch-quality" size="small"
|
||||
options={info.qualities.map(q => ({ value: q.label, label: q.label }))} placeholder="清晰度" />
|
||||
<Select value={activeLine} onChange={setActiveLine} style={{ width: 150 }} size="small"
|
||||
<Select value={activeLine || undefined} onChange={setActiveLine} className="watch-line" size="small"
|
||||
options={info.lines.map(l => ({ value: l.node_id, label: l.node_name }))}
|
||||
prefix={<WifiOutlined />} placeholder="线路" />
|
||||
</Space>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Video */}
|
||||
<main className="watch-stage">
|
||||
<div className="watch-frame">
|
||||
<video ref={videoRef} controls autoPlay playsInline
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />
|
||||
{info.status !== 'live' && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(10,10,8,0.92)',
|
||||
}}>
|
||||
<Space direction="vertical" align="center" size={16}>
|
||||
<div className="watch-content">
|
||||
<div className="watch-frame">
|
||||
<video ref={videoRef} controls autoPlay playsInline className="watch-video" />
|
||||
{info.status !== 'live' && !info.replay_url && (
|
||||
<div className="watch-waiting">
|
||||
<Spin size="large" />
|
||||
<Text style={{
|
||||
color: '#949088', fontSize: 17,
|
||||
fontFamily: '"Source Serif 4", "Noto Serif SC", Georgia, serif',
|
||||
}}>
|
||||
等待主播开播...
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
<Text>等待主播开播...</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(info.description || info.chapters.length > 0) && (
|
||||
<aside className="watch-details">
|
||||
{info.description && <p>{info.description}</p>}
|
||||
{info.chapters.length > 0 && (
|
||||
<section>
|
||||
<h2>节目章节</h2>
|
||||
<div className="chapter-list">
|
||||
{info.chapters.map((chapter, index) => (
|
||||
<button key={`${chapter.time}-${index}`} type="button" onClick={() => jumpToChapter(chapter.time)}>
|
||||
<span>{chapter.title}</span>
|
||||
<time>{Math.floor(chapter.time / 60)}:{String(chapter.time % 60).padStart(2, '0')}</time>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</aside>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -219,6 +219,28 @@ button.stream-card:focus-visible {
|
||||
background: #1d1a16;
|
||||
}
|
||||
|
||||
.watch-heading,
|
||||
.watch-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.watch-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: rgba(235, 229, 219, 0.1);
|
||||
}
|
||||
|
||||
.watch-title {
|
||||
color: #ebe5db;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.watch-quality { width: 100px; }
|
||||
.watch-line { width: 150px; }
|
||||
|
||||
.watch-stage {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -227,6 +249,11 @@ button.stream-card:focus-visible {
|
||||
padding: 36px;
|
||||
}
|
||||
|
||||
.watch-content {
|
||||
width: 100%;
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.watch-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -239,6 +266,52 @@ button.stream-card:focus-visible {
|
||||
background: #080706;
|
||||
}
|
||||
|
||||
.watch-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.watch-waiting {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
background: rgba(10, 10, 8, 0.92);
|
||||
}
|
||||
|
||||
.watch-waiting .ant-typography { color: #949088; font-size: 17px; }
|
||||
|
||||
.watch-details {
|
||||
margin-top: 24px;
|
||||
padding: 24px;
|
||||
border: 1px solid #3d3730;
|
||||
border-radius: 8px;
|
||||
background: #1d1a16;
|
||||
}
|
||||
|
||||
.watch-details p { margin: 0 0 20px; color: #b7aea2; line-height: 1.7; }
|
||||
.watch-details h2 { margin: 0 0 12px; font-size: 16px; }
|
||||
.chapter-list { display: grid; gap: 8px; }
|
||||
.chapter-list button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #3d3730;
|
||||
border-radius: 6px;
|
||||
background: #24201b;
|
||||
color: #eee7dc;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
.chapter-list button:hover { border-color: #806d56; background: #2b261f; }
|
||||
.chapter-list time { color: #949088; font-variant-numeric: tabular-nums; }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.watch-header {
|
||||
align-items: flex-start;
|
||||
@@ -246,6 +319,27 @@ button.stream-card:focus-visible {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.watch-heading,
|
||||
.watch-options {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.watch-title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.watch-options .ant-tag,
|
||||
.watch-options .ant-typography { display: none; }
|
||||
|
||||
.watch-quality,
|
||||
.watch-line { flex: 1; width: auto; min-width: 120px; }
|
||||
|
||||
.watch-stage {
|
||||
align-items: flex-start;
|
||||
padding: 20px 0;
|
||||
@@ -256,6 +350,11 @@ button.stream-card:focus-visible {
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.watch-details {
|
||||
margin: 16px;
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
Reference in new issue
Block a user