index.tsx 14 KB

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