CourseDetail.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <template>
  2. <div class="CourseDetail container-height">
  3. <Breadcrumb />
  4. <div>
  5. <div class="flex_1 bg_color_fff padding16 border_radius_16 box_shadow_card fit_content">
  6. <div class="gap10">
  7. <el-button type="primary">{{info.skillTagName}}</el-button>
  8. <div class="bold font_size30">{{info.courseCategoryName}}(第1期)</div>
  9. </div>
  10. <div class="gap5 mt10">
  11. <img :src="riliIcon" alt="" style="width: 16px; height: 16px;">
  12. <span class="font_size14">{{info.createTime}}</span>
  13. </div>
  14. </div>
  15. <div class="flex-between mt20">
  16. <div class="flex_1">
  17. <div class="video-player mr20">
  18. <VideoPlayer
  19. ref="videoPlayer"
  20. :src="currentVideoUrl"
  21. :poster="currentCourseCover"
  22. @play="onPlayerPlay"
  23. @pause="onPlayerPause"
  24. @ended="onPlayerEnded"
  25. @timeupdate="onPlayerTimeupdate"
  26. @loadedmetadata="onPlayerLoadedmetadata"
  27. @error="onPlayerError"
  28. @ready="onPlayerReady"
  29. />
  30. <!-- <img :src="currentCourse.cover" :alt="currentCourse.title">
  31. <div class="play-button">▶</div> -->
  32. </div>
  33. <div class="bg_color_fff padding16 border_radius_16 box_shadow_card mt20">
  34. <div class="flex-center-between border_bottom">
  35. <div class="gap10">
  36. <div class="gap5 cursor-pointer" @click="collectFn">
  37. <img :src="weishoucangIcon" alt="" style="width: 24px; height: 24px;">
  38. <span class="bold font_size14">{{$t('common.collect')}}</span>
  39. </div>
  40. <div class="gap5 cursor-pointer" @click="collectFn">
  41. <img :src="shoucangIcon" alt="" style="width: 24px; height: 24px;">
  42. <span class="bold font_size14">{{$t('common.unCollect')}}</span>
  43. </div>
  44. <div class="gap5 cursor-pointer" @click="copyCurrentUrl">
  45. <img :src="fenxiangIcon" alt="" style="width: 24px; height: 24px;">
  46. <span class="bold font_size14">{{$t('common.share')}}</span>
  47. </div>
  48. </div>
  49. <div class="addBtn flex-center gradient border_radius_10">
  50. <div class="gap10" @click="toAddNote">
  51. <img :src="addIcon" alt="" style="width:30px;height:30px">
  52. <span class="font_size18">120{{$t('common.notes')}}</span>
  53. </div>
  54. </div>
  55. </div>
  56. <div class="gray font_size16 mt10 line2" :title="info.courseIntro">
  57. {{info.courseIntro}}
  58. </div>
  59. </div>
  60. <div class="bg_color_fff padding16 border_radius_16 box_shadow_card mt20">
  61. <Pinglun :info="info"/>
  62. </div>
  63. </div>
  64. <div class="detail_right">
  65. <div class="bg_color_fff padding16 border_radius_16 box_shadow_card" v-show="!isShowNote">
  66. <CourseDirectory :info="info" />
  67. </div>
  68. <div class=" bg_color_fff padding16 border_radius_16 box_shadow_card mt10" v-show="!isShowNote">
  69. <OtherCourse />
  70. </div>
  71. <div class="isShowNote bg_color_fff padding16 border_radius_16 box_shadow_card mt10" v-show="isShowNote">
  72. <div class="CloseBold" @click="isShowNote=false">
  73. <el-icon size="18"><CloseBold /></el-icon>
  74. </div>
  75. <Xuxibiji :info="info" />
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </template>
  82. <script setup>
  83. import riliIcon from '@/assets/imgs/rili.png'
  84. import weishoucangIcon from '@/assets/imgs/weishoucang.png'
  85. import shoucangIcon from '@/assets/imgs/shoucang.png'
  86. import fenxiangIcon from '@/assets/imgs/fenxiang.png'
  87. import addIcon from '@/assets/imgs/add.png'
  88. import OtherCourse from './components/OtherCourse.vue'
  89. import CourseDirectory from './components/CourseDirectory.vue'
  90. import VideoPlayer from '@/components/VideoPlayer.vue'
  91. import Pinglun from './components/pinglun.vue'
  92. import Xuxibiji from './components/Xuxibiji.vue'
  93. import DGTMessage from '@/utils/message'
  94. import { copyText } from '@/utils/util'
  95. // 引入api
  96. import { getCourseDetail,collect } from '@/api/course.js'
  97. import { useRouter, useRoute } from 'vue-router'
  98. const router = useRouter()
  99. const route = useRoute()
  100. console.log(router,route)
  101. import { ref, computed, reactive, onMounted } from 'vue'
  102. import { useAppStore } from '@/pinia/appStore'
  103. const appStore = useAppStore()
  104. import { useI18n } from 'vue-i18n'
  105. const { t } = useI18n()
  106. //获取参数
  107. const query = route.query;
  108. const courseId = ref(route.params.courseId || '');
  109. const info = ref({})
  110. // 视频相关
  111. // const currentVideoUrl = ref('http://baomiai.oss-cn-shanghai.aliyuncs.com/video/2025/12/30/123_20251226133117A047_20251230095248A001.mp4?response-content-disposition=inline&x-oss-date=20251230T015720Z&x-oss-expires=7199&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tG5dEtkqwDGcU6AtaYE%2F20251230%2Fcn-shanghai%2Foss%2Faliyun_v4_request&x-oss-signature=f93c20421dcfdc6822cd03d42ae66f8a28acd2c4d25c79ce16e2312537eabaa5')
  112. // const currentVideoUrl = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/19/actmOvmq0xOBc8448561c73a066a523821c6f7ae4868_20251219094240A008.mp4')
  113. const currentVideoUrl = ref('')
  114. // const currentCourseCover = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/15/3120938e-8205-416e-aedc-8b25ea23cc94_20251125142306A001_20251215110507A001.png')
  115. const currentCourseCover = ref('')
  116. const currentPlayingVideoId = ref(null)
  117. const currentVideoDuration = ref(0)
  118. const currentPlayTime = ref(0)
  119. const videoPlayer = ref(null)
  120. onMounted(() => {
  121. getDetail();
  122. });
  123. const getDetail = async () => {
  124. getCourseDetail({id: courseId.value}).then(res => {
  125. if(res.code === 200){
  126. console.log(res)
  127. info.value = res.data || {};
  128. currentCourseCover.value = info.value.coverImageUrl || ''
  129. }
  130. })
  131. };
  132. // 打开添加对话框
  133. const isShowNote = ref(false)
  134. const toAddNote = () => {
  135. isShowNote.value = true
  136. };
  137. // 播放指定视频
  138. const playVideo = (video) => {
  139. if (!video) return
  140. // 更新当前播放的视频ID
  141. currentPlayingVideoId.value = video.id
  142. // 这里可以根据video.id从API获取实际的视频地址
  143. // 暂时使用模拟地址
  144. // const videoUrl = `https://example.com/videos/${video.id}.mp4`
  145. // 更新视频源
  146. // currentVideoUrl.value = videoUrl
  147. // 播放视频
  148. if (videoPlayer.value) {
  149. videoPlayer.value.play()
  150. }
  151. }
  152. // 播放器事件处理
  153. const onPlayerPlay = () => {
  154. console.log('视频开始播放')
  155. }
  156. const onPlayerPause = () => {
  157. console.log('视频暂停')
  158. }
  159. const onPlayerEnded = () => {
  160. console.log('视频播放结束')
  161. // 自动播放下一个视频
  162. playNextVideo()
  163. }
  164. const onPlayerTimeupdate = (time) => {
  165. currentPlayTime.value = time
  166. // 这里可以保存播放进度
  167. console.log('当前播放时间:', time)
  168. }
  169. const onPlayerLoadedmetadata = (duration) => {
  170. currentVideoDuration.value = duration
  171. console.log('视频时长:', duration)
  172. }
  173. const onPlayerError = (error) => {
  174. console.error('视频播放错误:', error)
  175. // DGTMessage.error('视频播放失败,请稍后再试')
  176. }
  177. const onPlayerReady = (player) => {
  178. console.log('播放器就绪', player)
  179. // 可以在这里进行高级操作
  180. }
  181. // 播放下一个视频
  182. const playNextVideo = () => {
  183. if (!currentCourseChapters.value || currentPlayingVideoId.value === null) return
  184. // 查找当前播放视频的位置
  185. let currentIndex = -1
  186. let chapterIndex = -1
  187. for (let i = 0; i < currentCourseChapters.value.length; i++) {
  188. const chapter = currentCourseChapters.value[i]
  189. const index = chapter.videos.findIndex(v => v.id === currentPlayingVideoId.value)
  190. if (index !== -1) {
  191. chapterIndex = i
  192. currentIndex = index
  193. break
  194. }
  195. }
  196. // 如果找到当前视频
  197. if (chapterIndex !== -1 && currentIndex !== -1) {
  198. const currentChapter = currentCourseChapters.value[chapterIndex]
  199. // 如果当前章节还有下一个视频
  200. if (currentIndex < currentChapter.videos.length - 1) {
  201. playVideo(currentChapter.videos[currentIndex + 1])
  202. }
  203. // 如果是当前章节最后一个视频,且有下一个章节
  204. else if (chapterIndex < currentCourseChapters.value.length - 1) {
  205. playVideo(currentCourseChapters.value[chapterIndex + 1].videos[0])
  206. }
  207. }
  208. };
  209. const collectFn = () => {
  210. collect({objectId: courseId.value}).then(res => {
  211. if(res.code === 200){
  212. info.value.isCollect = !info.value.isCollect
  213. let msg = info.value.isCollect ? t('common.unCollect') : t('common.collect')
  214. DGTMessage.success(msg+t('common.success'))
  215. }
  216. })
  217. };
  218. //负责当前页面地址
  219. // 复制当前页面地址
  220. const copyCurrentUrl = () => {
  221. copyText(window.location.href, t)
  222. }
  223. </script>
  224. <style scoped lang="scss">
  225. .CourseDetail{
  226. .addBtn{
  227. cursor: pointer;
  228. padding: 10px 20px;
  229. color: #fff;
  230. }
  231. .course-video {
  232. display: flex;
  233. gap: 20px;
  234. margin-bottom: 40px;
  235. // height: 500px; /* 设置容器高度,可根据需要调整 */
  236. @media (max-width: 768px) {
  237. flex-direction: column;
  238. }
  239. .video-player {
  240. flex: 2;
  241. position: relative;
  242. // height: 500px;
  243. img {
  244. width: 100%;
  245. height: auto;
  246. border-radius: 8px;
  247. }
  248. .play-button {
  249. position: absolute;
  250. top: 50%;
  251. left: 50%;
  252. transform: translate(-50%, -50%);
  253. width: 80px;
  254. height: 80px;
  255. background: rgba(0,0,0,0.5);
  256. color: white;
  257. border-radius: 50%;
  258. display: flex;
  259. align-items: center;
  260. justify-content: center;
  261. font-size: 30px;
  262. cursor: pointer;
  263. }
  264. }
  265. .video-info {
  266. flex: 1;
  267. h2 {
  268. font-size: 1.8rem;
  269. margin-bottom: 15px;
  270. }
  271. .course-meta {
  272. margin-bottom: 15px;
  273. color: #666;
  274. span {
  275. display: block;
  276. margin-bottom: 5px;
  277. }
  278. }
  279. .course-desc {
  280. margin-bottom: 20px;
  281. line-height: 1.6;
  282. }
  283. }
  284. }
  285. .isShowNote{
  286. position: relative;
  287. .CloseBold{
  288. position: absolute;
  289. top: 16px;
  290. right: 16px;
  291. cursor: pointer;
  292. }
  293. }
  294. }
  295. </style>