更新记录
0.0.2(2026-10-29)
增加说明文档
0.0.1(2026-10-29)
-
修复 Android 切换视频时进度条短暂显示上一个视频进度或归零;缓存各视频时长,原生进度刷新提升至 100ms,事件频率保持不变。
-
Android 相邻视频预备不再依赖当前图片加载成功;保留相邻页暂停画面,播放器释放延后到分页停稳,减少切换瞬间的解码器销毁阻塞。
-
视频预备默认搜索范围扩大到前后 3 项,覆盖视频之间穿插图片的场景;仍最多预备一个视频,文档与两端默认配置同步。
-
修复 Android 视频进出全屏换窗口时的露白与画面间隙,补全暂停画面衔接及资源释放,减少尺寸不变时的重复布局。
-
显式实现 Android 生命周期前后置回调,修复切后台时找不到 ActivityLifecycleCallbacks$-CC 的崩溃。
-
修复 Android nvue 数组元素在 UTS 序列化时字段丢失,导致轮播及自定义小图空白的问题。
-
修复 Android 事件 detail 重复包装,恢复页码、静音和媒体高度同步。
-
修复 Android 播放及视频预加载时 Player.Listener 默认回调兼容导致的 AbstractMethodError。
-
图片、视频混合轮播。
-
全屏预览、图片缩放、视频进度控制。
-
小图切换条和外观配置。
-
支持 Android、iOS 的 App nvue 页面。
-
修复 iOS 事件参数丢失导致的高度同步异常和分页布局留白。
-
优化翻页时的播放状态与高度同步,示例增加 220ms 高度过渡。
-
新增 preload 分组:相邻图片预解码、静音备用视频预备和切换复用,支持关闭、范围、延迟、缓冲时长及超时。
-
修复快速切换时异步回调和 seek 进度覆盖;隐藏、退出全屏和卸载时补全资源清理,保留实例内各视频的播放状态。
-
底部分页支持小图、圆点、轮播进度线或隐藏,外观与全屏显示可配置。
-
整理最简接入、商品详情测试页和完整默认配置;常用示例统一分组配置,旧属性继续兼容。
-
图片退出全屏改为图片图层转场,保留放大/拖动起点,避免逐帧分页重排;示例在全屏遮罩下提前同步页面高度。
-
统一图片/视频进出全屏的媒体图层转场;视频保留播放输出,起止布局预先就绪,取消或卸载时释放转场资源。
平台兼容性
uni-app(5.25)
| Vue2 | Vue3 | Chrome | Safari | app-vue | app-nvue | Android | iOS | 鸿蒙 |
|---|---|---|---|---|---|---|---|---|
| √ | √ | × | × | × | √ | √ | √ | × |
| 微信小程序 | 支付宝小程序 | 抖音小程序 | 百度小程序 | 快手小程序 | 京东小程序 | 鸿蒙元服务 | QQ小程序 | 飞书小程序 | 小红书小程序 | 快应用-华为 | 快应用-联盟 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| × | × | × | × | - | - | - | × | - | - | - | - |
nk-Swiper
图片、视频混合轮播,支持全屏预览、不同高度的媒体过渡、图片缩放和小图切换。
仅支持 uni-app App 端的 nvue 环境,请在 .nvue 页面中使用。 普通 .vue 页面、H5 和小程序不支持;Vue 3 的 <script setup> 写法不影响这一要求。
底部分页
默认显示圆点,通过 appearance.pagination.mode 切换:
| 样式 | 配置 |
|---|---|
| 圆点(默认) | pagination: { mode: 'dots' } |
| 底部小图 | pagination: { mode: 'thumbnails' } |
| 进度线 | pagination: { mode: 'progress' } |
| 隐藏 | pagination: { mode: 'none' } |
使用环境与接入
- HBuilderX 5.0+,Android 6.0+、iOS 15.0+;Android compileSdk 35+。
- 将插件放入
uni_modules/nk-Swiper,在.nvue页面中通过 easycom 使用<nk-swiper />,并设置组件宽高。 - 将页面注册到
pages.json,路径不带扩展名。例如页面为pages/product/detail.nvue,注册路径为pages/product/detail。 - 插件包含原生代码,App 调试需要制作包含本插件的自定义基座。新增插件、修改原生代码或原生依赖后,需要重新制作基座;正式发布时重新打包 App。
下方示例请保存为 .nvue 文件。商品标题、价格和详情内容由业务页面自行编写。
完整示例
除媒体示例数据外,以下均为默认配置。尺寸单位 px,颜色支持 #RRGGBB、#RRGGBBAA、transparent。媒体地址替换后可运行,请保存为 .nvue 文件。
<template>
<view class="media-container">
<nk-swiper
class="media-swiper"
ref="swiperRef"
:items="items"
:thumbnails="thumbnails"
:current="current"
:muted="muted"
:active="active"
:autoplayVideo="config.autoplayVideo"
:loopVideo="config.loopVideo"
:previewEnabled="config.previewEnabled"
:appearance="appearance"
:style="{ width: `${width}px`, height: `${mediaHeight}px` }"
@change="onChange"
@mutechange="onMuteChange"
@heightchange="onHeightChange"
@previewchange="onPreviewChange"
@thumbnailmore="onThumbnailMore"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
const width = uni.getSystemInfoSync().windowWidth
const mediaHeight = ref(width) // 页面初始高度,组件本身无默认宽高
const swiperRef = ref(null)
const current = ref(0) // 当前索引
const muted = ref(true) // 默认静音
const active = ref(true) // 页面可见状态
// 默认 [],这里放两条媒体示例。width、height 为原始尺寸,可省略。
const items = ref([
{
id: 'image-1', // 唯一 ID
type: 'image', // 默认 image
src: 'https://cdn.example.com/image.jpg',
width: 1200,
height: 1200
},
{
id: 'video-1',
type: 'video',
src: 'https://cdn.example.com/video.mp4',
poster: 'https://cdn.example.com/poster.jpg', // 视频封面
width: 1080,
height: 1440
}
])
// 开启 thumbnails 模式后自动生成小图;自定义数组的 mediaId 对应 items.id。
const thumbnails = ref([])
// [
// { mediaId: 'image-1', label: '' }, // 默认无文字,需要时填写 label
// { mediaId: 'video-1', label: '', src: 'https://cdn.example.com/thumb.jpg' }
// ]
const config = reactive({
autoplayVideo: true, // 视频自动播放
loopVideo: true, // 视频循环播放
previewEnabled: true // 点击进入全屏
})
// 完整默认配置,仅供查阅。日常接入只保留需要覆盖的字段。
const options = reactive({
media: { // 主图
backgroundColor: '#ffffff', // 背景颜色
objectFit: 'contain', // 图片/视频适配:contain 保留完整画面,cover 铺满裁切
pagingEnabled: true, // 允许手势横向翻页(代码和小图切换仍可用)
minHeight: 0, // 推荐主图最小高度,0 不限制
maxHeight: 0 // 推荐主图最大高度,0 不限制
},
indicator: { // 页码
show: true, // 显示开关
fontSize: 12, // 字号
color: '#ffffff', // 前景颜色
backgroundColor: '#00000066', // 背景颜色
previewBackgroundColor: '#00000000', // 全屏模式背景颜色
radius: 3, // 圆角
height: 20, // 高度
minWidth: 28, // 最小宽度
paddingHorizontal: 4, // 横向内边距
top: 10, // 顶部距离
extraTop: 0, // 额外顶部偏移(对应原 indicatorTop)
previewTop: 44, // 全屏顶部距离
left: 10, // 左侧距离
right: 10, // 右侧距离
bottom: 10, // 底部距离
position: 'top-right', // top-left / top-right / bottom-left / bottom-right;顶部距离基于安全区,底部距离基于主图底部
previewPosition: 'top-right', // top-left / top-right / bottom-left / bottom-right;顶部距离基于安全区,底部距离基于主图底部
format: '{current}/{total}' // 页码格式,支持 {current} / {total}
},
mute: { // 静音按钮
show: true, // 显示开关
size: 30, // 控件边长
iconSize: 16, // 图标尺寸
color: '#ffffff', // 前景颜色
backgroundColor: '#00000033', // 背景颜色
radius: 15, // 圆角
left: 12, // 左侧距离
right: 12, // 右侧距离
bottom: 12, // 底部距离
position: 'bottom-left' // bottom-left / bottom-right;基于视频实际画面边缘,进度条开启时向上避让
},
play: { // 播放按钮
show: true, // 显示开关
size: 58, // 控件边长
iconSize: 30, // 图标尺寸
color: '#ffffff', // 前景颜色
backgroundColor: '#00000000', // 背景颜色
radius: 29, // 圆角
offsetX: 0, // 中心点水平偏移,可为负
offsetY: 0 // 中心点垂直偏移,可为负
},
preview: { // 全屏预览
backgroundColor: '#000000', // 背景颜色
headerHeight: 72, // 全屏主图上方预留高度,另加顶部安全区
bottomInset: 16, // 全屏底部预留高度,iOS 另加底部安全区
transitionDuration: 260, // 放大/收回转场时间,毫秒
showClose: true, // 显示关闭按钮
closeSize: 44, // 关闭按钮边长(最小 24)
closeIconSize: 20, // 返回图标尺寸
closeColor: '#ffffff', // 返回图标颜色
closeBackgroundColor: '#00000000', // 关闭按钮背景色
closeRadius: 22, // 关闭按钮圆角
closeLeft: 8, // 关闭按钮左侧距离
closeTop: 0, // 关闭按钮距顶部安全区距离
showStatusBar: true, // 全屏显示状态栏,下次打开全屏时应用
objectFit: 'contain', // 图片/视频适配:contain 保留完整画面,cover 铺满裁切
statusBarStyle: 'light' // 状态栏文字颜色:light 白色 / dark 黑色,下次打开全屏时应用
},
controls: { // 视频进度
show: false, // 内嵌视频显示进度条
progressColor: '#b3b3b3', // 已播放进度颜色
trackColor: '#66666680', // 未播放轨道颜色
thumbColor: '#b3b3b3', // 进度拖动圆点颜色
thumbSize: 6, // 进度圆点尺寸,限制 2~32
timeColor: '#ffffff', // 时间文字颜色
timeFontSize: 11, // 时间字号
height: 32, // 进度和时间区域高度,最小 16
bottom: 6, // 底部距离
timeLeft: 12, // 时间区域左侧距离
timeWidth: 52, // 时间区域宽度
gap: 12, // 间距
showDuration: false, // 显示总时长;开启时可增大 timeWidth
showInPreview: true, // 全屏视频显示进度条,独立于 show;位于主图和小图条之间
showTime: true, // 内嵌进度条显示时间文字
previewShowTime: false // 全屏进度条显示时间文字;false 时进度条使用整行
},
error: { // 加载失败提示
imageText: '图片加载失败,点击重试', // 图片加载失败文案
videoText: '视频加载失败,点击重试', // 视频加载失败文案
fontSize: 14, // 字号
color: '#ffffff', // 前景颜色
backgroundColor: '#00000088', // 背景颜色
radius: 6, // 圆角
margin: 20, // 提示条左右边距
height: 44, // 失败提示条高度,最小 24
retryEnabled: true // 允许点击提示重试
},
thumbnail: { // 小图条
show: true, // 显示开关
showInPreview: true, // 小图总开关开启时,在全屏显示
placement: 'below', // below 占据主图下方高度;overlay 覆盖主图底部(全屏始终独立预留)
size: 50, // 小图默认边长,限制 16~200
width: 0, // 小图宽度,0 使用 size;有效尺寸限制 16~200
height: 0, // 小图高度,0 使用 size;有效尺寸限制 16~200
gap: 8, // 小图间距,限制 0~64
radius: 3, // 小图圆角,不超过宽高中较小值的一半
borderWidth: 1, // 选中/未选中边框宽度
activeColor: '#ff6b00', // 选中边框颜色
inactiveBorderColor: '#ffffff', // 未选中边框颜色
tileBackgroundColor: '#ffffff', // 小图容器底色
imageInset: 2, // 图片相对容器边缘的内缩距离
imageFit: 'contain', // 小图适配:contain / cover
paddingLeft: 16, // 小图滚动区左侧留白
paddingRight: 16, // 无右侧入口时滚动区右侧留白
paddingTop: 7, // 小图条上留白
paddingBottom: 7, // 小图条下留白
barHeight: 0, // 小图条最小高度,0 自动计算
backgroundColor: '#d5d4da', // 背景颜色
gradientColors: ['#cfced6', '#d8d7dc'], // 内嵌背景渐变颜色数组,2~8 个;[] 关闭渐变
gradientDirection: 'horizontal', // 渐变方向:horizontal / vertical
previewBackgroundColor: '#000000', // 全屏模式背景颜色
previewGradientColors: [], // 全屏小图条渐变数组,[] 使用纯色
opacity: 1, // 整个小图条透明度,0~1
labelMode: 'pill', // pill 居中短标签;bar 横向标签条
labelFontSize: 11, // 标签字号
labelHeight: 18, // 标签高度
labelColor: '#ffffff', // 标签文字颜色
labelBackgroundColor: 'transparent', // 标签底色
labelRadius: 2, // 标签圆角
labelBottom: 4, // 标签距小图底部距离
labelPaddingHorizontal: 3, // 标签横向留白
rightText: '', // 右侧入口文案,空字符串隐藏;由业务决定数量
rightWidth: 0, // 右侧入口宽度,0 根据文字和箭头自动计算,最大为组件一半
rightFontSize: 14, // 右侧文字字号
rightColor: '#ffffff', // 右侧文字和箭头颜色
rightArrow: true, // 显示右侧箭头
rightArrowSize: 12, // 箭头尺寸
rightGap: 6, // 文字和箭头间距
rightInset: 16, // 右侧入口距组件右边缘距离
rightShadowColor: '#00000000', // 右侧文字阴影颜色
rightShadowRadius: 0, // 右侧文字阴影半径
rightShadowOffsetY: 0 // 右侧文字阴影垂直偏移
},
preload: { // 相邻预加载与视频预备
enabled: true, // 开启相邻预加载
imageRadius: 1, // 图片/封面预加载半径,0 关闭,最大 3 页
videoRadius: 3, // 候选视频搜索半径,0 关闭,最大 3 页
prepareVideo: true, // 预备相邻视频,最多一个静音备用播放器
videoBufferSeconds: 2, // 备用视频目标缓冲秒数,0.5~5
delay: 120, // 页面停稳后的预加载延迟,0~2000ms
videoTimeout: 8000 // 视频预备超时,1000~30000ms
},
pagination: { // 底部分页样式,独立于右上角页码和视频进度条
mode: 'dots', // thumbnails 小图 / dots 圆点 / progress 轮播进度线 / none 隐藏
showInPreview: true, // 全屏也显示圆点/轮播进度线
hideOnSingle: true, // 只有一项时隐藏圆点/轮播进度线
dotSize: 6, // 圆点直径,1~40
activeDotWidth: 6, // 选中圆点宽度,至少为 dotSize,最大 100
gap: 6, // 圆点间距,0~64
color: '#ffffff80', // 未选中圆点/进度线轨道颜色
activeColor: '#ffffff', // 选中圆点/进度线颜色
bottom: 12, // 距主图底部距离;视频进度条显示时自动上移
left: 16, // 左侧留白
right: 16, // 右侧留白
align: 'center', // 对齐:left / center / right
progressWidth: 80, // 轮播进度线宽度
progressHeight: 3 // 轮播进度线高度,1~40
}
})
const appearance = computed(() => JSON.stringify(options))
function detail(event) {
const data = event.detail
return data && (data.index != null || data.value != null) ? data : event
}
let previewing = false
let pendingHeight = mediaHeight.value
function onChange(event) {
current.value = detail(event).index
}
function onMuteChange(event) {
muted.value = detail(event).muted
}
// 按当前图片/视频比例调整高度,开启小图模式时 value 已包含小图条高度。固定高度时移除该事件绑定。
function onHeightChange(event) {
const data = detail(event)
if (!Number.isFinite(data.value) || data.value <= 0) return
pendingHeight = data.value
if (!data.fullscreen || previewing) mediaHeight.value = pendingHeight
}
function onPreviewChange(event) {
previewing = detail(event).fullscreen
mediaHeight.value = pendingHeight
}
function onThumbnailMore(event) {
console.log('小图条右侧入口', detail(event).index)
}
onShow(() => { active.value = true })
onHide(() => { active.value = false })
onUnload(() => { active.value = false })
</script>
<style>
.media-container { transition-property: height; transition-duration: 0.22s; transition-timing-function: ease-out; }
.media-swiper { transition-property: height; transition-duration: 0.22s; transition-timing-function: ease-out; }
</style>
默认显示圆点;需要小图条时,设置 options.pagination.mode = 'thumbnails'。小图标签改 thumbnails.label,空字符串隐藏;右侧文字改 options.thumbnail.rightText,空字符串隐藏。字段含义和取值范围见下方“外观参数说明”。
全屏转场
图片和视频进出全屏使用同一套画面位置与裁剪计算。布局只在起止状态准备,动画期间缩放媒体图层,避免整块分页容器逐帧重排。视频复用当前播放实例与画面输出,保持静音、暂停状态和播放进度;未就绪时先使用已加载封面,封面也没有就绪时淡入/淡出。
Android 换窗口时短暂保留当前画面,等目标视图完成绘制再交接,减少露白、黑帧和位置跳变。衔接画面在交接完成、切换媒体或卸载时释放,无需额外配置。
Android 切换视频时,进度条立即使用该视频已保存的位置和时长,加载期间不会先显示上一个视频的进度或临时归零。返回相邻视频时保留暂停画面与输出;旧播放器在分页停稳后释放,期间不再创建新的备用播放器。原生进度条每 100ms 更新,timeupdate 事件仍最多每 500ms 触发一次。
转场时长为 appearance.preview.transitionDuration,默认 260 毫秒,范围 0~3000,设为 0 直接切换。不需要增加其他配置。
自适应高度时,沿用示例的 heightchange / previewchange:全屏展开后在遮罩下提前同步页面高度,让退出目标位置先就绪。避免只在退出后才改变页面高度,否则主图缩回后还会调整一次高度。固定高度页面不需要绑定这两个事件。
下方附有最简接入、商品详情与播放测试的完整代码,以及全部外观参数说明。
事件
通过 event.detail || event 获取参数。
| 事件 | 常用参数 |
|---|---|
| change | index、id、mediaType |
| mutechange | muted |
| previewchange / previewclose | fullscreen、index |
| timeupdate | value:播放秒数;duration:总秒数 |
| heightchange | value:建议总高度,包含小图条;fullscreen |
| play / pause / ended | index、id |
| thumbnailmore | index、id,点击小图条右侧入口时触发 |
| error | message |
方法
通过组件 ref 调用,例如 swiperRef.value?.setCurrent(1, true)。
| 方法 | 说明 |
|---|---|
| setCurrent(index, animated) | 切换媒体 |
| play() / pause() | 播放、暂停 |
| seekTo(seconds) | 跳转播放位置 |
| updateMuted(value) | 设置静音 |
| updateActive(value) | 设置可见状态 |
| openPreview() / closePreview() | 打开、关闭全屏 |
属性默认值
只传要修改的属性即可。样式优先使用 appearance 分组,不必重复绑定同类旧属性。
| 属性 | 类型 | 默认值 |
|---|---|---|
| items | Array | [] |
| appearance | String | "{}" |
| thumbnails | Array | [] |
| showThumbnails | Boolean | true |
| previewThumbnails | Boolean | true |
| thumbnailSize | Number | 50 |
| thumbnailGap | Number | 8 |
| thumbnailRadius | Number | 3 |
| thumbnailActiveColor | String | "#ff6b00" |
| thumbnailBarColor | String | "#d5d4da" |
| thumbnailRightText | String | "" |
| current | Number | 0 |
| muted | Boolean | true |
| autoplayVideo | Boolean | true |
| loopVideo | Boolean | true |
| active | Boolean | true |
| previewEnabled | Boolean | true |
| showIndicator | Boolean | true |
| showVideoControls | Boolean | false |
| indicatorTop | Number | 0 |
| objectFit | String | "contain" |
最简接入示例
保存为 .nvue 页面,包含图片、视频、自适应高度和页面离开时暂停。未填写的配置使用默认值。
<template>
<view :style="swiperStyle">
<nk-swiper
ref="swiperRef"
class="media-swiper"
:items="items"
:appearance="appearance"
:active="active"
:muted="muted"
:style="swiperStyle"
@mutechange="muted = $event.detail.muted"
@heightchange="onHeightChange"
@previewchange="onPreviewChange"
/>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
const width = uni.getSystemInfoSync().windowWidth
const height = ref(width)
const swiperRef = ref(null)
const active = ref(true)
const muted = ref(true)
const items = [
{ id: 'photo', type: 'image', src: 'https://picsum.photos/id/1018/1000/1000', width: 1000, height: 1000 },
{ id: 'video', type: 'video', src: 'https://media.w3.org/2010/05/sintel/trailer.mp4', poster: 'https://media.w3.org/2010/05/sintel/poster.png', width: 854, height: 480 }
]
// 只写要修改的配置;不传 appearance 就使用全部默认值。
const options = reactive({
pagination: { mode: 'dots' }
})
const appearance = computed(() => JSON.stringify(options))
const swiperStyle = computed(() => ({ width: `${width}px`, height: `${height.value}px` }))
let previewing = false
let pendingHeight = height.value
function onHeightChange({ detail }) {
if (!Number.isFinite(detail.value) || detail.value <= 0) return
pendingHeight = detail.value
// 全屏已展开时,提前更新被遮住的页面高度,让退出的目标位置就绪。
if (!detail.fullscreen || previewing) height.value = pendingHeight
}
function onPreviewChange({ detail }) {
previewing = detail.fullscreen
height.value = pendingHeight
}
onShow(() => { active.value = true })
onHide(() => { active.value = false })
onUnload(() => { active.value = false })
</script>
<style>
.media-swiper { transition-property: height; transition-duration: 0.22s; transition-timing-function: ease-out; }
</style>
商品详情与播放测试示例
保存为 .nvue 页面并在 pages.json 注册。包含媒体测试数据、分页样式切换、播放控制和自定义商品详情,商品内容可按业务替换。
<template>
<view class="page">
<list class="detail-list" :show-scrollbar="false">
<cell>
<view :style="swiperStyle">
<nk-swiper
ref="swiperRef"
class="media-swiper"
:items="items"
:appearance="appearance"
:active="active"
:muted="muted"
:style="swiperStyle"
@change="onChange"
@mutechange="muted = $event.detail.muted"
@heightchange="onHeightChange"
@previewchange="onPreviewChange"
/>
</view>
</cell>
<cell>
<view class="product-card">
<view class="price-row">
<text class="currency">¥</text>
<text class="price">{{ product.price }}</text>
<text class="original-price">¥{{ product.originalPrice }}</text>
<text class="sales">已售 {{ product.sales }}</text>
</view>
<text class="product-title">{{ product.title }}</text>
<text class="description">{{ product.subtitle }}</text>
<view class="tag-row">
<text v-for="tag in product.tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
</cell>
<cell>
<view class="section">
<text class="section-title">轮播效果测试</text>
<text class="hint">当前第 {{ selected + 1 }} 项,可切换底部样式后继续滑动或进入全屏。</text>
<view class="mode-row">
<view v-for="mode in modes" :key="mode.value" :class="['mode-button', options.pagination.mode === mode.value ? 'mode-active' : '']" @tap="options.pagination.mode = mode.value">
<text :class="['mode-label', options.pagination.mode === mode.value ? 'mode-label-active' : '']">{{ mode.label }}</text>
</view>
</view>
<view class="test-row">
<view class="test-button" @tap="swiperRef?.setCurrent((selected + 1) % items.length, true)"><text class="test-label">切换下一项</text></view>
<view class="test-button" @tap="swiperRef?.openPreview()"><text class="test-label">全屏预览</text></view>
<view class="test-button" @tap="swiperRef?.pause()"><text class="test-label">暂停</text></view>
<view class="test-button" @tap="swiperRef?.play()"><text class="test-label">播放</text></view>
</view>
</view>
</cell>
<cell>
<view class="section">
<view v-for="row in product.services" :key="row.label" class="info-row">
<text class="info-label">{{ row.label }}</text>
<text class="info-value">{{ row.value }}</text>
</view>
</view>
</cell>
<cell>
<view class="section">
<view class="section-heading"><text class="section-title">商品评价</text><text class="hint">{{ product.reviewCount }} 条 · 好评率 98%</text></view>
<view v-for="review in reviews" :key="review.name" class="review">
<view class="review-heading"><text class="review-name">{{ review.name }}</text><text class="hint">{{ review.variant }}</text></view>
<text class="review-content">{{ review.content }}</text>
</view>
</view>
</cell>
<cell>
<view class="section">
<text class="section-title">商品参数</text>
<view v-for="row in product.parameters" :key="row.label" class="info-row"><text class="info-label">{{ row.label }}</text><text class="info-value">{{ row.value }}</text></view>
</view>
</cell>
<cell>
<view class="section detail-heading"><text class="section-title">商品详情</text><text class="description">以下为页面自定义内容,图片和文案均为演示数据。</text></view>
</cell>
<cell v-for="detail in details" :key="detail.title">
<view class="detail-block">
<text class="detail-title">{{ detail.title }}</text>
<text class="detail-copy">{{ detail.text }}</text>
<image :src="detail.image" mode="aspectFill" :style="{ width: `${width}px`, height: `${width * detail.ratio}px` }" />
</view>
</cell>
<cell><view class="page-footer"><text class="hint">演示商品 · 不提供实际购买服务</text></view></cell>
</list>
</view>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { onHide, onShow, onUnload } from '@dcloudio/uni-app'
const width = uni.getSystemInfoSync().windowWidth
const height = ref(width * 800 / 1200)
const swiperRef = ref(null)
const active = ref(true)
const muted = ref(true)
const selected = ref(0)
const items = [
{
id: 'landscape-1',
type: 'image',
src: 'https://picsum.photos/id/1018/1200/800',
width: 1200,
height: 800
},
{
id: 'video-demo',
type: 'video',
src: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/2minute-demo.mp4',
poster: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni-app.png',
width: 1280,
height: 720
},
{
id: 'portrait-1',
type: 'image',
src: 'https://picsum.photos/id/1015/900/1200',
width: 900,
height: 1200
},
{
id: 'square-1',
type: 'image',
src: 'https://picsum.photos/id/1025/1000/1000',
width: 1000,
height: 1000
},
{
id: 'video-sintel',
type: 'video',
src: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
poster: 'https://media.w3.org/2010/05/sintel/poster.png',
width: 854,
height: 480
},
{
id: 'landscape-2',
type: 'image',
src: 'https://picsum.photos/id/1016/1200/800',
width: 1200,
height: 800
},
{
id: 'portrait-2',
type: 'image',
src: 'https://picsum.photos/id/1020/900/1200',
width: 900,
height: 1200
}
]
// 只覆盖常用项。其余样式、播放和预加载行为均使用插件默认值。
const options = reactive({
pagination: { mode: 'dots' }
})
const appearance = computed(() => JSON.stringify(options))
const swiperStyle = computed(() => ({ width: `${width}px`, height: `${height.value}px` }))
const modes = [
{ label: '小图', value: 'thumbnails' },
{ label: '圆点', value: 'dots' },
{ label: '进度线', value: 'progress' },
{ label: '隐藏', value: 'none' }
]
const product = {
title: '山野出行 · 轻量户外旅行套装',
subtitle: '周末轻徒步与日常出行,带上轻便装备去看沿途风景。',
price: '338.00', originalPrice: '429.00', sales: '1,286', reviewCount: 268,
tags: ['包邮', '7天无理由', '48小时发货'],
services: [
{ label: '选择', value: '苔原绿 / 标准套装 / 1件' },
{ label: '配送', value: '浙江杭州发货 · 快递免运费' },
{ label: '服务', value: '运费险 · 退货保障 · 在线客服' }
],
parameters: [
{ label: '品牌', value: '山野出行(演示)' },
{ label: '材质', value: '锦纶 / 聚酯纤维' },
{ label: '颜色', value: '苔原绿、岩石灰、暖沙色' },
{ label: '适用场景', value: '城市通勤、周末露营、轻徒步' },
{ label: '包装清单', value: '主包 × 1、收纳袋 × 1、说明卡 × 1' }
]
}
const reviews = [
{ name: '小林', variant: '苔原绿 · 标准套装', content: '周末出门背了一天,分区放东西很方便,颜色也耐看。' },
{ name: '山间来信', variant: '岩石灰 · 标准套装', content: '轻便好收纳,日常出行装水杯和外套刚好,整体做工满意。' }
]
const details = [
{ title: '轻装出发', text: '常用物品分区收纳,让每一次短途出行都从容一点。', image: 'https://picsum.photos/id/1018/1200/800', ratio: 800 / 1200 },
{ title: '走进自然', text: '城市之外,还有值得慢慢发现的风景。', image: 'https://picsum.photos/id/1015/900/1200', ratio: 1200 / 900 },
{ title: '日常也好用', text: '兼顾通勤与周末出游,一套装备应对不同场景。', image: 'https://picsum.photos/id/1016/1200/800', ratio: 800 / 1200 }
]
let previewing = false
let pendingHeight = height.value
function onChange({ detail }) { selected.value = detail.index }
function onHeightChange({ detail }) {
if (!Number.isFinite(detail.value) || detail.value <= 0) return
pendingHeight = detail.value
// 全屏已展开时,提前更新被遮住的页面高度,让退出的目标位置就绪。
if (!detail.fullscreen || previewing) height.value = pendingHeight
}
function onPreviewChange({ detail }) {
previewing = detail.fullscreen
height.value = pendingHeight
}
onShow(() => { active.value = true })
onHide(() => { active.value = false })
onUnload(() => { active.value = false })
</script>
<style>
.page { flex: 1; background-color: #f5f6f8; }
.detail-list { flex: 1; }
.media-swiper { transition-property: height; transition-duration: 0.22s; transition-timing-function: ease-out; }
.product-card { background-color: #ffffff; padding-top: 28rpx; padding-bottom: 30rpx; padding-left: 30rpx; padding-right: 30rpx; }
.price-row { flex-direction: row; align-items: center; }
.currency { font-size: 32rpx; color: #f45b2b; font-weight: bold; }
.price { font-size: 56rpx; color: #f45b2b; font-weight: bold; }
.original-price { font-size: 24rpx; color: #a0a3aa; margin-left: 16rpx; text-decoration: line-through; }
.sales { flex: 1; font-size: 23rpx; color: #969aa3; text-align: right; }
.product-title { margin-top: 14rpx; font-size: 34rpx; line-height: 48rpx; color: #22252b; font-weight: bold; }
.description { margin-top: 12rpx; font-size: 25rpx; line-height: 38rpx; color: #92969e; }
.tag-row { flex-direction: row; margin-top: 20rpx; }
.tag { font-size: 22rpx; color: #d8764f; background-color: #fff3eb; padding-top: 6rpx; padding-bottom: 6rpx; padding-left: 12rpx; padding-right: 12rpx; margin-right: 12rpx; border-radius: 5rpx; }
.section { margin-top: 18rpx; background-color: #ffffff; padding-top: 26rpx; padding-bottom: 26rpx; padding-left: 30rpx; padding-right: 30rpx; }
.section-title { font-size: 29rpx; color: #282c34; font-weight: bold; }
.hint { font-size: 23rpx; color: #9298a2; line-height: 36rpx; }
.mode-row { flex-direction: row; margin-top: 20rpx; }
.mode-button { flex: 1; align-items: center; justify-content: center; background-color: #f3f4f7; border-radius: 10rpx; height: 64rpx; margin-right: 10rpx; }
.mode-active { background-color: #fff0e8; }
.mode-label { font-size: 25rpx; color: #666d78; }
.mode-label-active { color: #e36c35; font-weight: bold; }
.test-row { flex-direction: row; margin-top: 20rpx; }
.test-button { flex: 1; align-items: center; justify-content: center; height: 54rpx; background-color: #ffffff; }
.test-label { font-size: 23rpx; color: #727a88; }
.info-row { flex-direction: row; padding-top: 16rpx; padding-bottom: 16rpx; }
.info-label { width: 140rpx; font-size: 25rpx; line-height: 38rpx; color: #92969e; }
.info-value { width: 550rpx; flex: 1; font-size: 25rpx; line-height: 38rpx; color: #3d434d; }
.section-heading { flex-direction: row; justify-content: space-between; align-items: center; }
.review { margin-top: 24rpx; background-color: #ffffff; }
.review-heading { flex-direction: row; justify-content: space-between; align-items: center; }
.review-name { font-size: 25rpx; color: #4b525d; }
.review-content { margin-top: 12rpx; font-size: 25rpx; color: #616976; line-height: 40rpx; }
.detail-heading { margin-bottom: 0; }
.detail-block { padding-top: 28rpx; background-color: #ffffff; }
.detail-title { font-size: 34rpx; color: #343b47; font-weight: bold; margin-left: 30rpx; }
.detail-copy { margin-top: 14rpx; margin-bottom: 24rpx; margin-left: 30rpx; margin-right: 30rpx; font-size: 25rpx; line-height: 40rpx; color: #828a96; }
.page-footer { height: 150rpx; justify-content: center; align-items: center; background-color: #f5f6f8; }
</style>
外观参数说明
通过 :appearance="JSON.stringify(options)" 传入,只需填写要覆盖的字段。上方完整示例已列出全部默认配置。
尺寸单位为逻辑 px,非 rpx。颜色支持 #RRGGBB、#RRGGBBAA、transparent,透明度在最后。
优先级:appearance 显式字段 > 独立属性 > 默认值。修改对象后重新序列化会同步原生界面,不重建播放器;未知字段忽略,类型不正确的标量回退默认值。
media · 主媒体
| 字段 | 默认值 | 说明 |
|---|---|---|
backgroundColor |
"#ffffff" |
背景颜色 |
objectFit |
"contain" |
图片/视频适配:contain 保留完整画面,cover 铺满裁切 |
pagingEnabled |
true |
允许手势横向翻页(代码和小图切换仍可用) |
minHeight |
0 |
推荐主图最小高度,0 不限制 |
maxHeight |
0 |
推荐主图最大高度,0 不限制 |
indicator · 页码
| 字段 | 默认值 | 说明 |
|---|---|---|
show |
true |
显示开关 |
fontSize |
12 |
字号 |
color |
"#ffffff" |
前景颜色 |
backgroundColor |
"#00000066" |
背景颜色 |
previewBackgroundColor |
"#00000000" |
全屏模式背景颜色 |
radius |
3 |
圆角 |
height |
20 |
高度 |
minWidth |
28 |
最小宽度 |
paddingHorizontal |
4 |
横向内边距 |
top |
10 |
顶部距离 |
extraTop |
0 |
额外顶部偏移(对应原 indicatorTop) |
previewTop |
44 |
全屏顶部距离 |
left |
10 |
左侧距离 |
right |
10 |
右侧距离 |
bottom |
10 |
底部距离 |
position |
"top-right" |
top-left / top-right / bottom-left / bottom-right;顶部距离基于安全区,底部距离基于主图底部 |
previewPosition |
"top-right" |
top-left / top-right / bottom-left / bottom-right;顶部距离基于安全区,底部距离基于主图底部 |
format |
"{current}/{total}" |
页码格式,支持 {current} / {total} |
mute · 静音按钮
| 字段 | 默认值 | 说明 |
|---|---|---|
show |
true |
显示开关 |
size |
30 |
控件边长 |
iconSize |
16 |
图标尺寸 |
color |
"#ffffff" |
前景颜色 |
backgroundColor |
"#00000033" |
背景颜色 |
radius |
15 |
圆角 |
left |
12 |
左侧距离 |
right |
12 |
右侧距离 |
bottom |
12 |
底部距离 |
position |
"bottom-left" |
bottom-left / bottom-right;基于视频实际画面边缘,进度条开启时向上避让 |
play · 播放按钮
| 字段 | 默认值 | 说明 |
|---|---|---|
show |
true |
显示开关 |
size |
58 |
控件边长 |
iconSize |
30 |
图标尺寸 |
color |
"#ffffff" |
前景颜色 |
backgroundColor |
"#00000000" |
背景颜色 |
radius |
29 |
圆角 |
offsetX |
0 |
中心点水平偏移,可为负 |
offsetY |
0 |
中心点垂直偏移,可为负 |
preview · 全屏预览
| 字段 | 默认值 | 说明 |
|---|---|---|
backgroundColor |
"#000000" |
背景颜色 |
headerHeight |
72 |
全屏主图上方预留高度,另加顶部安全区 |
bottomInset |
16 |
全屏底部预留高度,iOS 另加底部安全区 |
transitionDuration |
260 |
放大/收回转场时间,毫秒 |
showClose |
true |
显示关闭按钮 |
closeSize |
44 |
关闭按钮边长(最小 24) |
closeIconSize |
20 |
返回图标尺寸 |
closeColor |
"#ffffff" |
返回图标颜色 |
closeBackgroundColor |
"#00000000" |
关闭按钮背景色 |
closeRadius |
22 |
关闭按钮圆角 |
closeLeft |
8 |
关闭按钮左侧距离 |
closeTop |
0 |
关闭按钮距顶部安全区距离 |
showStatusBar |
true |
全屏显示状态栏,下次打开全屏时应用 |
objectFit |
"contain" |
图片/视频适配:contain 保留完整画面,cover 铺满裁切 |
statusBarStyle |
"light" |
状态栏文字颜色:light 白色 / dark 黑色,下次打开全屏时应用 |
controls · 进度与时间
| 字段 | 默认值 | 说明 |
|---|---|---|
show |
false |
内嵌视频显示进度条 |
progressColor |
"#b3b3b3" |
已播放进度颜色 |
trackColor |
"#66666680" |
未播放轨道颜色 |
thumbColor |
"#b3b3b3" |
进度拖动圆点颜色 |
thumbSize |
6 |
进度圆点尺寸,限制 2~32 |
timeColor |
"#ffffff" |
时间文字颜色 |
timeFontSize |
11 |
时间字号 |
height |
32 |
进度和时间区域高度,最小 16 |
bottom |
6 |
底部距离 |
timeLeft |
12 |
时间区域左侧距离 |
timeWidth |
52 |
时间区域宽度 |
gap |
12 |
间距 |
showDuration |
false |
显示总时长;开启时可增大 timeWidth |
showInPreview |
true |
全屏视频显示进度条,独立于 show;位于主图和小图条之间 |
showTime |
true |
内嵌进度条显示时间文字 |
previewShowTime |
false |
全屏进度条显示时间文字;false 时进度条使用整行 |
error · 失败提示
| 字段 | 默认值 | 说明 |
|---|---|---|
imageText |
"图片加载失败,点击重试" |
图片加载失败文案 |
videoText |
"视频加载失败,点击重试" |
视频加载失败文案 |
fontSize |
14 |
字号 |
color |
"#ffffff" |
前景颜色 |
backgroundColor |
"#00000088" |
背景颜色 |
radius |
6 |
圆角 |
margin |
20 |
提示条左右边距 |
height |
44 |
失败提示条高度,最小 24 |
retryEnabled |
true |
允许点击提示重试 |
thumbnail · 小图切换条
| 字段 | 默认值 | 说明 |
|---|---|---|
show |
true |
显示开关 |
showInPreview |
true |
小图总开关开启时,在全屏显示 |
placement |
"below" |
below 占据主图下方高度;overlay 覆盖主图底部(全屏始终独立预留) |
size |
50 |
小图默认边长,限制 16~200 |
width |
0 |
小图宽度,0 使用 size;有效尺寸限制 16~200 |
height |
0 |
小图高度,0 使用 size;有效尺寸限制 16~200 |
gap |
8 |
小图间距,限制 0~64 |
radius |
3 |
小图圆角,不超过宽高中较小值的一半 |
borderWidth |
1 |
选中/未选中边框宽度 |
activeColor |
"#ff6b00" |
选中边框颜色 |
inactiveBorderColor |
"#ffffff" |
未选中边框颜色 |
tileBackgroundColor |
"#ffffff" |
小图容器底色 |
imageInset |
2 |
图片相对容器边缘的内缩距离 |
imageFit |
"contain" |
小图适配:contain / cover |
paddingLeft |
16 |
小图滚动区左侧留白 |
paddingRight |
16 |
无右侧入口时滚动区右侧留白 |
paddingTop |
7 |
小图条上留白 |
paddingBottom |
7 |
小图条下留白 |
barHeight |
0 |
小图条最小高度,0 自动计算 |
backgroundColor |
"#d5d4da" |
背景颜色 |
gradientColors |
["#cfced6","#d8d7dc"] |
内嵌背景渐变颜色数组,2~8 个;[] 关闭渐变 |
gradientDirection |
"horizontal" |
渐变方向:horizontal / vertical |
previewBackgroundColor |
"#000000" |
全屏模式背景颜色 |
previewGradientColors |
[] |
全屏小图条渐变数组,[] 使用纯色 |
opacity |
1 |
整个小图条透明度,0~1 |
labelMode |
"pill" |
pill 居中短标签;bar 横向标签条 |
labelFontSize |
11 |
标签字号 |
labelHeight |
18 |
标签高度 |
labelColor |
"#ffffff" |
标签文字颜色 |
labelBackgroundColor |
"transparent" |
标签底色 |
labelRadius |
2 |
标签圆角 |
labelBottom |
4 |
标签距小图底部距离 |
labelPaddingHorizontal |
3 |
标签横向留白 |
rightText |
"" |
右侧入口文案,空字符串隐藏;由业务决定数量 |
rightWidth |
0 |
右侧入口宽度,0 根据文字和箭头自动计算,最大为组件一半 |
rightFontSize |
14 |
右侧文字字号 |
rightColor |
"#ffffff" |
右侧文字和箭头颜色 |
rightArrow |
true |
显示右侧箭头 |
rightArrowSize |
12 |
箭头尺寸 |
rightGap |
6 |
文字和箭头间距 |
rightInset |
16 |
右侧入口距组件右边缘距离 |
rightShadowColor |
"#00000000" |
右侧文字阴影颜色 |
rightShadowRadius |
0 |
右侧文字阴影半径 |
rightShadowOffsetY |
0 |
右侧文字阴影垂直偏移 |
preload · 相邻预加载
| 字段 | 默认值 | 说明 |
|---|---|---|
enabled |
true |
开启相邻预加载 |
imageRadius |
1 |
图片/封面预加载半径,0 关闭,最大 3 页 |
videoRadius |
3 |
候选视频搜索半径,0 关闭,最大 3 页 |
prepareVideo |
true |
预备相邻视频,最多一个静音备用播放器 |
videoBufferSeconds |
2 |
备用视频目标缓冲秒数,0.5~5 |
delay |
120 |
页面停稳后的预加载延迟,0~2000ms |
videoTimeout |
8000 |
视频预备超时,1000~30000ms |
纯色与渐变:单独传 thumbnail.backgroundColor 会清除内嵌默认渐变;同时传 gradientColors 则以该数组为准。全屏用 previewBackgroundColor 和 previewGradientColors 单独控制。默认小图条高度 64px,实际为 max(barHeight, 小图高度 + 上下内边距)。
右侧入口点击发出 thumbnailmore;label 文本来自 thumbnails 数据项。页码支持格式变量,播放、静音、返回使用平台原生/绘制图标,可调整尺寸和颜色。
修改默认配置时运行 node uni_modules/nk-Swiper/scripts/generate-appearance.mjs,同步生成 Swift/Kotlin 默认配置;随后重新编译原生插件。
预加载按 items 的相邻索引计算,优先后页、再前页;视频范围内最多准备一个,跳过主播放器已保留的媒体。当前视频首帧显示后才启动后台预备,备用播放器始终静音暂停;切换时复用已准备的播放器。失败或超时不影响当前媒体,切入时仍可正常加载和重试。关闭、页面隐藏、替换列表、内存紧张或销毁时取消待执行任务和备用播放器;当前视频重新缓冲时暂停后台预备。关闭仅停止额外预加载,当前媒体和所选分页样式仍会正常加载。
图片仅预加载相邻范围,已解码图片由平台缓存管理;小图仍按小尺寸独立加载。videoBufferSeconds 为目标缓冲时长,系统与片源可能导致实际数据量不同。未提供持久化视频磁盘缓存;本版不承诺任意网络环境下零等待。
Android 不再等待当前图片下载完成才预备相邻视频。切回仍保留在相邻页中的视频时复用原来的暂停画面和 Surface;切换到备用播放器后,旧播放器先静音暂停,等分页停稳再释放。这段交接期间不会创建新的备用播放器,最多保留两个播放实例。
pagination · 底部分页
| 字段 | 默认值 | 说明 |
|---|---|---|
mode |
"dots" |
thumbnails 小图 / dots 圆点 / progress 轮播进度线 / none 隐藏 |
showInPreview |
true |
全屏也显示圆点/轮播进度线 |
hideOnSingle |
true |
只有一项时隐藏圆点/轮播进度线 |
dotSize |
6 |
圆点直径,1~40 |
activeDotWidth |
6 |
选中圆点宽度,至少为 dotSize,最大 100 |
gap |
6 |
圆点间距,0~64 |
color |
"#ffffff80" |
未选中圆点/进度线轨道颜色 |
activeColor |
"#ffffff" |
选中圆点/进度线颜色 |
bottom |
12 |
距主图底部距离;视频进度条显示时自动上移 |
left |
16 |
左侧留白 |
right |
16 |
右侧留白 |
align |
"center" |
对齐:left / center / right |
progressWidth |
80 |
轮播进度线宽度 |
progressHeight |
3 |
轮播进度线高度,1~40 |
默认显示圆点。设置 pagination.mode: 'thumbnails' 可切换为小图条。dots / progress 覆盖主图底部,不计入 heightchange 的额外高度,数量按 items 计算。与 indicator 页码、controls 视频播放进度独立;圆点过多时自动缩小以适应可用宽度。
图片与视频的进入、退出全屏共用 preview.transitionDuration。画面按实际显示区域计算起止位置,视频转场期间保留当前播放器;自适应高度建议沿用示例在全屏遮罩下提前同步外层高度。

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 50
赞赏 0
下载 12726210
赞赏 1958
赞赏
京公网安备:11010802035340号