uni-vue3-welive基于uniapp+vue3直播商城|uniapp仿抖音带货直播视频

2,207 阅读3分钟

最近基于uni-app+vue3+pinia等技术开发一款全新跨多端仿制微信/抖音直播商城项目uni-dyLive。

未标题-3.png

uni-dylive整合uniapp和vue3技术,遵循vue3 setup语法开发模式,搭配uview多端vue3组件库。

未标题-4.png

p2.gif

项目结构

360截图20231231114054154.png

001360截图20231231105504685.png

003360截图20231231105816522.png

004360截图20231231105834371.png

005360截图20231231105903290.png

008360截图20231231110106682.png

011360截图20231231102923855.png

013360截图20231231103044388.png

014360截图20231231103200468.png

017360截图20231231103453073.png

015360截图20231231103242073.png

020360截图20231231104024200.png

022360截图20231231104452867.png

023360截图20231231104551620.png

024360截图20231231104338138.png

026360截图20231231104946521.png

027360截图20231231105057383.png

028360截图20231231105155531.png

uni-dylive短视频/直播页面采用Nvue编码,包含了商品详情/商品下单/订单列表/订单详情/购物车等多种功能。

入口配置

/**
 * 入口配置
 */

import { createSSRApp } from 'vue'
import App from './App'

// 引入Pinia状态管理
import Pinia from '@/store'

// 引入vk-uview-ui组件库
import VKuview from '@/uni_modules/vk-uview-ui'

export function createApp() {
	const app = createSSRApp(App)
	app.use(Pinia)
	app.use(VKuview)
	return {
		app,
		Pinia
	}
}

App.vue模板

<script>
	export default {
		globalData: {
			// 全局设置状态栏和导航栏高度
			statusBarH: 0,
			customBarH: 0,
			screenWidth: 0,
			screenHeight: 0,
			menuBar: null
		},
		onLaunch: function() {
			console.log('App Launch')
			
			// 隐藏系统tabbar
			uni.hideTabBar()
			this.appInit()
		},
		onShow: function() {
			console.log('App Show')
		},
		onHide: function() {
			console.log('App Hide')
		},
		onPageNotFound: function() {
			console.log('Page Not Found', e.path)
			uni.redirectTo({
				url: '/pages/index/index'
			})
		},
		methods: {
			appInit: function() {
				uni.getSystemInfo({
					success: (e) => {
						// 获取手机状态栏信息
						let statusBar = e.statusBarHeight || 0
						let customBar
						let menuBar
						
						// #ifndef MP
						customBar = statusBar + (e.osName === 'android' ? 50 : 45)
						// #endif
						
						// #ifdef MP-WEIXIN
						// 获取微信小程序胶囊按钮信息
						let menu = wx.getMenuButtonBoundingClientRect()
						// 导航栏高度 = 胶囊下距离 + 胶囊上距离 - 状态栏高度
						customBar = menu.bottom + menu.top - statusBar
						menuBar = menu
						// #endif
						
						// // #ifdef MP-ALIPAY
						customBar = statusBar + e.titleBarHeight
						// #endif
						
						// 兼容nvue写法(H5/小程序/APP/APP-Nvue)
						this.globalData.statusBarH = statusBar
						this.globalData.customBarH = customBar
						this.globalData.screenWidth = e.screenWidth
						this.globalData.screenHeight = e.screenHeight
						this.globalData.menuBar = menuBar
					}
				})
			}
		}
	}
</script>

<style>
	/* #ifndef APP-NVUE */
	@import 'static/fonts/iconfont.css';
	/* #endif */
	.nvueicon {font-family: nvueicon;}
</style>
<style lang="scss">
	// 引入vk-uview-ui基础样式
	@import 'uni_modules/vk-uview-ui/index.scss';
	
	@import 'styles/reset.scss';
	@import 'styles/layout.scss';
</style>

uniapp+vue3短视频功能模块

008360截图20231231102311711.png

短视频模块支持上下滑动,支持发送视频弹幕。

