Procházet zdrojové kódy

feat(video): 引入 video.js 实现课程视频播放功能

新增 video.js 播放器组件,替换原有图片占位符,支持视频播放、暂停、
结束、进度更新等事件。实现章节视频列表点击播放及自动播放下一个视频功能。
添加播放器就绪、错误处理等回调逻辑,并引入相关依赖包。
zhangningning před 6 dny
rodič
revize
eb2b322897
4 změnil soubory, kde provedl 616 přidání a 3 odebrání
  1. 215 1
      package-lock.json
  2. 1 0
      package.json
  3. 270 0
      src/components/VideoPlayer.vue
  4. 130 2
      src/pages/CourseDetail.vue

+ 215 - 1
package-lock.json

@@ -19,6 +19,7 @@
         "react-dom": "^18.3.1",
         "sass": "^1.94.2",
         "veaury": "^2.6.3",
+        "video.js": "^8.23.4",
         "vue": "^3.5.24",
         "vue-i18n": "^9.14.5",
         "vue-react-wrapper": "^0.3.1",
@@ -67,7 +68,6 @@
       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
       "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
       "license": "MIT",
-      "peer": true,
       "engines": {
         "node": ">=6.9.0"
       }
@@ -1895,6 +1895,54 @@
       "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
       "license": "ISC"
     },
+    "node_modules/@videojs/http-streaming": {
+      "version": "3.17.2",
+      "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.17.2.tgz",
+      "integrity": "sha512-VBQ3W4wnKnVKb/limLdtSD2rAd5cmHN70xoMf4OmuDd0t2kfJX04G+sfw6u2j8oOm2BXYM9E1f4acHruqKnM1g==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "aes-decrypter": "^4.0.2",
+        "global": "^4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "7.1.0",
+        "video.js": "^7 || ^8"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      },
+      "peerDependencies": {
+        "video.js": "^8.19.0"
+      }
+    },
+    "node_modules/@videojs/vhs-utils": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-4.1.1.tgz",
+      "integrity": "sha512-5iLX6sR2ownbv4Mtejw6Ax+naosGvoT9kY+gcuHzANyUZZ+4NpeNdKMUhb6ag0acYej1Y7cmr/F2+4PrggMiVA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
+    "node_modules/@videojs/xhr": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.7.0.tgz",
+      "integrity": "sha512-giab+EVRanChIupZK7gXjHy90y3nncA2phIOyG3Ne5fvpiMJzvqYwiTOnEVW2S4CoYcuKJkomat7bMXA/UoUZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "global": "~4.4.0",
+        "is-function": "^1.0.1"
+      }
+    },
     "node_modules/@vitejs/plugin-vue": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.2.tgz",
@@ -2133,6 +2181,27 @@
         }
       }
     },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.8.11",
+      "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz",
+      "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/aes-decrypter": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.2.tgz",
+      "integrity": "sha512-lc+/9s6iJvuaRe5qDlMTpCFjnwpkeOXp8qP3oiZ5jsj1MRg+SBVUmmICrhxHvc8OELSmc+fEyyxAuppY6hrWzw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0",
+        "pkcs7": "^1.0.4"
+      }
+    },
     "node_modules/argparse": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2408,6 +2477,11 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/dom-walk": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
+      "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2711,6 +2785,16 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/global": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz",
+      "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==",
+      "license": "MIT",
+      "dependencies": {
+        "min-document": "^2.19.0",
+        "process": "^0.11.10"
+      }
+    },
     "node_modules/gopd": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -3071,6 +3155,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-function": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz",
+      "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==",
+      "license": "MIT"
+    },
     "node_modules/is-glob": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -3221,6 +3311,17 @@
         "loose-envify": "cli.js"
       }
     },
+    "node_modules/m3u8-parser": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-7.2.0.tgz",
+      "integrity": "sha512-CRatFqpjVtMiMaKXxNvuI3I++vUumIXVVT/JpCpdU/FynV/ceVw1qpPyyBNindL+JlPMSesx+WX1QJaZEJSaMQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.1.1",
+        "global": "^4.4.0"
+      }
+    },
     "node_modules/magic-string": {
       "version": "0.30.21",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -4098,18 +4199,59 @@
         "node": ">= 0.6"
       }
     },
+    "node_modules/min-document": {
+      "version": "2.19.2",
+      "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz",
+      "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==",
+      "license": "MIT",
+      "dependencies": {
+        "dom-walk": "^0.1.0"
+      }
+    },
     "node_modules/mitt": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
       "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
       "license": "MIT"
     },
