浏览代码

Merge branch 'iteration-course-timer' into online

lex 1 年之前
父节点
当前提交
8bb4b0906e

+ 1 - 1
src/school/orchestra/compontent/photo-detail.tsx

@@ -246,7 +246,7 @@ export default defineComponent({
             }
           })
           if (res?.content?.status === 'success') {
-            showSuccessToast('保存成功')
+            showSuccessToast('已保存到相册')
           } else {
             showFailToast('保存失败')
           }

+ 1 - 1
src/school/save-share-image/index.tsx

@@ -139,7 +139,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存成功')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }

+ 1 - 1
src/school/train-report/month-report.tsx

@@ -206,7 +206,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存成功')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }

+ 1 - 1
src/school/train-report/week-report.tsx

@@ -199,7 +199,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存成功')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }

+ 4 - 8
src/views/coursewarePlay/component/tools/pen.tsx

@@ -60,7 +60,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存成功')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }
@@ -86,15 +86,14 @@ export default defineComponent({
           // const url = await canvas.toDataURL()
           try {
             imgs.image = canvas.toDataURL()
-            
           } catch (error) {
             console.log(error)
           }
-          console.log("🚀 ~ imgs.image:", imgs.image)
+          console.log('🚀 ~ imgs.image:', imgs.image)
           saveImg()
         })
         .catch((error) => {
-          console.log("🚀 ~ error:", error)
+          console.log('🚀 ~ error:', error)
           closeToast()
           imgs.saveLoading = false
           imgs.exported = false
@@ -122,10 +121,7 @@ export default defineComponent({
         {imgs.exported ? (
           <img crossorigin="anonymous" class={styles.img} src={imgs.base64} />
         ) : (
-          <div
-            class={styles.rightItem}
-            onClick={() => props.close()}
-          >
+          <div class={styles.rightItem} onClick={() => props.close()}>
             <svg width="22px" height="20px" viewBox="0 0 22 20">
               <path
                 transform="translate(-1.000000, -2.000000)"

+ 28 - 7
src/views/coursewarePlay/component/video-item/index.tsx

@@ -1,9 +1,8 @@
-import { defineComponent, nextTick, onMounted, reactive, toRefs, watch } from 'vue'
+import { defineComponent, nextTick, onMounted, reactive, toRefs, watch, ref } from 'vue'
 import 'plyr/dist/plyr.css'
 import Plyr from 'plyr'
 import styles from './index.module.less'
-
-import { iconVideoBg, iconLoop, iconLoopActive, iconPlay,  iconPause } from '../../image/icons.json'
+import { iconVideoBg, iconLoop, iconLoopActive, iconPlay, iconPause } from '../../image/icons.json'
 
 export default defineComponent({
   name: 'video-play',
@@ -19,7 +18,7 @@ export default defineComponent({
       default: true
     }
   },
-  emits: ['play', 'pause', 'ended', 'close'],
+  emits: ['play', 'pause', 'ended', 'close', 'seeked', 'seeking', 'waiting', 'timeupdate'],
   setup(props, { emit, expose }) {
     const { item } = toRefs(props)
     const data = reactive({
@@ -144,11 +143,15 @@ export default defineComponent({
       }
     )
     let videoTimer = null as any
+    let videoTimerErrorCount = 0
     const handlePlayVideo = () => {
+      // if (videoTimerErrorCount > 5) {
+      //   return
+      // }
       clearTimeout(videoTimer)
       nextTick(() => {
         data.videoContianerRef.play().catch((err) => {
-          console.log('🚀 ~ err:', err)
+          // console.log('🚀 ~ err:', err)
           videoTimer = setTimeout(() => {
             if (err?.message?.includes('play()')) {
               emit('play')
@@ -157,6 +160,7 @@ export default defineComponent({
           }, 1000)
         })
       })
+      videoTimerErrorCount++
     }
 
     let videoErrorTimer = null as any
@@ -178,10 +182,15 @@ export default defineComponent({
       videoErrorCount++
     }
     const getVideoRef = () => {
-      return data.videoContianerRef;
+      return data.videoContianerRef
+    }
+
+    const getPlyrRef = () => {
+      return data.videoItem
     }
     expose({
-      getVideoRef
+      getVideoRef,
+      getPlyrRef
     })
 
     return () => (
@@ -223,6 +232,18 @@ export default defineComponent({
             changePlayBtn('play')
             emit('ended')
           }}
+          onSeeked={() => {
+            emit('seeked')
+          }}
+          onSeeking={() => {
+            emit('seeking')
+          }}
+          onTimeupdate={() => {
+            emit('timeupdate')
+          }}
+          onWaiting={() => {
+            emit('waiting')
+          }}
           onError={handleErrorVideo}
         ></video>
       </div>

+ 204 - 3
src/views/coursewarePlay/index.tsx

@@ -26,12 +26,14 @@ import { browser } from '@/helpers/utils'
 import { Vue3Lottie } from 'vue3-lottie'
 import playLoadData from './datas/data.json'
 import { usePageVisibility } from '@vant/use'
+import { useInterval, useIntervalFn } from '@vueuse/core'
 import PlayRecordTime from './playRecordTime'
 import { handleCheckVip } from '../hook/useFee'
 import OGuide from '@/components/o-guide'
 import Tool, { ToolItem, ToolType } from './component/tool'
 import Pen from './component/tools/pen'
 import VideoItem from './component/video-item'
+import deepClone from '@/helpers/deep-clone'
 
 export default defineComponent({
   name: 'CoursewarePlay',
@@ -110,6 +112,7 @@ export default defineComponent({
       detail: null as any,
       knowledgePointList: [] as any,
       itemList: [] as any,
+      lookVideoDataList: [] as any, // 观看视频统计数据
       showHead: true,
       isCourse: false,
       isRecordPlay: false,
@@ -177,9 +180,13 @@ export default defineComponent({
             material.content = localData.content.localPath
           }
         }
-
+        const videoData = data.lookVideoDataList.find(
+          (i: any) => i.materialId === material.materialId
+        )
         list.push({
           ...material,
+          moreTime: videoData?.videoBrowseData ? JSON.parse(videoData.videoBrowseData) : [],
+          videoTime: videoData?.videoTime || 0, // 视频时长
           iframeRef: null,
           videoEle: null,
           tabName: name,
@@ -315,10 +322,31 @@ export default defineComponent({
       }
     }
 
+    // 获取学生观看数据
+    const getLookVideoData = async () => {
+      try {
+        const res = await request.get(
+          state.platformApi + `/studentCoursewareMaterialRelation/findByDetailId`,
+          {
+            hideLoading: true,
+            params: {
+              lessonCoursewareDetailId: route.query.id
+            }
+          }
+        )
+        data.lookVideoDataList = res.data || [] // 视频播放数据
+      } catch {
+        //
+      }
+    }
+
     onMounted(async () => {
+      if (state.platformType === 'STUDENT') {
+        await getLookVideoData()
+      }
       await getDetail()
       const hasFree = String(data.detail?.accessScope) === '0'
-      if (!hasFree){
+      if (!hasFree) {
         const hasVip = handleCheckVip()
         if (!hasVip) {
           nextTick(() => {
@@ -623,6 +651,149 @@ export default defineComponent({
       return {}
     })
     let closeModelTimer: any = null
+
+    /**
+     * 统计视频播放时间段
+     */
+    const intervalFnRef = ref() // 定时任务
+    // 播放视频总时长
+    const videoIntervalRef = useInterval(1000, { controls: true })
+    videoIntervalRef.pause()
+    /**
+     * 格式化视屏播放有效时间 - 合并区间
+     * @param intervals [[], []]
+     * @example [[4, 8],[0, 4],[10, 30]]
+     * @returns [[0, 8], [10, 30]]
+     */
+    const formatEffectiveTime = (intervals: any[]) => {
+      const res: any = []
+      intervals.sort((a, b) => a[0] - b[0])
+      let prev = intervals[0]
+      for (let i = 1; i < intervals.length; i++) {
+        const cur = intervals[i]
+        if (prev[1] >= cur[0]) {
+          // 有重合
+          prev[1] = Math.max(cur[1], prev[1])
+        } else {
+          // 不重合,prev推入res数组
+          res.push(prev)
+          prev = cur // 更新 prev
+        }
+      }
+      res.push(prev)
+      // console.log(res, 'formatEffectiveTime')
+
+      return res
+    }
+    /**
+     * 获取数据有效期
+     * @param intervals [[], []]
+     * @returns 0s
+     */
+    const formatTimer = (intervals: any[]) => {
+      const afterIntervals = formatEffectiveTime(intervals)
+      let time = 0
+      afterIntervals.forEach((t: any) => {
+        time += t[1] - t[0]
+      })
+      return time
+    }
+
+    // 保存零时时间
+    // const moreTime: any = ref([]) // 多个观看时间段 已经放到列表里面了
+    let tempTime: any = [] // 临时存储时间
+    const currentTimer = useInterval(1000, { controls: true })
+    // 监听播放状态,
+    watch(
+      () => videoIntervalRef.isActive.value,
+      (newVal: boolean) => {
+        initVideoCount(newVal)
+      }
+    )
+
+    /**
+     * 初始化视频时长
+     * @param newVal 播放状态
+     * @param repeat 是否为定时发送的
+     */
+    const initVideoCount = (newVal: any, repeat = false) => {
+      // console.log('watch', forms.player.currentTime)
+      const activeVideoRef = data.videoItemRef?.getPlyrRef()
+      const initTime = deepClone(tempTime)
+      if (repeat) {
+        if (tempTime.length > 0) {
+          // console.log('join video', tempTime, 'initTime', initTime)
+          tempTime[1] = Math.floor(activeVideoRef.currentTime)
+        }
+      } else {
+        if (newVal) {
+          tempTime[0] = Math.floor(activeVideoRef.currentTime)
+        } else {
+          tempTime[1] = Math.floor(activeVideoRef.currentTime)
+        }
+      }
+
+      // console.log(newVal, repeat, tempTime, tempTime.length, 'videoIntervalRef.isActive.value in')
+      // console.log(activeVideoRef.speed, 'speed')
+
+      if (tempTime.length >= 2) {
+        // console.log(tempTime, 'tempTime', moreTime.value)
+        // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
+        const diffTime = tempTime[1] - tempTime[0] - currentTimer.counter.value > 2
+        // 结束时间,如果 大于开始时间则清除
+        if (tempTime[1] >= tempTime[0] && !diffTime) {
+          data.itemList[popupData.activeIndex].moreTime.push(tempTime)
+          // moreTime.value.push(tempTime)
+        }
+        if (repeat) {
+          tempTime = deepClone(initTime)
+        } else {
+          tempTime = []
+          currentTimer.counter.value = 0
+        }
+      }
+    }
+    // 更新时间
+    const updateStat = async () => {
+      try {
+        const itemList = data.itemList
+        const params: any = []
+        itemList.forEach((item: any) => {
+          if (item.moreTime.length > 0) {
+            const videoBrowseData = formatEffectiveTime(item.moreTime)
+            const time = videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0
+            const temp = {
+              lessonCoursewareDetailId: route.query.id,
+              browseTime: time, // 播放时长
+              videoBrowseData: JSON.stringify(videoBrowseData), // 播放的数据
+              videoTime: item.videoTime, // 视频时长
+              materialId: item.materialId
+            }
+            params.push(temp)
+          }
+        })
+
+        if (params.length > 0) {
+          await request.post('/api-student/studentCoursewareMaterialRelation/save', {
+            data: params
+          })
+        }
+      } catch {
+        //
+      }
+    }
+
+    onMounted(() => {
+      // 间隔多少时间同步数据
+      intervalFnRef.value = useIntervalFn(async () => {
+        // 同步数据时先进行有效时间进行保存
+        initVideoCount(false, true)
+
+        await updateStat()
+        videoIntervalRef.counter.value = 0
+      }, 10000)
+    })
+    /** 统计视频播放时间段 */
     return () => (
       <div id="playContent" class={styles.playContent}>
         <div
@@ -663,15 +834,45 @@ export default defineComponent({
                 onClose={setModelOpen}
                 onPlay={() => {
                   data.videoState = 'play'
+
+                  // 设置视频时长
+                  const videoTime = data.videoItemRef.getPlyrRef().duration || 0
+                  data.itemList[popupData.activeIndex].videoTime = Math.floor(videoTime)
                 }}
                 onPause={() => {
                   clearTimeout(activeData.timer)
                   activeData.model = true
+                  videoIntervalRef.pause()
                 }}
-                onEnded={() => {
+                onEnded={async () => {
                   const _index = popupData.activeIndex + 1
                   if (_index < data.itemList.length) {
                     handleSwipeChange(_index)
+                  } else {
+                    // 说明是最后一个
+                    intervalFnRef.value.pause()
+                    // 同步数据时先进行有效时间进行保存
+                    initVideoCount(false, true)
+                    await updateStat()
+                  }
+                }}
+                onSeeked={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause()
+                }}
+                onSeeking={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause()
+                }}
+                onWaiting={() => {
+                  videoIntervalRef.isActive.value && videoIntervalRef.pause()
+                }}
+                onTimeupdate={() => {
+                  const activeVideoRef = data.videoItemRef?.getPlyrRef()
+                  if (
+                    !videoIntervalRef.isActive.value &&
+                    activeVideoRef?.currentTime > 0 &&
+                    activeVideoRef?.playing
+                  ) {
+                    videoIntervalRef.resume()
                   }
                 }}
               />

+ 1 - 1
src/views/creation/share-model/index.tsx

@@ -56,7 +56,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存至相册')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }

+ 1 - 1
src/views/follow-account/index.tsx

@@ -67,7 +67,7 @@ export default defineComponent({
         }
       })
       if (res?.content?.status === 'success') {
-        showSuccessToast('保存成功')
+        showSuccessToast('已保存到相册')
       } else {
         showFailToast('保存失败')
       }

+ 1 - 1
src/views/mine-orchestra/photo-list/detail.tsx

@@ -126,7 +126,7 @@ export default defineComponent({
             }
           })
           if (res?.content?.status === 'success') {
-            showSuccessToast('保存成功')
+            showSuccessToast('已保存到相册')
           } else {
             showFailToast('保存失败')
           }