Преглед изворни кода

```
feat(course): 新增课程收藏和分享功能

- 新增collect API接口,实现收藏/取消收藏功能
- 添加国际化文案:收藏、取消收藏、分享、复制成功/失败等
- 在课程详情页实现收藏按钮点击功能
- 实现页面链接复制分享功能
- 优化课程详情页UI显示,动态展示课程信息
- 完善章节目录组件的选中状态和跳转逻辑
- 添加视频播放相关功能的优化和注释
```

zhangningning пре 1 месец
родитељ
комит
6c4636ad77

+ 4 - 0
src/api/course.js

@@ -19,4 +19,8 @@ export function getDictType(data = {}) {
 // 查询章节列表
 export function getChaptersList(data = {}) {
   return request.get('/chapter/chaptersList/'+data.id)
+}
+// 新增删除收藏记录(未被收藏新增,否则删除)
+export function collect(data = {}) {
+  return request.post('/collect',data)
 }

+ 6 - 0
src/locales/en.js

@@ -45,6 +45,12 @@ export default {
     send: 'Send',
     pleaseInputTitle: 'Please input title',
     pleaseInputContent: 'Please input content',
+    unCollect: 'Uncollect',
+    collect: 'Collect',
+    share: 'Share',
+    notes: ' Notes',
+    copyError: 'Copy Failed',
+    copySuccess: 'Copy Success',
   },
   login: {
     smsLogin: 'SMS Login',

+ 6 - 0
src/locales/zh-CN.js

@@ -46,6 +46,12 @@ export default {
     send: '发送',
     pleaseInputTitle: '请输入标题',
     pleaseInputContent: '请输入内容',
+    unCollect: '取消收藏',
+    collect: '收藏',
+    share: '分享',
+    notes: '篇笔记',
+    copyError: '复制失败',
+    copySuccess: '复制成功',
   },
   login: {
     smsLogin: '短信登录',

+ 41 - 16
src/pages/LearningSystem/CourseDetail.vue

@@ -4,12 +4,12 @@
     <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>
+          <el-button type="primary">{{info.skillTagName}}</el-button>
+          <div class="bold font_size30">{{info.courseCategoryName}}(第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>
+          <span class="font_size14">{{info.createTime}}</span>
         </div>
       </div>
       <div class="flex-between mt20">
@@ -33,26 +33,29 @@
           <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">
+                <div class="gap5 cursor-pointer" @click="collectFn">
                   <img :src="weishoucangIcon" alt="" style="width: 24px; height: 24px;">
-                  <span class="bold font_size14">未收藏</span>
+                  <span class="bold font_size14">{{$t('common.collect')}}</span>
                 </div>
-                <div class="gap5">
+                <div class="gap5 cursor-pointer"  @click="collectFn">
                   <img :src="shoucangIcon" alt="" style="width: 24px; height: 24px;">
-                  <span class="bold font_size14">已收藏</span>
+                  <span class="bold font_size14">{{$t('common.unCollect')}}</span>
                 </div>
-                <div class="gap5">
+                <div class="gap5 cursor-pointer" @click="copyCurrentUrl">
                   <img :src="fenxiangIcon" alt="" style="width: 24px; height: 24px;">
-                  <span class="bold font_size14">分享</span>
+                  <span class="bold font_size14">{{$t('common.share')}}</span>
                 </div>
               </div>
               <div class="addBtn flex-center gradient border_radius_10">
                 <div class="gap10" @click="toAddNote">
                   <img :src="addIcon" alt="" style="width:30px;height:30px">
-                  <span class="font_size18">120篇笔记</span>
+                  <span class="font_size18">120{{$t('common.notes')}}</span>
                 </div>
               </div>
             </div>
+            <div class="gray font_size16 mt10 line2" :title="info.courseIntro">
+              {{info.courseIntro}}
+            </div>
           </div>
           <div class="bg_color_fff padding16 border_radius_16 box_shadow_card mt20">
             <Pinglun :info="info"/>
@@ -90,9 +93,10 @@ import VideoPlayer from '@/components/VideoPlayer.vue'
 import Pinglun from './components/pinglun.vue'
 import Xuxibiji from './components/Xuxibiji.vue'
 import DGTMessage from '@/utils/message'
+import { copyText } from '@/utils/util'
 
 // 引入api
-import { getCourseDetail } from '@/api/course.js'
+import { getCourseDetail,collect } from '@/api/course.js'
 
 import { useRouter, useRoute } from 'vue-router'
 const router = useRouter()
@@ -102,6 +106,9 @@ import { ref, computed, reactive, onMounted } from 'vue'
 import { useAppStore } from '@/pinia/appStore'
 const appStore = useAppStore()
 
+import { useI18n } from 'vue-i18n' 
+const { t } = useI18n() 
+
 //获取参数
 const query = route.query;
 const courseId = ref(route.params.courseId || '');
@@ -109,8 +116,10 @@ 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 currentVideoUrl = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/19/actmOvmq0xOBc8448561c73a066a523821c6f7ae4868_20251219094240A008.mp4')
+const currentVideoUrl = ref('')
+// const currentCourseCover = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/15/3120938e-8205-416e-aedc-8b25ea23cc94_20251125142306A001_20251215110507A001.png')
+const currentCourseCover = ref('')
 const currentPlayingVideoId = ref(null)
 const currentVideoDuration = ref(0)
 const currentPlayTime = ref(0)
@@ -124,6 +133,7 @@ const getDetail = async () => {
     if(res.code === 200){
       console.log(res)
       info.value = res.data || {};
+      currentCourseCover.value = info.value.coverImageUrl || ''
     }
   })
 };
@@ -143,10 +153,10 @@ const playVideo = (video) => {
   
   // 这里可以根据video.id从API获取实际的视频地址
   // 暂时使用模拟地址
-  const videoUrl = `https://example.com/videos/${video.id}.mp4`
+  // const videoUrl = `https://example.com/videos/${video.id}.mp4`
   
   // 更新视频源
-  currentVideoUrl.value = videoUrl
+  // currentVideoUrl.value = videoUrl
   
   // 播放视频
   if (videoPlayer.value) {
@@ -183,7 +193,7 @@ const onPlayerLoadedmetadata = (duration) => {
 
 const onPlayerError = (error) => {
   console.error('视频播放错误:', error)
-  DGTMessage.error('视频播放失败,请稍后再试')
+  // DGTMessage.error('视频播放失败,请稍后再试')
 }
 
 const onPlayerReady = (player) => {
@@ -224,6 +234,21 @@ const playNextVideo = () => {
     }
   }
 };
+const collectFn = () => {
+  collect({objectId: courseId.value}).then(res => {
+    if(res.code === 200){
+      info.value.isCollect = !info.value.isCollect
+      let msg = info.value.isCollect ? t('common.unCollect') : t('common.collect')
+      DGTMessage.success(msg+t('common.success'))
+    }
+  })
+};
+//负责当前页面地址
+// 复制当前页面地址
+const copyCurrentUrl = () => {
+  copyText(window.location.href, t)
+}
+
 
 
 </script>

+ 2 - 2
src/pages/LearningSystem/LearningSystemDetail.vue

@@ -20,7 +20,7 @@
             <el-button type="primary"  plain v-if="info.studyStageName">{{info.studyStageName}}</el-button>
             <el-button type="primary" plain v-if="info.courseCategoryName">{{info.courseCategoryName}}</el-button>
           </div>
-          <div class="gray font_size16 mt10 line5">
+          <div class="gray font_size16 mt10 line2" :title="info.courseIntro">
             {{info.courseIntro}}
           </div>
         </div>
@@ -44,7 +44,7 @@
         <div class="flex_1 bg_color_fff padding16 border_radius_16 box_shadow_card mr20">
           <el-tabs v-model="activeName" class="demo-tabs">
             <el-tab-pane :label="$t('common.kechengjieshao')" name="first">
-              <CourseDescription :info="info" />
+              <CourseDescription :info="info" formPage="LearningSystemDetail" />
             </el-tab-pane>
             <el-tab-pane :label="$t('common.kechengmulu')" name="kechengmulu">
               <keep-alive>

+ 26 - 2
src/pages/LearningSystem/components/CourseDirectory.vue

@@ -5,12 +5,13 @@
       <div class="font_size18 bold">{{$t('common.kechengmulu')}} (1/{{list.length}})</div>
     </div>
     <div class="flex-center-between font_size16 gray list_item" 
-    :class="{'active': index === 0}"
+    @click="handleCheck(item)"
+    :class="{'active': selectedItem.chapterId === item.chapterId}"
     v-for="(item, index) in list" :key="index">
       <div class="gap10">
         <div>{{item.chapterOrderName}}</div>
         <div class="gap5">
-          <img :src="muluIcon" alt="" style="width:20px;height:20px" v-if="index === 0">
+          <img :src="muluIcon" alt="" style="width:20px;height:20px" v-show="selectedItem.chapterId === item.chapterId">
           <span>{{item.chapterName}}</span>
         </div>
       </div>
@@ -26,8 +27,14 @@ const props = defineProps({
   info: {
     type: Object,
     default: () => ({})
+  },
+  formPage: {
+    type: String,
+    default: ''
   }
 })
+const selectedItem = ref({});//选中的章节
+
 
 //监听props.info.courseId变化
 watch(() => props.info.courseId, (newVal, oldVal) => {
@@ -48,10 +55,27 @@ const getChaptersListFn = () => {
    getChaptersList({id: props.info.courseId}).then(res => {
     if(res.code === 200){
       list.value = res.rows || [];
+      if(list.value.length > 0){
+        selectedItem.value = list.value[0];
+      }
     }
   })
 };
 
+
+const handleCheck = (item) => {
+  selectedItem.value = item;
+  if(props.formPage === 'LearningSystemDetail'){
+    // 学习系统详情页点击章节跳转
+    window.location.href = '#/LearningSystemDetail/' + props.info.courseId + '/' + item.chapterId;
+  }else{
+    // 课程详情页点击章节跳转
+    window.location.href = '#/CourseDetail/' + props.info.courseId + '/' + item.chapterId;
+  }
+}
+
+
+
 </script>
 <style scoped lang="scss">
   .kechengmulu{

+ 14 - 0
src/utils/util.js

@@ -113,3 +113,17 @@ export function isLogin({callback,t}){
   }
   return true;
 }
+//复制功能
+export function copyText(text, t) {
+  if (!text) {
+    DGTMessage.error(t('common.copyError'));
+    return;
+  }
+  const input = document.createElement('input');
+  input.value = text;
+  document.body.appendChild(input);
+  input.select();
+  document.execCommand('copy');
+  document.body.removeChild(input);
+  DGTMessage.success(t('common.copySuccess'));
+}