|
@@ -0,0 +1,299 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="CourseDetail container-height">
|
|
|
|
|
+ <Breadcrumb />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div class="flex_1 bg_color_fff padding16 border_radius_16 box_shadow_card fit_content">
|
|
|
|
|
+ <div class="gap10">
|
|
|
|
|
+ <el-button type="primary">标签</el-button>
|
|
|
|
|
+ <div class="bold font_size30">UI设计求职实战班(第1期)</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="gap5 mt10">
|
|
|
|
|
+ <img :src="riliIcon" alt="" style="width: 16px; height: 16px;">
|
|
|
|
|
+ <span class="font_size14">2023-10-10 00:00:00</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex-between mt20">
|
|
|
|
|
+ <div class="flex_1">
|
|
|
|
|
+ <div class="video-player mr20">
|
|
|
|
|
+ <VideoPlayer
|
|
|
|
|
+ ref="videoPlayer"
|
|
|
|
|
+ :src="currentVideoUrl"
|
|
|
|
|
+ :poster="currentCourseCover"
|
|
|
|
|
+ @play="onPlayerPlay"
|
|
|
|
|
+ @pause="onPlayerPause"
|
|
|
|
|
+ @ended="onPlayerEnded"
|
|
|
|
|
+ @timeupdate="onPlayerTimeupdate"
|
|
|
|
|
+ @loadedmetadata="onPlayerLoadedmetadata"
|
|
|
|
|
+ @error="onPlayerError"
|
|
|
|
|
+ @ready="onPlayerReady"
|
|
|
|
|
+ />
|
|
|
|
|
+ <!-- <img :src="currentCourse.cover" :alt="currentCourse.title">
|
|
|
|
|
+ <div class="play-button">▶</div> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bg_color_fff padding16 border_radius_16 box_shadow_card mt20">
|
|
|
|
|
+ <div class="flex-center-between border_bottom">
|
|
|
|
|
+ <div class="gap10">
|
|
|
|
|
+ <div class="gap5">
|
|
|
|
|
+ <img :src="weishoucangIcon" alt="" style="width: 24px; height: 24px;">
|
|
|
|
|
+ <span class="bold font_size14">未收藏</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="gap5">
|
|
|
|
|
+ <img :src="shoucangIcon" alt="" style="width: 24px; height: 24px;">
|
|
|
|
|
+ <span class="bold font_size14">已收藏</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="gap5">
|
|
|
|
|
+ <img :src="fenxiangIcon" alt="" style="width: 24px; height: 24px;">
|
|
|
|
|
+ <span class="bold font_size14">分享</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="addBtn flex-center gradient border_radius_10">
|
|
|
|
|
+ <div class="gap10" @click="openAddDialog">
|
|
|
|
|
+ <img :src="addIcon" alt="" style="width:30px;height:30px">
|
|
|
|
|
+ <span class="font_size18">{{$t('common.add')}}{{$t('common.xuxibiji')}}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="bg_color_fff padding16 border_radius_16 box_shadow_card mt20">
|
|
|
|
|
+ <Pinglun :info="info"/>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="detail_right">
|
|
|
|
|
+ <div class="bg_color_fff padding16 border_radius_16 box_shadow_card">
|
|
|
|
|
+ <CourseDirectory :info="info" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class=" bg_color_fff padding16 border_radius_16 box_shadow_card mt10">
|
|
|
|
|
+ <OtherCourse />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <BlockNoteEditorDialog ref="blockNoteEditorDialogRef" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup>
|
|
|
|
|
+import riliIcon from '@/assets/imgs/rili.png'
|
|
|
|
|
+import weishoucangIcon from '@/assets/imgs/weishoucang.png'
|
|
|
|
|
+import shoucangIcon from '@/assets/imgs/shoucang.png'
|
|
|
|
|
+import fenxiangIcon from '@/assets/imgs/fenxiang.png'
|
|
|
|
|
+import addIcon from '@/assets/imgs/add.png'
|
|
|
|
|
+
|
|
|
|
|
+import OtherCourse from './components/OtherCourse.vue'
|
|
|
|
|
+import CourseDirectory from './components/CourseDirectory.vue'
|
|
|
|
|
+import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
|
|
|
+import Pinglun from './components/pinglun.vue'
|
|
|
|
|
+import BlockNoteEditorDialog from '@/components/BlockNoteEditorDialog.vue'
|
|
|
|
|
+import DGTMessage from '@/utils/message'
|
|
|
|
|
+
|
|
|
|
|
+// 引入api
|
|
|
|
|
+import { getCourseDetail } from '@/api/course.js'
|
|
|
|
|
+
|
|
|
|
|
+import { useRouter, useRoute } from 'vue-router'
|
|
|
|
|
+const router = useRouter()
|
|
|
|
|
+const route = useRoute()
|
|
|
|
|
+console.log(router,route)
|
|
|
|
|
+import { ref, computed, reactive, onMounted } from 'vue'
|
|
|
|
|
+import { useAppStore } from '@/pinia/appStore'
|
|
|
|
|
+const appStore = useAppStore()
|
|
|
|
|
+
|
|
|
|
|
+//获取参数
|
|
|
|
|
+const query = route.query;
|
|
|
|
|
+const courseId = ref(query.courseId || '');
|
|
|
|
|
+const info = ref({})
|
|
|
|
|
+
|
|
|
|
|
+// 视频相关
|
|
|
|
|
+// 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')
|
|
|
|
|
+const currentVideoUrl = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/19/actmOvmq0xOBc8448561c73a066a523821c6f7ae4868_20251219094240A008.mp4')
|
|
|
|
|
+const currentCourseCover = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/15/3120938e-8205-416e-aedc-8b25ea23cc94_20251125142306A001_20251215110507A001.png')
|
|
|
|
|
+const currentPlayingVideoId = ref(null)
|
|
|
|
|
+const currentVideoDuration = ref(0)
|
|
|
|
|
+const currentPlayTime = ref(0)
|
|
|
|
|
+const videoPlayer = ref(null)
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ getDetail();
|
|
|
|
|
+});
|
|
|
|
|
+const getDetail = async () => {
|
|
|
|
|
+ getCourseDetail({id: courseId.value}).then(res => {
|
|
|
|
|
+ if(res.code === 200){
|
|
|
|
|
+ console.log(res)
|
|
|
|
|
+ info.value = res.data || {};
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 打开添加对话框
|
|
|
|
|
+const blockNoteEditorDialogRef = ref(null)
|
|
|
|
|
+const openAddDialog = () => {
|
|
|
|
|
+ blockNoteEditorDialogRef.value.openDialog();
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 播放指定视频
|
|
|
|
|
+const playVideo = (video) => {
|
|
|
|
|
+ if (!video) return
|
|
|
|
|
+
|
|
|
|
|
+ // 更新当前播放的视频ID
|
|
|
|
|
+ currentPlayingVideoId.value = video.id
|
|
|
|
|
+
|
|
|
|
|
+ // 这里可以根据video.id从API获取实际的视频地址
|
|
|
|
|
+ // 暂时使用模拟地址
|
|
|
|
|
+ const videoUrl = `https://example.com/videos/${video.id}.mp4`
|
|
|
|
|
+
|
|
|
|
|
+ // 更新视频源
|
|
|
|
|
+ currentVideoUrl.value = videoUrl
|
|
|
|
|
+
|
|
|
|
|
+ // 播放视频
|
|
|
|
|
+ if (videoPlayer.value) {
|
|
|
|
|
+ videoPlayer.value.play()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 播放器事件处理
|
|
|
|
|
+const onPlayerPlay = () => {
|
|
|
|
|
+ console.log('视频开始播放')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerPause = () => {
|
|
|
|
|
+ console.log('视频暂停')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerEnded = () => {
|
|
|
|
|
+ console.log('视频播放结束')
|
|
|
|
|
+
|
|
|
|
|
+ // 自动播放下一个视频
|
|
|
|
|
+ playNextVideo()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerTimeupdate = (time) => {
|
|
|
|
|
+ currentPlayTime.value = time
|
|
|
|
|
+ // 这里可以保存播放进度
|
|
|
|
|
+ console.log('当前播放时间:', time)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerLoadedmetadata = (duration) => {
|
|
|
|
|
+ currentVideoDuration.value = duration
|
|
|
|
|
+ console.log('视频时长:', duration)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerError = (error) => {
|
|
|
|
|
+ console.error('视频播放错误:', error)
|
|
|
|
|
+ DGTMessage.error('视频播放失败,请稍后再试')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const onPlayerReady = (player) => {
|
|
|
|
|
+ console.log('播放器就绪', player)
|
|
|
|
|
+ // 可以在这里进行高级操作
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 播放下一个视频
|
|
|
|
|
+const playNextVideo = () => {
|
|
|
|
|
+ if (!currentCourseChapters.value || currentPlayingVideoId.value === null) return
|
|
|
|
|
+
|
|
|
|
|
+ // 查找当前播放视频的位置
|
|
|
|
|
+ let currentIndex = -1
|
|
|
|
|
+ let chapterIndex = -1
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < currentCourseChapters.value.length; i++) {
|
|
|
|
|
+ const chapter = currentCourseChapters.value[i]
|
|
|
|
|
+ const index = chapter.videos.findIndex(v => v.id === currentPlayingVideoId.value)
|
|
|
|
|
+
|
|
|
|
|
+ if (index !== -1) {
|
|
|
|
|
+ chapterIndex = i
|
|
|
|
|
+ currentIndex = index
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果找到当前视频
|
|
|
|
|
+ if (chapterIndex !== -1 && currentIndex !== -1) {
|
|
|
|
|
+ const currentChapter = currentCourseChapters.value[chapterIndex]
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前章节还有下一个视频
|
|
|
|
|
+ if (currentIndex < currentChapter.videos.length - 1) {
|
|
|
|
|
+ playVideo(currentChapter.videos[currentIndex + 1])
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果是当前章节最后一个视频,且有下一个章节
|
|
|
|
|
+ else if (chapterIndex < currentCourseChapters.value.length - 1) {
|
|
|
|
|
+ playVideo(currentCourseChapters.value[chapterIndex + 1].videos[0])
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+ .CourseDetail{
|
|
|
|
|
+ .addBtn{
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ }
|
|
|
|
|
+ .course-video {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ margin-bottom: 40px;
|
|
|
|
|
+ // height: 500px; /* 设置容器高度,可根据需要调整 */
|
|
|
|
|
+
|
|
|
|
|
+ @media (max-width: 768px) {
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-player {
|
|
|
|
|
+ flex: 2;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ // height: 500px;
|
|
|
|
|
+
|
|
|
|
|
+ img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: auto;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .play-button {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ height: 80px;
|
|
|
|
|
+ background: rgba(0,0,0,0.5);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ font-size: 30px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .video-info {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ h2 {
|
|
|
|
|
+ font-size: 1.8rem;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .course-meta {
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+
|
|
|
|
|
+ span {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .course-desc {
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+</style>
|