jwyu 2 gadi atpakaļ
vecāks
revīzija
9e8475e77a

+ 1 - 1
package.json

@@ -1,5 +1,5 @@
 {
-  "name": "vue3-admin-tem",
+  "name": "hongze_yb_pc",
   "private": true,
   "version": "0.0.0",
   "scripts": {

+ 12 - 0
src/api/video.js

@@ -28,4 +28,16 @@ export const apiVideoPlayLog=params=>{
 		source_agent=4
 	}
     return post('/community/video/play_log',{...params,source_agent:source_agent})
+}
+
+/**
+ * 视频弹幕
+ * @param content
+ * @param seconds
+ * @param primary_id 视频id
+ * @param source 来源:1-视频社区 2-路演视频
+ * @param source_agent 来源平台:1:小程序、2:小程序(pc)、3:公众号、4:官网web(pc)
+ */
+export const apiVideoDanmuSend=params=>{
+    return post('/bullet_chat/add',{source_agent:2,...params})
 }

BIN
src/assets/video/danmu-close-icon.png


BIN
src/assets/video/danmu-close2-icon.png


BIN
src/assets/video/danmu-show-icon.png


BIN
src/assets/video/danmu-show2-icon.png


BIN
src/assets/video/fullscreen-icon.png


BIN
src/assets/video/smallscreen-icon.png


+ 570 - 0
src/components/VideoBox.vue

@@ -0,0 +1,570 @@
+<script setup>
+import {reactive, ref} from 'vue'
+import {apiVideoDanmuSend} from '@/api/video'
+import moment from 'moment'
+
+const props=defineProps({
+    videoInfo:null,
+    curVideoId:{
+        type:Number,
+        default:0
+    }
+})
+const emit = defineEmits(['clickPlay','ended', 'pause'])
+
+let info=ref(props.videoInfo)
+const videoIns=ref(null)
+
+// 弹幕状态值
+let danmuState=reactive({
+    show:true,//显示弹幕
+    temList:props.videoInfo.bullet_chat_list||[],
+    list:[],
+    content:'',//弹幕内容
+})
+
+// 添加弹幕到页面
+const handleAddDanmu=(time)=>{
+    danmuState.temList.forEach(item => {
+        if(item.seconds>time-1&&item.seconds<time+1){// 前后误差一秒
+            if(!item.done){
+                item.done=true
+                danmuState.list.push({
+                    ...item,
+                    top:handleGetTopPosition(),
+                    speed:Math.floor(Math.random()*(16-8+1))+8//4~8 之间的随机数
+                })
+            }
+        }else{
+            // 如果播放过了 手贱又把进度条拖回去了 则重置done
+            if(time-1<item.seconds){
+                item.done=false
+            }
+            if(time+1>item.seconds){
+                item.done=true
+            }
+        }
+    });
+}
+const handleGetTopPosition=()=>{
+    const length=danmuState.list.length
+    let num=0
+    if(length%3===1){
+        num=10
+    }else if(length%3===2){
+        num=30
+    }else{
+        num=50
+    }
+    return num+'px'
+}
+
+
+//视频状态值
+let videoState=reactive({
+    ctime:0,//当前播放的时间
+    play:false,
+    isPageFullScreen:false,//网页全屏
+    isHover:false,
+    speedOpts:['0.5','0.8','1.0','1.25','1.5','2.0'],
+    speed:'1.0',
+    showSpeedOpt:false,
+    showSpeedOptInner:false,//显示内部
+})
+
+// 视频事件
+const handleVideoPlay=()=>{
+    videoState.play=true
+}
+const handleVideoEnd=()=>{
+    videoState.play=false
+    videoState.isPageFullScreen=false
+    emit('ended')
+}
+const handleVideoPause=(e)=>{
+    videoState.play=false
+    emit('pause',e)
+}
+const handelClickPlay=()=>{
+    emit('clickPlay',props.videoInfo)
+}
+const handleVideoTimeUpdate=(e)=>{
+    const time=e.target.currentTime
+    handleAddDanmu(time)
+    videoState.ctime=time
+}
+//改变倍速
+const handleVideoSpeedChange=(item)=>{
+    if(videoState.speed==item) return
+    videoState.speed=item
+    videoIns.value.playbackRate=Number(item)
+}
+
+// 鼠标进入视频区域
+const handleMouseMoveInVideo=()=>{
+    videoState.isHover=true
+}
+const handleMouseOutVideo=()=>{
+    videoState.isHover=false
+}
+
+// 新建一条弹幕
+const handleSendDanmu=async ()=>{
+    if(!danmuState.content||info.value.id!=props.curVideoId) return
+    const res=await apiVideoDanmuSend({
+        content:danmuState.content,
+        seconds:parseInt(videoState.ctime),
+        primary_id:props.videoInfo.id,
+        source:props.videoInfo.source
+    })
+    if(res.code===200){
+        danmuState.content=''
+        danmuState.temList.push({...res.data,seconds:Number(res.data.seconds)+3})
+    }
+}
+
+</script>
+
+<template>
+    <div class="video-play-wrap" @keyup.esc="videoState.isPageFullScreen=false">
+        <div 
+            :class="[
+                'video-content-box',
+                videoState.isPageFullScreen?'page-full-screen':''
+            ]" 
+            @mouseover="handleMouseMoveInVideo" 
+            @mouseout="handleMouseOutVideo"
+        >
+            <video 
+                class="video"
+                ref="videoIns"
+                :src="info.video_url" 
+                controls
+                :poster="info.cover_img_url"
+                controlslist="nodownload nofullscreen noplaybackrate"
+                disablePictureInPicture
+                autoplay
+                v-if="info.id==props.curVideoId"
+                @ended="handleVideoEnd"
+                @pause="handleVideoPause"
+                @play="handleVideoPlay"
+                @timeupdate="handleVideoTimeUpdate"
+            ></video>
+            <div v-else class="poster-img" :style="'background-image:url('+info.cover_img_url+')'" @click="handelClickPlay"></div>
+            <!-- 视频按钮 -->
+            <div 
+                class="no-select-text video-control-btns-box" 
+                :style="{right:videoState.isPageFullScreen?'30px':'10px',bottom:videoState.isPageFullScreen?'95px':'70px'}"
+                v-if="info.id==props.curVideoId"
+            >
+                <!-- 小屏时倍速 -->
+                <div 
+                    class="small-screen-speed-btn" 
+                    v-show="!videoState.isPageFullScreen&&videoState.isHover"
+                    @click.stop="videoState.showSpeedOpt=true"
+                >倍速</div>
+                <div 
+                    :class="['screen-change-box', videoState.isPageFullScreen?'full-screen':'small-screen']" 
+                    v-show="videoState.isHover" 
+                    @click.stop="videoState.isPageFullScreen=!videoState.isPageFullScreen"
+                ></div>
+            </div>
+            
+            <!-- 全屏时发送弹幕模块 -->
+            <div class="flex inside-danmu-input-box" v-if="videoState.isPageFullScreen">
+                <div :class="danmuState.show?'show-icon':'close-icon'" @click.stop="danmuState.show=!danmuState.show"></div>
+                <div class="flex input-box">
+                    <input v-model="danmuState.content" type="text" maxlength="50" placeholder="发个友善的弹幕见证当下~" @keyup.enter="handleSendDanmu">
+                    <span class="btn" @click.stop="handleSendDanmu">发送</span>
+                </div>
+                <div style="position:relative">
+                    <div class="speed-btn">倍速{{videoState.speed}}X</div>
+                    <div class="speed-opt-box">
+                        <div 
+                            :class="['item',item==videoState.speed?'active':'']" 
+                            v-for="item in videoState.speedOpts" 
+                            :key="item"
+                            @click.stop="handleVideoSpeedChange(item)"
+                        >{{item}}X</div>
+                    </div>
+                </div>
+            </div>
+            <!-- 弹幕滚动区域 -->
+            <div class="danmu-scroll-box" :style="{opacity: danmuState.show?1:0}">
+                <span 
+                    :class="[
+                        'no-select-text danmu-item',
+                        videoState.play?'animat-run':'animat-pause',
+                        item.user_id==$store.state.userInfo.user_id?'border':''
+                    ]"
+                    v-for="item in danmuState.list"
+                    :key="item.id"
+                    :style="{
+                        color:item.color,
+                        top:item.top,
+                        animationDuration:videoState.isPageFullScreen?item.speed+20+'s':item.speed+'s'
+                    }"
+                >{{item.content}}</span>
+            </div>
+        </div>
+
+        <!-- 视频外面发弹幕输入模块 -->
+        <div class="flex outside-danmu-input-box">
+            <div :class="danmuState.show?'show-icon':'close-icon'" @click.stop="danmuState.show=!danmuState.show"></div>
+            <div class="flex input-box">
+                <input 
+                    :disabled="info.id!=props.curVideoId" 
+                    v-model="danmuState.content" 
+                    type="text" 
+                    maxlength="50" 
+                    placeholder="发个友善的弹幕见证当下~"
+                    @keyup.enter="handleSendDanmu"
+                    @keyup.esc="videoState.isPageFullScreen=false"
+                >
+                <span class="btn" :style="{background:info.id!=props.curVideoId?'#929292':''}" @click.stop="handleSendDanmu">发送</span>
+            </div>
+        </div>
+
+        <!-- 视频倍速选择弹窗 -->
+        <el-dialog
+            v-model="videoState.showSpeedOpt"
+            title="倍速"
+            width="350px"
+            draggable
+            center
+        >
+            <div class="outside-speed-opt-box">
+                <span 
+                    :class="['item',videoState.speed==item?'active':'']" 
+                    v-for="item in videoState.speedOpts" 
+                    :key="item"
+                    @click.stop="handleVideoSpeedChange(item)"
+                >{{item}}X</span>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.video-play-wrap{
+    width: 100%;
+    height: 100%;
+    position: relative;
+    overflow: hidden;
+    .video-content-box{
+        position: relative;
+        overflow: hidden;
+        width: 100%;
+        height: 200px;
+    }
+    .page-full-screen{
+        position: fixed;
+        left: 0 !important;
+        right: 0 !important;
+        top: 0 !important;
+        bottom: 0 !important;
+        width: auto !important;
+        height: auto !important;
+        z-index: 999999 !important;
+        background: rgba(0, 0, 0, 1);
+    }
+
+    .video-control-btns-box{
+        position: absolute;
+        right: 10px;
+        bottom: 70px;
+        .screen-change-box{
+            cursor: pointer;
+            position: relative;
+            width: 30px;
+            height: 30px;
+            border-radius: 50%;
+            &:hover{
+                background: rgba(0, 0, 0, 0.3);
+            }
+            &::after{
+                content: '';
+                display: block;
+                width: 20px;
+                height: 20px;
+                background-size: cover;
+                position: absolute;
+                top: 5px;
+                left: 5px;
+            }
+            &.full-screen{
+                width: 40px;
+                height: 40px;
+                &::after{
+                    background-image: url('@/assets/video/smallscreen-icon.png');
+                    width: 30px;
+                    height: 30px;
+                }
+            }
+            
+            &.small-screen::after{
+                background-image: url('@/assets/video/fullscreen-icon.png');
+            }
+        }
+
+        .small-screen-speed-btn{
+            background: rgba(0, 0, 0, 0.4);
+            border-radius: 22px;
+            color: #fff;
+            font-size: 12px;
+            cursor: pointer;
+            width: 40px;
+            height: 20px;
+            text-align: center;
+            line-height: 20px;
+            margin-bottom: 5px;
+        }
+    }
+
+    .video{
+        width: 100%;
+        height: 100%;
+        display: block;
+        object-fit: contain;
+        box-sizing: border-box;
+        &::-webkit-media-controls-fullscreen-button {
+            display: none;
+        }
+    }
+
+    .poster-img{
+        width: 100%;
+        height: 100%;
+        position: relative;
+        background-size: cover;
+        background-position: center;
+        cursor: pointer;
+        &::after{
+            content:'';
+            display: block;
+            position: absolute;
+            width: 80px;
+            height: 80px;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%,-50%);
+            background-image: url('@/assets/video-play-btn.png');
+            background-size: cover;
+        }
+    }
+
+    .inside-danmu-input-box{
+        position: absolute;
+        left: 50%;
+        transform: translateX(-50%);
+        bottom: 100px;
+        .close-icon{
+            width: 32px;
+            height: 32px;
+            background-image: url('@/assets/video/danmu-close2-icon.png');
+            background-size: cover;
+            flex-shrink: 0;
+            cursor: pointer;
+        }
+        .show-icon{
+            width: 34px;
+            height: 32px;
+            background-image: url('@/assets/video/danmu-show2-icon.png');
+            background-size: cover;
+            flex-shrink: 0;
+            cursor: pointer;
+        }
+        .input-box{
+            margin-left: 10px;
+            margin-right: 20px;
+            background: rgba(0, 0, 0, 0.2);
+            border: 1px solid #E5E5E5;
+            border-radius: 42.5px;
+            width: 40vw;
+            align-items: center;
+            overflow: hidden;
+            height: 32px;
+            font-size: 14px;
+            padding-left: 10px;
+            input{
+                flex: 1;
+                border: none;
+                background: transparent;
+                height: 100%;
+                outline: none;
+                color: #fff;
+                &::placeholder{
+                    color: #fff;
+                }
+            }
+            .btn{
+                display: inline-block;
+                flex-shrink: 0;
+                color: #fff;
+                background: #F3A52F;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                width: 60px;
+                height: 32px;
+                font-size: 14px;
+                cursor: pointer;
+            }
+        }
+        .speed-btn{
+            display: inline-block;
+            width: 90px;
+            height: 32px;
+            line-height: 32px;
+            text-align: center;
+            color: #fff;
+            background: rgba(0, 0, 0, 0.4);
+            border-radius: 22px;
+            &:hover+.speed-opt-box{
+                display: block;
+            }
+        }
+        .speed-opt-box{
+            width: 120px;
+            padding: 20px 0 0px 0;
+            position: absolute;
+            bottom: 105%;
+            left: 50%;
+            display: none;
+            transform: translateX(-50%);
+            background: rgba(0, 0, 0, 0.8);
+            &:hover{
+                display: block;
+            }
+            .item{
+                cursor: pointer;
+                margin-bottom: 20px;
+                color: #fff;
+                text-align: center;
+                &.active{
+                    color: #F3A52F;
+                }
+            }
+        }
+    }
+
+    .outside-danmu-input-box{
+        align-items: center;
+        margin-top: 20px;
+        .close-icon{
+            width: 32px;
+            height: 32px;
+            background-image: url('@/assets/video/danmu-close-icon.png');
+            background-size: cover;
+            flex-shrink: 0;
+            cursor: pointer;
+        }
+        .show-icon{
+            width: 32px;
+            height: 32px;
+            background-image: url('@/assets/video/danmu-show-icon.png');
+            background-size: cover;
+            flex-shrink: 0;
+            cursor: pointer;
+        }
+        .input-box{
+            margin-left: 10px;
+            background: #F4F4F4;
+            border: 1px solid #E5E5E5;
+            border-radius: 42.5px;
+            flex: 1;
+            align-items: center;
+            overflow: hidden;
+            height: 32px;
+            font-size: 12px;
+            padding-left: 10px;
+            input{
+                flex: 1;
+                border: none;
+                background: transparent;
+                height: 100%;
+                outline: none;
+            }
+            .btn{
+                display: inline-block;
+                flex-shrink: 0;
+                color: #fff;
+                background: #F3A52F;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                width: 60px;
+                height: 32px;
+                font-size: 14px;
+                cursor: pointer;
+            }
+        }
+    }   
+
+
+    .danmu-scroll-box{
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        height: 70px;
+        // background: linear-gradient(180deg, rgba(0, 0, 0, 0.3) 0%, rgba(0, 0, 0, 0) 100%);
+        .danmu-item{
+            color: #fff;
+            animation: move 6s linear;
+            position: absolute;
+            top: 0;
+            display: block;
+            left: calc(100% + 1000px);
+            position: absolute;
+            font-size: 12px;
+            height: 18px;
+            white-space: nowrap;
+        }
+        .animat-pause{
+            animation-play-state: paused;
+        }
+        .animat-run{
+            animation-play-state: running;
+        }
+        .border{
+            border: 1px solid #fff;
+            border-radius: 10px;
+            padding-left: 2px;
+            padding-right: 2px;
+            line-height: 20px;
+        }
+        @keyframes move {
+            0%{
+                left: calc(100% + 1000px);
+            }
+            100%{
+                left: -1000px;
+            }
+        }
+    }
+
+    .outside-speed-opt-box{
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: space-between;
+        .item{
+            display: inline-block;
+            width: 80px;
+            height: 36px;
+            text-align: center;
+            line-height: 36px;
+            border: 1px solid #F3A52F;
+            border-radius: 4px;
+            background: #fff;
+            color: #F3A52F;
+            margin: 10px 0;
+            cursor: pointer;
+            &.active{
+                background-color: #F3A52F;
+                color: #fff;
+            }
+        }
+    }
+}
+</style>

