index.tsx 17 KB

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