直播页:全屏包含底部弹幕区,可登录与发送

Made-with: Cursor
This commit is contained in:
whm
2026-03-26 15:07:47 +08:00
parent 07ae6c02ef
commit d441fe33fd

View File

@@ -18,6 +18,8 @@
</select> </select>
</div> </div>
<p class="live-watch-status">{{ watchStatus }}</p> <p class="live-watch-status">{{ watchStatus }}</p>
<div ref="liveStageRef" class="live-stage">
<div class="live-stage-media">
<div class="live-video-wrap"> <div class="live-video-wrap">
<video <video
ref="watchVideoRef" ref="watchVideoRef"
@@ -37,9 +39,13 @@
</div> </div>
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制"> <div class="live-video-toolbar" role="toolbar" aria-label="播放控制">
<button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button> <button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button>
<button type="button" class="live-video-toolbtn" @click="toggleVideoFullscreen">全屏</button> <button type="button" class="live-video-toolbtn" @click="toggleStageFullscreen">
{{ stageFullscreen ? '退出全屏' : '全屏' }}
</button>
</div> </div>
</div> </div>
</div>
<div class="live-stage-footer">
<div class="live-dm-auth-row"> <div class="live-dm-auth-row">
<template v-if="dmLoggedIn"> <template v-if="dmLoggedIn">
<span class="live-dm-user">已登录{{ dmDisplayName }}</span> <span class="live-dm-user">已登录{{ dmDisplayName }}</span>
@@ -65,6 +71,8 @@
<button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button> <button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button>
</div> </div>
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p> <p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
</div>
</div>
</section> </section>
<section class="live-block live-block--divider" aria-label="外链直播间"> <section class="live-block live-block--divider" aria-label="外链直播间">
@@ -92,6 +100,8 @@ import { startViewing, liveDanmakuWsURL } from '../utils/liveWebRTC'
import { getSiteDmToken, getSiteDmUsername, clearSiteDmSession } from '../utils/siteUserAuth' import { getSiteDmToken, getSiteDmUsername, clearSiteDmSession } from '../utils/siteUserAuth'
const watchVideoRef = ref(null) const watchVideoRef = ref(null)
const liveStageRef = ref(null)
const stageFullscreen = ref(false)
const rawLiveUrl = ref('') const rawLiveUrl = ref('')
const pageTitle = ref('视频直播') const pageTitle = ref('视频直播')
const watchStatus = ref('正在检测本站直播…') const watchStatus = ref('正在检测本站直播…')
@@ -177,20 +187,55 @@ function unmuteAndPlay() {
v.play().catch(() => {}) v.play().catch(() => {})
} }
function toggleVideoFullscreen() { function syncStageFullscreenFlag() {
const d = document
const el = liveStageRef.value
if (!el) {
stageFullscreen.value = false
return
}
stageFullscreen.value =
d.fullscreenElement === el || d.webkitFullscreenElement === el
}
/** 整页舞台全屏:含画面 + 底部登录与发弹幕(非仅 video避免全屏后看不到输入框 */
function toggleStageFullscreen() {
const stage = liveStageRef.value
const v = watchVideoRef.value const v = watchVideoRef.value
if (!v) return if (!stage) return
const doc = document const doc = document
if (doc.fullscreenElement || doc.webkitFullscreenElement) { const fsEl = doc.fullscreenElement || doc.webkitFullscreenElement
if (fsEl) {
doc.exitFullscreen?.() || doc.webkitExitFullscreen?.() doc.exitFullscreen?.() || doc.webkitExitFullscreen?.()
return return
} }
if (typeof v.webkitEnterFullscreen === 'function') { if (typeof stage.requestFullscreen === 'function') {
Promise.resolve(stage.requestFullscreen())
.then(() => syncStageFullscreenFlag())
.catch(() => {
if (typeof stage.webkitRequestFullscreen === 'function') {
try {
stage.webkitRequestFullscreen()
} catch (_) {
if (v && typeof v.webkitEnterFullscreen === 'function') v.webkitEnterFullscreen()
}
} else if (v && typeof v.webkitEnterFullscreen === 'function') {
v.webkitEnterFullscreen() v.webkitEnterFullscreen()
}
})
return return
} }
const el = v if (typeof stage.webkitRequestFullscreen === 'function') {
el.requestFullscreen?.() || el.webkitRequestFullscreen?.() try {
stage.webkitRequestFullscreen()
} catch (_) {
if (v && typeof v.webkitEnterFullscreen === 'function') v.webkitEnterFullscreen()
}
return
}
if (v && typeof v.webkitEnterFullscreen === 'function') {
v.webkitEnterFullscreen()
}
} }
function pushDmLine(text, fromRaw) { function pushDmLine(text, fromRaw) {
@@ -327,6 +372,8 @@ function sendDm() {
onMounted(async () => { onMounted(async () => {
dmIntentionalClose = false dmIntentionalClose = false
document.addEventListener('fullscreenchange', syncStageFullscreenFlag)
document.addEventListener('webkitfullscreenchange', syncStageFullscreenFlag)
loadCaptureQualityPref() loadCaptureQualityPref()
loadHomepage() loadHomepage()
await nextTick() await nextTick()
@@ -340,6 +387,8 @@ onMounted(async () => {
}) })
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('fullscreenchange', syncStageFullscreenFlag)
document.removeEventListener('webkitfullscreenchange', syncStageFullscreenFlag)
dmIntentionalClose = true dmIntentionalClose = true
dmSendQueue.length = 0 dmSendQueue.length = 0
if (dmReconnectTimer) { if (dmReconnectTimer) {
@@ -432,11 +481,72 @@ onUnmounted(() => {
margin: 0 0 14px; margin: 0 0 14px;
min-height: 1.4em; min-height: 1.4em;
} }
.live-stage {
max-width: 480px;
margin: 0 auto;
}
.live-video-wrap { .live-video-wrap {
position: relative; position: relative;
max-width: 480px; max-width: 480px;
margin: 0 auto; margin: 0 auto;
} }
.live-stage:fullscreen,
.live-stage:-webkit-full-screen {
max-width: none;
width: 100%;
height: 100%;
margin: 0;
padding: 10px 14px calc(12px + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
background: #0a0a12;
display: flex;
flex-direction: column;
overflow: hidden;
}
.live-stage:fullscreen .live-stage-media,
.live-stage:-webkit-full-screen .live-stage-media {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.live-stage:fullscreen .live-video-wrap,
.live-stage:-webkit-full-screen .live-video-wrap {
flex: 1;
min-height: 0;
max-width: none;
width: 100%;
margin: 0;
display: flex;
flex-direction: column;
}
.live-stage:fullscreen .live-room-video.live-room-video--contain,
.live-stage:-webkit-full-screen .live-room-video.live-room-video--contain {
flex: 1;
min-height: 0;
width: 100%;
max-height: none;
aspect-ratio: unset;
object-fit: contain;
}
.live-stage:fullscreen .live-video-toolbar,
.live-stage:-webkit-full-screen .live-video-toolbar {
flex-shrink: 0;
margin-top: 8px;
}
.live-stage:fullscreen .live-stage-footer,
.live-stage:-webkit-full-screen .live-stage-footer {
flex-shrink: 0;
padding-top: 6px;
}
.live-stage:fullscreen .live-dm-auth-row,
.live-stage:fullscreen .live-dm-bar,
.live-stage:fullscreen .live-dm-hint,
.live-stage:-webkit-full-screen .live-dm-auth-row,
.live-stage:-webkit-full-screen .live-dm-bar,
.live-stage:-webkit-full-screen .live-dm-hint {
max-width: none;
}
.live-dm-layer { .live-dm-layer {
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;