Browse Source

路演视频

jwyu 2 years ago
parent
commit
b654cf50df

+ 3 - 2
src/api/report.js

@@ -15,9 +15,10 @@ export const apiReportDetail=params=>{
 
 /**
  * 研报分类
+ * @param classify_type 0是英文研报,1是线上路演
  */
-export const apiReportClassifyList=()=>{
-    return get('/english_report/classify',{})
+export const apiReportClassifyList=(params)=>{
+    return get('/english_report/classify',params)
 }
 
 /**

+ 34 - 0
src/api/roadShow.js

@@ -0,0 +1,34 @@
+/**
+ * 路演视频模块
+ */
+ import {get,post} from './http'
+
+/**
+ * 路演视频列表
+ * @param page_size  
+ * @param current
+ * @param classify_id_first
+ * @param classify_id_second
+ */
+export const apiRoadShowVideoList=params=>{
+    return get('/english_report/video/list',params)
+}
+
+/**
+ * 路演视频详情
+ * @param video_code  
+ * @param share_email
+ */
+export const apiRoadShowVideoDetail=params=>{
+    return get('/english_report/video/detail',params)
+}
+
+/**
+ * 视频播放日志
+ * @param id 日志ID 为0时 新增,大于0时更新
+ * @param video_id 路演视频ID, 新增时必传
+ * @param stop_seconds 停止时的秒数
+ */
+export const apiVideoPlayLog=params=>{
+    return post('/english_report/video/play_log',params)
+}

BIN
src/assets/icon_play.png


+ 4 - 4
src/components/VideoPlayBox.vue

@@ -74,9 +74,9 @@ let videoState=reactive({
 })
 const emit=defineEmits(['play','pause','timeupdate'])
 //视频事件
-function videoPlay(){
+function videoPlay(e){
     videoState.play=true
-    emit('play')
+    emit('play',e)
 }
 function videoCanPlay(e){
     const target=e.target
@@ -88,9 +88,9 @@ function videoTimeUpdate(e){
     videoState.duration=target.duration
     emit('timeupdate')
 }
-function videoPause(){
+function videoPause(e){
     videoState.play=false
-    emit('pause')
+    emit('pause',e)
 }
 function videoProgress(e){
     const target=e.target

+ 5 - 0
src/router/index.js

@@ -25,6 +25,11 @@ const routes=[
     name: "ReportDetail",
     component: ()=>import("@/views/report/Detail.vue"),
   },
+  {
+    path: "/roadshow/detail",
+    name: "RoadshowDetail",
+    component: ()=>import("@/views/roadShow/Detail.vue"),
+  },
   
   {
     path: '/:pathMatch(.*)',

+ 22 - 7
src/views/report/Index.vue

@@ -4,6 +4,7 @@ import { useRoute, useRouter } from "vue-router";
 import {apiReportClassifyList,apiReportList} from '@/api/report'
 import { useWindowSize } from '@vueuse/core'
 import videoPlayBox from '@/components/VideoPlayBox.vue'
+import roadShowList from '@/views/roadShow/List.vue'
 
 const { width, height } = useWindowSize()
 
@@ -13,18 +14,30 @@ const route=useRoute()
 //分类
 let navList=ref([])
 async function getReportClassify(){
-    const res=await apiReportClassifyList()
+    const res=await apiReportClassifyList({classify_type:0})
+    const videores=await apiReportClassifyList({classify_type:1})
+
     if(res.code===200){
         const arr=res.data.list||[]
+        let videoResList=[]
+        if(videores.code===200){
+            videoResList=videores.data.list||[]
+        }
         navList.value=[
             {
                 id: 0,
                 classify_name: "ABOUT US",
                 child: []
             },
-            ...arr
+            ...arr,
+            {
+                id: -1,
+                classify_name: "ONLINE VEDIO",
+                child: videoResList
+            }
         ]
 
+
         if(route.query.firstclassifyid){
             arr.forEach(item => {
                 if(item.id==route.query.firstclassifyid){
@@ -99,14 +112,13 @@ async function getList(){
         if(arr.length<listState.pageSize){
             listState.finished=true
         }
-        
     }
 }
 
 // 监听页面滚动
 function listenScroll(){
     window.onscroll=(e)=>{
-        if(listState.firstClassifyId===0) return
+        if([0,-1].includes(listState.firstClassifyId)) return
         if(listState.loading||listState.finished) return
         const scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
         const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
@@ -157,7 +169,7 @@ let filterSize=ref('30%')
                 @click="handleChangeFirstClassify(item)"
             >{{item.classify_name}}</span>
         </div>
-        <div class="sub-nav-wrap">
+        <div class="sub-nav-wrap" v-if="listState.firstClassifyId!=-1">
             <h2 class="label">{{listState.firstClassifyName}}</h2>
             <div class="sub-nav-list">
                 <span 
@@ -169,7 +181,7 @@ let filterSize=ref('30%')
             </div>
         </div>
         <!-- 固定展示的报告区域 -->
-        <div class="fix-report-wrap" v-show="listState.firstClassifyId!=0&&listState.secClassifyId===0">
+        <div class="fix-report-wrap" v-show="![-1,0].includes(listState.firstClassifyId)&&listState.secClassifyId===0">
             <div class="left-wrap">
                 <div class="report-item-normal" v-for="item in listState.list.slice(0,3)" :key="item.id" @click="goReportDetail(item)">
                     <div class="title">{{item.title}}</div>
@@ -204,7 +216,7 @@ let filterSize=ref('30%')
             </div>
         </div>
         <!-- 报告列表 -->
-        <div class="report-list-wrap" v-show="listState.secClassifyId!=0">
+        <div class="report-list-wrap" v-show="listState.secClassifyId!=0&&listState.firstClassifyId!=-1">
             <div class="item" v-for="item in listState.list" :key="item.id" @click="goReportDetail(item)">
                 <div class="title">{{item.title}}</div>
                 <div class="intro">{{item.abstract}}</div>
@@ -247,6 +259,9 @@ let filterSize=ref('30%')
                 </div>
             </div>
         </div>
+
+        <!-- 路演视频模块 -->
+        <roadShowList v-if="listState.firstClassifyId==-1" :defaultClassifyId="listState.secClassifyId"></roadShowList>
         </div>
     </div>
     <!-- 筛选 -->

+ 56 - 4
src/views/report/Search.vue

@@ -52,6 +52,16 @@ function goReportDetail(item){
     })
     window.open(url.href,'_blank')
 }
+//跳转详情
+function goVideoDetail(item){
+    const url=router.resolve({
+        path:'/roadshow/detail',
+        query:{
+            code:item.report_code
+        }
+    })
+    window.open(url.href,'_blank')
+}
 
 </script>
 
@@ -71,10 +81,19 @@ function goReportDetail(item){
         </div>
         <div v-if="listState.list.length===0" class="empty">no results</div>
         <div class="report-list-wrap" v-else>
-            <div class="item" v-for="item in listState.list" :key="item.id" @click="goReportDetail(item)">
-                <div class="title" v-html="item.title"></div>
-                <div class="intro" v-html="item.content_sub"></div>
-                <div class="time">{{item.publish_time}}</div>
+            <div class="item" v-for="item in listState.list" :key="item.id" >
+                <div @click="goReportDetail(item)" v-if="item.report_type===0">
+                    <div class="title" v-html="item.title"></div>
+                    <div class="intro" v-html="item.content_sub"></div>
+                    <div class="time">{{item.publish_time}}</div>
+                </div>
+                <div @click="goVideoDetail(item)" v-if="item.report_type===1">
+                    <div class="title" v-html="item.title"></div>
+                    <div class="video-img">
+                        <img :src="item.video_cover_url" alt="">
+                    </div>
+                    <div class="time">{{item.publish_time}}</div>
+                </div>
             </div>
         </div>
     </div>
@@ -148,6 +167,31 @@ function goReportDetail(item){
             line-height: 17px;
             margin: 10px 0;
         }
+        .video-img{
+            width: 216px;
+            height: 132px;
+            margin: 10px 0;
+            img{
+                width: 100%;
+                height: 100%;
+                object-fit: cover;
+                display: block;
+            }
+            position: relative;
+                &::after{
+                    content: '';
+                    display: block;
+                    width: 32px;
+                    height: 32px;
+                    background-image: url('@/assets/icon_play.png');
+                    background-size: cover;
+                    position: absolute;
+                    left: 50%;
+                    top: 50%;
+                    transform: translate(-50%,-50%);
+                    cursor: pointer;
+                }
+        }
         .time{
             font-size: 14px;
             color: #999;
@@ -167,6 +211,14 @@ function goReportDetail(item){
             .time{
                 font-size: 12px;
             }
+            .video-img{
+                width: 100%;
+                height: 200px;
+                &::after{
+                    width: 50px;
+                    height: 50px;
+                }
+            }
         }
     }
 }

+ 140 - 0
src/views/roadShow/Detail.vue

@@ -0,0 +1,140 @@
+<script setup>
+import {ref} from 'vue'
+import {apiRoadShowVideoDetail,apiVideoPlayLog} from '@/api/roadShow'
+import videoPlayBox from '@/components/VideoPlayBox.vue'
+import { useRoute } from 'vue-router'
+
+const route=useRoute()
+
+const code=route.query.code
+const email=route.query.ShareEmail||0
+let info=ref(null)
+// 视频详情
+async function getDetail(){
+    const res=await apiRoadShowVideoDetail({
+        video_code:code,
+        share_email:email
+    })
+    if(res.code===200){
+        info.value=res.data.Video
+    }
+}
+getDetail()
+
+let videoPlayRecordId=0
+//视频播放
+function handleVideoPlay(){
+    if(videoPlayRecordId) return
+    apiVideoPlayLog({
+        id:0,
+        video_id:info.value.id,
+        stop_seconds:0
+    }).then(res=>{
+        if(res.code==200){
+            videoPlayRecordId=res.data.id
+        }
+    })
+}
+
+function handleVideoPause(e){
+    const target=e.target||e.path[0]
+    const t=target.currentTime||0
+    apiVideoPlayLog({
+        id:videoPlayRecordId,
+        video_id:info.value.id,
+        stop_seconds:parseInt(t)
+    }).then(res=>{
+        if(res.code==200){
+            console.log('记录视频播放时长成功');
+        }
+    })
+}
+
+
+
+</script>
+
+<template>
+    <div class="roadshow-video-detail-page">
+        <div class="header-wrap">
+            <span>HORIZON INSIGHTS</span>
+            <div class="search-box" @click="$router.push('/report/search')">
+                <img src="@/assets/search.svg" alt="">
+                <span style="margin-left:10px">search for</span>
+            </div>
+        </div>
+        <div class="detail-wrap" v-if="info">
+            <div class="title">{{info.title}}</div>
+            <videoPlayBox
+                :videoUrl="info.video_url"
+                @play="handleVideoPlay"
+                @pause="handleVideoPause"
+            />
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.header-wrap{
+    text-align: center;
+    font-size: 40px;
+    font-weight: bold;
+    color: var(--el-color-primary);
+    padding: 40px 0;
+    border-bottom: 1px solid#E6E6E6;
+    position: sticky;
+    top: 0;
+    background-color: #fff;
+    z-index: 99;
+    .search-box{
+        position: absolute;
+        top: 50%;
+        transform: translateY(-50%);
+        right: 20px;
+        font-size: 16px;
+        color: #333;
+        font-weight: 500;
+        cursor: pointer;
+        span,img{
+            vertical-align: middle;
+        }
+    }
+    .menu-icon{
+        position: absolute;
+        left: 20px;
+        top: 50%;
+        transform: translateY(-50%);
+        display: none;
+    }
+}
+@media (max-width: 768px){
+    .header-wrap{
+        border: none;
+        font-size: 17px;
+        box-shadow: 0px 4px 20px rgba(180, 180, 180, 0.16);
+        padding: 15px 0;
+        .search-box{
+            span{
+                display: none;
+            }
+        }
+        .menu-icon{
+            display: block;
+            width: 17px;
+        }
+    }
+}
+.detail-wrap{
+    margin-top: 20px;
+    .title{
+        margin-bottom: 20px;
+    }
+}
+@media (max-width: 768px){
+    .detail-wrap{
+        .title{
+            padding: 0 16px;
+        }
+    }
+}
+</style>

+ 300 - 0
src/views/roadShow/List.vue

@@ -0,0 +1,300 @@
+<script setup>
+import {ref,reactive, onMounted, watch} from 'vue'
+import {apiReportClassifyList} from '@/api/report'
+import {apiRoadShowVideoList} from '@/api/roadShow'
+import { useRouter } from 'vue-router'
+
+const props=defineProps({
+    defaultClassifyId:{
+        type:Number,
+        default:0
+    }
+})
+
+const router=useRouter()
+
+watch(
+    ()=>props.defaultClassifyId,
+    (n)=>{
+        if(n&&navList.value.length){
+             navList.value.forEach(item=>{
+                if(item.id==props.defaultClassifyId){
+                    handleChangeFirstClassify(item)
+                }
+            })
+        }
+    }
+)
+
+let navList=ref([])
+async function getReportClassify(){
+    const res=await apiReportClassifyList({classify_type:1})
+    if(res.code===200){
+        const arr=res.data.list||[]
+        navList.value=[
+            {
+                id: 0,
+                classify_name: "ALL",
+                child: []
+            },
+            ...arr,
+        ]
+        if(props.defaultClassifyId){
+            navList.value.forEach(item=>{
+                if(item.id==props.defaultClassifyId){
+                    handleChangeFirstClassify(item)
+                }
+            })
+        }else{
+            initList()
+        }
+    }
+}
+getReportClassify()
+
+let listState=reactive({
+    firstClassifyId:0,
+    secClassifyId:0,
+    secClassifyOpt:[],
+
+    page:1,
+    pageSize:20,
+    list:[],
+    loading:false,
+    finished:false
+})
+
+function handleChangeFirstClassify(item){
+    // if(listState.firstClassifyId===item.id) return
+    listState.firstClassifyId=item.id
+    listState.secClassifyId=0
+    listState.secClassifyOpt=item.child||[]
+    initList()
+}
+
+function handleChangeSecClassify(item){
+    if(listState.secClassifyId===item.id) return
+    listState.secClassifyId=item.id
+    initList()
+}
+
+//初始化列表
+function initList(){
+    listState.page=1
+    listState.finished=false
+    listState.list=[]
+    getVideoList()
+}
+
+
+async function getVideoList(){
+    listState.loading=true
+    const res=await apiRoadShowVideoList({
+        page_size:listState.pageSize,
+        current:listState.page,
+        classify_id_first:listState.firstClassifyId,
+        classify_id_second:listState.secClassifyId
+    })
+    setTimeout(() => {
+        listState.loading=false
+    }, 100);
+    if(res.code===200){
+        const arr=res.data.list||[]
+        listState.list=[...listState.list,...arr]
+        if(arr.length<listState.pageSize){
+            listState.finished=true
+        }
+    }
+}
+
+// 监听页面滚动
+function listenScroll(){
+    window.onscroll=(e)=>{
+        if(listState.loading||listState.finished) return
+        const scrollTop = document.documentElement.scrollTop||document.body.scrollTop;
+        const windowHeight = document.documentElement.clientHeight || document.body.clientHeight;
+        const scrollHeight = document.documentElement.scrollHeight||document.body.scrollHeight;
+        if(scrollTop+windowHeight>=scrollHeight){   //考虑到滚动的位置一般可能会大于一点可滚动的高度,所以这里不能用等于
+            console.log("距顶部"+scrollTop+"可视区高度"+windowHeight+"滚动条总高度"+scrollHeight);
+            listState.page++
+            getVideoList()
+        }   
+    }
+}
+listenScroll()
+
+function goDetail(item){
+    const url=router.resolve({
+        path:'/roadshow/detail',
+        query:{
+            code:item.video_code
+        }
+    })
+    window.open(url.href,'_blank')
+}
+
+
+</script>
+
+<template>
+    <div class="roadshow-list-page">
+        <div class="nav-warp">
+            <div class="first-nav-box">
+                <span 
+                    v-for="item in navList" 
+                    :key="item.id"
+                    :class="item.id===listState.firstClassifyId?'active':''"
+                    @click="handleChangeFirstClassify(item)"
+                >{{item.classify_name}}</span>
+            </div>
+            <div class="sec-nav-box">
+                <span 
+                    v-for="item in listState.secClassifyOpt" 
+                    :key="item.id"
+                    :class="item.id===listState.secClassifyId?'active':''"
+                    @click="handleChangeSecClassify(item)"
+                >{{item.classify_name}}</span>
+            </div>
+        </div>
+        <div class="video-list-wrap">
+            <div class="item" v-for="item in listState.list" :key="item.id" @click="goDetail(item)">
+                <div class="multi-ellipsis-l2 title">{{item.title}}</div>
+                <div class="img">
+                    <img :src="item.video_cover_url" alt="">
+                </div>
+                <div class="time">{{item.publish_time}}</div>
+            </div>
+            <div class="last-item"></div>
+            <div class="last-item"></div>
+        </div>
+    </div>
+</template>
+
+<style lang="scss" scoped>
+.roadshow-list-page{
+    .first-nav-box{
+        padding: 20px 0 28px 0;
+        span{
+            display: inline-block;
+            margin-right: 40px;
+            cursor: pointer;
+            position: relative;
+            &.active::after{
+                display: block;
+                content: '';
+                position: absolute;
+                bottom: -10px;
+                width: 100%;
+                height: 4px;
+                background-color: var(--el-color-primary);
+            }
+            &:hover{
+                color: var(--el-color-primary);
+            }
+        }
+    }
+    .sec-nav-box{
+        span{
+            display: inline-block;
+            padding: 5px 20px;
+            cursor: pointer;
+            background: #F3F3F3;
+            border-radius: 4px;
+            color: #666;
+            margin-right: 20px;
+            &.active{
+                color: var(--el-color-primary);
+            }
+        }
+    }
+    .video-list-wrap{
+        margin-top: 30px;
+        display: flex;
+        flex-wrap: wrap;
+        justify-content: center;
+        .item{
+            width: 384px;
+            padding: 37px 12px 30px 12px;
+            border: 1px solid #E6E6E6;
+            .title{
+                min-height: 46px;
+                padding-bottom: 10px;
+            }
+            .time{
+                text-align: right;
+                margin-top: 10px ;
+                color: #999;
+            }
+            .img{
+                width: 358px;
+                height: 220px;
+                img{
+                    object-fit: cover;
+                    display: block;
+                    cursor: pointer;
+                    width: 100%;
+                    height: 100%;
+                }
+                
+                position: relative;
+                &::after{
+                    content: '';
+                    display: block;
+                    width: 55px;
+                    height: 55px;
+                    background-image: url('@/assets/icon_play.png');
+                    background-size: cover;
+                    position: absolute;
+                    left: 50%;
+                    top: 50%;
+                    transform: translate(-50%,-50%);
+                    cursor: pointer;
+                }
+            }
+        }
+        .last-item{
+            width: 384px;
+            height: 0;
+        }
+    }
+}
+@media (max-width: 768px){
+    .roadshow-list-page{
+        .first-nav-box{
+            padding: 15px 20px 10px 20px;
+            border-bottom: 1px solid #E6E6E6;
+            width: 100vw;
+            position: relative;
+            left: -20px;
+            display: flex;
+            overflow-x: auto;
+            margin-bottom: 10px;
+            span{
+                margin-right: 25px;
+                &.active::after{
+                    bottom: -10px;
+                    height: 2px;
+                }
+            }
+        }
+        .video-list-wrap{
+            display: block;
+            .item{
+                width: 100%;
+                border-top: none;
+                border-left: none;
+                border-right: none;
+                padding: 15px 0;
+                .title{
+                    min-height: 0;
+                    padding-bottom: 5px;
+                }
+                .img{
+                    width: 100%;
+                    height: 200px;
+                }
+            }
+        }
+    }
+}
+</style>