index.tsx 14 KB

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