Vue Reel Player
面向 Vue 3 的全屏 Instagram / TikTok 风格竖向媒体播放器,基于 @reelkit/vue-reel-player.
特性
安装
npm install @reelkit/vue-reel-player @reelkit/vue lucide-vue-next在应用入口(或任意组件)里引入一次样式表:
import '@reelkit/vue-reel-player/styles.css';图标
默认控件使用 lucide-vue-next 作为图标(关闭、 声音、导航箭头)。如果你想换一套图标库,可以用 #controls 和 #navigation 作用域插槽提供自己的实现。
Vue 3.3 或更高版本
这些覆盖层是泛型组件,Vue 从 3.3 起才支持。在更旧的 Vue 上,运行时会丢掉它们的 props 和事件,所以本包把 vue 3.3 或更高版本列为 peer 依赖。
快速上手
渲染一个缩略图网格,点击后在对应索引打开浮层。绑定 v-model:is-open 意味着当用户通过按钮、手势或 Escape 关闭播放器时,父级的 ref 会保持同步。
<script setup lang="ts">
import { ref } from 'vue';
import { ReelPlayerOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import '@reelkit/vue-reel-player/styles.css';
const content: ContentItem[] = [
{
id: '1',
media: [{
id: 'v1',
type: 'video',
src: '/cdn/samples/videos/video-01.mp4',
poster: '/cdn/samples/videos/video-poster-01.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Alex Johnson', avatar: '/cdn/samples/avatars/avatar-01.jpg' },
likes: 1234,
description: 'Amazing sunset vibes',
},
{
id: '2',
media: [{
id: 'img1',
type: 'image',
src: '/cdn/samples/images/image-01.jpg',
aspectRatio: 2 / 3,
}],
author: { name: 'Sarah Miller', avatar: '/cdn/samples/avatars/avatar-02.jpg' },
likes: 5678,
description: 'Nature at its finest',
},
{
id: '3',
media: [{
id: 'v2',
type: 'video',
src: '/cdn/samples/videos/video-02.mp4',
poster: '/cdn/samples/videos/video-poster-02.jpg',
aspectRatio: 16 / 9,
}],
author: { name: 'Mike Chen', avatar: '/cdn/samples/avatars/avatar-03.jpg' },
likes: 3456,
description: 'Adventure awaits',
},
];
const isOpen = ref(false);
const startIndex = ref(0);
function openAt(i: number) {
startIndex.value = i;
isOpen.value = true;
}
</script>
<template>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:6px">
<button
v-for="(item, i) in content"
:key="item.id"
@click="openAt(i)"
style="aspect-ratio:9/16;cursor:pointer;overflow:hidden;padding:0;border:0"
>
<img
:src="item.media[0].poster || item.media[0].src"
style="width:100%;height:100%;object-fit:cover"
/>
</button>
</div>
<ReelPlayerOverlay
v-model:is-open="isOpen"
:content="content"
:initial-index="startIndex"
/>
</template>作用域插槽
八个作用域插槽让你替换播放器界面的任意部分。每个插槽都会收到带完整类型的作用域对象。没传的插槽会回退到默认实现。
| 插槽 | 作用域 | 说明 |
|---|---|---|
| #controls | { item, soundState, activeIndex, content, onClose } | 自定义全局控件栏(关闭、 声音、分享等) |
| #error | { item, activeIndex, innerActiveIndex } | 自定义错误提示(替换默认图标) |
| #loading | { item, activeIndex, innerActiveIndex } | 自定义加载提示(替换默认的波浪加载动画) |
| #navigation | { item, activeIndex, count, onPrev, onNext } | 自定义上一张 / 下一张导航箭头(桌面端) |
| #nestedNavigation | { media, activeIndex, count, onPrev, onNext } | 内层横向滑动器的自定义箭头 |
| #nestedSlide | { item, media, index, size, isActive, isInnerActive, slideKey, defaultContent, onReady, onWaiting, onError } | 内层横向滑动器里的自定义幻灯片内容 |
| #slide | { item, index, size, isActive, slideKey, defaultContent, onReady, onWaiting, onError } | 完全自定义的幻灯片内容(省略则回退到默认) |
| #slideOverlay | { item, index, isActive } | 逐张幻灯片的浮层(作者信息、点赞、描述等) |
| #timeline | { item, activeIndex, timelineState, defaultContent } | 自定义播放时间轴条。只有在内置门控(timeline 模式 + 最小时长)会渲染默认条时才会调用,复用同样的 auto/always/never 逻辑。用 defaultContent() 包裹内置的 <TimelineBar />。 |
<script setup lang="ts">
import { ref } from 'vue';
import {
ReelPlayerOverlay,
CloseButton,
SoundButton,
type ContentItem,
type SlideOverlaySlotScope,
type ControlsSlotScope,
} from '@reelkit/vue-reel-player';
const isOpen = ref(false);
const content = ref<ContentItem[]>([/* ... */]);
</script>
<template>
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<!-- Custom per-slide overlay: branded caption -->
<template #slideOverlay="{ item, isActive }: SlideOverlaySlotScope">
<div v-if="isActive" style="position:absolute;bottom:80px;left:16px;color:#fff">
<div style="display:flex;align-items:center;gap:8px">
<img :src="item.author.avatar" style="width:40px;height:40px;border-radius:50%" />
<span style="font-weight:600">{{ item.author.name }}</span>
</div>
<p style="margin-top:8px">{{ item.description }}</p>
</div>
</template>
<!-- Custom global controls -->
<template #controls="{ onClose }: ControlsSlotScope">
<div style="position:absolute;top:16px;right:16px;display:flex;gap:8px">
<SoundButton />
<CloseButton :on-click="onClose" />
</div>
</template>
<!-- Custom playback timeline -->
<template #timeline="{ timelineState }: TimelineSlotScope">
<CustomTimelineBar :state="timelineState" />
</template>
</ReelPlayerOverlay>
</template>自定义时间轴
用你自己的拖动界面替换内置的播放条,方式是 #timeline 插槽。只有在浮层的门控规则会渲染默认条时插槽才会触发(同样是 timeline 模式 + timelineMinDurationSeconds),所以不必自己重写一遍。在你的根元素上复用 .rk-reel-timeline 类,即可继承贴底定位、安全区内边距和触摸设备上的留白。
<script setup lang="ts">
import { shallowRef, onMounted, onBeforeUnmount } from 'vue';
import {
ReelPlayerOverlay,
type TimelineSlotScope,
} from '@reelkit/vue-reel-player';
import { toVueRef, type TimelineController } from '@reelkit/vue';
const trackRef = shallowRef<HTMLDivElement | null>(null);
let dispose: (() => void) | null = null;
const bind = (state: TimelineController) => {
if (trackRef.value) dispose = state.bindInteractions(trackRef.value);
};
onBeforeUnmount(() => dispose?.());
</script>
<template>
<ReelPlayerOverlay :is-open="open" :content="items" timeline="always">
<template #timeline="{ timelineState }: TimelineSlotScope">
<div class="rk-reel-timeline" style="padding: 0 16px" @vue:mounted="bind(timelineState)">
<div ref="trackRef" role="slider" style="height:6px;background:rgba(255,255,255,0.2)">
<div :style="{
width: (timelineState.progress.value * 100) + '%',
height: '100%',
background: 'linear-gradient(90deg, #6366f1, #ec4899)',
}" />
</div>
</div>
</template>
</ReelPlayerOverlay>
</template>自定义内容类型
ReelPlayerOverlay 和 ReelPlayerUrlOverlay 对内容条目的形状是泛型的,与 React 播放器一致。扩展 BaseContentItem 即可使用任意数据模型:条目类型从 content 推断,所以不符合 BaseContentItem 的内容列表无法通过类型检查。在 Vue 中插槽无法携带这个类型,插槽作用域在你标注之前没有类型。引入对应的插槽作用域类型并标注解构,额外字段在模板中就能保持类型。这个标注不会与 content 核对,所以请让两者保持一致:
<script setup lang="ts">
import { ref } from 'vue';
import {
ReelPlayerOverlay,
type BaseContentItem,
type SlideOverlaySlotScope,
} from '@reelkit/vue-reel-player';
interface MyItem extends BaseContentItem {
title: string;
category: 'video' | 'photo';
}
const open = ref(false);
const items: MyItem[] = [/* ... */];
</script>
<template>
<ReelPlayerOverlay v-model:is-open="open" :content="items">
<template #slideOverlay="{ item }: SlideOverlaySlotScope<MyItem>">
<div class="my-overlay">
<h2>{{ item.title }}</h2>
<span>{{ item.category }}</span>
</div>
</template>
</ReelPlayerOverlay>
</template>其余每个插槽都是同样的写法。引入对应的作用域类型(SlideSlotScope, ControlsSlotScope, NavigationSlotScope, NestedSlideSlotScope, LoadingSlotScope)并给解构加上类型标注即可。
URL 状态
查看在线演示 →用 useOverlayUrlState from @reelkit/vue 构建控制器,再把它交给 ReelPlayerUrlOverlay 作为 controller:地址栏拥有播放器,参数指向某张幻灯片时它就打开,参数消失时就关闭。打开会压入一条历史记录,之后每次切换都是替换,因此翻信息流不会多出记录,退一步永远就是离开。URL 的深度取决于控制器用的 key:单轴的 urlIndexKey 只寻址帖子本身(?reel=3),双轴的 urlIndexTwoAxisKey 还会携带多媒体帖子的内层媒体索引(?reel=3.2);每个应用只选一种 key,两种形态不会互相解码。它是一个独立于 ReelPlayerOverlay的组件,因此每个组件都只有一个打开状态的驱动源 —— 要么是 is-open 模型,要么是 URL controller,绝不会同时用两个。
带路由的应用应当传入基于路由器的适配器,让路由器始终是导航的唯一真相来源 —— 绕过它直接写历史会让 location 过期,下一次导航就会把参数丢掉。 useVueRouterUrlAdapter from @reelkit/vue/vue-router-url-adapter 就是为 Vue Router 准备的现成适配器。
<script setup lang="ts">
import { ReelPlayerUrlOverlay, type ContentItem } from '@reelkit/vue-reel-player';
import { useOverlayUrlState, urlIndexKey, urlStableIdKey } from '@reelkit/vue';
import { useVueRouterUrlAdapter } from '@reelkit/vue/vue-router-url-adapter';
import '@reelkit/vue-reel-player/styles.css';
const props = defineProps<{ content: ContentItem[] }>();
const reel = useOverlayUrlState({
param: 'reel',
adapter: useVueRouterUrlAdapter(),
...urlIndexKey(() => props.content.length),
});
</script>
<template>
<!-- Opening is a link — the overlay reads the URL and opens itself. -->
<RouterLink v-for="(post, i) in props.content" :key="post.id" :to="`?reel=${i}`">
<img :src="post.media[0].src" />
</RouterLink>
<ReelPlayerUrlOverlay :controller="reel" :content="props.content" />
</template>完整的 useOverlayUrlState 选项见 Vue API 参考.
- 打开时压入 一条 历史记录。滑动信息流则是 替换 它,因此滑 N 次也不会多出记录,退一步永远就是离开播放器。返回键关闭播放器,不会逐张后退。
- 只有在应用内部打开播放器时返回键才会关闭它 —— 因为那次链接压入了一条记录。在新标签页里直接打开的分享链接背后没有历史,浏览器返回会离开站点;这时用 ✕ 按钮或 Escape 就地移除参数并留在页面上。
- 深链
?reel=3会在加载时直接把播放器打开到那一张。 - 指向不存在幻灯片的参数 —— 过期的书签、手改的值 —— 会从 URL 中移除,而不是让地址栏继续声称一张打不开的幻灯片。
- URL 的深度取决于控制器用的 key:单轴只表示帖子,双轴(
urlIndexTwoAxisKey)还会携带多媒体帖子的内层图片索引。每个应用只选一种;两种形态不会互相解码。
一条轴还是两条 —— 自己决定 URL 的深度
同一个 ReelPlayerOverlay 两种形态都能驱动;它在运行时根据控制器的 position 自行判别,所以没有 mode 属性。在构建控制器时选好 key 即可:
| Key | URL 形态 | 携带内容 |
|---|---|---|
urlIndexKey(…) | ?reel=3 | 只有竖向的帖子。 |
urlIndexTwoAxisKey(…) | ?reel=3.2 | 帖子 和 轮播内层媒体索引。 |
两种形态是刻意区分开的 —— 双轴 key 严格使用点分隔(3.0,绝不会是裸的 3),因此单轴链接不会被错误解码。也正因如此,应用在两种 key 之间切换会让此前分享出去的链接全部失效。选定一种形态就别再改。
import { useOverlayUrlState, urlIndexTwoAxisKey } from '@reelkit/vue';
const reel = useOverlayUrlState({
param: 'reel',
...urlIndexTwoAxisKey({
outerCount: () => content.value.length,
innerCounts: () => content.value.map((post) => post.media.length),
}),
});
// A link now names both axes: post 3, inner media 2 — ?reel=3.2稳定的链接。 索引是按位置的,所以收藏下来的 ?reel=3 在信息流重新排序后就会打开另一条帖子 —— 对信息流来说这是常态,而不是例外。 urlStableIdKey 按每条帖子稳定的 id来寻址,扫描当前的信息流 —— 一次调用就覆盖了常见场景。
const reel = useOverlayUrlState({
param: 'reel',
...urlStableIdKey({ items: () => content.value }),
});传入 hashCodec: base64UrlCodec 即可把 URL 中的 id 做 base64url 编码 —— 这是可逆的混淆,不是加密哈希。
想按别的字段(比如 slug)来寻址,或者用 locateAsync给无限信息流翻页,就自己构建 codec/locator 。这是两件事: codec 把身份写进 URL, locator 则负责找到这个身份在哪。
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.value.findIndex((x) => x.id === id),
identify: (index) => content.value[index].id,
},
});无限信息流。 locate 是同步的,因此只能回答已经加载过的帖子 —— 只加载了 20 条时,指向第 400 条的分享链接就查不到。 locateAsync 是兜底,只有在 locate 未命中时才调用。
快捷键
只想按条目的 id来寻址?那就不必手写编解码器和定位器 —— 直接把 locateAsync 传给 urlStableIdKey({ items, locateAsync }) (未命中时它会去拉取,然后返回索引)。下面更完整的写法是给按别的字段寻址、或者需要完全掌控的场景准备的。
const reel = useOverlayUrlState({
param: 'reel',
codec: { decode: (raw) => raw, encode: (id) => id },
locator: {
locate: (id) => content.value.findIndex((x) => x.id === id),
identify: (index) => content.value[index].id,
locateAsync: async (id) => {
const loaded = await loadById(id); // or loadUntil(id) — fetch just that one, or page up to it
if (!loaded) return null; // exhausted — link names no post
content.value = loaded; // commit — the overlay renders from this state
return loaded.findIndex((x) => x.id === id); // wherever it landed
},
},
});- 在
locateAsync未完成期间,播放器保持关闭,参数也不动,因此深链能熬过这次请求。null或请求被拒绝则会移除参数。 - 如果结果在 URL 已经变化、播放器已关闭或组件已卸载之后才到达,就会被丢弃 —— 慢请求不能打开一张没人要的幻灯片。
- 等待期间什么都不渲染;加载状态本来就归页面自己管,所以请渲染你自己的骨架屏。
- 没有超时机制 —— 播放器无从得知信息流有多长。分页用尽时请以
null结束,否则浮层会一直关着。
API 参考
ReelPlayerOverlay 属性
ReelPlayerOverlayProps<T>
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ariaLabel | string | 'Video player' | 对话框区域的无障碍标签;浮层打开时由屏幕阅读器播报 |
aspectRatio | number | 9 / 16 | 桌面端容器的宽高比。宽度在 768px 及以下时,播放器占满整个视口。为零、为负或非有限的值会回退到 9/16。 |
content | T[] (extends BaseContentItem) | 必填 | 播放器中要展示的内容条目数组 |
enableNavKeys | boolean | true | 启用键盘方向键导航 |
enableWheel | boolean | true | 启用鼠标滚轮导航 |
initialIndex | number | 0 | 初始可见条目的索引(从 0 开始) |
initialInnerIndex | number | 0 | 打开时定位的内层媒体索引,只对最初可见的那条帖子生效 —— 让双轴 URL 能直达多媒体帖子里的某一张图。用户一开始导航就会忽略它。 |
isOpen | boolean | 必填 | 控制浮层显示;为 false 时浮层会从 DOM 中移除 |
loop | boolean | false | 在幻灯片之间启用无限循环 |
swipeDistanceFactor | number | 0.12 | 触发切换所需的最小滑动距离占比 |
timeline | 'auto' | 'always' | 'never' | 'auto' | 内置播放进度条的显示策略。'auto' 只在视频时长超过 timelineMinDurationSeconds 时渲染;'always' 只要当前幻灯片有视频就渲染;'never' 关闭内置进度条(需要完全自定义时使用 #timeline 插槽)。 |
timelineMinDurationSeconds | number | 30 | timeline='auto' 时渲染内置进度条所需的最短视频时长(秒)。低于该阈值的循环短片不显示进度条。 |
transitionDuration | number | 300 | 幻灯片动画时长(毫秒) |
wheelDebounceMs | number | 200 | 滚轮事件的防抖时长(毫秒) |
ReelPlayerUrlOverlay 属性
ReelPlayerUrlOverlayProps<T>
接受上面所有属性,除了 is-open,它被 controller. initial-index 会被忽略 —— 由控制器的 position 决定打开哪一张,所以同时传入的值每次打开都会被覆盖。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
controller | UrlStateController | 必填 | 来自 useOverlayUrlState的控制器。它的 position 决定浮层是否打开、显示哪一张;浮层会在切换幻灯片和关闭时通过它写回。 |
事件
| 事件 | 负载 | 说明 |
|---|---|---|
| @api-ready | ReelPlayerApi | 滑动器就绪时发出一次,同时暴露命令式 API |
| @close | void | 播放器关闭时发出 |
| @slide-change | number | 切换后发出,带上新的活动幻灯片索引 |
| @inner-slide-change | outer: number, inner: number | Emitted when the active post's inner media index changes — on inner navigation and on outer activation (the activated post's current inner index, 0 for single-media). |
| @update:is-open | boolean | 关闭时发出;用于支持 `v-model:is-open` |
v-model:is-open
使用 v-model:is-open 即可用一个绑定驱动浮层。旧的 :is-open + @close 写法依然可用,如果你需要显式的事件的话。
<template>
<button @click="open = true">Open</button>
<ReelPlayerOverlay v-model:is-open="open" :content="content" />
</template>类型
ContentItem
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 唯一标识 |
media | MediaItem[] | 一个或多个媒体资源(图片或视频) |
author | { name: string; avatar?: string } | 默认幻灯片浮层中显示的作者 |
description | string? | 说明文字 |
likes | number? | 点赞数 |
TimelineBarProps
interface TimelineBarProps {
class?: string;
style?: CSSProperties;
}TimelineSlotScope<T>
interface TimelineSlotScope<T extends BaseContentItem> {
item: T;
activeIndex: number;
timelineState: TimelineController;
defaultContent: () => VNode | VNode[];
}MediaItem
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 唯一标识 |
type | 'image' | 'video' | 媒体类型 |
src | string | 媒体资源的 URL |
poster | string? | 视频条目的封面缩略图 URL |
aspectRatio | number | 宽高比。小于 1 表示竖向(cover),大于等于 1 表示横向(contain)。 |
联合类型
| 类型 | 取值 | 说明 |
|---|---|---|
MediaType | 'image' | 'video' | MediaItem 的种类,决定幻灯片渲染 ImageSlide 还是 VideoSlide。 |
TimelineMode | 'auto' | 'always' | 'never' | timeline 属性的取值:'auto' 只为时长超过 timelineMinDurationSeconds 的视频显示进度条,'always' 为所有视频显示,'never' 隐藏它。 |
子组件
把它们放进你自定义的 #controls, #slide, or #slideOverlay 模板里。请把插槽作用域里的尺寸和回调透传下去,这样自动播放、封面抽帧和声音同步才能继续工作。
CloseButton
带默认播放器样式的独立圆形关闭按钮。在 #controls.
<script setup lang="ts">
import { CloseButton } from '@reelkit/vue-reel-player';
</script>
<template>
<CloseButton :on-click="onClose" />
<CloseButton :on-click="onClose" class-name="my-close-btn" :style="{ top: '24px', right: '24px' }" />
</template>SoundButton
静音开关。请把它渲染在 SoundProvider (ReelPlayerOverlay 会自动提供一个)。当前幻灯片没有视频时会隐藏。
<script setup lang="ts">
import { SoundButton } from '@reelkit/vue-reel-player';
</script>
<template>
<SoundButton />
<SoundButton disabled class-name="my-sound-btn" />
</template>PlayerControls
未提供 #controls 插槽时浮层渲染的默认控件:一个 CloseButton,开启 show-sound 时再加一个 SoundButton。属性:showSound、soundDisabled(让声音按钮变暗)和 onClose。放进你自己的 #controls 插槽,即可在额外按钮旁保留内置外观。
LoadingIndicator 与 ErrorIndicator
LoadingIndicator 是媒体缓冲时显示的全幻灯片波浪加载动画;ErrorIndicator 是媒体加载失败时显示的默认提示。两者都不接收属性。在你自己的 #loading 或 #error 插槽里渲染它们,即可在额外内容周围保留内置外观。
TimelineBar
默认的播放拖动条。它读取最近的 TimelineProvider (automatically mounted inside ReelPlayerOverlay内部自动挂载),并渲染轨道、缓冲区间、进度填充和拖动手柄。通过 --rk-reel-timeline-* 自定义属性做主题定制,或者用 #timeline 插槽替换。
<script setup lang="ts">
import type { TimelineSlotScope } from '@reelkit/vue-reel-player';
</script>
<template>
<!-- Wrap or augment the default bar from #timeline: -->
<ReelPlayerOverlay>
<template #timeline="{ defaultContent }: TimelineSlotScope">
<MyTimecode />
<component :is="defaultContent" />
</template>
</ReelPlayerOverlay>
</template>SlideOverlay
默认的渐变浮层,显示作者、描述和点赞。内容带有这些字段时才渲染。可通过 #slideOverlay 插槽替换。
<script setup lang="ts">
import { SlideOverlay } from '@reelkit/vue-reel-player';
</script>
<template>
<SlideOverlay
:author="{ name: 'John', avatar: '/avatar.jpg' }"
description="Amazing content"
:likes="12500"
/>
</template>ImageSlide
带懒加载的图片幻灯片,默认使用 object-fit: cover 。把它组合进 #slide 插槽,即可在保留内置行为的前提下自定义图片渲染。
<script setup lang="ts">
import { ImageSlide } from '@reelkit/vue-reel-player';
</script>
<template>
<ImageSlide :src="media.src" :size="size" />
<ImageSlide
:src="media.src"
:size="size"
class-name="my-image-slide"
:style="{ backgroundColor: '#1a1a1a', borderRadius: '12px' }"
:img-style="{ objectFit: 'contain' }"
/>
</template>VideoSlide
由共享的 <video> 元素驱动的视频幻灯片。它负责 iOS 上的声音连续、封面帧和位置记忆。请把它渲染在 SoundProvider (ReelPlayerOverlay 会自动提供一个)。
<script setup lang="ts">
import { VideoSlide } from '@reelkit/vue-reel-player';
</script>
<template>
<VideoSlide
:src="media.src"
:poster="media.poster"
:aspect-ratio="9 / 16"
:size="size"
:is-active="isActive"
:slide-key="slideKey"
:style="{ borderRadius: '12px' }"
/>
</template>组合自定义幻灯片
使用 #slide ,并带 ImageSlide / VideoSlide 即可在保留全部内置行为(自动播放、封面抽帧、声音同步)的前提下自定义媒体渲染。
<script setup lang="ts">
import {
ReelPlayerOverlay,
ImageSlide,
VideoSlide,
} from '@reelkit/vue-reel-player';
</script>
<template>
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<template #slide="{ item, size, isActive, slideKey }">
<ImageSlide
v-if="item.media[0].type === 'image'"
:src="item.media[0].src"
:size="size"
:style="{ backgroundColor: '#111' }"
:img-style="{ objectFit: 'contain' }"
/>
<VideoSlide
v-else
:src="item.media[0].src"
:poster="item.media[0].poster"
:aspect-ratio="item.media[0].aspectRatio"
:size="size"
:is-active="isActive"
:slide-key="slideKey"
:style="{ borderRadius: '16px' }"
/>
</template>
</ReelPlayerOverlay>
</template>内容加载与错误处理
播放器会逐张跟踪加载和错误状态。内容加载时显示波浪加载动画;媒体损坏时显示错误图标。失败的 URL 会被缓存,因此再次打开损坏的幻灯片不会重试。
生命周期回调
使用 #slide 插槽时,请从插槽作用域调用这些回调来驱动加载提示:
| 回调 | 何时调用 |
|---|---|
onReady | 图片已加载,或视频已开始播放。会清除加载和错误状态。 |
onWaiting | 视频在播放途中正在缓冲。显示加载提示。 |
onError | 内容加载失败。显示错误浮层,并把该 URL 标记为损坏缓存起来。 |
<!-- Inside #slide — wire callbacks to your custom media -->
<template #slide="{ item, size, isActive, onReady, onWaiting, onError }">
<div :style="{ width: size[0] + 'px', height: size[1] + 'px' }">
<img
v-if="item.media[0].type === 'image'"
:src="item.media[0].src"
@load="onReady"
@error="onError"
style="width:100%;height:100%;object-fit:cover"
/>
<video
v-else
:src="item.media[0].src"
:autoplay="isActive"
@canplay="onReady"
@waiting="onWaiting"
@error="onError"
style="width:100%;height:100%;object-fit:cover"
/>
</div>
</template>自定义加载与错误界面
通过 #loading 和 #error 插槽替换默认的波浪加载动画和错误图标:
<ReelPlayerOverlay v-model:is-open="isOpen" :content="content">
<template #loading="{ activeIndex }">
<div
style="position:absolute;inset:0;z-index:10;display:flex;
align-items:center;justify-content:center;color:#fff;font-size:14px"
>
Loading slide {{ activeIndex + 1 }}...
</div>
</template>
<template #error="{ activeIndex }">
<div
style="position:absolute;inset:0;z-index:10;display:flex;
flex-direction:column;align-items:center;justify-content:center;
gap:12px;color:rgba(255,255,255,0.5)"
>
<span style="font-size:48px">!</span>
<span>Slide {{ activeIndex + 1 }} failed to load</span>
</div>
</template>
</ReelPlayerOverlay>时间轴
浮层会在当前视频上方渲染一个内置的播放时间轴条。用 timeline 属性控制它: 'auto' (默认)只要当前媒体是时长超过 timelineMinDurationSeconds (默认 30)的视频就渲染, 'always' 则只要有视频在播就渲染, 'never' 则关闭。若要完全自定义拖动条,请用 #timeline 插槽;它的作用域会暴露一个 timelineState ,其数据来自底层的 TimelineController.
<ReelPlayerOverlay
:is-open="isOpen"
:content="items"
timeline="auto"
:timeline-min-duration-seconds="30"
@close="isOpen = false"
/>通过 --rk-reel-timeline-* CSS 自定义属性。
在插槽之外,可以用组合式函数读取同一个控制器:
| 导出 | 说明 |
|---|---|
useTimelineState() | 返回最近的 TimelineController;在 TimelineProvider(由浮层挂载) 外部会抛出错误。 |
useTimelineStateOptional() | 与 useTimelineState 相同,但在 TimelineProvider 外部返回 null 而不是抛出错误——适合可能在浮层外部渲染的组件。 |
RK_TIMELINE_KEY | 两个组合式函数读取的注入键;自己 provide 它,即可用你自己的控制器驱动 TimelineBar。 |
useViewportSize(aspectRatio) | 浮层用来确定自身尺寸的响应式 [width, height]:桌面端按给定宽高比,宽度 768px 及以下时占满整个视口。挂载前返回 [0, 0],因此在 SSR 期间也是安全的。 |
声音上下文
ReelPlayerOverlay 会在根节点挂载一个 SoundProvider ,因此渲染在内部的任何组件都能通过 useSoundState读取或切换静音状态。 这个组合式函数从 @reelkit/vue-reel-player 重新导出,所以你不需要额外的 @reelkit/vue 引入。
<script setup lang="ts">
import { useSoundState, toVueRef } from '@reelkit/vue';
// Inside a custom control rendered from the #controls slot:
const soundState = useSoundState();
const muted = toVueRef(soundState.muted);
</script>
<template>
<button @click="soundState.toggle()">
{{ muted ? 'Unmute' : 'Mute' }}
</button>
</template>在播放器内部, #controls 插槽的作用域上还暴露了 soundState 。只在控件模板里用得到时,优先用它。
CSS 类名
CSS 类名都是普通类名(没有 scoped)。在 @reelkit/vue-reel-player/styles.css 之后加载的样式表可以用更高优先级的选择 器覆盖它们。若只是改颜色、尺寸和 z-index,请使用 主题定制 一节。
| 类名 | 组件 | 说明 |
|---|---|---|
.rk-reel-overlay | Overlay | 固定的全屏背景层(背景、z-index) |
.rk-reel-container | Overlay | 播放器容器(定位、溢出) |
.rk-reel-loader | Overlay | 波浪加载动画浮层 |
.rk-reel-media-error | Overlay | 错误状态浮层(居中图标 + 文字) |
.rk-reel-media-error-text | Overlay | 错误信息文字 |
.rk-reel-button | 控制内容 | 共用的圆形图标按钮(关闭、声音、导航箭头) |
.rk-reel-close-btn | 控制内容 | 关闭按钮 |
.rk-reel-sound-btn | 控制内容 | 声音开关按钮 |
.rk-reel-nav-arrows | 导航 | 仅桌面端的箭头容器(宽度在 768px 及以下时隐藏) |
.rk-reel-nav-button | 导航 | 单个上一张 / 下一张导航箭头 |
.rk-reel-slide-wrapper | Slide | 媒体 + 浮层的包装层 |
.rk-reel-slide-overlay | SlideOverlay | 渐变浮层容器 |
.rk-reel-slide-overlay-author | SlideOverlay | 作者行(头像 + 名称) |
.rk-reel-slide-overlay-avatar | SlideOverlay | 作者头像图片 |
.rk-reel-slide-overlay-name | SlideOverlay | 作者名称文字 |
.rk-reel-slide-overlay-description | SlideOverlay | 描述文字 |
.rk-reel-slide-overlay-likes | SlideOverlay | 点赞行(爱心 + 数量) |
.rk-reel-video-container | VideoSlide | 视频包装层(背景、溢出) |
.rk-reel-video-element | VideoSlide | <video> 元素 |
.rk-reel-video-poster | VideoSlide | 封面图(播放时淡出) |
.rk-reel-video-poster.rk-visible | VideoSlide | 视频暂停 / 加载时施加在封面图上的状态修饰类 |
.rk-reel-nested-indicator | NestedSlider | 多媒体幻灯片下方的圆点分页(桌面端与触摸端位置不同) |
.rk-reel-nested-nav | NestedSlider | 横向轮播箭头(宽度在 768px 及以下时隐藏) |
.rk-reel-nested-nav-next | NestedSlider | 嵌套的下一张箭头位置 |
.rk-reel-nested-nav-prev | NestedSlider | 嵌套的上一张箭头位置 |
.rk-reel-timeline | TimelineBar | 拖动条包装层。在自定义的 `#timeline` 插槽根元素上复用它,即可继承贴底定位、安全区内边距,以及触摸设备上为幻灯片浮层预留的空间。 |
.rk-reel-timeline-track | TimelineBar | 轨道(未播放区域) |
.rk-reel-timeline-buffered | TimelineBar | 缓冲分段层 |
.rk-reel-timeline-fill | TimelineBar | 已播放进度填充 |
.rk-reel-timeline-cursor | TimelineBar | 拖动手柄(浮在轨道上方) |
主题定制
每一个颜色、尺寸、z-index 和过渡都放在 CSS 自定义属性里。在 :root (或浮层的任意祖先元素)上覆盖,即可在不改组件源码的情况下换主题。这些变量与 @reelkit/react-reel-player保持一致,因此覆盖样式可以在不同框架绑定之间通用。
| 变量 | 默认值 | 控制内容 |
|---|---|---|
--rk-reel-overlay-bg | #000 | 全屏背景颜色 |
--rk-reel-overlay-z | 1000 | 浮层 z-index |
--rk-reel-button-bg | rgba(0, 0, 0, 0.5) | 圆形按钮的默认背景 |
--rk-reel-button-bg-hover | rgba(255, 255, 255, 0.1) | 导航箭头背景(以及基础悬停状态) |
--rk-reel-button-bg-hover-strong | rgba(255, 255, 255, 0.2) | 导航箭头悬停背景 |
--rk-reel-button-fg | #fff | 按钮图标颜色 |
--rk-reel-button-size | 44px | 按钮宽高 |
--rk-reel-button-radius | 50% | 按钮圆角半径 |
--rk-reel-ui-z | 10 | 关闭、声音和导航按钮的 z-index |
--rk-reel-edge-padding | 16px | 关闭、声音和导航箭头距边缘的内缩 |
--rk-reel-nav-gap | 8px | 堆叠导航箭头之间的间距 |
--rk-reel-transition | 0.2s | 悬停过渡时长 |
--rk-reel-loader-color | rgba(255, 255, 255, 0.12) | 波浪加载渐变颜色 |
--rk-reel-loader-duration | 1.8s | 波浪加载动画时长 |
--rk-reel-error-fg | rgba(255, 255, 255, 0.4) | 错误图标和文字颜色 |
--rk-reel-slide-overlay-bg | linear-gradient(transparent, rgba(0, 0, 0, 0.7)) | 说明文字遮罩渐变 |
--rk-reel-slide-overlay-padding | 48px 16px 16px | 说明文字内边距 |
--rk-reel-slide-overlay-name-color | #fff | 作者名称颜色 |
--rk-reel-video-bg | #000 | <video> 后方的留黑背景 |
--rk-reel-nested-button-bg | rgba(0, 0, 0, 0.5) | 内层箭头背景 |
--rk-reel-nested-button-size | 36px | 内层箭头尺寸 |
--rk-reel-timeline-track | rgba(255, 255, 255, 0.22) | 轨道背景(未播放区域) |
--rk-reel-timeline-buffered | rgba(255, 255, 255, 0.4) | 已缓冲片段颜色 |
--rk-reel-timeline-fill | #fff | 已播放进度的填充颜色 |
--rk-reel-timeline-cursor | #fff | 拖动手柄胶囊颜色 |
--rk-reel-timeline-height | 3px | 轨道静止时的高度 |
--rk-reel-timeline-height-active | 6px | 悬停、聚焦或拖动时的轨道高度 |
--rk-reel-timeline-cursor-width | 10px | 拖动胶囊静止时的宽度 |
--rk-reel-timeline-cursor-width-active | 14px | 拖动时胶囊的宽度 |
--rk-reel-timeline-cursor-height | 24px | 拖动胶囊静止时的高度 |
--rk-reel-timeline-cursor-height-active | 32px | 拖动时胶囊的高度 |
--rk-reel-timeline-transition | 0.15s ease-out | 轨道与胶囊的伸缩动画 |
把下面这段放进在 @reelkit/vue-reel-player/styles.css.
/* Brand the reel-player overlay */
:root {
--rk-reel-overlay-bg: #0f172a;
--rk-reel-button-bg: rgba(99, 102, 241, 0.65);
--rk-reel-button-bg-hover-strong: rgba(168, 85, 247, 0.85);
--rk-reel-edge-padding: 24px;
--rk-reel-button-size: 52px;
/* Timeline bar: brand-matched, beefier on desktop */
--rk-reel-timeline-track: rgba(99, 102, 241, 0.25);
--rk-reel-timeline-buffered: rgba(168, 85, 247, 0.45);
--rk-reel-timeline-fill: #a855f7;
--rk-reel-timeline-cursor: #a855f7;
--rk-reel-timeline-height: 4px;
--rk-reel-timeline-height-active: 8px;
--rk-reel-timeline-cursor-width-active: 18px;
--rk-reel-timeline-transition: 0.2s ease-out;
}无障碍
浮层根节点是一个模态对话框(role="dialog", aria-modal="true"). Set the aria-label 属性可以改变屏幕阅读器的播报内容,默认是 “Video player”。每张幻灯片都带有 role="group", aria-roledescription="slide"、 aria-label="第 N 张,共 M 张".
浮层打开时捕获焦点,关闭时把焦点还给触发元素。Tab 和 Shift+Tab 在内部的可聚焦元素之间循环;跑出去的焦点(点击外部、程序化聚焦)会被拉回来。实现基于 captureFocusForReturn 和 createFocusTrap from @reelkit/core.
键盘快捷键
| Key | 作用 |
|---|---|
ArrowUp | 上一张幻灯片 |
ArrowDown | 下一张幻灯片 |
ArrowLeft | 上一个媒体(内层轮播) |
ArrowRight | 下一个媒体(内层轮播) |
Escape | 关闭播放器 |