KMediaViewer
图片 / 音频 / 视频统一展示组件:稳定占位、加载/解码/超时/重试、深色 16:9 播放器控制栏、动态水印、诚实的下载交互开关。源码卡片、预览与阅读页应共用本组件,保证三种上下文行为一致。
示例
播放器选型验证(2026-09-09)
| 项 | 结论 |
|---|---|
| 版本 | plyr@3.8.4(锁定精确版本,见 package.json dependencies) |
| 许可证 | MIT |
| 样式 | plyr/dist/plyr.css(约 32.5 KB),通过 ?inline 内联进 plyrBundle 懒加载 chunk,随音视频分支一起到达,无需手动 <link> |
| 全屏 | fullscreen.container 支持指定选择器使容器(含水印层)在全屏中保持可见;fallback: true 提供容器内全屏降级 |
| PiP | 控件中可移除 pip 入口,同时给媒体元素设置 disablePictureInPicture,双保险屏蔽绕过水印层的画中画 |
| 打包 | 不引入第三方 Vue 包装器,Plyr 由本组件内部管理初始化与 destroy();图片分支不触发任何播放器下载 |
Props
| Name | Type | Default | Description |
|---|---|---|---|
| kind | 'image' | 'audio' | 'video' | — | 媒体类型(必填) |
| source | string | — | 稳定媒体引用(URL 或调用方令牌),经 resolveSource 解析后使用 |
| alt / title | string | '' | 图片替代文本 / 悬停标题;alt 也作为默认下载文件名 |
| poster | string | '' | 视频封面 |
| intrinsicWidth / intrinsicHeight | number | — | 已知尺寸元数据:视频占位期按其 aspect-ratio 预留高度;图片/音频就绪后用作最终比例 |
| align | 'left' | 'center' | 'right' | 图片 center,音视频 left | 布局对齐 |
| width | number | '50%' | 'auto' … | 'auto' | 像素数、百分比字符串或 auto(图片=容器 50%,音视频=100%),均不超过容器 |
| height | number | '50%' | 'auto' … | 'auto' | 显式设置后固定组件高度,媒体仅在容器内部按 contain 适配,不再用固有比例改变组件尺寸 |
| timeoutMs | number | 10000 | 解析/加载超时毫秒数 |
| allowDownload | boolean | true | 下载交互开关,见下方边界说明 |
| watermark | MediaWatermark | null | null | 水印配置,默认关闭 |
| resolveSource | (source, signal) => Promise<ResolvedMediaSource | null> | ResolvedMediaSource | null | — | 稳定引用 → 短期 URL 适配器;signal 在换源/卸载时中止 |
| downloadHandler | (source, resolvedUrl) => void | Promise<void> | — | 自定义下载回调;未提供时默认请求 Blob,再通过同源临时 URL 下载,避免跨域地址忽略 download 后跳转 |
| allowPlaybackRate | boolean | false | 在播放器控制栏显示倍速按钮(音视频通用,位于原设置入口的位置):默认 1x 只显示图标,切换后显示对应倍速并点击弹出档位菜单;关闭时播放器没有任何倍速入口 |
| playbackRates | number[] | [0.5, 0.75, 1, 1.25, 1.5, 2] | 倍速菜单档位,自动过滤非正数/非数值并升序去重 |
MediaWatermark:{ text, opacity?(0–1, 默认 0.15), fontSize?(8–96, 默认 14), color?, rotate?(-180–180, 默认 -22), gapX?(60–600, 默认 140), gapY?(40–400, 默认 90) }。文本仅作为编码 SVG 平铺渲染,绝不作为 HTML 注入。水印覆盖整个展示区(含音频卡片),pointer-events: none 不阻挡操作。
Emits
| Name | Parameters | Description |
|---|---|---|
| ready | { url, width?, height? } | 图片完成 load+decode、音视频到达可播放后触发;携带实际尺寸供调用方缓存为 intrinsic 元数据 |
| error | reason: string | 解析/加载/超时失败 |
| retry | — | 用户点击重试 |
| download-request | — | 用户点击下载按钮(先于 downloadHandler) |
Slots
| Name | Description |
|---|---|
| default | 覆盖在展示区右下角的工具区(稳定容器,支持 hover / focus-within),编辑器可将删除、布局工具放入 |
状态机
resolving → loading → ready | error。URL 解析成功不代表加载成功:图片完成 load 与 decode 后才挂载显示。就绪前图片与音频只显示一个居中的加载胶囊(失败时为带重试的卡片),不预留空白框——即使配置了 width / height,也在就绪后才生效;视频始终保留黑色播放区,已知尺寸用 aspect-ratio 预留最终高度,未知尺寸使用 16:9。切换源或卸载会取消旧任务,旧请求结果不能覆盖新状态。音视频等待 loadedmetadata / canplay,Plyr 与其样式仅在音视频分支动态加载。
主题变量
图片 / 音频的加载胶囊直接落在宿主页面上(视频的胶囊在黑色播放区上,固定为深色半透明),可通过 CSS 变量适配宿主主题:
| 变量 | 默认值 | 说明 |
|---|---|---|
--k-media-viewer-pill-bg | #ffffff | 胶囊背景 |
--k-media-viewer-pill-border | #e2e8e5 | 胶囊边框 |
--k-media-viewer-pill-shadow | 0 1px 3px rgba(15, 23, 42, 0.06) | 胶囊阴影 |
--k-media-viewer-pill-fg | #4b5563 | 文字颜色 |
--k-media-viewer-pill-icon-bg | #f4f6f5 | 加载图标底色 |
--k-media-viewer-pill-track | rgba(15, 23, 42, 0.12) | 加载圈轨道色 |
下载开关的真实边界
allowDownload=false 只是交互限制:隐藏下载按钮、阻止组件区域内的右键下载菜单、设置 controlsList="nodownload"。它无法阻止 F12 开发者工具、Network 面板、缓存或脚本提取已传输到浏览器的媒体字节;水印同样是覆盖层,不是防篡改或烧录水印。该布尔值不是服务端授权凭据——真正的下载权限必须由服务端策略控制。若需要更强的保护,需另行设计加密分发 / DRM 方案,且仍不能保证绝对无法复制。
Basic Usage
Prototype-aligned video controls
Video instances use a dark 16:9 stage with a bottom control bar, volume and fullscreen, and optional download. The control bar keeps only the mute button for volume; the volume slider pops up vertically above it on hover or keyboard focus, so it never squeezes the progress bar in narrow players. Set watermark.mode to single for one moving mark (the {time} token is refreshed every second), or leave it as grid for a tiled watermark. Set allowPlaybackRate (default false) to show a dedicated rate button in the control bar where the settings entry used to live — icon-only at the default 1x, the active rate (e.g. 1.5x) once changed; playbackRates customizes its menu options. Audio cards get the same button.
从稳定引用解析短期 URL 的适配器(组件不依赖任何业务 API / 令牌 / SDK):
const resolve = async (source: string, signal: AbortSignal) => {
if (!source.startsWith('attachment://')) return { url: source }
const uuid = source.slice('attachment://'.length)
const response = await fetch(`/api/attachments/${encodeURIComponent(uuid)}/media-preview-url`, { signal })
if (!response.ok) return null
const { url, width, height } = (await response.json()).data
return { url, width, height }
}自定义下载(例如走服务端鉴权下载端点):
<KMediaViewer
kind="video"
source="attachment://9c2d…"
:resolve-source="resolve"
:allow-download="true"
:download-handler="(source) => openDownloadPage(source)"
/>