+    "node_modules/mpd-parser": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.1.tgz",
+      "integrity": "sha512-1FuyEWI5k2HcmhS1HkKnUAQV7yFPfXPht2DnRRGtoiiAAW+ESTbtEXIDpRkwdU+XyrQuwrIym7UkoPKsZ0SyFw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/vhs-utils": "^4.0.0",
+        "@xmldom/xmldom": "^0.8.3",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "mpd-to-m3u8-json": "bin/parse.js"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
       "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
       "license": "MIT"
     },
+    "node_modules/mux.js": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.1.0.tgz",
+      "integrity": "sha512-NTxawK/BBELJrYsZThEulyUMDVlLizKdxyAsMuzoCD1eFj97BVaA8D/CvKsKu6FOLYkFojN5CbM9h++ZTZtknA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.11.2",
+        "global": "^4.4.0"
+      },
+      "bin": {
+        "muxjs-transmux": "bin/transmux.js"
+      },
+      "engines": {
+        "node": ">=8",
+        "npm": ">=5"
+      }
+    },
     "node_modules/nanoid": {
       "version": "3.3.11",
       "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4217,6 +4359,18 @@
         }
       }
     },
+    "node_modules/pkcs7": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
+      "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5"
+      },
+      "bin": {
+        "pkcs7": "bin/cli.js"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.6",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -4245,6 +4399,15 @@
         "node": "^10 || ^12 || >=14"
       }
     },
