coursewarePlay.vue 38 KB

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