index.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. import {
  2. closeToast,
  3. Icon,
  4. Loading,
  5. Popup,
  6. showToast,
  7. Slider,
  8. Swipe,
  9. SwipeInstance,
  10. SwipeItem
  11. } from 'vant'
  12. import {
  13. defineComponent,
  14. onMounted,
  15. reactive,
  16. nextTick,
  17. onUnmounted,
  18. ref,
  19. watch,
  20. Transition
  21. } from 'vue'
  22. import iconBack from './image/back.svg'
  23. import styles from './index.module.less'
  24. import 'plyr/dist/plyr.css'
  25. import request from '@/helpers/request'
  26. import { state } from '@/state'
  27. import { useRoute, useRouter } from 'vue-router'
  28. import { listenerMessage, postMessage, promisefiyPostMessage } from '@/helpers/native-message'
  29. import MusicScore from './component/musicScore'
  30. import iconMenu from './image/icon-menu.svg'
  31. import iconDian from './image/icon-dian.svg'
  32. import iconPoint from './image/icon-point.svg'
  33. import iconLoop from './image/icon-loop.svg'
  34. import iconLoopActive from './image/icon-loop-active.svg'
  35. import iconplay from './image/icon-play.svg'
  36. import iconpause from './image/icon-pause.svg'
  37. import iconUp from './image/icon-up.svg'
  38. import iconDown from './image/icon-down.svg'
  39. import Points from './component/points'
  40. import { browser, getSecondRPM } from '@/helpers/utils'
  41. import { Vue3Lottie } from 'vue3-lottie'
  42. import playLoadData from './datas/data.json'
  43. import { usePageVisibility } from '@vant/use'
  44. import PlayRecordTime from './playRecordTime'
  45. export default defineComponent({
  46. name: 'CoursewarePlay',
  47. setup() {
  48. const pageVisibility = usePageVisibility()
  49. const isPlay = ref(false)
  50. /** 页面显示和隐藏 */
  51. watch(pageVisibility, (value) => {
  52. const activeItem = data.itemList[popupData.activeIndex]
  53. if (activeItem.type != 'VIDEO') return
  54. if (value == 'hidden') {
  55. isPlay.value = !activeItem.paused
  56. handlePaused(activeItem)
  57. } else {
  58. // 页面显示,并且
  59. if (isPlay.value) handlePlay(activeItem)
  60. }
  61. })
  62. /** 设置播放容器 16:9 */
  63. const parentContainer = reactive({
  64. width: '100vw'
  65. })
  66. const setContainer = () => {
  67. let min = Math.min(screen.width, screen.height)
  68. let max = Math.max(screen.width, screen.height)
  69. let width = min * (16 / 9)
  70. if (width > max) {
  71. parentContainer.width = '100vw'
  72. return
  73. } else {
  74. parentContainer.width = width + 'px'
  75. }
  76. }
  77. const handleInit = (type = 0) => {
  78. // postMessage({
  79. // api: 'courseLoading',
  80. // content: {
  81. // show: true,
  82. // type: 'fullscreen'
  83. // }
  84. // })
  85. //设置容器16:9
  86. setContainer()
  87. // 横屏
  88. postMessage({
  89. api: 'setRequestedOrientation',
  90. content: {
  91. orientation: type
  92. }
  93. })
  94. // 头,包括返回箭头
  95. postMessage({
  96. api: 'setTitleBarVisibility',
  97. content: {
  98. status: type
  99. }
  100. })
  101. // 安卓的状态栏
  102. postMessage({
  103. api: 'setStatusBarVisibility',
  104. content: {
  105. isVisibility: type
  106. }
  107. })
  108. }
  109. handleInit()
  110. onUnmounted(() => {
  111. handleInit(1)
  112. window.removeEventListener('message', iframeHandle)
  113. })
  114. const route = useRoute()
  115. const router = useRouter()
  116. const headeRef = ref()
  117. const data = reactive({
  118. detail: null,
  119. knowledgePointList: [] as any,
  120. itemList: [] as any,
  121. showHead: true,
  122. isCourse: false
  123. })
  124. const activeData = reactive({
  125. nowTime: 0,
  126. model: true, // 遮罩
  127. videoBtns: true, // 视频
  128. currentTime: 0,
  129. duration: 0,
  130. timer: null as any,
  131. item: null as any
  132. })
  133. // 获取缓存路径
  134. const getCacheFilePath = async (material: any) => {
  135. const res = await promisefiyPostMessage({
  136. api: 'getCourseFilePath',
  137. content: {
  138. url: material.content,
  139. localPath: '',
  140. materialId: material.materialId,
  141. updateTime: material.updateTime,
  142. type: material.type // SONG VIDEO IMAGE
  143. }
  144. })
  145. // console.log('缓存路径返回', res)
  146. return res
  147. }
  148. // 获取当前课程是否签退
  149. const getCourseSchedule = async () => {
  150. if (!route.query.courseId) return
  151. try {
  152. const res = await request.get(
  153. `${state.platformApi}/courseSchedule/detail/${route.query.courseId}`,
  154. {
  155. hideLoading: true
  156. }
  157. )
  158. if (res?.data) {
  159. data.isCourse =
  160. res.data.status === 'ING' && state.platformType == 'TEACHER' ? true : false
  161. }
  162. } catch (e) {
  163. console.log(e)
  164. }
  165. }
  166. const getItemList = async () => {
  167. const list: any = []
  168. const browserInfo = browser()
  169. for (let i = 0; i < data.knowledgePointList.length; i++) {
  170. const item = data.knowledgePointList[i]
  171. const itemLength = item.materialList.length - 1
  172. for (let j = 0; j < item.materialList.length; j++) {
  173. const material = item.materialList[j]
  174. //请求本地缓存
  175. if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.type)) {
  176. const localData = await getCacheFilePath(material)
  177. if (localData?.content?.localPath) {
  178. material.url = material.content
  179. material.content = localData.content.localPath
  180. // console.log("🚀 ~ material", material)
  181. }
  182. }
  183. let videoItem = {}
  184. if (material.type === 'VIDEO') {
  185. videoItem = {
  186. currentTime: 0,
  187. duration: 0,
  188. progress: 0,
  189. paused: true,
  190. loop: false,
  191. videoEle: null,
  192. timer: null,
  193. playModel: false,
  194. isprepare: false,
  195. isDrage: false,
  196. muted: true // 是否静音
  197. }
  198. }
  199. list.push({
  200. ...material,
  201. ...videoItem,
  202. iframeRef: null,
  203. tabName: item.name,
  204. isLast: j === itemLength, // 当前知识点
  205. autoPlay: false, //加载完成是否自动播放
  206. display: false
  207. })
  208. }
  209. }
  210. let item: any = null
  211. if (route.query.kId) {
  212. item = list.find((n: any) => n.materialId == route.query.kId)
  213. const _firstIndex = list.findIndex((n: any) => n.materialId == route.query.kId)
  214. popupData.firstIndex = _firstIndex > -1 ? _firstIndex : 0
  215. }
  216. item = item ? item : list[0] || {}
  217. if (item) {
  218. popupData.tabName = item.tabName
  219. popupData.tabActive = item.knowledgePointId
  220. popupData.itemActive = item.id
  221. popupData.itemName = item.name
  222. popupData.activeIndex = popupData.firstIndex
  223. item.autoPlay = true
  224. item.muted = true
  225. item.display = true
  226. }
  227. // console.log('🚀 ~ list', list)
  228. data.itemList = list
  229. // setTimeout(() => {
  230. // postMessage({
  231. // api: 'courseLoading',
  232. // content: {
  233. // show: false,
  234. // type: 'fullscreen'
  235. // }
  236. // })
  237. // }, 300)
  238. }
  239. const getDetail = async () => {
  240. try {
  241. const res: any = await request.get(
  242. state.platformApi + `/lessonCoursewareDetail/detail/${route.query.id}`,
  243. {
  244. hideLoading: true
  245. }
  246. )
  247. if (Array.isArray(res?.data)) {
  248. data.detail = res.data
  249. }
  250. if (Array.isArray(res?.data?.knowledgePointList)) {
  251. let index = 0
  252. data.knowledgePointList = res.data.knowledgePointList.map((n: any) => {
  253. if (Array.isArray(n.materialList)) {
  254. n.materialList = n.materialList.map((item: any) => {
  255. index++
  256. return {
  257. ...item,
  258. materialId: item.id,
  259. id: index + ''
  260. }
  261. })
  262. }
  263. return n
  264. })
  265. getItemList()
  266. }
  267. } catch (error) {}
  268. }
  269. // ifram事件处理
  270. const iframeHandle = (ev: MessageEvent) => {
  271. if (ev.data?.api === 'headerTogge') {
  272. // console.log("🚀 ~ ev.data", ev.data)
  273. activeData.model = ev.data.show || (ev.data.playState == 'play' ? true : false)
  274. }
  275. }
  276. onMounted(() => {
  277. getDetail()
  278. getCourseSchedule()
  279. window.addEventListener('message', iframeHandle)
  280. })
  281. // 返回
  282. const goback = () => {
  283. if (route.query.source == 'my-course') {
  284. router.back()
  285. }
  286. postMessage({ api: 'goBack' })
  287. }
  288. const swipeRef = ref<SwipeInstance>()
  289. const popupData = reactive({
  290. firstIndex: 0,
  291. open: false,
  292. activeIndex: 0,
  293. tabActive: '',
  294. tabName: '',
  295. itemActive: '',
  296. itemName: ''
  297. })
  298. /**停止所有的播放 */
  299. const handleStop = () => {
  300. const activeItem = data.itemList[popupData.activeIndex]
  301. for (let i = 0; i < data.itemList.length; i++) {
  302. const item = data.itemList[i]
  303. // 停止视频播放
  304. if (item.type === 'VIDEO') {
  305. // console.log("🚀 ~ item", item)
  306. if (item?.id != activeItem.id) {
  307. item.currentTime = 0
  308. item.progress = 0
  309. if (item.videoEle) {
  310. item.videoEle.currentTime = 0
  311. item.videoEle.pause()
  312. }
  313. }
  314. }
  315. // 停止曲谱的播放
  316. if (item.type === 'SONG') {
  317. item.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
  318. item.display = false
  319. }
  320. }
  321. }
  322. // 切换素材
  323. const toggleMaterial = () => {
  324. const index = data.itemList.findIndex((n: any) => n.id == popupData.itemActive)
  325. if (index > -1) {
  326. swipeRef.value?.swipeTo(index, {
  327. immediate: true
  328. })
  329. }
  330. }
  331. /** 延迟收起模态框 */
  332. const setModelOpen = () => {
  333. clearTimeout(activeData.timer)
  334. closeToast()
  335. activeData.timer = setTimeout(() => {
  336. activeData.model = false
  337. }, 4000)
  338. }
  339. // 轮播切换
  340. const handleSwipeChange = (val: any) => {
  341. console.log('轮播切换')
  342. popupData.activeIndex = val
  343. const item = data.itemList[val]
  344. handleStop()
  345. if (item) {
  346. popupData.tabActive = item.knowledgePointId
  347. popupData.itemActive = item.id
  348. popupData.itemName = item.name
  349. popupData.tabName = item.tabName
  350. if (item.type == 'SONG') {
  351. activeData.model = true
  352. item.display = true
  353. }
  354. if (item.type === 'VIDEO') {
  355. // console.log("🚀 ~ item", item)
  356. // 自动播放下一个视频
  357. clearTimeout(activeData.timer)
  358. closeToast()
  359. item.currentTime = 0
  360. item.videoEle && (item.videoEle.currentTime = 0)
  361. nextTick(() => {
  362. item.autoPlay = true
  363. item.videoEle?.play()
  364. })
  365. }
  366. }
  367. }
  368. // 上一个知识点, 下一个知识点
  369. const handlePreAndNext = (type: string) => {
  370. if (type === 'up') {
  371. swipeRef.value?.prev()
  372. } else {
  373. swipeRef.value?.next()
  374. }
  375. }
  376. // 去点名,签退
  377. const gotoRollCall = (pageTag: string) => {
  378. postMessage({
  379. api: 'open_app_page',
  380. content: {
  381. action: 'app',
  382. pageTag: pageTag,
  383. url: '',
  384. params: JSON.stringify({ courseId: route.query.courseId })
  385. }
  386. })
  387. }
  388. // 双击
  389. const handleDbClick = (item: any) => {
  390. // console.log(item)
  391. if (item && item.type === 'VIDEO') {
  392. const videoEle: HTMLVideoElement = item.videoEle
  393. if (videoEle) {
  394. if (videoEle.paused) {
  395. closeToast()
  396. videoEle.play()
  397. } else {
  398. showToast('已暂停')
  399. videoEle.pause()
  400. }
  401. }
  402. }
  403. }
  404. // 暂停播放
  405. const handlePaused = (m: any) => {
  406. m.videoEle?.pause()
  407. m.paused = true
  408. }
  409. // 开始播放
  410. const handlePlay = (m: any) => {
  411. closeToast()
  412. m.videoEle?.play()
  413. }
  414. // 调整播放进度
  415. const handleChangeSlider = (m: any) => {
  416. if (m?.videoEle) {
  417. // console.log('进度条', m.progress)
  418. m.currentTime = m.duration * (m.progress / 100)
  419. m.videoEle.currentTime = m.currentTime
  420. }
  421. }
  422. //当前视频播放完
  423. const handleEnded = (m: any) => {
  424. // console.log(m)
  425. if (popupData.activeIndex != data.itemList.length - 1) {
  426. swipeRef.value?.next()
  427. }
  428. }
  429. return () => (
  430. <div class={styles.playContent}>
  431. <div class={styles.coursewarePlay} style={{ width: parentContainer.width }}>
  432. <Swipe
  433. style={{ height: '100%' }}
  434. ref={swipeRef}
  435. showIndicators={false}
  436. loop={false}
  437. duration={0}
  438. vertical
  439. lazyRender={true}
  440. touchable={false}
  441. initialSwipe={popupData.firstIndex}
  442. onChange={handleSwipeChange}
  443. >
  444. {data.itemList.map((m: any, mIndex: number) => {
  445. return (
  446. <SwipeItem class={styles.swipeItem}>
  447. <>
  448. <div
  449. class={styles.itemDiv}
  450. onClick={() => {
  451. clearTimeout(activeData.timer)
  452. if (Date.now() - activeData.nowTime < 300) {
  453. handleDbClick(m)
  454. return
  455. }
  456. activeData.nowTime = Date.now()
  457. activeData.timer = setTimeout(() => {
  458. activeData.model = !activeData.model
  459. setModelOpen()
  460. }, 300)
  461. }}
  462. >
  463. {m.type === 'VIDEO' ? (
  464. <>
  465. <video
  466. playsinline="false"
  467. muted={m.muted}
  468. preload="auto"
  469. class="player"
  470. data-vid={m.id}
  471. src={m.content}
  472. loop={m.loop}
  473. autoplay={m.autoPlay}
  474. onLoadedmetadata={(e: Event) => {
  475. const videoEle = e.target as unknown as HTMLVideoElement
  476. m.currentTime = videoEle.currentTime
  477. m.duration = videoEle.duration
  478. m.videoEle = videoEle
  479. m.isprepare = true
  480. }}
  481. onTimeupdate={(e: Event) => {
  482. if (!m.isprepare) return
  483. const videoEle = e.target as unknown as HTMLVideoElement
  484. m.currentTime = videoEle.currentTime
  485. m.progress = Number((videoEle.currentTime / m.duration) * 100)
  486. }}
  487. onPlay={() => {
  488. // 播放
  489. m.paused = false
  490. console.log('播放')
  491. setModelOpen()
  492. // 第一次播放
  493. if (m.muted) {
  494. m.muted = false
  495. m.autoPlay = false
  496. }
  497. }}
  498. onPause={() => {
  499. //暂停
  500. clearTimeout(activeData.timer)
  501. m.paused = true
  502. }}
  503. onEnded={() => handleEnded(m)}
  504. >
  505. <source src={m.content} type="video/mp4" />
  506. </video>
  507. {m.muted && (
  508. <div class={styles.loadWrap}>
  509. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  510. </div>
  511. )}
  512. <div
  513. style={{ transform: activeData.model ? '' : 'translateY(100%)' }}
  514. class={styles.bottomFixedContainer}
  515. onClick={(e: Event) => {
  516. e.stopPropagation()
  517. setModelOpen()
  518. }}
  519. >
  520. <div style={{ opacity: m.isprepare ? '1' : '0' }}>
  521. <div class={styles.time}>
  522. <span>{getSecondRPM(m.currentTime)}</span>
  523. <span>{getSecondRPM(m.duration)}</span>
  524. </div>
  525. <div class={styles.slider}>
  526. <Slider
  527. onClick={() => setModelOpen()}
  528. buttonSize={16}
  529. step={1}
  530. modelValue={m.progress}
  531. onUpdate:modelValue={(val: any) => {
  532. console.log('val', val)
  533. m.progress = val
  534. handleChangeSlider(m)
  535. }}
  536. onDragStart={(e: Event) => {
  537. // 开始拖动,暂停播放
  538. console.log('开始拖动')
  539. // 如果拖动之前,视频是播放状态,拖动完毕后继续播放
  540. if (!m.paused) {
  541. m.isDrage = true
  542. }
  543. handlePaused(m)
  544. }}
  545. onDragEnd={(e: Event) => {
  546. console.log('结束拖动')
  547. if (m.isDrage) {
  548. m.isDrage = false
  549. handlePlay(m)
  550. }
  551. }}
  552. min={0}
  553. max={100}
  554. />
  555. </div>
  556. </div>
  557. <div class={styles.actions}>
  558. <div class={styles.actionBtn}>
  559. {m.isprepare ? (
  560. <>
  561. {m.paused ? (
  562. <img src={iconplay} onClick={(e: Event) => handlePlay(m)} />
  563. ) : (
  564. <img
  565. src={iconpause}
  566. onClick={(e: Event) => handlePaused(m)}
  567. />
  568. )}
  569. </>
  570. ) : (
  571. <Loading color="#fff" />
  572. )}
  573. {m.loop ? (
  574. <img
  575. src={iconLoopActive}
  576. onClick={(e: Event) => (m.loop = false)}
  577. />
  578. ) : (
  579. <img src={iconLoop} onClick={(e: Event) => (m.loop = true)} />
  580. )}
  581. </div>
  582. <div>{m.name}</div>
  583. </div>
  584. </div>
  585. </>
  586. ) : m.type === 'IMG' ? (
  587. <img src={m.content} />
  588. ) : (
  589. <MusicScore
  590. data-vid={m.id}
  591. music={m}
  592. onSetIframe={(el: any) => {
  593. m.iframeRef = el
  594. }}
  595. />
  596. )}
  597. </div>
  598. </>
  599. </SwipeItem>
  600. )
  601. })}
  602. </Swipe>
  603. <div
  604. style={{ transform: activeData.model ? '' : 'translateY(-100%)' }}
  605. id="coursePlayHeader"
  606. class={styles.headerContainer}
  607. ref={headeRef}
  608. >
  609. <div class={styles.backBtn} onClick={() => goback()}>
  610. <Icon name={iconBack} />
  611. 返回
  612. </div>
  613. <div class={styles.menu}>{popupData.tabName}</div>
  614. {data.isCourse && <PlayRecordTime list={data.itemList} />}
  615. </div>
  616. <Transition name="right">
  617. {activeData.model && (
  618. <div class={styles.rightFixedBtns}>
  619. <div
  620. class={styles.fullBtn}
  621. onClick={() => {
  622. clearTimeout(activeData.timer)
  623. popupData.open = true
  624. }}
  625. >
  626. <img src={iconMenu} />
  627. <span>知识点</span>
  628. </div>
  629. {data.isCourse && (
  630. <>
  631. <div
  632. class={[styles.fullBtn, styles.point]}
  633. onClick={() => gotoRollCall('student_roll_call')}
  634. >
  635. <img src={iconDian} />
  636. <span>点名</span>
  637. </div>
  638. <div class={styles.fullBtn} onClick={() => gotoRollCall('sign_out')}>
  639. <img src={iconPoint} />
  640. <span>签退</span>
  641. </div>
  642. </>
  643. )}
  644. </div>
  645. )}
  646. </Transition>
  647. <Transition name="left">
  648. {activeData.model && (
  649. <div class={styles.leftFixedBtns}>
  650. {popupData.activeIndex != 0 && (
  651. <div
  652. class={[styles.fullBtn, styles.prePoint]}
  653. onClick={() => handlePreAndNext('up')}
  654. >
  655. <img src={iconUp} />
  656. <span style={{ textAlign: 'center' }}>上一个</span>
  657. </div>
  658. )}
  659. {popupData.activeIndex != data.itemList.length - 1 && (
  660. <div class={styles.fullBtn} onClick={() => handlePreAndNext('down')}>
  661. <span style={{ textAlign: 'center' }}>下一个</span>
  662. <img src={iconDown} />
  663. </div>
  664. )}
  665. </div>
  666. )}
  667. </Transition>
  668. <Popup
  669. class={styles.popup}
  670. overlayClass={styles.overlayClass}
  671. position="right"
  672. round
  673. v-model:show={popupData.open}
  674. onClose={() => {
  675. const item = data.itemList[popupData.activeIndex]
  676. if (item?.type == 'VIDEO') {
  677. setModelOpen()
  678. }
  679. }}
  680. >
  681. <Points
  682. data={data.knowledgePointList}
  683. tabActive={popupData.tabActive}
  684. itemActive={popupData.itemActive}
  685. onHandleSelect={(res: any) => {
  686. // console.log(res)
  687. popupData.tabActive = res.tabActive
  688. popupData.itemActive = res.itemActive
  689. popupData.tabName = res.tabName
  690. popupData.open = false
  691. toggleMaterial()
  692. }}
  693. />
  694. </Popup>
  695. </div>
  696. </div>
  697. )
  698. }
  699. })