陌路短视频

陌路短视频技术栈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

GitHub 加速计划 / vu / vue
82
16
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:4 个月前 )
9e887079 [skip ci] 2 个月前
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 6 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