<ua-layout>
    <view class="ua__swipervideo flex1">
        <swiper
            class="ua__swipervideo-wrap flex1"
            :current="currentVideo"
            vertical
            :circular="true"
            :duration="200"
            @change="handleChange"
            @transition="handleTransition"
        >
            <swiper-item v-for="(item, index) in videoList" :key="index">
                <video
                    class="ua__swipervideo-player flex1"
                    :id="'uplayer' + index"
                    :src="item.src"
                    :danmu-list="item.danmu"
                    :enable-danmu="true"
                    :controls="false"
                    :loop="true"
                    :autoplay="index == currentVideo"
                    :show-center-play-btn="false"
                    object-fit="contain"
                    @click="handleClickVideo"
                    @play="isPlaying=true"
                    @timeupdate="handleTimeUpdate"
                    :style="{'width': `${winWidth}px`, 'height': `${winHeight}px`}"
                >
                </video>
                
                <!-- 浮层模块 -->
                <view class="ulive__video-float__info flexbox flex-col">
                    <view class="flexbox flex-row flex-alignb">
                        <!-- 左侧 -->
                        <view class="vdinfo__left flex1 flexbox flex-col">
                            <view class="ltrow danmu flexbox" @click="handleOpenDanmu"><text class="danmu-txt"></text><uv-icon class="ico" name="edit-pen" color="#fff" size="14" /></view>
                            <view class="ltrow"><text class="ait">@{{item.author}}</text></view>
                            <view class="ltrow"><text class="desc">{{item.desc}}</text></view>
                        </view>
                        <!-- 右侧操作栏 -->
                        <view class="vdinfo__right flexbox flex-col">
                            <view class="rtbtn avatar flexbox flex-col">
                                <view class="ubox"><image class="uimg" :src="item.avatar" mode="aspectFill" /></view>
                                <view class="btn flexbox" :class="{'active': item.isFollow}" @click="handleFollow(index)"><uv-icon :name="item.isFollow ? 'checkmark' : 'plus'" :color="item.isFollow ? '#ff007f' : '#fff'" size="11" /></view>
                            </view>
                            <view class="rtbtn flexbox flex-col" @click="handleLiked(index)"><uv-icon name="heart-fill" :color="item.isLike ? '#ff007f' : '#fff'" size="40" /><text class="num">{{item.likeNum+(item.isLike ? 1 : 0)}}</text></view>
                            <view class="rtbtn flexbox flex-col" @click="handleOpenComment(index)"><uv-icon name="chat-fill" color="#fff" size="40" /><text class="num">{{item.replyNum}}</text></view>
                            <view class="rtbtn flexbox flex-col"><uv-icon name="star-fill" color="#fff" size="40" /><text class="num">{{item.starNum}}</text></view>
                            <view class="rtbtn flexbox flex-col" @click="handleOpenShare(index)"><uv-icon name="share-fill" color="#fff" size="40" /><text class="num">{{item.shareNum}}</text></view>
                        </view>
                    </view>
                </view>
            </swiper-item>
        </swiper>
        
        <!-- 固定tabs(脱离滑动区) -->
        <view class="ulive__video-header__tabs" :style="{'margin-top': `${menuBarT}px`}">
            <uv-tabs :current="tabsCurrent" :list="tabsList" />
        </view>
        
        <!-- 播放暂停按钮 -->
        <view v-if="!isPlaying" class="ua__swipervideo-playbtn" :style="{'left': `${winWidth/2}px`, 'top': `${winHeight/2}px`}" @click="handleClickVideo">
            <text class="ua__swipervideo-playico welive-icon welive-icon-play nvueicon"></text>
        </view>
        <!-- 播放mini进度条 -->
        <view class="ua__swipervideo-progress" :style="{'width': `${winWidth}px`}"><view class="ua__swipervideo-progressbar" :style="{'width': `${progressBar}px`}"></view></view>
    </view>
    
    <template #footer>
        <ua-tabbar bgcolor="transparent" color="rgba(255,255,255,.7)" :border="false" :dock="false" transparent z-index="1000" />
    </template>
