Vue3+Vant4实现抖音短视频功能
陌路短视频
陌路短视频技术栈:
Vue3、Vant4
效果展示
添加关注
点击
+
关注作者,关注完成后点击√
则取消关注
点赞、收藏、添加关注功能展示
关注:点击+
关注,点击√
取消关注
取消|点赞
点击点赞,再次点击取消点赞
点赞:点击
❤
点赞,再次点击取消点赞
收藏:点击☆
收藏,再次点击取消收藏
发表评论
点击评论,可以查看评论内容
下方输入评论内容,点击评论即可发表评论
删除评论
点击发表的评论,选择删除可以删除此评论内容
用户只能删除自己的评论内容,无法操作其他人的评论
准备开发
创建项目
vue create molu-vue3-short-video
初始化项目
npm install
运行项目
npm run serve
编译打包项目
npm run build
安装组件
npm i vant
使用组件
在
main.js
中导入组件(这里是全局注册,也可以局部注册使用)
main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 引入组件
import 'vant/lib/index.css'
import * as vant from 'vant'
// 使用组件
createApp(App).use(vant).use(store).use(router).mount('#app')
视图页面
项目主页
应用主题:使用
<van-config-provider theme="dark">
将项目主题变为深色
底部标签:使用Vant4
提供的<van-tabbar>
作为底部导航标签
完整代码如下:
App.vue
<template>
<van-config-provider theme="dark">
<van-tabbar v-model="active" @change="onChange">
<van-tabbar-item to="/">首页</van-tabbar-item>
<van-tabbar-item to="/friend">朋友</van-tabbar-item>
<van-tabbar-item><span class="ud—item__icon"></span></van-tabbar-item>
<van-tabbar-item to="/message">消息</van-tabbar-item>
<van-tabbar-item to="/center">我</van-tabbar-item>
</van-tabbar>
<router-view />
</van-config-provider>
</template>
<script setup>
import { ref } from "vue";
import { showToast, showNotify, closeNotify } from "vant";
const active = ref(0);
const onChange = (index) => {
active.value = index;
showToast(
index == 0
? "首页推荐"
: index == 1
? "朋友动态"
: index == 2
? "发布视频"
: index == 3
? "消息中心"
: "个人主页"
);
closeNotify();
if (index != 0) {
showNotify({ type: "primary", message: "暂未开通此功能" });
}
};
</script>
<style>
.van-theme-dark body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
overflow: hidden;
background-color: #1c1c1e;
-webkit-text-size-adjust: none;
}
.ud—item__icon {
background-color: aliceblue;
}
.ud—item__icon::before {
content: "➕";
}
.container {
width: 100%;
height: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
:root:root {
--van-dialog-background: #f5f5f5;
--van-button-default-background: #f5f5f5;
--van-button-default-color: #ff5d5d ;
}
</style>
首页推荐
滑动组件:使用
Vant4
提供的<van-swipe>
来实现上下滑动效果
视频组件:使用原生<video>
标签实现视频的加载、播放和暂停
弹框组件::使用Vant4
提供的<van-popup>
弹窗查看评论信息
以下是完整的代码实现:
HomeView.vue
<template>
<div :style="{ height: height + 'px', width: width + 'px', overflow: 'hidden' }" >
<van-swipe
:loop="false"
vertical
:height="height"
:width="width"
:show-indicators="false"
@change="onChange"
:style="{ height: height + 'px' }"
>
<van-swipe-item v-for="(item, index) in videoList" :key="index" class="swiperItem" >
<div @click="onVideoClick(item, index)" z-index="1" class="swiperItemVideo">
<video
class="videoPlayer"
:id="'videoPlayer_' + index"
ref="player"
:key="index"
:autoplay="true"
:controls="false"
:height="height"
:width="width"
:poster="item.poster"
:src="item.src"
:muted="false"
:loop="true"
preload="preload"
language="zh-CN"
x5-video-player-type="h5"
x5-video-player-fullscreen="true"
x5-video-player-style="position:fixed;z-index:0;"
x5-video-orientation="portraint"
playsinlin
z-index="0"
></video>
</div>
<div class="rightView">
<div class="userAvatar">
<div>
<div style="display: flex" @click="onUserClick(item)">
<van-image
width="40px"
height="40px"
fit="cover"
position="center"
round
:src="item.author.avatar"
/>
</div>
<div class="addFollowIcon" @click="followClick(item.author.authorId)">
<van-icon
v-if="followUsers.includes(item.author.authorId)"
name="success"
color="#fff"
size="14"
style="font-weight: bold"
/>
<van-icon
v-else
name="plus"
color="#fff"
size="13"
style="font-weight: bold"
/>
</div>
</div>
</div>
<div>
<div>
<van-icon
name="like"
size="30"
@click="likeClick(item, index)"
:color="likeCollect.includes(item.videoId) ? '#ed4809' : ''"
/>
</div>
<div>{{ likeCount }}</div>
</div>
<div>
<div>
<van-icon
name="chat"
size="30"
@click="commentClick(item, index)"
/>
</div>
<div>{{ commentList.length||1 }}</div>
</div>
<div>
<div>
<van-icon
name="star"
size="30"
@click="startClick(item, index)"
:color="startCollect.includes(item.videoId) ? '#f2a808' : ''"
/>
</div>
<div>{{ starCount }}</div>
</div>
<div>
<div><van-icon name="share" size="30" @click="shareClick(item, index)" /></div>
<div>{{ shareCount }}</div>
</div>
</div>
<div class="videoInfo">
<div class="videoAuthor">@{{ item.author.nickName }}</div>
<div class="videoTitle">{{ item.title }}</div>
</div>
</van-swipe-item>
</van-swipe>
<van-popup
class="viewPopup"
v-model:show="showBottomPopup"
position="bottom"
:style="{ height: '60%' }"
closeable
close-icon="close"
round
@close="showBottomPopup = false"
>
<div class="videoCommentTitle">评论列表 {{commentList.length||1}} 条</div>
<div class="videoCommentList">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
error-text="请求失败,点击重新加载"
@load="onLoad"
>
<van-cell v-for="(comment, index) in commentList" :key="comment.commentId" :border="false">
<template #icon>
<van-image
width="30px"
height="30px"
fit="cover"
position="center"
round
:src="comment.userAvatar"
/>
</template>
<template #title>
<div class="videoCommentUser">{{ comment.userNickname }}</div>
</template>
<template #label>
<div v-if="comment.commentUserId==currentUser.id" @click="showPopover = true">
<van-popover v-model:show="showPopover"
theme="dark"
:actions="[{text: '删除'}]"
placement="top"
actions-direction="horizontal"
@select="delComment(comment, index)">
<template #reference>
<div class="videoCommentContent">{{ comment.commentContent }}</div>
<div class="videoCommentTime">{{ comment.commentTime }} · {{ comment.ipLocation }}</div>
</template>
</van-popover>
</div>
<div v-else>
<div class="videoCommentContent">{{ comment.commentContent }}</div>
<div class="videoCommentTime">{{ comment.commentTime }} · {{ comment.ipLocation }}</div>
</div>
</template>
</van-cell>
</van-list>
</div>
<div class="videoCommentInput">
<van-field
v-model="commentContent"
type="textarea"
maxlength="150"
placeholder="请输入评论内容"
:rows="1"
:autosize="{ maxHeight: 60, minHeight: 20 }"
show-word-limit
:maxlength="150"
>
<template #button>
<van-button size="small" type="primary" @click="onCommentClick">评论</van-button>
</template>
</van-field>
</div>
</van-popup>
<van-share-sheet
class="viewPopup"
v-model:show="showShareSheet"
title="立即分享给好友"
:options="shareOptions"
@cancel="showShareSheet = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import { showToast, showNotify, showConfirmDialog } from "vant";
const currentUser = ref({
id: 100,
name: "小明",
avatar: "https://mobilepics.ws.126.net/007939a747ba61f8a0ec74f419c61b430874.jpg",
});
const player = ref(null);
const currentIndex = ref(0);
const playing = ref(true);
const width = ref(window.innerWidth);
const height = ref(window.innerHeight - 50);
const likeCollect = ref([]);
const startCollect = ref([]);
const followUsers = ref([101]);
const showBottomPopup = ref(false);
const showShareSheet = ref(false);
const noMoreFlag = ref(false);
const shareOptions = ref([
[
{ name: "微信", icon: "wechat" },
{ name: "朋友圈", icon: "wechat-moments" },
{ name: "微博", icon: "weibo" },
{ name: "QQ", icon: "qq" },
],
[
{ name: "复制链接", icon: "link" },
{ name: "分享海报", icon: "poster" },
{ name: "二维码", icon: "qrcode" },
{ name: "小程序码", icon: "weapp-qrcode" },
],
]);
const commentList = ref([]);
const likeCount = ref(Math.ceil(Math.random() * 10000));
const starCount = ref(Math.ceil(Math.random() * 1000));
const shareCount = ref(Math.ceil(Math.random() * 1000));
const commentContent = ref("");
const currentVideoInfo = ref({});
const loading = ref(false);
const finished = ref(false);
const showPopover = ref(false);
const tempObject = {};
/**
* 发表评论
*/
const onCommentClick = () => {
if (commentContent.value) {
commentList.value.unshift({
userAvatar: currentUser.value.avatar,
userNickname: currentUser.value.name,
commentUserId: currentUser.value.id,
commentId: Math.ceil(Math.random() * 10000),
commentContent: commentContent.value,
commentTime: new Date().toLocaleString().substring(0,15),
commentVideoId: currentVideoInfo.value.videoId,
ipLocation: "上海",
});
tempObject[currentVideoInfo.value.videoId]=commentList.value;
commentContent.value = "";
showToast("评论成功!");
} else {
showToast("内容不能为空!");
}
};
/**
* 切换视频
*/
const onChange = (index) => {
commentList.value = [];currentVideoInfo.value = {};
const preVideoId = "videoPlayer_" + currentIndex.value;
const preVideo = document.getElementById(preVideoId);
if (preVideo) preVideo.pause();
const nextVideoId = "videoPlayer_" + index;
const nextVideo = document.getElementById(nextVideoId);
if (nextVideo) nextVideo.play();
currentIndex.value = index;
player.value = nextVideo;
getVideoCount(videoList[index]);
if (index == videoList.length - 1) {
showNotify({ type: "danger", message: "没有更多了..." });
noMoreFlag.value = true;
showToast("到底了!");
return false;
}
};
/**
* 获取视频信息
*/
const getVideoCount = (videoInfo) => {
likeCount.value = Math.ceil(Math.random() * 10000);
starCount.value = Math.ceil(Math.random() * 1000);
shareCount.value = Math.ceil(Math.random() * 1000);
};
/**
* 播放|暂停
*/
const onVideoClick = (item, index) => {
const video = document.getElementById("videoPlayer_" + index);
if (video && playing.value) {
video.pause();
playing.value = false;
showToast("已暂停播放");
} else {
video.play();
playing.value = true;
showToast("继续播放");
}
};
/**
* 点赞|取消点赞
*/
const likeClick = (item, index) => {
console.log(index);
if (likeCollect.value.includes(item.videoId)) {
likeCollect.value = likeCollect.value.filter((id) => id != item.videoId);
likeCount.value = likeCount.value - 1;
showToast("已取消点赞");
} else {
likeCollect.value.push(item.videoId);
likeCount.value = likeCount.value + 1;
showToast("已点赞成功");
}
};
/**
* 收藏|取消收藏
*/
const startClick = (item, index) => {
if (startCollect.value.includes(item.videoId)) {
startCollect.value = startCollect.value.filter((id) => id != item.videoId);
starCount.value = starCount.value - 1;
showToast("已取消收藏");
} else {
startCollect.value.push(item.videoId);
starCount.value = starCount.value + 1;
showToast("已添加收藏");
}
};
/**
* 查看评论
*/
const commentClick = (item, index) => {
currentVideoInfo.value = item;
commentList.value = tempObject[item.videoId];
if (!commentList.value || commentList.value.length < 1) {
commentList.value = [{
userAvatar: item.author.avatar,
userNickname: item.author.nickName,
commentUserId: item.author.authorId,
commentId: Math.ceil(Math.random() * 10000),
commentContent: item.title,
commentTime: item.uploadTime,
commentVideoId: item.videoId,
ipLocation: item.ipLocation,
}];
tempObject[item.videoId] = commentList.value;
}
showBottomPopup.value = true;
};
/**
* 分享
*/
const shareClick = (item, index) => {
showShareSheet.value = true;
shareCount.value = shareCount.value + 1;
};
/**
* 关注|取消关注
*/
const followClick = (authorId) => {
if (followUsers.value.includes(authorId)) {
followUsers.value = followUsers.value.filter((id) => id != authorId);
showToast("已取消关注");
} else {
followUsers.value.push(authorId);
showToast("已添加关注");
}
};
/**
* 去用户主页
*/
const onUserClick = (item) => {
showToast("去作者首页");
showNotify({ type: "warning", message: "暂未开通此功能" });
};
/**
* 加载更多评论
*/
const onLoad = () => {
loading.value = true;
setTimeout(() => {
loading.value = false;
finished.value = true;
}, 1000);
};
/**
* 删除我的评论
*/
const delComment = (comment, index) => {
showConfirmDialog({
title: '提示',
message: '是否要删除评论内容?',
}).then(() => {
commentList.value.splice(index, 1);
showToast("删除成功");
}).catch(() => {
showToast("已取消删除");
});
};
onMounted(() => {
const index = currentIndex.value;
const videos = document.querySelectorAll("video");
for (const video of videos) {
let videoId = "videoPlayer_" + index;
if (video && video.id && video.id != videoId) {
video.pause();
} else {
player.value = video;
}
}
});
onBeforeUnmount(() => {
const videos = document.querySelectorAll("video");
for (const video of videos) {
if (video && video.id) {
video.pause();
}
}
});
/**
* 短视频列表
*/
const videoList = [
{
videoId: Date.now() + 1,
title: "抖音美女主播,JK超短裙学生妆美女跳舞展示,爱了爱了。",
poster: "http://img01.sogoucdn.com/app/a/201023/27e5400e26fbef1ea32f9aff60c0b015",
src: "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/4f85fc70df81d04a",
nickName: "陌路",
genderName: "男"
}
},
{
videoId: Date.now() + 2,
title: "御姐美女抖音作品,来个自拍视频把,好美啊。",
poster: "http://img02.sogoucdn.com/app/a/201023/0866f6a339e58d647eb476f72045e980",
src: "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: "http://img02.sogoucdn.com/app/a/201023/0866f6a339e58d647eb476f72045e980",
nickName: "御姐呀",
genderName: "女"
}
},
{
videoId: Date.now() + 3,
title: "抖音主播可爱妹子新学的舞蹈,超可爱的美女主播。",
poster: "https://i02piccdn.sogoucdn.com/2acf176d90718d73",
src: "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: Date.now() + 4,
title: "多个美女带着遮阳帽出去散步自拍视频,好好看。",
poster: "https://i02piccdn.sogoucdn.com/45c34c84c106bbb7",
src: "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/45c34c84c106bbb7",
nickName: "蓝姬",
genderName: "女"
}
},
];
</script>
<style scoped>
.videoInfo {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
color: #f5f5f5;
padding: 5px;
text-align: left;
}
.videoAuthor {
font-size: 15px;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-wrap: break-word;
display: -webkit-box;
overflow: hidden;
}
.videoTitle {
font-size: 13px;
max-height: 50px;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-wrap: break-word;
display: -webkit-box;
overflow: hidden;
}
.swiperItem {
z-index: 9;
}
.swiperItemVideo {
z-index: 1;
}
.videoPlayer {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
object-fit: cover;
object-position: center;
z-index: 0;
}
.rightView {
position: absolute;
right: 0;
top: 35%;
bottom: 25%;
width: 50px;
color: #f5f5f5;
text-align: center;
font-size: 15px;
line-height: 50px;
}
.rightView > div {
margin: 15px auto;
text-align: center;
color: #f5f5f5;
line-height: 15px;
}
.userAvatar {
display: flex;
width: 40px;
height: 40px;
background: aliceblue;
border-radius: 100%;
align-items: center;
justify-content: center;
object-fit: cover;
}
.addFollowIcon {
position: absolute;
top: 45px;
left: 17px;
width: 13px;
height: 13px;
padding: 3px;
border-radius: 100%;
background-color: #ff0000;
color: #fff;
}
.viewPopup {
background-color: #f5f5f5;
}
.videoCommentTitle {
display: flex;
align-items: center;
justify-content: center;
line-height: 40px;
color: #464646;
}
.videoCommentInput {
position: absolute;
background-color: #f5f5f5;
width: 100%;
bottom: 0;
color: #464646;
}
.van-cell {
background-color: #f5f5f5;
color: #464646;
}
::v-deep .van-field__control {
color: #464646;
}
::v-deep .van-share-sheet__cancel {
background-color: #f5f5f5;
}
::v-deep .van-popup {
background-color: #f5f5f5;
}
::v-deep .van-share-sheet__cancel:before {
background-color: #f5f5f5;
}
.videoCommentList {
width: 100%;
height: 75%;
overflow: auto;
}
.videoCommentUser {
margin-left: 8px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.videoCommentContent {
font-size: 12px;
color: #656565;
}
.videoCommentTime{
font-size: 12px;
color: #9e9e9e;
}
</style>
不足和问题
一、数据量大
问题:当视频数据量多是,加载会很卡,展示出来的效果非常的差
解决:可以实时更新videoList
数据,上下滑动时,可以向videoList
增加或移除数据,始终保持videoList
中只存在5条数据。(还有更多方法可以解决这个问题,欢迎各位评论更多解决方案)
二、兼容问题
手机浏览器:不同型号不同机型的手机展示出来的效果不一样,需要做适配
视频层级:手机端会出现video
悬浮在浏览器的最顶层,导致无法上下滑动
(这个可以使用z-index
来控制层架,但是无法解决无法上下滑动的问题)
本人使用苹果手机出现了无法上下滑动的问题,不知道有没有好的解决方案,欢迎评论区留言
三、自动播放
部分无法自动播放:浏览器一般不允许
video
自动播放,但是将muted
设置成静音模式就可以了
此案例使用的是"Google Chrome"浏览器可以正常使用并自动播放
目前不知如何解决手机端浏览器中video
悬浮至顶层的问题,有知道的欢迎评论区留言
完整代码地址
demo地址:https://gitee.com/mmolu/open-source/tree/master/molu-vue3-short-video
说明
本实例仅学习供参考使用,更难并未完善
本示例仅供学习参考
本示例仅供学习参考
本示例仅供学习参考
更多技术分享请关注:.陌路-CSDN博客
创作不易,采集、转发请注明出处:https://blog.csdn.net/qq_51076413?type=blog
更多推荐
所有评论(0)