index.tsx 40 KB

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