coursewarePlay.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  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. @playbackRate="showController"
  14. :disableEvents="true"
  15. :isShowController="isShowController"
  16. />
  17. <div class="imgPlayBox" v-if="fileType === 'IMG'">
  18. <ElImage :hide-on-click-modal="true" fit="contain" :src="activeCourseware?.content" class="imgPlay" />
  19. </div>
  20. <div class="songPlayBox" v-if="fileType === 'SONG'">
  21. <iframe class="songIframe" @mousemove="handleMousemove" :src="songPlaySrc" frameborder="0"></iframe>
  22. </div>
  23. </div>
  24. <div class="leftTools posTools">
  25. <div v-if="activeCoursewareIndex > 0" class="posBtn" @click="handleChangeCourseware(-1)">
  26. <img src="@/img/coursewarePlay/shang.png" />
  27. <div>上一个</div>
  28. </div>
  29. <div v-if="activeCoursewareIndex < flattenCoursewareList.length - 1" class="posBtn" @click="handleChangeCourseware(1)">
  30. <img src="@/img/coursewarePlay/xia.png" />
  31. <div>下一个</div>
  32. </div>
  33. </div>
  34. <div class="rightTools posTools">
  35. <div
  36. class="posBtn"
  37. @click="
  38. () => {
  39. handleVideoPause()
  40. whitePenShow = true
  41. }
  42. "
  43. >
  44. <img src="@/img/coursewarePlay/baiban.png" />
  45. <div>白板</div>
  46. </div>
  47. <div
  48. class="posBtn"
  49. @click="
  50. () => {
  51. handleVideoPause()
  52. penShow = true
  53. }
  54. "
  55. >
  56. <img src="@/img/coursewarePlay/pizhu.png" />
  57. <div>批注</div>
  58. </div>
  59. <div class="posBtn" @click="drawerShow = true">
  60. <img src="@/img/coursewarePlay/zhishidian.png" />
  61. <div>知识点</div>
  62. </div>
  63. <div class="posBtn" @click="handleCoursewareEnd">
  64. <img src="@/img/coursewarePlay/jieshu.png" />
  65. <div>结束</div>
  66. </div>
  67. </div>
  68. <div
  69. v-if="activeCoursewareResourceId"
  70. @click="
  71. () => {
  72. handleVideoPause()
  73. handleGoPracticeBtn(activeCoursewareResourceId!)
  74. }
  75. "
  76. class="goPracticeBtn"
  77. ></div>
  78. <div class="topTools">
  79. <div class="leftMenu">
  80. <img @click="handleGoBack" class="backImg" src="@/img/coursewarePlay/back.png" />
  81. <playRecordTime
  82. v-if="route.query.modeId && coursewareTotalTime && userStoreHook.roles === 'GYT'"
  83. :modeId="route.query.modeId as string"
  84. :coursewareTotalTime="coursewareTotalTime"
  85. />
  86. </div>
  87. <div class="midMenu">{{ activeCourseware?.parentData.name || "" }}</div>
  88. <div class="rightMenu"></div>
  89. </div>
  90. <el-drawer class="elDrawer" v-model="drawerShow" :show-close="false">
  91. <template #header="{ close }">
  92. <img class="directory" src="@/img/coursewarePlay/kcml.png" />
  93. <div class="tit">课程目录</div>
  94. <img class="close" @click="close" src="@/img/coursewarePlay/close.png" />
  95. </template>
  96. <ElScrollbar class="elScrollbar">
  97. <courseCollapse :activeCollapse="activeCourseware" :courseList="coursewareList" @handleClick="handleCourseClick" />
  98. </ElScrollbar>
  99. </el-drawer>
  100. <pen
  101. :close="
  102. () => {
  103. penShow = false
  104. }
  105. "
  106. v-model="penShow"
  107. />
  108. <pen
  109. :is-white="true"
  110. :close="
  111. () => {
  112. whitePenShow = false
  113. }
  114. "
  115. v-model="whitePenShow"
  116. />
  117. </div>
  118. </template>
  119. <script setup lang="ts">
  120. import videoPlay from "./videoPlay"
  121. import { getLessonCourseDetail_gym, getLessonCoursewareDetail_gyt, getLessonCourseDetail_klx } from "@/api/cloudTextbooks.api"
  122. import { checkWebCourse_gyt } from "@/api/coursewarePlay.api"
  123. import { httpAjaxErrMsg, httpAjaxLoadingErrMsg } from "@/plugin/httpAjax"
  124. import userStore from "@/store/modules/user"
  125. import { useRoute, useRouter } from "vue-router"
  126. import { shallowRef, ref, computed, onUnmounted, onMounted, watch, nextTick } from "vue"
  127. import { ElMessageBox } from "element-plus"
  128. import courseCollapse from "./components/courseCollapse"
  129. import pen from "./components/pen"
  130. import playRecordTime from "./components/playRecordTime"
  131. import useDialogConfirm from "@/hooks/useDialogConfirm"
  132. import { getRecentCourseSchedule_gym } from "@/api/homePage.api"
  133. import { getToken } from "@/libs/auth"
  134. import { URL_TEACH_GYT, URL_TEACH_GYM, URL_TEACH_KLX } from "@/config"
  135. import { handleFullscreen } from "@/libs/fullscreen"
  136. const route = useRoute()
  137. const router = useRouter()
  138. const userStoreHook = userStore()
  139. // 批注
  140. const penShow = ref(false)
  141. // 白板
  142. const whitePenShow = ref(false)
  143. /* 获取资源 */
  144. const videoPlayDom = ref<InstanceType<typeof videoPlay>>()
  145. const coursewareList = shallowRef<any[]>([]) // 知识点
  146. const flattenCoursewareList = shallowRef<any[]>([]) // 扁平化coursewareList
  147. // 选中的知识点
  148. const activeCourseware = computed<undefined | Record<string, any>>(() => {
  149. return flattenCoursewareList.value[activeCoursewareIndex.value]
  150. })
  151. // 文件类型
  152. const fileType = computed<"VIDEO" | "IMG" | "SONG">(() => {
  153. return activeCourseware.value?.typeCode || activeCourseware.value?.type
  154. })
  155. const songPlaySrc = computed<string>(() => {
  156. if (fileType.value !== "SONG") {
  157. return ""
  158. }
  159. // GYM,GYT,KLX 区分 云教练
  160. const urlObj = {
  161. GYT: `${URL_TEACH_GYT}?id=${activeCourseware.value?.content}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1`,
  162. GYM: `${URL_TEACH_GYM}#/detail/${
  163. activeCourseware.value?.content
  164. }?Authorization=${getToken()}&platform=web&liveConfig=1&isHideBack=true&isYjt=1`,
  165. KLX: `${URL_TEACH_KLX}??Authorization=${getToken()}&id=${activeCourseware.value?.content}&isHideBack=true&limitModel=practice&isYjt=1`
  166. }
  167. return urlObj[userStoreHook.roles!]
  168. })
  169. const activeCoursewareIndex = ref(0)
  170. const drawerShow = ref(false)
  171. // 课程总时间
  172. const coursewareTotalTime = ref(0)
  173. // 监控播放
  174. watch(activeCourseware, () => {
  175. handleVideoPause()
  176. fileType.value === "VIDEO" &&
  177. nextTick(() => {
  178. handlePlayVideo({
  179. src: activeCourseware.value?.content,
  180. name: activeCourseware.value?.name
  181. })
  182. })
  183. showController()
  184. })
  185. getCoursewareList()
  186. function getCoursewareList() {
  187. // GYM,GYT,KLX 区分 查询接口
  188. const LessonCoursewareDetailApi = {
  189. GYT: getLessonCoursewareDetail_gyt,
  190. GYM: getLessonCourseDetail_gym,
  191. KLX: getLessonCourseDetail_klx
  192. }
  193. httpAjaxErrMsg(LessonCoursewareDetailApi[userStoreHook.roles!], route.params.id as string).then(res => {
  194. if (res.code === 200) {
  195. const { lockFlag, knowledgePointList } = res.data || {}
  196. if (lockFlag) {
  197. ElMessageBox.alert("课件已锁定", "温馨提示", {
  198. confirmButtonText: "退出",
  199. type: "error"
  200. })
  201. .then(() => {
  202. handleGoBack()
  203. })
  204. .catch(() => {
  205. handleGoBack()
  206. })
  207. return
  208. }
  209. if ((knowledgePointList || []).length < 1) {
  210. ElMessageBox.alert("没有找到课件", "温馨提示", {
  211. confirmButtonText: "退出",
  212. type: "error"
  213. })
  214. .then(() => {
  215. handleGoBack()
  216. })
  217. .catch(() => {
  218. handleGoBack()
  219. })
  220. return
  221. }
  222. // 处理返回的数据
  223. handlePointList(knowledgePointList)
  224. }
  225. })
  226. }
  227. let flattenCoursewareListData: any = [] // 临时扁平化数据
  228. function handlePointList(pointList: any[]) {
  229. coursewareList.value = filterPointList(pointList)
  230. // 如果url里面有materialId 代表指定资料播放
  231. if (route.query.materialId) {
  232. const index = flattenCoursewareListData.findIndex((item: any) => {
  233. return route.query.materialId === item.id + "" && route.query.knowledgePointId === item.knowledgePointId + ""
  234. })
  235. index > -1 && (activeCoursewareIndex.value = index)
  236. }
  237. flattenCoursewareList.value = flattenCoursewareListData
  238. }
  239. function filterPointList(pointList: any[], parentData?: { ids: string[]; name: string }): any[] {
  240. // 设置父级及以上id数组和父级name
  241. return pointList.map(point => {
  242. if (point.children) {
  243. return Object.assign(point, {
  244. children: filterPointList(point.children, { ids: [...(parentData?.ids || []), point.id], name: point.name })
  245. })
  246. } else {
  247. coursewareTotalTime.value += point.totalMaterialTimeSecond
  248. return Object.assign(point, {
  249. materialList: point.materialList.map((item: any) => {
  250. item.parentData = {
  251. ids: [...(parentData?.ids || []), point.id],
  252. name: point.name
  253. }
  254. flattenCoursewareListData.push(item)
  255. return item
  256. })
  257. })
  258. }
  259. })
  260. }
  261. function handleChangeCourseware(index: -1 | 1) {
  262. const newIndex = index + activeCoursewareIndex.value
  263. if (newIndex < 0 || newIndex > flattenCoursewareList.value.length - 1) {
  264. return
  265. }
  266. activeCoursewareIndex.value = newIndex
  267. }
  268. function handleCourseClick(value: any) {
  269. activeCoursewareIndex.value = flattenCoursewareList.value.findIndex((item: any) => {
  270. return value.id === item.id && value.knowledgePointId === item.knowledgePointId
  271. })
  272. }
  273. /* 播放器相关 */
  274. // 视频播放或者暂停
  275. function handleVideoPlay() {
  276. videoPlayDom.value?.handlePlay()
  277. showController()
  278. }
  279. // 视频快进快退
  280. function handleVideoSpeedCurrentTime(type: "fast" | "slow") {
  281. videoPlayDom.value?.speedCurrentTime(type)
  282. showController()
  283. }
  284. // 视频暂停
  285. function handleVideoPause() {
  286. videoPlayDom.value?.pauseVideo()
  287. showController()
  288. }
  289. // 播放视频
  290. function handlePlayVideo({ src, name }: { src: string; name: string }) {
  291. videoPlayDom.value?.playVideo({
  292. src,
  293. name
  294. })
  295. showController()
  296. }
  297. // 全屏显示
  298. handleFullscreen(true, false)
  299. /* 按键事件相关 */
  300. onMounted(() => {
  301. document.addEventListener("keydown", handleKeydown)
  302. document.addEventListener("contextmenu", preventDefaultContextmenu)
  303. showController()
  304. })
  305. onUnmounted(() => {
  306. document.removeEventListener("keydown", handleKeydown)
  307. document.removeEventListener("contextmenu", preventDefaultContextmenu)
  308. })
  309. function preventDefaultContextmenu(event: MouseEvent) {
  310. event.preventDefault()
  311. }
  312. function handleKeydown(e: KeyboardEvent) {
  313. const key = e.key
  314. if (key === " ") {
  315. // 视频类型的时候才触发
  316. fileType.value === "VIDEO" && handleVideoPlay()
  317. } else if (key === "ArrowLeft") {
  318. // 视频类型的时候才触发
  319. fileType.value === "VIDEO" && handleVideoSpeedCurrentTime("slow")
  320. } else if (key === "ArrowRight") {
  321. // 视频类型的时候才触发
  322. fileType.value === "VIDEO" && handleVideoSpeedCurrentTime("fast")
  323. } else if (key === "ArrowDown") {
  324. handleChangeCourseware(1)
  325. } else if (key === "ArrowUp") {
  326. handleChangeCourseware(-1)
  327. }
  328. }
  329. function handleMousemove() {
  330. showController()
  331. }
  332. function handleClick() {
  333. fileType.value === "VIDEO" && isShowController.value && handleVideoPlay()
  334. showController()
  335. }
  336. // 是否显示控制器
  337. const isShowController = ref(true)
  338. let _showTimer: any
  339. function showController() {
  340. isShowController.value = true
  341. _showTimer && clearTimeout(_showTimer)
  342. _showTimer = setTimeout(hideController, 3000)
  343. }
  344. function hideController() {
  345. if (fileType.value === "VIDEO" && videoPlayDom.value?.playType === "pause") {
  346. return
  347. }
  348. isShowController.value = false
  349. }
  350. /* 结束课程 */
  351. function handleGoBack() {
  352. // window.open("about:blank", "_self")
  353. // window.close()
  354. router.go(-1)
  355. }
  356. function handleCoursewareEnd() {
  357. if (route.query.modeId) {
  358. if (userStoreHook.roles === "GYM") {
  359. httpAjaxLoadingErrMsg(getRecentCourseSchedule_gym, route.query.modeId as string).then(res => {
  360. if (res.code === 200) {
  361. if (res.data?.signOutStatusEnum === 3) {
  362. useDialogConfirm({
  363. headImg: require("@/img/coursewarePlay/ts.png"),
  364. text: `请确认是否结束课程,结束后请到APP进行签退。`,
  365. btnShow: [true, true],
  366. onOk() {
  367. handleGoBack()
  368. }
  369. })
  370. } else {
  371. handleGoBack()
  372. }
  373. }
  374. })
  375. } else if (userStoreHook.roles === "GYT") {
  376. httpAjaxLoadingErrMsg(checkWebCourse_gyt, route.query.modeId as string).then(res => {
  377. if (res.code === 200) {
  378. if (res.data?.signOut === false) {
  379. useDialogConfirm({
  380. headImg: require("@/img/coursewarePlay/ts.png"),
  381. text: `请确认是否结束课程,结束后请到APP进行签退。`,
  382. btnShow: [true, true],
  383. onOk() {
  384. handleGoBack()
  385. }
  386. })
  387. } else {
  388. handleGoBack()
  389. }
  390. }
  391. })
  392. }
  393. } else {
  394. handleGoBack()
  395. }
  396. }
  397. // 去练习
  398. const activeCoursewareResourceId = computed<string | undefined>(() => {
  399. const materialRefs = activeCourseware.value?.materialRefs
  400. return materialRefs ? (["GYM", "KLX"].includes(userStoreHook.roles!) ? materialRefs[0]?.resourceIdStr : materialRefs[0]?.resourceId) : undefined
  401. })
  402. function handleGoPracticeBtn(activeCoursewareResourceId: string) {
  403. // GYM,GYT,KLX 区分 云教练
  404. const urlObj = {
  405. GYT: `${URL_TEACH_GYT}?id=${activeCoursewareResourceId}&modelType=practice&modeType=json&Authorization=${getToken()}&isYjt=1&&isHideBack=false`,
  406. GYM: `${URL_TEACH_GYM}#/detail/${activeCoursewareResourceId}?Authorization=${getToken()}&platform=web&liveConfig=1&isYjt=1`,
  407. KLX: `${URL_TEACH_KLX}??Authorization=${getToken()}&id=${activeCoursewareResourceId}&limitModel=practice&isYjt=1`
  408. }
  409. window.open(urlObj[userStoreHook.roles!], "_blank")
  410. }
  411. </script>
  412. <style lang="scss" scoped>
  413. .coursewarePlay {
  414. width: 100%;
  415. height: 100%;
  416. position: relative;
  417. overflow: hidden;
  418. &.hideController {
  419. .leftTools {
  420. opacity: 0;
  421. transform: translate(-100%, -50%);
  422. }
  423. .rightTools {
  424. opacity: 0;
  425. transform: translate(100%, -50%);
  426. }
  427. .topTools {
  428. opacity: 0;
  429. transform: translateY(-100%);
  430. }
  431. .goPracticeBtn {
  432. transform: translatex(-135px);
  433. }
  434. }
  435. &.fileType_song.hideController {
  436. .leftTools {
  437. opacity: initial;
  438. transform: translateY(-50%);
  439. }
  440. .rightTools {
  441. opacity: initial;
  442. transform: translateY(-50%);
  443. }
  444. .goPracticeBtn {
  445. transform: initial;
  446. }
  447. }
  448. .coursewarePlayCon {
  449. width: 100%;
  450. height: 100%;
  451. overflow: hidden;
  452. .imgPlayBox {
  453. width: 100%;
  454. height: 100%;
  455. display: flex;
  456. justify-content: center;
  457. align-items: center;
  458. .imgPlay {
  459. width: 84%;
  460. height: 100%;
  461. }
  462. }
  463. .songPlayBox {
  464. width: 100%;
  465. height: 100%;
  466. .songIframe {
  467. display: block;
  468. width: 100%;
  469. height: 100%;
  470. }
  471. }
  472. }
  473. .topTools {
  474. position: absolute;
  475. top: 0;
  476. left: 0;
  477. width: 100%;
  478. background: linear-gradient(180deg, rgba(0, 0, 0, 0.6), transparent);
  479. transition: all 0.5s;
  480. display: flex;
  481. align-items: center;
  482. justify-content: space-between;
  483. padding: 20px 30px;
  484. .leftMenu {
  485. display: flex;
  486. align-items: center;
  487. .backImg {
  488. cursor: pointer;
  489. width: 22px;
  490. &:hover {
  491. opacity: $opacity-hover;
  492. }
  493. }
  494. }
  495. .midMenu {
  496. font-weight: 500;
  497. font-size: 20px;
  498. color: #ffffff;
  499. }
  500. }
  501. .posTools {
  502. position: absolute;
  503. top: 50%;
  504. transform: translateY(-50%);
  505. transition: all 0.5s;
  506. &.leftTools {
  507. left: 12px;
  508. }
  509. &.rightTools {
  510. right: 12px;
  511. }
  512. .posBtn {
  513. background: rgba(0, 0, 0, 0.3);
  514. border-radius: 8px;
  515. padding: 12px 6px;
  516. font-weight: 500;
  517. font-size: 16px;
  518. color: #ffffff;
  519. display: flex;
  520. flex-direction: column;
  521. align-items: center;
  522. cursor: pointer;
  523. margin-bottom: 12px;
  524. &:hover {
  525. opacity: $opacity-hover;
  526. }
  527. &:last-child {
  528. margin-bottom: 0;
  529. }
  530. > img {
  531. margin-bottom: 5px;
  532. width: 34px;
  533. height: 34px;
  534. }
  535. }
  536. }
  537. .goPracticeBtn {
  538. position: absolute;
  539. left: 30px;
  540. bottom: 124px;
  541. width: 178px;
  542. height: 64px;
  543. background: url("@/img/coursewarePlay/goPracticeBtn.png") no-repeat;
  544. background-size: 100% 100%;
  545. cursor: pointer;
  546. transition: all 0.5s;
  547. &:hover {
  548. opacity: $opacity-hover;
  549. }
  550. }
  551. &:deep(.elDrawer.el-drawer) {
  552. width: 346px !important;
  553. .el-drawer__header {
  554. height: 54px;
  555. background: #ededed;
  556. padding: 0 20px;
  557. margin-bottom: 0;
  558. .directory {
  559. flex-grow: 0;
  560. flex-shrink: 0;
  561. width: 24px;
  562. height: 24px;
  563. }
  564. .tit {
  565. flex-grow: 1;
  566. margin-left: 10px;
  567. font-weight: 600;
  568. font-size: 18px;
  569. color: #333333;
  570. }
  571. .close {
  572. cursor: pointer;
  573. width: 14px;
  574. flex-shrink: 0;
  575. &:hover {
  576. opacity: $opacity-hover;
  577. }
  578. }
  579. }
  580. .el-drawer__body {
  581. padding: 0;
  582. overflow: hidden;
  583. & > .elScrollbar {
  584. .el-scrollbar__view {
  585. padding: 0 22px;
  586. width: 100%;
  587. }
  588. .el-scrollbar__wrap {
  589. overflow-x: hidden;
  590. }
  591. }
  592. }
  593. }
  594. }
  595. </style>