skyblued 2 年 前
コミット
b221d391be

+ 113 - 0
src/views/exercise-after-class/index copy.tsx

@@ -0,0 +1,113 @@
+import request from '@/helpers/request'
+import { browser } from '@/helpers/utils'
+import { state } from '@/state'
+import Plyr from 'plyr'
+import 'plyr/dist/plyr.css'
+import { NoticeBar } from 'vant'
+import { defineComponent, onMounted, reactive, nextTick, computed } from 'vue'
+import { useRoute } from 'vue-router'
+import styles from './index.module.less'
+export default defineComponent({
+  name: 'exercise-after-class',
+  setup() {
+    const route = useRoute()
+    const data = reactive({
+      loading: true,
+      recordLoading: false,
+      currentTime: 0,
+      duration: 0,
+      video: '',
+    })
+    console.log(route.query)
+    const getLessonTraining = async () => {
+      let details = []
+      try {
+        const res: any = await request.get(
+          state.platformApi + `/lessonTraining/courseSchedule/${route.query.courseScheduleId}`
+        )
+        if (Array.isArray(res?.data)) {
+          const studentLevel = state.user?.data?.studentLevel || 1
+          details = res.data.find((n: any) => n.studentLevel === studentLevel)?.details || []
+          console.log('🚀 ~ details', details)
+        }
+      } catch (error) {
+        console.log('error')
+      }
+      if (details.length) {
+        data.video =
+          (details.find((n: any) => n.materialId == route.query.materialId) as any)?.content || ''
+        console.log('🚀 ~ data.video', data.video)
+        nextTick(() => {
+          initVideo()
+        })
+      }
+    }
+    const initVideo = () => {
+      const player = new Plyr('#player', {
+        clickToPlay: true,
+        invertTime: true,
+        controls: ['play-large', 'play', 'current-time', 'restart', 'fullscreen']
+      })
+      player.on('loadeddata', () => {
+        data.duration = player.duration
+        console.log('🚀 ~ player', player.duration)
+        data.loading = false
+      })
+      player.on('timeupdate', () => {
+        data.currentTime = player.currentTime
+        // console.log(timeRemaining.value)
+        if (timeRemaining.value === 0) {
+          if (data.recordLoading || data.currentTime === 0) return
+          console.log('完成观看次数')
+          addTrainingRecord()
+        }
+      })
+    }
+    const timeRemaining = computed(() => {
+      return Math.ceil(data.duration - data.currentTime)
+    })
+    onMounted(() => {
+      getLessonTraining()
+    })
+    // 达到指标,记录
+    const addTrainingRecord = async () => {
+      data.recordLoading = true
+      const browserInfo = browser()
+      const body = {
+        materialType: 'VIDEO',
+        record: {
+          sourceTime: data.duration,
+          clientType: state.platformType,
+          feature: 'LESSON_TRAINING',
+          deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
+        },
+        courseScheduleId: route.query.courseScheduleId,
+        lessonTrainingId: route.query.lessonTrainingId,
+        materialId: route.query.materialId
+      }
+      try {
+        const res: any = await request.post(
+          state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
+          {
+            data: body
+          }
+        )
+      } catch (error) {}
+      data.recordLoading = false
+    }
+    return () => (
+      <div class={styles['exercise-after-class']}>
+        <video id="player" src={data.video}></video>
+        {!data.loading && (
+          <>
+            {timeRemaining.value === 0 ? (
+              <NoticeBar background="green" color="#fff" text={`已完成本次训练`} />
+            ) : (
+              <NoticeBar text={`还需要观看 ${timeRemaining.value} 秒,就可以完成课后训练了`} />
+            )}
+          </>
+        )}
+      </div>
+    )
+  }
+})

+ 184 - 2
src/views/exercise-after-class/index.module.less

