<template> <div class="container" :style="{'padding-top':navHeight +'px'}"> <div class="masker"></div> <live-pusher id="pusher" :url="tuiStream" :mode="clear" bindstatechange="statechange" :beauty="beauty" :enable-camera="enableCamera" :whiteness="whiteness" :device-position="devicePosition" :enable-mic="enableMic" :remote-mirror="mirror" :mirror="mirror" :local-mirror="remoteMirror" :waiting-image="liveInfo.coverUrl" style="width: 100%; height: 100vh;position:fixed;top:0;left:0;z-index:-1" @statechange="statechange" @error="binderror" @netstatus="bindnetstatus" v-if="refreshLive" /> <image class="bg-img" v-if="isBgImg" :src="liveInfo.coverUrl" mode="aspectFill" alt=""></image> <div class="content" :style="{'padding-top':navHeight +'px'}"> <navBar :navTop="navTop" :navHeight="navHeight" :isSetBeauty="isSetBeauty" :isLive="isLive" :type="1" :id="liveId" :pusher="pusher" :liveInfo="liveInfo" :realStartTime="realStartTime" @closeSetBeauty="closeSetBeauty" @clearAllInterval="clearAllInterval" @changeOver="changeOver" > </navBar> <div class="live-active" v-if="!isSetBeauty"> <span>{{look}} 观看</span> <span>{{praise}} 赞</span> <span>{{online}} 在线</span> </div> <!-- 开播倒计时 --> <div class="countdown" v-show="isCountDown"> {{liveCountDown}} </div> <!-- 还没开始直播 --> <div class="wait-start" v-show="noStartLive"> <div class="title">{{liveInfo.title}}</div> <div class="open-live-title">{{!liveTimeStatus ? '开播倒计时' : '已超时,请尽快开始'}}</div> <div class="wait-time">{{liveTime}}</div> <div class="start-btn" @click="toSetBeauty('start')">开始直播</div> </div> <!-- 直播异常 --> <div class="unusual-wrap" v-show="recoverLive"> <div class="title">{{liveInfo.title}}</div> <div class="unusual-text">直播异常中断</div> <div class="recover-btn" @click="checkLiveEnv('recover')">恢复直播</div> </div> <!-- 公告 --> <div class="notice" v-show="!isSetBeauty"> 公告:{{liveInfo.liveNotice}} </div> <!-- 设置 --> <div class="setting-wrap" @click="setBeauty" v-show="!isSetBeauty && !isCountDown"> <img class="set-img" src="../../../static/images/shezhi.png" alt=""> </div> <!-- 商品 --> <div class="product-wrap" v-show="!isSetBeauty && liveInfo.liveBroadcastSales == 0" @click="showPopup"> <div class="product-num">{{productList.length}}</div> <img class="product-img" src="../../../static/images/shangpin.png" alt=""> </div> <!-- 商品弹窗 --> <product :productList="productList" :productDialogStatus="productDialogStatus" @hideProductPopup="hideProductPopup" @changeProduct="changeProduct" > </product> <!-- 讲解商品列表 --> <div class="speak-product" v-show="!isSetBeauty && isLive"> <!-- <div class="speak-product"> --> <div class="speak-item" v-for="(item,index) in speakProductList" :key="index"> <div class="item-status"> 讲解中 </div> <div class="item-top"> <img :src="item.productImgUrl" alt=""> </div> <div class="item-bottom"> ¥{{item.minPrice}} </div> </div> </div> <!-- 设置 --> <setting :pusher="pusher" :beauty="beauty" :whiteness="whiteness" :clear="clear" :devicePosition="devicePosition" :enableMic="enableMic" :remoteMirror="remoteMirror" :isLive="isLive" :liveInfo="liveInfo" @updateBeauty="updateBeauty" @updateWhiteness="updateWhiteness" @updateClear="updateClear" @updateDevicePosition="updateDevicePosition" @updateRemoteMirror="updateRemoteMirror" @updateEnableMic="updateEnableMic" v-if="isSetBeauty" @setBeautyStart="checkLiveEnv" > </setting> <!-- 评论 --> <comments v-if="!isSetBeauty" :updateVal="updateVal" :id="liveId" :commentsList="commentsList"></comments> <!-- 用户进入通知 --> <div class="userComing userComingAni" v-if="userComing && !isSetBeauty"> <span>{{entryNoticeText}}</span> <span>来了</span> </div> </div> </div> </template> <script> import navBar from '@/components/common/navbar.vue' import product from '@/components/product' import setting from '@/components/setting' import comments from '@/components/comments' import { getNavbarInfo,DFSImg } from '../../utils/common.js' import liveApi from '@/api/liveing' export default { data(){ return{ navTop : '', navHeight : '', isSetBeauty : false, //是否点击了设置 isUsual : false, //直播异常 productDialogStatus : false, //商品弹出框状态 isLive : false, //是否在直播 enableCamera : false, //是否开启摄像头 isPruductUp : false, speakProductList : [], pusher:null, beauty:5, //美颜 whiteness:5,//美白 clear:'HD', devicePosition : 'front', //前置或者后置 back => 后置 enableMic: true, //麦克风是否开启 remoteMirror:'enable', //镜像 userInfo : null, liveInfo : {}, //直播信息 liveTime :'', //开播时间 liveTimeStatus : false , //是否超时 liveCountDown : 3, //开播倒计时 isCountDown : false, //是否显示倒计时 liveId : '', //直播间id userComing: false, entryNoticeText : '', //进入通知 online : 0, //在线 look : 0, //观看 praise:0, //点赞 commentsList : [], //评论 noStartLive : false,//还没开始直播, recoverLive : false,//恢复直播 openLiveTimer : null, //开播时间定时器 tuiStream : '', //推流 productList : [], //商品列表 realStartTime : 0, //直播时间 liveDetailTimer : null, //获取直播评论等数据的定时器 getProductTimer : null, //刷新商品定时器 isBgImg : true, //是否展示背景图片 openRecordSet : false, //控制录音授权 openCameraSet : false, //控制相机授权 isChangeProduct : true, //是否可以上下屏,防止多次点击上下屏 isTimeLock : true, //时间锁 isTimeFirstReq : true, //第一次数据请求 isDetailLock : true, //查看详情锁 updateVal : 0, overOrStop : 1, //是暂停还是结束 isDetailControlLock : true, mirror : true, refreshLive : true } }, filter(){ formatNum : (val) => { return val - 0 > 10000 ? Math.floor(val / 10000)+ "w" : val } }, watch:{ //用户通知 entryNoticeText(){ if(this.entryNoticeText){ this.userComing=true; setTimeout(() => { this.userComing=false; }, 4600); }else{ this.userComing=false; } }, //开关摄像头 enableCamera(){ if(this.isLive){ this.enableCamera = true this.isBgImg = false; } }, }, components:{ navBar, product, setting, comments }, created(){ //获取导航栏信息 getNavbarInfo((res) => { this.navTop = res.navTop this.navHeight = res.navHeight }) }, onLoad(options){ this.isLive = false; this.enableCamera = false; this.isSetBeauty = false; this.noStartLive = false; this.recoverLive = false; this.realStartTime = 0; this.isBgImg = true; this.isTimeLock = true; this.isTimeFirstReq = true; this.overOrStop = 1; this.isDetailLock = true; this.refreshLive = true; this.online = 0 //在线 this.look = 0 //观看 this.praise = 0 //点赞 this.liveInfo = {} this.liveTime = ''; this.liveId = options.id; this.getLiveDetail(options.id); this.openLiveTimer = setInterval(() => { this.getLiveDetail(options.id); //传入直播间id },5000) this.getServerTimeNow(); //获取服务器时间戳 }, onReady(){ this.userInfo = this.$store.state.userInfo; this.pusher = wx.createLivePusherContext('pusher') }, onShow(){ wx.setKeepScreenOn({ keepScreenOn: true }) if(this.openRecordSet){ this.openRecordSet = false; this.getAuthSet('record') } if(this.openCameraSet){ this.openCameraSet = false; this.getAuthSet('camera'); } }, onHide(){ this.commentsList = [] this.clearAllInterval() console.log('onHide',this.overOrStop) this.pusher.stop(); if(this.overOrStop){ if(this.liveInfo.liveBroadcastState == 1){ this.changeLiveStatus(3) this.isSetBeauty = false; } } }, onUnload(){ this.commentsList = [] this.clearAllInterval() this.changeLiveStatus(3) }, methods:{ getAuthSet(type){ wx.getSetting({ success :(res) => { if(type == 'record'){ if (!res.authSetting["scope.record"]) { this.refreshLive = false; this.openConfirm(type); }else{ this.refreshLive = true; this.pusher = wx.createLivePusherContext('pusher'); this.pusher.startPreview() } }else if(type == 'camera'){ if (!res.authSetting["scope.camera"]) { this.openConfirm(type); }else{ this.enableCamera = true; } } }, fail(res) { console.log("调用失败"); } }); }, openConfirm(type){ let contentText = ''; if(type == 'record'){ contentText = '检测到您没打开麦克风的权限,是否去设置打开?' }else if(type == 'camera'){ this.enableCamera = false; contentText = '检测到您没打开摄像头的权限,是否去设置打开?' } wx.showModal({ content: contentText, confirmText : '确认', confirmColor : '#07c160', success : (res) => { if (res.confirm) { wx.openSetting({ success : (res1) => { if(type == 'record'){ this.openRecordSet = true; }else if(type == 'camera'){ this.openCameraSet = true; } } }); } else if (res.cancel) { wx.reLaunch({url:`../liveList/main`}) } } }) }, //点击设置美颜 setBeauty(){ this.enableCamera = true; this.noStartLive = false; this.recoverLive = false this.isBgImg = false; this.isSetBeauty = true; this.isDetailControlLock = false; this.pusher.startPreview() }, showPopup(){ this.productDialogStatus = true this.getProductList() }, hideProductPopup(){ this.productDialogStatus = false }, //设置美颜 updateBeauty(val){ this.beauty = val; }, //设置美白 updateWhiteness(val){ this.whiteness = val; }, //设置清晰度 updateClear(val){ this.clear = val }, //镜像 updateRemoteMirror(val){ this.remoteMirror = val; this.mirror = val == 'enable' ? true : false }, //麦克风 updateEnableMic(val){ this.enableMic = val; }, //获取直播间详情 getLiveDetail(id){ if(this.isDetailLock && this.isDetailControlLock){ this.isDetailLock = false; liveApi.queryLiveDetail({id}).then(res => { this.isDetailLock = true; if(res.data.code == '200'){ let result = res.data.data if(result){ result.coverUrl = DFSImg(result.coverUrl) result.logoUrl = DFSImg(result.logoUrl) this.liveInfo = result; this.tuiStream = result.tuiStream if(result.liveBroadcastState == 0){ this.isLive = false this.isBgImg = true; this.noStartLive = true; }else if(result.liveBroadcastState == 1){ this.enableCamera = true this.isLive = true this.isBgImg = false; }else if(result.liveBroadcastState == 3){ console.log('llll') this.recoverLive = true; this.isUsual = true; this.isBgImg = true; } if(result.realStartTime){ this.realStartTime = Math.floor((new Date().getTime() - new Date(result.realStartTime.replace(/-/g, '/').replace(/-/g, '/')).getTime())/1000) }else{ this.realStartTime = 0 } this.getLiveTimeHandler(result.startTime) this.getProductList(); //获取商品列表 }else{ wx.showModal({ title: '提示', content: '直播已关闭', success (res3) { if (res3.confirm) { wx.reLaunch({url:`../liveList/main`}) } else if (res3.cancel) { console.log('用户点击取消') } } }) } }else if(res.data.code == '-1'){ wx.showModal({ title: '提示', content: '直播已关闭', success (res3) { if (res3.confirm) { wx.reLaunch({url:`../liveList/main`}) } else if (res3.cancel) { console.log('用户点击取消') } } }) } }) } }, //获取开播时间 getLiveTimeHandler(time){ let getTime = new Date(time.replace(/-/g, '/')).getTime() - new Date().getTime(); let getTimeAbs = Math.abs(getTime) let hours = ''; let minute = ''; let day = ''; if(getTime > 0){ this.liveTimeStatus = false }else{ this.liveTimeStatus = true } day = Math.floor(getTimeAbs / (3600*1000*24)) hours = Math.floor((getTimeAbs%(24*3600*1000))/(3600*1000)); minute = Math.floor((getTimeAbs%(24*3600*1000)%(3600*1000) )/(60*1000)) if(getTime > 0){ if(day > 0){ let showHours = ""; let showMinute = ""; if(hours > 0){ if(hours >= 10){ showHours = hours + '小时' }else{ showHours = '0' + hours + '小时' } } if(minute >= 10){ showMinute = minute + '分' }else{ showMinute = '0' + minute + '分' } this.liveTime = `${day >= 10 ? day : '0' + day}天${showHours}${showMinute}` }else if(day <= 0 && hours > 0){ let showMinute = ""; if(minute >= 10){ showMinute = minute + '分' }else{ showMinute = '0' + minute + '分' } this.liveTime = `${hours >= 10 ? hours : '0' + hours}小时${showMinute}` }else if(day <= 0 && hours <= 0 && minute >= 0){ this.liveTime = `${minute >= 10 ? minute : '0' + minute}分` } }else{ this.liveTime = `${15 - minute}分钟后关闭` } }, //开始直播去设置美颜 toSetBeauty(type){ this.noStartLive = false; this.isSetBeauty = true; this.enableCamera = true; this.isDetailControlLock = false; this.isBgImg = false; this.pusher.startPreview() }, //检测直播环境 checkLiveEnv(type){ wx.getNetworkType({ success :(res) => { if(res.errMsg == 'getNetworkType:ok'){ if(res.networkType != 'wifi'){ wx.showModal({ title: '流量提醒', content: '你目前处于非WIFI环境,是否继续', confirmText : '继续', confirmColor : '#07c160', success :(res1) => { if (res1.confirm) { liveApi.queryLiveDetail({id : this.liveId}).then(res2 => { if(res2.data.code == '200'){ if(res2.data.data.liveBroadcastState == 4 || res2.data.data.liveBroadcastState == 2){ wx.showModal({ title: '提示', content: '直播已关闭', success (res3) { if (res3.confirm) { wx.reLaunch({url:`../liveList/main`}) } else if (res3.cancel) { console.log('用户点击取消') } } }) }else{ this.startLive(type); } } }) } else if (res1.cancel) { // console.log('用户点击取消') } } }) }else{ liveApi.queryLiveDetail({id : this.liveId}).then(res2 => { if(res2.data.code == '200'){ if(res2.data.data == null || res2.data.data.liveBroadcastState == 4 || res2.data.data.liveBroadcastState == 2){ wx.showModal({ title: '提示', content: '直播已关闭', success (res3) { if (res3.confirm) { wx.reLaunch({url:`../liveList/main`}) } else if (res3.cancel) { console.log('用户点击取消') } } }) }else{ this.startLive(type); } } }) } } } }) }, //开始直播 startLive(type){ this.isDetailControlLock = false; if(type == 'recover'){ this.recoverLive = false; this.isUsual = false; }else if(type == 'start'){ this.noStartLive = false; } this.isPruductUp = true; this.liveCountDown = 3; this.isCountDown = true; let timer = setInterval(() => { --this.liveCountDown; if(this.liveCountDown == 0){ console.log('开始直播') this.pusher.start(); //开始推流 this.isCountDown = false; this.isSetBeauty = false; clearInterval(timer) this.changeLiveStatus(1); } },1000) }, //改变直播状态 changeLiveStatus(val){ liveApi.updateLiveStatus({ id : this.liveId, liveBroadcastState : val }).then(res => { if(res.data.code == '200'){ if(val == 1){ this.isLive = true; this.enableCamera = true; this.isBgImg = false; this.pusher.resume(); }else if(val == 3){ this.isLive = false; console.log('222222') this.recoverLive = true; this.isBgImg = true; this.enableCamera = false; this.pusher.pause(); } this.isDetailControlLock = true; this.$set(this.liveInfo,'liveBroadcastState',val) // this.pusher.resume(); } }) }, //设置点击右上角返回 closeSetBeauty(val){ this.isSetBeauty = val; this.isBgImg = true; this.enableCamera = false; this.isDetailControlLock = true; if(this.liveInfo.liveBroadcastState == 0){ this.noStartLive = true; }else if(this.liveInfo.liveBroadcastState == 1){ this.noStartLive = false; this.recoverLive = false; }else if(this.liveInfo.liveBroadcastState ==3){ this.recoverLive = true; } }, //定时查询数据 timeGetInfo(serverTime){ let newCommentsTime = serverTime let userActivebeginTime = serverTime clearInterval(this.liveDetailTimer) this.liveDetailTimer = setInterval(() => { if(this.isTimeLock){ //第一次请求用服务器的时间减去2秒 this.isTimeLock = false; let dataTime = this.isTimeFirstReq ? serverTime - 2000 : newCommentsTime let userTime = this.isTimeFirstReq ? serverTime - 2000 : userActivebeginTime liveApi.queryLiveComments({ id: Number(this.liveId), beginTime: dataTime, //评论时间 userActivebeginTime : userTime, //用户进入时间 }).then(res => { this.isTimeLock = true; if(res.data.code == '200'){ this.isTimeFirstReq = false; let result = res.data.data; // if(result.liveState == 'IN_LIVE' || result.liveState == 'SUSPEND_LIVE' || result.liveState == 'NO_LIVE' || result.liveState == 'PAUSE_LIVE'){ //用户通知 this.entryNoticeText = ""; if (result.entryNoticeList.length > 0) { if (result.entryNoticeList.length == 1) { this.entryNoticeText = `${result.entryNoticeList[0].userName}`;//用户进入 } else { this.entryNoticeText = `${ result.entryNoticeList[0].userName }等${result.entryNoticeList.length}人`;//用户进入 } } //观看 this.look = result.historyWatchNum - 0 >= 10000 ? ((result.historyWatchNum - 0) / 10000).toFixed(1)+ "w" : result.historyWatchNum //在线 this.online = result.watchNum - 0 >= 10000 ?((result.watchNum - 0) / 10000).toFixed(1)+ "w" : result.watchNum //点赞 this.praise = result.likeInfo.likeNum - 0 >= 10000 ? ((result.likeInfo.likeNum - 0) / 10000).toFixed(1)+ "w" : result.likeInfo.likeNum //评论 result.guestBookList.forEach(item => { this.commentsList.push(item) }) if(result.guestBookList && result.guestBookList.length){ newCommentsTime = result.guestBookList[result.guestBookList.length - 1].createTimeStamp } if(result.entryNoticeList && result.entryNoticeList.length){ userActivebeginTime = result.entryNoticeList[result.entryNoticeList.length - 1].createTimeStamp } this.updateVal = new Date().getTime(); //监听使用数据 // } } }) }else{ console.log('上次请求未结束,进行了下一次请求') } },2000) }, //服务器时间戳 getServerTimeNow(){ liveApi.queryServerTimeNow().then(res => { if(res.data.code == 200){ //定时查询数据 this.timeGetInfo(res.data.data); } }) }, //推流状态 statechange(e){ console.log('statechange',e.mp.detail) if(e.mp.detail.code == -1307){ this.isLive = false; this.clearAllInterval() wx.showToast({ title: e.mp.detail.message, icon: "none" }); } }, binderror(e){ console.log('binderror',e.mp.detail) if(e.mp.detail.errCode == 10002){ //禁用麦克风 this.getAuthSet('record') } if(e.mp.detail.errCode == 10001){ //禁用摄像头 this.getAuthSet('camera') } }, bindnetstatus(e){ console.log('binderror',e.mp.detail) }, //获取商品列表 getProductList(){ liveApi.queryProductList({id : this.liveId,pageNum : 0,pageSize:0}).then(res => { if(res.data.code == '200'){ let productIdArr = [] let productIdArr1 = [] this.productList.forEach(item2 => { productIdArr.push(item2.productId) }) res.data.data.forEach(item1 => { productIdArr1.push(item1.productId) }) if(JSON.stringify(productIdArr) != JSON.stringify(productIdArr1)){ this.speakProductList = []; res.data.data.forEach(item => { item.productImgUrl = DFSImg(item.productImgUrl) item.minPrice = Number(item.minPrice).toFixed(2) if(item.upperScreenState == '1'){ this.speakProductList.push(item) } }) this.productList = res.data.data } } }) }, //商品上下屏 changeProduct(type,id,item){ if(this.isChangeProduct){ this.isChangeProduct = false; liveApi.queryProductUpDown({ productId : id, liveBroadcastId : this.liveId, upperScreenState : type }).then(res => { if(res.data.code == '200'){ this.isChangeProduct = true; console.log('上下屏') if(type == 1){ if(this.speakProductList.length == 1){ if(Number(item.number) < Number(this.speakProductList[0].number)){ this.speakProductList.unshift(item) }else{ this.speakProductList.push(item) } }else{ this.speakProductList.push(item) } this.productList.forEach((productItem,productIndex) => { if(productItem.productId == id){ this.$set(this.productList[productIndex],'upperScreenState',1) } }) }else{ this.speakProductList.forEach((i,index) => { if(i.productId == id){ this.speakProductList.splice(index,1) } }) this.productList.forEach((productItem,productIndex) => { if(productItem.productId == id){ this.$set(this.productList[productIndex],'upperScreenState',0) } }) } console.log(this.speakProductList, '上下屏商品') }else{ wx.showModal({ title: '提示', content: res.data.msg, success (res) { if (res.confirm) { console.log('用户点击确定') } else if (res.cancel) { console.log('用户点击取消') } } }) } }) } }, //结束直播清除相关定时器 clearAllInterval(){ clearInterval(this.openLiveTimer) clearInterval(this.getProductTimer) clearInterval(this.liveDetailTimer) }, changeOver(val){ this.overOrStop = 0; } } } </script> <style scoped lang="scss"> @mixin btn { width: 148px; height: 40px; text-align: center; line-height: 40px; font-size: 16px; color: white; border-radius: 8px; background-image:-webkit-linear-gradient(to right,#FF877D, #FB566D); background-image:-moz-linear-gradient(to right,#FF877D, #FB566D); background-image:-o-linear-gradient(to right,#FF877D, #FB566D); background-image: linear-gradient(to right,#FF877D, #FB566D); } .container{ width: 100%; height: 100vh; position: fixed; top: 0; left: 0; box-sizing: border-box; } .masker{ position: fixed; top: 0; left: 0; width: 100%; height: 100vh; background-color: rgba(102,102,102,0.38); } .bg-img{ width: 100%; height: 100%; position: absolute; top: 0; left: 0; z-index: -1; } .content{ width: 100%; height: 100vh; z-index: 10; position: fixed; top: 0; left: 0; z-index: 10; } .live-active{ width: 155px; height: 17px; background-color: rgba(0,0,0,.6); margin-left: 36px; border-radius: 17px; font-size: 10px; color: white; display: flex; align-items: center; justify-content: space-around; } .wait-start{ text-align: center; margin-top: 20vh; .title{ font-size: 18px; color: white; font-weight: bold; } .open-live-title{ font-size: 15px; color: #EEEEED; line-height: 50px; } .wait-time{ font-size: 36px; color: white; font-weight: bold; } .start-btn{ @include btn; margin: 23px auto 0; } } .countdown{ width: 100%; height: 100vh; position: absolute; top: 0; left: 0; background-color: rgba(0,0,0,.38); padding-top: 35vh; color: white; font-size: 40px; text-align: center; z-index: 120; } .unusual-wrap{ text-align: center; margin-top: 20vh; color: white; .title{ font-size: 18px; font-weight: bold; } .unusual-text{ font-size: 36px; line-height: 80px; font-weight: bold; } .recover-btn{ @include btn; margin: 20px auto 0; } } .notice{ position: fixed; left: 16px; bottom: 60px; width: 195px; background-color: rgba(0,0,0,.4); border-radius: 14px; box-sizing: border-box; padding: 10px 7px 7px 10px; font-size: 12px; color: white; line-height: 18px; } .product-wrap{ position: fixed; left: 19px; bottom: 14px; width: 31px; height: 29px; .product-num{ font-size:12px; color: white; width: 100%; text-align: center; position: absolute; bottom: 4px; } .product-img{ width: 100%; height: 100%; } } .setting-wrap{ position: fixed; right: 13px; bottom: 14px; width: 37px; height: 37px; .set-img{ width: 100%; height: 100%; } } .speak-product{ position: fixed; left: 8px; top:20vh; .speak-item{ width: 78px; margin-bottom: 15px; border-radius: 2px; overflow: hidden; position: relative; .item-status{ position: absolute; top: 0; left: 0; width: 39px; height: 16px; color: white; background-color: #FF4240; border-bottom-right-radius: 2px; font-size: 11px; text-align: center; line-height: 16px; } .item-top{ height: 78px; background-color: #B1B1B1; img{ width: 100%; height: 100%; } } .item-bottom{ height: 23px; background-color: white; text-align: center; line-height: 23px; font-size: 14px; color: #FA7018; } } } .userComing { margin-bottom: 10px; background: rgba(#000, 0.4); color: #fff; font-size: 16px; height: 26px; line-height: 26px; border-radius: 26px; padding: 0 8px; display: inline-block; position: fixed; bottom: 30vh; } .userComingAni { animation: userComingAni 4.7s linear; } @keyframes userComingAni { 0% { transform: translateX(150vw); opacity: 0.3; } 30% { transform: translateX(24px); opacity: 1; } 80% { transform: translateX(24px); opacity: 1; } 100% { transform: translateX(-150vw); opacity: 0; } } </style>