coursewarePlay.vue 32 KB

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