coursewarePlay.vue 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105
  1. <!--
  2. * @FileDescription: 教程播放
  3. * @Author: 黄琪勇
  4. * @Date:2024-04-03 17:31:41
  5. -->
  6. <template>
  7. <div class="coursewarePlay" :class="[!isShowController && 'hideController', fileType === 'SONG' && 'fileType_song']">
  8. <div class="coursewarePlayCon" @mousemove="handleMousemove" @click="handleClick" @touchstart="handleClick">
  9. <videoPlay
  10. v-show="fileType === 'VIDEO'"
  11. ref="videoPlayDom"
  12. @ended="handleChangeCourseware(1)"
  13. @loadedmetadata="
  14. () => {
  15. isTempAutoPlay = false
  16. }
  17. "
  18. :autoPlay="true"
  19. @playbackRate="showController"
  20. :disableEvents="true"
  21. :isShowController="isShowController"
  22. />
  23. <div class="imgPlayBox" v-if="fileType === 'IMG'">
  24. <ElImage :hide-on-click-modal="true" fit="contain" :src="activeCourseware?.content" class="imgPlay" />
  25. </div>
  26. <div class="songPlayBox" v-if="fileType === 'SONG'">
  27. <iframe
  28. ref="songPlayDom"
  29. :key="activeCourseware?.content"
  30. class="songIframe"
  31. @mousemove="handleMousemove"
  32. :src="songPlaySrc"
  33. frameborder="0"
  34. ></iframe>
  35. <!-- <span></span> -->
  36. </div>
  37. </div>
  38. <div class="leftTools posTools">
  39. <div class="posBtn" @click="handleToolClick('menu')" v-if="searchObj.source !== 'search'">
  40. <img src="@/img/coursewarePlay/menu.png" />
  41. <!-- <div>课程类型</div> -->
  42. </div>
  43. <!-- 有没有搜索都展示 -->
  44. <div class="posBtn" @click="handleToolClick('search')">
  45. <img src="@/img/coursewarePlay/sousuo.png" />
  46. <!-- <div>知识点</div> -->
  47. </div>
  48. <div class="posBtn" @click="handleToolClick('point')" v-if="searchObj.source !== 'search'">
  49. <img src="@/img/coursewarePlay/zhishidian.png" />
  50. <!-- <div>知识点</div> -->
  51. </div>
  52. <div
  53. :class="['posBtn', activeCoursewareIndex > 0 ? '' : 'disabled']"
  54. @click="
  55. () => {
  56. if (activeCoursewareIndex > 0) {
  57. handleChangeCourseware(-1)
  58. }
  59. }
  60. "
  61. >
  62. <img src="@/img/coursewarePlay/shang.png" />
  63. <!-- <div>上一个</div> -->
  64. </div>
  65. <div
  66. :class="['posBtn', activeCoursewareIndex < flattenCoursewareList.length - 1 ? '' : 'disabled']"
  67. @click="
  68. () => {
  69. if (activeCoursewareIndex < flattenCoursewareList.length - 1) {
  70. handleChangeCourseware(1)
  71. }
  72. }
  73. "
  74. >
  75. <img src="@/img/coursewarePlay/xia.png" />
  76. <!-- <div>下一个</div> -->
  77. </div>
  78. </div>
  79. <div
  80. v-if="activeCoursewareResourceId"
  81. @click="
  82. () => {
  83. handleVideoPause()
  84. handleGoPracticeBtn(activeCoursewareResourceId!)
  85. }
  86. "
  87. class="goPracticeBtn"
  88. ></div>
  89. <div class="topTools">
  90. <div class="leftMenu">
  91. <img @click="handleGoBack" class="backImg" src="@/img/coursewarePlay/back.png" />
  92. <div class="title-section">
  93. <div class="title">{{ activeCourseware?.parentData.name || "" }}</div>
  94. <div class="content">
  95. <p>{{ activeCourseware?.name || "" }}</p>
  96. <!-- <span v-if="activeCourseware?.phaseGoals" @click="onTitleTip('phaseGoals', activeCourseware?.phaseGoals)">阶段目标</span> -->
  97. <span v-if="lessonTargetDetail" @click="onTitleTip('phaseGoals', lessonTargetDetail)">阶段目标</span>
  98. <span v-if="activeCourseware?.checkItem" @click="onTitleTip('checkItem', activeCourseware?.checkItem)">检查事项</span>
  99. </div>
  100. </div>
  101. </div>
  102. <div class="midMenu">
  103. <playRecordTime
  104. v-if="route.query.modeId && coursewareTotalTime && userStoreHook.roles === 'GYT'"
  105. :modeId="route.query.modeId as string"
  106. :isCurrentCoursewareMenu="isCurrentCoursewareMenu"
  107. :coursewareTotalTime="coursewareTotalTime"
  108. />
  109. </div>
  110. <div class="rightMenu">
  111. <div class="posCloseBtn" @click="handleCoursewareEnd">
  112. <img src="@/img/coursewarePlay/jieshu.png" />
  113. </div>
  114. </div>
  115. </div>
  116. <el-drawer class="elDrawer elDrawerHeader" direction="ltr" v-model="drawerSearchShow" :show-close="false">
  117. <template #header="{ close }">
  118. <myInput
  119. class="queryIpt"
  120. v-model="searchObj.queryStr"
  121. :height="36"
  122. placeholder="请输入素材关键词"
  123. clearable
  124. @handleQuery="handleQuery"
  125. @keyup.enter="handleQuery"
  126. />
  127. <img class="close" @click="close" src="@/img/coursewarePlay/close.png" />
  128. </template>
  129. <ElScrollbar class="elScrollbar" v-loading="searchObj.loading">
  130. <el-empty
  131. class="empty"
  132. v-if="!tempCoursewareList.length && !searchObj.loading"
  133. :image="require('@/img/layout/empty.png')"
  134. description="暂无搜索结果"
  135. />
  136. <courseCollapse
  137. :activeCollapse="tempActiveCourseware || activeCourseware"
  138. :search="searchObj.tempSearch || searchObj.search"
  139. :courseList="tempCoursewareList"
  140. @handleClick="handleCourseSearchClick"
  141. />
  142. </ElScrollbar>
  143. </el-drawer>
  144. <el-drawer class="elDrawer" direction="ltr" v-model="drawerShow" :show-close="false">
  145. <template #header="{ close }">
  146. <img class="directory" src="@/img/coursewarePlay/kcml.png" />
  147. <div class="tit">知识点目录</div>
  148. <img class="close" @click="close" src="@/img/coursewarePlay/close.png" />
  149. </template>
  150. <ElScrollbar class="elScrollbar" v-loading="searchObj.loading">
  151. <el-empty
  152. class="empty"
  153. v-if="!tempCoursewareList.length && !searchObj.loading"
  154. :image="require('@/img/layout/empty.png')"
  155. description="暂无搜索结果"
  156. />
  157. <courseCollapse
  158. :activeCollapse="tempActiveCourseware || activeCourseware"
  159. :search="searchObj.tempSearch || searchObj.search"
  160. :courseList="tempCoursewareList"
  161. @handleClick="handleCourseClick"
  162. />
  163. </ElScrollbar>
  164. </el-drawer>
  165. <el-drawer class="elDrawer elCourseMenu" direction="ltr" v-model="drawerMenuShow" :show-close="false">
  166. <template #header="{ close }">
  167. <img class="directory" src="@/img/coursewarePlay/menuActive.png" />
  168. <div class="tit">课程类型</div>
  169. <img class="close" @click="close" src="@/img/coursewarePlay/close.png" />
  170. </template>
  171. <ElScrollbar class="elScrollbar">
  172. <courseMenuCollapse :courseMenuList="coursewareMenuList" @handleClick="handleCourseMenuClick" />
  173. </ElScrollbar>
  174. </el-drawer>
  175. <practiceForm v-model="isPracticeShow" :practiceUrl="practiceUrl" @close="handlePracticeClose" />
  176. </div>
  177. </template>
  178. <script setup lang="ts">
  179. import videoPlay from "./videoPlay"
  180. import {
  181. getLessonCourseDetail_gym,
  182. getLessonCoursewareDetail_gyt,
  183. getLessonCourseDetail_klx,
  184. getLessonCoursewareCourseList_gym,
  185. getMyCoursewareDetail_gyt,
  186. getLessonCoursewareCourseList_klx
  187. } from "@/api/cloudTextbooks.api"
  188. import myInput from "@/components/myInput"
  189. import { checkWebCourse_gyt, refLevel_gym, refLevel_gyt, refLevel_klx } from "@/api/coursewarePlay.api"
  190. import { httpAjax, httpAjaxErrMsg, httpAjaxLoadingErrMsg } from "@/plugin/httpAjax"
  191. import userStore from "@/store/modules/user"
  192. import { useRoute, useRouter } from "vue-router"
  193. import { shallowRef, ref, computed, onUnmounted, onMounted, watch, nextTick, reactive } from "vue"
  194. import { ElMessageBox } from "element-plus"
  195. import courseCollapse from "./components/courseCollapse"
  196. import courseMenuCollapse from "./components/courseMenuCollapse"
  197. import playRecordTime from "./components/playRecordTime"
  198. import practiceForm from "@/businessComponents/practiceForm"
  199. import { getRecentCourseSchedule_gym } from "@/api/homePage.api"
  200. import { getToken } from "@/libs/auth"
  201. import { URL_TEACH_GYT, URL_TEACH_GYM, URL_TEACH_KLX } from "@/config"
  202. import { handleFullscreen } from "@/libs/fullscreen"
  203. import useCoursewarePlayTip from "./components/useCoursewarePlayTip"
  204. import useDialogConfirm from "@/hooks/useDialogConfirm"
  205. import LoadingBar from "@/plugin/loadingBar"
  206. import { isPlay, penShow, toolOpen, whitePenShow } from "@/businessComponents/globalTools/globalTools"
  207. import { closeAllModalFrame } from "@/plugin/modalFrame"
  208. import { deepCopy } from "@/libs/tools"
  209. const route = useRoute()
  210. const router = useRouter()
  211. const userStoreHook = userStore()
  212. /* 获取资源 */
  213. const videoPlayDom = ref<InstanceType<typeof videoPlay>>()
  214. const songPlayDom = ref<any>() // 曲谱对象
  215. const lessonTargetDetail = ref<string>("") // 阶段目标
  216. const coursewareMenuList = shallowRef<any[]>([]) // 课程类型
  217. const coursewareList = shallowRef<any[]>([]) // 知识点
  218. const coursewareSearchList = shallowRef<any[]>([]) // 搜索的知识点
  219. const tempCoursewareList = shallowRef<any[]>([]) // 临时知识点
  220. const flattenCoursewareList = ref<any[]>([]) // 扁平化coursewareList
  221. const tempFlattenCoursewareList = ref<any[]>([]) // 临时 扁平化coursewareList
  222. const isCurrentCoursewareMenu = shallowRef(true) // 是否为当前选的课程类型
  223. const currentId = ref<any>(route.params.id)
  224. const isTempAutoPlay = ref(false)
  225. let coursewareDetailController: AbortController
  226. const searchObj = reactive({
  227. loading: false,
  228. isSearch: false, // 是否搜索 标识
  229. lessonCoursewareId: null as any, // 课程id,
  230. queryStr: route.query.search as any,
  231. source: route.query.source as any, // 从哪里来的
  232. search: route.query.search as any, // 默认的搜索条件 -
  233. tempSearch: route.query.search as any
  234. })
  235. // 选中的知识点
  236. const tempActiveCourseware: any = ref() // 临时数据
  237. const activeCourseware = computed<undefined | Record<string, any>>(() => {
  238. return flattenCoursewareList.value[activeCoursewareIndex.value]
  239. })
  240. // 文件类型
  241. const fileType = computed<"VIDEO" | "IMG" | "SONG">(() => {
  242. return activeCourseware.value?.typeCode || activeCourseware.value?.type
  243. })
  244. const songPlaySrc = computed<string>(() => {
  245. if (fileType.value !== "SONG") {
  246. return ""
  247. }
  248. // GYM,GYT,KLX 区分 云教练
  249. const urlObj = {
  250. GYT: `${URL_TEACH_GYT}?id=${activeCourseware.value?.content}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1`,
  251. GYM: `${URL_TEACH_GYM}#/?id=${
  252. activeCourseware.value?.content
  253. }&Authorization=${getToken()}&platform=pc&isHideBack=true&isHideMusicList=true&isYjt=1&systemType=teacher`,
  254. KLX: `${URL_TEACH_KLX}#/?id=${
  255. activeCourseware.value?.content
  256. }&Authorization=${getToken()}&platform=pc&isHideBack=true&isHideMusicList=true&isYjt=1&systemType=teacher`
  257. }
  258. // const iframeRef = document.querySelector("#songPlayDom") as any
  259. // iframeRef?.contentWindow.location.replace(urlObj[userStoreHook.roles!])
  260. // console.log(iframeRef, "iframeRef")
  261. return urlObj[userStoreHook.roles!]
  262. })
  263. // 视频是否自动播放
  264. // const videoIsAutoPlay = computed<boolean>(() => {
  265. // // 如果为视频且有阶段目前则不自动播放
  266. // console.log(fileType.value, isTempAutoPlay.value, "isTempAutoPlay")
  267. // return (fileType.value === "VIDEO" && activeCourseware.value?.phaseGoals) || isTempAutoPlay.value ? false : true
  268. // })
  269. const activeCoursewareIndex = ref(0)
  270. const drawerSearchShow = ref(false) // 弹窗带搜索条件的
  271. const drawerShow = ref(false)
  272. const drawerMenuShow = ref(false)
  273. // 课程总时间
  274. const coursewareTotalTime = ref(0)
  275. // 监控播放
  276. watch(activeCourseware, () => {
  277. fileType.value === "VIDEO" &&
  278. nextTick(() => {
  279. handlePlayVideo({
  280. src: activeCourseware.value?.content,
  281. name: activeCourseware.value?.name
  282. })
  283. })
  284. showController()
  285. })
  286. async function getCoursewareList(id?: string) {
  287. // GYM,GYT,KLX 区分 查询接口
  288. const LessonCoursewareDetailApi = {
  289. GYT: getLessonCoursewareDetail_gyt,
  290. GYM: getLessonCourseDetail_gym,
  291. KLX: getLessonCourseDetail_klx
  292. }
  293. await httpAjaxErrMsg(LessonCoursewareDetailApi[userStoreHook.roles!], id || (route.params.id as string)).then(res => {
  294. if (res.code === 200) {
  295. const { lockFlag, knowledgePointList, lessonTargetDesc, lessonCoursewareId } = res.data || {}
  296. if (lockFlag) {
  297. ElMessageBox.alert("课件已锁定", "温馨提示", {
  298. confirmButtonText: "退出",
  299. type: "error"
  300. })
  301. .then(() => {
  302. handleGoBack()
  303. })
  304. .catch(() => {
  305. handleGoBack()
  306. })
  307. return
  308. }
  309. if ((knowledgePointList || []).length < 1) {
  310. ElMessageBox.alert("没有找到课件", "温馨提示", {
  311. confirmButtonText: "退出",
  312. type: "error"
  313. })
  314. .then(() => {
  315. handleGoBack()
  316. })
  317. .catch(() => {
  318. handleGoBack()
  319. })
  320. return
  321. }
  322. lessonTargetDetail.value = lessonTargetDesc ? lessonTargetDesc.replace(/\n/g, "<br />") : ""
  323. searchObj.lessonCoursewareId = lessonCoursewareId
  324. // 处理返回的数据
  325. handlePointList(knowledgePointList)
  326. }
  327. })
  328. // 请求带搜索条件数据
  329. searchObj.lessonCoursewareId && getLessCoursewareList("onlySearch", searchObj.lessonCoursewareId)
  330. }
  331. /**
  332. * 带搜索条件的
  333. * @param type onlySearch 只搜索 展示
  334. */
  335. async function getLessCoursewareList(type?: string, id?: string) {
  336. // GYM,GYT,KLX 区分 查询接口
  337. const LessonCoursewareDetailApi = {
  338. GYT: getMyCoursewareDetail_gyt,
  339. GYM: getLessonCoursewareCourseList_gym,
  340. KLX: getLessonCoursewareCourseList_klx
  341. }
  342. if (coursewareDetailController) {
  343. coursewareDetailController.abort()
  344. }
  345. coursewareDetailController = new AbortController()
  346. searchObj.loading = true
  347. await httpAjax(LessonCoursewareDetailApi[userStoreHook.roles!], {
  348. id: id || (route.params.id as string),
  349. data: {
  350. search: searchObj.queryStr,
  351. detailFlag: "1"
  352. },
  353. abortController: coursewareDetailController
  354. }).then(res => {
  355. searchObj.loading = false
  356. if (res.code === 200) {
  357. const result = res.data || []
  358. for (let i = 0; i < result.length; i++) {
  359. const itemResult = result[i]
  360. itemResult.name = itemResult.coursewareDetailName
  361. itemResult.id = itemResult.coursewareDetailId || itemResult.lessonCoursewareDetailId
  362. itemResult.lessonTargetDesc = itemResult.lessonTargetDesc ? itemResult.lessonTargetDesc.replace(/\n/g, "<br />") : ""
  363. itemResult.children = itemResult.knowledgePointList || []
  364. itemResult.knowledgePointList = []
  365. }
  366. if (type === "onlySearch") {
  367. handlePointListOnlySearch(result)
  368. } else {
  369. handlePointList(result, searchObj.isSearch)
  370. }
  371. // 初始化检查事项
  372. const item = flattenCoursewareList.value[activeCoursewareIndex.value]
  373. const parentId = item ? item.parentData.ids[0] : ""
  374. if (parentId) {
  375. const parentItem = coursewareList.value.find((item: any) => item.id === parentId)
  376. if (parentItem) {
  377. lessonTargetDetail.value = parentItem.lessonTargetDesc
  378. }
  379. }
  380. }
  381. // 标识
  382. searchObj.isSearch = false
  383. })
  384. }
  385. // 处理是从搜索过来的,还是
  386. if (route.query.source === "search") {
  387. getLessCoursewareList()
  388. } else {
  389. getCoursewareList()
  390. getCoursewareMenuList()
  391. }
  392. function getCoursewareMenuList(id?: string) {
  393. // GYM,GYT,KLX 区分 查询接口
  394. const LessonCoursewareMenuDetailApi = {
  395. GYT: refLevel_gyt,
  396. GYM: refLevel_gym,
  397. KLX: refLevel_klx
  398. }
  399. httpAjaxErrMsg(LessonCoursewareMenuDetailApi[userStoreHook.roles!], {
  400. lessonCoursewareDetailId: id || route.params.id,
  401. courseScheduleId: route.query.modeId as any
  402. } as any).then(res => {
  403. if (res.code === 200) {
  404. coursewareMenuList.value = res.data || []
  405. }
  406. })
  407. }
  408. let flattenCoursewareListData: any = [] // 临时扁平化数据
  409. function handlePointList(pointList: any[], isSearch?: boolean) {
  410. flattenCoursewareListData = []
  411. const list = filterPointList(pointList)
  412. if (!isSearch) {
  413. // 重置数据
  414. coursewareTotalTime.value = 0
  415. coursewareList.value = list
  416. // 如果url里面有materialId 代表指定资料播放
  417. if (route.query.materialId) {
  418. const index = flattenCoursewareListData.findIndex((item: any) => {
  419. return route.query.materialId === item.id + "" && route.query.knowledgePointId === item.knowledgePointId + ""
  420. })
  421. index > -1 && (activeCoursewareIndex.value = index)
  422. }
  423. flattenCoursewareList.value = deepCopy(flattenCoursewareListData)
  424. } else {
  425. if (flattenCoursewareListData[0]) {
  426. // 默认展开第一个
  427. tempActiveCourseware.value = {
  428. ...flattenCoursewareListData[0],
  429. id: null
  430. }
  431. }
  432. }
  433. tempCoursewareList.value = list
  434. tempFlattenCoursewareList.value = deepCopy(flattenCoursewareListData)
  435. }
  436. function handlePointListOnlySearch(pointList: any[]) {
  437. flattenCoursewareListData = []
  438. const list = filterPointList(pointList)
  439. if (flattenCoursewareListData[0]) {
  440. // 默认展开第一个
  441. tempActiveCourseware.value = {
  442. ...flattenCoursewareListData[0],
  443. id: null
  444. }
  445. }
  446. if (!searchObj.isSearch) {
  447. coursewareSearchList.value = list
  448. }
  449. tempCoursewareList.value = list
  450. }
  451. function filterPointList(pointList: any[], parentData?: { ids: string[]; name: string }): any[] {
  452. // 设置父级及以上id数组和父级name
  453. return pointList.map(point => {
  454. if (point.children) {
  455. return Object.assign(point, {
  456. children: filterPointList(point.children, { ids: [...(parentData?.ids || []), point.id], name: point.name })
  457. })
  458. } else {
  459. // coursewareTotalTime.value += point.totalMaterialTimeSecond
  460. return Object.assign(point, {
  461. materialList: point.materialList.map((item: any) => {
  462. item.parentData = {
  463. ids: [...(parentData?.ids || []), point.id],
  464. name: point.name
  465. }
  466. flattenCoursewareListData.push(item)
  467. return item
  468. })
  469. })
  470. }
  471. })
  472. }
  473. function handleChangeCourseware(index: -1 | 1) {
  474. handleVideoPause()
  475. handleSongPause()
  476. const newIndex = index + activeCoursewareIndex.value
  477. if (newIndex < 0 || newIndex > flattenCoursewareList.value.length - 1) {
  478. return
  479. }
  480. if (searchObj.source === "search") {
  481. const newItem = flattenCoursewareList.value[newIndex]
  482. const newParentId = newItem ? newItem.parentData?.ids[0] : ""
  483. if (newParentId) {
  484. const parentItem = coursewareList.value.find((item: any) => item.id === newParentId)
  485. if (parentItem) {
  486. lessonTargetDetail.value = parentItem.lessonTargetDesc
  487. }
  488. }
  489. }
  490. activeCoursewareIndex.value = newIndex
  491. }
  492. function handleCourseSearchClick(value: any) {
  493. if (searchObj.source === "search") {
  494. searchObj.search = searchObj.tempSearch ? JSON.parse(JSON.stringify(searchObj.tempSearch)) : ""
  495. coursewareList.value = deepCopy(tempCoursewareList.value)
  496. flattenCoursewareList.value = deepCopy(tempFlattenCoursewareList.value)
  497. const newIndex = flattenCoursewareList.value.findIndex((item: any) => {
  498. return value.id === item.id && value.knowledgePointId === item.knowledgePointId
  499. })
  500. const newItem = flattenCoursewareList.value[newIndex]
  501. const newParentId = newItem ? newItem.parentData?.ids[0] : ""
  502. if (newParentId) {
  503. const parentItem = coursewareList.value.find((item: any) => item.id === newParentId)
  504. if (parentItem) {
  505. lessonTargetDetail.value = parentItem.lessonTargetDesc
  506. }
  507. }
  508. activeCoursewareIndex.value = newIndex
  509. drawerSearchShow.value = false
  510. } else {
  511. const urls = router.resolve({
  512. path: `/coursewarePlay/${searchObj.lessonCoursewareId}`,
  513. query: {
  514. materialId: value.id,
  515. source: "search",
  516. knowledgePointId: value.knowledgePointId,
  517. search: searchObj.tempSearch ? JSON.parse(JSON.stringify(searchObj.tempSearch)) : ""
  518. }
  519. })
  520. console.log(urls, "urls")
  521. const url = window.location.origin + window.location.pathname + urls.href
  522. window.open(url)
  523. }
  524. }
  525. function handleCourseClick(value: any) {
  526. searchObj.search = searchObj.tempSearch ? JSON.parse(JSON.stringify(searchObj.tempSearch)) : ""
  527. coursewareList.value = deepCopy(tempCoursewareList.value)
  528. flattenCoursewareList.value = deepCopy(tempFlattenCoursewareList.value)
  529. const newIndex = flattenCoursewareList.value.findIndex((item: any) => {
  530. return value.id === item.id && value.knowledgePointId === item.knowledgePointId
  531. })
  532. if (searchObj.source === "search") {
  533. const newItem = flattenCoursewareList.value[newIndex]
  534. const newParentId = newItem ? newItem.parentData?.ids[0] : ""
  535. if (newParentId) {
  536. const parentItem = coursewareList.value.find((item: any) => item.id === newParentId)
  537. if (parentItem) {
  538. lessonTargetDetail.value = parentItem.lessonTargetDesc
  539. }
  540. }
  541. }
  542. activeCoursewareIndex.value = newIndex
  543. drawerShow.value = false
  544. }
  545. async function handleCourseMenuClick(value: any) {
  546. // 判断是否为当前课程类型
  547. if (currentId.value === value.id) {
  548. return
  549. }
  550. LoadingBar.loading(true)
  551. currentId.value = value.id
  552. isCurrentCoursewareMenu.value = value.id === route.params.id ? true : false
  553. flattenCoursewareListData = [] // 重置数据
  554. isTempAutoPlay.value = true
  555. await getCoursewareList(value.id)
  556. getCoursewareMenuList(value.id)
  557. drawerMenuShow.value = false
  558. activeCoursewareIndex.value = 0
  559. nextTick(() => {
  560. // if (!activeCourseware.value?.phaseGoals) {
  561. // 切换之后默认打开课程目录
  562. drawerShow.value = true
  563. // }
  564. })
  565. LoadingBar.loading(false)
  566. }
  567. /** 曲目相关 */
  568. // 暂停曲目播放
  569. function handleSongPause() {
  570. songPlayDom.value?.contentWindow?.postMessage({ api: "setPlayState" }, "*")
  571. showController()
  572. }
  573. /* 播放器相关 */
  574. // 视频播放或者暂停
  575. function handleVideoPlay() {
  576. videoPlayDom.value?.handlePlay()
  577. showController()
  578. }
  579. // 视频快进快退
  580. function handleVideoSpeedCurrentTime(type: "fast" | "slow") {
  581. videoPlayDom.value?.speedCurrentTime(type)
  582. showController()
  583. }
  584. // 视频暂停
  585. function handleVideoPause() {
  586. videoPlayDom.value?.pauseVideo()
  587. showController()
  588. }
  589. // 播放视频
  590. function handlePlayVideo({ src, name }: { src: string; name: string }) {
  591. videoPlayDom.value?.playVideo({
  592. src,
  593. name
  594. })
  595. showController()
  596. }
  597. // 全屏显示
  598. handleFullscreen(true, false)
  599. /* 按键事件相关 */
  600. onMounted(() => {
  601. document.addEventListener("keydown", handleKeydown)
  602. document.addEventListener("contextmenu", preventDefaultContextmenu)
  603. showController()
  604. })
  605. onUnmounted(() => {
  606. document.removeEventListener("keydown", handleKeydown)
  607. document.removeEventListener("contextmenu", preventDefaultContextmenu)
  608. })
  609. function preventDefaultContextmenu(event: MouseEvent) {
  610. event.preventDefault()
  611. }
  612. function handleKeydown(e: KeyboardEvent) {
  613. const key = e.key
  614. // 打开弹窗之后快捷键失效
  615. if (drawerShow.value || drawerMenuShow.value || drawerSearchShow.value) {
  616. return
  617. }
  618. if (key === " ") {
  619. closeAllModalFrame()
  620. drawerShow.value = false
  621. // 视频类型的时候才触发
  622. fileType.value === "VIDEO" && handleVideoPlay()
  623. } else if (key === "ArrowLeft") {
  624. closeAllModalFrame()
  625. drawerShow.value = false
  626. // 视频类型的时候才触发
  627. fileType.value === "VIDEO" && handleVideoSpeedCurrentTime("slow")
  628. } else if (key === "ArrowRight") {
  629. closeAllModalFrame()
  630. drawerShow.value = false
  631. // 视频类型的时候才触发
  632. fileType.value === "VIDEO" && handleVideoSpeedCurrentTime("fast")
  633. } else if (key === "ArrowDown") {
  634. closeAllModalFrame()
  635. drawerShow.value = false
  636. handleChangeCourseware(1)
  637. } else if (key === "ArrowUp") {
  638. closeAllModalFrame()
  639. drawerShow.value = false
  640. handleChangeCourseware(-1)
  641. }
  642. }
  643. function handleToolClick(type: string) {
  644. fileType.value === "VIDEO" && handleVideoPause()
  645. if (type === "menu") {
  646. drawerMenuShow.value = true
  647. } else if (type === "point") {
  648. tempCoursewareList.value = deepCopy(coursewareList.value)
  649. searchObj.queryStr = searchObj.search ? JSON.parse(JSON.stringify(searchObj.search)) : ""
  650. tempActiveCourseware.value = null
  651. drawerShow.value = true
  652. } else if (type === "search") {
  653. if (searchObj.source === "search") {
  654. tempCoursewareList.value = deepCopy(coursewareList.value)
  655. tempActiveCourseware.value = null
  656. } else {
  657. tempCoursewareList.value = deepCopy(coursewareSearchList.value)
  658. tempActiveCourseware.value = {}
  659. }
  660. searchObj.queryStr = searchObj.search ? JSON.parse(JSON.stringify(searchObj.search)) : ""
  661. drawerSearchShow.value = true
  662. }
  663. }
  664. function handleMousemove() {
  665. showController()
  666. }
  667. function handleClick() {
  668. fileType.value === "VIDEO" && isShowController.value && handleVideoPlay()
  669. showController()
  670. }
  671. // 是否显示控制器
  672. const isShowController = ref(true)
  673. let _showTimer: any
  674. function showController() {
  675. isShowController.value = true
  676. _showTimer && clearTimeout(_showTimer)
  677. _showTimer = setTimeout(hideController, 3000)
  678. }
  679. function hideController() {
  680. // 取消暂停的时候收起
  681. // if (fileType.value === "VIDEO" && videoPlayDom.value?.playType === "pause") {
  682. // return
  683. // }
  684. isShowController.value = false
  685. }
  686. /* 结束课程 */
  687. function handleGoBack() {
  688. // window.open("about:blank", "_self")
  689. // window.close()
  690. // 如果从课件列表进来的,直接关闭
  691. if (!router.options.history.state?.back) {
  692. window.close()
  693. return
  694. }
  695. router.go(-1)
  696. }
  697. function handleCoursewareEnd() {
  698. if (route.query.modeId) {
  699. if (userStoreHook.roles === "GYM") {
  700. httpAjaxLoadingErrMsg(getRecentCourseSchedule_gym, route.query.modeId as string).then(res => {
  701. if (res.code === 200) {
  702. if (res.data?.signOutStatusEnum === 3) {
  703. useDialogConfirm({
  704. headImg: require("@/img/coursewarePlay/ts.png"),
  705. text: `请确认是否结束课程,结束后请到APP进行签退。`,
  706. btnShow: [true, true],
  707. onOk() {
  708. handleGoBack()
  709. }
  710. })
  711. } else {
  712. handleGoBack()
  713. }
  714. }
  715. })
  716. } else if (userStoreHook.roles === "GYT") {
  717. httpAjaxLoadingErrMsg(checkWebCourse_gyt, route.query.modeId as string).then(res => {
  718. if (res.code === 200) {
  719. if (res.data?.signOut === false) {
  720. useDialogConfirm({
  721. headImg: require("@/img/coursewarePlay/ts.png"),
  722. text: `请确认是否结束课程,结束后请到APP进行签退。`,
  723. btnShow: [true, true],
  724. onOk() {
  725. handleGoBack()
  726. }
  727. })
  728. } else {
  729. handleGoBack()
  730. }
  731. }
  732. })
  733. }
  734. } else {
  735. handleGoBack()
  736. }
  737. }
  738. // 是否收起
  739. watch(
  740. () => isShowController.value,
  741. () => {
  742. if (isShowController.value) {
  743. isPlay.value = false
  744. } else {
  745. isPlay.value = true
  746. toolOpen.value = false
  747. }
  748. }
  749. )
  750. // 白板的批注打开时暂停播放
  751. watch(
  752. () => [whitePenShow.value, penShow.value],
  753. () => {
  754. if (whitePenShow.value || penShow.value) {
  755. handleVideoPause()
  756. handleSongPause()
  757. }
  758. }
  759. )
  760. // 去练习
  761. const activeCoursewareResourceId = computed<string | undefined>(() => {
  762. const materialRefs = activeCourseware.value?.materialRefs
  763. return materialRefs ? (["GYM", "KLX"].includes(userStoreHook.roles!) ? materialRefs[0]?.resourceIdStr : materialRefs[0]?.resourceId) : undefined
  764. })
  765. const isPracticeShow = ref(false)
  766. const practiceUrl = ref("")
  767. function handleGoPracticeBtn(activeCoursewareResourceId: string) {
  768. // GYM,GYT,KLX 区分 云教练
  769. const urlObj = {
  770. GYT: `${URL_TEACH_GYT}?id=${activeCoursewareResourceId}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1&&isHideBack=false`,
  771. GYM: `${URL_TEACH_GYM}#/?id=${activeCoursewareResourceId}&Authorization=${getToken()}&platform=pc&isYjt=1&systemType=teacher&isHideMusicList=true`,
  772. KLX: `${URL_TEACH_KLX}#/?id=${activeCoursewareResourceId}&Authorization=${getToken()}&platform=pc&isYjt=1&systemType=teacher&isHideMusicList=true`
  773. }
  774. isPracticeShow.value = true
  775. practiceUrl.value = urlObj[userStoreHook.roles!]
  776. //window.open(urlObj[userStoreHook.roles!], "_blank")
  777. }
  778. function handlePracticeClose() {
  779. isPracticeShow.value = false
  780. practiceUrl.value = ""
  781. }
  782. function onTitleTip(type: "phaseGoals" | "checkItem", text: string) {
  783. useCoursewarePlayTip({
  784. headImg: require(`@/img/coursewarePlay/${type === "phaseGoals" ? "ts3" : "ts4"}.png`),
  785. text,
  786. btnShow: [false, false]
  787. })
  788. handleVideoPause()
  789. }
  790. function handleQuery() {
  791. //
  792. searchObj.isSearch = true
  793. searchObj.tempSearch = searchObj.queryStr ? JSON.parse(JSON.stringify(searchObj.queryStr)) : ""
  794. if (searchObj.source === "search") {
  795. getLessCoursewareList()
  796. } else {
  797. getLessCoursewareList("onlySearch", searchObj.lessonCoursewareId)
  798. }
  799. }
  800. </script>
  801. <style lang="scss" scoped>
  802. .coursewarePlay {
  803. width: 100%;
  804. height: 100%;
  805. position: relative;
  806. overflow: hidden;
  807. &.hideController {
  808. .leftTools {
  809. opacity: 0;
  810. transform: translate(-100%, -50%);
  811. }
  812. .topTools {
  813. opacity: 0;
  814. transform: translateY(-100%);
  815. }
  816. .goPracticeBtn {
  817. transform: translateY(74px);
  818. }
  819. }
  820. &.fileType_song.hideController {
  821. .leftTools {
  822. opacity: initial;
  823. transform: translateY(-50%);
  824. }
  825. .goPracticeBtn {
  826. transform: initial;
  827. }
  828. }
  829. .coursewarePlayCon {
  830. width: 100%;
  831. height: 100%;
  832. overflow: hidden;
  833. .imgPlayBox {
  834. width: 100%;
  835. height: 100%;
  836. display: flex;
  837. justify-content: center;
  838. align-items: center;
  839. .imgPlay {
  840. width: 84%;
  841. height: 100%;
  842. }
  843. }
  844. .songPlayBox {
  845. width: 100%;
  846. height: 100%;
  847. .songIframe {
  848. display: block;
  849. width: 100%;
  850. height: 100%;
  851. }
  852. }
  853. }
  854. .topTools {
  855. position: absolute;
  856. top: 0;
  857. left: 0;
  858. width: 100%;
  859. background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0) 100%);
  860. transition: all 0.5s;
  861. display: flex;
  862. align-items: flex-start;
  863. justify-content: space-between;
  864. padding: 20px 30px 40px;
  865. .leftMenu {
  866. display: flex;
  867. align-items: flex-start;
  868. .backImg {
  869. cursor: pointer;
  870. width: 22px;
  871. padding-top: 5px;
  872. &:hover {
  873. opacity: $opacity-hover;
  874. }
  875. }
  876. .title-section {
  877. font-weight: 500;
  878. font-size: 22px;
  879. color: #ffffff;
  880. line-height: 30px;
  881. padding-left: 20px;
  882. .content {
  883. padding-top: 6px;
  884. font-weight: 500;
  885. font-size: 18px;
  886. color: #ffffff;
  887. line-height: 26px;
  888. display: flex;
  889. align-items: center;
  890. p {
  891. line-height: 1;
  892. padding-top: 1px;
  893. }
  894. span {
  895. background: rgba(0, 0, 0, 0.1);
  896. border-radius: 16px;
  897. border: 1px solid rgba(255, 255, 255, 0.7);
  898. font-size: 14px;
  899. color: #ffffff;
  900. line-height: 22px;
  901. padding: 2px 10px;
  902. margin-left: 10px;
  903. cursor: pointer;
  904. }
  905. }
  906. }
  907. }
  908. .midMenu {
  909. position: absolute;
  910. left: 50%;
  911. top: 23px;
  912. // transform: translate(-50%, -50%);
  913. transform: translateX(-50%);
  914. }
  915. .rightMenu {
  916. .posCloseBtn {
  917. cursor: pointer;
  918. img {
  919. width: 34px;
  920. height: 34px;
  921. padding: 6px;
  922. box-sizing: content-box;
  923. &:hover {
  924. background-color: rgba(255, 255, 255, 0.2);
  925. border-radius: 6px;
  926. }
  927. }
  928. }
  929. }
  930. }
  931. .posTools {
  932. position: absolute;
  933. top: 50%;
  934. transform: translateY(-50%);
  935. transition: all 0.5s;
  936. &.leftTools {
  937. background: rgba(0, 0, 0, 0.4);
  938. border-radius: 8px;
  939. left: 32px;
  940. }
  941. .posBtn {
  942. padding: 14px 6px;
  943. font-weight: 500;
  944. font-size: 16px;
  945. color: #ffffff;
  946. display: flex;
  947. flex-direction: column;
  948. align-items: center;
  949. cursor: pointer;
  950. // &:hover {
  951. // opacity: $opacity-hover;
  952. // }
  953. &.disabled {
  954. opacity: $opacity-disabled;
  955. }
  956. > img {
  957. width: 34px;
  958. height: 34px;
  959. padding: 6px;
  960. box-sizing: content-box;
  961. &:hover {
  962. background-color: rgba(255, 255, 255, 0.2);
  963. border-radius: 6px;
  964. }
  965. }
  966. }
  967. }
  968. .goPracticeBtn {
  969. position: absolute;
  970. right: 32px;
  971. bottom: 24px;
  972. width: 143px;
  973. height: 50px;
  974. background: url("@/img/coursewarePlay/goPracticeBtn.png") no-repeat;
  975. background-size: 100% 100%;
  976. cursor: pointer;
  977. transition: all 0.5s;
  978. &:hover {
  979. opacity: $opacity-hover;
  980. }
  981. }
  982. &:deep(.elDrawer.el-drawer) {
  983. width: 348px !important;
  984. .el-drawer__header {
  985. height: 54px;
  986. background: #ededed;
  987. padding: 0 20px;
  988. margin-bottom: 0;
  989. .directory {
  990. flex-grow: 0;
  991. flex-shrink: 0;
  992. width: 24px;
  993. height: 24px;
  994. }
  995. .tit {
  996. flex-grow: 1;
  997. margin-left: 10px;
  998. font-weight: 600;
  999. font-size: 18px;
  1000. color: #333333;
  1001. }
  1002. .close {
  1003. cursor: pointer;
  1004. width: 14px;
  1005. flex-shrink: 0;
  1006. &:hover {
  1007. opacity: $opacity-hover;
  1008. }
  1009. }
  1010. .el-input {
  1011. margin-right: 10px;
  1012. font-size: 14px;
  1013. }
  1014. .btnSelect {
  1015. width: 56px;
  1016. height: 26px;
  1017. line-height: 26px;
  1018. font-size: 14px;
  1019. }
  1020. }
  1021. .el-drawer__body {
  1022. padding: 0;
  1023. overflow: hidden;
  1024. & > .elScrollbar {
  1025. .el-scrollbar__view {
  1026. padding: 0 22px;
  1027. width: 100%;
  1028. }
  1029. .el-scrollbar__wrap {
  1030. overflow-x: hidden;
  1031. }
  1032. }
  1033. }
  1034. }
  1035. &:deep(.elDrawerHeader.el-drawer) {
  1036. .el-drawer__header {
  1037. padding: 0 10px;
  1038. }
  1039. }
  1040. &:deep(.elCourseMenu.el-drawer) {
  1041. width: 363px !important;
  1042. .el-drawer__body {
  1043. & > .elScrollbar {
  1044. .el-scrollbar__view {
  1045. padding: 0;
  1046. }
  1047. }
  1048. }
  1049. }
  1050. }
  1051. .empty {
  1052. height: calc(100vh - 54px);
  1053. :deep(.el-empty__image) {
  1054. width: 228px;
  1055. }
  1056. :deep(.el-empty__description) {
  1057. color: #aaa;
  1058. p {
  1059. font-size: 18px;
  1060. }
  1061. }
  1062. }
  1063. </style>