coursewarePlay.vue 31 KB

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