实现思路
-
单视频元素 + 占位容器
使用同一个
<video>标签,通过 CSS 绝对定位覆盖在占位容器上。占位容器(aspect-ratio或padding-top)保证视频宽高比,避免布局抖动。 -
Intersection Observer 检测可视性
监听视频容器的可见性,当完全离开视口时触发小窗模式,进入视口时恢复。
-
小窗模式样式切换
为
<video>添加fixed定位,设置宽高、右下角偏移和阴影,同时占位容器保持原有尺寸,防止页面布局塌陷。 -
手动关闭与智能恢复
提供关闭按钮,用户手动关闭后设置标志,只有当视频再次进入视口时才重置标志,保证手动行为优先。
完整组件代码
javascript
<!-- components/FloatingVideoPlayer.vue -->
<template>
<div ref="videoContainer" class="video-container">
<!-- 占位元素,保证原位置高度不变 -->
<div class="video-placeholder"></div>
<!-- 视频元素:正常状态覆盖占位,小窗时脱离文档流悬浮 -->
<video
ref="video"
class="video-player"
:class="{ 'video-player--minimized': isMinimized }"
:src="src"
:poster="poster"
controls
preload="auto"
playsinline
></video>
<!-- 小窗模式下的关闭按钮(仅在小窗时显示) -->
<button
v-if="isMinimized"
class="video-minimize-close"
@click="handleCloseMinimize"
title="恢复原位"
>
✕
</button>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const props = defineProps({
src: { type: String, required: true },
poster: { type: String, default: '' }
})
// DOM 元素引用
const videoContainer = ref(null)
const video = ref(null)
// 状态
const isMinimized = ref(false) // 是否处于小窗模式
const userDismissed = ref(false) // 用户是否手动关闭了小窗
let observer = null
// 关闭小窗(用户手动操作)
const handleCloseMinimize = () => {
isMinimized.value = false
userDismissed.value = true
}
// 重置用户标记(当视频进入视口时调用)
const resetUserDismissed = () => {
userDismissed.value = false
}
// 初始化 Intersection Observer
const initObserver = () => {
if (!videoContainer.value) return
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 视频进入视口:退出小窗,重置用户标记
isMinimized.value = false
resetUserDismissed()
} else {
// 视频离开视口:若用户未手动关闭,则进入小窗
if (!userDismissed.value) {
isMinimized.value = true
}
}
})
},
{
threshold: 0, // 只要有一像素不可见即触发离开
}
)
observer.observe(videoContainer.value)
}
onMounted(() => {
initObserver()
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
</script>
<style scoped>
/* 容器相对定位,作为视频绝对定位的参考 */
.video-container {
position: relative;
width: 100%;
background-color: #000;
border-radius: 8px;
overflow: hidden;
}
/* 占位元素:使用 padding-top 维持 16:9 宽高比 */
.video-placeholder {
width: 100%;
padding-top: 56.25%; /* 16:9 比例 */
}
/* 视频元素默认绝对定位覆盖占位 */
.video-player {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
background: #000;
transition: all 0.2s ease; /* 平滑切换效果 */
cursor: pointer;
}
/* 小窗模式:固定定位到右下角,缩小尺寸 */
.video-player--minimized {
position: fixed;
bottom: 20px;
right: 20px;
width: 320px;
height: 180px; /* 与占位比例一致 */
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
z-index: 1000;
object-fit: contain; /* 避免裁剪,显示完整画面 */
}
/* 小窗关闭按钮 */
.video-minimize-close {
position: fixed;
bottom: 190px; /* 位于小窗右上角附近,简单定位 */
right: 20px;
width: 30px;
height: 30px;
background-color: rgba(0, 0, 0, 0.6);
color: white;
border: none;
border-radius: 50%;
font-size: 18px;
line-height: 1;
cursor: pointer;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.video-minimize-close:hover {
background-color: rgba(0, 0, 0, 0.9);
}
</style>
使用示例
在父组件中引入并使用:
javascript
<template>
<div class="page">
<h1>仿 B 站小窗视频播放</h1>
<p>向下滚动测试视频小窗效果...</p>
<div style="height: 600px;"></div> <!-- 模拟滚动空间 -->
<FloatingVideoPlayer
src="https://example.com/video.mp4"
poster="https://example.com/poster.jpg"
/>
<div style="height: 800px;"></div>
</div>
</template>
<script setup>
import FloatingVideoPlayer from './components/FloatingVideoPlayer.vue'
</script>
关键点解析
-
占位容器
.video-placeholder通过padding-top: 56.25%维持 16:9 比例,确保视频容器在正常文档流中占据正确高度。当视频变为fixed时,占位容器依然存在,防止下方内容上移。 -
单视频元素
使用同一个
<video>元素,播放状态(暂停、进度、音量等)自然保持同步,无需额外逻辑。 -
Intersection Observer
-
threshold: 0表示只要有任意像素离开视口,即触发离开回调。 -
进入视口时,不仅退出小窗,还重置
userDismissed标记,使下次离开能再次自动小窗。
-
-
手动关闭逻辑
用户点击关闭按钮后,设置
isMinimized = false且userDismissed = true。此时即使视频仍在视口外,也不会自动进入小窗。只有当视频再次滚动进入视口(触发isIntersecting),userDismissed被重置,小窗功能恢复。 -
样式平滑
transition: all 0.2s ease让位置和大小变化更自然。小窗时使用object-fit: contain保证视频内容完整显示(避免裁剪黑边)。
扩展与优化建议
-
可拖拽小窗
如需实现类似 B 站的拖拽功能,可使用 Vue 自定义指令或第三方库(如
vue-draggable-plus)实现。为.video-player--minimized添加draggable属性并处理拖拽事件。 -
多个视频共存
若页面有多个视频,需为每个视频创建独立的观察者实例,并确保
videoContainer引用正确。 -
性能优化
在
onUnmounted中断开观察者,避免内存泄漏。 -
自定义小窗尺寸与位置
可通过
props传入小窗宽度、偏移量等,增强组件灵活性。 -
播放状态同步
当前方案单视频元素自动同步,无需额外处理。
注意事项
-
小窗关闭按钮的位置在小窗右上角,本例中简单计算了坐标(基于小窗宽高)。更稳健的做法是使用绝对定位相对于小窗,而非固定定位,可将关闭按钮放入小窗内部,或使用
position: absolute相对于.video-player--minimized。 -
如果视频容器原本具有
border-radius,小窗时仍需保留圆角。 -
视频预加载和
playsinline属性对移动端友好。