更新记录
1.1.1(2024-10-16)
- 提供 vue2 代码示例
1.0.0(2024-10-13)
仿抖音短视频(超高性能)H5、APP、小程序全端支持,支持 m3u8;该插件支持扩展和自定义,插件内有详细注释说明
平台兼容性
Vue2 | Vue3 |
---|---|
√ | √ |
App | 快应用 | 微信小程序 | 支付宝小程序 | 百度小程序 | 字节小程序 | QQ小程序 |
---|---|---|---|---|---|---|
HBuilderX 4.0 app-vue app-nvue | × | √ | × | × | × | × |
钉钉小程序 | 快手小程序 | 飞书小程序 | 京东小程序 |
---|---|---|---|
× | × | × | × |
H5-Safari | Android Browser | 微信浏览器(Android) | QQ浏览器(Android) | Chrome | IE | Edge | Firefox | PC-Safari |
---|---|---|---|---|---|---|---|---|
√ | √ | √ | √ | √ | √ | √ | √ | √ |
组件版本说明
ml-swiper-v2、ml-swiper-v3 均支持 VUE2、VUE3
下文提供了 vue3 版本代码示例,末尾提供了 vue2 版本代码示例
特别说明:
ml-swiper-v2、ml-swiper-v3 后面的 -v{n} 仅表示 ml-swiper 组件的版本 并不代表 vue 的版本
ml-swiper-v2、ml-swiper-v3 后面的 -v{n} 仅表示 ml-swiper 组件的版本 并不代表 vue 的版本
ml-swiper-v2、ml-swiper-v3 后面的 -v{n} 仅表示 ml-swiper 组件的版本 并不代表 vue 的版本
ml-swiper-v2、ml-swiper-v3 均支持 VUE2、VUE3
扩展或自定义
扩展或自定义 请阅读本插件中的:抖音官方示例.md
无需扩展和自定义 推荐使用:ml-swiper-v3
组件支持情况
ml-swiper-v2
版本:支持 VUE2、VUE3
H5:支持 m3u8 流媒体资源
组件其他版本
更多组件 请前往 作者主页查看 :https://ext.dcloud.net.cn/publisher?id=1784252
ml-swiper :该版本的插件 为 ml-swiper
的第一版 插件 适用于 案例测试、组件学习、抖音功能分析等;该版本的插件 仅支持 VUE3;
ml-swiper-v2 :该 插件 是在 ml-swiper
版本上做的升级,支持 VUE2、VUE3,并且支持了 m3u8 流媒体资源;此版本的插件 适用于 个人或者公司 的二次开发 定制专属插件;插件内 功能、方法 均有详细注释说明,并且 组件内详细说明了 抖音APP
的实现逻辑,以及抖音官方的说明;(通过该 版本的组件 可以实现一个 适用于自己或者公司 专属的 扩展组件)
ml-swiper-v3 :该 插件 是在 ml-swiper-v2
的基础上 做出的最终版,并且优化了 大资源数据下 导致的上下滑动卡顿的问题。该组件不需要特殊配置,容易上手,内置插槽 便于实现个性化UI页面,并且提供了 很多组件方法 便于自定义实现更多的业务逻辑;
扩展或自定义
扩展或自定义 请阅读本插件中的:抖音官方示例.md
无需扩展和自定义 推荐使用:ml-swiper-v3
真实运行示例
APP-雷电模拟器
手机端-QQ浏览器
组件参数props
属性名 | 类型 | 默认值 | 说明 | 必须 |
---|---|---|---|---|
list |
Array | [] | 视频数据,默认空数组,参数详情下见options 介绍 |
是 |
width |
String | 100% | 组件宽度,默认与设备同宽 | 否 |
height |
String | 100% | 组件高度,默认与设备同高 | 否 |
criticalVal |
Number | 2 | 临界值,当 list.length - currentIndex >= criticalVal 时触发加载更多事件 |
否 |
progress |
Boolean | true | 是否显示进度条,默认显示进度条 | 否 |
duration |
Boolean | true | 是否显示播放时间,默认显示播放时长 | 否 |
视频数据详情options
属性名 | 类型 | 默认值 | 说明 | 必须 |
---|---|---|---|---|
url |
String | "" | 视频资源地址 | 是 |
title |
String | "" | 视频标题 | 否 |
poster |
String | "" | 视频封面,支持JPG、PNG等常见图片文件 | 否 |
----- | ----- | 其他属性可根据需要自定义 | ----- | ----- |
比如: | userInfo = {} |
用户相关数据 | ||
比如: | commentList = [] |
评论数据列表 | ||
比如: | date |
视频发布时间 | ||
比如: | location |
IP属地信息 | ||
。。。 | 。。。 | 。。。 | 。。。 | 。。。 |
组件事件Events
事件 | 参数 | 解释 | 说明 |
---|---|---|---|
onchange (event) 滑动事件 |
event = {index, context, video} | index:当前视频的索引 context:video的上下文对象 video:正在播放的视频数据 |
当视频上下滑动时,触发 onchange 事件 |
onplay (event) 播放事件 |
event = {index, context, video} | index:当前视频的索引 context:video的上下文对象 video:正在播放的视频数据 |
当前视频播放时,触发 onplay 事件 |
onpause (event) 暂停事件 |
event = {index, context, video} | index:当前视频的索引 context:video的上下文对象 video:正在播放的视频数据 |
当前视频暂停时,触发 onpause 事件 |
onended (event) 结束事件 |
event = {index, context, video} | index:当前视频的索引 context:video的上下文对象 |
当前视频播放结束时,触发 onpause 事件 |
ontimeupdate (event) 进度变更事件 |
event事件 | event:APP、小程序、H5 原生事件 | 视频进度条变化时触发ontimeupdate 事件,可用来自定义进度条 |
onwaiting (event) 出现缓冲事件 |
event = {index, context, video} | index:当前视频的索引 context:video的上下文对象 |
当前视频播放出现缓冲时,触发 onwaiting 事件 |
onerror (event) 播放出错事件 |
event = {index, context, video, error, event} | index:当前视频的索引 context:video的上下文对象 |
当前视频播放出错时,触发 onerror 事件 |
onclick (event) 视频单击事件 |
event = {index, context, video, playing} | index:当前视频的索引 context:video的上下文对象 video:当前视频的数据信息 playing:是否正在播放 |
当单击视频时触发onclick 事件 |
ondblclick (event) 视频双击事件 |
event = {index, context, video, playing} | index:当前视频的索引 context:video的上下文对象 video:当前视频的数据信息 |
当双击视频时触发ondblclick 事件 |
loadmore(event) 加载更多数据 |
event = { index } | index:当前视频的索引 | 当 list.length - currentIndex >= criticalVal 时触发加载更多事件 |
组件方法methods
事件 | 参数 | 说明 |
---|---|---|
fullScreen() 进入全屏 |
- | 全屏播放 |
exitFullScreen() 退出全屏 |
- | 退出全屏播放 |
setRate(rate) 设置倍速 |
Number: 0.5 | 0.8 | 1.0 | 1.25 | 1.5 | 2.0 | 当前视频倍速,不同平台支持不一样 |
setSeek(val) 指定播放时间 |
Number: 60 | 指定播放时间,单位秒,从 60秒处 播放 |
方法使用示例
<!-- ...省略... -->
<ml-swiper-v2 ref="mlSwiperRef"></ml-swiper-v2>
<!-- ...省略... -->
<script setup>
import { ref } from 'vue';
const mlSwiperRef = ref(null);
// ...省略...
/**
* 全屏
*/
function fullScreen() {
mlSwiperRef.value?.fullScreen();
// context?.requestFullScreen();
}
/**
* 退出全屏
*/
function exitFullScreen() {
mlSwiperRef.value?.exitFullScreen();
// context?.exitFullScreen();
}
/**
* 倍速
* @param {Number} rate 0.5 | 0.8 | 1.0 | 1.25 | 1.5 | 2.0
*/
function setRate(rate) {
mlSwiperRef.value?.setRate(rate);
// context?.playbackRate(rate);
}
/**
* 跳转到指定位置播放
* @param {Number} val 单位秒
*/
function setSeek(val) {
mlSwiperRef.value?.setSeek(val);
// context?.seek(val);
}
/// ...省略...
</script>
扩展或自定义
扩展或自定义 请阅读本插件中的:抖音官方示例.md
无需扩展和自定义 推荐使用:ml-swiper-v3
vue3 版本示例代码
组件示例代码
微信小程序 和 H5 网页端示例代码
(注:H5端 需要安装 hls,详见下文: H5端使用说明
)
# H5 端 安装插件
npm install hls.js
index.vue
<template>
<template v-if="type == 'v2'">
<!-- 这里使用 v-if 是为了防止 请求后台时还没返回数据 导致组件初始化时没有视频资源 而出现错误 -->
<ml-swiper-v2 v-if="lists && lists.length > 0" :list="lists" @loadmore="loadmore" @ondblclick="" >
<!-- 自定义内容,这里设置100%后 层级高于video,会导致单击、双击功能失效 -->
<template v-slot:default="{ index }">
<view class="body">
<text class="text">{{ index + 1 }} / {{ lists.length }}</text>
</view>
</template>
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right">
<image class="userAvatar" :src="item?.author?.avatar" @click="avatarClick"></image>
<!-- 喜欢 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0]?.includes(item.videoId) ? '#ff0004' : '#fff'" @tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1]?.includes(item.videoId) ? '#ff0' : '#fff'" @tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom">
<text class="title">{{ item?.title }}</text>
</view>
</template>
</ml-swiper-v2>
</template>
<template v-if="type == 'v3'">
<ml-swiper-v3 v-if="lists && lists.length > 0" :list="lists" @loadmore="loadmore" @ondblclick="" >
<!-- 自定义内容,这里设置100%后 层级高于video,会导致单击、双击功能失效 -->
<template v-slot:default="{ index }">
<view class="body">
<text class="text">{{ index + 1 }} / {{ lists.length }}</text>
</view>
</template>
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right">
<image class="userAvatar" :src="item?.author?.avatar" @click="avatarClick"></image>
<!-- 喜欢 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0]?.includes(item.videoId) ? '#ff0004' : '#fff'" @tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1]?.includes(item.videoId) ? '#ff0' : '#fff'" @tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom">
<text class="title">{{ item?.title }}</text>
</view>
</template>
</ml-swiper-v3>
</template>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
const lists = ref([]);
const type = ref("v2"); // 当前 ml-swiper版本 (当前版本是 ml-swiper-v2)
const datas = reactive({});
let count = 0;
function avatarClick() {
uni.showToast({ title: "点击头像", icon: "none", duration: 1000 });
}
function comment(_id) {
uni.showToast({ title: "查看评论", icon: "none", duration: 1000 });
}
function forward(_id) {
uni.showToast({ title: "转发", icon: "none", duration: 1000 });
}
function iconClick(type, id) {
datas[type] = (datas[type] || []);
let index = datas[type]?.indexOf(id);
if (index >= 0) {
datas[type].splice(index, 1);
} else {
datas[type].push(id);
}
}
function ({ video }) {
console.log(video?.videoId);
iconClick(0, video?.videoId);
}
function loadmore() {
uni.showToast({ title: "加载更多", icon: "none", duration: 1000 });
for (var i = 0; i < 2; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
}
onMounted(() => {
// 这里直接生成 200 条视频数据进行测试(模拟请求后台获取数据)
uni.showToast({ title: "加载中...", icon: "loading", duration: 1500 });
setTimeout(() => {
let size = type.value == 'v2' ? 2 : 50;
for (var i = 0; i < size; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
uni.hideToast();
}, 1500);
});
const getList = () => {
return [
{
videoId: lists.value.length + 1,
title: `抖音美女主播,JK超短裙学生妆美女跳舞展示,爱了爱了。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/11/08/19/BMjAyMDExMDgxOTQxNTlfNTIzNDczMzQ0XzM4OTQ1MDk5MTI4XzFfMw==_b_Bc770a92f0cf153407d60a2eddffeae2a.mp4",
uploadTime: "2023-11-08 19:41",
ipLocation: "上海",
author: {
authorId: 101,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "陌路",
genderName: "男"
}
},
{
videoId: lists.value.length + 2,
title: `御姐美女抖音作品,来个自拍视频把,好美啊。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/10/02/09/BMjAyMDEwMDIwOTAwMDlfMTIyMjc0NTk0Ml8zNjk3Mjg0NjcxOF8xXzM=_b_B28a4518e86e2cf6155a6c1fc9cf79c6d.mp4",
uploadTime: "2023-10-02 09:41",
ipLocation: "贵州",
author: {
authorId: 102,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "御姐呀",
genderName: "女"
}
},
{
videoId: lists.value.length + 3,
title: `抖音主播可爱妹子新学的舞蹈,超可爱的美女主播。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov6.a.yximgs.com/upic/2020/08/23/00/BMjAyMDA4MjMwMDMyNDRfMTYzMzY5MDA0XzM0ODI4MDcyMzQ5XzFfMw==_b_B9a1c9d4e3a090bb2815994d7f33a906a.mp4",
uploadTime: "2023-08-23 00:41",
ipLocation: "广州",
author: {
authorId: 103,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "野花猫",
genderName: "女"
}
},
{
videoId: lists.value.length + 4,
title: `多个美女带着遮阳帽出去散步自拍视频,好好看。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://alimov2.a.yximgs.com/upic/2020/07/02/14/BMjAyMDA3MDIxNDUyMDlfOTExMjIyMjRfMzE1OTEwNjAxNTRfMV8z_b_Bf3005d42ce9c01c0687147428c28d7e6.mp4",
uploadTime: "2023-07-02 14:41",
ipLocation: "山西",
author: {
authorId: 104,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "蓝姬",
genderName: "女"
}
}
];
}
</script>
<style scoped lang="scss">
.body {
position: absolute;
top: 0;
margin: 0 auto;
}
.text {
font-size: 50px;
color: #ff5918;
}
.right {
width: 50px;
padding: 10px 0;
margin: 0 auto;
}
.icon {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
margin: 8px auto;
}
.icon-val {
color: #fff;
font-size: 13px;
text-align: center;
}
.userAvatar {
width: 40px;
height: 40px;
border-radius: 100%;
}
.bottom {
margin: 10px;
}
.title {
color: #fff;
font-size: 13px;
}
</style>
H5端使用说明
如果需要编译为 H5 端,则需要安装插件 hls
,APP端 和 微信小程序 不需要安装
npm install hls.js
测试说明:H5 端测试了 QQ浏览器 支持良好,夸克浏览器 不支持该插件(夸克浏览器只支持内置播放器且悬浮于顶层)。 其他浏览器均为测试,如需使用 请自行测试。
APP代码示例
app端
APP端开发,暂不支持 在 vue 页面中使用
ml-swiper-v3
组件,如需开发 APP 应用,请将 vue 页面 改为 nvue 页面,其他不变即可正常使用该组件。特别说明:APP 端 不支持动画效果的
progress
进度条,其他功能依然可以正常使用
index.nvue
<template>
<!-- 这里使用 v-if 是为了防止 请求后台时还没返回数据 导致组件初始化时没有视频资源 而出现错误 -->
<ml-swiper-v2 v-if="lists && lists.length > 0" :list="lists" @loadmore="loadmore" @ondblclick="" >
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right">
<image class="userAvatar" :src="item?.author?.avatar" @click="avatarClick"></image>
<!-- 喜欢 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0]?.includes(item.videoId) ? '#ff0004' : '#fff'" @tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1]?.includes(item.videoId) ? '#ff0' : '#fff'" @tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom">
<text class="title">{{ item?.title }}</text>
</view>
</template>
</ml-swiper-v2>
</template>
<script setup >
import { onMounted, reactive, ref } from 'vue';
const lists = ref([]);
const type = ref("v3");
const datas = reactive({});
let count = 0;
function avatarClick() {
uni.showToast({ title: "点击头像", icon: "none", duration: 1000 });
}
function comment(_id) {
uni.showToast({ title: "查看评论", icon: "none", duration: 1000 });
}
function forward(_id) {
uni.showToast({ title: "转发", icon: "none", duration: 1000 });
}
function iconClick(type, id) {
datas[type] = (datas[type] || []);
let index = datas[type]?.indexOf(id);
if (index >= 0) {
datas[type].splice(index, 1);
} else {
datas[type].push(id);
}
}
function ({ video }) {
console.log(video?.videoId);
iconClick(0, video?.videoId);
}
function loadmore() {
uni.showToast({ title: "加载更多", icon: "none", duration: 1000 });
for (var i = 0; i < 2; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
}
onMounted(() => {
// 这里直接生成 200 条视频数据进行测试(模拟请求后台获取数据)
uni.showToast({ title: "加载中...", icon: "loading", duration: 1500 });
setTimeout(() => {
let size = type.value == 'v2' ? 2 : 50;
for (var i = 0; i < size; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
uni.hideToast();
}, 1500);
});
const getList = () => {
return [
{
videoId: lists.value.length + 1,
title: `抖音美女主播,JK超短裙学生妆美女跳舞展示,爱了爱了。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/11/08/19/BMjAyMDExMDgxOTQxNTlfNTIzNDczMzQ0XzM4OTQ1MDk5MTI4XzFfMw==_b_Bc770a92f0cf153407d60a2eddffeae2a.mp4",
uploadTime: "2023-11-08 19:41",
ipLocation: "上海",
author: {
authorId: 101,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "陌路",
genderName: "男"
}
},
{
videoId: lists.value.length + 2,
title: `御姐美女抖音作品,来个自拍视频把,好美啊。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/10/02/09/BMjAyMDEwMDIwOTAwMDlfMTIyMjc0NTk0Ml8zNjk3Mjg0NjcxOF8xXzM=_b_B28a4518e86e2cf6155a6c1fc9cf79c6d.mp4",
uploadTime: "2023-10-02 09:41",
ipLocation: "贵州",
author: {
authorId: 102,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "御姐呀",
genderName: "女"
}
},
{
videoId: lists.value.length + 3,
title: `抖音主播可爱妹子新学的舞蹈,超可爱的美女主播。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov6.a.yximgs.com/upic/2020/08/23/00/BMjAyMDA4MjMwMDMyNDRfMTYzMzY5MDA0XzM0ODI4MDcyMzQ5XzFfMw==_b_B9a1c9d4e3a090bb2815994d7f33a906a.mp4",
uploadTime: "2023-08-23 00:41",
ipLocation: "广州",
author: {
authorId: 103,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "野花猫",
genderName: "女"
}
},
{
videoId: lists.value.length + 4,
title: `多个美女带着遮阳帽出去散步自拍视频,好好看。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://alimov2.a.yximgs.com/upic/2020/07/02/14/BMjAyMDA3MDIxNDUyMDlfOTExMjIyMjRfMzE1OTEwNjAxNTRfMV8z_b_Bf3005d42ce9c01c0687147428c28d7e6.mp4",
uploadTime: "2023-07-02 14:41",
ipLocation: "山西",
author: {
authorId: 104,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "蓝姬",
genderName: "女"
}
}
];
}
</script>
<style scoped lang="scss">
.right {
width: 50px;
padding: 10px 0;
margin: 0;
}
.userAvatar {
width: 40px;
height: 40px;
border-radius: 100;
}
.icon {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
margin: 8px 0;
}
.icon-val {
color: #fff;
font-size: 13px;
text-align: center;
}
.bottom {
margin: 10px;
}
.title {
color: #fff;
font-size: 13px;
}
</style>
完整参数事件
以下是 ml-swiper-v3
中 所有的 参数、事件和方法的完整示例
<template>
<ml-swiper-v2
v-if="lists && lists.length > 0"
ref="mlSwiperRef"
:list="lists"
:width="width"
:height="height"
:criticalVal="criticalVal"
:progress="progress"
:duration="duration"
@onchange="onchange"
@loadmore="loadmore"
@onclick="onclick"
@ondblclick=""
@onplay="onplay"
@onpause="onpause"
@onended=""
@ontimeupdate=""
@onwaiting=""
@onerror="onerror"
>
<!-- 自定义内容,这里设置100%后 层级高于video,会导致单击、双击功能失效 -->
<template v-slot:default="{ index }">
<view class="body">
<text class="text">{{ index + 1 }} / {{ lists.length }}</text>
</view>
</template>
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right">
<image class="userAvatar" :src="item?.author?.avatar" @click="avatarClick"></image>
<!-- 喜欢 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0]?.includes(item.videoId) ? '#ff0004' : '#fff'" @tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1]?.includes(item.videoId) ? '#ff0' : '#fff'" @tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom">
<text class="title">{{ item?.title }}</text>
</view>
</template>
</ml-swiper-v2>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
const lists = ref([]); // 视频资源列表 [{url: "资源链接必须", title: "标题", poster: "封面"}]
const windowInfo = uni.getWindowInfo();
const width = ref(windowInfo.windowWidth + 'px'); // 组件宽度(不写默认设备同宽)
const height = ref(windowInfo.windowHeight + 'px'); // 组件高度(不写默认设备同高)
const criticalVal = ref(2); // 临界值(当观看到倒数 2 个视频时,触发加载更多事件,不写默认 2)
const progress = ref(true); // 进度条(不写默认 true,APP 端不显示进度动画)
const duration = ref(true); // 播放时间(不写默认 true,显示播放时间)
const mlSwiperRef = ref(null);
const datas = reactive({});
let count = 0;
let context = null;
onMounted(() => {
// 这里直接生成 200 条视频数据进行测试
for (var i = 0; i < 50; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
});
function avatarClick() {
uni.showToast({ title: "点击头像", icon: "none", duration: 1000 });
}
function comment(_id) {
uni.showToast({ title: "查看评论", icon: "none", duration: 1000 });
fullScreen();
}
function forward(_id) {
uni.showToast({ title: "转发", icon: "none", duration: 1000 });
}
function iconClick(type, id) {
datas[type] = (datas[type] || []);
let index = datas[type]?.indexOf(id);
if (index >= 0) {
datas[type].splice(index, 1);
} else {
datas[type].push(id);
}
}
function onchange(e) {
console.log("滑动事件:", e);
}
/**
* 加载更多数据(请求后台 获取更多数据)
*/
function loadmore() {
uni.showToast({ title: "加载更多", icon: "none", duration: 1000 });
for (var i = 0; i < 2; i++) {
getList().forEach((item) => {
count = count + 1;
item.title = `【${count}】` + item.title;
lists.value.push(item);
});
}
}
function onclick(e) {
console.log("单击事件:", e);
}
function (e) {
console.log("双击事件:", e);
}
function onplay(e) {
console.log("播放事件:", e);
context = e.context;
}
function onpause(e) {
console.log("暂停事件:", e);
}
function (e) {
console.log("结束事件:", e);
}
function (e) {
// console.log("加载事件:", e);
}
function (e) {
console.log("缓冲事件:", e);
}
function onerror(e) {
console.log("报错事件:", e);
}
/**
* 全屏
*/
function fullScreen() {
uni.showToast({ title: "全屏", icon: "none", duration: 1000 });
mlSwiperRef.value?.fullScreen();
// context?.requestFullScreen();
}
/**
* 退出全屏
*/
function exitFullScreen() {
uni.showToast({ title: "退出全屏", icon: "none", duration: 1000 });
mlSwiperRef.value?.exitFullScreen();
// context?.exitFullScreen();
}
/**
* 倍速
* @param {Number} rate 0.5 | 0.8 | 1.0 | 1.25 | 1.5 | 2.0
*/
function setRate(rate) {
uni.showToast({ title: "倍速" + rate, icon: "none", duration: 1000 });
mlSwiperRef.value?.setRate(rate);
// context?.playbackRate(rate);
}
/**
* 跳转到指定位置播放
* @param {Number} val 单位秒
*/
function setSeek(val) {
mlSwiperRef.value?.setSeek(val);
// context?.seek(val);
}
const getList = () => {
return [
{
videoId: lists.value.length + 1,
title: `抖音美女主播,JK超短裙学生妆美女跳舞展示,爱了爱了。`,
poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/11/08/19/BMjAyMDExMDgxOTQxNTlfNTIzNDczMzQ0XzM4OTQ1MDk5MTI4XzFfMw==_b_Bc770a92f0cf153407d60a2eddffeae2a.mp4",
uploadTime: "2023-11-08 19:41",
ipLocation: "上海",
author: {
authorId: 101,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "陌路",
genderName: "男"
}
},
{
videoId: lists.value.length + 2,
title: `御姐美女抖音作品,来个自拍视频把,好美啊。`,
poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/10/02/09/BMjAyMDEwMDIwOTAwMDlfMTIyMjc0NTk0Ml8zNjk3Mjg0NjcxOF8xXzM=_b_B28a4518e86e2cf6155a6c1fc9cf79c6d.mp4",
uploadTime: "2023-10-02 09:41",
ipLocation: "贵州",
author: {
authorId: 102,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "御姐呀",
genderName: "女"
}
},
{
videoId: lists.value.length + 3,
title: `抖音主播可爱妹子新学的舞蹈,超可爱的美女主播。`,
poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov6.a.yximgs.com/upic/2020/08/23/00/BMjAyMDA4MjMwMDMyNDRfMTYzMzY5MDA0XzM0ODI4MDcyMzQ5XzFfMw==_b_B9a1c9d4e3a090bb2815994d7f33a906a.mp4",
uploadTime: "2023-08-23 00:41",
ipLocation: "广州",
author: {
authorId: 103,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "野花猫",
genderName: "女"
}
},
{
videoId: lists.value.length + 4,
title: `多个美女带着遮阳帽出去散步自拍视频,好好看。`,
poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://alimov2.a.yximgs.com/upic/2020/07/02/14/BMjAyMDA3MDIxNDUyMDlfOTExMjIyMjRfMzE1OTEwNjAxNTRfMV8z_b_Bf3005d42ce9c01c0687147428c28d7e6.mp4",
uploadTime: "2023-07-02 14:41",
ipLocation: "山西",
author: {
authorId: 104,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "蓝姬",
genderName: "女"
}
}
];
}
</script>
<style scoped lang="scss">
.body {
position: absolute;
top: 0;
margin: 0 auto;
}
.text {
font-size: 50px;
color: #ff5918;
}
.right {
width: 50px;
padding: 10px 0;
margin: 0 auto;
}
.icon {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
margin: 8px auto;
}
.icon-val {
color: #fff;
font-size: 13px;
text-align: center;
}
.userAvatar {
width: 40px;
height: 40px;
border-radius: 100%;
}
.bottom {
margin: 10px;
}
.title {
color: #fff;
font-size: 13px;
}
</style>
扩展或自定义
扩展或自定义 请阅读本插件中的:抖音官方示例.md
无需扩展和自定义 推荐使用:ml-swiper-v3
vue2 版本示例代码
<template>
<view class="wh-full">
<template v-if="type == 'v2'">
<!-- 这里使用 v-if 是为了防止 请求后台时还没返回数据 导致组件初始化时没有视频资源 而出现错误 -->
<ml-swiper-v2 v-if="lists && lists.length > 0" :list="lists" @loadmore="loadmore" @ondblclick="">
<!-- 自定义内容,这里设置100%后 层级高于video,会导致单击、双击功能失效 -->
<template v-slot:default="{ index }">
<view class="body">
<text class="text">{{ index + 1 }} / {{ lists.length }}</text>
</view>
</template>
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right" v-show="item != null">
<!-- 用户头像 -->
<image v-show="item.author != null" class="userAvatar" :src="item.author.avatar" @click="avatarClick">
</image>
<!-- 点赞 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0].includes(item.videoId) ? '#ff0004' : '#fff'"
@tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1].includes(item.videoId) ? '#ff0' : '#fff'"
@tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom" v-show="item != null">
<text class="title">{{ item.title }}</text>
</view>
</template>
</ml-swiper-v2>
</template>
<template v-if="type == 'v3'">
<!-- 这里使用 v-if 是为了防止 请求后台时还没返回数据 导致组件初始化时没有视频资源 而出现错误 -->
<ml-swiper-v3 v-if="lists && lists.length > 0" :list="lists" @loadmore="loadmore" @ondblclick="">
<!-- 自定义内容,这里设置100%后 层级高于video,会导致单击、双击功能失效 -->
<template v-slot:default="{ index }">
<view class="body">
<text class="text">{{ index + 1 }} / {{ lists.length }}</text>
</view>
</template>
<!-- 右侧 -->
<template v-slot:right="{ item }">
<view class="right" v-show="item != null">
<!-- 用户头像 -->
<image v-show="item.author != null" class="userAvatar" :src="item.author.avatar" @click="avatarClick">
</image>
<!-- 点赞 -->
<view class="icon">
<uni-icons type="heart-filled" size="35" :color="datas[0].includes(item.videoId) ? '#ff0004' : '#fff'"
@tap="iconClick(0, item.videoId)" />
<text class="icon-val">666</text>
</view>
<!-- 评论 -->
<view class="icon">
<uni-icons type="chat-filled" size="35" color="#fff" @tap="comment(item.videoId)" />
<text class="icon-val">668</text>
</view>
<!-- 收藏 -->
<view class="icon">
<uni-icons type="star-filled" size="35" :color="datas[1].includes(item.videoId) ? '#ff0' : '#fff'"
@tap="iconClick(1, item.videoId)" />
<text class="icon-val">888</text>
</view>
<!-- 转发 -->
<view class="icon">
<uni-icons type="redo-filled" size="35" color="#fff" @tap="forward(item.videoId)" />
<text class="icon-val">999</text>
</view>
</view>
</template>
<!-- 底部 -->
<template v-slot:bottom="{ item }">
<view class="bottom" v-show="item != null">
<text class="title">{{ item.title }}</text>
</view>
</template>
</ml-swiper-v3>
</template>
</view>
</template>
<script>
export default {
data() {
return {
lists: [], // 资源数据
datas: [], // 点赞、收藏等数据
count: 0, // 计数器
type: "v2" // 使用的组件版本,v2:ml-swiper-v2、v3:ml-swiper-v3
}
},
onLoad() {
// (模拟请求后台获取数据)
uni.showToast({ title: "加载中...", icon: "loading", duration: 1500 });
setTimeout(() => {
let size = this.type == "v3" ? 50 : 2;
for (var i = 0; i < size; i++) {
this.getList().forEach((item) => {
this.count = this.count + 1;
item.title = `【${this.count}】` + item.title;
this.lists.push(item);
});
}
uni.hideToast();
}, 1500);
},
methods: {
avatarClick() {
uni.showToast({ title: "点击头像", icon: "none", duration: 1000 });
},
comment(_id) {
uni.showToast({ title: "查看评论", icon: "none", duration: 1000 });
},
forward(_id) {
uni.showToast({ title: "转发", icon: "none", duration: 1000 });
},
iconClick(type, id) {
this.datas[type] = (this.datas[type] || []);
let index = this.datas[type]?.indexOf(id);
if (index >= 0) {
this.datas[type].splice(index, 1);
} else {
this.datas[type].push(id);
}
},
loadmore() {
uni.showToast({ title: "加载更多", icon: "none", duration: 1000 });
for (var i = 0; i < 2; i++) {
this.getList().forEach((item) => {
this.count = this.count + 1;
item.title = `【${this.count}】` + item.title;
this.lists.push(item);
});
}
},
({ video }) {
console.log(video?.videoId);
this.iconClick(0, video?.videoId);
},
getList() {
return [
{
videoId: this.lists.length + 1,
title: `抖音美女主播,JK超短裙学生妆美女跳舞展示,爱了爱了。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/11/08/19/BMjAyMDExMDgxOTQxNTlfNTIzNDczMzQ0XzM4OTQ1MDk5MTI4XzFfMw==_b_Bc770a92f0cf153407d60a2eddffeae2a.mp4",
uploadTime: "2023-11-08 19:41",
ipLocation: "上海",
author: {
authorId: 101,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "陌路",
genderName: "男"
}
},
{
videoId: this.lists.length + 2,
title: `御姐美女抖音作品,来个自拍视频把,好美啊。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov2.a.yximgs.com/upic/2020/10/02/09/BMjAyMDEwMDIwOTAwMDlfMTIyMjc0NTk0Ml8zNjk3Mjg0NjcxOF8xXzM=_b_B28a4518e86e2cf6155a6c1fc9cf79c6d.mp4",
uploadTime: "2023-10-02 09:41",
ipLocation: "贵州",
author: {
authorId: 102,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "御姐呀",
genderName: "女"
}
},
{
videoId: this.lists.length + 3,
title: `抖音主播可爱妹子新学的舞蹈,超可爱的美女主播。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://txmov6.a.yximgs.com/upic/2020/08/23/00/BMjAyMDA4MjMwMDMyNDRfMTYzMzY5MDA0XzM0ODI4MDcyMzQ5XzFfMw==_b_B9a1c9d4e3a090bb2815994d7f33a906a.mp4",
uploadTime: "2023-08-23 00:41",
ipLocation: "广州",
author: {
authorId: 103,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "野花猫",
genderName: "女"
}
},
{
videoId: this.lists.length + 4,
title: `多个美女带着遮阳帽出去散步自拍视频,好好看。`,
// poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
url: "https://alimov2.a.yximgs.com/upic/2020/07/02/14/BMjAyMDA3MDIxNDUyMDlfOTExMjIyMjRfMzE1OTEwNjAxNTRfMV8z_b_Bf3005d42ce9c01c0687147428c28d7e6.mp4",
uploadTime: "2023-07-02 14:41",
ipLocation: "山西",
author: {
authorId: 104,
avatar: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
nickName: "蓝姬",
genderName: "女"
}
}
];
}
}
}
</script>
<style scoped>
.body {
position: absolute;
top: 0;
margin: 0 auto;
}
.text {
font-size: 50px;
color: #ff5918;
}
.right {
width: 50px;
padding: 10px 0;
margin: 0 auto;
}
.icon {
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: center;
margin: 8px auto;
}
.icon-val {
color: #fff;
font-size: 13px;
text-align: center;
}
.userAvatar {
width: 40px;
height: 40px;
border-radius: 100%;
}
.bottom {
margin: 10px;
}
.title {
color: #fff;
font-size: 13px;
}
.wh-full {
width: 100%;
height: 100%;
padding: 0;
margin: 0 auto;
overflow: hidden;
}
</style>