項目概述
Vue3DouYin短視頻 基於vite2+vue3.x+vant3+vuex4+v3popup
等技術開發的高仿抖音app|微信直播
實例項目。實現上下左右滑動切換小視頻、點贊/評論、聊天、送禮物/彈幕/紅包等功能。
有贊移動端vue3組件庫中的swipe
組件實現小視頻滑動切換效果。
實現小視頻播放/暫停、點贊/評論等功能。
運用技術
- 編碼器:Vscode
- 技術框架:vue3.x+vuex4.x+vue-router4
- 組件庫:vant^3.0.4 (有贊移動端vue3.x組件庫)
- 彈層組件:v3popup(vue3自定義彈出框組件)
- 字體圖標:阿里iconfont圖標庫
- 導航欄+標籤欄:基於vue3自定義navbar/tabbar組件
- 構建工具:vite2 (新一代web構建工具)
項目結構目錄
vue3全局自定義手機端彈框
vue3-douyin項目中的所有彈窗場景均是自定義彈框組件實現。
至於彈框實現,這裏不作介紹,之前有過一篇歷史分享文章。
vue3.0自定義組件系列:vue3全局mobile彈窗組件v3popup
vue3主入口配置
在main.js中配置一些路由/狀態管理、公共組件引入。
import { createApp } from 'vue'
import App from './App.vue'
// 引入Router及Vuex
import router from './router'
import store from './store'
// 引入公用組件
import Plugins from './plugins'
// 引入Js
import '@/assets/js/fontSize'
// 引入公用樣式
import '@/assets/fonts/iconfont.css'
import '@/assets/css/reset.css'
import '@/assets/css/layout.css'
const app = createApp(App)
app.use(router)
app.use(store)
app.use(Plugins)
app.mount('#app')
vite.config.js配置
使用vite構建工具搭建的項目,會自動生成一個vite.config.js配置文件。進行一些簡單的環境及路徑別名配置。
/**
* Vite2項目配置
*/
import vue from '@vitejs/plugin-vue'
import path from 'path'
/**
* @type {import('vite').UserConfig}
*/
export default {
plugins: [vue()],
build: {
// 基本目錄
// base: '/',
/**
* 輸出文件目錄
* @default dist(默認)
*/
// outDir: 'target',
},
// 環境配置
server: {
// 自定義接口
port: 3000,
// 是否自動瀏覽器打開
open: false,
// 是否開啓https
https: false,
// 服務端渲染
ssr: false,
// 代理配置
proxy: {
// ...
}
},
// 設置路徑別名
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@views': path.resolve(__dirname, './src/views')
}
}
vue3小視頻功能實現
小視頻頁面總體分爲底部導航、視頻信息區、底部tab三個部分。
<template>
<div class="bg-161823">
<!-- >>頂部NavBar -->
<navbar :back="false" bgcolor="transparent" transparent>
<template v-slot:title>
...
</template>
<template v-slot:right><div><i class="iconfont icon-search"></i></div></template>
</navbar>
<!-- >>主面板 -->
<div class="vui__scrollview flex1">
<div class="vui__swipeview">
<!-- ///滑動切換區 -->
<van-swipe ref="swipeHorizontalRef" :show-indicators="false" :loop="false" @change="handleSwipeHorizontal">
<van-swipe-item v-for="(item,index) in videoLs" :key="index">
<template v-if="item.category == 'nearby'">
<div class="swipe__nearLs">
...
</div>
</template>
<template v-if="item.category == 'recommend' || item.category == 'follow'">
<van-swipe vertical lazy-render :show-indicators="false" :loop="false" @change="handleSwipeVertical">
<van-swipe-item v-for="(item2, index2) in item.list" :key="index2">
<!-- ///視頻模塊 -->
<div class="swipe__video">
<video class="vdplayer" :id="'vd-'+index+'-'+index2" loop preload="auto"
:src="item2.src"
:poster="item2.poster"
webkit-playsinline="true"
x5-video-player-type="h5-page"
x5-video-player-fullscreen="true"
playsinline
@click="handleVideoClicked"
>
</video>
<span v-show="!isPlay" class="btn__play" @click="handleVideoClicked"><i class="iconfont icon-bofang"></i></span>
</div>
<!-- ///信息模塊 -->
<div class="swipe__vdinfo flexbox flex-col">
<div class="flexbox flex-alignb">
<!-- ///底部信息欄 -->
<div class="swipe__footbar flex1">
<div v-if="item2.ads" class="item swipe__superlk ads" @click="handleOpenLink(item2)">
<i class="iconfont icon-copylink fs-28"></i>查看詳情<i class="iconfont icon-arrR fs-24"></i>
</div>
<div v-if="item2.collectionLs&&item2.collectionLs.length>0" class="item swipe__superlk">
<i class="iconfont icon-copylink fs-24 mr-10"></i><div class="flex1">合集《小鬼當家》主演花絮</div><i class="iconfont icon-arrR fs-24"></i>
</div>
<div class="item uinfo flexbox flex-alignc">
<router-link to="/friend/uhome"><img class="avatar" :src="item2.avatar" /></router-link>
<router-link to="/friend/uhome"><em class="name">{{item2.author}}</em></router-link>
<button class="btn vui__btn vui__btn-primary" :class="item2.isFollow ? 'isfollow' : ''" @click="handleIsFollow(item.category, index2)">{{item2.isFollow ? '已關注' : '關注'}}</button>
</div>
<div class="item at">@{{item2.author}}</div>
<div v-if="item2.topic" class="item kw"><em v-for="(kw,idx) in item2.topic" :key="idx">#{{kw}}</em></div>
<div class="item desc">{{item2.desc}}</div>
</div>
<!-- ///右側工具欄 -->
<div class="swipe__toolbar">
...
</div>
</div>
</div>
</van-swipe-item>
</van-swipe>
</template>
</van-swipe-item>
</van-swipe>
<!-- ///底部進度條 -->
<div class="swipe__progress"><i class="bar" :style="{'width': vdProgress+'%'}"></i></div>
</div>
</div>
<!-- >>底部TabBar -->
<tabbar
bgcolor="linear-gradient(to bottom, transparent, rgba(0,0,0,.6))"
color="rgba(255,255,255,.6)"
activeColor="#fff"
fixed
/>
<!-- ……商品模板 -->
<v3-popup v-model="isShowGoodsPopup" position="bottom" round xclose title="熱銷商品" @end="handlePopStateClose" opacity=".2">
<div v-if="goodsLs" class="wrap_goodsList">
...
</div>
</v3-popup>
<!-- ……評論列表模板 -->
<v3-popup v-model="isShowReplyPopup" position="bottom" round xclose opacity=".2">
<div class="nt__commentWrap">
<!-- 評論列表 -->
...
</div>
</v3-popup>
<!-- ……評論編輯器模板 -->
<v3-popup v-model="isShowReplyEditor" position="bottom" opacity=".2">
<div class="vui__footTool nt__commentWrap">
...
</div>
</v3-popup>
<!-- ……分享模板 -->
<v3-popup v-model="isShowSharePopup" anim="footer" type="actionsheet" round xclose opacity=".2"
title="<div style='text-align:left;'>分享至</div>"
:btns="[
{text: '取消', style: 'color:#999;', click: () => isShowSharePopup=false},
]"
>
...
</v3-popup>
</div>
</template>
<script>
/**
* @Desc Vue3.0實現小視頻功能
* @Time andy by 2021-02
* @About Q:282310962 wx:xy190310
*/
import { onMounted, onUnmounted, ref, reactive, toRefs, inject, nextTick } from 'vue'
import CmtEditor from '@components/cmtEditor.vue'
import videoJSON from '@/mock/videolist.js'
import emojJSON from '@/mock/cmt-emoj.js'
export default {
components: {
CmtEditor,
},
setup() {
// 定時器
const vdTimer = ref(null)
const tapTimer = ref(null)
const swipeHorizontalRef = ref(null)
const editorRef = ref(null)
const v3popup = inject('v3popup')
const data = reactive({
// ...
})
onMounted(() => {
swipeHorizontalRef.value.swipeTo(data.activeNav, {immediate: true})
// ...
})
// ...
// 垂直切換頁面事件
const handleSwipeVertical = (index) => {
if(data.activeNav == 0) {
// 附近頁
data.activeOneIdx = index
}else if(data.activeNav == 1) {
// 關注頁
data.activeTwoIdx = index
// console.log('關注頁索引:' + index)
}else if(data.activeNav == 2) {
// 推薦頁
data.activeThreeIdx = index
// console.log('推薦頁索引:' + index)
}
vdTimer.value && clearInterval(vdTimer.value)
data.vdProgress = 0
data.isPlay = false
let video = getVideoContext()
if(!video) return
video.pause()
// 重新開始
video.currentTime = 0
data.activeSwipeIndex = index
// 自動播放下一個
handlePlay()
}
// 播放
const handlePlay = () => {
console.log('播放視頻...')
let video = getVideoContext()
if(!video) return
video.play()
data.isPlay = true
// 設置進度條
vdTimer.value = setInterval(() => {
handleProgress()
}, 16)
}
// 暫停
const handlePause = () => {
console.log('暫停視頻...')
let video = getVideoContext()
if(!video) return
video.pause()
data.isPlay = false
vdTimer.value && clearInterval(vdTimer.value)
}
// 視頻點擊事件(判斷單/雙擊)
const handleVideoClicked = () => {
console.log('觸發視頻點擊事件...')
tapTimer.value && clearTimeout(tapTimer.value)
data.clickNum++
tapTimer.value = setTimeout(() => {
if(data.clickNum >= 2) {
console.log('雙擊事件')
}else {
console.log('單擊事件')
if(data.isPlay) {
handlePause()
}else {
handlePlay()
}
}
data.clickNum = 0
}, 300)
}
// 播放進度條
const handleProgress = () => {
let video = getVideoContext()
if(!video) return
let curTime = video.currentTime.toFixed(1)
let duration = video.duration.toFixed(1)
data.vdProgress = parseInt((curTime / duration).toFixed(2) * 100)
}
// ...
return {
...toRefs(data),
swipeHorizontalRef,
editorRef,
// ...
}
}
}
</script>
另外底部迷你進度條功能是使用視頻時長、當前播放進度轉換爲百分比,加上css3來控制。每一次切換視頻都會重置進度條狀態。
直播頁面仿製了微信直播界面,新增彈幕功能,設置了三條航道滾動顯示。
<!-- //送禮物模板 -->
<v3-popup v-model="isShowGiftPopup" position="bottom" round popupStyle="background:#36384a;">
<div class="wrap_giftList">
<div class="gt__hdtit flex-c">
<i class="back iconfont icon-close" @click="isShowGiftPopup=false"></i>
<div class="flex1">贈送禮物</div>
<div class="num" @click="isShowRechargePopup=true"><i class="iconfont icon-douzi fs-24"></i> 0 <i class="iconfont icon-arrR fs-24"></i></div>
</div>
<div class="gt__swipe">
<!-- <div class="gtitem">
<div class="inner flex-c flex-col">
<img class="gtimg" src="/static/gift/gift-img22.png" />
<p class="gtlbl">鼓掌</p>
<p class="gtnum"><i class="iconfont icon-douzi"></i> 166</p>
</div>
</div> -->
<div class="gtitem" :class="giftCur == index ? 'on' : ''" v-for="(item,index) in giftLs" :key="index" @click="handleGiftClicked(item, index)">
<div class="inner flex-c flex-col">
<img class="gtimg" :src="item.giftPic" />
<p class="gtlbl">{{item.giftLabel}}</p>
<p class="gtnum"><i class="iconfont icon-douzi"></i> {{item.giftCoins}}</p>
</div>
</div>
</div>
</div>
</v3-popup>
<!-- //充值模板 -->
<v3-popup v-model="isShowRechargePopup" position="bottom" round popupStyle="background:#36384a;" opacity="0">
<div class="wrap_giftList">
<div class="gt__hdtit flex-c">
<i class="back iconfont icon-arrD" @click="isShowRechargePopup=false"></i>
<div class="flex1">選擇充值金額</div>
<div class="num"><i class="iconfont icon-douzi fs-24"></i> 0</div>
</div>
<div class="gt__swipe gt__recharge">
<div class="gtitem" :class="rechargeIdx == index ? 'cur' : ''" v-for="(item,index) in rechargeLs" :key="index" @click="handleRecharge(index)">
<div class="inner flex-c flex-col">
<p class="gtcoins"><i class="iconfont icon-douzi"></i> {{item.gtcoins}}</p>
<p class="gtmoney">售價{{item.gtmoney}}元</p>
</div>
</div>
<div class="pad10"><button class="vui__btn vui__btn-primary" style="border-radius:.1rem;height:40px;" @click="isShowSubmitRecharge=true">確認支付(¥{{rechargeLs[rechargeIdx].gtmoney}})</button></div>
</div>
</div>
</v3-popup>
ok,使用vue3+vant3開發短視頻/直播項目就分享到這裏。希望對大家有些幫助哈!✏️💪🏻