CourseDetail.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. <template>
  2. <div class="course-detail-page" v-if="currentCourse">
  3. <!-- <el-page-header @back="goBack">
  4. <template #content>{{ currentCourse.title }}</template>
  5. </el-page-header> -->
  6. <Breadcrumb />
  7. <div class="course-detail">
  8. <div class="course-main">
  9. <div class="course-video">
  10. <!-- 视频播放器占位 -->
  11. <div class="video-player">
  12. <VideoPlayer
  13. ref="videoPlayer"
  14. :src="currentVideoUrl"
  15. :poster="currentCourse.cover"
  16. @play="onPlayerPlay"
  17. @pause="onPlayerPause"
  18. @ended="onPlayerEnded"
  19. @timeupdate="onPlayerTimeupdate"
  20. @loadedmetadata="onPlayerLoadedmetadata"
  21. @error="onPlayerError"
  22. @ready="onPlayerReady"
  23. />
  24. <!-- <img :src="currentCourse.cover" :alt="currentCourse.title">
  25. <div class="play-button">▶</div> -->
  26. </div>
  27. <div class="video-info">
  28. <h2>{{ currentCourse.title }}</h2>
  29. <div class="course-meta">
  30. <span>讲师:{{ currentCourse.teacher }}</span>
  31. <span>价格:¥{{ currentCourse.price }}</span>
  32. </div>
  33. <p class="course-desc">{{ currentCourse.description }}</p>
  34. <el-button type="primary" size="large">立即学习</el-button>
  35. </div>
  36. </div>
  37. <div class="course-chapters">
  38. <h3>课程章节</h3>
  39. <el-collapse>
  40. <el-collapse-item
  41. v-for="chapter in currentCourseChapters"
  42. :key="chapter.id"
  43. :title="chapter.title"
  44. @click="playVideo(video)"
  45. >
  46. <div
  47. v-for="video in chapter.videos"
  48. :key="video.id"
  49. class="chapter-video"
  50. >
  51. <el-icon><VideoPlay /></el-icon>
  52. {{ video.title }} ({{ video.duration }})
  53. </div>
  54. </el-collapse-item>
  55. </el-collapse>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. </template>
  61. <script setup>
  62. import { onMounted,ref } from 'vue'
  63. import { useRoute, useRouter } from 'vue-router'
  64. import { useCourseStore } from '@/pinia/courseStore'
  65. import { VideoPlay } from '@element-plus/icons-vue'
  66. import VideoPlayer from '@/components/VideoPlayer.vue'
  67. import DGTMessage from '@/utils/message'
  68. const route = useRoute()
  69. const router = useRouter()
  70. const courseStore = useCourseStore()
  71. const courseId = route.params.id
  72. const currentCourse = ref(null)
  73. const currentCourseChapters = ref(null)
  74. const videoPlayer = ref(null)
  75. // const currentVideoUrl = ref('')
  76. // const currentVideoUrl = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/19/actmOvmq0xOBc8448561c73a066a523821c6f7ae4868_20251219094240A008.mp4')
  77. // const currentVideoUrl = ref('http://baomiai.oss-cn-shanghai.aliyuncs.com/video/2025/12/30/123_20251226133117A047_20251230095248A001.mp4?response-content-disposition=inline&response-content-type=video%2Fmp4&x-oss-date=20251230T015258Z&x-oss-expires=7200&x-oss-signature-version=OSS4-HMAC-SHA256&x-oss-credential=LTAI5tG5dEtkqwDGcU6AtaYE%2F20251230%2Fcn-shanghai%2Foss%2Faliyun_v4_reque')
  78. 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')
  79. const currentPlayingVideoId = ref(null)
  80. const currentVideoDuration = ref(0)
  81. const currentPlayTime = ref(0)
  82. onMounted(() => {
  83. courseStore.fetchCourseDetail(courseId)
  84. courseStore.fetchCourseChapters(courseId)
  85. currentCourse.value = courseStore.currentCourse
  86. currentCourseChapters.value = courseStore.currentCourseChapters
  87. })
  88. const goBack = () => {
  89. router.back()
  90. }
  91. // 播放指定视频
  92. const playVideo = (video) => {
  93. if (!video) return
  94. // 更新当前播放的视频ID
  95. currentPlayingVideoId.value = video.id
  96. // 这里可以根据video.id从API获取实际的视频地址
  97. // 暂时使用模拟地址
  98. const videoUrl = `https://example.com/videos/${video.id}.mp4`
  99. // 更新视频源
  100. currentVideoUrl.value = videoUrl
  101. // 播放视频
  102. if (videoPlayer.value) {
  103. videoPlayer.value.play()
  104. }
  105. }
  106. // 播放器事件处理
  107. const onPlayerPlay = () => {
  108. console.log('视频开始播放')
  109. }
  110. const onPlayerPause = () => {
  111. console.log('视频暂停')
  112. }
  113. const onPlayerEnded = () => {
  114. console.log('视频播放结束')
  115. // 自动播放下一个视频
  116. playNextVideo()
  117. }
  118. const onPlayerTimeupdate = (time) => {
  119. currentPlayTime.value = time
  120. // 这里可以保存播放进度
  121. console.log('当前播放时间:', time)
  122. }
  123. const onPlayerLoadedmetadata = (duration) => {
  124. currentVideoDuration.value = duration
  125. console.log('视频时长:', duration)
  126. }
  127. const onPlayerError = (error) => {
  128. console.error('视频播放错误:', error)
  129. DGTMessage.error('视频播放失败,请稍后再试')
  130. }
  131. const onPlayerReady = (player) => {
  132. console.log('播放器就绪', player)
  133. // 可以在这里进行高级操作
  134. }
  135. // 播放下一个视频
  136. const playNextVideo = () => {
  137. if (!currentCourseChapters.value || currentPlayingVideoId.value === null) return
  138. // 查找当前播放视频的位置
  139. let currentIndex = -1
  140. let chapterIndex = -1
  141. for (let i = 0; i < currentCourseChapters.value.length; i++) {
  142. const chapter = currentCourseChapters.value[i]
  143. const index = chapter.videos.findIndex(v => v.id === currentPlayingVideoId.value)
  144. if (index !== -1) {
  145. chapterIndex = i
  146. currentIndex = index
  147. break
  148. }
  149. }
  150. // 如果找到当前视频
  151. if (chapterIndex !== -1 && currentIndex !== -1) {
  152. const currentChapter = currentCourseChapters.value[chapterIndex]
  153. // 如果当前章节还有下一个视频
  154. if (currentIndex < currentChapter.videos.length - 1) {
  155. playVideo(currentChapter.videos[currentIndex + 1])
  156. }
  157. // 如果是当前章节最后一个视频,且有下一个章节
  158. else if (chapterIndex < currentCourseChapters.value.length - 1) {
  159. playVideo(currentCourseChapters.value[chapterIndex + 1].videos[0])
  160. }
  161. }
  162. }
  163. // 格式化时间
  164. const formatTime = (seconds) => {
  165. if (!seconds || isNaN(seconds)) return '00:00'
  166. const minutes = Math.floor(seconds / 60)
  167. const secs = Math.floor(seconds % 60)
  168. return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
  169. }
  170. </script>
  171. <style scoped lang="scss">
  172. .course-detail {
  173. .course-video {
  174. display: flex;
  175. gap: 20px;
  176. margin-bottom: 40px;
  177. // height: 500px; /* 设置容器高度,可根据需要调整 */
  178. @media (max-width: 768px) {
  179. flex-direction: column;
  180. }
  181. .video-player {
  182. flex: 2;
  183. position: relative;
  184. // height: 500px;
  185. img {
  186. width: 100%;
  187. height: auto;
  188. border-radius: 8px;
  189. }
  190. .play-button {
  191. position: absolute;
  192. top: 50%;
  193. left: 50%;
  194. transform: translate(-50%, -50%);
  195. width: 80px;
  196. height: 80px;
  197. background: rgba(0,0,0,0.5);
  198. color: white;
  199. border-radius: 50%;
  200. display: flex;
  201. align-items: center;
  202. justify-content: center;
  203. font-size: 30px;
  204. cursor: pointer;
  205. }
  206. }
  207. .video-info {
  208. flex: 1;
  209. h2 {
  210. font-size: 1.8rem;
  211. margin-bottom: 15px;
  212. }
  213. .course-meta {
  214. margin-bottom: 15px;
  215. color: #666;
  216. span {
  217. display: block;
  218. margin-bottom: 5px;
  219. }
  220. }
  221. .course-desc {
  222. margin-bottom: 20px;
  223. line-height: 1.6;
  224. }
  225. }
  226. }
  227. .course-chapters {
  228. h3 {
  229. font-size: 1.5rem;
  230. margin-bottom: 15px;
  231. }
  232. .chapter-video {
  233. padding: 10px;
  234. border-bottom: 1px solid #eee;
  235. cursor: pointer;
  236. &:hover {
  237. background-color: #f5f5f5;
  238. }
  239. }
  240. }
  241. }
  242. </style>