index.tsx 32 KB

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