|
@@ -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>
|
|
|
)
|
|
|
}
|