+    "node_modules/process": {
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+      "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6.0"
+      }
+    },
     "node_modules/property-information": {
       "version": "7.1.0",
       "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
@@ -5331,6 +5494,57 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/video.js": {
+      "version": "8.23.4",
+      "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.23.4.tgz",
+      "integrity": "sha512-qI0VTlYmKzEqRsz1Nppdfcaww4RSxZAq77z2oNSl3cNg2h6do5C8Ffl0KqWQ1OpD8desWXsCrde7tKJ9gGTEyQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime": "^7.12.5",
+        "@videojs/http-streaming": "^3.17.2",
+        "@videojs/vhs-utils": "^4.1.1",
+        "@videojs/xhr": "2.7.0",
+        "aes-decrypter": "^4.0.2",
+        "global": "4.4.0",
+        "m3u8-parser": "^7.2.0",
+        "mpd-parser": "^1.3.1",
+        "mux.js": "^7.0.1",
+        "videojs-contrib-quality-levels": "4.1.0",
+        "videojs-font": "4.2.0",
+        "videojs-vtt.js": "0.15.5"
+      }
+    },
+    "node_modules/videojs-contrib-quality-levels": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz",
+      "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.4.0"
+      },
+      "engines": {
+        "node": ">=16",
+        "npm": ">=8"
+      },
+      "peerDependencies": {
+        "video.js": "^8"
+      }
+    },
+    "node_modules/videojs-font": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.2.0.tgz",
+      "integrity": "sha512-YPq+wiKoGy2/M7ccjmlvwi58z2xsykkkfNMyIg4xb7EZQQNwB71hcSsB3o75CqQV7/y5lXkXhI/rsGAS7jfEmQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/videojs-vtt.js": {
+      "version": "0.15.5",
+      "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz",
+      "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "global": "^4.3.1"
+      }
+    },
     "node_modules/vite": {
       "version": "7.2.6",
       "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",

+ 1 - 0
package.json

@@ -21,6 +21,7 @@
     "react-dom": "^18.3.1",
     "sass": "^1.94.2",
     "veaury": "^2.6.3",
+    "video.js": "^8.23.4",
     "vue": "^3.5.24",
     "vue-i18n": "^9.14.5",
     "vue-react-wrapper": "^0.3.1",

+ 270 - 0
src/components/VideoPlayer.vue

@@ -0,0 +1,270 @@
+<template>
+  <div class="video-player-container">
+    <video 
+      ref="videoElement"
+      class="video-js vjs-default-skin vjs-big-play-centered"
+      
+      preload="auto"
+      :poster="poster"
+    >
+      <source :src="src" :type="type">
+      <!-- <track 
+        v-if="subtitle" 
+        kind="subtitles" 
+        :src="subtitle" 
+        srclang="zh-CN" 
+        label="中文" 
+        default
+      > -->
+      您的浏览器不支持HTML5视频播放
+    </video>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, watch } from 'vue'
+import videojs from 'video.js'
+import 'video.js/dist/video-js.css'
+
+// 组件属性
+const props = defineProps({
+  // 视频地址
+  src: {
+    type: String,
+    required: true
+  },
+  // 视频封面
+  poster: {
+    type: String,
+    default: ''
+  },
+  // 视频类型
+  type: {
+    type: String,
+    default: 'video/mp4'
+  },
+  // 是否自动播放
+  autoplay: {
+    type: Boolean,
+    default: true
+  },
+  // 是否显示控件
+  controls: {
+    type: Boolean,
+    default: true
+  },
+  // 是否自适应容器大小
+  fluid: {
+    type: Boolean,
+    default: true
+  },
+  // 预加载方式
+  preload: {
+    type: String,
+    default: 'auto'
+  },
+  // 字幕文件地址
+  subtitle: {
+    type: String,
+    default: ''
+  },
+  // 播放速度选项
+  playbackRates: {
+    type: Array,
+    default: () => [ 0.75, 1, 1.25, 1.5, 2]
+  },
+  // 自定义控件配置
+  controlBar: {
+    type: Object,
+    default: () => ({
+      // timeDivider: true,
+      // durationDisplay: true,
+      // remainingTimeDisplay: false,
+      // fullscreenToggle: true,
+      currentTimeDisplay:true,
+      timeDivider:true,
+      durationDisplay:true,
+      remainingTimeDisplay:false,
+      volumePanel: {
+        inline: false,
+      },
+      children: [
+        {name: 'playToggle'}, // 播放按钮
+        {name: 'currentTimeDisplay'}, // 当前已播放时间
+        {name: 'timeDivider'}, // 总时间
+        {name: 'durationDisplay'}, // 总时间
+        // {name: 'remainingTimeDisplay'}, // 剩余时间
+        {name: 'progressControl'}, // 播放进度条
+        {
+          name: 'volumePanel', // 音量控制
+          inline: false, // 不使用水平方式
+        },
+        {name: 'FullscreenToggle'}
+      ]
+    })
+  },
+  // 视频高度(可选,支持固定高度)
+  height: {
+    type: String,
+    default: 'auto'
+  }
+})
+
+// 组件事件
+const emit = defineEmits([
+  'play',          // 播放事件
+  'pause',         // 暂停事件
+  'ended',         // 播放结束事件
+  'timeupdate',    // 播放时间更新事件
+  'loadedmetadata',// 元数据加载完成事件
+  'error',         // 错误事件
+  'ready'          // 播放器就绪事件
+])
+
+const videoElement = ref(null)
+let player = null
+
+// 初始化播放器
+onMounted(() => {
+  // 创建video.js播放器实例
+  player = videojs(videoElement.value, {
+    autoplay: props.autoplay,
+    controls: props.controls,
+    fluid: props.fluid,
+    preload: props.preload,
+    aspectRatio: "16:9",
+    // playbackRates: props.playbackRates,// 播放速度选项
+    controlBar: props.controlBar,
+  })
+
+  // 绑定事件
+  player.on('play', () => emit('play'))
+  player.on('pause', () => emit('pause'))
+  player.on('ended', () => emit('ended'))
+  player.on('timeupdate', () => emit('timeupdate', player.currentTime()))
+  player.on('loadedmetadata', () => emit('loadedmetadata', player.duration()))
+  player.on('error', () => emit('error', player.error()))
+  player.on('ready', () => emit('ready', player))
+})
+
+// 监听src变化,更新视频源
+watch(() => props.src, (newSrc, oldSrc) => {
+  if (player && newSrc !== oldSrc) {
+    player.src({ src: newSrc, type: props.type })
+    player.load()
+    if (props.autoplay) {
+      player.play()
+    }
+  }
+})
+
+// 监听poster变化,更新封面
+watch(() => props.poster, (newPoster) => {
+  if (player) {
+    player.poster(newPoster)
+  }
+})
+
+// 组件销毁时清理播放器
+onUnmounted(() => {
+  if (player) {
+    player.dispose()
+    player = null
+  }
+})
+
+// 暴露方法给父组件
+defineExpose({
+  // 播放
+  play: () => player && player.play(),
+  // 暂停
+  pause: () => player && player.pause(),
+  // 切换播放状态
+  togglePlay: () => player && (player.paused() ? player.play() : player.pause()),
+  // 跳转到指定时间
+  seekTo: (time) => player && player.currentTime(time),
+  // 获取当前播放时间
+  getCurrentTime: () => player && player.currentTime(),
+  // 获取视频总时长
+  getDuration: () => player && player.duration(),
+  // 判断是否正在播放
+  isPlaying: () => player && !player.paused(),
+  // 设置音量
+  setVolume: (volume) => player && player.volume(volume),
+  // 获取音量
+  getVolume: () => player && player.volume(),
+  // 设置播放速度
+  setPlaybackRate: (rate) => player && player.playbackRate(rate),
+  // 获取播放速度
+  getPlaybackRate: () => player && player.playbackRate(),
+  // 获取播放器实例(高级用法)
+  getPlayerInstance: () => player
+})
+</script>
+
+<style scoped lang="scss">
+.video-player-container {
+  width: 100%;
+  border-radius: 8px;
+  overflow: hidden;
+  
+  :deep(.video-js) {
+    width: 100%;
+  //  height: 500px;
+    border-radius: 8px;
+    background-color: #000;
+    // 自定义当前时间和总时间显示
+    .vjs-current-time{
+      display: block;
+    }
+    // 自定义总时间显示
+    .vjs-duration{
+      display: block;
+    }
+    // 自定义总时间和当前时间之间的分隔符
+    .vjs-time-divider{
+      display: block;
+    }
+    
+    // 自定义大播放按钮样式
+    .vjs-big-play-button {
+      // width: 80px;
+      // height: 80px;
+      // line-height: 80px;
+      // border-radius: 50%;
+      // font-size: 30px;
+      // border: none;
+      // background-color: rgba(0, 0, 0, 0.6);
+      
+      // &:hover {
+      //   background-color: rgba(0, 0, 0, 0.8);
+      // }
+    }
+    
+    // 自定义控制栏样式
+    .vjs-control-bar {
+      // background: linear-gradient(to top, rgba(0, 0, 0, 0.7) 0%, rgba(0, 0, 0, 0) 100%);
+      // height: 60px;
+      // padding: 0 10px;
+    }
+    
+    // 自定义进度条样式
+    .vjs-progress-control {
+      // position: absolute;
+      // top: 0;
+      // left: 0;
+      // width: 100%;
+      // height: 5px;
+      // margin: 0;
+      
+      // .vjs-progress-holder {
+      //   height: 100%;
+        
+      //   .vjs-play-progress {
+      //     background-color: #409eff;
+      //   }
+      // }
+    }
+  }
+}
+</style>

