index.tsx 35 KB

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