</ua-layout>
<script setup>
    /**
    ** uniapp+vue3短视频模块  Q:282310962
    */
    import { ref, computed, getCurrentInstance } from 'vue'
    import { onShow, onHide } from '@dcloudio/uni-app'
    import { getRandomColor } from '@/utils'
    
    // ...
    
    const { globalData } = getApp()
    const menuBarT = ref(globalData.menu?.top || globalData.statusBarH)
    const winWidth = ref(globalData.screenWidth)
    const winHeight = ref(globalData.screenHeight)
    
    const tabsList = ref([
        { name: '推荐', count: 5 },
        { name: '关注' },
        { name: '同城' }
    ])
    const tabsCurrent = ref(0)
    
    // 视频参数
    const currentVideo = ref(0)
    const isPlaying = ref(false)
    const clickNum = ref(0)
    const clickTimer = ref(null)
    const progressBar = ref(0)
    
    // 视频源
    const videoList = ref(videoJson)
    const danmuEditor = ref('')
    const isVisibleDanmu = ref(false)
    const commentRef = ref(null)
    const shareRef = ref(null)
    
    // ...
    
    
    /**
     * ====================== 视频播放模块 ======================
    */
    // 创建并返回 video 上下文 videoContext 对象
    const getVideoContext = () => {
        // return uni.createVideoContext(`uplayer${currentVideo.value}`, this)
        return uni.createVideoContext(`uplayer${currentVideo.value}`, getCurrentInstance())
    }
    
    // 垂直滑动视频,滑动改变时会触发 change 事件
    const handleChange = (e) => {
        const index = e.detail.current
        progressBar.value = 0
        handleReset()
        
        currentVideo.value = index
        // 播放
        handlePlay()
    }
    
    // 播放
    const handlePlay = () => {
        console.log('video play')
        let video = getVideoContext()
        if(!video) return
        video.play()
        isPlaying.value = true
    }
    
    // 暂停
    const handlePause = () => {
        console.log('video pause')
        let video = getVideoContext()
        if(!video) return
        video.pause()
        isPlaying.value = false
    }
    
    // 重置播放
    const handleReset = () => {
        console.log('video reset')
        let video = getVideoContext()
        if(!video) return
        video.pause()
        video.seek(0)
        video.stop()
        isPlaying.value = false
    }
    
    // 监听播放进度条
    const handleTimeUpdate = (e) => {
        let { currentTime, duration } = e.detail
        progressBar.value = parseInt((currentTime / duration).toFixed(2) * parseInt(winWidth.value))
    }
    
    // 点击视频(监听单双击)
    const handleClickVideo = () => {
        console.log('video click')
        clearTimeout(clickTimer.value)
        clickNum.value++
        clickTimer.value = setTimeout(() => {
            if(clickNum.value >= 2) {
                console.log('double click')
            }else {
                if(isPlaying.value) {
                    handlePause()
                }else {
                    handlePlay()
                }
            }
            clickNum.value = 0
        }, 200)
    }
    
    
    /**
     * ====================== 其它功能模块 ======================
    */
    // 打开弹幕弹框
    const handleOpenDanmu = () => {
        isVisibleDanmu.value = true
    }
    // 关闭弹幕弹框
    const handleCloseDanmu = () => {
        uni.hideKeyboard()
        isVisibleDanmu.value = false
        danmuEditor.value = ''
    }
    // 发送弹幕
    const handleSendDanmu = () => {
        let video = getVideoContext()
        if(!video) return
        video.sendDanmu({
            text: danmuEditor.value,
            color: getRandomColor()
        })
        handleCloseDanmu()
    }
    
    // 打开评论框
    const handleOpenComment = (index) => {
        commentRef.value.open()
    }
    
    // ...

</script>

uniapp+vue3直播功能模块

018360截图20231231103756265.png

直播模块包含了加入直播间提示、送礼物提示、购买提示、滚动聊天信息。

image.png

