更新记录

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。画面按实际显示区域计算起止位置,视频转场期间保留当前播放器;自适应高度建议沿用示例在全屏遮罩下提前同步外层高度。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android INTERNET;不主动申请相册、摄像头或存储权限。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不主动收集用户数据。按使用者传入的 URL 加载媒体,媒体服务器会接收对应网络请求。

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

无

暂无用户评论。