Browse Source

Merge branch 'video-tcplayer' into jenkins-main

lex 1 năm trước cách đây
mục cha
commit
241759fcc3

+ 11 - 10
src/components/o-upload-all/index.tsx

@@ -8,6 +8,7 @@ import iconUploader from '@common/images/icon-upload.png'
 import iconUploadClose from '@common/images/icon-upload-close.png'
 import request from '@/helpers/request'
 import { getOssUploadUrl, state } from '@/state'
+import OVideo from '../o-video'
 import { getUploadSign, onOnlyFileUpload } from '@/helpers/oss-file-upload'
 
 export default defineComponent({
@@ -239,11 +240,11 @@ export default defineComponent({
                 {this.uploadType === 'IMAGE' ? (
                   <Image src={item} class={styles.previewImg} fit="cover" />
                 ) : (
-                  <video
-                    ref="videoUpload"
-                    style={{ backgroundColor: '#F8F8F8' }}
+                  <OVideo
                     class={styles.previewImg}
-                    src={item + '#t=1,4'}
+                    style={{ backgroundColor: '#F8F8F8' }}
+                    src={item}
+                    controls={false}
                   />
                 )}
               </div>
@@ -272,11 +273,11 @@ export default defineComponent({
                       {this.uploadType === 'IMAGE' ? (
                         <Image fit="cover" position="center" class={styles.uploadImg} src={item} />
                       ) : (
-                        <video
-                          ref="videoUpload"
+                        <OVideo
                           class={styles.uploadImg}
                           style={{ backgroundColor: '#F8F8F8' }}
-                          src={item + '#t=1,4'}
+                          src={item}
+                          controls={false}
                         />
                       )}
                     </>
@@ -323,11 +324,11 @@ export default defineComponent({
                     {this.uploadType === 'IMAGE' ? (
                       <Image fit="cover" position="center" class={styles.uploadImg} src={item} />
                     ) : (
-                      <video
-                        ref="videoUpload"
+                      <OVideo
                         class={styles.uploadImg}
                         style={{ backgroundColor: '#F8F8F8' }}
-                        src={item + '#t=1,4'}
+                        src={item}
+                        controls={false}
                       />
                     )}
                   </>

+ 12 - 0
src/components/o-video/index.module.less

@@ -31,14 +31,25 @@
       border: 1px solid #fff;
       background-color: rgba(0, 0, 0, 0.2) !important;
     }
+
     .plyr--video .plyr__control:hover {
       background-color: transparent !important;
     }
+
+    .video-js {
+      width: 100%;
+      height: 100%;
+    }
+
+    .tcp-skin .vjs-play-progress {
+      background-color: var(--van-primary) !important;
+    }
   }
 
   .video {
     position: relative;
   }
+
 }
 
 .loadingVideo {
@@ -83,6 +94,7 @@
   line-height: 21px;
   padding-top: 10px;
 }
+
 .freeRate {
   color: #32ffd8;
 }

+ 63 - 60
src/components/o-video/index.tsx

@@ -4,6 +4,8 @@ import Plyr from 'plyr'
 import 'plyr/dist/plyr.css'
 import { Button, Icon, Loading, Toast } from 'vant'
 
+import TCPlayer from 'tcplayer.js'
+import 'tcplayer.js/dist/tcplayer.css'
 import iconVideoPlay from '@/common/images/icon_video_play.png'
 import { browser } from '@/helpers/utils'
 export default defineComponent({
@@ -13,7 +15,10 @@ export default defineComponent({
       type: Object,
       default: () => {}
     },
-    controls: Boolean,
+    controls: {
+      type: Boolean,
+      default: true
+    },
     height: String,
     src: {
       type: String,
@@ -47,6 +52,7 @@ export default defineComponent({
   emits: ['exitfullscreen'],
   data() {
     return {
+      videoID: 'video' + Date.now() + Math.floor(Math.random() * 100),
       player: null as any,
       loading: true // 首次进入加载中
     }
@@ -74,70 +80,63 @@ export default defineComponent({
       //   'download ' ,  // 显示一个下载按钮,其中包含指向当前源或您在选项中指定的自定义 URL 的链接
       //   'fullscreen' ,  // 切换全屏
       // ] ;
-      const controls = ['play-large', 'play', 'progress', 'captions', 'fullscreen']
-      if (this.currentTime) {
-        controls.push('current-time')
-      }
-      const params: any = {
-        controls: controls,
-        ...this.setting,
-        invertTime: false
-      }
+      const Button = TCPlayer.getComponent('Button')
+      const BigPlayButton = TCPlayer.getComponent('BigPlayButton')
+      BigPlayButton.prototype.createEl = function () {
+        const el = Button.prototype.createEl.call(this)
+        const _html =
+          '<button><svg width="41px"height="41px"viewBox="0 0 41 41"version="1.1"xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"><g stroke="none"stroke-width="1"fill="none"fill-rule="evenodd"><g transform="translate(-167.000000, -155.000000)"><g transform="translate(0.000000, 85.000000)"><g transform="translate(158.000000, 70.000000)"><g transform="translate(9.000000, 0.000000)"><circle id="椭圆形"stroke="#FFFFFF"fill-opacity="0.1"fill="#D8D8D8"cx="20.5"cy="20.5"r="20"></circle><path d="M14.5483871,27.6859997 L14.5483871,13.4342349 C14.5480523,12.8729571 14.8729597,12.356555 15.3949624,12.0887034 C15.9169651,11.8208518 16.5522696,11.8445472 17.0503046,12.1504437 L28.6530473,19.2778563 C29.1119763,19.5602271 29.3887725,20.0426422 29.3887725,20.5601173 C29.3887725,21.0775924 29.1119763,21.5600075 28.6530473,21.8423783 L17.0503046,28.9697909 C16.5522696,29.2756874 15.9169651,29.2993828 15.3949624,29.0315312 C14.8729597,28.7636796 14.5480523,28.2472775 14.5483871,27.6859997 Z"id="路径"fill="#FFFFFF"fill-rule="nonzero"></path></g></g></g></g></g></svg></button>'
 
-      if (browser().iPhone) {
-        params.fullscreen = {
-          enabled: true,
-          fallback: 'force',
-          iosNative: true
-        }
+        el.appendChild(
+          TCPlayer.dom.createEl('div', {
+            className: 'vjs-button-icon',
+            innerHTML: _html
+          })
+        )
+        return el
       }
+      this.player = TCPlayer(this.videoID, {
+        appID: '',
+        controls: this.controls
+      }) // player-container-id 为播放器容器 ID,必须与 html 中一致
+      if (this.player) {
+        this.player.src(this.src) // url 播放地址
+        this.player.poster(this.poster || '')
 
-      this.player = new Plyr((this as any).$refs.video, params)
-
-      // fullscreen: {
-      //     enabled: true,
-      //     fallback: 'force',
-      //     iosNative: true
-      //   }
-      this.player.elements.container
-        ? (this.player.elements.container.style.height = this.height || '210px')
-        : null
-
-      if (this.preload === 'none') {
-        this.loading = false
-      }
-      this.player.on('loadedmetadata', () => {
-        this.loading = false
-        this.domPlayVisibility(false)
-      })
-
-      this.player.on('play', () => {
-        this.onPlay && this.onPlay(this.player)
-      })
-
-      this.player.on('enterfullscreen', () => {
-        console.log('fullscreen')
-        const i = document.createElement('i')
-        i.id = 'fullscreen-back'
-        i.className = 'van-icon van-icon-arrow-left video-back'
-        i.addEventListener('click', () => {
-          this.player.fullscreen.exit()
+        if (this.preload === 'none') {
+          this.loading = false
+        }
+        this.player.on('loadstart', () => {
+          this.loading = false
+          this.domPlayVisibility(false)
+        })
+        this.player.on('play', () => {
+          this.onPlay && this.onPlay(this.player)
         })
-        console.log(document.getElementsByClassName('plyr'))
-        document.getElementsByClassName('plyr')[0].appendChild(i)
-      })
 
-      this.player.on('exitfullscreen', () => {
-        console.log('exitfullscreen')
-        const i = document.getElementById('fullscreen-back')
-        i && i.remove()
-        this.$emit('exitfullscreen')
-      })
+        this.player.on('fullscreenchange', () => {
+          if (this.player.isFullscreen()) {
+            console.log('fullscreen')
+            const i = document.createElement('i')
+            i.id = 'fullscreen-back'
+            i.className = 'van-icon van-icon-arrow-left video-back'
+            i.addEventListener('click', () => {
+              this.player.exitFullscreen()
+            })
+            // console.log(document.getElementsByClassName('video-js'))
+            document.getElementsByClassName('video-js')[0].appendChild(i)
+          } else {
+            console.log('exitfullscreen')
+            const i = document.getElementById('fullscreen-back')
+            i && i.remove()
+          }
+        })
+      }
     },
     // 操作功能
     domPlayVisibility(hide = true) {
-      const controls = document.querySelector('.plyr__controls')
-      const controls2 = document.querySelector('.plyr__control--overlaid')
+      const controls = document.querySelector('.vjs-big-play-button')
+      const controls2 = document.querySelector('.vjs-control-bar')
       if (hide) {
         controls?.setAttribute('style', 'display:none')
         controls2?.setAttribute('style', 'display:none')
@@ -150,22 +149,26 @@ export default defineComponent({
     },
 
     onReplay() {
-      this.player.restart()
+      this.player.currentTime(0)
       this.player.play()
       this.domPlayVisibility(false)
     },
     onStop() {
-      this.player.stop()
+      this.player.currentTime(0)
+      this.player.pause()
     }
   },
   unmounted() {
-    this.player?.destroy()
+    this.player?.pause()
+    this.player?.src('')
+    this.player?.dispose()
   },
   render() {
     return (
       <div class={styles['video-container']}>
         <video
           ref="video"
+          id={this.videoID}
           class={styles['video']}
           src={this.src}
           playsinline={this.playsinline}

+ 120 - 63
src/student/pre-register-active/compontent-show/video-show.tsx

@@ -2,10 +2,12 @@ import { defineComponent, nextTick, onMounted, onUnmounted, reactive, ref, watch
 import styles from '../video.module.less'
 import { Button } from 'vant'
 import { browser } from '@/helpers/utils'
-import Plyr from 'plyr'
-import 'plyr/dist/plyr.css'
+// import Plyr from 'plyr'
+// import 'plyr/dist/plyr.css'
 import { useRoute } from 'vue-router'
 import deepClone from '@/helpers/deep-clone'
+import TCPlayer from 'tcplayer.js'
+import 'tcplayer.js/dist/tcplayer.css'
 
 export default defineComponent({
   name: 'pre-register',
@@ -16,6 +18,7 @@ export default defineComponent({
     console.log(route.query, 'query')
     const forms = reactive({
       coverImg: route.query.coverImg,
+      videoID: 'video' + Date.now() + Math.floor(Math.random() * 100),
       introductionVideo: route.query.introductionVideo as any,
       id: null as any,
       videoDetails: deepClone(video),
@@ -32,74 +35,128 @@ export default defineComponent({
      * 5、点击视频进度或拖动进度时,时间暂停
      */
     const _init = () => {
-      const controls = [
-        'play-large',
-        'play',
-        'progress',
-        'captions',
-        'current-time',
-        'duration',
-        'settings',
-        'fullscreen'
-      ]
-      const params: any = {
-        controls: controls,
-        settings: ['speed'],
-        speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
-        i18n: {
-          speed: '速度',
-          normal: '默认'
-        },
-        invertTime: false
-      }
+      // const controls = [
+      //   'play-large',
+      //   'play',
+      //   'progress',
+      //   'captions',
+      //   'current-time',
+      //   'duration',
+      //   'settings',
+      //   'fullscreen'
+      // ]
+      // const params: any = {
+      //   controls: controls,
+      //   settings: ['speed'],
+      //   speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
+      //   i18n: {
+      //     speed: '速度',
+      //     normal: '默认'
+      //   },
+      //   invertTime: false
+      // }
 
-      if (browser().iPhone) {
-        params.fullscreen = {
-          enabled: true,
-          fallback: 'force',
-          iosNative: true
-        }
-      }
+      // if (browser().iPhone) {
+      //   params.fullscreen = {
+      //     enabled: true,
+      //     fallback: 'force',
+      //     iosNative: true
+      //   }
+      // }
 
-      const times: any = []
-      deepClone(forms.videoDetails).forEach((item: any) => {
-        times.push({
-          time: item.startNode,
-          label: item.desc
-        })
-      })
-      params.markers = { enabled: true, points: times }
+      // const times: any = []
+      // deepClone(forms.videoDetails).forEach((item: any) => {
+      //   times.push({
+      //     time: item.startNode,
+      //     label: item.desc
+      //   })
+      // })
+      // params.markers = { enabled: true, points: times }
 
-      forms.player = new Plyr('#register-video', params)
+      // forms.player = new Plyr('#register-video', params)
 
-      forms.player.on('loadedmetadata', () => {
-        checkVideoDetails(forms.player.currentTime)
-      })
+      // forms.player.on('loadedmetadata', () => {
+      //   checkVideoDetails(forms.player.currentTime)
+      // })
 
-      // 如何视频在缓存不会触发
-      forms.player.on('timeupdate', (e: any) => {
-        // 时间变化时更新每一段的状态
-        console.log(forms.player.currentTime, 'forms.player.currentTime', e)
-        checkVideoDetails(forms.player.currentTime)
-      })
+      // // 如何视频在缓存不会触发
+      // forms.player.on('timeupdate', (e: any) => {
+      //   // 时间变化时更新每一段的状态
+      //   console.log(forms.player.currentTime, 'forms.player.currentTime', e)
+      //   checkVideoDetails(forms.player.currentTime)
+      // })
+
+      // forms.player.on('enterfullscreen', () => {
+      //   console.log('fullscreen')
+      //   const i = document.createElement('i')
+      //   i.id = 'fullscreen-back'
+      //   i.className = 'van-icon van-icon-arrow-left video-back'
+      //   i.addEventListener('click', () => {
+      //     forms.player.fullscreen.exit()
+      //   })
+      //   console.log(document.getElementsByClassName('plyr'))
+      //   document.getElementsByClassName('plyr')[0].appendChild(i)
+      // })
+
+      // forms.player.on('exitfullscreen', () => {
+      //   console.log('exitfullscreen')
+      //   const i = document.getElementById('fullscreen-back')
+      //   i && i.remove()
+      // })
+      const Button = TCPlayer.getComponent('Button')
+      const BigPlayButton = TCPlayer.getComponent('BigPlayButton')
+      BigPlayButton.prototype.createEl = function () {
+        const el = Button.prototype.createEl.call(this)
+        const _html =
+          '<button><svg width="41px"height="41px"viewBox="0 0 41 41"version="1.1"xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"><g stroke="none"stroke-width="1"fill="none"fill-rule="evenodd"><g transform="translate(-167.000000, -155.000000)"><g transform="translate(0.000000, 85.000000)"><g transform="translate(158.000000, 70.000000)"><g transform="translate(9.000000, 0.000000)"><circle id="椭圆形"stroke="#FFFFFF"fill-opacity="0.1"fill="#D8D8D8"cx="20.5"cy="20.5"r="20"></circle><path d="M14.5483871,27.6859997 L14.5483871,13.4342349 C14.5480523,12.8729571 14.8729597,12.356555 15.3949624,12.0887034 C15.9169651,11.8208518 16.5522696,11.8445472 17.0503046,12.1504437 L28.6530473,19.2778563 C29.1119763,19.5602271 29.3887725,20.0426422 29.3887725,20.5601173 C29.3887725,21.0775924 29.1119763,21.5600075 28.6530473,21.8423783 L17.0503046,28.9697909 C16.5522696,29.2756874 15.9169651,29.2993828 15.3949624,29.0315312 C14.8729597,28.7636796 14.5480523,28.2472775 14.5483871,27.6859997 Z"id="路径"fill="#FFFFFF"fill-rule="nonzero"></path></g></g></g></g></g></svg></button>'
 
-      forms.player.on('enterfullscreen', () => {
-        console.log('fullscreen')
-        const i = document.createElement('i')
-        i.id = 'fullscreen-back'
-        i.className = 'van-icon van-icon-arrow-left video-back'
-        i.addEventListener('click', () => {
-          forms.player.fullscreen.exit()
+        el.appendChild(
+          TCPlayer.dom.createEl('div', {
+            className: 'vjs-button-icon',
+            innerHTML: _html
+          })
+        )
+        return el
+      }
+      forms.player = TCPlayer('register-video', {
+        appID: '',
+        controls: true,
+        plugins: {
+          ProgressMarker: true
+        }
+      }) // player-container-id 为播放器容器 ID,必须与 html 中一致
+      if (forms.player) {
+        forms.player.src(forms.introductionVideo) // url 播放地址
+        forms.player.poster(forms.coverImg || '')
+
+        forms.player.on('loadedmetadata', () => {
+          checkVideoDetails(forms.player.currentTime())
         })
-        console.log(document.getElementsByClassName('plyr'))
-        document.getElementsByClassName('plyr')[0].appendChild(i)
-      })
 
-      forms.player.on('exitfullscreen', () => {
-        console.log('exitfullscreen')
-        const i = document.getElementById('fullscreen-back')
-        i && i.remove()
-      })
+        // 如何视频在缓存不会触发
+        forms.player.on('timeupdate', (e: any) => {
+          // 时间变化时更新每一段的状态
+          console.log(forms.player.currentTime(), 'forms.player.currentTime()', e)
+          checkVideoDetails(forms.player.currentTime())
+        })
+
+        forms.player.on('fullscreenchange', () => {
+          if (forms.player.isFullscreen()) {
+            console.log('fullscreen')
+            const i = document.createElement('i')
+            i.id = 'fullscreen-back'
+            i.className = 'van-icon van-icon-arrow-left video-back'
+            i.addEventListener('click', () => {
+              forms.player.exitFullscreen()
+            })
+            document.getElementsByClassName('video-js')[0].appendChild(i)
+          } else {
+            console.log('exitfullscreen')
+            const i = document.getElementById('fullscreen-back')
+            i && i.remove()
+          }
+        })
+      }
     }
 
     const checkVideoDetails = (time: number) => {
@@ -170,7 +227,7 @@ export default defineComponent({
               <span
                 class={[item.id === forms.id ? styles.active : '']}
                 onClick={() => {
-                  forms.player.currentTime = item.startNode
+                  forms.player.currentTime(item.startNode)
                 }}
               >
                 {item.desc}

+ 517 - 0
src/student/pre-register-active/video copy.tsx

@@ -0,0 +1,517 @@
+import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
+import styles from './video.module.less'
+import { Button, Loading } from 'vant'
+import { browser } from '@/helpers/utils'
+import Plyr from 'plyr'
+import 'plyr/dist/plyr.css'
+import { useInterval, useIntervalFn } from '@vueuse/core'
+import { useRoute, useRouter } from 'vue-router'
+import request from '@/helpers/request'
+import qs from 'query-string'
+import { usePageVisibility } from '@vant/use'
+
+export default defineComponent({
+  name: 'pre-register',
+  setup() {
+    const route = useRoute()
+    const router = useRouter()
+    const pageVisibility = usePageVisibility()
+    const openId = sessionStorage.getItem('active-open-id')
+    // 页面定时
+    const pageTimer = useInterval(1000, { controls: true })
+    pageTimer.pause()
+
+    const forms = reactive({
+      coverImg: '',
+      introductionVideo: '',
+      introductionVideoTime: 0, // 视频总时长
+      videoBrowsePoint: 0, // 视频最后观看点
+      saveId: route.query.saveId,
+      orchestraId: route.query.id,
+      openId: route.query.openId || openId,
+      loading: false,
+      player: null as any,
+      playerSpeed: 1,
+      intervalFnRef: null as any,
+      videoDetails: [] as any, // 节点列表
+      pointVideo: {} as any, // 需要处理有效的时间段
+      pointVideoTime: 0, // 有效时长
+      videoSelectId: null, // 选中的编号
+      isPageHide: false // 处理页面返回没有刷新的问题
+    })
+
+    // 播放视频总时长
+    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)
+
+      return formatEffectiveTimeToAfter(res)
+    }
+
+    const formatEffectiveTimeToAfter = (res: any[]) => {
+      // 格式化有效时间
+      const effective: any = []
+      const startNode = forms.pointVideo.startNode
+      const endNode = forms.pointVideo.endNode
+      res.forEach((item: any) => {
+        // 开始时间大于 设置时间
+        if (item[0] >= startNode && item[1] <= endNode) {
+          effective.push(item)
+        }
+        if (item[0] >= startNode && item[1] <= endNode && item[1] >= endNode) {
+          effective.push([item[0], endNode])
+        }
+        if (item[0] < startNode && item[1] > startNode && item[1] < endNode) {
+          effective.push(startNode, item[1])
+        }
+      })
+      return effective
+    }
+
+    /**
+     * 获取数据有效期
+     * @param intervals [[], []]
+     * @returns 0s
+     */
+    const formatTimer = (intervals: any[]) => {
+      const afterIntervals = formatEffectiveTime(intervals)
+      console.log(afterIntervals, 'afterIntervals')
+      let time = 0
+      afterIntervals.forEach((t: any) => {
+        time += t[1] - t[0]
+      })
+      return time
+    }
+
+    const checkVideoDetails = (time: number) => {
+      forms.videoDetails.forEach((item: any) => {
+        if (item.startNode <= time && time <= item.endNode) {
+          forms.videoSelectId = item.id
+        }
+      })
+    }
+
+    /**
+     * 视屏累计时长
+     * 1、视屏开始播放时-开始计时
+     * 2、视频暂停时暂停-停止计时
+     * 3、视频加载时-停止计时
+     * 4、视频倍数播放时,时间正常计时
+     * 5、点击视频进度或拖动进度时,时间暂停
+     */
+    const _init = () => {
+      const controls = [
+        'play-large',
+        'play',
+        'progress',
+        'captions',
+        'current-time',
+        'duration',
+        'settings',
+        'fullscreen'
+      ]
+      const params: any = {
+        controls: controls,
+        settings: ['speed'],
+        speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
+        i18n: {
+          speed: '速度',
+          normal: '默认'
+        },
+        autoplay: false,
+        invertTime: false
+      }
+
+      if (browser().iPhone) {
+        params.fullscreen = {
+          enabled: true,
+          fallback: 'force',
+          iosNative: true
+        }
+      }
+      const times: any = []
+      forms.videoDetails.forEach((item: any) => {
+        times.push({
+          time: item.startNode,
+          label: item.desc
+        })
+      })
+      params.markers = { enabled: true, points: times }
+
+      forms.player = new Plyr('#register-video', params)
+
+      forms.player.on('ready', (item: any) => {
+        console.log('ready', item)
+
+        // forms.player.pause()
+      })
+      forms.player.on('loadedmetadata', () => {
+        console.log('loadedmetadata')
+        forms.loading = false
+        forms.player.currentTime = forms.videoBrowsePoint
+        checkVideoDetails(forms.player.currentTime)
+      })
+
+      // 速度变化时
+      forms.player.on('ratechange', () => {
+        forms.playerSpeed =
+          forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
+      })
+
+      forms.player.on('seeking', () => {
+        console.log('seeking')
+        videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      })
+
+      // // 拖动结束时
+      forms.player.on('seeked', () => {
+        console.log('seeked')
+        videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      })
+
+      // 正在搜索中
+      forms.player.on('waiting', () => {
+        // console.log('waiting pause')
+        videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      })
+
+      // 如何视频在缓存不会触发
+      forms.player.on('timeupdate', () => {
+        console.log('timeupdate', forms.player.currentTime)
+        // 时间变化时更新每一段的状态
+        checkVideoDetails(forms.player.currentTime)
+        // 判断视频计时器是否暂停,如果暂停则恢复
+        // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
+        if (
+          !videoIntervalRef.isActive.value &&
+          forms.player.currentTime > 0 &&
+          forms.player.playing
+        ) {
+          // console.log('timeupdate play')
+          videoIntervalRef.resume()
+        }
+      })
+
+      // 视屏播放时暂停
+      forms.player.on('ended', () => {
+        forms.player.pause()
+      })
+
+      // 开始播放
+      forms.player.on('play', () => {
+        console.log('play')
+        // 判断视频计时器是否暂停,如果暂停则恢复
+        videoIntervalRef.resume()
+      })
+
+      // 暂停播放
+      forms.player.on('pause', () => {
+        console.log('pause', videoIntervalRef.isActive.value)
+
+        videoIntervalRef.pause()
+      })
+
+      forms.player.on('enterfullscreen', () => {
+        console.log('fullscreen')
+        const i = document.createElement('i')
+        i.id = 'fullscreen-back'
+        i.className = 'van-icon van-icon-arrow-left video-back'
+        i.addEventListener('click', () => {
+          forms.player.fullscreen.exit()
+        })
+        console.log(document.getElementsByClassName('plyr'))
+        document.getElementsByClassName('plyr')[0].appendChild(i)
+      })
+
+      forms.player.on('exitfullscreen', () => {
+        console.log('exitfullscreen')
+        const i = document.getElementById('fullscreen-back')
+        i && i.remove()
+      })
+      checkVideoDetails(0)
+    }
+
+    // 保存零时时间
+    const moreTime: any = ref([]) // 多个观看时间段
+    let tempTime: any = [] // 临时存储时间
+
+    const currentTimer = useInterval(1000, { controls: true })
+    // 监听播放状态,
+    watch(
+      () => videoIntervalRef.isActive.value,
+      (newVal: boolean) => {
+        initVideoCount(newVal)
+      }
+    )
+
+    const initVideoCount = (newVal: any) => {
+      // console.log(newVal, 'videoIntervalRef.isActive.value in')
+      // console.log('watch', forms.player.currentTime)
+      // console.log('保留两个小数:', forms.player.currentTime.toFixed(2))
+      // console.log('向下取整:', Math.floor(forms.player.currentTime))
+      // console.log('向上取整:', Math.ceil(forms.player.currentTime))
+      // console.log('四舍五入:', Math.round(forms.player.currentTime))
+      if (newVal) {
+        tempTime[0] = Math.floor(forms.player.currentTime)
+      } else {
+        tempTime[1] = Math.floor(forms.player.currentTime)
+      }
+      // console.log(forms.player.speed, 'speed')
+
+      if (tempTime.length >= 2) {
+        // console.log(tempTime, 'tempTime', moreTime.value)
+        // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
+        const diffTime =
+          tempTime[1] - tempTime[0] - currentTimer.counter.value * forms.playerSpeed > 2
+        // console.log(diffTime, 'diffTime', currentTimer.counter.value, 'value')
+        // 结束时间,如果 大于开始时间则清除
+        if (tempTime[1] >= tempTime[0] && !diffTime) moreTime.value.push(tempTime)
+        tempTime = []
+        currentTimer.counter.value = 0
+      }
+
+      // console.log('观看的时间', moreTime)
+    }
+
+    watch(pageVisibility, (value: any) => {
+      console.log('watch', value)
+      if (value == 'hidden') {
+        forms.player.pause()
+      }
+    })
+
+    // 更新时间
+    const updateStat = async (pageBrowseTime = 10) => {
+      try {
+        const videoBrowseData = moreTime.value.length > 0 ? formatEffectiveTime(moreTime.value) : []
+        // console.log(moreTime.value, videoBrowseData, 'video')
+        const time = videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0
+        // const videoCountTime = videoIntervalRef?.counter.value
+        // 判断如何视屏播放时间大于视屏播放有效时间则说明数据有问题,进行重置数据
+        const rate = Math.floor((time / Math.floor(forms.pointVideoTime)) * 100)
+        // console.log('videoIntervalRef?.counter.value', videoIntervalRef?.counter.value)
+        await request.post('/api-student/open/studentBrowseRecord/updateStat', {
+          data: {
+            id: forms.saveId,
+            pageBrowseTime, // 固定10秒
+            videoBrowseData: JSON.stringify(videoBrowseData), // 视屏播放数据
+            videoBrowseDataTime: time || 0, // 有效的视频观看时长
+            videoBrowsePercentage: rate || 0, // 有效的视频观看时长百分比
+            videoBrowseTime: videoIntervalRef?.counter.value, // 视频观看时长
+            videoBrowsePoint: Math.floor(forms.player.currentTime || 0) // 视频最后观看点 - 向下取整
+          }
+        })
+      } catch {
+        //
+      }
+    }
+
+    // 提交
+    const onSubmit = async () => {
+      try {
+        forms.player.pause() // 视屏
+        forms.intervalFnRef?.pause() // 页面订时器
+        currentTimer.pause()
+        videoIntervalRef.pause()
+        // 页面计时暂停
+        pageTimer.pause()
+        initVideoCount(videoIntervalRef.isActive.value)
+
+        await updateStat()
+        window.location.href =
+          window.location.origin +
+          window.location.pathname +
+          '/project/preRegister.html?' +
+          qs.stringify({
+            orchestraId: forms.orchestraId,
+            openId: forms.openId
+          })
+
+        // window.location.href =
+        //   window.location.origin +
+        //   '/project/preRegister.html?' +
+        //   qs.stringify({
+        //     orchestraId: forms.orchestraId,
+        //     openId: forms.openId
+        //   })
+      } catch (e) {
+        console.log(e, 'e')
+        // 还原
+        forms.intervalFnRef?.resume()
+        pageTimer.resume()
+        currentTimer.resume()
+      }
+    }
+
+    onMounted(async () => {
+      try {
+        const { data } = await request.get('/api-student/open/studentBrowseRecord/query', {
+          params: {
+            openId: forms.openId,
+            orchestraId: forms.orchestraId
+          }
+        })
+        forms.videoBrowsePoint = data.videoBrowsePoint || 0
+        if (forms.player) {
+          forms.player.currentTime = data.videoBrowsePoint || 0
+        }
+        forms.introductionVideo = data.introductionVideo
+        forms.introductionVideoTime = data.introductionVideoTime
+        forms.coverImg = data.coverImg
+        moreTime.value = data.videoBrowseData ? JSON.parse(data.videoBrowseData) : []
+
+        const videoDetails = data.videoDetails || []
+        videoDetails.forEach((video: any) => {
+          forms.videoDetails.push({
+            startNode: video.startNode,
+            endNode: video.endNode,
+            desc: video.desc,
+            id: video.id
+          })
+          if (video.pointFlag) {
+            forms.pointVideo = video
+            forms.pointVideoTime = video.endNode - video.startNode
+          }
+        })
+
+        _init()
+        // 间隔多少时间同步数据
+        forms.intervalFnRef = useIntervalFn(async () => {
+          // 页面时间恢复
+          pageTimer.counter.value = 0
+          pageTimer.resume()
+          await updateStat()
+          videoIntervalRef.counter.value = 0
+        }, 10000)
+      } catch {
+        //
+      }
+    })
+
+    onUnmounted(() => {
+      forms.intervalFnRef?.pause()
+      currentTimer.pause()
+      // 页面计时暂停
+      pageTimer.pause()
+    })
+
+    // 判断是否有openId
+    if (!forms.openId) {
+      router.replace({
+        path: '/pre-register-video',
+        query: {
+          id: forms.orchestraId
+        }
+      })
+    }
+
+    const onPageShow = () => {
+      console.log(forms.isPageHide, 'showInfo')
+      if (forms.isPageHide) {
+        window.location.reload()
+      }
+    }
+    // 处理监听页面返回不刷新的问题
+    window.addEventListener('pageshow', onPageShow)
+
+    const onPageHide = () => {
+      console.log(forms.isPageHide, 'showInfo')
+      forms.isPageHide = true
+    }
+    window.addEventListener('pagehide', onPageHide)
+
+    onUnmounted(() => {
+      window.removeEventListener('pageshow', onPageShow)
+      window.removeEventListener('pagehide', onPageHide)
+    })
+    return () => (
+      <div class={styles['pre-register-video']}>
+        <div class={styles.videoContainer}>
+          <div class={styles['video-content']}>
+            <video
+              id="register-video"
+              class={styles['video']}
+              src={forms.introductionVideo}
+              // src={
+              //   'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/1684981545808.mp4?time' + Date.now()
+              // }
+              playsinline={true}
+              poster={forms.coverImg}
+              preload="auto"
+            ></video>
+            {/* 加载视频使用 */}
+            {forms.loading && (
+              <div class={styles.loadingVideo}>
+                <Loading
+                  size={36}
+                  color="#FF8057"
+                  vertical
+                  style={{ height: '100%', justifyContent: 'center' }}
+                >
+                  加载中...
+                </Loading>
+              </div>
+            )}
+          </div>
+        </div>
+        <div class={styles.videoCount}>
+          <div class={styles.videoCountContent}>
+            {forms.videoDetails.map((item: any) => (
+              <span
+                class={[item.id === forms.videoSelectId ? styles.active : '']}
+                onClick={() => {
+                  forms.player.currentTime = item.startNode
+                  forms.player.play()
+                  forms.videoBrowsePoint = item.startNode
+                  checkVideoDetails(forms.player.currentTime)
+                }}
+              >
+                {item.desc}
+              </span>
+            ))}
+          </div>
+        </div>
+        <div class={styles.messageContainer}>
+          <div class={styles.messageContent}>
+            <p>家长您好!</p>
+            <p class={styles.c1}>
+              请家长们合理安排时间,<span>认真观看</span>家长会内容。在<span>详细了解</span>
+              所有要求后,有意向让孩子加入乐团的家长,请在<span>明晚20:00前</span>,为孩子完成
+              <span>乐团报名</span>。
+            </p>
+            <p class={styles.c1}>
+              下周,专业老师将针对意向入团学员进行身体条件确认。谢谢各位的支持!
+            </p>
+            <p class={styles.bottom}>
+              注:乐团于下学期正式开始训练,训练时间下学期开学前另行通知,训练时间会与学校其他社团错开,家长无需担心时间冲突问题。
+            </p>
+          </div>
+
+          <Button class={styles.submitBtn} onClick={onSubmit}></Button>
+        </div>
+      </div>
+    )
+  }
+})

+ 7 - 0
src/student/pre-register-active/video.module.less

@@ -93,6 +93,13 @@
         }
       }
     }
+
+    .video-js {
+      width: 100%;
+      height: 100%;
+      border-radius: 30px;
+      overflow: hidden;
+    }
   }
 
   .video {

+ 245 - 121
src/student/pre-register-active/video.tsx

@@ -2,8 +2,10 @@ import { defineComponent, onMounted, onUnmounted, reactive, ref, watch } from 'v
 import styles from './video.module.less'
 import { Button, Loading } from 'vant'
 import { browser } from '@/helpers/utils'
-import Plyr from 'plyr'
-import 'plyr/dist/plyr.css'
+// import Plyr from 'plyr'
+// import 'plyr/dist/plyr.css'
+import TCPlayer from 'tcplayer.js'
+import 'tcplayer.js/dist/tcplayer.css'
 import { useInterval, useIntervalFn } from '@vueuse/core'
 import { useRoute, useRouter } from 'vue-router'
 import request from '@/helpers/request'
@@ -23,6 +25,7 @@ export default defineComponent({
     pageTimer.pause()
 
     const forms = reactive({
+      videoID: 'video' + Date.now() + Math.floor(Math.random() * 100),
       coverImg: '',
       introductionVideo: '',
       introductionVideoTime: 0, // 视频总时长
@@ -144,136 +147,256 @@ export default defineComponent({
      * 5、点击视频进度或拖动进度时,时间暂停
      */
     const _init = () => {
-      const controls = [
-        'play-large',
-        'play',
-        'progress',
-        'captions',
-        'current-time',
-        'duration',
-        'settings',
-        'fullscreen'
-      ]
-      const params: any = {
-        controls: controls,
-        settings: ['speed'],
-        speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
-        i18n: {
-          speed: '速度',
-          normal: '默认'
-        },
-        autoplay: false,
-        invertTime: false
+      // const controls = [
+      //   'play-large',
+      //   'play',
+      //   'progress',
+      //   'captions',
+      //   'current-time',
+      //   'duration',
+      //   'settings',
+      //   'fullscreen'
+      // ]
+      // const params: any = {
+      //   controls: controls,
+      //   settings: ['speed'],
+      //   speed: { selected: 1, options: [0.5, 1, 1.5, 2] },
+      //   i18n: {
+      //     speed: '速度',
+      //     normal: '默认'
+      //   },
+      //   autoplay: false,
+      //   invertTime: false
+      // }
+
+      // if (browser().iPhone) {
+      //   params.fullscreen = {
+      //     enabled: true,
+      //     fallback: 'force',
+      //     iosNative: true
+      //   }
+      // }
+      // const times: any = []
+      // forms.videoDetails.forEach((item: any) => {
+      //   times.push({
+      //     time: item.startNode,
+      //     label: item.desc
+      //   })
+      // })
+      // params.markers = { enabled: true, points: times }
+
+      // forms.player = new Plyr('#register-video', params)
+
+      // forms.player.on('ready', (item: any) => {
+      //   console.log('ready', item)
+
+      //   // forms.player.pause()
+      // })
+      // forms.player.on('loadedmetadata', () => {
+      //   console.log('loadedmetadata')
+      //   forms.loading = false
+      //   forms.player.currentTime() = forms.videoBrowsePoint
+      //   checkVideoDetails(forms.player.currentTime())
+      // })
+
+      // // 速度变化时
+      // forms.player.on('ratechange', () => {
+      //   forms.playerSpeed =
+      //     forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
+      // })
+
+      // forms.player.on('seeking', () => {
+      //   console.log('seeking')
+      //   videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      // })
+
+      // // // 拖动结束时
+      // forms.player.on('seeked', () => {
+      //   console.log('seeked')
+      //   videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      // })
+
+      // // 正在搜索中
+      // forms.player.on('waiting', () => {
+      //   // console.log('waiting pause')
+      //   videoIntervalRef.isActive.value && videoIntervalRef.pause()
+      // })
+
+      // // 如何视频在缓存不会触发
+      // forms.player.on('timeupdate', () => {
+      //   console.log('timeupdate', forms.player.currentTime())
+      //   // 时间变化时更新每一段的状态
+      //   checkVideoDetails(forms.player.currentTime())
+      //   // 判断视频计时器是否暂停,如果暂停则恢复
+      //   // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
+      //   if (
+      //     !videoIntervalRef.isActive.value &&
+      //     forms.player.currentTime() > 0 &&
+      //     forms.player.playing
+      //   ) {
+      //     // console.log('timeupdate play')
+      //     videoIntervalRef.resume()
+      //   }
+      // })
+
+      // // 视屏播放时暂停
+      // forms.player.on('ended', () => {
+      //   forms.player.pause()
+      // })
+
+      // // 开始播放
+      // forms.player.on('play', () => {
+      //   console.log('play')
+      //   // 判断视频计时器是否暂停,如果暂停则恢复
+      //   videoIntervalRef.resume()
+      // })
+
+      // // 暂停播放
+      // forms.player.on('pause', () => {
+      //   console.log('pause', videoIntervalRef.isActive.value)
+
+      //   videoIntervalRef.pause()
+      // })
+
+      // forms.player.on('enterfullscreen', () => {
+      //   console.log('fullscreen')
+      //   const i = document.createElement('i')
+      //   i.id = 'fullscreen-back'
+      //   i.className = 'van-icon van-icon-arrow-left video-back'
+      //   i.addEventListener('click', () => {
+      //     forms.player.fullscreen.exit()
+      //   })
+      //   console.log(document.getElementsByClassName('plyr'))
+      //   document.getElementsByClassName('plyr')[0].appendChild(i)
+      // })
+
+      // forms.player.on('exitfullscreen', () => {
+      //   console.log('exitfullscreen')
+      //   const i = document.getElementById('fullscreen-back')
+      //   i && i.remove()
+      // })
+      const Button = TCPlayer.getComponent('Button')
+      const BigPlayButton = TCPlayer.getComponent('BigPlayButton')
+      BigPlayButton.prototype.createEl = function () {
+        const el = Button.prototype.createEl.call(this)
+        const _html =
+          '<button><svg width="41px"height="41px"viewBox="0 0 41 41"version="1.1"xmlns="http://www.w3.org/2000/svg"xmlns:xlink="http://www.w3.org/1999/xlink"><g stroke="none"stroke-width="1"fill="none"fill-rule="evenodd"><g transform="translate(-167.000000, -155.000000)"><g transform="translate(0.000000, 85.000000)"><g transform="translate(158.000000, 70.000000)"><g transform="translate(9.000000, 0.000000)"><circle id="椭圆形"stroke="#FFFFFF"fill-opacity="0.1"fill="#D8D8D8"cx="20.5"cy="20.5"r="20"></circle><path d="M14.5483871,27.6859997 L14.5483871,13.4342349 C14.5480523,12.8729571 14.8729597,12.356555 15.3949624,12.0887034 C15.9169651,11.8208518 16.5522696,11.8445472 17.0503046,12.1504437 L28.6530473,19.2778563 C29.1119763,19.5602271 29.3887725,20.0426422 29.3887725,20.5601173 C29.3887725,21.0775924 29.1119763,21.5600075 28.6530473,21.8423783 L17.0503046,28.9697909 C16.5522696,29.2756874 15.9169651,29.2993828 15.3949624,29.0315312 C14.8729597,28.7636796 14.5480523,28.2472775 14.5483871,27.6859997 Z"id="路径"fill="#FFFFFF"fill-rule="nonzero"></path></g></g></g></g></g></svg></button>'
+
+        el.appendChild(
+          TCPlayer.dom.createEl('div', {
+            className: 'vjs-button-icon',
+            innerHTML: _html
+          })
+        )
+        return el
       }
-
-      if (browser().iPhone) {
-        params.fullscreen = {
-          enabled: true,
-          fallback: 'force',
-          iosNative: true
+      forms.player = TCPlayer('register-video', {
+        appID: '',
+        controls: true,
+        plugins: {
+          // ProgressMarker: {
+          //   markers: [
+          //     {
+          //       content: '1111',
+          //       timeOffset: 1000
+          //     }
+          //   ]
+          // }
         }
-      }
-      const times: any = []
-      forms.videoDetails.forEach((item: any) => {
-        times.push({
-          time: item.startNode,
-          label: item.desc
-        })
-      })
-      params.markers = { enabled: true, points: times }
-
-      forms.player = new Plyr('#register-video', params)
-
-      forms.player.on('ready', (item: any) => {
-        // console.log('ready', item)
-        // forms.player.pause()
-      })
-      forms.player.on('loadedmetadata', () => {
-        console.log('loadedmetadata')
-        forms.loading = false
-        forms.player.currentTime = forms.videoBrowsePoint
-        checkVideoDetails(forms.player.currentTime)
-      })
+      }) // player-container-id 为播放器容器 ID,必须与 html 中一致
+      if (forms.player) {
+        forms.player.src(forms.introductionVideo) // url 播放地址
+        forms.player.poster(forms.coverImg || '')
 
-      // 速度变化时
-      forms.player.on('ratechange', () => {
-        forms.playerSpeed =
-          forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
-      })
+        // forms.player.on('loadstart', () => {})
+        forms.player.on('ready', (item: any) => {
+          console.log('ready', item)
 
-      forms.player.on('seeking', () => {
-        console.log('seeking')
-        videoIntervalRef.isActive.value && videoIntervalRef.pause()
-      })
+          // forms.player.pause()
+        })
+        forms.player.on('loadedmetadata', () => {
+          console.log('loadedmetadata')
+          forms.loading = false
+          forms.player.currentTime(forms.videoBrowsePoint)
+          checkVideoDetails(forms.player.currentTime())
+        })
 
-      // // 拖动结束时
-      forms.player.on('seeked', () => {
-        console.log('seeked')
-        videoIntervalRef.isActive.value && videoIntervalRef.pause()
-      })
+        // 速度变化时
+        forms.player.on('ratechange', () => {
+          forms.playerSpeed =
+            forms.playerSpeed < forms.player.speed ? forms.player.speed : forms.playerSpeed
+        })
 
-      // 正在搜索中
-      forms.player.on('waiting', () => {
-        // console.log('waiting pause')
-        videoIntervalRef.isActive.value && videoIntervalRef.pause()
-      })
+        forms.player.on('seeking', () => {
+          console.log('seeking')
+          videoIntervalRef.isActive.value && videoIntervalRef.pause()
+        })
 
-      // 如何视频在缓存不会触发
-      forms.player.on('timeupdate', () => {
-        // console.log('timeupdate', forms.player.currentTime)
-        // console.log(videoIntervalRef.isActive.value, 'timeupdate')
-        // 时间变化时更新每一段的状态
-        checkVideoDetails(forms.player.currentTime)
-        // 判断视频计时器是否暂停,如果暂停则恢复
-        // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
-        if (
-          !videoIntervalRef.isActive.value &&
-          forms.player.currentTime > 0 &&
-          forms.player.playing
-        ) {
-          // console.log('timeupdate play')
-          videoIntervalRef.resume()
-        }
-      })
+        // // 拖动结束时
+        forms.player.on('seeked', () => {
+          console.log('seeked')
+          videoIntervalRef.isActive.value && videoIntervalRef.pause()
+        })
 
-      // 视屏播放时暂停
-      forms.player.on('ended', () => {
-        forms.player.pause()
+        // 正在搜索中
+        forms.player.on('waiting', () => {
+          // console.log('waiting pause')
+          videoIntervalRef.isActive.value && videoIntervalRef.pause()
+        })
 
-        console.log(videoIntervalRef.isActive.value, 'ended')
-      })
+        // 如何视频在缓存不会触发
+        forms.player.on('timeupdate', () => {
+          console.log('timeupdate', forms.player.currentTime())
+          // 时间变化时更新每一段的状态
+          checkVideoDetails(forms.player.currentTime())
+          // 判断视频计时器是否暂停,如果暂停则恢复
+          // 添加 「forms.player.playing」 是由会跳转到上次播放时间,会触发些方法
+          if (
+            !videoIntervalRef.isActive.value &&
+            forms.player.currentTime() > 0 &&
+            forms.player.playing
+          ) {
+            // console.log('timeupdate play')
+            videoIntervalRef.resume()
+          }
+        })
 
-      // 开始播放
-      forms.player.on('play', () => {
-        console.log('play')
-        // 判断视频计时器是否暂停,如果暂停则恢复
-        videoIntervalRef.resume()
-      })
+        // 视屏播放时暂停
+        forms.player.on('ended', () => {
+          forms.player.pause()
+        })
 
-      // 暂停播放
-      forms.player.on('pause', () => {
-        console.log('pause', videoIntervalRef.isActive.value)
+        // 开始播放
+        forms.player.on('play', () => {
+          console.log('play')
+          // 判断视频计时器是否暂停,如果暂停则恢复
+          videoIntervalRef.resume()
+        })
 
-        videoIntervalRef.pause()
-      })
+        // 暂停播放
+        forms.player.on('pause', () => {
+          console.log('pause', videoIntervalRef.isActive.value)
 
-      forms.player.on('enterfullscreen', () => {
-        console.log('fullscreen')
-        const i = document.createElement('i')
-        i.id = 'fullscreen-back'
-        i.className = 'van-icon van-icon-arrow-left video-back'
-        i.addEventListener('click', () => {
-          forms.player.fullscreen.exit()
+          videoIntervalRef.pause()
         })
-        // console.log(document.getElementsByClassName('plyr'))
-        document.getElementsByClassName('plyr')[0].appendChild(i)
-      })
 
-      forms.player.on('exitfullscreen', () => {
-        console.log('exitfullscreen')
-        const i = document.getElementById('fullscreen-back')
-        i && i.remove()
-      })
+        forms.player.on('fullscreenchange', () => {
+          if (forms.player.isFullscreen()) {
+            console.log('fullscreen')
+            const i = document.createElement('i')
+            i.id = 'fullscreen-back'
+            i.className = 'van-icon van-icon-arrow-left video-back'
+            i.addEventListener('click', () => {
+              forms.player.exitFullscreen()
+            })
+            document.getElementsByClassName('video-js')[0].appendChild(i)
+          } else {
+            console.log('exitfullscreen')
+            const i = document.getElementById('fullscreen-back')
+            i && i.remove()
+          }
+        })
+      }
       checkVideoDetails(0)
     }
 
@@ -286,6 +409,7 @@ export default defineComponent({
     watch(
       () => videoIntervalRef.isActive.value,
       (newVal: boolean) => {
+        console.log(videoIntervalRef.isActive.value, 'videoIntervalRef')
         initVideoCount(newVal)
       }
     )
@@ -358,7 +482,7 @@ export default defineComponent({
             videoBrowseDataTime: time || 0, // 有效的视频观看时长
             videoBrowsePercentage: rate || 0, // 有效的视频观看时长百分比
             videoBrowseTime: videoIntervalRef?.counter.value, // 视频观看时长
-            videoBrowsePoint: Math.floor(forms.player.currentTime || 0) // 视频最后观看点 - 向下取整
+            videoBrowsePoint: Math.floor(forms.player.currentTime() || 0) // 视频最后观看点 - 向下取整
           }
         })
       } catch {
@@ -419,7 +543,7 @@ export default defineComponent({
         })
         forms.videoBrowsePoint = data.videoBrowsePoint || 0
         if (forms.player) {
-          forms.player.currentTime = data.videoBrowsePoint || 0
+          forms.player.currentTime(data.videoBrowsePoint || 0)
         }
         forms.introductionVideo = data.introductionVideo
         forms.introductionVideoTime = data.introductionVideoTime
@@ -543,10 +667,10 @@ export default defineComponent({
               <span
                 class={[item.id === forms.videoSelectId ? styles.active : '']}
                 onClick={() => {
-                  forms.player.currentTime = item.startNode
+                  forms.player.currentTime(item.startNode)
                   forms.player.play()
                   forms.videoBrowsePoint = item.startNode
-                  checkVideoDetails(forms.player.currentTime)
+                  checkVideoDetails(forms.player.currentTime())
                 }}
               >
                 {item.desc}

+ 0 - 1
src/teacher/main.ts

@@ -32,7 +32,6 @@ promisefiyPostMessage({ api: 'getToken' }).then((res: any) => {
   }
 })
 
-
 // 设置是否显示导航栏 0 不显示 1 显示
 postMessage({ api: 'setBarStatus', content: { status: 0 } })
 // 导航栏高度

+ 252 - 0
src/views/coursewarePlay/component/video-item/index copy.tsx

@@ -0,0 +1,252 @@
+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'
+
+export default defineComponent({
+  name: 'video-play',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    activeModel: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['play', 'pause', 'ended', 'close', 'seeked', 'seeking', 'waiting', 'timeupdate'],
+  setup(props, { emit, expose }) {
+    const { item } = toRefs(props)
+    const data = reactive({
+      videoContianerRef: null as unknown as HTMLAudioElement,
+      videoState: 'pause' as 'init' | 'play' | 'pause',
+      animationState: 'start' as 'start' | 'end',
+      videoItem: null as unknown as Plyr
+    })
+    const controlID = 'v' + Date.now() + Math.floor(Math.random() * 100)
+    const playBtnId = 'play' + Date.now() + Math.floor(Math.random() * 100)
+    const loopBtnId = 'loop' + Date.now() + Math.floor(Math.random() * 100)
+
+    const togglePlay = (e: Event) => {
+      e.stopPropagation()
+      if (!data.videoContianerRef.paused) {
+        data.videoItem?.pause()
+      } else {
+        data.videoContianerRef?.play()
+      }
+    }
+    const toggleLoop = () => {
+      const loopBtn = document.getElementById(loopBtnId)
+      if (!loopBtn || !data.videoItem) return
+      const isLoop = data.videoItem.loop
+      if (isLoop) {
+        loopBtn.classList.remove(styles.active)
+      } else {
+        loopBtn.classList.add(styles.active)
+      }
+      data.videoItem.loop = !data.videoItem.loop
+    }
+    const onDefault = () => {
+      document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
+        e.stopPropagation()
+        if (data.videoContianerRef.paused) return
+        emit('close')
+      })
+      document.getElementById(controlID)?.addEventListener('touchmove', () => {
+        if (data.videoContianerRef.paused) return
+        emit('close')
+      })
+      document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
+      document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
+      setName()
+    }
+    const setName = () => {
+      const nameEl = document.getElementById('videoItemName')
+      if (nameEl) {
+        nameEl.innerHTML = item.value.name || ''
+      }
+    }
+
+    const changePlayBtn = (code: string) => {
+      const playBtn = document.getElementById(playBtnId)
+      if (!playBtn) return
+      if (code == 'play') {
+        playBtn.classList.remove(styles.btnPause)
+        playBtn.classList.add(styles.btnPlay)
+      } else {
+        playBtn.classList.remove(styles.btnPlay)
+        playBtn.classList.add(styles.btnPause)
+      }
+    }
+    const controls = `
+            <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
+                <div class="${styles.time}">
+                    <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
+                    <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
+                </div>
+                <div class="${styles.slider}">
+                    <div class="plyr__progress">
+                        <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
+                        <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
+                        <span role="tooltip" class="plyr__tooltip">00:00</span>
+                    </div>
+                </div>
+                <div class="${styles.actions}">
+                    <div class="${styles.actionWrap}">
+                        <button id="${playBtnId}" class="${styles.actionBtn}">
+                            <div class="van-loading van-loading--circular" aria-live="polite" aria-busy="true"><span class="van-loading__spinner van-loading__spinner--circular" style="color: rgb(255, 255, 255);"><svg class="van-loading__circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none"></circle></svg></span></div>
+                            <img class="${styles.playIcon}" src="${iconPlay}" />
+                            <img class="${styles.playIcon}" src="${iconPause}" />
+                        </button>
+                        <button id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
+                            <img class="loop" src="${iconLoop}" />
+                            <img class="loopActive" src="${iconLoopActive}" />
+                        </button>
+                    </div>
+                    <div id="videoItemName"></div>
+                </div>
+            </div>`
+
+    onMounted(() => {
+      data.videoItem = new Plyr(data.videoContianerRef, {
+        autoplay: true,
+        controls: controls,
+        ratio: '16:9', // 强制所有视频的纵横比
+        hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
+        clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
+        fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
+      })
+
+      nextTick(() => {
+        onDefault()
+      })
+    })
+
+    const toggleHideControl = (isShow: boolean) => {
+      data.videoItem?.toggleControls(isShow)
+    }
+    watch(
+      () => props.activeModel,
+      () => {
+        toggleHideControl(props.activeModel)
+      }
+    )
+
+    watch(
+      () => props.item,
+      () => {
+        setName()
+      }
+    )
+    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)
+          videoTimer = setTimeout(() => {
+            if (err?.message?.includes('play()')) {
+              emit('play')
+            }
+            handlePlayVideo()
+          }, 1000)
+        })
+      })
+      videoTimerErrorCount++
+    }
+
+    let videoErrorTimer = null as any
+    let videoErrorCount = 0
+    const handleErrorVideo = () => {
+      if (videoErrorCount > 5) {
+        return
+      }
+      clearTimeout(videoErrorTimer)
+      nextTick(() => {
+        videoErrorTimer = setTimeout(() => {
+          data.videoContianerRef.src = props.item?.content
+          emit('play')
+          data.videoContianerRef.load()
+          // eslint-disable-next-line @typescript-eslint/no-unused-vars
+          handleErrorVideo()
+        }, 1000)
+      })
+      videoErrorCount++
+    }
+    const getVideoRef = () => {
+      return data.videoContianerRef
+    }
+
+    const getPlyrRef = () => {
+      return data.videoItem
+    }
+    expose({
+      getVideoRef,
+      getPlyrRef
+    })
+
+    return () => (
+      <div class={styles.videoWrap}>
+        <video
+          ref={(el) => (data.videoContianerRef = el as unknown as HTMLAudioElement)}
+          class={styles.itemDiv}
+          src={props.item?.content}
+          poster={iconVideoBg}
+          webkit-playsinline
+          playsinline
+          x5-video-player-type="h5"
+          onLoadedmetadata={() => {
+            data.videoState = 'pause'
+            changePlayBtn('play')
+            nextTick(() => {
+              data.videoContianerRef.currentTime = 0
+              nextTick(handlePlayVideo)
+            })
+          }}
+          onPlay={() => {
+            videoErrorCount = 0
+            console.log('开始播放')
+            data.videoState = 'play'
+            changePlayBtn('pause')
+            emit('close')
+            emit('play')
+            clearTimeout(videoErrorTimer)
+          }}
+          onPause={() => {
+            console.log('暂停播放')
+            data.videoState = 'pause'
+            changePlayBtn('play')
+            emit('pause')
+          }}
+          onEnded={() => {
+            console.log('播放结束')
+            data.videoState = 'pause'
+            changePlayBtn('play')
+            emit('ended')
+          }}
+          onSeeked={() => {
+            emit('seeked')
+          }}
+          onSeeking={() => {
+            emit('seeking')
+          }}
+          onTimeupdate={() => {
+            emit('timeupdate')
+          }}
+          onWaiting={() => {
+            emit('waiting')
+          }}
+          onError={handleErrorVideo}
+        ></video>
+      </div>
+    )
+  }
+})

+ 176 - 0
src/views/coursewarePlay/component/video-play-copy.tsx

@@ -0,0 +1,176 @@
+import { defineComponent, nextTick, onMounted, toRefs, watch } from 'vue'
+import 'plyr/dist/plyr.css'
+import Plyr from 'plyr'
+import { ref } from 'vue'
+import styles from './video.module.less'
+
+import iconLoop from '../image/icon-loop.svg'
+import iconLoopActive from '../image/icon-loop-active.svg'
+import iconplay from '../image/icon-play.svg'
+import iconpause from '../image/icon-pause.svg'
+
+export default defineComponent({
+  name: 'video-play',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    isEmtry: {
+      type: Boolean,
+      default: false
+    },
+    isActive: {
+      type: Boolean,
+      default: false
+    }
+  },
+  emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset'],
+  setup(props, { emit, expose }) {
+    const { item, isEmtry } = toRefs(props)
+    const videoRef = ref()
+    const videoItem = ref<Plyr>()
+    const controlID = 'v' + Date.now() + Math.floor(Math.random() * 100)
+    const playBtnId = 'play' + Date.now() + Math.floor(Math.random() * 100)
+    const loopBtnId = 'loop' + Date.now() + Math.floor(Math.random() * 100)
+    const toggleHideControl = (isShow: false) => {
+      videoItem.value?.toggleControls(isShow)
+    }
+    const togglePlay = (e: Event) => {
+      e.stopPropagation()
+      videoItem.value?.togglePlay()
+    }
+    const toggleLoop = (e: Event) => {
+      const loopBtn = document.getElementById(loopBtnId)
+      if (!loopBtn || !videoItem.value) return
+      const isLoop = videoItem.value.loop
+      if (isLoop) {
+        loopBtn.classList.remove(styles.active)
+      } else {
+        loopBtn.classList.add(styles.active)
+      }
+      videoItem.value.loop = !videoItem.value.loop
+    }
+    const onDefault = () => {
+      document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
+        e.stopPropagation()
+        emit('reset')
+      })
+      document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
+      document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
+    }
+
+    const changePlayBtn = (code: string) => {
+      const playBtn = document.getElementById(playBtnId)
+      if (!playBtn) return
+      if (code == 'play') {
+        playBtn.classList.remove(styles.btnPause)
+        playBtn.classList.add(styles.btnPlay)
+      } else {
+        playBtn.classList.remove(styles.btnPlay)
+        playBtn.classList.add(styles.btnPause)
+      }
+    }
+    const controls = `
+            <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
+                <div class="${styles.time}">
+                    <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
+                    <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
+                </div>
+                <div class="${styles.slider}">
+                    <div class="plyr__progress">
+                        <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
+                        <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
+                        <span role="tooltip" class="plyr__tooltip">00:00</span>
+                    </div>
+                </div>
+                <div class="${styles.actions}">
+                    <div class="${styles.actionWrap}">
+                        <button id="${playBtnId}" class="${styles.actionBtn}">
+                            <div class="van-loading van-loading--circular" aria-live="polite" aria-busy="true"><span class="van-loading__spinner van-loading__spinner--circular" style="color: rgb(255, 255, 255);"><svg class="van-loading__circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none"></circle></svg></span></div>
+                            <img class="${styles.playIcon}" src="${iconplay}" />
+                            <img class="${styles.playIcon}" src="${iconpause}" />
+                        </button>
+                        <button id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
+                            <img class="loop" src="${iconLoop}" />
+                            <img class="loopActive" src="${iconLoopActive}" />
+                        </button>
+                    </div>
+                    <div>${item.value.name}</div>
+                </div>
+            </div>`
+
+    onMounted(() => {
+      videoItem.value = new Plyr(videoRef.value, {
+        autoplay: true,
+        controls: controls,
+        autopause: true, // 一次只允许
+        ratio: '16:9', // 强制所有视频的纵横比
+        hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
+        clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
+        fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
+      })
+      if (videoItem.value) {
+        videoItem.value.on('play', () => {
+          if (videoItem.value) {
+            videoItem.value.muted = false
+            videoItem.value.volume = 1
+          }
+
+          // console.log('开始播放', item.value)
+          if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
+            // 加载完成后,取消静音播放
+
+            console.log(videoItem.value)
+            videoItem.value.pause()
+          }
+          changePlayBtn('')
+          emit('togglePlay', videoItem.value?.paused)
+        })
+        videoItem.value.on('pause', () => {
+          changePlayBtn('play')
+          emit('togglePlay', videoItem.value?.paused)
+        })
+        videoItem.value.on('ended', (e: Event) => {
+          emit('ended')
+          changePlayBtn('play')
+        })
+        videoItem.value.once('loadedmetadata', (e: Event) => {
+          changePlayBtn('play')
+          if (item.value.autoPlay && videoItem.value) {
+            videoItem.value.play()
+          }
+          emit('loadedmetadata', videoItem.value)
+        })
+
+        nextTick(() => {
+          onDefault()
+        })
+      }
+    })
+    expose({
+      changePlayBtn,
+      toggleHideControl
+    })
+    watch(
+      () => props.isActive,
+      (val) => {
+        if (!val) {
+          videoItem.value?.pause()
+        }
+      }
+    )
+    return () => (
+      <div class={styles.videoWrap}>
+        <video
+          style={{ width: '100%', height: '100%' }}
+          src={isEmtry.value ? '' : item.value.content}
+          ref={videoRef}
+          playsinline="false"
+        ></video>
+      </div>
+    )
+  }
+})

+ 285 - 106
src/views/coursewarePlay/component/video-play.tsx

@@ -1,6 +1,6 @@
-import { defineComponent, nextTick, onMounted, toRefs } from 'vue'
-import 'plyr/dist/plyr.css'
-import Plyr from 'plyr'
+import { defineComponent, nextTick, onMounted, reactive, toRefs, watch } from 'vue'
+// import 'plyr/dist/plyr.css'
+// import Plyr from 'plyr'
 import { ref } from 'vue'
 import styles from './video.module.less'
 
@@ -9,6 +9,26 @@ import iconLoopActive from '../image/icon-loop-active.svg'
 import iconplay from '../image/icon-play.svg'
 import iconpause from '../image/icon-pause.svg'
 
+import TCPlayer from 'tcplayer.js'
+import 'tcplayer.js/dist/tcplayer.min.css'
+import { Slider } from 'vant'
+
+// 秒转分
+export const getSecondRPM = (second: number, type?: string) => {
+  if (isNaN(second)) return '00:00'
+  const mm = Math.floor(second / 60)
+    .toString()
+    .padStart(2, '0')
+  const dd = Math.floor(second % 60)
+    .toString()
+    .padStart(2, '0')
+  if (type === 'cn') {
+    return mm + '分' + dd + '秒'
+  } else {
+    return mm + ':' + dd
+  }
+}
+
 export default defineComponent({
   name: 'video-play',
   props: {
@@ -25,152 +45,311 @@ export default defineComponent({
     isActive: {
       type: Boolean,
       default: false
+    },
+    activeModel: {
+      type: Boolean,
+      default: true
     }
   },
-  emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset', 'prepare'],
+  emits: [
+    'loadedmetadata',
+    'togglePlay',
+    'ended',
+    'reset',
+    'error',
+    'close',
+    'play',
+    'pause',
+    'seeked',
+    'seeking',
+    'waiting',
+    'timeupdate'
+  ],
   setup(props, { emit, expose }) {
     const { item, isEmtry } = toRefs(props)
+    const data = reactive({
+      timer: null as any,
+      currentTime: 0,
+      duration: 0.1,
+      loop: false,
+      playState: 'pause' as 'play' | 'pause',
+      vudio: null as any,
+      showBar: true
+    })
     const videoRef = ref()
-    const videoItem: any = ref()
-    const controlID = 'v' + Date.now() + Math.floor(Math.random() * 100)
-    const playBtnId = 'play' + Date.now() + Math.floor(Math.random() * 100)
-    const loopBtnId = 'loop' + Date.now() + Math.floor(Math.random() * 100)
+    const videoItem = ref()
+    const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100)
     const toggleHideControl = (isShow: false) => {
-      videoItem.value?.toggleControls(isShow)
+      data.showBar = isShow
     }
-    const togglePlay = (e: Event) => {
-      e.stopPropagation()
-      videoItem.value?.togglePlay()
+    // const togglePlay = (e: Event) => {
+    //   e.stopPropagation()
+
+    // }
+    let playTimer = null as any
+    // 切换音频播放
+    const onToggleAudio = (state: 'play' | 'pause') => {
+      // console.log(state, 'state')
+      clearTimeout(playTimer)
+      if (state === 'play') {
+        playTimer = setTimeout(() => {
+          videoItem.value?.play()
+          data.playState = 'play'
+        }, 100)
+      } else {
+        videoItem.value?.pause()
+        data.playState = 'pause'
+      }
+
+      emit('togglePlay', data.playState)
     }
     const toggleLoop = () => {
-      const loopBtn = document.getElementById(loopBtnId)
-      if (!loopBtn || !videoItem.value) return
-      const isLoop = videoItem.value.loop
-      if (isLoop) {
-        loopBtn.classList.remove(styles.active)
+      if (!videoItem.value) return
+      if (data.loop) {
+        videoItem.value.loop(false)
       } else {
-        loopBtn.classList.add(styles.active)
+        videoItem.value.loop(true)
       }
-      videoItem.value.loop = !videoItem.value.loop
+      data.loop = !data.loop
     }
-    const onDefault = () => {
-      document.getElementById(controlID)?.addEventListener('click', (e: Event) => {
-        e.stopPropagation()
-        emit('reset')
-      })
-      document.getElementById(playBtnId)?.addEventListener('click', togglePlay)
-      document.getElementById(loopBtnId)?.addEventListener('click', toggleLoop)
-    }
-
     const changePlayBtn = (code: string) => {
-      const playBtn = document.getElementById(playBtnId)
-      if (!playBtn) return
       if (code == 'play') {
-        playBtn.classList.remove(styles.btnPause)
-        playBtn.classList.add(styles.btnPlay)
+        data.playState = 'play'
       } else {
-        playBtn.classList.remove(styles.btnPlay)
-        playBtn.classList.add(styles.btnPause)
+        data.playState = 'pause'
       }
     }
-    const controls = `
-            <div id="${controlID}" class="plyr__controls bottomFixed ${styles.controls}">
-                <div class="${styles.time}">
-                    <div class="plyr__time plyr__time--current" aria-label="Current time">00:00</div>
-                    <div class="plyr__time plyr__time--duration" aria-label="Duration">00:00</div>
-                </div>
-                <div class="${styles.slider}">
-                    <div class="plyr__progress">
-                        <input data-plyr="seek" type="range" min="0" max="100" step="0.01" value="0" aria-label="Seek">
-                        <progress class="plyr__progress__buffer" min="0" max="100" value="0">% buffered</progress>
-                        <span role="tooltip" class="plyr__tooltip">00:00</span>
-                    </div>
-                </div>
-                <div class="${styles.actions}">
-                    <div class="${styles.actionWrap}">
-                        <button id="${playBtnId}" class="${styles.actionBtn}">
-                            <div class="van-loading van-loading--circular" aria-live="polite" aria-busy="true"><span class="van-loading__spinner van-loading__spinner--circular" style="color: rgb(255, 255, 255);"><svg class="van-loading__circular" viewBox="25 25 50 50"><circle cx="50" cy="50" r="20" fill="none"></circle></svg></span></div>
-                            <img class="${styles.playIcon}" src="${iconplay}" />
-                            <img class="${styles.playIcon}" src="${iconpause}" />
-                        </button>
-                        <button id="${loopBtnId}" class="${styles.actionBtn} ${styles.loopBtn}">
-                            <img class="loop" src="${iconLoop}" />
-                            <img class="loopActive" src="${iconLoopActive}" />
-                        </button>
-                    </div>
-                    <div>${item.value.name}</div>
-                </div>
-            </div>`
 
-    onMounted(() => {
-      emit('prepare', false)
-      videoItem.value = new Plyr(videoRef.value, {
-        autoplay: false,
-        controls: controls,
-        autopause: false, // 一次只允许
-        ratio: '16:9', // 强制所有视频的纵横比
-        hideControls: false, // 在 2 秒没有鼠标或焦点移动、控制元素模糊(制表符退出)、播放开始或进入全屏时自动隐藏视频控件。只要移动鼠标、聚焦控制元素或暂停播放,控件就会立即重新出现。
-        clickToPlay: false, // 单击(或点击)视频容器将切换播放/暂停
-        fullscreen: { enabled: false, fallback: false, iosNative: false } // 不适用全屏
-      })
-      if (videoItem.value) {
-        videoItem.value.on('play', () => {
-          if (videoItem.value && videoItem.value.muted) {
-            videoItem.value.muted = false
-            videoItem.value.volume = 1
-          }
+    /** 改变播放时间 */
+    const handleChangeTime = (val: number) => {
+      data.currentTime = val
+      clearTimeout(data.timer)
+      data.timer = setTimeout(() => {
+        videoItem.value.currentTime(val)
+        data.timer = null
+      }, 300)
+    }
 
-          // console.log('开始播放', item.value)
-          if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
-            // 加载完成后,取消静音播放
-            videoItem.value.pause()
-            console.log(videoItem.value?.paused, 'video status')
+    const __initVideo = () => {
+      if (videoItem.value && props.item.id) {
+        videoItem.value.poster(props.item.coverImg) // 封面
+        videoItem.value.src(item.value.content) // url 播放地址
+
+        // 初步加载时
+        videoItem.value.on('loadedmetadata', (e: any) => {
+          console.log(' Loading metadata')
+
+          // 获取时长
+          data.duration = videoItem.value.duration()
+          // 必须在当前元素
+
+          if (item.value.autoPlay && videoItem.value && props.isActive) {
+            // videoItem.value?.play()
+            nextTick(() => {
+              videoItem.value.currentTime(0)
+              nextTick(handlePlayVideo)
+            })
           }
-          changePlayBtn('')
-          emit('togglePlay', videoItem.value?.paused)
+          emit('loadedmetadata', videoItem.value)
         })
-        videoItem.value.on('pause', () => {
-          changePlayBtn('play')
-          emit('togglePlay', videoItem.value?.paused)
+        // videoItem.value.on('timeupdate', () => {
+        //   if (!props.isActive) {
+        //     console.log('不是激活的视频,如果在播放,就暂停')
+        //     videoRef.value.pause()
+        //   }
+        // })
+
+        // 视频播放时加载
+        videoItem.value.on('timeupdate', () => {
+          if (data.timer) return
+          data.currentTime = videoItem.value.currentTime()
+          emit('timeupdate')
         })
+
+        // 视频播放结束
         videoItem.value.on('ended', () => {
-          emit('ended')
           changePlayBtn('play')
+          emit('ended')
+        })
+
+        //
+        videoItem.value.on('pause', () => {
+          data.playState = 'pause'
+          changePlayBtn('pause')
+          emit('togglePlay', true)
+          emit('pause')
         })
-        videoItem.value.once('loadedmetadata', () => {
-          console.log('loadedmetadata')
+
+        videoItem.value.on('seeked', () => {
+          emit('seeked')
+        })
+
+        videoItem.value.on('seeking', () => {
+          emit('seeking')
+        })
+        videoItem.value.on('waiting', () => {
+          emit('waiting')
+        })
+
+        videoItem.value.on('play', () => {
+          // console.log(play, 'playing')
           changePlayBtn('play')
-          videoItem.value.currentTime = 0
-          if (item.value.autoPlay && videoItem.value && props.isActive) {
-            videoItem.value.play()
+          if (videoItem.value) {
+            videoItem.value.muted = false
+            videoItem.value.volume = 1
           }
-          emit('loadedmetadata', videoItem.value)
-        })
-        videoItem.value.on('timeupdate', () => {
-          if (!props.isActive) {
-            console.log('不是激活的视频,如果在播放,就暂停')
-            videoRef.value.pause()
+          if (!item.value.autoPlay && !item.value.isprepare && videoItem.value) {
+            // 加载完成后,取消静音播放
+            // console.log(videoItem.value)
+            videoItem.value.pause()
           }
+          emit('togglePlay', videoItem.value?.paused)
+          emit('play')
         })
 
-        nextTick(() => {
-          onDefault()
+        // 视频播放异常
+        videoItem.value.on('error', (e: any) => {
+          handleErrorVideo()
+          emit('error')
+          console.log(e, 'error')
+        })
+      }
+    }
+
+    let videoTimer = null as any
+    let videoTimerErrorCount = 0
+    const handlePlayVideo = () => {
+      if (videoTimerErrorCount > 5) {
+        return
+      }
+      clearTimeout(videoTimer)
+      nextTick(() => {
+        videoItem.value?.play().catch((err) => {
+          // console.log('🚀 ~ err:', err)
+          videoTimer = setTimeout(() => {
+            if (err?.message?.includes('play()')) {
+              emit('play')
+            }
+            handlePlayVideo()
+          }, 1000)
         })
+      })
+      videoTimerErrorCount++
+    }
+
+    let videoErrorTimer = null as any
+    let videoErrorCount = 0
+    const handleErrorVideo = () => {
+      if (videoErrorCount > 5) {
+        return
       }
+      clearTimeout(videoErrorTimer)
+      nextTick(() => {
+        videoErrorTimer = setTimeout(() => {
+          videoItem.value.src = props.item?.content
+          emit('play')
+          videoItem.value.load()
+          // eslint-disable-next-line @typescript-eslint/no-unused-vars
+          handleErrorVideo()
+        }, 1000)
+      })
+      videoErrorCount++
+    }
+
+    onMounted(() => {
+      videoItem.value = TCPlayer(videoID, {
+        appID: '',
+        controls: false
+        // autoplay: true
+      }) // player-container-id 为播放器容器 ID,必须与 html 中一致
+      __initVideo()
     })
+
+    watch(
+      () => props.item,
+      () => {
+        __initVideo()
+      }
+    )
+
+    const getVideoRef = () => {
+      return videoRef.value
+    }
+
+    const getPlyrRef = () => {
+      return videoItem.value
+    }
     expose({
       changePlayBtn,
-      toggleHideControl
+      toggleHideControl,
+      getVideoRef,
+      getPlyrRef
     })
 
+    watch(
+      () => props.isActive,
+      (val) => {
+        if (!val) {
+          videoItem.value?.pause()
+        }
+      }
+    )
+
     return () => (
       <div class={styles.videoWrap}>
         <video
           style={{ width: '100%', height: '100%' }}
-          src={isEmtry.value ? '' : item.value.content}
+          src={item.value.content}
           ref={videoRef}
-          playsinline="false"
+          id={videoID}
+          preload="auto"
+          playsinline
+          webkit-playsinline
         ></video>
+        <div class={styles.videoSection}></div>
+
+        <div
+          class={[styles.controls, data.showBar ? '' : styles.hide]}
+          onClick={(e: Event) => {
+            e.stopPropagation()
+          }}
+          // onTouchmove={(e: TouchEvent) => {
+          //   emit('close')
+          // }}
+        >
+          <div class={styles.time}>
+            <div>{getSecondRPM(data.currentTime)}</div>
+            <div>{getSecondRPM(data.duration)}</div>
+          </div>
+          <div class={styles.slider}>
+            <Slider
+              step={0.01}
+              class={styles.timeProgress}
+              v-model={data.currentTime}
+              max={data.duration}
+              onUpdate:modelValue={(val) => {
+                handleChangeTime(val)
+              }}
+            />
+          </div>
+          <div class={styles.actionSection}>
+            <div class={styles.actions} onClick={() => emit('close')}>
+              <div
+                class={styles.actionBtn}
+                onClick={(e: any) => {
+                  e.stopPropagation()
+                  onToggleAudio(data.playState === 'pause' ? 'play' : 'pause')
+                }}
+              >
+                <img src={data.playState === 'pause' ? iconplay : iconpause} />
+              </div>
+              <div class={styles.actionBtn} onClick={toggleLoop}>
+                <img src={data.loop ? iconLoopActive : iconLoop} />
+              </div>
+            </div>
+            <div class={styles.name}>{item.value.name}</div>
+          </div>
+        </div>
       </div>
     )
   }

+ 284 - 103
src/views/coursewarePlay/component/video.module.less

@@ -1,133 +1,314 @@
+// .videoWrap {
+//     width: 100%;
+//     height: 100%;
+
+//     :global {
+//         .plyr--video {
+//             width: 100%;
+//             height: 100%;
+//         }
+
+//         .plyr__time {
+//             display: block !important;
+//         }
+//         .plyr__video-wrapper{
+//             pointer-events: none;
+//         }
+//     }
+// }
+
+// :global(.bottomFixed).controls {
+//     width: 100%;
+//     background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
+//     padding: 0 !important;
+//     flex-direction: column;
+//     transition: all 0.5s;
+
+//     .time {
+//         display: flex;
+//         justify-content: space-between;
+//         width: 100%;
+//         color: #fff;
+//         font-size: 10px;
+//         padding: 4px 20px;
+
+//         :global {
+//             .plyr__time+.plyr__time:before {
+//                 content: '';
+//             }
+//         }
+//     }
+
+//     .slider {
+//         width: 100%;
+//         padding: 0 20px;
+
+//         :global {
+//             .van-slider__button {
+//                 background: var(--van-primary);
+//             }
+
+//             .van-loading {
+//                 width: 100%;
+//                 height: 100%;
+//             }
+//         }
+//     }
+
+//     .actions {
+//         display: flex;
+//         justify-content: space-between;
+//         width: 100%;
+//         color: #fff;
+//         font-size: 12px;
+//         padding: 0 20px;
+//         align-items: center;
+
+//         .actionWrap {
+//             display: flex;
+//         }
+
+//         .actionBtn {
+//             display: flex;
+//             width: 38px;
+//             height: 38px;
+//             padding: 4px 0;
+//             background: transparent;
+//         }
+
+//         .actionBtn>img {
+//             width: 100%;
+//             height: 100%;
+//         }
+
+//         :global {
+//             .van-loading__circular {
+//                 width: 100%;
+//                 height: 100%;
+//             }
+//         }
+
+//         .playIcon {
+//             display: none;
+//         }
+
+//         .btnPlay img:nth-child(2) {
+//             display: block;
+//         }
+
+//         .btnPause img:nth-child(3) {
+//             display: block;
+//         }
+
+//         .btnPlay,
+//         .btnPause {
+//             :global {
+//                 .van-loading {
+//                     display: none;
+//                 }
+//             }
+//         }
+//         .loopBtn{
+//             :global{
+//                 .loop{
+//                     display: block;
+//                 }
+//                 .loopActive{
+//                     display: none;
+//                 }
+//             }
+//         }
+//         .loopBtn.active{
+//             :global{
+//                 .loop{
+//                     display: none;
+//                 }
+//                 .loopActive{
+//                     display: block;
+//                 }
+//             }
+//         }
+
+//     }
+// }
+
 .videoWrap {
+  position: relative;
+  width: 100%;
+  height: 100%;
+
+  .videoSection {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 8;
+  }
+}
+
+.content {
+  position: relative;
+  height: 100%;
+}
+
+.contentWrap {
+  height: 100%;
+
+  video {
     width: 100%;
     height: 100%;
+  }
+}
+
+.videoSection {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+}
+
+.controls {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  height: 80px;
+  background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
+  display: flex;
+  flex-direction: column;
+  justify-content: space-between;
+  transition: all 0.5s;
+  width: 100%;
+  z-index: 9;
+
+  &.hide {
+    transform: translateY(100%);
+  }
+
+  .time {
+    display: flex;
+    justify-content: space-between;
+    // width: 100%;
+    color: #fff;
+    font-size: 10px;
+    padding: 4px 20px;
+  }
+
+  .slider {
+    // width: 100%;
+    padding: 0 20px;
+    --van-slider-button-width: 13px !important;
+    --van-slider-button-height: 13px !important;
 
     :global {
-        .plyr--video {
-            width: 100%;
-            height: 100%;
-        }
+      .n-slider {
+        --n-handle-size: 13px !important;
+        --n-fill-color: var(--van-primary-color) !important;
+        --n-fill-color-hover: var(--van-primary-color) !important;
+      }
 
-        .plyr__time {
-            display: block !important;
-        }
-        .plyr__video-wrapper{
-            pointer-events: none;
-        }
+      .van-loading {
+        width: 100%;
+        height: 100%;
+      }
     }
-}
+  }
 
-:global(.bottomFixed).controls {
-    width: 100%;
-    background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
-    padding: 0 !important;
-    flex-direction: column;
-    transition: all 0.5s;
-
-    .time {
-        display: flex;
-        justify-content: space-between;
-        width: 100%;
-        color: #fff;
-        font-size: 10px;
-        padding: 4px 20px;
-
-        :global {
-            .plyr__time+.plyr__time:before {
-                content: '';
-            }
-        }
+  .actionSection {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 0 16px 8px 15px;
+
+    .name {
+      font-size: 14px;
+      font-weight: 500;
+      color: #FFFFFF;
     }
+  }
 
-    .slider {
-        width: 100%;
-        padding: 0 20px;
+  .actions {
+    display: flex;
+    // width: 100%;
+    color: #fff;
+    font-size: 12px;
 
-        :global {
-            .van-slider__button {
-                background: var(--van-primary);
-            }
+    align-items: center;
 
-            .van-loading {
-                width: 100%;
-                height: 100%;
-            }
-        }
+    .actionWrap {
+      display: flex;
     }
 
-    .actions {
-        display: flex;
-        justify-content: space-between;
+    .actionBtn {
+      display: flex;
+      width: 24px;
+      height: 24px;
+      padding: 4px 0;
+      background: transparent;
+
+      &+.actionBtn {
+        margin-left: 12px;
+      }
+    }
+
+    .actionBtn>img {
+      width: 100%;
+      height: 100%;
+    }
+
+    :global {
+      .van-loading__circular {
         width: 100%;
-        color: #fff;
-        font-size: 12px;
-        padding: 0 20px;
-        align-items: center;
+        height: 100%;
+      }
+    }
 
-        .actionWrap {
-            display: flex;
-        }
+    .playIcon {
+      display: none;
+    }
 
-        .actionBtn {
-            display: flex;
-            width: 38px;
-            height: 38px;
-            padding: 4px 0;
-            background: transparent;
-        }
+    .btnPlay img:nth-child(2) {
+      display: block;
+    }
 
-        .actionBtn>img {
-            width: 100%;
-            height: 100%;
-        }
+    .btnPause img:nth-child(3) {
+      display: block;
+    }
 
-        :global {
-            .van-loading__circular {
-                width: 100%;
-                height: 100%;
-            }
+    .btnPlay,
+    .btnPause {
+      :global {
+        .van-loading {
+          display: none;
         }
+      }
+    }
 
-        .playIcon {
-            display: none;
+    .loopBtn {
+      :global {
+        .loop {
+          display: block;
         }
 
-        .btnPlay img:nth-child(2) {
-            display: block;
+        .loopActive {
+          display: none;
         }
+      }
+    }
 
-        .btnPause img:nth-child(3) {
-            display: block;
+    .loopBtn.active {
+      :global {
+        .loop {
+          display: none;
         }
 
-        .btnPlay,
-        .btnPause {
-            :global {
-                .van-loading {
-                    display: none;
-                }
-            }
-        }
-        .loopBtn{
-            :global{
-                .loop{
-                    display: block;
-                }
-                .loopActive{
-                    display: none;
-                }
-            }
+        .loopActive {
+          display: block;
         }
-        .loopBtn.active{
-            :global{
-                .loop{
-                    display: none;
-                }
-                .loopActive{
-                    display: block;
-                }
-            }
-        }
-
+      }
     }
+
+  }
 }

+ 119 - 23
src/views/coursewarePlay/index.tsx

@@ -34,6 +34,7 @@ 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'
+import VideoPlay from './component/video-play'
 
 export default defineComponent({
   name: 'CoursewarePlay',
@@ -403,11 +404,19 @@ export default defineComponent({
     }
 
     /**停止所有的播放 */
-    const handleStop = async () => {
-      const videos = document.querySelectorAll('video')
-      for (let i = 0; i < videos.length; i++) {
-        const videoEle = videos[i] as HTMLVideoElement
-        await stopVideo(videoEle)
+    const handleStop = () => {
+      for (let i = 0; i < data.itemList.length; i++) {
+        const activeItem = data.itemList[i]
+        if (activeItem.type === 'VIDEO') {
+          activeItem.videoEle?.currentTime(0)
+          activeItem.videoEle?.pause()
+          // activeItem.videoEle?.stop()
+        }
+        // console.log('🚀 ~ activeItem:', activeItem)
+        // 停止曲谱的播放
+        if (activeItem.type === 'SONG') {
+          activeItem.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
+        }
       }
       console.log('视频暂停完成')
       data.itemList.forEach((item: any) => {
@@ -540,7 +549,6 @@ export default defineComponent({
       checkedAnimation(popupData.activeIndex, index)
       nextTick(() => {
         popupData.activeIndex = index
-
         acitveTimer.value = setTimeout(
           () => {
             popupData.playIndex = index
@@ -554,20 +562,28 @@ export default defineComponent({
                 activeData.model = true
               }
             }
+            if (item.type === 'VIDEO') {
+              // 自动播放下一个视频
+              clearTimeout(activeData.timer)
+              closeToast()
+              item.autoPlay = true
+              // console.log(item, 'item')
+              // 当视屏异常时重置链接
+              if (item.error) {
+                item.videoEle?.src(item.content)
+                item.error = false
+              }
+              nextTick(() => {
+                item.videoEle?.play()
+              })
+            }
+
             requestAnimationFrame(() => {
               const _effectIndex = effectIndex.value + 1
               effectIndex.value = _effectIndex >= effects.length - 1 ? 0 : _effectIndex
-
-              if (item && item.type === 'VIDEO') {
-                // 自动播放下一个视频
-                clearTimeout(activeData.timer)
-                closeToast()
-                item.autoPlay = true
-                data.animationState = 'end'
-              }
             })
           },
-          activeData.isAnimation ? 850 : 0
+          activeData.isAnimation ? 800 : 0
         )
       })
     }
@@ -726,19 +742,16 @@ export default defineComponent({
       if (repeat) {
         if (tempTime.length > 0) {
           // console.log('join video', tempTime, 'initTime', initTime)
-          tempTime[1] = Math.floor(activeVideoRef.currentTime)
+          tempTime[1] = Math.floor(activeVideoRef.currentTime())
         }
       } else {
         if (newVal) {
-          tempTime[0] = Math.floor(activeVideoRef.currentTime)
+          tempTime[0] = Math.floor(activeVideoRef.currentTime())
         } else {
-          tempTime[1] = Math.floor(activeVideoRef.currentTime)
+          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)
         // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
@@ -830,7 +843,7 @@ export default defineComponent({
               }
               class={styles.itemDiv}
             >
-              <VideoItem
+              {/* <VideoItem
                 ref={(el: any) => (data.videoItemRef = el)}
                 item={activeVideoItem.value}
                 activeModel={activeData.model}
@@ -878,6 +891,81 @@ export default defineComponent({
                     videoIntervalRef.resume()
                   }
                 }}
+              /> */}
+              <VideoPlay
+                ref={(el: any) => (data.videoItemRef = el)}
+                item={activeVideoItem.value}
+                activeModel={activeData.model}
+                // isEmtry={isEmtry}
+                onPlay={() => {
+                  data.videoState = 'play'
+                  data.animationState = 'end'
+                }}
+                onLoadedmetadata={(videoItem: any) => {
+                  data.videoState = 'play'
+                  activeVideoItem.value.videoEle = videoItem
+                  if (!activeVideoItem.value.isprepare) {
+                    activeVideoItem.value.isprepare = true
+                  }
+                }}
+                onPause={() => {
+                  clearTimeout(activeData.timer)
+                  activeData.model = true
+                  videoIntervalRef.pause()
+                }}
+                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?.paused()
+                  ) {
+                    videoIntervalRef.resume()
+                  }
+                }}
+                onTogglePlay={(paused: boolean) => {
+                  // console.log('播放切换', paused)
+                  // 首次播放完成
+                  if (!activeVideoItem.value.isprepare) {
+                    activeVideoItem.value.isprepare = true
+                  }
+                  activeVideoItem.value.autoPlay = false
+                  if (paused || popupData.open || popupData.guideOpen) {
+                    clearTimeout(activeData.timer)
+                  } else {
+                    setModelOpen()
+                  }
+                }}
+                onEnded={async () => {
+                  const _index = popupData.activeIndex + 1
+                  if (_index < data.itemList.length) {
+                    handleSwipeChange(_index)
+                  } else {
+                    // 说明是最后一个
+                    intervalFnRef.value.pause()
+                    // 同步数据时先进行有效时间进行保存
+                    initVideoCount(false, true)
+                    await updateStat()
+                  }
+                }}
+                onReset={() => {
+                  if (!activeVideoItem.value.videoEle?.paused) {
+                    setModelOpen()
+                  }
+                }}
+                onError={() => {
+                  // 视屏异常
+                  activeVideoItem.value.error = true
+                }}
               />
             </div>
             {data.itemList.map((m: any, mIndex: number) => {
@@ -916,6 +1004,9 @@ export default defineComponent({
                           }}
                           onLoadedmetadata={(videoItem: any) => {
                             m.videoEle = videoItem
+                            if (!m.isprepare) {
+                              m.isprepare = true
+                            }
                           }}
                           onTogglePlay={(paused: boolean) => {
                             // console.log('播放切换', paused)
@@ -940,6 +1031,10 @@ export default defineComponent({
                               setModelOpen()
                             }
                           }}
+                          onError={() => {
+                            // 视屏异常
+                            m.error = true
+                          }}
                         />
                         <Transition name="van-fade">
                           {!m.isprepare && (
@@ -953,7 +1048,8 @@ export default defineComponent({
                   <Transition name="van-fade">
                     {m.type === 'VIDEO' &&
                       data.animationState !== 'end' &&
-                      data.videoState != 'play' && (
+                      data.videoState != 'play' &&
+                      !m.isprepare && (
                         <div class={styles.loadWrap}>
                           <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
                         </div>

+ 9 - 0
src/views/exercise-after-class/index.module.less

@@ -126,6 +126,15 @@
     height: 100%;
     object-fit: contain;
   }
+
+  .videoSection {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 8;
+  }
 }
 
 .videoModel {

+ 10 - 105
src/views/exercise-after-class/index.tsx

@@ -1,15 +1,5 @@
-import { Icon, showConfirmDialog, showDialog, Slider, Swipe, SwipeItem } from 'vant'
-import {
-  defineComponent,
-  onMounted,
-  reactive,
-  onUnmounted,
-  ref,
-  watch,
-  Transition,
-  nextTick,
-  computed
-} from 'vue'
+import { Icon, showConfirmDialog, Swipe, SwipeItem } from 'vant'
+import { defineComponent, onMounted, reactive, onUnmounted, ref, Transition } from 'vue'
 import styles from './index.module.less'
 import 'plyr/dist/plyr.css'
 import request from '@/helpers/request'
@@ -28,6 +18,7 @@ import qs from 'query-string'
 import { Vue3Lottie } from 'vue3-lottie'
 import playLoadData from '../coursewarePlay/datas/data.json'
 import { handleCheckVip } from '../hook/useFee'
+import VideoClass from './video-class'
 import item from '@/student/coupons/item'
 import { usePageVisibility } from '@vant/use'
 
@@ -379,100 +370,14 @@ export default defineComponent({
               return (
                 <SwipeItem>
                   <>
-                    <div
-                      class={styles.itemDiv}
-                      onClick={() => {
-                        clearTimeout(m.timer)
-                        activeData.model = !activeData.model
+                    <VideoClass
+                      item={m}
+                      modal={activeData.model}
+                      onEnded={(m: any) => addTrainingRecord(m)}
+                      onChangeModal={(status: boolean) => {
+                        activeData.model = status
                       }}
-                    >
-                      <video
-                        playsinline="false"
-                        preload="auto"
-                        class="player"
-                        poster={iconVideobg}
-                        data-vid={m.id}
-                        src={m.content}
-                        loop={m.loop}
-                        autoplay={m.autoplay}
-                        muted={m.muted}
-                        onLoadedmetadata={async (e: Event) => {
-                          const videoEle = e.target as unknown as HTMLVideoElement
-                          m.duration = videoEle.duration
-                          m.videoEle = videoEle
-                          m.loaded = true
-                        }}
-                        onTimeupdate={(e: Event) => {
-                          if (!m.loaded) return
-                          const videoEle = e.target as unknown as HTMLVideoElement
-                          m.currentTime = videoEle.currentTime
-                        }}
-                        onPlay={() => {
-                          console.log('播放')
-                          // 播放
-                          m.paused = false
-                          if (m.muted) {
-                            m.muted = false
-                            m.videoEle.pause()
-                          }
-                        }}
-                        onPause={() => {
-                          console.log('暂停')
-                          //暂停
-                          m.paused = true
-                        }}
-                        onEnded={() => addTrainingRecord(m)}
-                      >
-                        <source src={m.content} type="video/mp4" />
-                      </video>
-                    </div>
-                    <Transition name="bottom">
-                      {activeData.model && !m.muted && (
-                        <div class={styles.bottomFixedContainer}>
-                          <div class={styles.time}>
-                            <span>{getSecondRPM(m.currentTime)}</span>
-                            <span>{getSecondRPM(m.duration)}</span>
-                          </div>
-                          <div class={styles.slider}>
-                            {m.duration && (
-                              <Slider
-                                buttonSize={16}
-                                modelValue={m.currentTime}
-                                min={0}
-                                max={m.duration}
-                              />
-                            )}
-                          </div>
-
-                          <div class={styles.actions}>
-                            <div class={styles.actionBtn}>
-                              {m.paused ? (
-                                <img
-                                  src={iconplay}
-                                  onClick={(e: Event) => {
-                                    clearTimeout(m.timer)
-                                    m.videoEle?.play()
-                                    m.paused = false
-                                    m.timer = setTimeout(() => {
-                                      activeData.model = false
-                                    }, 4000)
-                                  }}
-                                />
-                              ) : (
-                                <img
-                                  src={iconpause}
-                                  onClick={(e: Event) => {
-                                    clearTimeout(m.timer)
-                                    m.videoEle?.pause()
-                                    m.paused = true
-                                  }}
-                                />
-                              )}
-                            </div>
-                          </div>
-                        </div>
-                      )}
-                    </Transition>
+                    />
                     {m.muted && (
                       <div class={styles.loadWrap}>
                         <Vue3Lottie animationData={playLoadData}></Vue3Lottie>

+ 169 - 0
src/views/exercise-after-class/video-class.tsx

@@ -0,0 +1,169 @@
+import { defineComponent, onMounted, ref, watch, Transition, toRefs } from 'vue'
+import styles from './index.module.less'
+import { Slider } from 'vant'
+import iconplay from '../coursewarePlay/image/icon-play.svg'
+import iconpause from '../coursewarePlay/image/icon-pause.svg'
+import iconVideobg from '../coursewarePlay/image/icon-videobg.png'
+import { getSecondRPM } from '@/helpers/utils'
+import TCPlayer from 'tcplayer.js'
+import 'tcplayer.js/dist/tcplayer.min.css'
+export default defineComponent({
+  name: 'video-class',
+  props: {
+    item: {
+      type: Object,
+      default: () => {
+        return {}
+      }
+    },
+    modal: {
+      type: Boolean,
+      default: true
+    }
+  },
+  emits: ['loadedmetadata', 'togglePlay', 'ended', 'reset', 'error', 'close', 'changeModal'],
+  setup(props, { emit }) {
+    const { item, modal } = toRefs(props)
+    const videoItem = ref()
+    const videoID = 'video' + Date.now() + Math.floor(Math.random() * 100)
+
+    onMounted(() => {
+      videoItem.value = TCPlayer(videoID, {
+        appID: '',
+        controls: false,
+        loop: item.value.loop,
+        muted: item.value.muted
+        // autoplay: true
+      }) // player-container-id 为播放器容器 ID,必须与 html 中一致
+      if (videoItem.value) {
+        videoItem.value.poster(props.item.coverImg) // 封面
+        videoItem.value.src(item.value.content) // url 播放地址
+
+        // 初步加载时
+        videoItem.value.one('loadedmetadata', (e: any) => {
+          // console.log(' Loading metadata')
+
+          if (item.value.autoPlay && videoItem.value) {
+            videoItem.value?.play()
+          }
+          // 获取时长
+          const videoEle = videoItem.value
+          item.value.duration = videoEle.duration()
+          item.value.videoEle = videoEle
+          item.value.loaded = true
+          emit('loadedmetadata', videoItem.value)
+        })
+
+        // 视频播放时加载
+        videoItem.value.on('timeupdate', () => {
+          if (!item.value.loaded) return
+          const videoEle = videoItem.value
+          item.value.currentTime = videoEle.currentTime()
+        })
+
+        // 视频播放结束
+        videoItem.value.on('ended', () => {
+          emit('ended', item.value)
+        })
+
+        //
+        videoItem.value.on('pause', () => {
+          console.log('暂停')
+          //暂停
+          item.value.paused = true
+        })
+
+        videoItem.value.on('play', () => {
+          console.log('播放')
+          // 播放
+          item.value.paused = false
+          if (item.value.muted) {
+            item.value.muted = false
+            item.value.videoEle.pause()
+          }
+        })
+
+        // 视频播放异常
+        videoItem.value.on('error', () => {
+          emit('error')
+        })
+      }
+    })
+    return () => (
+      <>
+        <div
+          class={styles.itemDiv}
+          onClick={() => {
+            clearTimeout(item.value.timer)
+            // activeData.model = !activeData.model
+            emit('changeModal', !modal.value)
+          }}
+        >
+          <video
+            id={videoID}
+            playsinline="false"
+            preload="auto"
+            class="player"
+            poster={iconVideobg}
+            data-vid={item.value.id}
+            src={item.value.content}
+            loop={item.value.loop}
+            autoplay={item.value.autoplay}
+            muted={item.value.muted}
+          >
+            <source src={item.value.content} type="video/mp4" />
+          </video>
+          <div class={styles.videoSection}></div>
+        </div>
+        <Transition name="bottom">
+          {modal.value && !item.value.muted && (
+            <div class={styles.bottomFixedContainer}>
+              <div class={styles.time}>
+                <span>{getSecondRPM(item.value.currentTime)}</span>
+                <span>{getSecondRPM(item.value.duration)}</span>
+              </div>
+              <div class={styles.slider}>
+                {item.value.duration && (
+                  <Slider
+                    buttonSize={16}
+                    modelValue={item.value.currentTime}
+                    min={0}
+                    max={item.value.duration}
+                  />
+                )}
+              </div>
+
+              <div class={styles.actions}>
+                <div class={styles.actionBtn}>
+                  {item.value.paused ? (
+                    <img
+                      src={iconplay}
+                      onClick={() => {
+                        clearTimeout(item.value.timer)
+                        item.value.videoEle?.play()
+                        item.value.paused = false
+                        item.value.timer = setTimeout(() => {
+                          // activeData.model = false
+                          emit('changeModal', false)
+                        }, 4000)
+                      }}
+                    />
+                  ) : (
+                    <img
+                      src={iconpause}
+                      onClick={() => {
+                        clearTimeout(item.value.timer)
+                        item.value.videoEle?.pause()
+                        item.value.paused = true
+                      }}
+                    />
+                  )}
+                </div>
+              </div>
+            </div>
+          )}
+        </Transition>
+      </>
+    )
+  }
+})

+ 3 - 3
vite.config.ts

@@ -11,9 +11,8 @@ function resolve(dir: string) {
 }
 // https://vitejs.dev/config/
 // https://github.com/vitejs/vite/issues/1930 .env
-// const proxyUrl = 'https://online.lexiaoya.cn/'
-// const proxyUrl = 'https://test.lexiaoya.cn/'
-const proxyUrl = 'https://dev.lexiaoya.cn/'
+// const proxyUrl = 'https://online.lexiaoya.cn/';
+const proxyUrl = 'https://test.lexiaoya.cn/'
 // const proxyUrl = 'http://47.98.131.38:8989/'
 // const proxyUrl = 'http://192.168.3.20:8989/' // 邹旋
 // const proxyUrl = 'http://192.168.3.143:8989/' // 尚科
@@ -51,6 +50,7 @@ export default defineConfig({
     port: 1000,
     strictPort: true,
     cors: true,
+    // https: true,
     proxy: {
       '/api-oauth': {
         target: proxyUrl,