index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import { Icon, showConfirmDialog, Slider, Swipe, SwipeItem } from 'vant'
  2. import {
  3. defineComponent,
  4. onMounted,
  5. reactive,
  6. onUnmounted,
  7. ref,
  8. watch,
  9. Transition,
  10. nextTick,
  11. computed
  12. } from 'vue'
  13. import styles from './index.module.less'
  14. import 'plyr/dist/plyr.css'
  15. import request from '@/helpers/request'
  16. import { state } from '@/state'
  17. import { useRoute, useRouter } from 'vue-router'
  18. import iconBack from '../coursewarePlay/image/back.svg'
  19. import { postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  20. import iconLoop from '../coursewarePlay/image/icon-loop.svg'
  21. import iconLoopActive from '../coursewarePlay/image/icon-loop-active.svg'
  22. import iconplay from '../coursewarePlay/image/icon-play.svg'
  23. import iconpause from '../coursewarePlay/image/icon-pause.svg'
  24. import iconVideobg from '../coursewarePlay/image/icon-videobg.png'
  25. import { browser, getSecondRPM } from '@/helpers/utils'
  26. import qs from 'query-string'
  27. import { Vue3Lottie } from 'vue3-lottie'
  28. import playLoadData from '../coursewarePlay/datas/data.json'
  29. const materialType = {
  30. 视频: 'VIDEO',
  31. 图片: 'IMG',
  32. 曲目: 'SONG'
  33. }
  34. export default defineComponent({
  35. name: 'exercise-after-class',
  36. setup() {
  37. /** 设置播放容器 16:9 */
  38. const parentContainer = reactive({
  39. width: '100vw'
  40. })
  41. const setContainer = () => {
  42. let min = Math.min(screen.width, screen.height)
  43. let max = Math.max(screen.width, screen.height)
  44. let width = min * (16 / 9)
  45. if (width > max) {
  46. parentContainer.width = '100vw'
  47. return
  48. } else {
  49. parentContainer.width = width + 'px'
  50. }
  51. }
  52. const handleInit = (type = 0) => {
  53. setContainer()
  54. // 横屏
  55. postMessage({
  56. api: 'setRequestedOrientation',
  57. content: {
  58. orientation: type
  59. }
  60. })
  61. // 头,包括返回箭头
  62. postMessage({
  63. api: 'setTitleBarVisibility',
  64. content: {
  65. status: type
  66. }
  67. })
  68. // 安卓的状态栏
  69. postMessage({
  70. api: 'setStatusBarVisibility',
  71. content: {
  72. isVisibility: type
  73. }
  74. })
  75. }
  76. handleInit()
  77. onUnmounted(() => {
  78. handleInit(1)
  79. })
  80. const route = useRoute()
  81. const router = useRouter()
  82. const query = route.query
  83. const browserInfo = browser()
  84. const headeRef = ref()
  85. const data = reactive({
  86. videoData: null as any,
  87. details: [] as any,
  88. trainings: [] as any[],
  89. trainingTimes: 0,
  90. itemList: [] as any,
  91. showHead: true,
  92. loading: true,
  93. recordLoading: false
  94. })
  95. const activeData = reactive({
  96. nowTime: 0,
  97. model: true, // 遮罩
  98. timer: null as any,
  99. item: null as any
  100. })
  101. const getDetail = async () => {
  102. data.itemList = []
  103. let details = []
  104. try {
  105. const res: any = await request.get(
  106. state.platformApi + `/lessonTraining/courseSchedule/${route.query.courseScheduleId}`,
  107. {
  108. hideLoading: true
  109. }
  110. )
  111. if (Array.isArray(res?.data)) {
  112. const studentLevel = state.user?.data?.studentLevel || 1
  113. details = res.data.find((n: any) => n.studentLevel === studentLevel)?.details || []
  114. }
  115. } catch (error) {
  116. console.log('error')
  117. }
  118. if (details.length) {
  119. details.forEach((n: any) => {
  120. try {
  121. n.trainingConfigJson = JSON.parse(n.lessonTrainingTemp.trainingConfigJson)
  122. } catch (error) {
  123. n.trainingConfigJson = {}
  124. }
  125. })
  126. data.details = details
  127. // console.log("🚀 ~ data.details", data.details)
  128. data.videoData = details.find((n: any) => n.materialId == route.query.materialId) || null
  129. // console.log('🚀 ~ data.videoData', data.videoData)
  130. data.itemList = [
  131. {
  132. ...data.videoData,
  133. id: data.videoData?.materialId || '',
  134. loaded: false,
  135. currentTime: 0,
  136. duration: 100,
  137. paused: true,
  138. loop: false,
  139. videoEle: null,
  140. timer: null,
  141. muted: true, // 静音
  142. autoplay: true //自动播放
  143. }
  144. ]
  145. }
  146. }
  147. // 获取课后练习记录
  148. const trainingRecord = async () => {
  149. try {
  150. const res: any = await request.post(
  151. state.platformApi +
  152. `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`,
  153. {
  154. hideLoading: true
  155. }
  156. )
  157. if (Array.isArray(res?.data.trainings)) {
  158. data.trainings = res.data.trainings
  159. handleExerciseCompleted()
  160. }
  161. } catch (error) {}
  162. }
  163. const currentNum = computed(() => {
  164. const item = data.trainings.find((n: any) => n.materialId == data.videoData?.materialId)
  165. // console.log(item)
  166. if (item) handleExerciseCompleted()
  167. return item?.trainingTimes || 0
  168. })
  169. onMounted(async () => {
  170. await getDetail()
  171. trainingRecord()
  172. })
  173. // 返回
  174. const goback = () => {
  175. postMessage({ api: 'back' })
  176. }
  177. const swipeRef = ref()
  178. const popupData = reactive({
  179. firstIndex: 0,
  180. open: false,
  181. activeIndex: -1,
  182. tabActive: '',
  183. tabName: '',
  184. itemActive: '',
  185. itemName: ''
  186. })
  187. // 达到指标,记录
  188. const addTrainingRecord = async (m: any) => {
  189. if (data.recordLoading) return
  190. console.log('记录观看次数')
  191. data.recordLoading = true
  192. const query = route.query
  193. const body = {
  194. materialType: 'VIDEO',
  195. record: {
  196. sourceTime: m.duration,
  197. clientType: state.platformType,
  198. feature: 'LESSON_TRAINING',
  199. deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
  200. },
  201. courseScheduleId: query.courseScheduleId,
  202. lessonTrainingId: query.lessonTrainingId,
  203. materialId: query.materialId
  204. }
  205. try {
  206. const res: any = await request.post(
  207. state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
  208. {
  209. data: body,
  210. hideLoading: true
  211. }
  212. )
  213. trainingRecord()
  214. } catch (error) {}
  215. data.recordLoading = false
  216. }
  217. // 停止所有的播放
  218. const handleStopVideo = () => {
  219. data.itemList.forEach((m: any) => {
  220. m.videoEle?.pause()
  221. })
  222. }
  223. // 判断练习是否完成
  224. const handleExerciseCompleted = () => {
  225. if (
  226. currentNum.value != 0 &&
  227. currentNum.value + '' === data.videoData?.trainingConfigJson?.practiceTimes
  228. ) {
  229. // handleStopVideo()
  230. const itemIndex = data.details.findIndex(
  231. (n: any) => n.materialId == data.videoData?.materialId
  232. )
  233. const isLastIndex = itemIndex === data.details.length - 1
  234. showConfirmDialog({
  235. title: '课后训练',
  236. message: '你已完成该练习~',
  237. confirmButtonColor: 'var(--van-primary)',
  238. confirmButtonText: isLastIndex ? '完成' : '下一题',
  239. cancelButtonText: '继续'
  240. })
  241. .then(() => {
  242. if (!isLastIndex) {
  243. const nextItem = data.details[itemIndex + 1]
  244. if (nextItem?.type === materialType.视频) {
  245. data.videoData = nextItem
  246. data.itemList = [
  247. {
  248. ...nextItem,
  249. id: nextItem?.materialId || '',
  250. currentTime: 0,
  251. duration: 100,
  252. paused: true,
  253. loop: false,
  254. videoEle: null,
  255. timer: null,
  256. muted: true, // 静音
  257. autoplay: true //自动播放
  258. }
  259. ]
  260. nextTick(() => {
  261. console.log(data.itemList[0].videoEle)
  262. })
  263. }
  264. if (nextItem?.type === materialType.曲目) {
  265. handleInit(1)
  266. goback()
  267. const parmas = qs.stringify({
  268. id: nextItem.content,
  269. courseScheduleId: query.courseScheduleId,
  270. lessonTrainingId: query.lessonTrainingId,
  271. materialId: nextItem.materialId
  272. })
  273. let src = `${location.origin}/orchestra-music-score/?` + parmas
  274. postMessage({
  275. api: 'openAccompanyWebView',
  276. content: {
  277. url: src,
  278. orientation: 0,
  279. isHideTitle: true,
  280. statusBarTextColor: false,
  281. isOpenLight: true
  282. }
  283. })
  284. }
  285. } else {
  286. postMessage({ api: 'goBack' })
  287. }
  288. })
  289. .catch(() => {
  290. data.details[itemIndex].currentTime = 0
  291. })
  292. }
  293. }
  294. return () => (
  295. <div class={styles.playContent}>
  296. <div class={styles.coursewarePlay} style={{ width: parentContainer.width }}>
  297. <Swipe
  298. style={{ height: '100%' }}
  299. ref={swipeRef}
  300. showIndicators={false}
  301. loop={false}
  302. vertical
  303. lazyRender={true}
  304. touchable={false}
  305. duration={0}
  306. >
  307. {data.itemList.map((m: any, mIndex: number) => {
  308. return (
  309. <SwipeItem>
  310. <>
  311. <div
  312. class={styles.itemDiv}
  313. onClick={() => {
  314. clearTimeout(m.timer)
  315. activeData.model = !activeData.model
  316. }}
  317. >
  318. <video
  319. playsinline="false"
  320. preload="auto"
  321. class="player"
  322. poster={iconVideobg}
  323. data-vid={m.id}
  324. src={m.content}
  325. loop={m.loop}
  326. autoplay={m.autoplay}
  327. muted={m.muted}
  328. onLoadedmetadata={async (e: Event) => {
  329. const videoEle = e.target as unknown as HTMLVideoElement
  330. m.duration = videoEle.duration
  331. m.videoEle = videoEle
  332. m.loaded = true
  333. console.time('播放')
  334. videoEle.play()
  335. }}
  336. onTimeupdate={(e: Event) => {
  337. if (!m.loaded) return
  338. const videoEle = e.target as unknown as HTMLVideoElement
  339. m.currentTime = videoEle.currentTime
  340. }}
  341. onPlay={() => {
  342. console.log('播放')
  343. // 播放
  344. m.paused = false
  345. if (m.muted) {
  346. m.muted = false
  347. m.videoEle.pause()
  348. }
  349. }}
  350. onPause={() => {
  351. console.log('暂停')
  352. //暂停
  353. m.paused = true
  354. }}
  355. onEnded={() => addTrainingRecord(m)}
  356. >
  357. <source src={m.content} type="video/mp4" />
  358. </video>
  359. </div>
  360. <Transition name="bottom">
  361. {activeData.model && !m.muted && (
  362. <div class={styles.bottomFixedContainer}>
  363. <div class={styles.time}>
  364. <span>{getSecondRPM(m.currentTime)}</span>
  365. <span>{getSecondRPM(m.duration)}</span>
  366. </div>
  367. <div class={styles.slider}>
  368. {m.duration && (
  369. <Slider
  370. buttonSize={16}
  371. modelValue={m.currentTime}
  372. min={0}
  373. max={m.duration}
  374. />
  375. )}
  376. </div>
  377. <div class={styles.actions}>
  378. <div class={styles.actionBtn}>
  379. {m.paused ? (
  380. <img
  381. src={iconplay}
  382. onClick={(e: Event) => {
  383. clearTimeout(m.timer)
  384. m.videoEle?.play()
  385. m.paused = false
  386. m.timer = setTimeout(() => {
  387. activeData.model = false
  388. }, 4000)
  389. }}
  390. />
  391. ) : (
  392. <img
  393. src={iconpause}
  394. onClick={(e: Event) => {
  395. clearTimeout(m.timer)
  396. m.videoEle?.pause()
  397. m.paused = true
  398. }}
  399. />
  400. )}
  401. </div>
  402. </div>
  403. </div>
  404. )}
  405. </Transition>
  406. {m.muted && (
  407. <div class={styles.loadWrap}>
  408. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  409. </div>
  410. )}
  411. </>
  412. </SwipeItem>
  413. )
  414. })}
  415. </Swipe>
  416. <Transition name="top">
  417. {activeData.model && (
  418. <div class={styles.headerContainer} ref={headeRef}>
  419. <div class={styles.backBtn} onClick={() => goback()}>
  420. <Icon name={iconBack} />
  421. 返回
  422. </div>
  423. <div class={styles.menu}>{popupData.tabName}</div>
  424. <div class={styles.nums}>
  425. 练习次数:{currentNum.value}/
  426. {data.videoData?.trainingConfigJson?.practiceTimes || 0}
  427. </div>
  428. </div>
  429. )}
  430. </Transition>
  431. </div>
  432. </div>
  433. )
  434. }
  435. })