index.tsx 15 KB

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