@@ -1,4 +1,186 @@
-.exercise-after-class{
+.coursewarePlay {
+    position: relative;
+    height: 100vh;
+    background-color: rgba(89, 98, 126, 0.2);
+  }
+  .playModel {
+    position: absolute;
+    left: 0;
+    top: 0;
+    right: 0;
+    bottom: 0;
+    box-shadow: inset 0px 0px 164px 0px rgba(0, 0, 0, 1);
+    pointer-events: none;
+  }
+  .headerContainer {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    z-index: 1;
+    padding: 10px;
+    display: flex;
+    align-items: center;
+    color: #fff;
+    font-size: 12px;
+    background: linear-gradient(180deg, rgba(0, 0, 0, .6), transparent);
+  }
+  .backBtn {
+    color: #fff;
+    width: 40px;
+    height: 26px;
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    z-index: 10;
+  }
+  .menu {
+    flex: 1;
+    display: flex;
+    justify-content: center;
+    color: #fff;
+  }
+  .tabsContent {
     width: 100vw;
     height: 100vh;
-}
+    :global {
+      .van-tabs__wrap {
+        display: none !important;
+      }
+      .van-tabs__content {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+  .itemDiv {
+    position: relative;
+    width: 100%;
+    height: 100%;
+    video {
+      width: 100%;
+      height: 100%;
+    }
+    img {
+      display: block;
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+  }
+  .rightFixedBtns {
+    position: fixed;
+    top: 50%;
+    transform: translateY(-50%);
+    right: 20px;
+    .point {
+      margin-top: 10px;
+      border-bottom-left-radius: 0;
+      border-bottom-right-radius: 0;
+    }
+    .point + .fullBtn {
+      border-top-left-radius: 0;
+      border-top-right-radius: 0;
+    }
+  }
+  .leftFixedBtns {
+    position: fixed;
+    top: 50%;
+    transform: translateY(-50%);
+    left: 20px;
+    .prePoint {
+      margin-bottom: 8px;
+    }
+  }
+  .fullBtn {
+    width: 38px;
+    height: 55px;
+    background: rgba(51, 51, 51, 0.15);
+    border-radius: 8px;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    color: #fff;
+    justify-content: space-evenly;
+    &:active {
+      opacity: 0.8;
+    }
+  }
+  .bottomFixedContainer {
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 10;
+    background: linear-gradient(0deg, rgba(0, 0, 0, 0.5), transparent);
+    .time {
+      display: flex;
+      justify-content: space-between;
+      color: #fff;
+      font-size: 10px;
+      padding: 4px 10px;
+    }
+    .slider {
+      padding: 8px 10px;
+    }
+    .actions {
+      display: flex;
+      justify-content: space-between;
+      color: #fff;
+      font-size: 12px;
+      padding: 8px 10px;
+      align-items: center;
+      :global {
+        .van-icon {
+          font-size: 20px;
+          margin-right: 14px;
+        }
+      }
+    }
+  }
+  .popup {
+    background: rgba(0, 0, 0, 0.5);
+  }
+  .overlayClass {
+    --van-overlay-background: transparent;
+  }
+  :global {
+    .top-enter-active,
+    .top-leave-active {
+      transition: transform 0.5s;
+    }
+    .top-enter-from,
+    .top-leave-to {
+      transform: translateY(-100%);
+    }
+  
+    .left-enter-active,
+    .left-leave-active {
+      transition: all 0.5s;
+    }
+    .left-enter-from,
+    .left-leave-to {
+      left: -60px;
+    }
+  
+    .right-enter-active,
+    .right-leave-active {
+      transition: all 0.5s;
+    }
+  
+    .right-enter-from,
+    .right-leave-to {
+      right: -60px;
+    }
+  
+    .bottom-enter-active,
+    .bottom-leave-active {
+      transition: transform 0.5s;
+    }
+  
+    .bottom-enter-from,
+    .bottom-leave-to {
+      transform: translateY(100%);
+    }
+  }
+  

+ 398 - 60
src/views/exercise-after-class/index.tsx

@@ -1,25 +1,110 @@
+import {
+  closeToast,
+  Icon,
+  Popup,
+  showConfirmDialog,
+  showToast,
+  Slider,
+  Swipe,
+  SwipeItem
+} from 'vant'
+import {
+  defineComponent,
+  onMounted,
+  reactive,
+  nextTick,
+  onUnmounted,
+  ref,
+  watch,
+  Transition,
+  computed
+} from 'vue'
+import styles from './index.module.less'
+import 'plyr/dist/plyr.css'
 import request from '@/helpers/request'
-import { browser } from '@/helpers/utils'
 import { state } from '@/state'
-import Plyr from 'plyr'
-import 'plyr/dist/plyr.css'
-import { NoticeBar } from 'vant'
-import { defineComponent, onMounted, reactive, nextTick, computed } from 'vue'
-import { useRoute } from 'vue-router'
-import styles from './index.module.less'
+import { useRoute, useRouter } from 'vue-router'
+import iconBack from '../coursewarePlay/image/back.svg'
+import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
+import iconLoop from '../coursewarePlay/image/icon-loop.svg'
+import iconLoopActive from '../coursewarePlay/image/icon-loop-active.svg'
+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 { browser, getSecondRPM } from '@/helpers/utils'
+
+const materialType = {
+  视频: 'VIDEO',
+  图片: 'IMG',
+  曲目: 'SONG'
+}
+
 export default defineComponent({
   name: 'exercise-after-class',
   setup() {
+    const handleInit = (type = 0) => {
+      // 横屏
+      postMessage({
+        api: 'setRequestedOrientation',
+        content: {
+          orientation: type
+        }
+      })
+      // 头,包括返回箭头
+      postMessage({
+        api: 'setTitleBarVisibility',
+        content: {
+          status: type
+        }
+      })
+      // 安卓的状态栏
+      postMessage({
+        api: 'setStatusBarVisibility',
+        content: {
+          isVisibility: type
+        }
+      })
+    }
+    handleInit()
+    onUnmounted(() => {
+      handleInit(1)
+    })
+
     const route = useRoute()
+    const router = useRouter()
+    const query = route.query
+    const browserInfo = browser()
+    const headeRef = ref()
     const data = reactive({
+      videoData: null as any,
+      details: [] as any,
+      trainingTimes: 0,
+      itemList: [] as any,
+      showHead: true,
       loading: true,
-      recordLoading: false,
-      video: '',
-      currentTime: 0,
-      duration: 0
+      recordLoading: false
+    })
+    const activeData = reactive({
+      nowTime: 0,
+      model: true, // 遮罩
+      timer: null as any,
+      item: null as any
     })
-    console.log(route.query)
-    const getLessonTraining = async () => {
+    // 获取缓存路径
+    const getCacheFilePath = async (material: any) => {
+      const res = await promisefiyPostMessage({
+        api: 'getCourseFilePath',
+        content: {
+          url: material.content,
+          localPath: '',
+          materialId: material.id,
+          updateTime: material.updateTime,
+          type: material.type
+        }
+      })
+      return res
+    }
+    const getDetail = async () => {
       let details = []
       try {
         const res: any = await request.get(
@@ -28,62 +113,118 @@ export default defineComponent({
         if (Array.isArray(res?.data)) {
           const studentLevel = state.user?.data?.studentLevel || 1
           details = res.data.find((n: any) => n.studentLevel === studentLevel)?.details || []
-          console.log('🚀 ~ details', details)
         }
       } catch (error) {
         console.log('error')
       }
       if (details.length) {
-        data.video =
-          (details.find((n: any) => n.materialId == route.query.materialId) as any)?.content || ''
-        console.log('🚀 ~ data.video', data.video)
-        nextTick(() => {
-          initVideo()
+        data.details = details
+        const videoData: any =
+          details.find((n: any) => n.materialId == route.query.materialId) || {}
+        try {
+          videoData.training = JSON.parse(videoData?.lessonTrainingTemp?.trainingConfigJson)
+        } catch (error) {}
+        //请求本地缓存
+        if (browserInfo.isApp && ['VIDEO'].includes(videoData.type)) {
+          const localData = await getCacheFilePath(videoData)
+          if (localData?.content?.localPath) {
+            videoData.url = videoData.content
+            videoData.content = localData.content.localPath
+          }
+        }
+        data.itemList.push({
+          ...videoData,
+          id: videoData.materialId,
+          currentTime: 0,
+          duration: 100,
+          paused: true,
+          loop: false,
+          videoEle: null,
+          timer: null,
+          playModel: true
         })
+        popupData.itemActive = videoData.id
+        popupData.tabName = videoData.materialName
+        data.videoData = videoData
+        handleExerciseCompleted()
       }
     }
-    const initVideo = () => {
-      const player = new Plyr('#player', {
-        clickToPlay: true,
-        invertTime: true,
-        controls: ['play-large', 'play', 'current-time', 'restart', 'fullscreen']
-      })
-      player.on('loadeddata', () => {
-        data.duration = player.duration
-        console.log('🚀 ~ player', player.duration)
-        data.loading = false
-      })
-      player.on('timeupdate', () => {
-        data.currentTime = player.currentTime
-        // console.log(timeRemaining.value)
-        if (timeRemaining.value === 0) {
-          if (data.recordLoading || data.currentTime === 0) return
-          console.log('完成观看次数')
-          addTrainingRecord()
+    const getTrainingTimes = (res: any) => {
+      let trainingTimes = 0
+      if (Array.isArray(res?.trainings)) {
+        const train = res.trainings.find((n: any) => n.materialId === query.materialId)
+        if (train) {
+          trainingTimes = train.trainingTimes
         }
-      })
+      }
+      data.trainingTimes = trainingTimes
+    }
+    // 获取课后练习记录
+    const trainingRecord = async () => {
+      try {
+        const res: any = await request.post(
+          state.platformApi +
+            `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`
+        )
+        if (res?.data) {
+          getTrainingTimes(res.data)
+          handleExerciseCompleted()
+        }
+      } catch (error) {}
     }
-    const timeRemaining = computed(() => {
-      return Math.ceil(data.duration - data.currentTime)
-    })
     onMounted(() => {
-      getLessonTraining()
+      getDetail()
+      trainingRecord()
     })
+    // 返回
+    const goback = () => {
+      postMessage({ api: 'back' })
+    }
+
+    const swipeRef = ref()
+    const popupData = reactive({
+      firstIndex: 0,
+      open: false,
+      activeIndex: -1,
+      tabActive: '',
+      tabName: '',
+      itemActive: '',
+      itemName: ''
+    })
+
+    // 双击
+    const handleDbClick = (item: any) => {
+      if (item && item.type === 'VIDEO') {
+        const videoEle: HTMLVideoElement = document.querySelector(`[data-vid='${item.id}']`)!
+        if (videoEle) {
+          if (videoEle.paused) {
+            closeToast()
+            videoEle.play()
+          } else {
+            showToast('已暂停')
+            videoEle.pause()
+          }
+        }
+        item.timer = setTimeout(() => {
+          activeData.model = false
+        }, 3000)
+      }
+    }
+
     // 达到指标,记录
-    const addTrainingRecord = async () => {
+    const addTrainingRecord = async (m: any) => {
       data.recordLoading = true
-      const browserInfo = browser()
       const body = {
         materialType: 'VIDEO',
         record: {
-          sourceTime: data.duration,
+          sourceTime: m.duration,
           clientType: state.platformType,
           feature: 'LESSON_TRAINING',
           deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
         },
-        courseScheduleId: route.query.courseScheduleId,
-        lessonTrainingId: route.query.lessonTrainingId,
-        materialId: route.query.materialId
+        courseScheduleId: query.courseScheduleId,
+        lessonTrainingId: query.lessonTrainingId,
+        materialId: query.materialId
       }
       try {
         const res: any = await request.post(
@@ -92,21 +233,218 @@ export default defineComponent({
             data: body
           }
         )
+        trainingRecord()
       } catch (error) {}
-      data.recordLoading = false
+      setTimeout(() => {
+        data.recordLoading = false
+      }, 2000)
+    }
+    // 停止所有的播放
+    const handleStopVideo = () => {
+      data.itemList.forEach((m: any) => {
+        m.videoEle?.pause()
+      })
+    }
+
+    // 判断练习是否完成
+    const handleExerciseCompleted = () => {
+      if (
+        data.trainingTimes != 0 &&
+        data.trainingTimes == (data.videoData as any)?.training?.practiceTimes
+      ) {
+        const itemIndex = data.details.findIndex(
+          (n: any) => n.materialId == data.videoData?.materialId
+        )
+        const isLastIndex = itemIndex === data.details.length - 1
+        showConfirmDialog({
+          title: '课后训练',
+          message: '你已完成该练习~',
+          confirmButtonColor: 'var(--van-primary)',
+          confirmButtonText: isLastIndex ? '完成' : '下一题',
+          cancelButtonText: '继续'
+        }).then(() => {
+          if (!isLastIndex) {
+            const nextItem = data.details[itemIndex + 1]
+            if (nextItem?.type === materialType.视频) {
+              // console.log('下一题视频', nextItem)
+              router.replace({
+                path: '/exerciseAfterClass',
+                query: {
+                  ...query,
+                  materialId: nextItem.materialId
+                }
+              })
+            }
+            if (nextItem?.type === materialType.曲目) {
+              let src = `${location.origin}/orchestra-music-score/?id=${nextItem.content}`
+              postMessage({
+                api: 'openAccompanyWebView',
+                content: {
+                  url: src,
+                  orientation: 0,
+                  isHideTitle: true,
+                  statusBarTextColor: false,
+                  isOpenLight: true
+                }
+              })
+            }
+          }
+        })
+      }
     }
     return () => (
-      <div class={styles['exercise-after-class']}>
-        <video id="player" src={data.video}></video>
-        {!data.loading && (
-          <>
-            {timeRemaining.value === 0 ? (
-              <NoticeBar background="green" color="#fff" text={`已完成本次训练`} />
-            ) : (
-              <NoticeBar text={`还需要观看 ${timeRemaining.value} 秒,就可以完成课后训练了`} />
-            )}
-          </>
-        )}
+      <div class={styles.coursewarePlay}>
+        <Swipe
+          style={{ height: '100vh' }}
+          ref={swipeRef}
+          showIndicators={false}
+          loop={false}
+          vertical
+          lazyRender={true}
+        >
+          {data.itemList.map((m: any, mIndex: number) => {
+            return (
+              <SwipeItem>
+                <>
+                  <div
+                    class={styles.itemDiv}
+                    onClick={() => {
+                      clearTimeout(activeData.timer)
+                      clearTimeout(m.timer)
+                      if (Date.now() - activeData.nowTime < 300) {
+                        handleDbClick(m)
+                        return
+                      }
+                      activeData.nowTime = Date.now()
+                      activeData.timer = setTimeout(() => {
+                        activeData.model = !activeData.model
+                      }, 300)
+                    }}
+                  >
+                    <video
+                      playsinline="false"
+                      preload="auto"
+                      class="player"
+                      poster={iconVideobg}
+                      data-vid={m.id}
+                      src={m.content}
+                      loop={m.loop}
+                      onLoadedmetadata={(e: Event) => {
+                        const videoEle = e.target as unknown as HTMLVideoElement
+                        m.currentTime = videoEle.currentTime
+                        m.duration = videoEle.duration
+                        m.videoEle = videoEle
+                      }}
+                      onTimeupdate={(e: Event) => {
+                        const videoEle = e.target as unknown as HTMLVideoElement
+                        m.currentTime = videoEle.currentTime
+                        if (m.duration - m.currentTime < 1) {
+                          if (data.recordLoading) return
+                          console.log('完成观看次数')
+                          addTrainingRecord(m)
+                        }
+                      }}
+                      onPlay={() => {
+                        // 播放
+                        m.paused = false
+                      }}
+                      onPause={() => {
+                        //暂停
+                        clearTimeout(m.timer)
+                        m.paused = true
+                      }}
+                    >
+                      <source src={m.content} type="video/mp4" />
+                    </video>
+                    <Transition name="bottom">
+                      {activeData.model && (
+                        <div class={styles.bottomFixedContainer}>
+                          <div class={styles.time}>
+                            <span>{getSecondRPM(m.currentTime)}</span>
+                            <span>{getSecondRPM(m.duration)}</span>
+                          </div>
+                          <div class={styles.slider}>
+                            <Slider
+                              buttonSize={16}
+                              step={0.01}
+                              modelValue={m.currentTime}
+                              min={0}
+                              max={m.duration}
+                            />
+                          </div>
+
+                          <div class={styles.actions}>
+                            <div>
+                              {m.paused ? (
+                                <Icon
+                                  name={iconplay}
+                                  onClick={(e: Event) => {
+                                    e.stopPropagation()
+                                    clearTimeout(m.timer)
+                                    closeToast()
+                                    m.videoEle?.play()
+                                    m.paused = false
+                                    m.timer = setTimeout(() => {
+                                      activeData.model = false
+                                    }, 3000)
+                                  }}
+                                />
+                              ) : (
+                                <Icon
+                                  name={iconpause}
+                                  onClick={(e: Event) => {
+                                    e.stopPropagation()
+                                    console.log('点击暂停')
+                                    m.videoEle?.pause()
+                                    m.paused = true
+                                  }}
+                                />
+                              )}
+                              {m.loop ? (
+                                <Icon
+                                  name={iconLoopActive}
+                                  onClick={(e: Event) => {
+                                    e.stopPropagation()
+                                    m.loop = false
+                                  }}
+                                />
+                              ) : (
+                                <Icon
+                                  name={iconLoop}
+                                  onClick={(e: Event) => {
+                                    e.stopPropagation()
+                                    m.loop = true
+                                  }}
+                                />
+                              )}
+                            </div>
+                            <div>{m.name}</div>
+                          </div>
+                        </div>
+                      )}
+                    </Transition>
+                  </div>
+                </>
+              </SwipeItem>
+            )
+          })}
+        </Swipe>
+
+        <Transition name="top">
+          {activeData.model && (
+            <div class={styles.headerContainer} ref={headeRef}>
+              <div class={styles.backBtn} onClick={() => goback()}>
+                <Icon name={iconBack} />
+                返回
+              </div>
+              <div class={styles.menu}>{popupData.tabName}</div>
+              <div class={styles.nums}>
+                练习次数:{data.trainingTimes}/
+                {(data.videoData as any)?.training?.practiceTimes || 0}
+              </div>
+            </div>
+          )}
+        </Transition>
       </div>
     )
   }