index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  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. trainings: [] as any[],
  88. trainingTimes: 0,
  89. itemList: [] as any,
  90. showHead: true,
  91. loading: true,
  92. recordLoading: false
  93. })
  94. const activeData = reactive({
  95. nowTime: 0,
  96. model: true, // 遮罩
  97. timer: null as any,
  98. item: null as any
  99. })
  100. // 获取课后练习记录
  101. const getTrainingRecord = async () => {
  102. try {
  103. const res: any = await request.post(
  104. state.platformApi +
  105. `/studentLessonTraining/trainingRecord/${query.courseScheduleId}?userId=${state.user?.data?.id}`,
  106. {
  107. hideLoading: true
  108. }
  109. )
  110. if (Array.isArray(res?.data?.trainings)) {
  111. return res.data.trainings
  112. }
  113. } catch (error) {}
  114. return []
  115. }
  116. const setRecord = async (trainings: any[]) => {
  117. if (Array.isArray(trainings)) {
  118. data.trainings = trainings.map((n: any) => {
  119. try {
  120. n.trainingContent = JSON.parse(n.trainingContent)
  121. } catch (error) {
  122. n.trainingContent = ''
  123. }
  124. return {
  125. ...n,
  126. currentTime: 0,
  127. duration: 100,
  128. paused: true,
  129. loop: false,
  130. videoEle: null,
  131. timer: null,
  132. muted: true, // 静音
  133. autoplay: true //自动播放
  134. }
  135. })
  136. data.itemList = data.trainings.filter((n: any) => n.materialId == route.query.materialId)
  137. data.videoData = data.itemList[0]
  138. handleExerciseCompleted()
  139. }
  140. }
  141. onMounted(async () => {
  142. const trainings = await getTrainingRecord()
  143. setRecord(trainings)
  144. })
  145. // 返回
  146. const goback = () => {
  147. postMessage({ api: 'back' })
  148. }
  149. const swipeRef = ref()
  150. const popupData = reactive({
  151. firstIndex: 0,
  152. open: false,
  153. activeIndex: -1,
  154. tabActive: '',
  155. tabName: '',
  156. itemActive: '',
  157. itemName: ''
  158. })
  159. // 达到指标,记录
  160. const addTrainingRecord = async (m: any) => {
  161. if (data.recordLoading) return
  162. console.log('记录观看次数')
  163. data.recordLoading = true
  164. const query = route.query
  165. const body = {
  166. materialType: 'VIDEO',
  167. record: {
  168. sourceTime: m.duration,
  169. clientType: state.platformType,
  170. feature: 'LESSON_TRAINING',
  171. deviceType: browserInfo.android ? 'ANDROID' : browserInfo.isApp ? 'IOS' : 'WEB'
  172. },
  173. courseScheduleId: query.courseScheduleId,
  174. lessonTrainingId: query.lessonTrainingId,
  175. materialId: data.videoData?.materialId || ''
  176. }
  177. try {
  178. const res: any = await request.post(
  179. state.platformApi + '/studentLessonTraining/lessonTrainingRecord',
  180. {
  181. data: body,
  182. hideLoading: true
  183. }
  184. )
  185. } catch (error) {}
  186. data.recordLoading = false
  187. try {
  188. const trainings: any[] = await getTrainingRecord()
  189. if (Array.isArray(trainings)) {
  190. const item = trainings.find((n: any) => n.materialId == data.videoData?.materialId)
  191. if (item) {
  192. data.videoData.trainingTimes = item.trainingTimes
  193. handleExerciseCompleted()
  194. }
  195. }
  196. } catch (error) {}
  197. }
  198. // 停止所有的播放
  199. const handleStopVideo = () => {
  200. data.itemList.forEach((m: any) => {
  201. m.videoEle?.pause()
  202. })
  203. }
  204. // 判断练习是否完成
  205. const handleExerciseCompleted = () => {
  206. if (
  207. data?.videoData.trainingTimes != 0 &&
  208. data?.videoData.trainingTimes + '' === data.videoData?.trainingContent?.practiceTimes
  209. ) {
  210. // handleStopVideo()
  211. const itemIndex = data.trainings.findIndex(
  212. (n: any) => n.materialId == data.videoData?.materialId
  213. )
  214. // console.log(itemIndex ,data.trainings, data.videoData?.materialId)
  215. const isLastIndex = itemIndex === data.trainings.length - 1
  216. showConfirmDialog({
  217. title: '课后作业',
  218. message: '你已完成该练习~',
  219. confirmButtonColor: 'var(--van-primary)',
  220. confirmButtonText: isLastIndex ? '完成' : '下一题',
  221. cancelButtonText: '继续'
  222. })
  223. .then(() => {
  224. if (!isLastIndex) {
  225. const nextItem = data.trainings[itemIndex + 1]
  226. if (nextItem?.type === materialType.视频) {
  227. data.itemList = [nextItem]
  228. data.videoData = nextItem
  229. handleExerciseCompleted()
  230. }
  231. if (nextItem?.type === materialType.曲目) {
  232. handleInit(1)
  233. goback()
  234. const parmas = qs.stringify({
  235. id: nextItem.content,
  236. courseScheduleId: query.courseScheduleId,
  237. lessonTrainingId: query.lessonTrainingId,
  238. materialId: nextItem.materialId
  239. })
  240. let src = `${location.origin}/orchestra-music-score/?` + parmas
  241. postMessage({
  242. api: 'openAccompanyWebView',
  243. content: {
  244. url: src,
  245. orientation: 0,
  246. isHideTitle: true,
  247. statusBarTextColor: false,
  248. isOpenLight: true
  249. }
  250. })
  251. }
  252. } else {
  253. postMessage({ api: 'goBack' })
  254. }
  255. })
  256. .catch(() => {
  257. data.trainings[itemIndex].currentTime = 0
  258. })
  259. }
  260. }
  261. return () => (
  262. <div class={styles.playContent}>
  263. <div class={styles.coursewarePlay} style={{ width: parentContainer.width }}>
  264. <Swipe
  265. style={{ height: '100%' }}
  266. ref={swipeRef}
  267. showIndicators={false}
  268. loop={false}
  269. vertical
  270. lazyRender={true}
  271. touchable={false}
  272. duration={0}
  273. >
  274. {data.itemList.map((m: any, mIndex: number) => {
  275. return (
  276. <SwipeItem>
  277. <>
  278. <div
  279. class={styles.itemDiv}
  280. onClick={() => {
  281. clearTimeout(m.timer)
  282. activeData.model = !activeData.model
  283. }}
  284. >
  285. <video
  286. playsinline="false"
  287. preload="auto"
  288. class="player"
  289. poster={iconVideobg}
  290. data-vid={m.id}
  291. src={m.content}
  292. loop={m.loop}
  293. autoplay={m.autoplay}
  294. muted={m.muted}
  295. onLoadedmetadata={async (e: Event) => {
  296. const videoEle = e.target as unknown as HTMLVideoElement
  297. m.duration = videoEle.duration
  298. m.videoEle = videoEle
  299. m.loaded = true
  300. }}
  301. onTimeupdate={(e: Event) => {
  302. if (!m.loaded) return
  303. const videoEle = e.target as unknown as HTMLVideoElement
  304. m.currentTime = videoEle.currentTime
  305. }}
  306. onPlay={() => {
  307. console.log('播放')
  308. // 播放
  309. m.paused = false
  310. if (m.muted) {
  311. m.muted = false
  312. m.videoEle.pause()
  313. }
  314. }}
  315. onPause={() => {
  316. console.log('暂停')
  317. //暂停
  318. m.paused = true
  319. }}
  320. onEnded={() => addTrainingRecord(m)}
  321. >
  322. <source src={m.content} type="video/mp4" />
  323. </video>
  324. </div>
  325. <Transition name="bottom">
  326. {activeData.model && !m.muted && (
  327. <div class={styles.bottomFixedContainer}>
  328. <div class={styles.time}>
  329. <span>{getSecondRPM(m.currentTime)}</span>
  330. <span>{getSecondRPM(m.duration)}</span>
  331. </div>
  332. <div class={styles.slider}>
  333. {m.duration && (
  334. <Slider
  335. buttonSize={16}
  336. modelValue={m.currentTime}
  337. min={0}
  338. max={m.duration}
  339. />
  340. )}
  341. </div>
  342. <div class={styles.actions}>
  343. <div class={styles.actionBtn}>
  344. {m.paused ? (
  345. <img
  346. src={iconplay}
  347. onClick={(e: Event) => {
  348. clearTimeout(m.timer)
  349. m.videoEle?.play()
  350. m.paused = false
  351. m.timer = setTimeout(() => {
  352. activeData.model = false
  353. }, 4000)
  354. }}
  355. />
  356. ) : (
  357. <img
  358. src={iconpause}
  359. onClick={(e: Event) => {
  360. clearTimeout(m.timer)
  361. m.videoEle?.pause()
  362. m.paused = true
  363. }}
  364. />
  365. )}
  366. </div>
  367. </div>
  368. </div>
  369. )}
  370. </Transition>
  371. {m.muted && (
  372. <div class={styles.loadWrap}>
  373. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  374. </div>
  375. )}
  376. </>
  377. </SwipeItem>
  378. )
  379. })}
  380. </Swipe>
  381. <Transition name="top">
  382. {activeData.model && (
  383. <div class={styles.headerContainer} ref={headeRef}>
  384. <div class={styles.backBtn} onClick={() => goback()}>
  385. <Icon name={iconBack} />
  386. 返回
  387. </div>
  388. <div class={styles.menu}>{popupData.tabName}</div>
  389. <div class={styles.nums}>
  390. 练习次数:{data.videoData?.trainingTimes || 0}/
  391. {data.videoData?.trainingContent?.practiceTimes || 0}
  392. </div>
  393. </div>
  394. )}
  395. </Transition>
  396. </div>
  397. </div>
  398. )
  399. }
  400. })