如上图:在nvue中如何实现自适应内容布局效果,这一块调试了比较久,坑比较多。

<!-- 聊天消息 -->
<view class="ulive__ft-livewrap-chats flex1">
	<scroll-view class="ulive__ft-livewrap-chats__scrollview flex1" scroll-y show-scrollbar="false" :scroll-into-view="scrollToView" :lower-threshold="5" @scroll="handleMsgScroll" @scrolltolower="handleMsgScrollLower">
		<block v-for="(msgitem, msgidx) in item.message" :key="msgidx">
			<view v-if="msgitem.type == 'notice'" class="notice" :id="`msg-${msgitem.id}`"><view class="item"><text class="noticetext">{{msgitem.content}}</text></view></view>
			<view v-else-if="msgitem.type == 'gift'" class="gift" :id="`msg-${msgitem.id}`">
				<view class="item">
					<text class="giftuser">{{msgitem.user}}</text>
					<text class="gifttext">送出了{{msgitem.content}}</text>
					<image class="giftimg" :src="msgitem.img" mode="widthFix" />
					<text class="giftnum">x{{msgitem.num}}</text>
				</view>
			</view>
			<view v-else class="msg" :id="`msg-${msgitem.id}`">
				<view class="item">
					<text v-if="msgitem.tag" class="tag">{{msgitem.tag}}</text>
					<text class="user">{{msgitem.user}}</text>
					<text class="text" :style="[fixTextStyle]">{{msgitem.isbuy ? '正在购买' : msgitem.content}}</text>
					<text v-if="msgitem.isbuy" class="tag tag-buy">去购买</text>
				</view>
			</view>
		</block>
	</scroll-view>
	<view v-if="!isEmpty(msgUnread)" class="ulive__ft-livewrap-chats__unread" @click="handleMsgIsRead"><text class="c-eb4868 fs-24">{{msgUnread.length}}条新消息</text></view>
</view>
.ulive__ft-livewrap-chats {
	height: 400rpx; max-width: 600rpx; pointer-events: auto; position: relative;
	.ulive__ft-livewrap-chats__scrollview {height: 400rpx; width: fit-content;}
	.notice {margin-bottom: 10rpx;}
	.msg {display: flex; align-items: flex-start; margin-bottom: 10rpx;}
	.gift {display: flex; align-items: flex-start; margin-bottom: 10rpx;}
	.item {background-color: rgba(0, 0, 0, .3); border-radius: 30rpx; padding: 5rpx 15rpx;}
	.msg .item, .gift .item {
		display: inline-block;
		/* #ifdef APP-NVUE */
		display: inline-flex; flex-direction: row;
		/* #endif */
	}
	.noticetext {color: #77e8e1; font-size: 28rpx;}
	.user {color: #77e8e1; font-size: 24rpx; margin-right: 10rpx;}
	.text {color: #fff; font-size: 28rpx;}
	.giftuser {color: #ffdd1a; margin-right: 15rpx;}
	.gifttext {color: #ff0ad3;}
	.giftimg {display: inline-block; margin: 0 10rpx; width: 50rpx; transform: scale(1.2);}
	.giftnum {color: #ff0ad3; font-size: 32rpx; font-style: italic;}
	.tag {background: linear-gradient(45deg, #f4bc61, #da8300); border-radius: 30rpx; color: #fff; display: inline-block; vertical-align: top; font-size: 24rpx; margin-right: 10rpx; padding: 3rpx 10rpx;}
	.tag-buy {background: #ff007f; margin-left: 10rpx; margin-right: 0;}
	.ulive__ft-livewrap-chats__unread {align-items: center; background-color: #fff; border-radius: 30rpx; padding: 7rpx 0; width: 160rpx; position: absolute; left: 0; bottom: 0;}
}

另外还需要注意,vue3中没有this,创建视频上下文的时候需要使用getCurrentInstance()获取上下文。

Okey,以上就是uniapp+vue3搭建跨端直播商城项目的一些分享,希望能喜欢哈~~

juejin.cn/post/730484…

20200520-173147-f8df.gif