10

Vue3.0短视频+直播|vue3+vite2+vant3仿抖音界面|vue3.x小视频实例

 3 years ago
source link: http://www.cnblogs.com/xiaoyan2017/p/14361160.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

基于 vue3.0 构建移动端仿抖音/快手短视频+直播实战项目 Vue3-DouYin

5G时代已来,短视频也越来越成为新一代年轻人的娱乐方式,在这个特殊之年,又将再一次成为新年俗!

基于 vue3.x+vite2+vuex4+vue-router+vant3+v3popup 等技术搭建开发仿抖音App界面 小视频/直播/聊天 实例项目。实现 短视频上下左右滑动切换、点赞/评论/聊天/红包及送礼物 等功能。

Uvq6BrN.png!mobile

一、运用技术

  • 编码器:VScode/Notepad++
  • 使用技术:Vue3.x+Vuex4.x+Vue-Router4
  • 组件库:Vant^3.0.4 (有赞移动端vue3组件库)
  • 弹层组件:V3Popup(基于vue3自定义弹层组件)
  • 字体图标:阿里iconfont图标库
  • 导航栏+标签栏:基于vue3自定义navbar/tabbar组件

ieMNj2N.gif!mobile

二、项目目录结构

zAzUveB.png!mobile

◆ 效果预览

uQzE3mM.png!mobile

7jy2Ezz.png!mobile

neqA3qJ.png!mobile

7RfY3iZ.png!mobile

6N7rQ3.png!mobile

meQnuaN.png!mobile

FvQBRz3.png!mobile

IBFJ3qZ.png!mobile

RRNJ73B.png!mobile

FzqMFnN.png!mobile

YFFBfmM.png!mobile

JN36Fbm.png!mobile

eA3yQzM.png!mobile

uM326fM.png!mobile

Z3umaqN.png!mobile

n6rQbyu.png!mobile

NnqM3qV.png!mobile

uAFBNvB.png!mobile

fmUrum6.png!mobile

uyumiaE.png!mobile

◆ vue3.x自定义顶部导航+标签栏

项目中所有顶部导航及底部tabbar均是使用vue3自定义组件来实现效果,支持自定义插槽内容。

Mb6fY3n.png!mobile

rYfeMzQ.png!mobile

eiuIFnQ.png!mobile

<navbar :back="false" bgcolor="transparent" transparent>
    <template v-slot:title>
        <div class="navbar__tab">
            ...
        </div>
    </template>
    <template v-slot:right><div><i class="iconfont icon-search"></i></div></template>
</navbar>

<tabbar
    bgcolor="linear-gradient(to bottom, transparent, rgba(0,0,0,.6))" 
    color="rgba(255,255,255,.6)" 
    activeColor="#fff" 
    fixed
/>

◆ vue3.x全局弹出层组件

项目中所有的弹框应用场景均是之前开发的一款vue3自定义组件v3popup来实现功能。

fUJNJvQ.png!mobile

vue3版的自定义弹框组件,拥有20+种自定义参数配置,多种弹框类型及动画效果。

https://www.cnblogs.com/xiaoyan2017/p/14210820.html

◆ vite.config.js配置文件

一些简单的vite2项目配置,可进行一些常用环境及alias路径别名设置。

/**
 * 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')
  }
}

◆ 引入公共组件

让项目代码更加整洁,在plugins.js中配置一些公共组件,然后在main.js中引入即可。

/**
 * 引入公共组件
 */

// 引入Vant3.x组件库
import Vant from 'vant'
import 'vant/lib/index.css'

// 引入Vue3.x移动端弹层组件
import V3Popup from '@components/v3popup'

import NavBar from '@components/navBar.vue'
import TabBar from '@components/tabBar.vue'

import Utils from './utils'
import Storage from './storage'

const Plugins = (app) => {
    app.use(Vant)
    app.use(V3Popup)

    // 注册公用组件
    app.component('navbar', NavBar)
    app.component('tabbar', TabBar)

    app.provide('utils', Utils)
    app.provide('storage', Storage)
}

export default Plugins

◆ vue3.x表单验证+60s倒计时

<!-- //注册表单模板 -->
<template>
    <div>
        <div class="vui__scrollview vui__scrollview-lgreg flex1">
            <div class="nt__lgregPanel">
                <div class="lgreg-header">
                    <div class="slogan">
                        <img class="logo" src="/static/logo.png" />
                        <p class="text ff-gg">Vue3.0-DouYin</p>
                    </div>
                    <div class="forms">
                        <form @submit.prevent="handleSubmit">
                            <div class="item flexbox flex_alignc">
                                <input class="iptxt flex1" type="text" v-model="formObj.tel" placeholder="请输入手机号" maxlength="11" />
                            </div>
                            <div class="item flexbox flex_alignc">
                                <input class="iptxt flex1" type="password" v-model="formObj.pwd" placeholder="请输入密码" />
                            </div>
                            <div class="item flexbox flex_alignc">
                                <input class="iptxt flex1" type="text" v-model="formObj.vcode" placeholder="验证码" />
                                <button class="btn-getcode" @click.prevent="handleVcode" :disabled="disabled">{{vcodeText}}</button>
                            </div>
                            <div class="item btns">
                                <button class="flex-c" type="submit"><i class="iconfont icon-go c-fff"></i></button>
                            </div>
                            <div class="item lgreg-lk">
                                <router-link class="navigator" to="/login">已有账号,去登录</router-link>
                            </div>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>
