index.tsx 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342
  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. computed
  12. } from 'vue';
  13. import iconBack from './image/back.svg';
  14. import styles from './index.module.less';
  15. import 'plyr/dist/plyr.css';
  16. import request from '@/helpers/request';
  17. import { state } from '@/state';
  18. import { useRoute } from 'vue-router';
  19. import {
  20. listenerMessage,
  21. postMessage,
  22. promisefiyPostMessage
  23. } from '@/helpers/native-message';
  24. import MusicScore from './component/musicScore';
  25. // import iconDian from './image/icon-dian.svg';
  26. // import iconPoint from './image/icon-point.svg';
  27. // import qs from 'query-string';
  28. import {
  29. iconUp,
  30. iconDown,
  31. // iconPen,
  32. iconTouping,
  33. iconMenu
  34. } from './image/icons.json';
  35. import Points from './component/points';
  36. import { browser } from '@/helpers/utils';
  37. import { Vue3Lottie } from 'vue3-lottie';
  38. import playLoadData from './datas/data.json';
  39. import { usePageVisibility } from '@vant/use';
  40. // import PlayRecordTime from './playRecordTime';
  41. import { handleCheckVip } from '../hook/useFee';
  42. import OGuide from '@/components/o-guide';
  43. import Tool, { ToolItem, ToolType } from './component/tool';
  44. import Pen from './component/tools/pen';
  45. // import VideoItem from './component/video-item';
  46. import VideoPlay from './component/video-play';
  47. import deepClone from '@/helpers/deep-clone';
  48. import { useInterval, useIntervalFn } from '@vueuse/core';
  49. export default defineComponent({
  50. name: 'CoursewarePlay',
  51. setup() {
  52. const pageVisibility = usePageVisibility();
  53. /** 页面显示和隐藏 */
  54. watch(
  55. () => pageVisibility.value,
  56. value => {
  57. if (value == 'hidden') {
  58. handleStop();
  59. }
  60. }
  61. );
  62. /** 设置播放容器 16:9 */
  63. const parentContainer = reactive({
  64. width: '100vw'
  65. });
  66. const setContainer = () => {
  67. const min = Math.min(screen.width, screen.height);
  68. const max = Math.max(screen.width, screen.height);
  69. const 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. //设置容器16:9
  79. setContainer();
  80. // 横屏
  81. postMessage(
  82. {
  83. api: 'setRequestedOrientation',
  84. content: {
  85. orientation: type
  86. }
  87. },
  88. () => {
  89. // console.log(234);
  90. }
  91. );
  92. // 头,包括返回箭头
  93. // postMessage({
  94. // api: 'setTitleBarVisibility',
  95. // content: {
  96. // status: type
  97. // }
  98. // })
  99. // 安卓的状态栏
  100. postMessage({
  101. api: 'setStatusBarVisibility',
  102. content: {
  103. isVisibility: type
  104. }
  105. });
  106. // 进入页面设置常量
  107. postMessage({
  108. api: 'keepScreenLongLight',
  109. content: {
  110. isOpenLight: type ? true : false
  111. }
  112. });
  113. };
  114. handleInit();
  115. onUnmounted(() => {
  116. handleInit(1);
  117. window.removeEventListener('message', iframeHandle);
  118. });
  119. const route = useRoute();
  120. const headeRef = ref();
  121. const data = reactive({
  122. detail: null as any,
  123. knowledgePointList: [] as any,
  124. itemList: [] as any,
  125. showHead: true,
  126. // isCourse: false,
  127. isRecordPlay: false,
  128. videoRefs: {},
  129. videoState: 'init' as 'init' | 'play',
  130. videoItemRef: null as any,
  131. animationState: 'start' as 'start' | 'end',
  132. disableScreenRecordingFlag: '0' // disable recording
  133. });
  134. const activeData = reactive({
  135. isAutoPlay: true, // 是否自动播放
  136. nowTime: 0,
  137. model: true, // 遮罩
  138. isAnimation: true, // 是否动画
  139. videoBtns: true, // 视频
  140. currentTime: 0,
  141. duration: 0,
  142. timer: null as any,
  143. item: null as any
  144. });
  145. // 获取缓存路径
  146. const getCacheFilePath = async (material: any) => {
  147. const res = await promisefiyPostMessage({
  148. api: 'getCourseFilePath',
  149. content: {
  150. url: material.content,
  151. localPath: '',
  152. materialId: material.materialId,
  153. updateTime: material.updateTime,
  154. type: material.typeCode // SONG VIDEO IMAGE
  155. }
  156. });
  157. // console.log('缓存路径返回', res)
  158. return res;
  159. };
  160. // 获取当前课程是否签退
  161. // const getCourseSchedule = async () => {
  162. // if (!route.query.courseId) return;
  163. // try {
  164. // const res = await request.get(
  165. // `${state.platformApi}/courseSchedule/detail/${route.query.courseId}`,
  166. // {
  167. // hideLoading: true
  168. // }
  169. // );
  170. // if (res?.data) {
  171. // data.isCourse =
  172. // res.data.status === 'ING' && state.platformType == 'TEACHER'
  173. // ? true
  174. // : false;
  175. // // data.isRecordPlay = Date.now() > dayjs(res.data.startTime).valueOf()
  176. // }
  177. // } catch (e) {
  178. // console.log(e);
  179. // }
  180. // };
  181. const getTempList = async (materialList: any, name: any) => {
  182. const list: any = [];
  183. const browserInfo = browser();
  184. for (let j = 0; j < materialList.length; j++) {
  185. const material = materialList[j];
  186. //请求本地缓存
  187. if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.typeCode)) {
  188. const localData: any = await getCacheFilePath(material);
  189. if (localData?.content?.localPath) {
  190. material.url = material.content;
  191. material.content = localData.content.localPath;
  192. }
  193. }
  194. material.iframeRef = null;
  195. material.videoEle = null;
  196. material.tabName = name;
  197. material.autoPlay = false; //加载完成是否自动播放
  198. material.isprepare = false; // 视频是否加载完成
  199. material.isRender = false; // 是否渲染了
  200. list.push(material);
  201. // list.push({
  202. // ...material,
  203. // iframeRef: null,
  204. // videoEle: null,
  205. // tabName: name,
  206. // autoPlay: false, //加载完成是否自动播放
  207. // isprepare: false, // 视频是否加载完成
  208. // isRender: false // 是否渲染了
  209. // });
  210. }
  211. return list;
  212. };
  213. const getItemList = async () => {
  214. const list: any = [];
  215. for (let i = 0; i < data.knowledgePointList.length; i++) {
  216. const item = data.knowledgePointList[i];
  217. if (item.materialList && item.materialList.length > 0) {
  218. const tempList = await getTempList(item.materialList, item.name);
  219. list.push(...tempList);
  220. }
  221. // 第二层级
  222. if (item.children && item.children.length > 0) {
  223. const childrenList = item.children || [];
  224. for (let j = 0; j < childrenList.length; j++) {
  225. const childItem = childrenList[j];
  226. const tempList = await getTempList(
  227. childItem.materialList,
  228. childItem.name
  229. );
  230. list.push(...tempList);
  231. }
  232. }
  233. }
  234. // console.log(list, 'list')
  235. let _firstIndex = list.findIndex(
  236. (n: any) =>
  237. n.knowledgePointMaterialRelationId == route.query.kId ||
  238. n.materialId == route.query.kId
  239. );
  240. _firstIndex = _firstIndex > -1 ? _firstIndex : 0;
  241. const item = list[_firstIndex];
  242. // console.log(_firstIndex, '_firstIndex', route.query.kId, 'route.query.kId', item)
  243. // 是否自动播放
  244. if (activeData.isAutoPlay) {
  245. item.autoPlay = true;
  246. }
  247. popupData.activeIndex = _firstIndex;
  248. popupData.playIndex = _firstIndex;
  249. popupData.tabName = item.tabName;
  250. popupData.tabActive = item.knowledgePointId;
  251. popupData.itemActive = item.id;
  252. popupData.itemName = item.name;
  253. nextTick(() => {
  254. data.itemList = list;
  255. checkedAnimation(popupData.activeIndex);
  256. postMessage({
  257. api: 'courseLoading',
  258. content: {
  259. show: false,
  260. type: 'fullscreen'
  261. }
  262. });
  263. if (data.disableScreenRecordingFlag === '1') {
  264. // 检测是否录屏
  265. handleLimitScreenRecord();
  266. }
  267. setTimeout(() => {
  268. data.animationState = 'end';
  269. }, 500);
  270. });
  271. };
  272. const getDetail = async () => {
  273. try {
  274. const res: any = await request.get(
  275. state.platformApi +
  276. `/lessonCourseware/getLessonCourseDetail/${route.query.id}`,
  277. {
  278. hideLoading: true
  279. }
  280. );
  281. data.detail = res.data;
  282. if (res?.data?.lockFlag) {
  283. postMessage({
  284. api: 'courseLoading',
  285. content: {
  286. show: false,
  287. type: 'fullscreen'
  288. }
  289. });
  290. showDialog({
  291. title: '温馨提示',
  292. message: '课件已锁定'
  293. }).then(() => {
  294. goback();
  295. });
  296. return;
  297. }
  298. if (Array.isArray(res?.data?.knowledgePointList)) {
  299. let index = 0;
  300. data.knowledgePointList = res.data.knowledgePointList.map(
  301. (n: any) => {
  302. if (Array.isArray(n.materialList)) {
  303. n.materialList = n.materialList.map((item: any) => {
  304. index++;
  305. const materialRefs = item.materialRefs
  306. ? item.materialRefs
  307. : [];
  308. const materialMusicId =
  309. materialRefs.length > 0
  310. ? materialRefs[0].resourceIdStr
  311. : null;
  312. return {
  313. ...item,
  314. materialMusicId,
  315. content: item.content,
  316. knowledgePointId: [item.knowledgePointId],
  317. materialId: item.id,
  318. id: index + ''
  319. };
  320. });
  321. }
  322. if (Array.isArray(n.children)) {
  323. n.children = n.children.map((cn: any) => {
  324. cn.materialList = cn.materialList.map((item: any) => {
  325. index++;
  326. const materialRefs = item.materialRefs
  327. ? item.materialRefs
  328. : [];
  329. const materialMusicId =
  330. materialRefs.length > 0
  331. ? materialRefs[0].resourceIdStr
  332. : null;
  333. return {
  334. ...item,
  335. materialMusicId,
  336. content: item.content,
  337. knowledgePointId: [n.id, item.knowledgePointId],
  338. materialId: item.id,
  339. id: index + ''
  340. };
  341. });
  342. return cn;
  343. });
  344. }
  345. return n;
  346. }
  347. );
  348. getItemList();
  349. }
  350. } catch (error) {
  351. console.log(error);
  352. }
  353. };
  354. // ifram事件处理
  355. const iframeHandle = (ev: MessageEvent) => {
  356. if (ev.data?.api === 'headerTogge') {
  357. activeData.model =
  358. ev.data.show || (ev.data.playState == 'play' ? false : true);
  359. }
  360. };
  361. // 切换播放
  362. const togglePlay = (m: any, isPlay: boolean) => {
  363. if (isPlay) {
  364. m.videoEle?.play();
  365. } else {
  366. m.videoEle?.pause();
  367. }
  368. };
  369. let timers: any = null;
  370. const checkVideoPlay = () => {
  371. const activeVideoRef = data.videoItemRef?.getPlyrRef();
  372. if (activeVideoRef) {
  373. timers = setInterval(() => {
  374. if (!activeVideoRef.paused()) {
  375. activeVideoRef.pause();
  376. clearInterval(timers);
  377. }
  378. activeVideoRef.pause();
  379. }, 100);
  380. }
  381. setTimeout(() => {
  382. clearInterval(timers);
  383. }, 3000);
  384. };
  385. //录屏时间触发
  386. const handleLimitScreenRecord = async () => {
  387. const result = await promisefiyPostMessage({
  388. api: 'getDeviceStatus',
  389. content: { type: 'video' }
  390. });
  391. const { status } = result?.content || {};
  392. if (status == '1') {
  393. data.itemList.forEach((item: any) => (item.autoPlay = false));
  394. handleStop();
  395. // 处理事件 - 事件事件后加载的
  396. checkVideoPlay();
  397. showDialog({
  398. title: '温馨提示',
  399. message: '课件内容请勿录屏',
  400. beforeClose: () => {
  401. return new Promise(resolve => {
  402. promisefiyPostMessage({
  403. api: 'getDeviceStatus',
  404. content: { type: 'video' }
  405. }).then((res: any) => {
  406. const content = res.content;
  407. if (content?.status == '1') {
  408. const activeItem = data.itemList[popupData.activeIndex];
  409. togglePlay(activeItem, false);
  410. resolve(false);
  411. } else {
  412. const activeItem = data.itemList[popupData.activeIndex];
  413. togglePlay(activeItem, true);
  414. resolve(true);
  415. }
  416. });
  417. });
  418. }
  419. });
  420. }
  421. };
  422. // 获取支付渠道
  423. const sysParamConfig = async () => {
  424. try {
  425. const res = await request.get(
  426. state.platformApi + '/sysConfig/queryByParamName',
  427. {
  428. params: {
  429. paramName: 'disable_screen_recording_flag'
  430. }
  431. }
  432. );
  433. data.disableScreenRecordingFlag = res.data.paranValue || '';
  434. } catch {
  435. //
  436. }
  437. };
  438. onMounted(async () => {
  439. await sysParamConfig();
  440. await getDetail();
  441. const hasFree = String(data.detail?.accessScope) === '0';
  442. if (!hasFree) {
  443. if (state.platformType === 'STUDENT') {
  444. const hasVip = handleCheckVip();
  445. if (!hasVip) {
  446. nextTick(() => {
  447. postMessage({
  448. api: 'courseLoading',
  449. content: {
  450. show: false,
  451. type: 'fullscreen'
  452. }
  453. });
  454. });
  455. return;
  456. }
  457. }
  458. }
  459. // getCourseSchedule();
  460. window.addEventListener('message', iframeHandle);
  461. if (data.disableScreenRecordingFlag === '1') {
  462. //禁止录屏 ios
  463. listenerMessage('setVideoPlayer', result => {
  464. if (result?.content?.status == 'pause') {
  465. handleLimitScreenRecord();
  466. }
  467. });
  468. // 安卓
  469. postMessage({
  470. api: 'limitScreenRecord',
  471. content: {
  472. type: 1
  473. }
  474. });
  475. }
  476. });
  477. const playRef = ref();
  478. // 返回
  479. const goback = () => {
  480. try {
  481. playRef.value?.handleOut();
  482. } catch (error) {
  483. console.log(error);
  484. }
  485. postMessage({ api: 'back' });
  486. };
  487. const popupData = reactive({
  488. open: false,
  489. activeIndex: 0,
  490. playIndex: 0,
  491. tabActive: '',
  492. tabName: '',
  493. itemActive: '',
  494. itemName: '',
  495. guideOpen: false,
  496. toolOpen: false // 工具弹窗控制
  497. });
  498. const stopVideo = (el: HTMLVideoElement) => {
  499. return new Promise(resolve => {
  500. if (el.paused) return resolve(true);
  501. el.onpause = () => {
  502. console.log('暂停');
  503. resolve(true);
  504. };
  505. el.pause();
  506. });
  507. };
  508. /**停止所有的播放 */
  509. const handleStop = async () => {
  510. const videos = document.querySelectorAll('video');
  511. for (let i = 0; i < videos.length; i++) {
  512. const videoEle = videos[i] as HTMLVideoElement;
  513. await stopVideo(videoEle);
  514. }
  515. console.log('视频暂停完成');
  516. data.itemList.forEach((item: any) => {
  517. if (item.typeCode === 'SONG') {
  518. item.iframeRef?.contentWindow?.postMessage(
  519. { api: 'setPlayState' },
  520. '*'
  521. );
  522. }
  523. });
  524. };
  525. // 切换素材
  526. const toggleMaterial = (itemActive: any) => {
  527. const index = data.itemList.findIndex((n: any) => n.id == itemActive);
  528. if (index > -1) {
  529. handleSwipeChange(index);
  530. }
  531. };
  532. /** 延迟收起模态框 */
  533. const setModelOpen = () => {
  534. clearTimeout(activeData.timer);
  535. closeToast();
  536. activeData.timer = setTimeout(() => {
  537. activeData.model = false;
  538. }, 4000);
  539. };
  540. /** 立即收起所有的模态框 */
  541. const clearModel = () => {
  542. clearTimeout(activeData.timer);
  543. closeToast();
  544. activeData.model = false;
  545. };
  546. const toggleModel = (type = true) => {
  547. activeData.model = type;
  548. };
  549. // 去点名,签退
  550. // const gotoRollCall = (pageTag: string) => {
  551. // postMessage({
  552. // api: 'open_app_page',
  553. // content: {
  554. // action: 'app',
  555. // pageTag: pageTag,
  556. // url: '',
  557. // params: JSON.stringify({ courseId: route.query.courseId })
  558. // }
  559. // });
  560. // };
  561. // 双击
  562. const handleDbClick = () => {
  563. if (activeVideoItem.value.typeCode === 'VIDEO') {
  564. const activeVideoRef = data.videoItemRef?.getPlyrRef();
  565. if (activeVideoRef) {
  566. if (activeVideoRef.paused()) {
  567. activeVideoRef.play();
  568. } else {
  569. activeVideoRef.pause();
  570. showToast('已暂停');
  571. }
  572. }
  573. }
  574. };
  575. const effectIndex = ref(0);
  576. const effects = [
  577. {
  578. prev: {
  579. transform: 'translate3d(0, 0, -800px) rotateX(180deg)'
  580. },
  581. next: {
  582. transform: 'translate3d(0, 0, -800px) rotateX(-180deg)'
  583. }
  584. },
  585. {
  586. prev: {
  587. transform: 'translate3d(-100%, 0, -800px)'
  588. },
  589. next: {
  590. transform: 'translate3d(100%, 0, -800px)'
  591. }
  592. },
  593. {
  594. prev: {
  595. transform: 'translate3d(-50%, 0, -800px) rotateY(80deg)'
  596. },
  597. next: {
  598. transform: 'translate3d(50%, 0, -800px) rotateY(-80deg)'
  599. }
  600. },
  601. {
  602. prev: {
  603. transform: 'translate3d(-100%, 0, -800px) rotateY(-120deg)'
  604. },
  605. next: {
  606. transform: 'translate3d(100%, 0, -800px) rotateY(120deg)'
  607. }
  608. },
  609. // 风车4
  610. {
  611. prev: {
  612. transform: 'translate3d(-50%, 50%, -800px) rotateZ(-14deg)',
  613. opacity: 0
  614. },
  615. next: {
  616. transform: 'translate3d(50%, 50%, -800px) rotateZ(14deg)',
  617. opacity: 0
  618. }
  619. },
  620. // 翻页5
  621. {
  622. prev: {
  623. transform: 'translateZ(-800px) rotate3d(0, -1, 0, 90deg)',
  624. opacity: 0
  625. },
  626. next: {
  627. transform: 'translateZ(-800px) rotate3d(0, 1, 0, 90deg)',
  628. opacity: 0
  629. },
  630. current: { transitionDelay: '700ms' }
  631. }
  632. ];
  633. const acitveTimer = ref();
  634. // 轮播切换
  635. const handleSwipeChange = async (index: number) => {
  636. // 如果是当前正在播放 或者是视频最后一个
  637. if (popupData.activeIndex == index) return;
  638. await handleStop();
  639. data.animationState = 'start';
  640. data.videoState = 'init';
  641. clearTimeout(acitveTimer.value);
  642. checkedAnimation(popupData.activeIndex, index);
  643. nextTick(() => {
  644. popupData.activeIndex = index;
  645. acitveTimer.value = setTimeout(
  646. () => {
  647. popupData.playIndex = index;
  648. const item = data.itemList[index];
  649. if (item) {
  650. popupData.tabActive = item.knowledgePointId;
  651. popupData.itemActive = item.id;
  652. popupData.itemName = item.name;
  653. popupData.tabName = item.tabName;
  654. if (item.typeCode == 'SONG') {
  655. activeData.model = true;
  656. }
  657. }
  658. requestAnimationFrame(() => {
  659. const _effectIndex = effectIndex.value + 1;
  660. effectIndex.value =
  661. _effectIndex >= effects.length - 1 ? 0 : _effectIndex;
  662. if (item && item.typeCode === 'VIDEO') {
  663. // 自动播放下一个视频
  664. clearTimeout(activeData.timer);
  665. closeToast();
  666. item.autoPlay = true;
  667. data.animationState = 'end';
  668. }
  669. });
  670. },
  671. activeData.isAnimation ? 850 : 0
  672. );
  673. });
  674. };
  675. /** 是否有转场动画 */
  676. const checkedAnimation = (index: number, nextIndex?: number) => {
  677. nextIndex = nextIndex ? nextIndex : index + 1;
  678. const item = data.itemList[index];
  679. const nextItem = data.itemList[nextIndex];
  680. if (nextItem) {
  681. if (nextItem.knowledgePointId != item.knowledgePointId) {
  682. activeData.isAnimation = true;
  683. return;
  684. }
  685. const videoEle = item.videoEle;
  686. const nextVideo = nextItem.videoEle;
  687. if (videoEle && videoEle.duration < 8 && index < nextIndex) {
  688. activeData.isAnimation = false;
  689. } else if (nextVideo && nextVideo.duration < 8 && index > nextIndex) {
  690. activeData.isAnimation = false;
  691. } else {
  692. activeData.isAnimation = true;
  693. }
  694. } else {
  695. activeData.isAnimation = item?.adviseStudyTimeSecond < 8 ? false : true;
  696. }
  697. };
  698. // 上一个知识点, 下一个知识点
  699. const handlePreAndNext = (type: string) => {
  700. if (type === 'up') {
  701. handleSwipeChange(popupData.activeIndex - 1);
  702. } else {
  703. handleSwipeChange(popupData.activeIndex + 1);
  704. }
  705. };
  706. /** 弹窗关闭 */
  707. const handleClosePopup = () => {
  708. const item = data.itemList[popupData.activeIndex];
  709. if (item?.typeCode == 'VIDEO' && !item.videoEle?.paused) {
  710. setModelOpen();
  711. }
  712. };
  713. /** 教学数据 */
  714. const studyData = reactive({
  715. type: '' as ToolType,
  716. penShow: false
  717. });
  718. /** 打开教学工具 */
  719. const openStudyTool = (item: ToolItem) => {
  720. const activeItem = data.itemList[popupData.activeIndex];
  721. // 暂停视频和曲谱的播放
  722. if (activeItem.typeCode === 'VIDEO' && activeItem.videoEle) {
  723. activeItem.videoEle.pause();
  724. }
  725. if (activeItem.typeCode === 'SONG') {
  726. activeItem.iframeRef?.contentWindow?.postMessage(
  727. { api: 'setPlayState' },
  728. '*'
  729. );
  730. }
  731. clearModel();
  732. popupData.toolOpen = false;
  733. studyData.type = item.type;
  734. switch (item.type) {
  735. case 'pen':
  736. studyData.penShow = true;
  737. break;
  738. }
  739. };
  740. /** 关闭教学工具 */
  741. const closeStudyTool = () => {
  742. studyData.type = 'init';
  743. toggleModel();
  744. };
  745. const activeVideoItem = computed(() => {
  746. const item = data.itemList[popupData.activeIndex];
  747. if (
  748. item &&
  749. item.typeCode &&
  750. item.typeCode.toLocaleUpperCase() === 'VIDEO'
  751. ) {
  752. return item;
  753. }
  754. return {};
  755. });
  756. let closeModelTimer: any = null;
  757. /**
  758. * 统计视频播放时间段
  759. */
  760. const intervalFnRef = ref(); // 定时任务
  761. // 播放视频总时长
  762. const videoIntervalRef = useInterval(1000, { controls: true });
  763. videoIntervalRef.pause();
  764. /**
  765. * 格式化视屏播放有效时间 - 合并区间
  766. * @param intervals [[], []]
  767. * @example [[4, 8],[0, 4],[10, 30]]
  768. * @returns [[0, 8], [10, 30]]
  769. */
  770. // const formatEffectiveTime = (intervals: any[]) => {
  771. // const res: any = [];
  772. // intervals.sort((a, b) => a[0] - b[0]);
  773. // let prev = intervals[0];
  774. // for (let i = 1; i < intervals.length; i++) {
  775. // const cur = intervals[i];
  776. // if (prev[1] >= cur[0]) {
  777. // // 有重合
  778. // prev[1] = Math.max(cur[1], prev[1]);
  779. // } else {
  780. // // 不重合,prev推入res数组
  781. // res.push(prev);
  782. // prev = cur; // 更新 prev
  783. // }
  784. // }
  785. // res.push(prev);
  786. // // console.log(res, 'formatEffectiveTime')
  787. // return res;
  788. // };
  789. /**
  790. * 获取数据有效期
  791. * @param intervals [[], []]
  792. * @returns 0s
  793. */
  794. // const formatTimer = (intervals: any[]) => {
  795. // const afterIntervals = formatEffectiveTime(intervals);
  796. // let time = 0;
  797. // afterIntervals.forEach((t: any) => {
  798. // time += t[1] - t[0];
  799. // });
  800. // return time;
  801. // };
  802. // 保存零时时间
  803. // const moreTime: any = ref([]) // 多个观看时间段 已经放到列表里面了
  804. let tempTime: any = []; // 临时存储时间
  805. const currentTimer = useInterval(1000, { controls: true });
  806. // 监听播放状态,
  807. watch(
  808. () => videoIntervalRef.isActive.value,
  809. (newVal: boolean) => {
  810. initVideoCount(newVal);
  811. }
  812. );
  813. /**
  814. * 初始化视频时长
  815. * @param newVal 播放状态
  816. * @param repeat 是否为定时发送的
  817. */
  818. const initVideoCount = (newVal: any, repeat = false) => {
  819. // console.log('watch', forms.player.currentTime)
  820. const activeVideoRef = data.videoItemRef?.getPlyrRef();
  821. const initTime = deepClone(tempTime);
  822. if (repeat) {
  823. if (tempTime.length > 0) {
  824. // console.log('join video', tempTime, 'initTime', initTime)
  825. tempTime[1] = Math.floor(activeVideoRef.currentTime());
  826. }
  827. } else {
  828. if (newVal) {
  829. tempTime[0] = Math.floor(activeVideoRef.currentTime());
  830. } else {
  831. tempTime[1] = Math.floor(activeVideoRef.currentTime());
  832. }
  833. }
  834. if (tempTime.length >= 2) {
  835. // console.log(tempTime, 'tempTime', moreTime.value)
  836. // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
  837. const diffTime =
  838. tempTime[1] - tempTime[0] - currentTimer.counter.value > 2;
  839. // 结束时间,如果 大于开始时间则清除
  840. if (tempTime[1] >= tempTime[0] && !diffTime) {
  841. data.itemList[popupData.activeIndex].moreTime?.push(tempTime);
  842. // moreTime.value.push(tempTime)
  843. }
  844. if (repeat) {
  845. tempTime = deepClone(initTime);
  846. } else {
  847. tempTime = [];
  848. currentTimer.counter.value = 0;
  849. }
  850. }
  851. };
  852. // 更新时间
  853. const updateStat = async () => {
  854. try {
  855. // const itemList = data.itemList;
  856. // const params: any = [];
  857. // itemList.forEach((item: any) => {
  858. // if (item.moreTime.length > 0) {
  859. // const videoBrowseData = formatEffectiveTime(item.moreTime);
  860. // const time =
  861. // videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0;
  862. // const temp = {
  863. // lessonCoursewareDetailId: route.query.id,
  864. // browseTime: time, // 播放时长
  865. // videoBrowseData: JSON.stringify(videoBrowseData), // 播放的数据
  866. // videoTime: item.videoTime, // 视频时长
  867. // materialId: item.materialId
  868. // };
  869. // params.push(temp);
  870. // }
  871. // });
  872. // 只有学生才统计数据
  873. if (state.platformType === 'STUDENT') {
  874. const videoTime = videoIntervalRef.counter.value;
  875. if (videoTime <= 0) return;
  876. videoIntervalRef.counter.value = 0;
  877. await request.post(
  878. `${state.platformApi}/studentCoursewarePlayRecord/save`,
  879. {
  880. data: {
  881. playTime: videoTime
  882. }
  883. }
  884. );
  885. }
  886. } catch {
  887. //
  888. }
  889. };
  890. onMounted(() => {
  891. if (state.platformType === 'STUDENT') {
  892. // 间隔多少时间同步数据
  893. intervalFnRef.value = useIntervalFn(async () => {
  894. // 同步数据时先进行有效时间进行保存
  895. // initVideoCount(false, true);
  896. await updateStat();
  897. }, 5000);
  898. }
  899. });
  900. /** 统计视频播放时间段 */
  901. return () => (
  902. <div id="playContent" class={styles.playContent}>
  903. <div
  904. class={styles.coursewarePlay}
  905. style={{ width: parentContainer.width }}
  906. onClick={() => {
  907. clearTimeout(closeModelTimer);
  908. clearTimeout(activeData.timer);
  909. closeToast();
  910. if (Date.now() - activeData.nowTime < 300) {
  911. handleDbClick();
  912. return;
  913. }
  914. activeData.nowTime = Date.now();
  915. closeModelTimer = setTimeout(() => {
  916. activeData.model = !activeData.model;
  917. }, 300);
  918. }}>
  919. <div class={styles.wraps}>
  920. <div
  921. style={
  922. activeVideoItem.value.typeCode &&
  923. data.animationState === 'end' &&
  924. data.videoState === 'play'
  925. ? {
  926. zIndex: 15,
  927. opacity: 1
  928. }
  929. : { opacity: 0, zIndex: -1, pointerEvents: "none" }
  930. }
  931. class={styles.itemDiv}>
  932. <VideoPlay
  933. ref={(el: any) => (data.videoItemRef = el)}
  934. item={activeVideoItem.value}
  935. activeModel={activeData.model}
  936. onPlay={() => {
  937. data.videoState = 'play';
  938. data.animationState = 'end';
  939. }}
  940. onLoadedmetadata={(videoItem: any) => {
  941. data.videoState = 'play';
  942. activeVideoItem.value.videoEle = videoItem;
  943. if (!activeVideoItem.value.isprepare) {
  944. activeVideoItem.value.isprepare = true;
  945. }
  946. }}
  947. onSeeked={() => {
  948. videoIntervalRef.isActive.value && videoIntervalRef.pause();
  949. }}
  950. onSeeking={() => {
  951. videoIntervalRef.isActive.value && videoIntervalRef.pause();
  952. }}
  953. onWaiting={() => {
  954. videoIntervalRef.isActive.value && videoIntervalRef.pause();
  955. }}
  956. onTimeupdate={() => {
  957. const activeVideoRef = data.videoItemRef?.getPlyrRef();
  958. if (
  959. !videoIntervalRef.isActive.value &&
  960. activeVideoRef?.currentTime() > 0 &&
  961. !activeVideoRef?.paused()
  962. ) {
  963. videoIntervalRef.resume();
  964. }
  965. }}
  966. onPause={() => {
  967. clearTimeout(activeData.timer);
  968. activeData.model = true;
  969. videoIntervalRef.pause();
  970. }}
  971. onEnded={async () => {
  972. const _index = popupData.activeIndex + 1;
  973. if (_index < data.itemList.length) {
  974. handleSwipeChange(_index);
  975. }
  976. }}
  977. onError={() => {
  978. // 视屏异常
  979. activeVideoItem.value.error = true;
  980. }}
  981. />
  982. </div>
  983. {data.itemList.map((m: any, mIndex: number) => {
  984. const isRenderItem = Math.abs(popupData.activeIndex - mIndex) < 2;
  985. const isRender = Math.abs(popupData.playIndex - mIndex) < 2;
  986. // 判断是否是当前选中的元素
  987. const activeEle = popupData.playIndex === mIndex ? true : false;
  988. return isRenderItem ? (
  989. <div
  990. key={'index' + mIndex}
  991. data-id={'data' + mIndex}
  992. class={[
  993. styles.itemDiv,
  994. activeEle && styles.itemActive,
  995. activeData.isAnimation && styles.acitveAnimation,
  996. isRenderItem ? styles.show : styles.hide
  997. ]}
  998. style={
  999. mIndex < popupData.activeIndex
  1000. ? effects[effectIndex.value].prev
  1001. : mIndex > popupData.activeIndex
  1002. ? effects[effectIndex.value].next
  1003. : {}
  1004. }>
  1005. {/* {m.type === 'VIDEO' && (
  1006. <>
  1007. <VideoPlay
  1008. ref={(v: any) => (data.videoRefs[mIndex] = v)}
  1009. item={m}
  1010. isActive={activeEle}
  1011. isEmtry={isEmtry}
  1012. onPrepare={(val) => {
  1013. m.isprepare = val
  1014. }}
  1015. onLoadedmetadata={(videoItem: any) => {
  1016. m.videoEle = videoItem
  1017. }}
  1018. onTogglePlay={(paused: boolean) => {
  1019. // console.log('播放切换', paused)
  1020. if (!m.isprepare) {
  1021. m.isprepare = true
  1022. }
  1023. m.autoPlay = false
  1024. if (paused || popupData.open || popupData.guideOpen) {
  1025. clearTimeout(activeData.timer)
  1026. } else {
  1027. setModelOpen()
  1028. }
  1029. }}
  1030. onEnded={() => {
  1031. const _index = popupData.activeIndex + 1
  1032. if (_index < data.itemList.length) {
  1033. handleSwipeChange(_index)
  1034. }
  1035. }}
  1036. onReset={() => {
  1037. if (!m.videoEle?.paused) {
  1038. setModelOpen()
  1039. }
  1040. }}
  1041. />
  1042. <Transition name="van-fade">
  1043. {!m.isprepare && (
  1044. <div class={styles.loadWrap}>
  1045. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  1046. </div>
  1047. )}
  1048. </Transition>
  1049. </>
  1050. )} */}
  1051. <Transition name="van-fade">
  1052. {m.typeCode === 'VIDEO' &&
  1053. data.animationState !== 'end' &&
  1054. data.videoState != 'play' && (
  1055. <div class={styles.loadWrap}>
  1056. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  1057. </div>
  1058. )}
  1059. </Transition>
  1060. {isRender && m.typeCode === 'IMG' && (
  1061. <>
  1062. <img src={m.content} />
  1063. {m.materialMusicId && (
  1064. <div
  1065. class={[
  1066. styles.goPractice,
  1067. activeData.model ? '' : styles.hide
  1068. ]}
  1069. onClick={(e: any) => {
  1070. // 去云练习完整版
  1071. e.stopPropagation();
  1072. // const Authorization =
  1073. // sessionStorage.getItem('Authorization') || '';
  1074. const origin = /(localhost|192)/.test(location.host)
  1075. ? 'https://test.gym.lexiaoya.cn/'
  1076. : location.origin;
  1077. const src = `${origin}/gym-music-score/?id=${m.materialMusicId}&isHideMusicList=true&systemType=${ state.platformType === 'TEACHER' ? 'teacher' : 'student'}`
  1078. postMessage({
  1079. api: 'openAccompanyWebView',
  1080. content: {
  1081. url: src,
  1082. orientation: 0,
  1083. c_orientation: 0,
  1084. isHideTitle: true,
  1085. statusBarTextColor: false,
  1086. isOpenLight: true
  1087. }
  1088. });
  1089. }}></div>
  1090. )}
  1091. </>
  1092. )}
  1093. {isRender && m.typeCode === 'SONG' && (
  1094. <MusicScore
  1095. activeModel={activeData.model}
  1096. data-vid={m.id}
  1097. music={m}
  1098. onSetIframe={(el: any) => {
  1099. m.iframeRef = el;
  1100. }}
  1101. />
  1102. )}
  1103. </div>
  1104. ) : (
  1105. ''
  1106. );
  1107. })}
  1108. </div>
  1109. <Transition name="right">
  1110. {activeData.model && (
  1111. <div
  1112. class={styles.rightFixedBtns}
  1113. onClick={(e: Event) => {
  1114. e.stopPropagation();
  1115. clearTimeout(activeData.timer);
  1116. }}>
  1117. <div class={styles.btnsWrap}>
  1118. <div
  1119. class={[styles.fullBtn, styles.point]}
  1120. onClick={() => (popupData.open = true)}>
  1121. <img src={iconMenu} />
  1122. <span>知识点</span>
  1123. </div>
  1124. </div>
  1125. <div class={[styles.btnsWrap, styles.btnsBottom]}>
  1126. {/* <div class={styles.fullBtn} onClick={() => (popupData.guideOpen = true)}>
  1127. <img src={iconTouping} />
  1128. <span>投屏</span>
  1129. </div> */}
  1130. {/* {data.isCourse && (
  1131. <>
  1132. <div
  1133. class={styles.fullBtn}
  1134. onClick={() => gotoRollCall('student_roll_call')}>
  1135. <img src={iconDian} />
  1136. <span>点名</span>
  1137. </div>
  1138. <div
  1139. class={styles.fullBtn}
  1140. onClick={() => gotoRollCall('sign_out')}>
  1141. <img src={iconPoint} />
  1142. <span>签退</span>
  1143. </div>
  1144. </>
  1145. )} */}
  1146. </div>
  1147. </div>
  1148. )}
  1149. </Transition>
  1150. <Transition name="left">
  1151. {activeData.model && (
  1152. <div
  1153. class={styles.leftFixedBtns}
  1154. onClick={(e: Event) => e.stopPropagation()}>
  1155. {popupData.activeIndex != 0 && (
  1156. <div class={[styles.btnsWrap, styles.prePoint]}>
  1157. <div
  1158. class={styles.fullBtn}
  1159. onClick={() => {
  1160. // useThrottleFn(() => {
  1161. // handlePreAndNext('up')
  1162. // }, 300)
  1163. // onChangeSwiper('up')
  1164. handlePreAndNext('up');
  1165. }}>
  1166. <img src={iconUp} />
  1167. <span style={{ textAlign: 'center' }}>上一个</span>
  1168. </div>
  1169. </div>
  1170. )}
  1171. {popupData.activeIndex != data.itemList.length - 1 && (
  1172. <div class={styles.btnsWrap}>
  1173. <div
  1174. class={styles.fullBtn}
  1175. onClick={() => {
  1176. // console.log('click down')
  1177. // useThrottleFn(() => {
  1178. // console.log('click down pass')
  1179. // handlePreAndNext('down')
  1180. // }, 300)
  1181. // onChangeSwiper('down')
  1182. handlePreAndNext('down');
  1183. }}>
  1184. <span style={{ textAlign: 'center' }}>下一个</span>
  1185. <img src={iconDown} />
  1186. </div>
  1187. </div>
  1188. )}
  1189. </div>
  1190. )}
  1191. </Transition>
  1192. </div>
  1193. <div
  1194. style={{ transform: activeData.model ? '' : 'translateY(-100%)' }}
  1195. id="coursePlayHeader"
  1196. class={styles.headerContainer}
  1197. ref={headeRef}>
  1198. <div class={styles.backBtn} onClick={() => goback()}>
  1199. <Icon name={iconBack} />
  1200. 返回
  1201. </div>
  1202. {/* {data.isCourse && (
  1203. <PlayRecordTime ref={playRef} list={data.knowledgePointList} />
  1204. )} */}
  1205. <div
  1206. class={styles.menu}
  1207. onClick={() => {
  1208. const _effectIndex = effectIndex.value + 1;
  1209. effectIndex.value =
  1210. _effectIndex >= effects.length - 1 ? 0 : _effectIndex;
  1211. setModelOpen();
  1212. }}>
  1213. {popupData.tabName}
  1214. </div>
  1215. {state.platformType == 'TEACHER' && (
  1216. <div
  1217. class={styles.headRight}
  1218. onClick={(e: Event) => {
  1219. e.stopPropagation();
  1220. clearTimeout(activeData.timer);
  1221. }}>
  1222. <div
  1223. class={styles.rightBtn}
  1224. onClick={() => (popupData.guideOpen = true)}>
  1225. <img src={iconTouping} />
  1226. </div>
  1227. {/* <div
  1228. class={styles.rightBtn}
  1229. onClick={() => {
  1230. openStudyTool({
  1231. type: 'pen',
  1232. icon: iconPen,
  1233. name: '批注'
  1234. });
  1235. }}>
  1236. <img src={iconPen} />
  1237. </div> */}
  1238. {/* <div class={styles.rightBtn} onClick={() => (popupData.toolOpen = true)}>
  1239. <img src={iconMore} />
  1240. </div> */}
  1241. </div>
  1242. )}
  1243. </div>
  1244. {/* 更多弹窗 */}
  1245. <Popup
  1246. class={styles.popupMore}
  1247. overlayClass={styles.overlayClass}
  1248. position="right"
  1249. round
  1250. v-model:show={popupData.toolOpen}
  1251. onClose={handleClosePopup}>
  1252. <Tool onHandleTool={openStudyTool} />
  1253. </Popup>
  1254. <Popup
  1255. class={styles.popup}
  1256. style={{ background: 'rgba(0,0,0, 0.75)' }}
  1257. overlayClass={styles.overlayClass}
  1258. position="right"
  1259. round
  1260. v-model:show={popupData.open}
  1261. onClose={handleClosePopup}>
  1262. <Points
  1263. data={data.knowledgePointList}
  1264. tabActive={popupData.tabActive}
  1265. itemActive={popupData.itemActive}
  1266. onHandleSelect={(res: any) => {
  1267. // onChangeSwiper('change', res.itemActive)
  1268. popupData.open = false;
  1269. toggleMaterial(res.itemActive);
  1270. }}
  1271. />
  1272. </Popup>
  1273. <Popup
  1274. class={styles.popup}
  1275. overlayClass={styles.overlayClass}
  1276. position="right"
  1277. round
  1278. v-model:show={popupData.guideOpen}
  1279. onClose={handleClosePopup}>
  1280. <OGuide />
  1281. </Popup>
  1282. {studyData.penShow && (
  1283. <Pen show={studyData.type === 'pen'} close={() => closeStudyTool()} />
  1284. )}
  1285. </div>
  1286. );
  1287. }
  1288. });