index.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. <template>
  2. <a-layout>
  3. <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible collapsedWidth="0">
  4. <div class="classTitle">
  5. <div>{{ classDetail.courseName }}</div>
  6. </div>
  7. <a-menu
  8. v-model:openKeys="openKeys"
  9. v-model:selectedKeys="selectedKeys"
  10. theme="dark"
  11. mode="inline"
  12. @click="menuClick"
  13. >
  14. <a-sub-menu :key="String(idx + 1)" v-for="(item, idx) in classTimeList">
  15. <template #title>{{ item.name }}</template>
  16. <a-menu-item :key="itemc.id" v-for="(itemc, idxc) in item.classHours">
  17. <span class="mr-2">
  18. <CheckSquareOutlined v-if="selectedKeys.includes(itemc.id)" />
  19. <BorderOutlined v-else />
  20. </span>
  21. <span>{{ itemc.name }}</span>
  22. </a-menu-item>
  23. </a-sub-menu>
  24. </a-menu>
  25. </a-layout-sider>
  26. <a-layout>
  27. <a-layout-header style="padding: 0 20px">
  28. <div class="flex-between">
  29. <div>
  30. <menu-unfold-outlined
  31. v-if="collapsed"
  32. class="trigger"
  33. @click="() => (collapsed = !collapsed)"
  34. style="font-size: 30px; color: #fff"
  35. />
  36. <menu-fold-outlined
  37. v-else
  38. class="trigger"
  39. @click="() => (collapsed = !collapsed)"
  40. style="font-size: 30px; color: #fff"
  41. />
  42. </div>
  43. <div @click="isLike">
  44. <a-tooltip title="收藏" style="cursor: pointer" :getPopupContainer="(trigger) => trigger.parentElement">
  45. <heart-outlined :style="{ 'font-size': '20px', color: classDetail.isCollect ? '#ff4d4f' : '#fff' }" />
  46. </a-tooltip>
  47. </div>
  48. </div>
  49. </a-layout-header>
  50. <a-layout-content style="overflow-y: auto">
  51. <a-card :bordered="false">
  52. <div v-if="videoUrl">
  53. <video
  54. style="height: 900px; width: 100%; background-color: #000"
  55. ref="videoRef"
  56. controls
  57. Controlslist="nodownload noplaybackrate noremoteplayback disablePictureInPicture"
  58. @timeupdate="timeUpdate"
  59. :currentTime="currentTimenew"
  60. :poster="videoPoster"
  61. >
  62. <source :src="videoUrl" type="video/mp4" />
  63. <source :src="videoUrl" type="video/ogg" />
  64. 您的浏览器不支持 HTML5 video 标签。
  65. </video>
  66. </div>
  67. <rightMenu
  68. :idsObj="idsObj"
  69. :dataList="classTimeData"
  70. ref="rightNenuRef"
  71. @videoSpeed="videoSpeed"
  72. @videoStopTime="videoStopTime"
  73. :videoObj="videoObj"
  74. :classDetailParams="classDetailParams(2)"
  75. ></rightMenu>
  76. </a-card>
  77. <a-card :bordered="false" class="mt-3">
  78. <div>
  79. <div>
  80. <div v-for="(item, idx) in webCon">
  81. <a :href="item.href" target="_blank">{{ item.title }}</a>
  82. </div>
  83. </div>
  84. <a-divider />
  85. <div>
  86. 在小学和初中,我们已经接触过一些集合.例如,自然数的集合,同一平面内到一个定点的距离等于定长的点的集合(即圆)等.为了更有效地使用集合语言,我们需要进一步了解集合的有关知识.下面先从集合的含义开始.
  87. 看下面的例子:<br />
  88. (1)1~10之间的所有偶数;<br />
  89. (2)立德中学今年人学的全体高一学生;<br />
  90. (3)所有的正方形;<br />
  91. (4)到直线l的距离等于定长d的所有点<br />
  92. (5)方程x’-3x+2=0的所有实数根;<br />
  93. (6)地球上的四大洋.<br />
  94. 例(1)中,我们把1~10之间的每一个偶数作为元素,这些元素的全体就是一个集合;同样地,<br />
  95. 例(2)中,把立德中学今年人学的每一位高一学生作为元素,这些元素的全体也是一个集合.
  96. </div>
  97. </div>
  98. </a-card>
  99. <div style="display: flex; justify-content: center">
  100. <a-card :bordered="false" class="mt-2" style="width: 100%; padding-right: 30px">
  101. <a-tabs v-model:activeKey="tabsActiveKey">
  102. <a-tab-pane key="1" tab="讲义" style="height: 800px">
  103. <handouts
  104. :itemObj="itemObj"
  105. :hourId="classHourData.id"
  106. v-if="classHourData"
  107. :videoObj="videoObj"
  108. ></handouts>
  109. </a-tab-pane>
  110. <a-tab-pane key="2" tab="字幕" style="height: 800px">
  111. <subtitleBox :url="danmuObj.url" v-if="tabsActiveKey == 2" @videoSpeed="videoSpeed"></subtitleBox>
  112. </a-tab-pane>
  113. <a-tab-pane key="3" tab="笔记" style="min-height: 800px">
  114. <note
  115. :idsObj="idsObj"
  116. ref="noteRef"
  117. v-if="tabsActiveKey == 3"
  118. @videoSpeed="videoSpeed"
  119. @videoStopTime="videoStopTime"
  120. ></note>
  121. </a-tab-pane>
  122. <a-tab-pane key="4" tab="问答" style="min-height: 800px">
  123. <askDiv
  124. :idsObj="idsObj"
  125. ref="askDivRef"
  126. v-if="tabsActiveKey == 4"
  127. @videoSpeed="videoSpeed"
  128. @videoStopTime="videoStopTime"
  129. ></askDiv>
  130. </a-tab-pane>
  131. </a-tabs>
  132. </a-card>
  133. </div>
  134. <forumBtn :forumData="forumData" resourceType="0"></forumBtn>
  135. </a-layout-content>
  136. </a-layout>
  137. </a-layout>
  138. </template>
  139. <script setup name="classCentre">
  140. import classCentre from '@/api/student/classCentre'
  141. import rightMenu from './rightMenu.vue'
  142. import { useRoute, useRouter } from 'vue-router'
  143. import sysConfig from '@/config/index'
  144. import note from './note.vue'
  145. import askDiv from './ask.vue'
  146. import handouts from './handouts.vue'
  147. import subtitleBox from './subtitle.vue'
  148. import Broadcast from '@/utils/Broadcast.js'
  149. import {message} from "ant-design-vue";
  150. const route = useRoute()
  151. const router = useRouter()
  152. const classDetail = ref({})
  153. const classTimeList = ref([])
  154. const classTimeData = ref([])
  155. const openKeys = ref(['1'])
  156. const selectedKeys = ref([])
  157. const collapsed = ref(false)
  158. const videoRef = ref()
  159. const currentTimenew = ref(0)
  160. const idsObj = computed(() => {
  161. let item = findNodeByKey(classTimeList.value, selectedKeys.value[0], classTimeList.value[0])
  162. return {
  163. courseId: route.query.id,
  164. chapterId: selectedKeys.value[0],
  165. hourId: classHourData.value?.id,
  166. ...item
  167. }
  168. })
  169. function findNodeByKey(list, id, parent = null) {
  170. for (const item of list) {
  171. const itemWithParent = {
  172. ...item,
  173. parent: parent
  174. }
  175. if (itemWithParent.id === id) {
  176. return itemWithParent
  177. }
  178. if (item.classHours && Array.isArray(item.classHours)) {
  179. const found = findNodeByKey(item.classHours, id, itemWithParent)
  180. if (found) return found
  181. }
  182. }
  183. return null
  184. }
  185. const videoPoster = computed(() => {
  186. return classTimeData.value.find((r) => r.funcType == 0)?.url
  187. })
  188. const getClassData = () => {
  189. classCentre.addViewCount({ courseId: route.query.id })
  190. classCentre.courseDetail({ courseId: route.query.id }).then((data) => {
  191. classDetail.value = data
  192. classCentre.coursechapterList({ courseId: data.courseId }).then((data) => {
  193. classTimeList.value = data
  194. selectedKeys.value = selectedKeys.value[0] ? selectedKeys.value : [data[0]?.classHours[0].id]
  195. if (selectedKeys.value[0]) {
  196. menuClick()
  197. }
  198. })
  199. })
  200. }
  201. const getClassStatus = (e) => {
  202. classCentre
  203. .classGetStatus({
  204. examPaperId: e.relateId
  205. })
  206. .then((res) => {
  207. const idx = classTimeData.value.findIndex((item) => item.funcType == e.funcType)
  208. classTimeData.value[idx].status = res.isFinish
  209. classTimeData.value[idx].answerId = res.answerId
  210. })
  211. }
  212. const classHourData = ref()
  213. const rightNenuRef = ref()
  214. const menuClick = (e) => {
  215. rightNenuRef.value.onClose()
  216. if (e) {
  217. addClassPlan(1)
  218. }
  219. classCentre.courseTimeDetail({ id: e?.key ? e.key : selectedKeys.value[0] }).then((data) => {
  220. classHourData.value = data
  221. classTimeData.value = data.courseRelates.map((r) => {
  222. return {
  223. ...r,
  224. url: sysConfig.FILE_URL + r.url
  225. }
  226. })
  227. videoUrl.value = classTimeData.value.find((r) => r.funcType == 1)?.url
  228. nextTick(() => {
  229. videoRef.value.src = classTimeData.value.find((r) => r.funcType == 1)?.url
  230. videoStart()
  231. footprintClassAdd()
  232. getVideoTime()
  233. const funcType4Item = classTimeData.value.find((r) => r.funcType == 4)
  234. const funcType5Item = classTimeData.value.find((r) => r.funcType == 5)
  235. if (funcType4Item) {
  236. getClassStatus(funcType4Item)
  237. }
  238. if (funcType5Item) {
  239. getClassStatus(funcType5Item)
  240. }
  241. })
  242. })
  243. }
  244. const tabsActiveKey = ref('1')
  245. const noteRef = ref()
  246. const askDivRef = ref()
  247. const itemObj = computed(() => {
  248. let item = classTimeData.value.length > 0 ? classTimeData.value.find((r) => r.funcType == 2) : { url: '' }
  249. return item
  250. })
  251. const danmuObj = computed(() => {
  252. let item = classTimeData.value.length > 0 ? classTimeData.value.find((r) => r.funcType == 3) : { url: '' }
  253. return item
  254. })
  255. const videoUrl = ref('')
  256. const newsschedule = ref(0)
  257. const currTime = ref(null)
  258. const maxTime = ref(0)
  259. const initialtime = ref(0)
  260. const videoContext = ref()
  261. const timeStamp1 = ref()
  262. const allTime = ref()
  263. const biNum = ref()
  264. const currentTime = ref()
  265. const videoObj = computed(() => {
  266. return {
  267. initialtime: initialtime.value,
  268. currentTime: currTime.value
  269. }
  270. })
  271. const videoStart = () => {
  272. videoContext.value = videoRef.value
  273. if (initialtime.value > 0) {
  274. videoContext.value.currentTime = initialtime.value
  275. timeStamp1.value = initialtime.value
  276. }
  277. }
  278. const timeUpdate = (e) => {
  279. //获取当前播放时间 durationinitialtime
  280. if (timeStamp1.value != parseInt(e.target.currentTime)) {
  281. allTime.value = parseInt(e.target.duration) //视频总时长(秒)
  282. //对播放的时间进行整
  283. timeStamp1.value = parseInt(e.target.currentTime) //播放进度 (秒)
  284. biNum.value = Math.floor((timeStamp1.value / allTime.value) * 10000) / 100 //暂时没用到
  285. currentTime.value = e.target.currentTime
  286. if (e.srcElement.currentTime - currTime.value > 3) {
  287. addClassPlan(3)
  288. }
  289. if (currTime.value - e.srcElement.currentTime > 3) {
  290. addClassPlan(3)
  291. }
  292. currTime.value = e.srcElement.currentTime
  293. }
  294. }
  295. const videoJumpTime = ref(false)
  296. const videoSpeed = (e) => {
  297. videoJumpTime.value = true
  298. currentTimenew.value = parseFloat(e.startTime)
  299. }
  300. const forumData = computed(() => {
  301. let item = findNodeByKey(classTimeList.value, selectedKeys.value[0], classTimeList.value[0]) ?? {}
  302. if (!item.src) {
  303. item.src = classTimeData.value.find((r) => r.funcType == 1)?.url
  304. }
  305. return {
  306. id: classHourData.value?.id,
  307. title: item.parent?.name,
  308. videoUrl: btoa(encodeURIComponent(videoRef.value?.src ? videoRef.value.src : item.src)),
  309. courseId: route.query.id,
  310. chapterId: selectedKeys.value[0],
  311. courseName: classDetail.value?.courseName,
  312. chapterName: classHourData.value?.name
  313. }
  314. })
  315. const showPdf = ref(true)
  316. function errorHandler() {
  317. showPdf.value = false
  318. }
  319. const getVideoTime = () => {
  320. classCentre
  321. .theLastDetail({
  322. funcType: 1,
  323. type: 1,
  324. hourId: classHourData.value?.id
  325. })
  326. .then((data) => {
  327. if (data?.endTime) {
  328. initialtime.value = parseFloat(data.endTime) / 1000
  329. currentTimenew.value = parseFloat(data.endTime) / 1000
  330. }
  331. })
  332. }
  333. // 毫秒转换时分秒
  334. function msToHMS(ms) {
  335. if (typeof ms !== 'number' || isNaN(ms)) return '00:00:00'
  336. let totalSeconds = Math.floor(ms / 1000)
  337. let hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0')
  338. let minutes = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0')
  339. let seconds = String(totalSeconds % 60).padStart(2, '0')
  340. return `${hours}:${minutes}:${seconds}`
  341. }
  342. // 时分秒转换毫秒
  343. function hmsToMs(hms) {
  344. if (!hms) return 0
  345. const parts = hms.split(':').map((p) => parseFloat(p.replace(',', '.')))
  346. // 支持 hh:mm:ss 或 mm:ss
  347. let ms = 0
  348. if (parts.length === 3) {
  349. ms = (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000
  350. } else if (parts.length === 2) {
  351. ms = (parts[0] * 60 + parts[1]) * 1000
  352. } else if (parts.length === 1) {
  353. ms = parts[0] * 1000
  354. }
  355. return ms
  356. }
  357. const nowTimesStr = Date.now()
  358. const outNowTimesStr = ref()
  359. const addClassPlan = (type) => {
  360. outNowTimesStr.value = Date.now()
  361. let currentTime = videoRef.value?.currentTime
  362. let progress = parseFloat((currentTime / videoRef.value?.duration) * 100)
  363. if (
  364. (progress || progress == 0) &&
  365. (initialtime.value || initialtime.value == 0) &&
  366. (currentTime || currentTime == 0)
  367. ) {
  368. classCentre.classPlanAdd({
  369. startTime: parseFloat(initialtime.value),
  370. endTime: Math.round(currentTime * 1000),
  371. progress: Math.round(progress),
  372. hourId: classHourData.value?.id,
  373. stayTime: outNowTimesStr.value - nowTimesStr,
  374. type: type,
  375. funcType: 1,
  376. ...classDetailParams.value(1)
  377. })
  378. }
  379. }
  380. const isLike = () => {
  381. console.log('收藏状态',classDetail.value.isCollect)
  382. classCentre
  383. .classCollectAdd(
  384. {
  385. courseId: route.query.id
  386. },
  387. classDetail.value.isCollect
  388. )
  389. .then(() => {
  390. if(classDetail.value.isCollect == false){
  391. message.success('添加收藏')
  392. }else{
  393. message.warn('取消收藏')
  394. }
  395. classCentre.courseDetail({ courseId: route.query.id }).then((data) => {
  396. classDetail.value = data
  397. })
  398. })
  399. }
  400. const classDetailParams = computed(() => {
  401. return (funcType)=>{
  402. return {
  403. hourName: classHourData.value?.name,
  404. chapterName: classHourData.value?.name,
  405. courseName: classDetail.value?.courseName,
  406. hourId: classHourData.value?.id,
  407. chapterId: selectedKeys.value[0],
  408. courseId: classDetail.value?.courseId,
  409. fileId: classHourData.value?.courseRelates.find((r) => r.funcType == funcType)?.relateId,
  410. fileName: classHourData.value?.courseRelates.find((r) => r.funcType == funcType)?.name,
  411. filePath: classHourData.value?.courseRelates.find((r) => r.funcType == funcType)?.url,
  412. extendName: classHourData.value?.courseRelates.find((r) => r.funcType == funcType)?.extendName,
  413. }
  414. }
  415. })
  416. //足迹
  417. const footprintClassAdd = () => {
  418. classCentre.footprintClassAdd({
  419. ...classDetailParams.value(1)
  420. })
  421. }
  422. // 监听消息
  423. const stopListening = Broadcast.on('getClassDetail', (event) => {
  424. if (event.type == 1) {
  425. getClassData()
  426. }
  427. })
  428. const videoStopTime = (callback) => {
  429. videoRef.value.pause()
  430. callback && callback(videoRef.value?.currentTime)
  431. }
  432. const webCon = ref([
  433. {
  434. title: '1.1 集合的概念',
  435. href: '/webEmpty'
  436. },
  437. {
  438. title: '1.2 集合间的基本关系',
  439. href: '/webEmpty'
  440. },
  441. {
  442. title: '1.3 集合的基本运算',
  443. href: '/webEmpty'
  444. },
  445. {
  446. title: '1.4 充分条件与必要条件',
  447. href: '/webEmpty'
  448. },
  449. {
  450. title: '1.5 全称量词与存在量词',
  451. href: '/webEmpty'
  452. }
  453. ])
  454. onMounted(() => {
  455. getClassData()
  456. })
  457. onBeforeUnmount(() => {
  458. stopListening()
  459. addClassPlan(1)
  460. })
  461. </script>
  462. <style scoped lang="less">
  463. .classTitle {
  464. height: 64px;
  465. width: 200px;
  466. font-size: 18px;
  467. color: #ccc;
  468. display: flex;
  469. justify-content: center;
  470. align-items: center;
  471. }
  472. .flex-between {
  473. display: flex;
  474. justify-content: space-between;
  475. }
  476. </style>