<script>
import { reactive, toRefs, inject, getCurrentInstance } from 'vue'
export default {
    components: {},
    setup() {
        const { ctx } = getCurrentInstance()

        const v3popup = inject('v3popup')

        const utils = inject('utils')

        const formObj = reactive({})
        const data = reactive({
            vcodeText: '获取验证码',
            disabled: false,
            time: 0,
        })

        const VTMsg = (content) => {
            v3popup({
                content: `<div style='text-align:center;'><i class='iconfont icon-error'></i> ${content}</div>`,
                popupStyle: 'background:#ffefe6;color:#fe2c55;',
                position: 'top',
                time: 2
            })
        }

        const handleSubmit = () => {
            if(!formObj.tel){
                VTMsg('手机号不能为空!')
            }else if(!utils.checkTel(formObj.tel)){
                VTMsg('手机号格式不正确!')
            }else if(!formObj.pwd){
                VTMsg('密码不能为空!')
            }else if(!formObj.vcode){
                VTMsg('验证码不能为空!')
            }else{
                // ...
            }
        }

        // 倒计时
        const handleVcode = () => {
            if(!formObj.tel) {
                VTMsg('手机号不能为空!')
            }else if(!utils.checkTel(formObj.tel)) {
                VTMsg('手机号格式不正确!')
            }else {
                data.time = 60
                data.disabled = true
                countDown()
            }
        }
        const countDown = () => {
            if(data.time > 0) {
                data.vcodeText = '获取验证码('+ data.time +')'
                data.time--
                setTimeout(countDown, 1000)
            }else{
                data.vcodeText = '获取验证码'
                data.time = 0
                data.disabled = false
            }
        }

        return {
            formObj,
            ...toRefs(data),
            handleSubmit,
            handleVcode
        }
    }
}
</script>

◆ vue3.x实现小视频功能

小视频页面使用了有赞组件库中的 swipe 组件来实现滑动切换,开启 lazy-render 让滑动更加流畅。

qMbqauf.gif!mobile

<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 v-if="item2.goods&&item2.goods.length>0" class="item ball flexbox" @click="handleOpenGoods(item2.goods)"><i class="ico iconfont icon-cart"></i></div>
                                    <div class="item" @click="handleIsLike(item.category, index2)"><i class="ico iconfont icon-like" :class="item2.isLike ? 'islike' : ''"></i><p class="num">{{item2.likeNum+(item2.isLike ? 1 : 0)}}</p></div>
                                    <div class="item" @click="isShowReplyPopup=true"><i class="ico iconfont icon-liuyan"></i><p class="num">{{item2.replyNum}}</p></div>
                                    <div class="item" @click="isShowSharePopup=true"><i class="ico iconfont icon-fenxiang"></i><p class="num">{{item2.shareNum}}</p></div>
                                </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>
<script>
import { onMounted, onUnmounted, ref, reactive, toRefs, inject, nextTick } from 'vue'

import CmtEditor from '@components/cmtEditor.vue'

// ...

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 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)
        }

        // ...

        // 打开链接
        const handleOpenLink = (item) => {
            // 监听路由地址栈
            handlePopStateOpen()

            data.isShowLinkPopup = true
            data.linkSrc = item.ads
            data.linkTitle = item.adstitle ? item.adstitle : '网址链接'
        }

        return {
            ...toRefs(data),
            swipeHorizontalRef,
            editorRef,

            handleTabNav,
            handleSwipeHorizontal,
            handleSwipeVertical,
            handlePlay,
            handlePause,
            handleVideoClicked,

            // ...
        }
    }
}
</script>

UVrqm2B.png!mobile

至于项目中的聊天模块就不详细介绍了,之前有分享过一篇vue3.0开发移动端聊天实例项目,感兴趣的可以去看看哈~~

https://www.cnblogs.com/xiaoyan2017/p/14250798.html

◆ vue3.x弹幕功能简单实现

直播页面在小视频页面功能基础上新增 弹幕,滚动消息区、送礼物、充值弹窗 等功能。

fEBBRvA.png!mobile

2eERj2A.png!mobile

弹幕功能的简单实现,共有 3 条滚动路线。

const data = reactive({
    // ...

    // 弹幕队列
    idx: 2,
    dmLs: [
        ...
    ],
    // 正在执行的弹幕队列
    dmActiveLs: []
})

onMounted(() => {
    // ...

    // 装载弹幕
    setInterval(() => {
        starDanMu()
    }, 1500)
})

const starDanMu = () => {
    let query = null
    if(!query) {
        query = data.dmLs.shift()
    }
    if(query) {
        query.row = data.idx
        data.idx = (data.idx % 3 + 1)
        data.dmActiveLs.push(query)
    }
}
<div class="lv__wrap-danmu">
    <div class="danmu__bx">
        <div class="danmu__ls" v-for="item in dmActiveLs" :key="item.id" :data-row="item.row" @animationend="dmAnimationEnd">
            <div class="item">
                <img class="avatar" :src="item.avatar" />
                <p class="name">{{item.name}}</p><p class="desc">{{item.desc}}</p>
            </div>
        </div>
    </div>
</div>

OK,以上就是使用vue3.x+vite2开发仿抖音小视频/直播的一些分享,希望对大家有些帮助哈~~ ✍ :blush:

最后附上一个vue3网页版聊天项目

https://www.cnblogs.com/xiaoyan2017/p/14307849.html

ZzYJfan.gif!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK