index.tsx 59 KB

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