前端开发攻略---Vue 3 实现视频小窗播放(画中画)效果的完整示例

实现思路

  1. 单视频元素 + 占位容器

    使用同一个 <video> 标签,通过 CSS 绝对定位覆盖在占位容器上。占位容器(aspect-ratiopadding-top)保证视频宽高比,避免布局抖动。

  2. Intersection Observer 检测可视性

    监听视频容器的可见性,当完全离开视口时触发小窗模式,进入视口时恢复。

  3. 小窗模式样式切换

    <video> 添加 fixed 定位,设置宽高、右下角偏移和阴影,同时占位容器保持原有尺寸,防止页面布局塌陷。

  4. 手动关闭与智能恢复

    提供关闭按钮,用户手动关闭后设置标志,只有当视频再次进入视口时才重置标志,保证手动行为优先。

完整组件代码

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>

关键点解析

  1. 占位容器
    .video-placeholder 通过 padding-top: 56.25% 维持 16:9 比例,确保视频容器在正常文档流中占据正确高度。当视频变为 fixed 时,占位容器依然存在,防止下方内容上移。

  2. 单视频元素

    使用同一个 <video> 元素,播放状态(暂停、进度、音量等)自然保持同步,无需额外逻辑。

  3. Intersection Observer

    • threshold: 0 表示只要有任意像素离开视口,即触发离开回调。

    • 进入视口时,不仅退出小窗,还重置 userDismissed 标记,使下次离开能再次自动小窗。

  4. 手动关闭逻辑

    用户点击关闭按钮后,设置 isMinimized = falseuserDismissed = true。此时即使视频仍在视口外,也不会自动进入小窗。只有当视频再次滚动进入视口(触发 isIntersecting),userDismissed 被重置,小窗功能恢复。

  5. 样式平滑
    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 属性对移动端友好。

相关推荐
lqj_本人1 小时前
基于 openYuanrong 的生成式推荐缓存高可用方向验证实践
前端·vue.js·缓存
We་ct2 小时前
React 中的双缓存 Fiber 树机制
前端·react.js·缓存·前端框架·reactjs·fiber·缓存机制
We་ct2 小时前
React Render 与 Commit 阶段详解
前端·react.js·面试·前端框架·react·commit·render
英俊潇洒美少年2 小时前
React Hook 钩子 useInsertionEffect、useLayoutEffect、useEffect的区别
前端·javascript·react.js
坚持学习前端日记2 小时前
Agent AI 后端接口对接与大模型适配指南
前端·人工智能·python·ios
坚持学习前端日记2 小时前
Agent AI 多模态交互与全场景架构设计
前端·javascript·人工智能·visual studio
王家视频教程图书馆2 小时前
vue3移动端组件库清单
前端
毕设源码-郭学长2 小时前
【开题答辩全过程】以 基于web的车辆检测管理系统的设计与实现为例,包含答辩的问题和答案
前端
向上的车轮2 小时前
TypeScript 一日速通指南:以订单管理系统实战为核心
前端·javascript·typescript