+ 130 - 2
src/pages/CourseDetail.vue

@@ -10,8 +10,20 @@
         <div class="course-video">
           <!-- 视频播放器占位 -->
           <div class="video-player">
-            <img :src="currentCourse.cover" :alt="currentCourse.title">
-            <div class="play-button">▶</div>
+            <VideoPlayer 
+              ref="videoPlayer"
+              :src="currentVideoUrl"
+              :poster="currentCourse.cover"
+              @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="video-info">
@@ -32,12 +44,14 @@
               v-for="chapter in currentCourseChapters" 
               :key="chapter.id" 
               :title="chapter.title"
+               @click="playVideo(video)"
             >
               <div 
                 v-for="video in chapter.videos" 
                 :key="video.id" 
                 class="chapter-video"
               >
+                <el-icon><VideoPlay /></el-icon>
                 {{ video.title }} ({{ video.duration }})
               </div>
             </el-collapse-item>
@@ -52,6 +66,9 @@
 import { onMounted,ref } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useCourseStore } from '@/pinia/courseStore'
+import { VideoPlay } from '@element-plus/icons-vue'
+import VideoPlayer from '@/components/VideoPlayer.vue'
+import DGTMessage from '@/utils/message'
 
 const route = useRoute()
 const router = useRouter()
@@ -61,6 +78,13 @@ const courseId = route.params.id
 const currentCourse = ref(null)
 const currentCourseChapters = ref(null)
 
+const videoPlayer = ref(null)
+// const currentVideoUrl = ref('')
+const currentVideoUrl = ref('http://jcxxpt.oss-cn-beijing.aliyuncs.com/common/2025/12/19/actmOvmq0xOBc8448561c73a066a523821c6f7ae4868_20251219094240A008.mp4')
+const currentPlayingVideoId = ref(null)
+const currentVideoDuration = ref(0)
+const currentPlayTime = ref(0)
+
 
 onMounted(() => {
   courseStore.fetchCourseDetail(courseId)
@@ -73,6 +97,108 @@ onMounted(() => {
 const goBack = () => {
   router.back()
 }
+
+
+
+// 播放指定视频
+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])
+    }
+  }
+}
+
+// 格式化时间
+const formatTime = (seconds) => {
+  if (!seconds || isNaN(seconds)) return '00:00'
+  
+  const minutes = Math.floor(seconds / 60)
+  const secs = Math.floor(seconds % 60)
+  return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
+}
 </script>
 
 <style scoped lang="scss">
@@ -82,6 +208,7 @@ const goBack = () => {
     display: flex;
     gap: 20px;
     margin-bottom: 40px;
+    // height: 500px; /* 设置容器高度,可根据需要调整 */
     
     @media (max-width: 768px) {
       flex-direction: column;
@@ -90,6 +217,7 @@ const goBack = () => {
     .video-player {
       flex: 2;
       position: relative;
+      // height: 500px;
       
       img {
         width: 100%;