index.tsx 38 KB

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