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

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

View File

@@ -18,53 +18,61 @@
</select> </select>
</div> </div>
<p class="live-watch-status">{{ watchStatus }}</p> <p class="live-watch-status">{{ watchStatus }}</p>
<div class="live-video-wrap"> <div ref="liveStageRef" class="live-stage">
<video <div class="live-stage-media">
ref="watchVideoRef" <div class="live-video-wrap">
class="live-room-video live-room-video--watch live-room-video--contain" <video
playsinline ref="watchVideoRef"
autoplay class="live-room-video live-room-video--watch live-room-video--contain"
></video> playsinline
<div class="live-dm-layer" aria-hidden="true"> autoplay
<div ></video>
v-for="d in dmItems" <div class="live-dm-layer" aria-hidden="true">
:key="d.id" <div
class="live-dm-line" v-for="d in dmItems"
:style="{ top: d.top + '%' }" :key="d.id"
> class="live-dm-line"
<span class="live-dm-from">{{ d.from }}</span>{{ d.text }} :style="{ top: d.top + '%' }"
>
<span class="live-dm-from">{{ d.from }}</span>{{ d.text }}
</div>
</div>
<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="toggleStageFullscreen">
{{ stageFullscreen ? '退出全屏' : '全屏' }}
</button>
</div>
</div> </div>
</div> </div>
<div class="live-video-toolbar" role="toolbar" aria-label="播放控制"> <div class="live-stage-footer">
<button type="button" class="live-video-toolbtn" @click="unmuteAndPlay">开声音</button> <div class="live-dm-auth-row">
<button type="button" class="live-video-toolbtn" @click="toggleVideoFullscreen">全屏</button> <template v-if="dmLoggedIn">
<span class="live-dm-user">已登录{{ dmDisplayName }}</span>
<button type="button" class="live-dm-auth-btn" @click="logoutDmUser">退出</button>
</template>
<template v-else>
<router-link class="live-dm-auth-link" to="/live/login">登录发弹幕</router-link>
<span class="live-dm-auth-sep">·</span>
<router-link class="live-dm-auth-link" to="/live/register">注册</router-link>
</template>
</div>
<div class="live-dm-bar">
<input
v-model="dmDraft"
class="live-dm-input"
type="text"
maxlength="120"
:placeholder="dmLoggedIn ? '发条弹幕…' : '登录后可发弹幕(仍可观看)'"
autocomplete="off"
:disabled="!dmLoggedIn"
@keydown.enter.prevent="sendDm"
/>
<button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button>
</div>
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
</div> </div>
</div> </div>
<div class="live-dm-auth-row">
<template v-if="dmLoggedIn">
<span class="live-dm-user">已登录{{ dmDisplayName }}</span>
<button type="button" class="live-dm-auth-btn" @click="logoutDmUser">退出</button>
</template>
<template v-else>
<router-link class="live-dm-auth-link" to="/live/login">登录发弹幕</router-link>
<span class="live-dm-auth-sep">·</span>
<router-link class="live-dm-auth-link" to="/live/register">注册</router-link>
</template>
</div>
<div class="live-dm-bar">
<input
v-model="dmDraft"
class="live-dm-input"
type="text"
maxlength="120"
:placeholder="dmLoggedIn ? '发条弹幕…' : '登录后可发弹幕(仍可观看)'"
autocomplete="off"
:disabled="!dmLoggedIn"
@keydown.enter.prevent="sendDm"
/>
<button type="button" class="live-dm-send" :disabled="!dmLoggedIn" @click="sendDm">发送</button>
</div>
<p v-if="dmHint" class="live-dm-hint">{{ dmHint }}</p>
</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') {
v.webkitEnterFullscreen() 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()
}
})
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;