index.tsx 60 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792
  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 { setLogin, state } from '@/state'
  20. import { useRoute, useRouter } 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, iconTouping, iconMenu, iconCourseType, iconSearch } 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. import PointsSearch from './component/points-search'
  46. export default defineComponent({
  47. name: 'CoursewarePlay',
  48. setup() {
  49. const pageVisibility = usePageVisibility()
  50. const browserInfo = browser()
  51. const { isOnline } = useNetwork()
  52. /** 页面显示和隐藏 */
  53. watch(
  54. () => pageVisibility.value,
  55. (value) => {
  56. if (value == 'hidden') {
  57. handleStop()
  58. }
  59. if (value === "visible") {
  60. if(data.source === 'search') {
  61. getUserInfo()
  62. }
  63. }
  64. }
  65. )
  66. const getUserInfo = async () => {
  67. try {
  68. // 重新初始化用户信息
  69. const userCash = await request.get(state.platformApi + '/user/getUserInfo')
  70. setLogin(userCash.data)
  71. } catch {
  72. //
  73. }
  74. }
  75. /** 设置播放容器 16:9 */
  76. const parentContainer = reactive({
  77. width: '100vw'
  78. })
  79. // const setContainer = () => {
  80. // const min = Math.min(screen.width, screen.height)
  81. // const max = Math.max(screen.width, screen.height)
  82. // const width = min * (16 / 9)
  83. // if (width > max) {
  84. // parentContainer.width = '100vw'
  85. // return
  86. // } else {
  87. // parentContainer.width = width + 'px'
  88. // }
  89. // }
  90. const handleInit = (type = 0) => {
  91. //设置容器16:9
  92. // setContainer()
  93. // 横屏
  94. postMessage(
  95. {
  96. api: 'setRequestedOrientation',
  97. content: {
  98. orientation: type
  99. }
  100. },
  101. () => {
  102. console.log(234)
  103. }
  104. )
  105. // 头,包括返回箭头
  106. // postMessage({
  107. // api: 'setTitleBarVisibility',
  108. // content: {
  109. // status: type
  110. // }
  111. // })
  112. // 安卓的状态栏
  113. postMessage({
  114. api: 'setStatusBarVisibility',
  115. content: {
  116. isVisibility: type
  117. }
  118. })
  119. // 进入页面设置常量
  120. postMessage({
  121. api: 'keepScreenLongLight',
  122. content: {
  123. isOpenLight: type ? true : false
  124. }
  125. })
  126. }
  127. handleInit()
  128. onUnmounted(() => {
  129. handleInit(1)
  130. window.removeEventListener('message', iframeHandle)
  131. })
  132. const route = useRoute()
  133. const router = useRouter()
  134. const headeRef = ref()
  135. const isCurrentCoursewareMenu = shallowRef(true) // 是否为当前选的课程类型
  136. const detailTempSearchList = shallowRef<any[]>()
  137. const detailList = shallowRef<any[]>()// 搜索来的所有数据
  138. const data = reactive({
  139. source: route.query.source as any, // 来源 search 搜索
  140. searchLoading: false, // 搜索加载状态
  141. search: route.query.search as any, // 默认的搜索条件 -
  142. searchTemp: route.query.search as any, // 默认的搜索条件 -
  143. isSearch: route.query.source === "search" ? true : false, // 是否搜索
  144. currentId: route.query.id as any,
  145. lessonId: null as any,
  146. detail: null as any,
  147. knowledgePointList: [] as any,
  148. itemList: [] as any,
  149. lookVideoDataList: [] as any, // 观看视频统计数据
  150. showHead: true,
  151. isCourse: false,
  152. isRecordPlay: false,
  153. videoRefs: {},
  154. refLevelList: [] as any,
  155. videoState: 'init' as 'init' | 'play',
  156. videoItemRef: null as any,
  157. animationState: 'start' as 'start' | 'end',
  158. disableScreenRecordingFlag: '0' // 是否禁止录屏
  159. })
  160. const activeData = reactive({
  161. isAutoPlay: true, // 是否自动播放
  162. nowTime: 0,
  163. model: true, // 遮罩
  164. isAnimation: true, // 是否动画
  165. videoBtns: true, // 视频
  166. currentTime: 0,
  167. duration: 0,
  168. timer: null as any,
  169. item: null as any
  170. })
  171. // 获取缓存路径
  172. const getCacheFilePath = async (material: any) => {
  173. const res = await promisefiyPostMessage({
  174. api: 'getCourseFilePath',
  175. content: {
  176. url: material.content,
  177. localPath: '',
  178. materialId: material.materialId,
  179. updateTime: material.updateTime,
  180. type: material.type // SONG VIDEO IMAGE
  181. }
  182. })
  183. // console.log('缓存路径返回', res)
  184. return res
  185. }
  186. // 获取当前课程是否签退
  187. const getCourseSchedule = async () => {
  188. if (!route.query.courseId) return
  189. try {
  190. const res = await request.get(
  191. `${state.platformApi}/courseSchedule/detail/${route.query.courseId}`,
  192. {
  193. hideLoading: true
  194. }
  195. )
  196. if (res?.data) {
  197. data.isCourse =
  198. res.data.status === 'ING' && state.platformType == 'TEACHER' ? true : false
  199. // data.isRecordPlay = Date.now() > dayjs(res.data.startTime).valueOf()
  200. }
  201. } catch (e) {
  202. console.log(e)
  203. }
  204. }
  205. const getTempList = async (materialList: any, name: any) => {
  206. const list: any = []
  207. // const browserInfo = browser()
  208. for (let j = 0; j < materialList.length; j++) {
  209. const material = materialList[j]
  210. //请求本地缓存
  211. // if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(material.type)) {
  212. // const localData = await getCacheFilePath(material)
  213. // if (localData?.content?.localPath) {
  214. // material.url = material.content
  215. // material.content = localData.content.localPath
  216. // }
  217. // }
  218. const videoData = data.lookVideoDataList.find(
  219. (i: any) => i.materialId === material.materialId
  220. )
  221. material.moreTime = videoData?.videoBrowseData ? JSON.parse(videoData.videoBrowseData) : []
  222. material.videoTime = videoData?.videoTime || 0 // 视频时长
  223. material.iframeRef = null
  224. material.videoEle = null
  225. material.tabName = name
  226. material.autoPlay = false //加载完成是否自动播放
  227. material.isprepare = false // 视频是否加载完成
  228. material.isRender = false // 是否渲染了
  229. list.push(material)
  230. // list.push({
  231. // ...material,
  232. // moreTime: videoData?.videoBrowseData ? JSON.parse(videoData.videoBrowseData) : [],
  233. // videoTime: videoData?.videoTime || 0, // 视频时长
  234. // iframeRef: null,
  235. // videoEle: null,
  236. // tabName: name,
  237. // autoPlay: false, //加载完成是否自动播放
  238. // isprepare: false, // 视频是否加载完成
  239. // isRender: false // 是否渲染了
  240. // })
  241. }
  242. return list
  243. }
  244. const getItemList = async () => {
  245. const list: any = []
  246. for (let i = 0; i < data.knowledgePointList.length; i++) {
  247. const item = data.knowledgePointList[i]
  248. if (item.materialList && item.materialList.length > 0) {
  249. const tempList = await getTempList(item.materialList, item.name)
  250. list.push(...tempList)
  251. }
  252. // 第二层级
  253. if (item.children && item.children.length > 0) {
  254. const childrenList = item.children || []
  255. for (let j = 0; j < childrenList.length; j++) {
  256. const childItem = childrenList[j]
  257. const tempList = await getTempList(childItem.materialList, childItem.name)
  258. list.push(...tempList)
  259. }
  260. }
  261. }
  262. // console.log(list, 'list')
  263. let _firstIndex = list.findIndex(
  264. (n: any) =>
  265. n.knowledgePointMaterialRelationId == route.query.kId || n.materialId == route.query.kId
  266. )
  267. _firstIndex = _firstIndex > -1 ? _firstIndex : 0
  268. const item = list[_firstIndex]
  269. // console.log(_firstIndex, '_firstIndex', route.query.kId, 'route.query.kId', item)
  270. // 是否自动播放
  271. if (activeData.isAutoPlay) {
  272. item.autoPlay = true
  273. }
  274. popupData.activeIndex = _firstIndex
  275. popupData.playIndex = _firstIndex
  276. popupData.tabName = item.tabName
  277. popupData.tabActive = item.knowledgePointId
  278. popupData.itemActive = item.id
  279. popupData.itemName = item.name
  280. nextTick(() => {
  281. data.itemList = list
  282. getCurrentItemCatch(popupData.activeIndex) // 获取当前元素的缓存
  283. checkedAnimation(popupData.activeIndex)
  284. postMessage({
  285. api: 'courseLoading',
  286. content: {
  287. show: false,
  288. type: 'fullscreen'
  289. }
  290. })
  291. //检测是否录屏
  292. if (data.disableScreenRecordingFlag === '1') {
  293. handleLimitScreenRecord()
  294. }
  295. setTimeout(() => {
  296. data.animationState = 'end'
  297. }, 500)
  298. })
  299. }
  300. /** 获取当前元素的缓存 */
  301. const getCurrentItemCatch = async (index: number) => {
  302. const item = data.itemList[index]
  303. if (browserInfo.isApp && ['VIDEO', 'IMG'].includes(item.typeCode) && !item.isReadCatch) {
  304. const localData: any = await getCacheFilePath(item)
  305. item.isReadCatch = true
  306. if (localData?.content?.localPath) {
  307. item.url = item.content
  308. item.content = localData.content.localPath
  309. }
  310. console.log('加载了缓存')
  311. }
  312. }
  313. const getDetail = async (id?: any) => {
  314. try {
  315. const res: any = await request.get(
  316. state.platformApi + `/lessonCoursewareDetail/detail/${id || route.query.id}`,
  317. {
  318. hideLoading: true
  319. }
  320. )
  321. const result = res.data || {}
  322. result.lessonTargetDesc = result.lessonTargetDesc ? result.lessonTargetDesc.replace(/\n/g, "<br />") : ""
  323. data.detail = result;
  324. data.lessonId = result.lessonCoursewareId
  325. if (res?.data?.lockFlag) {
  326. postMessage({
  327. api: 'courseLoading',
  328. content: {
  329. show: false,
  330. type: 'fullscreen'
  331. }
  332. })
  333. showDialog({
  334. title: '温馨提示',
  335. message: '课件已锁定'
  336. }).then(() => {
  337. goback()
  338. })
  339. return
  340. }
  341. if (Array.isArray(res?.data?.knowledgePointList)) {
  342. let index = 0
  343. data.knowledgePointList = res.data.knowledgePointList.map((n: any) => {
  344. if (Array.isArray(n.materialList)) {
  345. n.materialList = n.materialList.map((item: any) => {
  346. index++
  347. const materialRefs = item.materialRefs ? item.materialRefs : []
  348. const materialMusicId = materialRefs.length > 0 ? materialRefs[0].resourceId : null
  349. return {
  350. ...item,
  351. materialMusicId,
  352. knowledgePointId: [item.knowledgePointId],
  353. materialId: item.id,
  354. id: index + ''
  355. }
  356. })
  357. }
  358. if (Array.isArray(n.children)) {
  359. n.children = n.children.map((cn: any) => {
  360. cn.materialList = cn.materialList.map((item: any) => {
  361. index++
  362. const materialRefs = item.materialRefs ? item.materialRefs : []
  363. const materialMusicId =
  364. materialRefs.length > 0 ? materialRefs[0].resourceId : null
  365. return {
  366. ...item,
  367. materialMusicId,
  368. knowledgePointId: [n.id, item.knowledgePointId],
  369. materialId: item.id,
  370. id: index + ''
  371. }
  372. })
  373. return cn
  374. })
  375. }
  376. return n
  377. })
  378. getItemList()
  379. }
  380. return true
  381. } catch (error) {
  382. console.log(error)
  383. }
  384. }
  385. const getSearchItemList = async (knowledgePointList: any[]) => {
  386. const list: any = [];
  387. for (let i = 0; i < knowledgePointList.length; i++) {
  388. const item = knowledgePointList[i];
  389. if (item.materialList && item.materialList.length > 0) {
  390. const tempList = await getTempList(item.materialList, item.name);
  391. list.push(...tempList);
  392. }
  393. // 第二层级
  394. if (item.children && item.children.length > 0) {
  395. const childrenList = item.children || [];
  396. for (let j = 0; j < childrenList.length; j++) {
  397. const childItem = childrenList[j];
  398. const tempList = await getTempList(
  399. childItem.materialList,
  400. childItem.name
  401. );
  402. list.push(...tempList);
  403. }
  404. }
  405. }
  406. return list
  407. };
  408. /** 从搜索页面来的 */
  409. const getSearchDetail = async (params: { type?: string, id?: any, search?: string }) => {
  410. data.searchLoading = true
  411. try {
  412. const res = await request.post(
  413. state.platformApi +
  414. `/courseSchedule/myCoursewareDetail/${params.id || route.query.lessonId}`,
  415. {
  416. hideLoading: true,
  417. requestType: "form",
  418. data: {
  419. detailFlag: "1",
  420. searchFlag: true,
  421. search: params.search
  422. }
  423. }
  424. );
  425. const result = res.data || []
  426. const allList: any[] = []
  427. for(let i = 0; i < result.length; i++) {
  428. const itemResult = result[i];
  429. itemResult.name = itemResult.coursewareDetailName;
  430. itemResult.id = itemResult.lessonCoursewareDetailId;
  431. itemResult.lessonTargetDesc = itemResult.lessonTargetDesc ? itemResult.lessonTargetDesc.replace(/\n/g, "<br />") : ""
  432. if (Array.isArray(itemResult?.knowledgePointList)) {
  433. let index = 0;
  434. itemResult.children = itemResult.knowledgePointList.map(
  435. (n: any) => {
  436. if (Array.isArray(n.materialList)) {
  437. n.materialList = n.materialList.map((item: any) => {
  438. index++;
  439. const materialRefs = item.materialRefs
  440. ? item.materialRefs
  441. : [];
  442. const materialMusicId =
  443. materialRefs.length > 0
  444. ? materialRefs[0].resourceId
  445. : null;
  446. const useStatus = materialRefs.length > 0
  447. ? materialRefs[0]?.extend?.useStatus : null
  448. return {
  449. ...item,
  450. materialMusicId,
  451. content: item.content,
  452. coursewareDetailId: itemResult.lessonCoursewareDetailId,
  453. lessonCoursewareDetailId: itemResult.lessonCoursewareDetailId,
  454. knowledgePointId: [itemResult.lessonCoursewareDetailId, item.knowledgePointId],
  455. materialId: item.id,
  456. id: (i * 1000 + '') + index + ''
  457. };
  458. });
  459. }
  460. if (Array.isArray(n.children)) {
  461. n.children = n.children.map((cn: any) => {
  462. cn.materialList = cn.materialList.map((item: any) => {
  463. index++;
  464. const materialRefs = item.materialRefs
  465. ? item.materialRefs
  466. : [];
  467. const materialMusicId =
  468. materialRefs.length > 0
  469. ? materialRefs[0].resourceId
  470. : null;
  471. const useStatus = materialRefs.length > 0
  472. ? materialRefs[0]?.extend?.useStatus : null
  473. return {
  474. ...item,
  475. materialMusicId,
  476. coursewareDetailId: itemResult.lessonCoursewareDetailId,
  477. lessonCoursewareDetailId: itemResult.lessonCoursewareDetailId,
  478. content: item.content,
  479. knowledgePointId: [itemResult.lessonCoursewareDetailId, n.id, item.knowledgePointId],
  480. materialId: item.id,
  481. id: (i * 1000 + '') + index + ''
  482. };
  483. });
  484. return cn;
  485. });
  486. }
  487. return n;
  488. }
  489. );
  490. itemResult.knowledgePointList = null // 去掉不要的
  491. itemResult.list = await getSearchItemList(itemResult.children);
  492. allList.push(...itemResult.list)
  493. }
  494. }
  495. if(data.source !== 'search') {
  496. if(params.type === "pointSearch") {
  497. detailTempSearchList.value = result
  498. popupData.tempTabActive = allList.length > 0 ? allList[0].knowledgePointId : []
  499. popupData.tempItemActive = "-1"
  500. data.searchTemp = params.search
  501. return
  502. }
  503. detailList.value = result
  504. detailTempSearchList.value = result
  505. return
  506. }
  507. if(params.type === 'pointSearch') {
  508. detailTempSearchList.value = result
  509. // 初始化选中的数据 临时
  510. popupData.tempTabActive = allList.length > 0 ? allList[0].knowledgePointId : []
  511. popupData.tempItemActive = "-1"
  512. data.searchTemp = params.search
  513. return
  514. }
  515. detailList.value = result
  516. detailTempSearchList.value = result
  517. if(!params.type) {
  518. let _firstIndex = allList.findIndex(
  519. (n: any) =>
  520. n.knowledgePointMaterialRelationId == route.query.kId ||
  521. n.materialId == route.query.kId
  522. );
  523. _firstIndex = _firstIndex > -1 ? _firstIndex : 0;
  524. const item = allList[_firstIndex];
  525. // console.log(item, 'item')
  526. // console.log(_firstIndex, '_firstIndex', route.query.kId, 'route.query.kId', item)
  527. // 是否自动播放
  528. if (activeData.isAutoPlay) {
  529. item.autoPlay = true;
  530. }
  531. popupData.activeIndex = _firstIndex;
  532. popupData.playIndex = _firstIndex;
  533. popupData.tabName = item.tabName;
  534. popupData.tabActive = item.knowledgePointId;
  535. popupData.itemActive = item.id;
  536. popupData.itemName = item.name;
  537. data.detail = detailList.value?.find((child: any) => child.lessonCoursewareDetailId === item.lessonCoursewareDetailId)
  538. }
  539. nextTick(() => {
  540. data.itemList = allList;
  541. getCurrentItemCatch(popupData.activeIndex) // 获取当前元素的缓存
  542. checkedAnimation(popupData.activeIndex);
  543. postMessage({
  544. api: 'courseLoading',
  545. content: {
  546. show: false,
  547. type: 'fullscreen'
  548. }
  549. });
  550. if (data.disableScreenRecordingFlag === '1') {
  551. // 检测是否录屏
  552. handleLimitScreenRecord();
  553. }
  554. setTimeout(() => {
  555. data.animationState = 'end';
  556. }, 500);
  557. });
  558. return true
  559. } catch (error) {
  560. console.log(error);
  561. } finally {
  562. data.searchLoading = false
  563. }
  564. }
  565. const onTitleTip = (type: "phaseGoals" | "checkItem", text: string) => {
  566. handleStop()
  567. popupData.pointOpen = true
  568. popupData.pointContent = text
  569. if(type === "checkItem") {
  570. popupData.pointTitle = '检查事项'
  571. } else if(type === "phaseGoals") {
  572. popupData.pointTitle = '阶段目标'
  573. }
  574. }
  575. // ifram事件处理
  576. const iframeHandle = (ev: MessageEvent) => {
  577. if (ev.data?.api === 'headerTogge') {
  578. activeData.model = ev.data.show || (ev.data.playState == 'play' ? false : true)
  579. }
  580. }
  581. // 获取学生观看数据
  582. const getLookVideoData = async () => {
  583. try {
  584. const res = await request.get(
  585. state.platformApi + `/studentCoursewareMaterialRelation/findByDetailId`,
  586. {
  587. hideLoading: true,
  588. params: {
  589. lessonCoursewareDetailId: route.query.id
  590. }
  591. }
  592. )
  593. data.lookVideoDataList = res.data || [] // 视频播放数据
  594. } catch {
  595. //
  596. }
  597. }
  598. // 切换播放
  599. const togglePlay = (m: any, isPlay: boolean) => {
  600. if (isPlay) {
  601. m.videoEle?.play()
  602. } else {
  603. m.videoEle?.pause()
  604. }
  605. }
  606. let timers: any = null
  607. const checkVideoPlay = () => {
  608. const activeVideoRef = data.videoItemRef?.getPlyrRef()
  609. if (activeVideoRef) {
  610. timers = setInterval(() => {
  611. if (!activeVideoRef.paused()) {
  612. activeVideoRef.pause()
  613. clearInterval(timers)
  614. }
  615. activeVideoRef.pause()
  616. }, 100)
  617. }
  618. setTimeout(() => {
  619. clearInterval(timers)
  620. }, 3000)
  621. }
  622. //录屏时间触发
  623. const handleLimitScreenRecord = async () => {
  624. const result = await promisefiyPostMessage({
  625. api: 'getDeviceStatus',
  626. content: { type: 'video' }
  627. })
  628. const { status } = result?.content || {}
  629. if (status == '1') {
  630. data.itemList.forEach((item: any) => (item.autoPlay = false))
  631. handleStop()
  632. // 处理事件 - 事件事件后加载的
  633. checkVideoPlay()
  634. showDialog({
  635. title: '温馨提示',
  636. message: '课件内容请勿录屏',
  637. beforeClose: () => {
  638. return new Promise((resolve) => {
  639. promisefiyPostMessage({
  640. api: 'getDeviceStatus',
  641. content: { type: 'video' }
  642. }).then((res: any) => {
  643. const content = res.content
  644. if (content?.status == '1') {
  645. const activeItem = data.itemList[popupData.activeIndex]
  646. togglePlay(activeItem, false)
  647. resolve(false)
  648. } else {
  649. const activeItem = data.itemList[popupData.activeIndex]
  650. togglePlay(activeItem, true)
  651. resolve(true)
  652. }
  653. })
  654. })
  655. }
  656. })
  657. }
  658. }
  659. // 获取禁止录屏
  660. const sysParamConfig = async () => {
  661. try {
  662. const res = await request.get(`${state.platformApi}/sysParamConfig/queryByParamName`, {
  663. params: {
  664. paramName: 'disable_screen_recording_flag'
  665. }
  666. })
  667. data.disableScreenRecordingFlag = res.data.paramValue || ''
  668. } catch {
  669. //
  670. }
  671. }
  672. const getRefLevel = async (id?: any) => {
  673. try {
  674. const res = await request.post(state.platformApi + '/lessonCoursewareDetail/refLevel', {
  675. data: {
  676. lessonCoursewareDetailId: id || route.query.id,
  677. courseScheduleId: route.query.courseId as any
  678. }
  679. })
  680. data.refLevelList = res.data || []
  681. return true
  682. } catch {
  683. //
  684. }
  685. }
  686. onMounted(async () => {
  687. await sysParamConfig()
  688. if (state.platformType === 'STUDENT') {
  689. await getLookVideoData()
  690. }
  691. if(data.source === 'search') {
  692. await getSearchDetail({search: data.search})
  693. } else {
  694. // 只有老师有 课程类型 切换
  695. if(state.platformType === "TEACHER") {
  696. await getRefLevel()
  697. }
  698. await getDetail()
  699. if(state.platformType !== "SCHOOL") {
  700. data.lessonId && await getSearchDetail({search: data.search, id: data.lessonId})
  701. }
  702. }
  703. // console.log(data.detail, "data.detail");
  704. // const hasFree = String(data.detail?.accessScope) === '0'
  705. // if (!hasFree) {
  706. // const hasVip = handleCheckVip()
  707. // if (!hasVip) {
  708. // nextTick(() => {
  709. // postMessage({
  710. // api: 'courseLoading',
  711. // content: {
  712. // show: false,
  713. // type: 'fullscreen'
  714. // }
  715. // })
  716. // })
  717. // return
  718. // }
  719. // }
  720. getCourseSchedule()
  721. window.addEventListener('message', iframeHandle)
  722. if (data.disableScreenRecordingFlag === '1') {
  723. //禁止录屏 ios
  724. listenerMessage('setVideoPlayer', (result) => {
  725. if (result?.content?.status == 'pause') {
  726. handleLimitScreenRecord()
  727. }
  728. })
  729. // 安卓
  730. postMessage({
  731. api: 'limitScreenRecord',
  732. content: {
  733. type: 1
  734. }
  735. })
  736. }
  737. })
  738. onBeforeUnmount(() => {
  739. postMessage({
  740. api: 'limitScreenRecord',
  741. content: {
  742. type: 0
  743. }
  744. })
  745. })
  746. const playRef = ref()
  747. // 返回
  748. const goback = () => {
  749. try {
  750. playRef.value?.handleOut()
  751. } catch (error) {
  752. console.log(error)
  753. }
  754. postMessage({ api: 'goBack' })
  755. }
  756. const popupData = reactive({
  757. pointOpen: false,
  758. pointContent: "",
  759. pointTitle: "",
  760. coursewareOpen: false,
  761. open: false,
  762. activeIndex: 0,
  763. playIndex: 0,
  764. tempTabActive: '', // 临时选中
  765. tempItemActive: "", // 临时编号
  766. tabActive: '',
  767. tabName: '',
  768. itemActive: '',
  769. itemName: '',
  770. guideOpen: false,
  771. toolOpen: false // 工具弹窗控制
  772. })
  773. const stopVideo = (el: HTMLVideoElement) => {
  774. return new Promise((resolve) => {
  775. if (el.paused) return resolve(true)
  776. el.onpause = () => {
  777. console.log('暂停')
  778. resolve(true)
  779. }
  780. el.pause()
  781. })
  782. }
  783. /**停止所有的播放 */
  784. const handleStop = () => {
  785. for (let i = 0; i < data.itemList.length; i++) {
  786. const activeItem = data.itemList[i]
  787. if (activeItem.type === 'VIDEO') {
  788. // activeItem.videoEle?.currentTime(0)
  789. activeItem.videoEle?.pause()
  790. // activeItem.videoEle?.stop()
  791. }
  792. // 停止曲谱的播放
  793. if (activeItem.type === 'SONG') {
  794. activeItem.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
  795. }
  796. }
  797. console.log('视频暂停完成')
  798. data.itemList.forEach((item: any) => {
  799. if (item.type === 'SONG') {
  800. item.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
  801. }
  802. })
  803. }
  804. // 切换素材
  805. const toggleMaterial = (itemActive: any) => {
  806. const index = data.itemList.findIndex((n: any) => n.id == itemActive)
  807. if (index > -1) {
  808. handleSwipeChange(index)
  809. }
  810. }
  811. /** 延迟收起模态框 */
  812. const setModelOpen = () => {
  813. clearTimeout(activeData.timer)
  814. closeToast()
  815. activeData.timer = setTimeout(() => {
  816. activeData.model = false
  817. }, 4000)
  818. }
  819. /** 立即收起所有的模态框 */
  820. const clearModel = () => {
  821. clearTimeout(activeData.timer)
  822. closeToast()
  823. activeData.model = false
  824. }
  825. const toggleModel = (type = true) => {
  826. activeData.model = type
  827. }
  828. // 去点名,签退
  829. const gotoRollCall = (pageTag: string) => {
  830. postMessage({
  831. api: 'open_app_page',
  832. content: {
  833. action: 'app',
  834. pageTag: pageTag,
  835. url: '',
  836. params: JSON.stringify({ courseId: route.query.courseId })
  837. }
  838. })
  839. }
  840. // 双击
  841. const handleDbClick = () => {
  842. if (activeVideoItem.value.type === 'VIDEO') {
  843. const activeVideoRef = data.videoItemRef?.getPlyrRef()
  844. if (activeVideoRef) {
  845. if (activeVideoRef.paused()) {
  846. activeVideoRef.play()
  847. } else {
  848. activeVideoRef.pause()
  849. showToast('已暂停')
  850. }
  851. }
  852. }
  853. }
  854. const effectIndex = ref(0)
  855. const effects = [
  856. {
  857. prev: {
  858. transform: 'translate3d(0, 0, -800px) rotateX(180deg)'
  859. },
  860. next: {
  861. transform: 'translate3d(0, 0, -800px) rotateX(-180deg)'
  862. }
  863. },
  864. {
  865. prev: {
  866. transform: 'translate3d(-100%, 0, -800px)'
  867. },
  868. next: {
  869. transform: 'translate3d(100%, 0, -800px)'
  870. }
  871. },
  872. {
  873. prev: {
  874. transform: 'translate3d(-50%, 0, -800px) rotateY(80deg)'
  875. },
  876. next: {
  877. transform: 'translate3d(50%, 0, -800px) rotateY(-80deg)'
  878. }
  879. },
  880. {
  881. prev: {
  882. transform: 'translate3d(-100%, 0, -800px) rotateY(-120deg)'
  883. },
  884. next: {
  885. transform: 'translate3d(100%, 0, -800px) rotateY(120deg)'
  886. }
  887. },
  888. // 风车4
  889. {
  890. prev: {
  891. transform: 'translate3d(-50%, 50%, -800px) rotateZ(-14deg)',
  892. opacity: 0
  893. },
  894. next: {
  895. transform: 'translate3d(50%, 50%, -800px) rotateZ(14deg)',
  896. opacity: 0
  897. }
  898. },
  899. // 翻页5
  900. {
  901. prev: {
  902. transform: 'translateZ(-800px) rotate3d(0, -1, 0, 90deg)',
  903. opacity: 0
  904. },
  905. next: {
  906. transform: 'translateZ(-800px) rotate3d(0, 1, 0, 90deg)',
  907. opacity: 0
  908. },
  909. current: { transitionDelay: '700ms' }
  910. }
  911. ]
  912. const acitveTimer = ref()
  913. // 轮播切换
  914. const handleSwipeChange = async (index: number) => {
  915. if(data.source === 'search') {
  916. const item = data.itemList[index];
  917. console.log(item, detailList.value, "value");
  918. data.detail = detailList.value?.find((child: any) => child.lessonCoursewareDetailId === item.lessonCoursewareDetailId)
  919. popupData.tabActive = item.knowledgePointId;
  920. popupData.itemActive = item.id;
  921. popupData.itemName = item.name;
  922. popupData.tabName = item.tabName;
  923. if (item.typeCode == 'SONG') {
  924. activeData.model = true;
  925. }
  926. }
  927. // 如果是当前正在播放 或者是视频最后一个
  928. if (popupData.activeIndex == index) return
  929. await handleStop()
  930. data.animationState = 'start'
  931. data.videoState = 'init'
  932. clearTimeout(acitveTimer.value)
  933. checkedAnimation(popupData.activeIndex, index)
  934. nextTick(() => {
  935. popupData.activeIndex = index
  936. getCurrentItemCatch(index) // 获取当前元素的缓存
  937. acitveTimer.value = setTimeout(
  938. () => {
  939. popupData.playIndex = index
  940. const item = data.itemList[index]
  941. if (item) {
  942. popupData.tabActive = item.knowledgePointId
  943. popupData.itemActive = item.id
  944. popupData.itemName = item.name
  945. popupData.tabName = item.tabName
  946. if (item.type == 'SONG') {
  947. activeData.model = true
  948. }
  949. }
  950. if (item.type === 'VIDEO') {
  951. // 自动播放下一个视频
  952. clearTimeout(activeData.timer)
  953. closeToast()
  954. item.autoPlay = true
  955. // console.log(item, 'item')
  956. // 当视屏异常时重置链接
  957. if (item.error) {
  958. item.videoEle?.src(item.content)
  959. item.error = false
  960. }
  961. nextTick(() => {
  962. item.videoEle?.play()
  963. })
  964. }
  965. requestAnimationFrame(() => {
  966. const _effectIndex = effectIndex.value + 1
  967. effectIndex.value = _effectIndex >= effects.length - 1 ? 0 : _effectIndex
  968. })
  969. },
  970. activeData.isAnimation ? 800 : 0
  971. )
  972. })
  973. }
  974. /** 是否有转场动画 */
  975. const checkedAnimation = (index: number, nextIndex?: number) => {
  976. nextIndex = nextIndex ? nextIndex : index + 1
  977. const item = data.itemList[index]
  978. const nextItem = data.itemList[nextIndex]
  979. if (nextItem) {
  980. if (nextItem.knowledgePointId != item.knowledgePointId) {
  981. activeData.isAnimation = true
  982. return
  983. }
  984. const videoEle = item.videoEle
  985. const nextVideo = nextItem.videoEle
  986. if (videoEle && videoEle.duration < 8 && index < nextIndex) {
  987. activeData.isAnimation = false
  988. } else if (nextVideo && nextVideo.duration < 8 && index > nextIndex) {
  989. activeData.isAnimation = false
  990. } else {
  991. activeData.isAnimation = true
  992. }
  993. } else {
  994. activeData.isAnimation = item?.adviseStudyTimeSecond < 8 ? false : true
  995. }
  996. }
  997. // 上一个知识点, 下一个知识点
  998. const handlePreAndNext = (type: string) => {
  999. if(data.source === 'search') {
  1000. // 判断是否需要会员
  1001. const index = type === "up" ? popupData.activeIndex - 1 : popupData.activeIndex + 1
  1002. const item = data.itemList[index]
  1003. const parentItem = detailList.value?.find((n: any) => n.lessonCoursewareDetailId == item?.lessonCoursewareDetailId)
  1004. if(String(parentItem?.accessScope) === '1') {
  1005. const hasVip = handleCheckVip(false)
  1006. if (!hasVip) {
  1007. handleStop()
  1008. return
  1009. }
  1010. }
  1011. }
  1012. if (type === 'up') {
  1013. handleSwipeChange(popupData.activeIndex - 1)
  1014. } else {
  1015. handleSwipeChange(popupData.activeIndex + 1)
  1016. }
  1017. }
  1018. /** 弹窗关闭 */
  1019. const handleClosePopup = () => {
  1020. const item = data.itemList[popupData.activeIndex]
  1021. if (item?.type == 'VIDEO' && !item.videoEle?.paused) {
  1022. setModelOpen()
  1023. }
  1024. }
  1025. /** 教学数据 */
  1026. const studyData = reactive({
  1027. type: '' as ToolType,
  1028. penShow: false
  1029. })
  1030. /** 打开教学工具 */
  1031. const openStudyTool = (item: ToolItem) => {
  1032. const activeItem = data.itemList[popupData.activeIndex]
  1033. // 暂停视频和曲谱的播放
  1034. if (activeItem.type === 'VIDEO' && activeItem.videoEle) {
  1035. activeItem.videoEle.pause()
  1036. }
  1037. if (activeItem.type === 'SONG') {
  1038. activeItem.iframeRef?.contentWindow?.postMessage({ api: 'setPlayState' }, '*')
  1039. }
  1040. clearModel()
  1041. popupData.toolOpen = false
  1042. studyData.type = item.type
  1043. switch (item.type) {
  1044. case 'pen':
  1045. studyData.penShow = true
  1046. break
  1047. }
  1048. }
  1049. /** 关闭教学工具 */
  1050. const closeStudyTool = () => {
  1051. studyData.type = 'init'
  1052. toggleModel()
  1053. }
  1054. const activeVideoItem = computed(() => {
  1055. const item = data.itemList[popupData.activeIndex]
  1056. if (item && item.type && item.type.toLocaleUpperCase() === 'VIDEO') {
  1057. return item
  1058. }
  1059. return {}
  1060. })
  1061. let closeModelTimer: any = null
  1062. /**
  1063. * 统计视频播放时间段
  1064. */
  1065. const intervalFnRef = ref() // 定时任务
  1066. // 播放视频总时长
  1067. const videoIntervalRef = useInterval(1000, { controls: true })
  1068. videoIntervalRef.pause()
  1069. /**
  1070. * 格式化视屏播放有效时间 - 合并区间
  1071. * @param intervals [[], []]
  1072. * @example [[4, 8],[0, 4],[10, 30]]
  1073. * @returns [[0, 8], [10, 30]]
  1074. */
  1075. const formatEffectiveTime = (intervals: any[]) => {
  1076. const res: any = []
  1077. intervals.sort((a, b) => a[0] - b[0])
  1078. let prev = intervals[0]
  1079. for (let i = 1; i < intervals.length; i++) {
  1080. const cur = intervals[i]
  1081. if (prev[1] >= cur[0]) {
  1082. // 有重合
  1083. prev[1] = Math.max(cur[1], prev[1])
  1084. } else {
  1085. // 不重合,prev推入res数组
  1086. res.push(prev)
  1087. prev = cur // 更新 prev
  1088. }
  1089. }
  1090. res.push(prev)
  1091. // console.log(res, 'formatEffectiveTime')
  1092. return res
  1093. }
  1094. /**
  1095. * 获取数据有效期
  1096. * @param intervals [[], []]
  1097. * @returns 0s
  1098. */
  1099. const formatTimer = (intervals: any[]) => {
  1100. const afterIntervals = formatEffectiveTime(intervals)
  1101. let time = 0
  1102. afterIntervals.forEach((t: any) => {
  1103. time += t[1] - t[0]
  1104. })
  1105. return time
  1106. }
  1107. // 保存零时时间
  1108. // const moreTime: any = ref([]) // 多个观看时间段 已经放到列表里面了
  1109. let tempTime: any = [] // 临时存储时间
  1110. const currentTimer = useInterval(1000, { controls: true })
  1111. // 监听播放状态,
  1112. watch(
  1113. () => videoIntervalRef.isActive.value,
  1114. (newVal: boolean) => {
  1115. initVideoCount(newVal)
  1116. }
  1117. )
  1118. // 白板的批注打开时暂停播放
  1119. watch(
  1120. () => [whitePenShow.value, penShow.value],
  1121. () => {
  1122. if (whitePenShow.value || penShow.value) {
  1123. handleStop()
  1124. }
  1125. }
  1126. )
  1127. // 是否收起
  1128. watch(
  1129. () => activeData.model,
  1130. () => {
  1131. if (activeData.model) {
  1132. isPlay.value = false
  1133. } else {
  1134. isPlay.value = true
  1135. toolOpen.value = false
  1136. }
  1137. }
  1138. )
  1139. /**
  1140. * 初始化视频时长
  1141. * @param newVal 播放状态
  1142. * @param repeat 是否为定时发送的
  1143. */
  1144. const initVideoCount = (newVal: any, repeat = false) => {
  1145. // console.log('watch', forms.player.currentTime)
  1146. const activeVideoRef = data.videoItemRef?.getPlyrRef()
  1147. const initTime = deepClone(tempTime)
  1148. if (repeat) {
  1149. if (tempTime.length > 0) {
  1150. // console.log('join video', tempTime, 'initTime', initTime)
  1151. tempTime[1] = Math.floor(activeVideoRef.currentTime())
  1152. }
  1153. } else {
  1154. if (newVal) {
  1155. tempTime[0] = Math.floor(activeVideoRef.currentTime())
  1156. } else {
  1157. tempTime[1] = Math.floor(activeVideoRef.currentTime())
  1158. }
  1159. }
  1160. if (tempTime.length >= 2) {
  1161. // console.log(tempTime, 'tempTime', moreTime.value)
  1162. // 处理在短时间内的时间差 【视屏拖动,点击可能会导致时间差太大】
  1163. const diffTime = tempTime[1] - tempTime[0] - currentTimer.counter.value > 2
  1164. // 结束时间,如果 大于开始时间则清除
  1165. if (tempTime[1] >= tempTime[0] && !diffTime) {
  1166. data.itemList[popupData.activeIndex].moreTime.push(tempTime)
  1167. // moreTime.value.push(tempTime)
  1168. }
  1169. if (repeat) {
  1170. tempTime = deepClone(initTime)
  1171. } else {
  1172. tempTime = []
  1173. currentTimer.counter.value = 0
  1174. }
  1175. }
  1176. }
  1177. // 更新时间
  1178. const updateStat = async () => {
  1179. try {
  1180. const itemList = data.itemList
  1181. const params: any = []
  1182. itemList.forEach((item: any) => {
  1183. if (item.moreTime.length > 0) {
  1184. const videoBrowseData = formatEffectiveTime(item.moreTime)
  1185. const time = videoBrowseData.length > 0 ? formatTimer(videoBrowseData) : 0
  1186. const temp = {
  1187. lessonCoursewareDetailId: route.query.id || data.detail?.lessonCoursewareDetailId,
  1188. browseTime: time, // 播放时长
  1189. videoBrowseData: JSON.stringify(videoBrowseData), // 播放的数据
  1190. videoTime: item.videoTime, // 视频时长
  1191. materialId: item.materialId
  1192. }
  1193. params.push(temp)
  1194. }
  1195. })
  1196. // 只有学生才统计数据
  1197. if (params.length > 0 && state.platformType === 'STUDENT') {
  1198. await request.post(`${state.platformApi}/studentCoursewareMaterialRelation/save`, {
  1199. data: params
  1200. })
  1201. }
  1202. } catch {
  1203. //
  1204. }
  1205. }
  1206. onMounted(() => {
  1207. // 间隔多少时间同步数据
  1208. intervalFnRef.value = useIntervalFn(async () => {
  1209. // 同步数据时先进行有效时间进行保存
  1210. initVideoCount(false, true)
  1211. await updateStat()
  1212. videoIntervalRef.counter.value = 0
  1213. }, 10000)
  1214. })
  1215. /** 统计视频播放时间段 */
  1216. return () => (
  1217. <div id="playContent" class={styles.playContent}>
  1218. <div
  1219. class={styles.coursewarePlay}
  1220. style={{ width: parentContainer.width }}
  1221. onClick={() => {
  1222. clearTimeout(closeModelTimer)
  1223. clearTimeout(activeData.timer)
  1224. closeToast()
  1225. if (Date.now() - activeData.nowTime < 300) {
  1226. handleDbClick()
  1227. return
  1228. }
  1229. activeData.nowTime = Date.now()
  1230. closeModelTimer = setTimeout(() => {
  1231. activeData.model = !activeData.model
  1232. }, 300)
  1233. }}
  1234. >
  1235. <div class={styles.wraps}>
  1236. <div
  1237. style={
  1238. activeVideoItem.value.type &&
  1239. data.animationState === 'end' &&
  1240. data.videoState === 'play'
  1241. ? {
  1242. zIndex: 15,
  1243. opacity: 1
  1244. }
  1245. : { opacity: 0, zIndex: -1, pointerEvents: 'none' }
  1246. }
  1247. class={styles.itemDiv}
  1248. >
  1249. <VideoPlay
  1250. ref={(el: any) => (data.videoItemRef = el)}
  1251. item={activeVideoItem.value}
  1252. activeModel={activeData.model}
  1253. // isEmtry={isEmtry}
  1254. onPlay={() => {
  1255. data.videoState = 'play'
  1256. data.animationState = 'end'
  1257. if(whitePenShow.value || penShow.value || popupData.coursewareOpen || popupData.open || popupData.guideOpen || popupData.pointOpen) {
  1258. handleStop()
  1259. }
  1260. }}
  1261. onLoadedmetadata={(videoItem: any) => {
  1262. data.videoState = 'play'
  1263. activeVideoItem.value.videoEle = videoItem
  1264. if (!activeVideoItem.value.isprepare) {
  1265. activeVideoItem.value.isprepare = true
  1266. }
  1267. }}
  1268. onPause={() => {
  1269. clearTimeout(activeData.timer)
  1270. // activeData.model = true
  1271. videoIntervalRef.pause()
  1272. }}
  1273. onSeeked={() => {
  1274. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  1275. }}
  1276. onSeeking={() => {
  1277. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  1278. }}
  1279. onWaiting={() => {
  1280. videoIntervalRef.isActive.value && videoIntervalRef.pause()
  1281. }}
  1282. onTimeupdate={() => {
  1283. const activeVideoRef = data.videoItemRef?.getPlyrRef()
  1284. if (
  1285. !videoIntervalRef.isActive.value &&
  1286. activeVideoRef?.currentTime() > 0 &&
  1287. !activeVideoRef?.paused()
  1288. ) {
  1289. videoIntervalRef.resume()
  1290. }
  1291. }}
  1292. onTogglePlay={(paused: boolean) => {
  1293. // console.log('播放切换', paused)
  1294. // 首次播放完成
  1295. if (!activeVideoItem.value.isprepare) {
  1296. activeVideoItem.value.isprepare = true
  1297. }
  1298. activeVideoItem.value.autoPlay = false
  1299. if (paused || popupData.open || popupData.guideOpen) {
  1300. clearTimeout(activeData.timer)
  1301. } else {
  1302. setModelOpen()
  1303. }
  1304. }}
  1305. onEnded={async () => {
  1306. const _index = popupData.activeIndex + 1
  1307. if (_index < data.itemList.length) {
  1308. handleSwipeChange(_index)
  1309. } else {
  1310. // 说明是最后一个
  1311. intervalFnRef.value.pause()
  1312. // 同步数据时先进行有效时间进行保存
  1313. initVideoCount(false, true)
  1314. await updateStat()
  1315. }
  1316. }}
  1317. onReset={() => {
  1318. if (!activeVideoItem.value.videoEle?.paused) {
  1319. setModelOpen()
  1320. }
  1321. }}
  1322. onError={() => {
  1323. // 视屏异常
  1324. activeVideoItem.value.error = true
  1325. }}
  1326. />
  1327. </div>
  1328. {data.itemList.map((m: any, mIndex: number) => {
  1329. const isRenderItem = Math.abs(popupData.activeIndex - mIndex) < 2
  1330. const isRender = Math.abs(popupData.playIndex - mIndex) < 2
  1331. // 判断是否是当前选中的元素
  1332. const activeEle = popupData.playIndex === mIndex ? true : false
  1333. return isRenderItem ? (
  1334. <div
  1335. key={'index' + mIndex}
  1336. data-id={'data' + mIndex}
  1337. class={[
  1338. styles.itemDiv,
  1339. activeEle && styles.itemActive,
  1340. activeData.isAnimation && styles.acitveAnimation,
  1341. isRenderItem ? styles.show : styles.hide
  1342. ]}
  1343. style={
  1344. mIndex < popupData.activeIndex
  1345. ? effects[effectIndex.value].prev
  1346. : mIndex > popupData.activeIndex
  1347. ? effects[effectIndex.value].next
  1348. : {}
  1349. }
  1350. >
  1351. <Transition name="van-fade">
  1352. {m.type === 'VIDEO' &&
  1353. data.animationState !== 'end' &&
  1354. data.videoState != 'play' &&
  1355. !m.isprepare && (
  1356. <div class={styles.loadWrap}>
  1357. <Vue3Lottie animationData={playLoadData}></Vue3Lottie>
  1358. </div>
  1359. )}
  1360. </Transition>
  1361. {isRender && m.type === 'IMG' && (
  1362. <>
  1363. <img src={m.content} />
  1364. {m.materialMusicId && state.platformType !== 'SCHOOL' && (
  1365. <div
  1366. class={[styles.goPractice, activeData.model ? '' : styles.hide]}
  1367. onClick={(e: any) => {
  1368. // 去云练习完整版
  1369. e.stopPropagation()
  1370. const parmas = qs.stringify({
  1371. id: m.materialMusicId
  1372. })
  1373. const src = `${location.origin}/orchestra-music-score/?` + parmas
  1374. postMessage({
  1375. api: 'openAccompanyWebView',
  1376. content: {
  1377. url: src,
  1378. orientation: 0,
  1379. c_orientation: 0,
  1380. isHideTitle: true,
  1381. statusBarTextColor: false,
  1382. isOpenLight: true
  1383. }
  1384. })
  1385. }}
  1386. ></div>
  1387. )}
  1388. </>
  1389. )}
  1390. {isRender && m.type === 'SONG' && (
  1391. <MusicScore
  1392. activeModel={activeData.model}
  1393. data-vid={m.id}
  1394. music={m}
  1395. onSetIframe={(el: any) => {
  1396. m.iframeRef = el
  1397. }}
  1398. />
  1399. )}
  1400. </div>
  1401. ) : (
  1402. ''
  1403. )
  1404. })}
  1405. </div>
  1406. <Transition name="left">
  1407. {activeData.model && (
  1408. <div class={styles.leftFixedBtns} onClick={(e: Event) => e.stopPropagation()}>
  1409. <div class={[styles.btnsWrap, styles.prePoint]}>
  1410. {state.platformType === 'TEACHER' && data.source !== 'search' && <div class={styles.fullBtn} onClick={() => {
  1411. popupData.coursewareOpen = true
  1412. handleStop()
  1413. }}>
  1414. <img src={iconCourseType} />
  1415. </div>}
  1416. {state.platformType !== "SCHOOL" && <div class={styles.fullBtn} onClick={() => {
  1417. handleStop()
  1418. data.isSearch = true
  1419. detailTempSearchList.value = detailList.value
  1420. popupData.tempItemActive = ""
  1421. popupData.tempTabActive = ""
  1422. data.searchTemp = ""
  1423. // data.searchTemp = JSON.parse(JSON.stringify(data.search))
  1424. popupData.open = true
  1425. }}>
  1426. <img src={iconSearch} />
  1427. </div>}
  1428. {data.source !== 'search' && <div class={styles.fullBtn} onClick={() => {
  1429. popupData.open = true
  1430. data.isSearch = false
  1431. handleStop()
  1432. }}>
  1433. <img src={iconMenu} />
  1434. {/* <span>知识点</span> */}
  1435. </div>}
  1436. <div
  1437. class={[styles.fullBtn, !(popupData.activeIndex != 0) && styles.disabled]}
  1438. onClick={() => {
  1439. if(popupData.activeIndex != 0) handlePreAndNext('up')
  1440. }}
  1441. >
  1442. <img src={iconUp} />
  1443. {/* <span style={{ textAlign: 'center' }}>上一个</span> */}
  1444. </div>
  1445. <div
  1446. class={[styles.fullBtn, !(popupData.activeIndex != data.itemList.length - 1) && styles.disabled]}
  1447. onClick={() => {
  1448. if(popupData.activeIndex != data.itemList.length - 1) handlePreAndNext('down')
  1449. }}
  1450. >
  1451. {/* <span style={{ textAlign: 'center' }}>下一个</span> */}
  1452. <img src={iconDown} />
  1453. </div>
  1454. </div>
  1455. </div>
  1456. )}
  1457. </Transition>
  1458. </div>
  1459. <div
  1460. style={{ transform: activeData.model ? '' : 'translateY(-100%)' }}
  1461. id="coursePlayHeader"
  1462. class={styles.headerContainer}
  1463. ref={headeRef}
  1464. >
  1465. <div class={styles.backBtn}>
  1466. <Icon name={iconBack} onClick={() => goback()} />
  1467. <div class={styles.titleSection}>
  1468. <div class={styles.title}>{popupData.tabName}</div>
  1469. <div class={styles.titleContent}>
  1470. <p>{data.itemList[popupData.activeIndex]?.name}</p>
  1471. {data.detail?.lessonTargetDesc ? <span onClick={() => onTitleTip('phaseGoals', data.detail?.lessonTargetDesc)}>阶段目标</span>: ""}
  1472. {data.itemList[popupData.activeIndex]?.checkItem ? <span onClick={() => onTitleTip('checkItem', data.itemList[popupData.activeIndex]?.checkItem)}>检查事项</span> : ""}
  1473. </div>
  1474. </div>
  1475. </div>
  1476. {data.isCourse && isCurrentCoursewareMenu.value && <PlayRecordTime ref={playRef} isCurrentCoursewareMenu={isCurrentCoursewareMenu.value} list={data.knowledgePointList} />}
  1477. {/* <div
  1478. class={styles.menu}
  1479. onClick={() => {
  1480. const _effectIndex = effectIndex.value + 1
  1481. effectIndex.value = _effectIndex >= effects.length - 1 ? 0 : _effectIndex
  1482. setModelOpen()
  1483. }}
  1484. >
  1485. {popupData.tabName}
  1486. </div> */}
  1487. {state.platformType === 'TEACHER' && (
  1488. <div
  1489. class={styles.headRight}
  1490. onClick={(e: Event) => {
  1491. e.stopPropagation()
  1492. clearTimeout(activeData.timer)
  1493. }}
  1494. >
  1495. {data.isCourse && (
  1496. <>
  1497. <div class={styles.pointBtn} onClick={() => gotoRollCall('student_roll_call')}>
  1498. {/* <img src={iconDian} /> */}
  1499. <span>点名</span>
  1500. </div>
  1501. <div class={styles.pointBtn} onClick={() => gotoRollCall('sign_out')}>
  1502. {/* <img src={iconPoint} /> */}
  1503. <span>签退</span>
  1504. </div>
  1505. </>
  1506. )}
  1507. <div class={styles.rightBtn} onClick={() => (popupData.guideOpen = true)}>
  1508. <img src={iconTouping} />
  1509. </div>
  1510. {/* <div
  1511. class={styles.rightBtn}
  1512. onClick={() => {
  1513. openStudyTool({
  1514. type: 'pen',
  1515. icon: iconPen,
  1516. name: '批注'
  1517. })
  1518. }}
  1519. >
  1520. <img src={iconPen} />
  1521. </div> */}
  1522. {/* <div class={styles.rightBtn} onClick={() => (popupData.toolOpen = true)}>
  1523. <img src={iconMore} />
  1524. </div> */}
  1525. </div>
  1526. )}
  1527. </div>
  1528. {/* 更多弹窗 */}
  1529. <Popup
  1530. class={[styles.popupMore, styles.popupCoursewarePlay]}
  1531. overlayClass={styles.overlayClass}
  1532. position="right"
  1533. round
  1534. v-model:show={popupData.toolOpen}
  1535. onClose={handleClosePopup}
  1536. >
  1537. <Tool onHandleTool={openStudyTool} />
  1538. </Popup>
  1539. <Popup
  1540. class={[styles.popup, styles.popupCoursewarePlay]}
  1541. overlayClass={styles.overlayClass}
  1542. position="right"
  1543. round
  1544. v-model:show={popupData.open}
  1545. onClose={handleClosePopup}
  1546. >
  1547. {data.isSearch ?
  1548. <PointsSearch
  1549. data={detailTempSearchList.value}
  1550. search={data.searchTemp || data.search}
  1551. loading={data.searchLoading}
  1552. tabActive={popupData.tempTabActive || popupData.tabActive}
  1553. itemActive={popupData.tempItemActive || popupData.itemActive}
  1554. open={popupData.open}
  1555. onHandleSelect={(res: any) => {
  1556. if(data.source !== "search") {
  1557. const tempList: any[] = []
  1558. detailTempSearchList.value?.forEach((item: any) => {
  1559. if(Array.isArray(item.list)) {
  1560. tempList.push(...item.list)
  1561. }
  1562. })
  1563. const item = tempList.find((n: any) => n.id == res.itemActive)
  1564. const parentItem = detailTempSearchList.value?.find((n: any) => n.lessonCoursewareDetailId == item?.lessonCoursewareDetailId)
  1565. if(String(parentItem?.accessScope) === '1') {
  1566. const hasVip = handleCheckVip(false)
  1567. if (!hasVip) return
  1568. }
  1569. if (browser().isApp) {
  1570. postMessage({
  1571. api: 'openWebView',
  1572. content: {
  1573. url: `${location.origin}${location.pathname}#/coursewarePlay?lessonId=${data.lessonId}&source=search&kId=${res.materialId}&search=${encodeURIComponent(data.searchTemp ? JSON.parse(JSON.stringify(data.searchTemp)) : '')}`,
  1574. orientation: 0,
  1575. c_orientation: 0,
  1576. isHideTitle: true,
  1577. statusBarTextColor: false,
  1578. isOpenLight: true,
  1579. showLoadingAnim: true
  1580. }
  1581. });
  1582. return
  1583. } else {
  1584. router.push({
  1585. path: '/coursewarePlay',
  1586. query: {
  1587. lessonId: data.lessonId,
  1588. kId: res.materialId,
  1589. search: data.searchTemp ? JSON.parse(JSON.stringify(data.searchTemp)) : '',
  1590. source: 'search'
  1591. }
  1592. }).then(() => {
  1593. window.location.reload()
  1594. });
  1595. // Toast('请使用App打开')
  1596. return
  1597. }
  1598. }
  1599. if(res.isSearch) {
  1600. detailList.value = detailTempSearchList.value
  1601. const tempList: any[] = []
  1602. detailTempSearchList.value?.forEach((item: any) => {
  1603. if(Array.isArray(item.list)) {
  1604. tempList.push(...item.list)
  1605. }
  1606. })
  1607. data.itemList = tempList || []
  1608. data.search = data.searchTemp ? JSON.parse(JSON.stringify(data.searchTemp)) : ''
  1609. }
  1610. // 判断是否需要会员
  1611. const item = data.itemList.find((n: any) => n.id == res.itemActive)
  1612. const parentItem = detailList.value?.find((n: any) => n.lessonCoursewareDetailId == item?.lessonCoursewareDetailId)
  1613. if(String(parentItem?.accessScope) === '1') {
  1614. const hasVip = handleCheckVip(false)
  1615. if (!hasVip) return
  1616. }
  1617. toggleMaterial(res.itemActive);
  1618. popupData.open = false;
  1619. }}
  1620. onHandleSearch={async (val: any) => {
  1621. detailTempSearchList.value = []
  1622. if(data.source === 'search') {
  1623. await getSearchDetail({
  1624. type: 'pointSearch',
  1625. search: val.search
  1626. })
  1627. } else {
  1628. await getSearchDetail({
  1629. type: 'pointSearch',
  1630. search: val.search,
  1631. id: data.lessonId
  1632. })
  1633. }
  1634. data.searchTemp = val.search;
  1635. }} /> :
  1636. <Points
  1637. data={data.knowledgePointList}
  1638. tabActive={popupData.tabActive}
  1639. itemActive={popupData.itemActive}
  1640. onHandleSelect={(res: any) => {
  1641. // onChangeSwiper('change', res.itemActive)
  1642. popupData.open = false
  1643. toggleMaterial(res.itemActive)
  1644. }}
  1645. />}
  1646. </Popup>
  1647. <Popup
  1648. class={[styles.popup, styles.popupCoursewarePlay]}
  1649. overlayClass={styles.overlayClass}
  1650. position="right"
  1651. round
  1652. v-model:show={popupData.coursewareOpen}
  1653. onClose={handleClosePopup}>
  1654. {/* 课件类型 */}
  1655. <CoursewareType list={data.refLevelList} onConfirm={async (item: any) => {
  1656. // 判断是否为当前课程类型
  1657. if(data.currentId === item.id) {
  1658. return
  1659. }
  1660. const n = await getDetail(item.id);
  1661. const s = await getRefLevel(item.id);
  1662. data.isSearch = false
  1663. if(n && s) {
  1664. data.currentId = item.id;
  1665. isCurrentCoursewareMenu.value = item.id === route.query.id ? true : false
  1666. popupData.coursewareOpen = false;
  1667. popupData.activeIndex = 0;
  1668. getCurrentItemCatch(popupData.activeIndex) // 获取当前元素的缓存
  1669. nextTick(() => {
  1670. popupData.open = true
  1671. })
  1672. } else {
  1673. if(!isOnline.value) {
  1674. showToast('网络异常')
  1675. }
  1676. }
  1677. if(state.platformType !== "SCHOOL") {
  1678. data.lessonId && await getSearchDetail({search: data.search, id: data.lessonId})
  1679. }
  1680. }} />
  1681. </Popup>
  1682. <Popup
  1683. class={[styles.popup, styles.popupCoursewarePlay]}
  1684. overlayClass={styles.overlayClass}
  1685. position="right"
  1686. round
  1687. v-model:show={popupData.guideOpen}
  1688. onClose={handleClosePopup}
  1689. >
  1690. <OGuide />
  1691. </Popup>
  1692. <Popup
  1693. class={[styles.popup, styles.popupPoint]}
  1694. round
  1695. style={{ background: 'transparent !important' }}
  1696. v-model:show={popupData.pointOpen}
  1697. onClose={handleClosePopup}>
  1698. <CoursewareTips onClose={() => {
  1699. popupData.pointOpen = false
  1700. }} show={popupData.pointOpen} content={popupData.pointContent} titleName={popupData.pointTitle} />
  1701. </Popup>
  1702. <GlobalTools />
  1703. </div>
  1704. )
  1705. }
  1706. })