+ 6 - 6
src/views/video/components/Comment.vue → src/components/VideoComment.vue

@@ -17,10 +17,10 @@ let info=ref(props.videoInfo)
 //点赞\吐槽
 const handleSetLike=async (type)=>{
     const res=await apiSetLike({
-        community_question_id:info.value.community_video_id,
+        community_question_id:info.value.id,
         op_type: type,
         enable: type === 1 ? Number(info.value.op_type!==1) : Number(info.value.op_type!==2),
-        source:2
+        source:info.value.source
     })
     if(res.code===200){
         const { enabled,op_type,like_total,tease_total } = res.data;
@@ -53,10 +53,10 @@ const handleShowPop=()=>{
 //获取评论
 const getComment=async (flag)=>{
     const params={
-        community_question_id:info.value.community_video_id,
+        community_question_id:info.value.id,
         curr_page: 1,
         page_size: 10000,
-        source:2
+        source:info.value.source
     }
     const { code,data } = popData.select_comment_type===1 ? await apiHotComment(params) : await apiMyComment(params);
     if(code !== 200) return
@@ -102,8 +102,8 @@ const handlePublish=async ()=>{
 
     const { code } = await apiPublishComment({
         content: popData.msg,
-        community_question_id: info.value.community_video_id,
-        source:2,
+        community_question_id: info.value.id,
+        source:info.value.source,
     })
     if(code===200){
         ElMessage({

+ 43 - 41
src/views/roadShow/video/List.vue

@@ -8,6 +8,8 @@ import {apiRoadShowVideoList,apiRoadShowVideoPlayLog} from '@/api/roadShow'
 import {apiApplyPermission} from '@/api/user'
 
 import SelfList from '@/components/SelfList.vue'
+import VideoComment from '@/components/VideoComment.vue'
+import VideoBox from '@/components/VideoBox.vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useStore } from 'vuex'
 
@@ -253,6 +255,24 @@ const handelGetQRCodeImg=async (item)=>{
     }
 }
 
+// 处理评论模块数据
+const getCommentData=item=>{
+    return{
+        id:item.road_video_id,
+        source:3,
+        ...item
+    }
+}
+
+// 处理评论模块数据
+const getDanmuData=item=>{
+    return{
+        id:item.road_video_id,
+        source:2,
+        ...item
+    }
+}
+
 onMounted(() => {
   //向小程序发送消息
   let postData = {
@@ -340,7 +360,7 @@ onActivated(()=>{
             @listOnload="onLoad"
         >
             <div class="flex list-wrap">
-                <div class="flex video-item" v-for="item in listState.list" :key="item.road_video_id">
+                <div class="video-item" v-for="item in listState.list" :key="item.road_video_id">
                     <el-popover
                         :width="200"
                         trigger="hover"
@@ -361,23 +381,27 @@ onActivated(()=>{
                     >
                         <img class="collect-icon" :src="item.collection_id>0?collectSIcon:collectIcon" alt="">
                     </CollectBox>
-                    <div class="title">{{item.title}}</div>
-                    <div>
-                    <video 
-                        :src="item.video_url" 
-                        controls
-                        :poster="item.cover_img_url"
-                        controlslist="nodownload"
-                        disablePictureInPicture
-                        autoplay
-                        v-if="item.road_video_id==curVideoId"
-                        @ended="curVideoId=0"
-                        @pause="handleVideoPause"
-                    ></video>
-                    <div v-else class="poster-img" :style="'background-image:url('+item.cover_img_url+')'" @click="handelClickPlay(item)"></div>
-                    <div class="time">发布时间:{{item.publish_time}}</div>
-                    <span class="user-name">{{item.admin_real_name}}</span>
+                    <el-tooltip
+                        effect="dark"
+                        :content="item.title"
+                        placement="top-start"
+                    >
+                    <div class="multi-ellipsis title">{{item.title}}</div>
+                    </el-tooltip>
+                    <div style="margin-top:20px">
+                        <div class="time">发布时间:{{item.publish_time}}</div>
+                        <span class="user-name">{{item.admin_real_name}}</span>
                     </div>
+                    <div class="video-content">
+                        <VideoBox
+                            :videoInfo="getDanmuData(item)"
+                            :curVideoId="curVideoId"
+                            @clickPlay="handelClickPlay"
+                            @ended="curVideoId=0"
+                            @pause="handleVideoPause"
+                        ></VideoBox>
+                    </div>
+                    <VideoComment :videoInfo="getCommentData(item)"></VideoComment>
                 </div>
                 <div class="last-add-item"></div>
                 <div class="last-add-item"></div>
@@ -518,32 +542,10 @@ onActivated(()=>{
                 color: #666;
                 padding-right: 75px;
             }
-            video{
-                width: 100%;
-                height: 200px;
-                object-fit: contain;
-                margin: 19px 0;
-            }
-            .poster-img{
+            .video-content{
                 width: 100%;
-                height: 200px;
+                // height: 200px;
                 margin: 19px 0;
-                position: relative;
-                background-size: cover;
-                background-position: center;
-                cursor: pointer;
-                &::after{
-                    content:'';
-                    display: block;
-                    position: absolute;
-                    width: 80px;
-                    height: 80px;
-                    top: 50%;
-                    left: 50%;
-                    transform: translate(-50%,-50%);
-                    background-image: url('@/assets/video-play-btn.png');
-                    background-size: cover;
-                }
             }
             .time{
                 color: #999;

+ 11 - 2
src/views/video/List.vue

@@ -7,7 +7,7 @@ import {apiVideoList,apiVideoPlayLog} from '@/api/video'
 import {apiApplyPermission} from '@/api/user'
 
 import SelfList from '@/components/SelfList.vue'
-import Comment from './components/Comment.vue'
+import VideoComment from '@/components/VideoComment.vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useStore } from 'vuex'
 
@@ -252,6 +252,15 @@ const handelGetQRCodeImg=async (item)=>{
     }
 }
 
+// 处理评论模块数据
+const getCommentData=item=>{
+    return{
+        id:item.community_video_id,
+        source:2,
+        ...item
+    }
+}
+
 onMounted(() => {
   //向小程序发送消息
   let postData = {
@@ -381,7 +390,7 @@ onActivated(()=>{
                     ></video>
                     <div v-else class="poster-img" :style="'background-image:url('+item.cover_img_url+')'" @click="handelClickPlay(item)"></div>
              
-                    <Comment :videoInfo="item"></Comment>
+                    <VideoComment :videoInfo="getCommentData(item)"></VideoComment>
                 </div>
                 <div class="last-add-item"></div>
                 <div class="last-add-item"></div>