index.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645
  1. import { closeToast, Icon, Popup, showDialog, showToast } from 'vant';
  2. import {
  3. defineComponent,
  4. onMounted,
  5. reactive,
  6. nextTick,
  7. onUnmounted,
  8. ref,
  9. watch,
  10. Transition
  11. } from 'vue';
  12. import iconBack from './image/back.svg';
  13. import styles from './index.module.less';
  14. import 'plyr/dist/plyr.css';
  15. import { useRoute, useRouter } from 'vue-router';
  16. import {
  17. listenerMessage,
  18. postMessage,
  19. promisefiyPostMessage
  20. } from '@/helpers/native-message';
  21. import MusicScore from './component/musicScore';
  22. import iconMenu from './image/icon-menu.svg';
  23. import iconDian from './image/icon-dian.svg';
  24. import iconPoint from './image/icon-point.svg';
  25. import iconUp from './image/icon-up.svg';
  26. import iconDown from './image/icon-down.svg';
  27. import Points from './component/points';
  28. import { browser, getSecondRPM } from '@/helpers/utils';
  29. import { Vue3Lottie } from 'vue3-lottie';
  30. import playLoadData from './datas/data.json';
  31. import { usePageVisibility, useRect } from '@vant/use';
  32. import VideoPlay from './component/video-play';
  33. import Tool, { ToolItem, ToolType } from './component/tool';
  34. export default defineComponent({
  35. name: 'CoursewarePlay',
  36. setup() {
  37. const pageVisibility = usePageVisibility();
  38. const isPlay = ref(false);
  39. /** 页面显示和隐藏 */
  40. watch(pageVisibility, value => {
  41. const activeItem = data.itemList[popupData.activeIndex];
  42. if (activeItem.type != 'VIDEO') return;
  43. if (value == 'hidden') {
  44. isPlay.value = !activeItem.videoEle?.paused;
  45. togglePlay(activeItem, false);
  46. } else {
  47. // 页面显示,并且
  48. if (isPlay.value) togglePlay(activeItem, true);
  49. }
  50. });
  51. /** 设置播放容器 16:9 */
  52. const parentContainer = reactive({
  53. width: '100vw'
  54. });
  55. const setContainer = () => {
  56. let min = Math.min(screen.width, screen.height);
  57. let max = Math.max(screen.width, screen.height);
  58. let width = min * (16 / 9);
  59. if (width > max) {
  60. parentContainer.width = '100vw';
  61. return;
  62. } else {
  63. parentContainer.width = width + 'px';
  64. }
  65. };
  66. const handleInit = (type = 0) => {
  67. //设置容器16:9
  68. setContainer();
  69. // 横屏
  70. postMessage(
  71. {
  72. api: 'setRequestedOrientation',
  73. content: {
  74. orientation: type
  75. }
  76. },
  77. () => {
  78. console.log(234);
  79. }
  80. );
  81. // 头,包括返回箭头
  82. // postMessage({
  83. // api: 'setTitleBarVisibility',
  84. // content: {
  85. // status: type
  86. // }
  87. // })
  88. // 安卓的状态栏
  89. postMessage({
  90. api: 'setStatusBarVisibility',
  91. content: {
  92. isVisibility: type
  93. }
  94. });
  95. // 进入页面设置常量
  96. postMessage({
  97. api: 'keepScreenLongLight',
  98. content: {
  99. isOpenLight: type ? true : false
  100. }
  101. });
  102. };
  103. handleInit();
  104. onUnmounted(() => {
  105. handleInit(1);
  106. window.removeEventListener('message', iframeHandle);
  107. });
  108. const route = useRoute();
  109. const router = useRouter();
  110. const headeRef = ref();
  111. const data = reactive({
  112. detail: null,
  113. knowledgePointList: [] as any,
  114. itemList: [] as any,
  115. showHead: true,
  116. isCourse: false,
  117. isRecordPlay: false,
  118. videoRefs: {} as any[]
  119. });
  120. const activeData = reactive({
  121. isAutoPlay: true, // 是否自动播放
  122. nowTime: 0,
  123. model: true, // 遮罩
  124. isAnimation: true, // 是否动画
  125. videoBtns: true, // 视频
  126. currentTime: 0,
  127. duration: 0,
  128. timer: null as any,
  129. item: null as any
  130. });
  131. const getTempList = async (materialList: any, name: any) => {
  132. const list: any = [];
  133. const browserInfo = browser();
  134. for (let j = 0; j < materialList.length; j++) {
  135. const material = materialList[j];
  136. list.push({
  137. ...material,
  138. iframeRef: null,
  139. videoEle: null,
  140. tabName: name,
  141. autoPlay: false, //加载完成是否自动播放
  142. isprepare: false, // 视频是否加载完成
  143. isRender: false // 是否渲染了
  144. });
  145. }
  146. return list;
  147. };
  148. const getDetail = async () => {
  149. data.knowledgePointList = [
  150. {
  151. id: '1',
  152. name: '歌曲表演 大鹿',
  153. title: '歌曲表演 大鹿',
  154. type: 'VIDEO',
  155. content:
  156. 'https://gyt.ks3-cn-beijing.ksyuncs.com/courseware/1687838624636.mp4',
  157. url: 'https://daya.ks3-cn-beijing.ksyun.com/202306/TiLloDA.jpg'
  158. },
  159. {
  160. id: '2',
  161. name: '知识 音的高低',
  162. title: '知识 音的高低',
  163. type: 'IMG',
  164. content: 'https://daya.ks3-cn-beijing.ksyun.com/202306/TiLlteU.png',
  165. url: 'https://daya.ks3-cn-beijing.ksyun.com/202306/TiLlteU.png'
  166. },
  167. {
  168. id: '3',
  169. name: '欣赏 永远在童话里',
  170. title: '欣赏 永远在童话里',
  171. type: 'IMG',
  172. content: 'https://daya.ks3-cn-beijing.ksyun.com/202306/TiLlxJ0.png',
  173. url: 'https://daya.ks3-cn-beijing.ksyun.com/202306/TiLlxJ0.png'
  174. },
  175. {
  176. id: '4',
  177. name: '彩虹岛',
  178. title: '彩虹岛',
  179. type: 'SONG',
  180. content: '22078',
  181. url: 'https://cloud-coach.ks3-cn-beijing.ksyuncs.com/music-sheet-fixed/1675770786664-1.png'
  182. }
  183. ];
  184. popupData.itemActive = data.knowledgePointList[0].id;
  185. data.itemList = data.knowledgePointList.map((m: any, index: number) => {
  186. return {
  187. ...m,
  188. iframeRef: null,
  189. videoEle: null,
  190. autoPlay: index === 0, //加载完成是否自动播放
  191. isprepare: false, // 视频是否加载完成
  192. isRender: false // 是否渲染了
  193. };
  194. });
  195. };
  196. // ifram事件处理
  197. const iframeHandle = (ev: MessageEvent) => {
  198. if (ev.data?.api === 'headerTogge') {
  199. activeData.model =
  200. ev.data.show || (ev.data.playState == 'play' ? false : true);
  201. }
  202. };
  203. onMounted(() => {
  204. postMessage({
  205. api: 'courseLoading',
  206. content: {
  207. show: false,
  208. type: 'fullscreen'
  209. }
  210. });
  211. getDetail();
  212. window.addEventListener('message', iframeHandle);
  213. });
  214. const playRef = ref();
  215. // 返回
  216. const goback = () => {
  217. try {
  218. playRef.value?.handleOut();
  219. } catch (error) {}
  220. postMessage({ api: 'goBack' });
  221. };
  222. const popupData = reactive({
  223. open: false,
  224. activeIndex: 0,
  225. tabActive: '',
  226. tabName: '',
  227. itemActive: '',
  228. itemName: '',
  229. guideOpen: false,
  230. toolOpen: false // 工具弹窗控制
  231. });
  232. /**停止所有的播放 */
  233. const handleStop = () => {
  234. for (let i = 0; i < data.itemList.length; i++) {
  235. const activeItem = data.itemList[i];
  236. if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
  237. activeItem.videoEle.stop();
  238. }
  239. // console.log('🚀 ~ activeItem:', activeItem)
  240. // 停止曲谱的播放
  241. if (activeItem.type === 'SONG') {
  242. activeItem.iframeRef?.contentWindow?.postMessage(
  243. { api: 'setPlayState' },
  244. '*'
  245. );
  246. }
  247. }
  248. };
  249. // 切换素材
  250. const toggleMaterial = (itemActive: any) => {
  251. const index = data.itemList.findIndex((n: any) => n.id == itemActive);
  252. if (index > -1) {
  253. handleSwipeChange(index);
  254. }
  255. };
  256. /** 延迟收起模态框 */
  257. const setModelOpen = () => {
  258. clearTimeout(activeData.timer);
  259. closeToast();
  260. activeData.timer = setTimeout(() => {
  261. activeData.model = false;
  262. Object.values(data.videoRefs).map((n: any) =>
  263. n.toggleHideControl(false)
  264. );
  265. }, 4000);
  266. };
  267. /** 立即收起所有的模态框 */
  268. const clearModel = () => {
  269. clearTimeout(activeData.timer);
  270. closeToast();
  271. activeData.model = false;
  272. Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(false));
  273. };
  274. const toggleModel = (type: boolean = true) => {
  275. activeData.model = type;
  276. Object.values(data.videoRefs).map((n: any) => n.toggleHideControl(type));
  277. };
  278. // 双击
  279. const handleDbClick = (item: any) => {
  280. if (item && item.type === 'VIDEO') {
  281. const videoEle: HTMLVideoElement = item.videoEle;
  282. if (videoEle) {
  283. if (videoEle.paused) {
  284. closeToast();
  285. videoEle.play();
  286. } else {
  287. showToast('已暂停');
  288. videoEle.pause();
  289. }
  290. }
  291. }
  292. };
  293. // 切换播放
  294. const togglePlay = (m: any, isPlay: boolean) => {
  295. if (isPlay) {
  296. m.videoEle?.play();
  297. } else {
  298. m.videoEle?.pause();
  299. }
  300. };
  301. const showIndex = ref(-4);
  302. const effectIndex = ref(3);
  303. const effects = [
  304. {
  305. prev: {
  306. transform: 'translate3d(0, 0, -800px) rotateX(180deg)'
  307. },
  308. next: {
  309. transform: 'translate3d(0, 0, -800px) rotateX(-180deg)'
  310. }
  311. },
  312. {
  313. prev: {
  314. transform: 'translate3d(-100%, 0, -800px)'
  315. },
  316. next: {
  317. transform: 'translate3d(100%, 0, -800px)'
  318. }
  319. },
  320. {
  321. prev: {
  322. transform: 'translate3d(-50%, 0, -800px) rotateY(80deg)'
  323. },
  324. next: {
  325. transform: 'translate3d(50%, 0, -800px) rotateY(-80deg)'
  326. }
  327. },
  328. {
  329. prev: {
  330. transform: 'translate3d(-100%, 0, -800px) rotateY(-120deg)'
  331. },
  332. next: {
  333. transform: 'translate3d(100%, 0, -800px) rotateY(120deg)'
  334. }
  335. },
  336. // 风车4
  337. {
  338. prev: {
  339. transform: 'translate3d(-50%, 50%, -800px) rotateZ(-14deg)',
  340. opacity: 0
  341. },
  342. next: {
  343. transform: 'translate3d(50%, 50%, -800px) rotateZ(14deg)',
  344. opacity: 0
  345. }
  346. },
  347. // 翻页5
  348. {
  349. prev: {
  350. transform: 'translateZ(-800px) rotate3d(0, -1, 0, 90deg)',
  351. opacity: 0
  352. },
  353. next: {
  354. transform: 'translateZ(-800px) rotate3d(0, 1, 0, 90deg)',
  355. opacity: 0
  356. },
  357. current: { transitionDelay: '700ms' }
  358. }
  359. ];
  360. const acitveTimer = ref();
  361. // 轮播切换
  362. const handleSwipeChange = (index: number) => {
  363. // 如果是当前正在播放 或者是视频最后一个
  364. if (popupData.activeIndex == index) return;
  365. handleStop();
  366. clearTimeout(acitveTimer.value);
  367. // checkedAnimation(popupData.activeIndex, index);
  368. popupData.activeIndex = index;
  369. acitveTimer.value = setTimeout(
  370. () => {
  371. const item = data.itemList[index];
  372. if (item) {
  373. popupData.tabActive = item.knowledgePointId;
  374. popupData.itemActive = item.id;
  375. popupData.itemName = item.name;
  376. popupData.tabName = item.tabName;
  377. if (item.type == 'SONG') {
  378. activeData.model = true;
  379. }
  380. if (item.type === 'VIDEO') {
  381. // 自动播放下一个视频
  382. clearTimeout(activeData.timer);
  383. closeToast();
  384. item.autoPlay = true;
  385. nextTick(() => {
  386. item.videoEle?.play();
  387. });
  388. }
  389. }
  390. // requestAnimationFrame(() => {
  391. // const _effectIndex = effectIndex.value + 1;
  392. // effectIndex.value =
  393. // _effectIndex >= effects.length - 1 ? 0 : _effectIndex;
  394. // });
  395. },
  396. activeData.isAnimation ? 800 : 0
  397. );
  398. };
  399. /** 是否有转场动画 */
  400. const checkedAnimation = (index: number, nextIndex?: number) => {
  401. const item = data.itemList[index];
  402. const nextItem = data.itemList[nextIndex!];
  403. if (nextItem) {
  404. if (nextItem.knowledgePointId != item.knowledgePointId) {
  405. activeData.isAnimation = true;
  406. return;
  407. }
  408. const videoEle = item.videoEle;
  409. const nextVideo = nextItem.videoEle;
  410. if (videoEle && videoEle.duration < 8 && index < nextIndex!) {
  411. activeData.isAnimation = false;
  412. } else if (nextVideo && nextVideo.duration < 8 && index > nextIndex!) {
  413. activeData.isAnimation = false;
  414. } else {
  415. activeData.isAnimation = true;
  416. }
  417. } else {
  418. activeData.isAnimation = item?.adviseStudyTimeSecond < 8 ? false : true;
  419. }
  420. };
  421. // 上一个知识点, 下一个知识点
  422. const handlePreAndNext = (type: string) => {
  423. if (type === 'up') {
  424. handleSwipeChange(popupData.activeIndex - 1);
  425. } else {
  426. handleSwipeChange(popupData.activeIndex + 1);
  427. }
  428. };
  429. /** 弹窗关闭 */
  430. const handleClosePopup = () => {
  431. const item = data.itemList[popupData.activeIndex];
  432. if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
  433. setModelOpen();
  434. }
  435. };
  436. return () => (
  437. <div id="playContent" class={styles.playContent}>
  438. <div
  439. onClick={() => {
  440. clearTimeout(activeData.timer);
  441. activeData.model = !activeData.model;
  442. Object.values(data.videoRefs).map((n: any) =>
  443. n.toggleHideControl(activeData.model)
  444. );
  445. }}>
  446. <div
  447. class={styles.coursewarePlay}
  448. style={{ width: parentContainer.width }}
  449. onClick={(e: Event) => {
  450. e.stopPropagation();
  451. setModelOpen();
  452. }}>
  453. <div class={styles.wraps}>
  454. {data.itemList.map((m: any, mIndex: number) => {
  455. const isRender =
  456. m.isRender || Math.abs(popupData.activeIndex - mIndex) < 2;
  457. const isEmtry = Math.abs(popupData.activeIndex - mIndex) > 4;
  458. if (isRender) {
  459. m.isRender = true;
  460. }
  461. return isRender ? (
  462. <div
  463. key={'index' + mIndex}
  464. class={[
  465. styles.itemDiv,
  466. popupData.activeIndex === mIndex && styles.itemActive,
  467. activeData.isAnimation && styles.acitveAnimation,
  468. Math.abs(popupData.activeIndex - mIndex) < 2
  469. ? styles.show
  470. : styles.hide
  471. ]}
  472. style={
  473. mIndex < popupData.activeIndex
  474. ? effects[effectIndex.value].prev
  475. : mIndex > popupData.activeIndex
  476. ? effects[effectIndex.value].next
  477. : {}
  478. }
  479. onClick={(e: Event) => {
  480. e.stopPropagation();
  481. clearTimeout(activeData.timer);
  482. if (Date.now() - activeData.nowTime < 300) {
  483. handleDbClick(m);
  484. return;
  485. }
  486. activeData.nowTime = Date.now();
  487. activeData.timer = setTimeout(() => {
  488. activeData.model = !activeData.model;
  489. Object.values(data.videoRefs).map((n: any) =>
  490. n.toggleHideControl(activeData.model)
  491. );
  492. if (activeData.model) {
  493. setModelOpen();
  494. }
  495. }, 300);
  496. }}>
  497. {m.type === 'VIDEO' ? (
  498. <>
  499. <VideoPlay
  500. ref={(v: any) => (data.videoRefs[mIndex] = v)}
  501. item={m}
  502. isEmtry={isEmtry}
  503. onLoadedmetadata={(videoItem: any) => {
  504. m.videoEle = videoItem;
  505. m.isprepare = true;
  506. }}
  507. onTogglePlay={(paused: boolean) => {
  508. m.autoPlay = false;
  509. if (
  510. paused ||
  511. popupData.open ||
  512. popupData.guideOpen
  513. ) {
  514. clearTimeout(activeData.timer);
  515. } else {
  516. setModelOpen();
  517. }
  518. }}
  519. onEnded={() => {
  520. const _index = popupData.activeIndex + 1;
  521. if (_index < data.itemList.length) {
  522. handleSwipeChange(_index);
  523. }
  524. }}
  525. onReset={() => {
  526. if (!m.videoEle?.paused) {
  527. setModelOpen();
  528. }
  529. }}
  530. />
  531. <Transition name="van-fade">
  532. {!m.isprepare && (
  533. <div class={styles.loadWrap}>
  534. <Vue3Lottie
  535. animationData={playLoadData}></Vue3Lottie>
  536. </div>
  537. )}
  538. </Transition>
  539. </>
  540. ) : m.type === 'IMG' ? (
  541. <img src={m.content} />
  542. ) : (
  543. <MusicScore
  544. activeModel={activeData.model}
  545. data-vid={m.id}
  546. music={m}
  547. onSetIframe={(el: any) => {
  548. m.iframeRef = el;
  549. }}
  550. />
  551. )}
  552. </div>
  553. ) : null;
  554. })}
  555. </div>
  556. <Transition name="right">
  557. {activeData.model && (
  558. <div
  559. class={styles.rightFixedBtns}
  560. onClick={(e: Event) => {
  561. e.stopPropagation();
  562. clearTimeout(activeData.timer);
  563. }}>
  564. <div
  565. class={[styles.fullBtn, styles.point]}
  566. onClick={() => (popupData.open = true)}>
  567. <img src={iconMenu} />
  568. <span>课件</span>
  569. </div>
  570. <div
  571. class={[
  572. styles.fullBtn,
  573. popupData.activeIndex == 0 && styles.btnsDisabled
  574. ]}
  575. onClick={() => handlePreAndNext('up')}>
  576. <img src={iconUp} />
  577. <span style={{ textAlign: 'center' }}>上一个</span>
  578. </div>
  579. <div
  580. class={[
  581. styles.fullBtn,
  582. popupData.activeIndex == data.itemList.length - 1 &&
  583. styles.btnsDisabled
  584. ]}
  585. onClick={() => handlePreAndNext('down')}>
  586. <span style={{ textAlign: 'center' }}>下一个</span>
  587. <img src={iconDown} />
  588. </div>
  589. </div>
  590. )}
  591. </Transition>
  592. </div>
  593. </div>
  594. <div
  595. style={{ transform: activeData.model ? '' : 'translateY(-100%)' }}
  596. class={styles.headerContainer}
  597. ref={headeRef}>
  598. <div class={styles.backBtn} onClick={() => goback()}>
  599. <Icon name={iconBack} />
  600. 返回
  601. </div>
  602. <div class={styles.menu}>{popupData.itemName}</div>
  603. </div>
  604. <Popup
  605. class={styles.popup}
  606. style={{ background: 'rgba(0,0,0, 0.75)' }}
  607. overlayClass={styles.overlayClass}
  608. position="right"
  609. round
  610. v-model:show={popupData.open}
  611. onClose={handleClosePopup}>
  612. <Points
  613. data={data.knowledgePointList}
  614. itemActive={popupData.itemActive}
  615. onHandleSelect={(res: any) => {
  616. popupData.open = false;
  617. toggleMaterial(res.itemActive);
  618. }}
  619. />
  620. </Popup>
  621. </div>
  622. );
  623. }
  624. });