index.tsx 45 KB

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