index.tsx 41 KB

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