|
|
@@ -1,46 +1,46 @@
|
|
|
<template>
|
|
|
<div class="video-container">
|
|
|
<div class="box-video">
|
|
|
- <!-- 视频 -->
|
|
|
+ <!-- 视频 -->
|
|
|
<template v-if="contentType === 'video'">
|
|
|
<div class="video-wrapper" :class="{ 'video-paused': isVideoPaused }">
|
|
|
<video
|
|
|
- class="full-box-video"
|
|
|
- ref="videoRef"
|
|
|
- :controls="true"
|
|
|
- :controlsList="'nofullscreen'"
|
|
|
- @timeupdate="handleTimeUpdate"
|
|
|
- @seeked="handleSeeked"
|
|
|
- @ended="handleVideoEnded"
|
|
|
- @loadedmetadata="handleLoadedMetadata"
|
|
|
- @pause="handleVideoPause"
|
|
|
- @play="handleVideoPlay"
|
|
|
- @click="handleVideoClick"
|
|
|
+ class="full-box-video"
|
|
|
+ ref="videoRef"
|
|
|
+ :controls="true"
|
|
|
+ :controlsList="'nofullscreen'"
|
|
|
+ @timeupdate="handleTimeUpdate"
|
|
|
+ @seeked="handleSeeked"
|
|
|
+ @ended="handleVideoEnded"
|
|
|
+ @loadedmetadata="handleLoadedMetadata"
|
|
|
+ @pause="handleVideoPause"
|
|
|
+ @play="handleVideoPlay"
|
|
|
+ @click="handleVideoClick"
|
|
|
></video>
|
|
|
<!-- 历史播放位置提示 -->
|
|
|
- <div
|
|
|
- v-if="showHistoryTip"
|
|
|
- class="history-tip"
|
|
|
- :style="{ left: historyTipPosition + '%' }"
|
|
|
+ <div
|
|
|
+ v-if="showHistoryTip"
|
|
|
+ class="history-tip"
|
|
|
+ :style="{ left: historyTipPosition + '%' }"
|
|
|
>
|
|
|
<el-icon @click.stop="closeHistoryTip" class="close-icon"><CloseBold /></el-icon>
|
|
|
<span @click="goToLastPosition">回到上次播放位置</span>
|
|
|
</div>
|
|
|
<!-- 章节要点小圆点 -->
|
|
|
<div class="progress-markers" v-if="chapterMarkers.length > 0">
|
|
|
- <div
|
|
|
- v-for="marker in chapterMarkers"
|
|
|
- :key="marker.time"
|
|
|
- class="progress-marker"
|
|
|
- :style="{ left: marker.position + '%' }"
|
|
|
- @click="seekToTime(marker.time)"
|
|
|
+ <div
|
|
|
+ v-for="marker in chapterMarkers"
|
|
|
+ :key="marker.time"
|
|
|
+ class="progress-marker"
|
|
|
+ :style="{ left: marker.position + '%' }"
|
|
|
+ @click="seekToTime(marker.time)"
|
|
|
></div>
|
|
|
</div>
|
|
|
<!-- 网页全屏按钮 -->
|
|
|
- <el-tooltip
|
|
|
- :content="isWebFullscreen ? '退出网页全屏' : '进入网页全屏'"
|
|
|
- placement="left"
|
|
|
- effect="dark"
|
|
|
+ <el-tooltip
|
|
|
+ :content="isWebFullscreen ? '退出网页全屏' : '进入网页全屏'"
|
|
|
+ placement="left"
|
|
|
+ effect="dark"
|
|
|
>
|
|
|
<div class="fullscreen-btn" @click="toggleWebFullscreen">
|
|
|
<el-icon v-if="!isWebFullscreen"><FullScreen /></el-icon>
|
|
|
@@ -76,7 +76,7 @@ const props = defineProps({
|
|
|
contentType: { type: String, required: true }, // contentType类型
|
|
|
videoPath: { type: String }, // 变为可选
|
|
|
courseId: { type: String, required: true },
|
|
|
- typeId: { type: String, required: true },
|
|
|
+ typeId: { type: String, required: true },
|
|
|
courseConfigList: { type: Array, default: () => [] },
|
|
|
allIndices: { type: Array, default: () => [] },
|
|
|
currentIndex: { type: String, required: true }
|
|
|
@@ -129,12 +129,12 @@ const saveProgress = throttle(async (progress, currentTime) => {
|
|
|
try {
|
|
|
// 保存到localStorage,下次加载视频续播
|
|
|
localStorage.setItem(
|
|
|
- `videoProgress_${props.courseId}`,
|
|
|
- JSON.stringify({
|
|
|
- progress: progress,
|
|
|
- currentTime: currentTime,
|
|
|
- timestamp: Date.now()
|
|
|
- })
|
|
|
+ `videoProgress_${props.courseId}`,
|
|
|
+ JSON.stringify({
|
|
|
+ progress: progress,
|
|
|
+ currentTime: currentTime,
|
|
|
+ timestamp: Date.now()
|
|
|
+ })
|
|
|
)
|
|
|
|
|
|
// 通过emit事件通知父组件保存进度
|
|
|
@@ -152,11 +152,11 @@ const handleLoadedMetadata = () => {
|
|
|
if (duration && props.courseConfigList && props.courseConfigList.length) {
|
|
|
// 根据courseConfigList生成章节标记,过滤掉ccTime不存在或无效的配置项
|
|
|
chapterMarkers.value = props.courseConfigList
|
|
|
- .filter(config => config.ccTime !== undefined && config.ccTime !== null && config.ccTime !== '')
|
|
|
- .map(config => ({
|
|
|
- time: config.ccTime,
|
|
|
- position: (config.ccTime / duration) * 100
|
|
|
- }))
|
|
|
+ .filter(config => config.ccTime !== undefined && config.ccTime !== null && config.ccTime !== '')
|
|
|
+ .map(config => ({
|
|
|
+ time: config.ccTime,
|
|
|
+ position: (config.ccTime / duration) * 100
|
|
|
+ }))
|
|
|
} else {
|
|
|
// 确保在courseConfigList为空时清空标记
|
|
|
chapterMarkers.value = []
|
|
|
@@ -167,7 +167,7 @@ const handleLoadedMetadata = () => {
|
|
|
const seekToTime = (time) => {
|
|
|
if (videoRef.value) {
|
|
|
videoRef.value.currentTime = time
|
|
|
- // 清除暂停索引,用户点击后可以再次触发暂停
|
|
|
+ // 清除已暂停索引,确保用户点击后可以再次触发暂停
|
|
|
pausedIndices.value = pausedIndices.value.filter(t => t !== time)
|
|
|
}
|
|
|
}
|
|
|
@@ -182,7 +182,7 @@ const handleTimeUpdate = ev => {
|
|
|
}
|
|
|
|
|
|
const progressPercentage =
|
|
|
- duration > 0 ? Math.round((currentTime / duration) * 100) : 0
|
|
|
+ duration > 0 ? Math.round((currentTime / duration) * 100) : 0
|
|
|
|
|
|
// 更新最后播放进度
|
|
|
lastPlayProgress.value = progressPercentage
|
|
|
@@ -204,12 +204,13 @@ const handleTimeUpdate = ev => {
|
|
|
|
|
|
// 触发父组件的时间更新事件
|
|
|
emits('timeUpdate', { currentTime, progressPercentage })
|
|
|
- if (!props.courseConfigList.length) return
|
|
|
+ if (!props.courseConfigList.length) return
|
|
|
props.courseConfigList.forEach(courseCofig => {
|
|
|
// 暂停时间
|
|
|
let time = courseCofig.ccTime
|
|
|
// 检查是否到达时间点且还未暂停过
|
|
|
if (currentTime === time && !pausedIndices.value.includes(time)) {
|
|
|
+ // 无论 ccQuestSource 是什么值,都暂停视频
|
|
|
videoRef.value.pause()
|
|
|
// 记录暂停时间
|
|
|
pausedIndices.value.push(currentTime)
|
|
|
@@ -232,7 +233,7 @@ const handleSeeked = () => {
|
|
|
pausedIndices.value = []
|
|
|
}
|
|
|
|
|
|
-// 视频结束事件处理
|
|
|
+// 添加视频结束事件处理
|
|
|
const handleVideoEnded = () => {
|
|
|
// 视频结束时保存100%进度
|
|
|
if (!savedProgress.value.includes(100)) {
|
|
|
@@ -259,12 +260,12 @@ const setLastPlayPosition = () => {
|
|
|
const duration = videoRef.value.duration
|
|
|
historyTipPosition.value = duration > 0 ? (currentTime / duration) * 100 : 0
|
|
|
showHistoryTip.value = true
|
|
|
-
|
|
|
+
|
|
|
// 检查是否已有保存的进度点
|
|
|
if (progress >= 10) savedProgress.value.push(10)
|
|
|
if (progress >= 50) savedProgress.value.push(50)
|
|
|
if (progress >= 100) savedProgress.value.push(100)
|
|
|
-
|
|
|
+
|
|
|
// 5秒后自动隐藏历史位置提示
|
|
|
setTimeout(() => {
|
|
|
showHistoryTip.value = false
|
|
|
@@ -346,9 +347,9 @@ const initVideoPlayer = () => {
|
|
|
hlsRef.value.on(Hls.Events.ERROR, (event, data) => {
|
|
|
console.error('HLS错误:', data)
|
|
|
// 只对致命错误显示错误提示
|
|
|
- if (data.fatal) {
|
|
|
- ElMessage.error('视频加载失败,请稍后重试')
|
|
|
- }
|
|
|
+ if (data.fatal) {
|
|
|
+ ElMessage.error('视频加载失败,请稍后重试')
|
|
|
+ }
|
|
|
})
|
|
|
} else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
|
|
|
// 对于不支持HLS但支持原生m3u8的浏览器
|
|
|
@@ -410,10 +411,11 @@ const removeVideoKeyboardListener = () => {
|
|
|
videoRef.value.removeEventListener('keydown', handleKeyPress);
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
// 组件挂载时
|
|
|
onMounted(() => {
|
|
|
initVideoPlayer()
|
|
|
- // 键盘事件监听
|
|
|
+ // 键盘事件监听
|
|
|
document.addEventListener('keydown', handleKeyPress);
|
|
|
// 为视频元素添加键盘事件监听器
|
|
|
addVideoKeyboardListener();
|
|
|
@@ -442,7 +444,7 @@ watch([() => props.contentType, () => props.videoPath], () => {
|
|
|
const toggleWebFullscreen = () => {
|
|
|
const videoWrapper = document.querySelector('.video-wrapper')
|
|
|
if (!videoWrapper) return
|
|
|
-
|
|
|
+
|
|
|
if (!isWebFullscreen.value) {
|
|
|
// 进入网页全屏
|
|
|
videoWrapper.style.position = 'fixed'
|
|
|
@@ -455,7 +457,7 @@ const toggleWebFullscreen = () => {
|
|
|
videoWrapper.style.display = 'flex'
|
|
|
videoWrapper.style.justifyContent = 'center'
|
|
|
videoWrapper.style.alignItems = 'center'
|
|
|
-
|
|
|
+
|
|
|
// 调整视频大小
|
|
|
const video = videoWrapper.querySelector('.full-box-video')
|
|
|
if (video) {
|
|
|
@@ -463,7 +465,7 @@ const toggleWebFullscreen = () => {
|
|
|
video.style.height = '100%'
|
|
|
video.style.objectFit = 'contain'
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
isWebFullscreen.value = true
|
|
|
} else {
|
|
|
// 退出网页全屏
|
|
|
@@ -477,7 +479,7 @@ const toggleWebFullscreen = () => {
|
|
|
videoWrapper.style.display = 'flex'
|
|
|
videoWrapper.style.justifyContent = 'center'
|
|
|
videoWrapper.style.alignItems = 'center'
|
|
|
-
|
|
|
+
|
|
|
// 恢复视频大小
|
|
|
const video = videoWrapper.querySelector('.full-box-video')
|
|
|
if (video) {
|
|
|
@@ -485,7 +487,7 @@ const toggleWebFullscreen = () => {
|
|
|
video.style.height = '100%'
|
|
|
video.style.objectFit = 'cover'
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
isWebFullscreen.value = false
|
|
|
}
|
|
|
}
|
|
|
@@ -496,7 +498,7 @@ onBeforeUnmount(() => {
|
|
|
hlsRef.value.destroy()
|
|
|
hlsRef.value = null
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 确保退出网页全屏
|
|
|
if (isWebFullscreen.value) {
|
|
|
const videoWrapper = document.querySelector('.video-wrapper')
|
|
|
@@ -510,7 +512,7 @@ onBeforeUnmount(() => {
|
|
|
videoWrapper.style.backgroundColor = ''
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// 移除键盘事件监听
|
|
|
document.removeEventListener('keydown', handleKeyPress);
|
|
|
// 移除视频元素的键盘事件监听器
|
|
|
@@ -553,7 +555,7 @@ onBeforeUnmount(() => {
|
|
|
border-radius: rpx(12);
|
|
|
}
|
|
|
.ppt-box{
|
|
|
- width: 100%;
|
|
|
+ width: 100%;
|
|
|
height: rpx(300);
|
|
|
display: flex;
|
|
|
justify-content: center;
|
|
|
@@ -576,7 +578,7 @@ onBeforeUnmount(() => {
|
|
|
width: 70%;
|
|
|
height: 100%;
|
|
|
border-radius: rpx(12);
|
|
|
- overflow: hidden;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
.ppt-container ::v-deep(.pptx-preview-wrapper) {
|
|
|
// 滚动条整体样式
|
|
|
@@ -612,14 +614,14 @@ onBeforeUnmount(() => {
|
|
|
.ppt-btn {
|
|
|
background-color: rgb(255, 255, 255, 0.5);
|
|
|
color: white;
|
|
|
- border: 1px white solid;
|
|
|
+ border: 1px white solid;
|
|
|
border-radius: rpx(12);
|
|
|
font-size: rpx(7);
|
|
|
cursor: pointer;
|
|
|
transition: background-color 0.3s;
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
height: rpx(15);
|
|
|
}
|
|
|
|
|
|
@@ -635,8 +637,8 @@ onBeforeUnmount(() => {
|
|
|
/* 章节要点标记样式 */
|
|
|
.progress-markers {
|
|
|
position: absolute;
|
|
|
- bottom: 26px;
|
|
|
- left: 15px;
|
|
|
+ bottom: 26px;
|
|
|
+ left: 15px;
|
|
|
right: 15px;
|
|
|
height: 10px;
|
|
|
pointer-events: none;
|