index.tsx 60 KB

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