index.tsx 14 KB

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