index.tsx 38 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217
  1. import {
  2. defineComponent,
  3. onMounted,
  4. reactive,
  5. onUnmounted,
  6. ref,
  7. Transition,
  8. computed,
  9. nextTick
  10. } from 'vue';
  11. import styles from './index.module.less';
  12. import 'plyr/dist/plyr.css';
  13. import MusicScore from './component/musicScore';
  14. import iconMenu from './image/icon-menu.png';
  15. import iconUp from './image/icon-up.png';
  16. import iconDown from './image/icon-down.png';
  17. import iconNote from './image/icon-note.png';
  18. import iconWhiteboard from './image/icon-whiteboard.png';
  19. import iconAssignHomework from './image/icon-assignHomework.png';
  20. import iconClose from './image/icon-close.png';
  21. import iconOverPreivew from './image/icon-over-preview.png';
  22. import { Vue3Lottie } from 'vue3-lottie';
  23. import playLoadData from './datas/data.json';
  24. import Moveable from 'moveable';
  25. import VideoPlay from './component/video-play';
  26. import {
  27. useMessage,
  28. NDrawer,
  29. NDrawerContent,
  30. NModal,
  31. NSpace,
  32. NButton,
  33. NTooltip,
  34. NPopover,
  35. NImage
  36. } from 'naive-ui';
  37. import CardType from '@/components/card-type';
  38. import Pen from './component/tools/pen';
  39. import AudioPay from './component/audio-pay';
  40. import TrainSettings from './model/train-settings';
  41. import { useRoute } from 'vue-router';
  42. import { lessonPreTrainingPage, queryCourseware } from '../prepare-lessons/api';
  43. import Attentguide from '@/custom-plugins/guide-page/attent-guide';
  44. import { vaildUrl } from '/src/utils/urlUtils';
  45. import TimerMeter from '/src/components/timerMeter';
  46. import toneImage from '/src/components/layout/images/toneImage.png';
  47. import toolbox from '/src/components/layout/images/toolbox.png';
  48. import setTimeIcon from '/src/components/layout/images/setTimeIcon.png';
  49. import beatIcon from '/src/components/layout/images/beatIcon.png';
  50. import toneIcon from '/src/components/layout/images/toneIcon.png';
  51. import { px2vw } from '/src/utils';
  52. import PlaceholderTone from '/src/components/layout/modals/placeholderTone';
  53. import { state } from '/src/state';
  54. export type ToolType = 'init' | 'pen' | 'whiteboard';
  55. export type ToolItem = {
  56. type: ToolType;
  57. name: string;
  58. icon: string;
  59. };
  60. export default defineComponent({
  61. name: 'CoursewarePlay',
  62. props: {
  63. type: {
  64. type: String,
  65. default: ''
  66. },
  67. subjectId: {
  68. type: [String, Number],
  69. default: ''
  70. },
  71. detailId: {
  72. type: String,
  73. default: ''
  74. },
  75. classGroupId: {
  76. type: String,
  77. default: ''
  78. }
  79. },
  80. emits: ['close'],
  81. setup(props, { emit }) {
  82. const message = useMessage();
  83. const route = useRoute();
  84. /** 设置播放容器 16:9 */
  85. const parentContainer = reactive({
  86. width: '100vw'
  87. });
  88. const setContainer = () => {
  89. const min = Math.min(screen.width, screen.height);
  90. const max = Math.max(screen.width, screen.height);
  91. const width = min * (16 / 9);
  92. if (width > max) {
  93. parentContainer.width = '100vw';
  94. return;
  95. } else {
  96. parentContainer.width = width + 'px';
  97. }
  98. };
  99. const handleInit = (type = 0) => {
  100. //设置容器16:9
  101. // setContainer();
  102. };
  103. handleInit();
  104. onUnmounted(() => {
  105. handleInit(1);
  106. });
  107. const data = reactive({
  108. type: 'class' as '' | 'preview' | 'class', // 预览类型
  109. subjectId: '' as any, // 声部编号
  110. detailId: '' as any, // 编号 - 章节编号
  111. classGroupId: '' as any, // 上课时需要 班级编号
  112. // detail: null,
  113. knowledgePointList: [] as any,
  114. itemList: [] as any,
  115. // showHead: true,
  116. // isCourse: false,
  117. // isRecordPlay: false,
  118. videoRefs: {} as any[],
  119. audioRefs: {} as any[],
  120. modelAttendStatus: false, // 布置作业提示弹窗
  121. modalAttendMessage: '本节课未设置课后训练,是否继续?',
  122. modelTrainStatus: false, // 训练设置
  123. homeworkStatus: true, // 布置作业完成时
  124. removeVisiable: false,
  125. removeTitle: '',
  126. removeContent: ''
  127. });
  128. const activeData = reactive({
  129. // isAutoPlay: false, // 是否自动播放
  130. nowTime: 0,
  131. model: true, // 遮罩
  132. isAnimation: true, // 是否动画
  133. // videoBtns: true, // 视频
  134. // currentTime: 0,
  135. // duration: 0,
  136. timer: null as any,
  137. item: null as any
  138. });
  139. const showGuide = ref(false);
  140. const getDetail = async () => {
  141. try {
  142. const res = await queryCourseware({
  143. coursewareDetailKnowledgeId: data.detailId,
  144. subjectId: data.subjectId,
  145. pag: 1,
  146. rows: 99
  147. });
  148. const tempRows = res.data.rows || [];
  149. const temp: any = [];
  150. tempRows.forEach((row: any) => {
  151. if (!row.removeFlag) {
  152. temp.push({
  153. id: row.id,
  154. materialId: row.materialId,
  155. coverImg: row.coverImg,
  156. type: row.materialType,
  157. title: row.materialName,
  158. isCollect: !!row.favoriteFlag,
  159. isSelected: row.source === 'PLATFORM' ? true : false,
  160. content: row.content
  161. });
  162. }
  163. });
  164. data.knowledgePointList = temp;
  165. data.itemList = data.knowledgePointList.map((m: any) => {
  166. return {
  167. ...m,
  168. iframeRef: null,
  169. videoEle: null,
  170. audioEle: null,
  171. autoPlay: false, //加载完成是否自动播放
  172. isprepare: false, // 视频是否加载完成
  173. isRender: false // 是否渲染了
  174. };
  175. });
  176. setTimeout(() => {
  177. showGuide.value = true;
  178. }, 500);
  179. } catch {
  180. //
  181. }
  182. };
  183. const directionType = ref('left');
  184. const showModalBeat = ref(false);
  185. const showModalTone = ref(false);
  186. const showModalTime = ref(false);
  187. const isDragIng = ref(false);
  188. const initMoveable = async () => {
  189. if (document.querySelector('.wrap')) {
  190. const moveable = new Moveable(document.querySelector('.wrap') as any, {
  191. target: document.querySelector('#moveNPopover') as any,
  192. // If the container is null, the position is fixed. (default: parentElement(document.body))
  193. container: document.querySelector('.wrap') as any,
  194. // snappable: true,
  195. // bounds: {"left":100,"top":100,"right":100,"bottom":100},
  196. draggable: true,
  197. resizable: false,
  198. scalable: false,
  199. rotatable: false,
  200. warpable: false,
  201. pinchable: false, // ["resizable", "scalable", "rotatable"]
  202. origin: false,
  203. keepRatio: false,
  204. // Resize, Scale Events at edges.
  205. edge: false,
  206. throttleDrag: 0,
  207. throttleResize: 0,
  208. throttleScale: 0,
  209. throttleRotate: 0
  210. });
  211. moveable
  212. // .on('dragStart', ({ target, clientX, clientY }) => {
  213. // console.log('dragStart');
  214. // })
  215. .on(
  216. 'drag',
  217. ({
  218. target,
  219. // transform,
  220. left,
  221. top,
  222. right,
  223. bottom
  224. // beforeDelta,
  225. // beforeDist,
  226. // delta,
  227. // dist,
  228. // clientX,
  229. // clientY
  230. }) => {
  231. isDragIng.value = true;
  232. const subdEl = document.getElementById(
  233. `moveNPopover`
  234. ) as HTMLDivElement;
  235. // console.log(subdEl, "subdEl", "drag");
  236. const subdElStyle = getComputedStyle(subdEl, null);
  237. const RectInfo = {
  238. left: Number(subdElStyle.left.replace('px', '')),
  239. top: Number(subdElStyle.top.replace('px', '')),
  240. width: Number(subdElStyle.width.replace('px', '')),
  241. height: Number(subdElStyle.height.replace('px', ''))
  242. };
  243. const mainWidth =
  244. parseInt(
  245. window.getComputedStyle(
  246. document.querySelector('.wrap') as Element
  247. ).width
  248. ) - RectInfo.width;
  249. const mainHeight =
  250. parseInt(
  251. window.getComputedStyle(
  252. document.querySelector('.wrap') as Element
  253. ).height
  254. ) - RectInfo.height;
  255. if (left < 0) {
  256. left = 2;
  257. }
  258. if (top < 0) {
  259. top = 2;
  260. }
  261. if (right < 0) {
  262. right = 2;
  263. }
  264. if (bottom < 0) {
  265. bottom = 2;
  266. }
  267. if (left > mainWidth - 2) {
  268. left = mainWidth - 2;
  269. }
  270. if (top > mainHeight - 2) {
  271. top = mainHeight - 2;
  272. }
  273. target!.style.left = `${left}px`;
  274. target!.style.top = `${top}px`;
  275. }
  276. )
  277. .on(
  278. 'dragEnd',
  279. async ({
  280. // target, isDrag,
  281. clientX
  282. // clientY
  283. }) => {
  284. if (document.body.clientWidth / 2 - clientX > 0) {
  285. // 往左出
  286. directionType.value = 'right';
  287. } else {
  288. // 往又出
  289. directionType.value = 'left';
  290. }
  291. isDragIng.value = false;
  292. }
  293. );
  294. }
  295. };
  296. // ifram事件处理
  297. const iframeHandle = (ev: MessageEvent) => {
  298. if (ev.data?.api === 'headerTogge') {
  299. activeData.model =
  300. ev.data.show || (ev.data.playState == 'play' ? false : true);
  301. }
  302. };
  303. onMounted(() => {
  304. const query = route.query;
  305. // 先取参数,
  306. data.type = props.type || (query.type as any);
  307. data.subjectId = props.subjectId || query.subjectId;
  308. data.detailId = props.detailId || query.detailId;
  309. data.classGroupId = props.classGroupId || query.classGroupId;
  310. initMoveable();
  311. window.addEventListener('message', iframeHandle);
  312. getDetail();
  313. });
  314. const onFullScreen = () => {
  315. if (data.type === 'preview') {
  316. const el: any = document.querySelector('#app');
  317. if (el.mozRequestFullScreen) {
  318. el.mozRequestFullScreen();
  319. } else if (el.webkitRequestFullscreen) {
  320. el.webkitRequestFullscreen();
  321. } else if (el.requestFullScreen) {
  322. el.requestFullscreen();
  323. }
  324. }
  325. };
  326. const popupData = reactive({
  327. open: false,
  328. activeIndex: 0,
  329. toolOpen: false // 工具弹窗控制
  330. });
  331. const activeName = computed(() => {
  332. let name = '';
  333. data.knowledgePointList.forEach((item: any, index: number) => {
  334. if (popupData.activeIndex === index) {
  335. name = item.title;
  336. }
  337. });
  338. return name;
  339. });
  340. /**停止所有的播放 */
  341. const handleStop = () => {
  342. for (let i = 0; i < data.itemList.length; i++) {
  343. const activeItem = data.itemList[i];
  344. if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
  345. activeItem.videoEle.currentTime(0);
  346. activeItem.videoEle.pause();
  347. }
  348. if (activeItem.type === 'SONG' && activeItem.audioEle) {
  349. activeItem.audioEle.stop();
  350. }
  351. // console.log('🚀 ~ activeItem:', activeItem)
  352. // 停止曲谱的播放
  353. if (activeItem.type === 'MUSIC') {
  354. activeItem.iframeRef?.contentWindow?.postMessage(
  355. { api: 'setPlayState' },
  356. '*'
  357. );
  358. }
  359. }
  360. };
  361. // 切换素材
  362. const toggleMaterial = (itemActive: any) => {
  363. const index = data.itemList.findIndex((n: any) => n.id == itemActive);
  364. if (index > -1) {
  365. handleSwipeChange(index);
  366. }
  367. };
  368. /** 延迟收起模态框 */
  369. const setModelOpen = () => {
  370. clearTimeout(activeData.timer);
  371. message.destroyAll();
  372. activeData.timer = setTimeout(() => {
  373. activeData.model = false;
  374. Object.values(data.videoRefs).map((n: any) =>
  375. n.toggleHideControl(false)
  376. );
  377. Object.values(data.audioRefs).map((n: any) =>
  378. n.toggleHideControl(false)
  379. );
  380. }, 4000);
  381. };
  382. /** 立即收起所有的模态框 */
  383. const clearModel = () => {
  384. clearTimeout(activeData.timer);
  385. message.destroyAll();
  386. activeData.model = false;
  387. Object.values(data.videoRefs).map((n: any) =>
  388. n?.toggleHideControl(false)
  389. );
  390. Object.values(data.audioRefs).map((n: any) =>
  391. n?.toggleHideControl(false)
  392. );
  393. };
  394. const toggleModel = (type = true) => {
  395. activeData.model = type;
  396. Object.values(data.videoRefs).map((n: any) => n?.toggleHideControl(type));
  397. Object.values(data.audioRefs).map((n: any) => n?.toggleHideControl(type));
  398. };
  399. // 双击
  400. const handleDbClick = (item: any) => {
  401. if (item && item.type === 'VIDEO') {
  402. const videoEle: HTMLVideoElement = item.videoEle;
  403. if (videoEle) {
  404. if (videoEle.paused) {
  405. message.destroyAll();
  406. videoEle.play();
  407. } else {
  408. message.warning('已暂停');
  409. videoEle.pause();
  410. }
  411. }
  412. }
  413. };
  414. // 切换播放
  415. // const togglePlay = (m: any, isPlay: boolean) => {
  416. // if (isPlay) {
  417. // m.videoEle?.play();
  418. // } else {
  419. // m.videoEle?.pause();
  420. // }
  421. // };
  422. // const showIndex = ref(-4);
  423. const effectIndex = ref(3);
  424. const effects = [
  425. {
  426. prev: {
  427. transform: 'translate3d(0, 0, -800px) rotateX(180deg)'
  428. },
  429. next: {
  430. transform: 'translate3d(0, 0, -800px) rotateX(-180deg)'
  431. }
  432. },
  433. {
  434. prev: {
  435. transform: 'translate3d(-100%, 0, -800px)'
  436. },
  437. next: {
  438. transform: 'translate3d(100%, 0, -800px)'
  439. }
  440. },
  441. {
  442. prev: {
  443. transform: 'translate3d(-50%, 0, -800px) rotateY(80deg)'
  444. },
  445. next: {
  446. transform: 'translate3d(50%, 0, -800px) rotateY(-80deg)'
  447. }
  448. },
  449. {
  450. prev: {
  451. transform: 'translate3d(-100%, 0, -800px) rotateY(-120deg)'
  452. },
  453. next: {
  454. transform: 'translate3d(100%, 0, -800px) rotateY(120deg)'
  455. }
  456. },
  457. // 风车4
  458. {
  459. prev: {
  460. transform: 'translate3d(-50%, 50%, -800px) rotateZ(-14deg)',
  461. opacity: 0
  462. },
  463. next: {
  464. transform: 'translate3d(50%, 50%, -800px) rotateZ(14deg)',
  465. opacity: 0
  466. }
  467. },
  468. // 翻页5
  469. {
  470. prev: {
  471. transform: 'translateZ(-800px) rotate3d(0, -1, 0, 90deg)',
  472. opacity: 0
  473. },
  474. next: {
  475. transform: 'translateZ(-800px) rotate3d(0, 1, 0, 90deg)',
  476. opacity: 0
  477. },
  478. current: { transitionDelay: '700ms' }
  479. }
  480. ];
  481. const acitveTimer = ref();
  482. // 轮播切换
  483. const handleSwipeChange = (index: number) => {
  484. // 如果是当前正在播放 或者是视频最后一个
  485. if (popupData.activeIndex == index) return;
  486. handleStop();
  487. clearTimeout(acitveTimer.value);
  488. checkedAnimation(popupData.activeIndex, index);
  489. popupData.activeIndex = index;
  490. acitveTimer.value = setTimeout(
  491. () => {
  492. const item = data.itemList[index];
  493. if (item) {
  494. if (item.type == 'MUSIC') {
  495. activeData.model = true;
  496. }
  497. if (item.type === 'SONG') {
  498. // 自动播放下一个音频
  499. clearTimeout(activeData.timer);
  500. message.destroyAll();
  501. // item.autoPlay = false;
  502. // nextTick(() => {
  503. // item.audioEle?.onPlay();
  504. // });
  505. }
  506. if (item.type === 'VIDEO') {
  507. // 自动播放下一个视频
  508. clearTimeout(activeData.timer);
  509. message.destroyAll();
  510. nextTick(() => {
  511. if (item.error) {
  512. console.log(item, 'item error');
  513. item.videoEle?.src(item.content);
  514. item.error = false;
  515. // item.videoEle?.onPlay();
  516. }
  517. });
  518. // item.autoPlay = false;
  519. }
  520. }
  521. // requestAnimationFrame(() => {
  522. // const _effectIndex = effectIndex.value + 1;
  523. // effectIndex.value =
  524. // _effectIndex >= effects.length - 1 ? 0 : _effectIndex;
  525. // });
  526. },
  527. activeData.isAnimation ? 800 : 0
  528. );
  529. };
  530. /** 是否有转场动画 */
  531. const checkedAnimation = (index: number, nextIndex?: number) => {
  532. const item = data.itemList[index];
  533. const nextItem = data.itemList[nextIndex!];
  534. if (nextItem) {
  535. if (nextItem.knowledgePointId != item.knowledgePointId) {
  536. activeData.isAnimation = true;
  537. return;
  538. }
  539. const videoEle = item.videoEle;
  540. const nextVideo = nextItem.videoEle;
  541. if (videoEle && videoEle.duration < 8 && index < nextIndex!) {
  542. activeData.isAnimation = false;
  543. } else if (nextVideo && nextVideo.duration < 8 && index > nextIndex!) {
  544. activeData.isAnimation = false;
  545. } else {
  546. activeData.isAnimation = true;
  547. }
  548. } else {
  549. activeData.isAnimation = item?.adviseStudyTimeSecond < 8 ? false : true;
  550. }
  551. };
  552. // 上一个知识点, 下一个知识点
  553. const handlePreAndNext = (type: string) => {
  554. if (type === 'up') {
  555. handleSwipeChange(popupData.activeIndex - 1);
  556. } else {
  557. handleSwipeChange(popupData.activeIndex + 1);
  558. }
  559. };
  560. /** 弹窗关闭 */
  561. const handleClosePopup = () => {
  562. const item = data.itemList[popupData.activeIndex];
  563. if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
  564. setModelOpen();
  565. }
  566. if (item?.type == 'SONG' && !item.audioEle?.paused) {
  567. setModelOpen();
  568. }
  569. };
  570. // 监听页面键盘事件 - 上下切换
  571. document.body.addEventListener('keyup', (e: KeyboardEvent) => {
  572. // console.log(e, 'e');
  573. if (e.code === 'ArrowUp') {
  574. if (popupData.activeIndex === 0) return;
  575. handlePreAndNext('up');
  576. } else if (e.code === 'ArrowDown') {
  577. if (popupData.activeIndex === data.itemList.length - 1) return;
  578. handlePreAndNext('down');
  579. }
  580. // else if (e.code === 'Space') {
  581. // handleStop();
  582. // }
  583. });
  584. /** 教学数据 */
  585. const studyData = reactive({
  586. type: '' as ToolType,
  587. penShow: false,
  588. whiteboardShow: false
  589. });
  590. /** 打开教学工具 */
  591. const openStudyTool = (item: ToolItem) => {
  592. const activeItem = data.itemList[popupData.activeIndex];
  593. // 暂停视频和曲谱的播放
  594. if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
  595. activeItem.videoEle.pause();
  596. }
  597. if (activeItem.type === 'SONG' && activeItem.audioEle) {
  598. activeItem.audioEle.stop();
  599. }
  600. if (activeItem.type === 'MUSIC') {
  601. activeItem.iframeRef?.contentWindow?.postMessage(
  602. { api: 'setPlayState' },
  603. '*'
  604. );
  605. }
  606. clearModel();
  607. popupData.toolOpen = false;
  608. studyData.type = item.type;
  609. switch (item.type) {
  610. case 'pen':
  611. studyData.penShow = true;
  612. break;
  613. case 'whiteboard':
  614. studyData.whiteboardShow = true;
  615. }
  616. };
  617. /** 关闭教学工具 */
  618. const closeStudyTool = () => {
  619. studyData.type = 'init';
  620. toggleModel();
  621. };
  622. const startShowModal = (val: 'setTimeIcon' | 'beatIcon' | 'toneIcon') => {
  623. if (val == 'setTimeIcon') {
  624. showModalTime.value = true;
  625. }
  626. if (val == 'beatIcon') {
  627. showModalBeat.value = true;
  628. }
  629. if (val == 'toneIcon') {
  630. showModalTone.value = true;
  631. }
  632. };
  633. return () => (
  634. <div id="playContent" class={[styles.playContent, 'wrap']}>
  635. <div
  636. onClick={() => {
  637. clearTimeout(activeData.timer);
  638. activeData.model = !activeData.model;
  639. Object.values(data.videoRefs).map((n: any) =>
  640. n.toggleHideControl(activeData.model)
  641. );
  642. Object.values(data.audioRefs).map((n: any) =>
  643. n.toggleHideControl(activeData.model)
  644. );
  645. }}>
  646. <div
  647. class={styles.coursewarePlay}
  648. style={{ width: parentContainer.width }}
  649. onClick={(e: Event) => {
  650. e.stopPropagation();
  651. setModelOpen();
  652. }}>
  653. <div class={styles.wraps}>
  654. {data.itemList.map((m: any, mIndex: number) => {
  655. const isRender =
  656. m.isRender || Math.abs(popupData.activeIndex - mIndex) < 2;
  657. const isEmtry = Math.abs(popupData.activeIndex - mIndex) > 4;
  658. if (isRender) {
  659. m.isRender = true;
  660. }
  661. return isRender ? (
  662. <div
  663. key={'index' + mIndex}
  664. class={[
  665. styles.itemDiv,
  666. popupData.activeIndex === mIndex && styles.itemActive,
  667. activeData.isAnimation && styles.acitveAnimation,
  668. Math.abs(popupData.activeIndex - mIndex) < 2
  669. ? styles.show
  670. : styles.hide
  671. ]}
  672. style={
  673. mIndex < popupData.activeIndex
  674. ? effects[effectIndex.value].prev
  675. : mIndex > popupData.activeIndex
  676. ? effects[effectIndex.value].next
  677. : {}
  678. }
  679. onClick={(e: Event) => {
  680. e.stopPropagation();
  681. clearTimeout(activeData.timer);
  682. if (Date.now() - activeData.nowTime < 300) {
  683. handleDbClick(m);
  684. return;
  685. }
  686. activeData.nowTime = Date.now();
  687. activeData.timer = setTimeout(() => {
  688. activeData.model = !activeData.model;
  689. Object.values(data.videoRefs).map((n: any) =>
  690. n.toggleHideControl(activeData.model)
  691. );
  692. Object.values(data.audioRefs).map((n: any) =>
  693. n.toggleHideControl(activeData.model)
  694. );
  695. if (activeData.model) {
  696. setModelOpen();
  697. }
  698. }, 300);
  699. }}>
  700. {m.type === 'VIDEO' ? (
  701. <>
  702. <VideoPlay
  703. ref={(v: any) => (data.videoRefs[mIndex] = v)}
  704. item={m}
  705. isEmtry={isEmtry}
  706. onLoadedmetadata={(videoItem: any) => {
  707. m.videoEle = videoItem;
  708. m.isprepare = true;
  709. }}
  710. onTogglePlay={(paused: boolean) => {
  711. m.autoPlay = false;
  712. if (paused || popupData.open) {
  713. clearTimeout(activeData.timer);
  714. } else {
  715. setModelOpen();
  716. }
  717. }}
  718. onReset={() => {
  719. if (!m.videoEle?.paused) {
  720. setModelOpen();
  721. }
  722. }}
  723. onError={() => {
  724. console.log('video error');
  725. m.error = true;
  726. }}
  727. />
  728. <Transition name="van-fade">
  729. {!m.isprepare && (
  730. <div class={styles.loadWrap}>
  731. <Vue3Lottie
  732. animationData={playLoadData}></Vue3Lottie>
  733. </div>
  734. )}
  735. </Transition>
  736. </>
  737. ) : m.type === 'IMG' ? (
  738. <img src={m.content} />
  739. ) : m.type === 'SONG' ? (
  740. <AudioPay
  741. item={m}
  742. ref={(v: any) => (data.audioRefs[mIndex] = v)}
  743. onLoadedmetadata={(audioItem: any) => {
  744. m.audioEle = audioItem;
  745. m.isprepare = true;
  746. }}
  747. onTogglePlay={(paused: boolean) => {
  748. m.autoPlay = false;
  749. if (paused || popupData.open) {
  750. clearTimeout(activeData.timer);
  751. } else {
  752. setModelOpen();
  753. }
  754. }}
  755. onEnded={() => {
  756. const _index = popupData.activeIndex + 1;
  757. if (_index < data.itemList.length) {
  758. handleSwipeChange(_index);
  759. }
  760. }}
  761. onReset={() => {
  762. if (!m.audioEle?.paused) {
  763. setModelOpen();
  764. }
  765. }}
  766. />
  767. ) : (
  768. <MusicScore
  769. activeModel={activeData.model}
  770. data-vid={m.id}
  771. music={m}
  772. onSetIframe={(el: any) => {
  773. m.iframeRef = el;
  774. }}
  775. />
  776. )}
  777. </div>
  778. ) : null;
  779. })}
  780. </div>
  781. <Transition name="right">
  782. {activeData.model && (
  783. <div
  784. class={styles.rightFixedBtns}
  785. onClick={(e: Event) => {
  786. e.stopPropagation();
  787. clearTimeout(activeData.timer);
  788. }}>
  789. <div
  790. class={[
  791. styles.fullBtn,
  792. popupData.activeIndex === 0 ? styles.btnsDisabled : ''
  793. ]}
  794. onClick={() => {
  795. if (popupData.activeIndex === 0) return;
  796. handlePreAndNext('up');
  797. }}>
  798. <img src={iconUp} />
  799. </div>
  800. <div id="attent-0">
  801. <div
  802. class={[styles.fullBtn, styles.point]}
  803. onClick={() => (popupData.open = true)}>
  804. <img src={iconMenu} />
  805. </div>
  806. <div
  807. class={[
  808. styles.fullBtn,
  809. popupData.activeIndex === data.itemList.length - 1
  810. ? styles.btnsDisabled
  811. : ''
  812. ]}
  813. onClick={() => {
  814. if (popupData.activeIndex === data.itemList.length - 1)
  815. return;
  816. handlePreAndNext('down');
  817. }}>
  818. <img src={iconDown} />
  819. </div>
  820. </div>
  821. </div>
  822. )}
  823. </Transition>
  824. </div>
  825. </div>
  826. <div
  827. style={{ transform: activeData.model ? '' : 'translateY(-100%)' }}
  828. class={styles.headerContainer}
  829. // ref={headeRef}
  830. >
  831. {/* <div class={styles.backBtn} onClick={() => goback()}>
  832. <Icon name={iconBack} />
  833. 返回
  834. </div> */}
  835. <div class={styles.menu}>{activeName.value}</div>
  836. </div>
  837. {/* 布置作业按钮 */}
  838. {data.type !== 'preview' ? (
  839. <div
  840. class={[
  841. styles.assignHomeworkClose,
  842. activeData.model ? '' : styles.sectionAnimateUp
  843. ]}
  844. onClick={async () => {
  845. data.removeVisiable = true;
  846. data.removeTitle = '结束课程';
  847. data.removeContent = '请确认是否结束课程?';
  848. }}>
  849. <img src={iconClose} />
  850. </div>
  851. ) : (
  852. ''
  853. )}
  854. <div
  855. id="attent-3"
  856. class={[
  857. styles.assignHomework,
  858. data.type !== 'preview' ? styles.isClose : '',
  859. activeData.model ? '' : styles.sectionAnimateUp
  860. ]}
  861. onClick={async () => {
  862. if (data.type === 'preview') {
  863. handleStop();
  864. data.removeVisiable = true;
  865. data.removeTitle = '结束预览';
  866. data.removeContent = '请确认是否结束预览?';
  867. // onFullScreen();
  868. } else {
  869. const res = await lessonPreTrainingPage({
  870. coursewareKnowledgeDetailId: data.detailId,
  871. subjectId: data.subjectId,
  872. page: 1,
  873. rows: 99
  874. });
  875. console.log(res, 'res');
  876. if (res.data.rows && res.data.rows.length) {
  877. data.modalAttendMessage = '本节课已设置课后训练,是否布置?';
  878. }
  879. data.modelAttendStatus = true;
  880. }
  881. }}>
  882. <img
  883. src={data.type === 'preview' ? iconOverPreivew : iconAssignHomework}
  884. />
  885. </div>
  886. {/* 白板 批注 */}
  887. <div
  888. class={[
  889. styles.switchDisplaySection,
  890. activeData.model ? '' : styles.sectionAnimate
  891. ]}>
  892. <NTooltip trigger="hover">
  893. {{
  894. trigger: () => (
  895. <div
  896. id="attent-1"
  897. class={styles.displayBtn}
  898. onClick={() =>
  899. openStudyTool({
  900. type: 'pen',
  901. icon: iconNote,
  902. name: '批注'
  903. })
  904. }>
  905. <img src={iconNote} />
  906. </div>
  907. ),
  908. default: () => '批注'
  909. }}
  910. </NTooltip>
  911. <NTooltip trigger="hover">
  912. {{
  913. trigger: () => (
  914. <div
  915. id="attent-2"
  916. class={styles.displayBtn}
  917. onClick={() =>
  918. openStudyTool({
  919. type: 'whiteboard',
  920. icon: iconWhiteboard,
  921. name: '白板'
  922. })
  923. }>
  924. <img src={iconWhiteboard} />
  925. </div>
  926. ),
  927. default: () => '白板'
  928. }}
  929. </NTooltip>
  930. </div>
  931. {/* 显示列表 */}
  932. <NDrawer
  933. v-model:show={popupData.open}
  934. class={styles.drawerContainer}
  935. onAfterLeave={handleClosePopup}
  936. showMask={false}>
  937. <NDrawerContent title="资源列表" closable>
  938. {data.knowledgePointList.map((item: any, index: number) => (
  939. <CardType
  940. item={item}
  941. isActive={popupData.activeIndex === index}
  942. isCollect={false}
  943. isShowCollect={false}
  944. onClick={(item: any) => {
  945. popupData.open = false;
  946. toggleMaterial(item.id);
  947. }}
  948. />
  949. ))}
  950. </NDrawerContent>
  951. </NDrawer>
  952. {/* 批注 */}
  953. {studyData.penShow && (
  954. <Pen
  955. show={studyData.type === 'pen'}
  956. type={studyData.type}
  957. close={() => closeStudyTool()}
  958. />
  959. )}
  960. {studyData.whiteboardShow && (
  961. <Pen
  962. show={studyData.type === 'whiteboard'}
  963. type={studyData.type}
  964. close={() => closeStudyTool()}
  965. />
  966. )}
  967. {/* 布置作业 */}
  968. <NModal
  969. transformOrigin="center"
  970. v-model:show={data.modelAttendStatus}
  971. preset="card"
  972. class={styles.attendClassModal}
  973. title={'课后训练'}>
  974. <div class={styles.modelAttendContent}>{data.modalAttendMessage}</div>
  975. <NSpace class={styles.modelAttendBtnGroup}>
  976. <NButton
  977. type="default"
  978. round
  979. onClick={() => {
  980. data.modelAttendStatus = false;
  981. handleStop();
  982. // if (state.application) {
  983. // emit('close');
  984. // } else {
  985. // window.close();
  986. // }
  987. data.modelAttendStatus = false;
  988. }}>
  989. 暂不布置
  990. </NButton>
  991. <NButton
  992. type="primary"
  993. round
  994. onClick={() => {
  995. data.modelTrainStatus = true;
  996. data.modelAttendStatus = false;
  997. }}>
  998. 布置
  999. </NButton>
  1000. </NSpace>
  1001. </NModal>
  1002. {/* 训练设置 */}
  1003. <NModal
  1004. transformOrigin="center"
  1005. v-model:show={data.modelTrainStatus}
  1006. preset="card"
  1007. class={[styles.attendClassModal, styles.trainClassModal]}
  1008. title={'训练设置'}>
  1009. <TrainSettings
  1010. detailId={data.detailId}
  1011. subjectId={data.subjectId}
  1012. classGroupId={data.classGroupId}
  1013. onClose={() => (data.modelTrainStatus = false)}
  1014. onConfirm={() => {
  1015. // 布置完作业之后直接关闭
  1016. // setTimeout(() => {
  1017. // handleStop();
  1018. // if (state.application) {
  1019. // emit('close');
  1020. // } else {
  1021. // window.close();
  1022. // }
  1023. // }, 1000);
  1024. data.modelTrainStatus = false;
  1025. }}
  1026. />
  1027. </NModal>
  1028. {/* <NModal transformOrigin='center'
  1029. v-model:show={data.homeworkStatus}
  1030. preset="card"
  1031. class={[styles.attendClassModal]}
  1032. closable={false}
  1033. maskClosable={false}
  1034. title={' '}>
  1035. <div class={styles.workContainer}>
  1036. <h2>作业布置成功</h2>
  1037. <p>倒</p>
  1038. </div>
  1039. </NModal> */}
  1040. {showGuide.value ? <Attentguide></Attentguide> : null}
  1041. <NPopover
  1042. raw
  1043. trigger="click"
  1044. show-arrow={false}
  1045. style="--n-box-shadow: none;"
  1046. placement={directionType.value as 'left' | 'right'}
  1047. v-slots={{
  1048. trigger: () => (
  1049. // 首页不显示工具箱
  1050. <img
  1051. // src={isDragIng.value ? dragingBoxIcon : toolbox}
  1052. src={toolbox}
  1053. id="moveNPopover"
  1054. style={{
  1055. display: ['/', '/home'].includes(route.path)
  1056. ? 'none'
  1057. : 'block'
  1058. }}
  1059. class={[
  1060. styles.toolboxImg,
  1061. 'moveNPopover',
  1062. isDragIng.value ? styles.isDragIng : ''
  1063. ]}
  1064. alt=""
  1065. />
  1066. )
  1067. }}>
  1068. <div class={styles.booxToolWrap}>
  1069. <div
  1070. class={styles.booxToolItem}
  1071. onClick={() => startShowModal('beatIcon')}>
  1072. <img src={beatIcon} alt="" />
  1073. 节拍器
  1074. </div>
  1075. <div
  1076. class={styles.booxToolItem}
  1077. onClick={() => startShowModal('toneIcon')}>
  1078. <img src={toneIcon} alt="" />
  1079. 调音器
  1080. </div>
  1081. <div
  1082. class={styles.booxToolItem}
  1083. onClick={() => startShowModal('setTimeIcon')}>
  1084. <img src={setTimeIcon} alt="" />
  1085. 计时器
  1086. </div>
  1087. </div>
  1088. </NPopover>
  1089. <NModal
  1090. transformOrigin="center"
  1091. class={['modalTitle background']}
  1092. title={'节拍器'}
  1093. preset="card"
  1094. v-model:show={showModalBeat.value}
  1095. style={{ width: '687px' }}>
  1096. <div class={styles.modeWrap}>
  1097. <iframe
  1098. src={`${vaildUrl()}/metronome/?id=${new Date().getTime()}`}
  1099. scrolling="no"
  1100. frameborder="0"
  1101. width="100%"
  1102. height={'650px'}></iframe>
  1103. </div>
  1104. </NModal>
  1105. <NModal
  1106. transformOrigin="center"
  1107. class={['background']}
  1108. v-model:show={showModalTone.value}>
  1109. <div>
  1110. <PlaceholderTone
  1111. onClose={() => {
  1112. showModalTone.value = false;
  1113. }}></PlaceholderTone>
  1114. </div>
  1115. </NModal>
  1116. <NModal
  1117. transformOrigin="center"
  1118. v-model:show={showModalTime.value}
  1119. class={['modalTitle background']}
  1120. title={'计时器'}
  1121. preset="card"
  1122. style={{ width: px2vw(772) }}>
  1123. <div>
  1124. <TimerMeter></TimerMeter>
  1125. </div>
  1126. </NModal>
  1127. <NModal
  1128. transformOrigin="center"
  1129. v-model:show={data.removeVisiable}
  1130. preset="card"
  1131. class={['modalTitle', styles.removeVisiable]}
  1132. title={data.removeTitle}>
  1133. <div class={styles.studentRemove}>
  1134. <p>{data.removeContent}</p>
  1135. <NSpace class={styles.btnGroupModal} justify="center">
  1136. <NButton
  1137. round
  1138. type="primary"
  1139. onClick={() => {
  1140. //
  1141. if (state.application) {
  1142. document.exitFullscreen
  1143. ? document.exitFullscreen()
  1144. : document.mozCancelFullScreen
  1145. ? document.mozCancelFullScreen()
  1146. : document.webkitExitFullscreen
  1147. ? document.webkitExitFullscreen()
  1148. : ''
  1149. emit('close');
  1150. } else {
  1151. window.close();
  1152. }
  1153. }}>
  1154. 确定
  1155. </NButton>
  1156. <NButton round onClick={() => (data.removeVisiable = false)}>
  1157. 取消
  1158. </NButton>
  1159. </NSpace>
  1160. </div>
  1161. </NModal>
  1162. </div>
  1163. );
  1164. }
  1165. });