feat:课程详情切换ai伴学,里面接口重新调用,dplayer播放器切换,播放器配置,
This commit is contained in:
parent
068fc262ab
commit
829660dbda
@ -16,24 +16,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 清晰度选择器 -->
|
<!-- 视频控制工具栏 -->
|
||||||
<div v-if="videoQualities.length > 1" class="video-quality-selector">
|
<div v-if="playerInitialized" class="video-controls-overlay">
|
||||||
<div class="quality-dropdown">
|
<!-- 功能按钮组 -->
|
||||||
<button class="quality-btn" @click="showQualityMenu = !showQualityMenu">
|
<div class="video-function-buttons">
|
||||||
{{ currentQuality }}p
|
<!-- 清晰度选择器 -->
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" class="dropdown-icon">
|
<div v-if="videoQualities.length > 1" class="dplayer-quality-selector">
|
||||||
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
<button class="dplayer-control-btn dplayer-quality-btn" @click="showQualityMenu = !showQualityMenu" title="清晰度">
|
||||||
</svg>
|
{{ getCurrentQualityLabel() }}
|
||||||
</button>
|
<svg width="12" height="12" viewBox="0 0 12 12" class="quality-dropdown-icon">
|
||||||
<div v-if="showQualityMenu" class="quality-menu">
|
<path d="M3 4.5l3 3 3-3" stroke="currentColor" stroke-width="1.5" fill="none" />
|
||||||
<div v-for="quality in videoQualities" :key="quality.value" class="quality-option"
|
</svg>
|
||||||
:class="{ active: quality.value === currentQuality }"
|
</button>
|
||||||
@click="changeVideoQuality(quality.value); showQualityMenu = false">
|
<div v-if="showQualityMenu" class="dplayer-quality-menu">
|
||||||
{{ quality.label }}
|
<div v-for="quality in videoQualities" :key="quality.value"
|
||||||
|
class="dplayer-quality-option"
|
||||||
|
:class="{ active: quality.value === currentQuality }"
|
||||||
|
@click="switchQuality(quality); showQualityMenu = false">
|
||||||
|
{{ quality.label }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 截屏按钮 -->
|
||||||
|
<button class="dplayer-control-btn" @click="takeScreenshot" title="截屏">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M15 5v6c0 .55-.45 1-1 1H2c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1h1.5l.5-1h7l.5 1H14c.55 0 1 .45 1 1zM8 10.5c1.38 0 2.5-1.12 2.5-2.5S9.38 5.5 8 5.5 5.5 6.62 5.5 8 6.62 10.5 8 10.5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 画中画按钮 -->
|
||||||
|
<button class="dplayer-control-btn" @click="togglePictureInPicture" title="画中画">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm1 0v14h14V1H1zm5.5 4a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5H7a.5.5 0 0 1-.5-.5V5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 弹幕开关 -->
|
||||||
|
<button class="dplayer-control-btn" @click="toggleDanmaku" :class="{ active: danmakuEnabled }" title="弹幕">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M2.5 3A1.5 1.5 0 0 0 1 4.5v7A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 13.5 3h-11zM2 4.5a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5v-7z"/>
|
||||||
|
<path d="M3.5 6h9v1h-9V6zm0 2h7v1h-7V8zm0 2h5v1h-5v-1z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -72,12 +101,19 @@ const emit = defineEmits<{
|
|||||||
ended: []
|
ended: []
|
||||||
error: [error: any]
|
error: [error: any]
|
||||||
qualityChange: [quality: string]
|
qualityChange: [quality: string]
|
||||||
|
screenshot: [dataUrl: string]
|
||||||
|
danmakuSend: [text: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const dplayerContainer = ref<HTMLDivElement>()
|
const dplayerContainer = ref<HTMLDivElement>()
|
||||||
let player: any = null
|
let player: any = null
|
||||||
const playerInitialized = ref(false)
|
const playerInitialized = ref(false)
|
||||||
const isPlaying = ref(false)
|
const isPlaying = ref(false)
|
||||||
|
|
||||||
|
// 新增功能状态
|
||||||
|
const danmakuEnabled = ref(true)
|
||||||
|
const danmakuText = ref('')
|
||||||
|
const isPictureInPicture = ref(false)
|
||||||
const showQualityMenu = ref(false)
|
const showQualityMenu = ref(false)
|
||||||
|
|
||||||
// 加载 DPlayer
|
// 加载 DPlayer
|
||||||
@ -145,7 +181,20 @@ const initializePlayer = async (videoUrl?: string) => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
if (!dplayerContainer.value) return
|
if (!dplayerContainer.value) return
|
||||||
|
|
||||||
|
// 检查容器尺寸
|
||||||
|
const containerRect = dplayerContainer.value.getBoundingClientRect()
|
||||||
|
console.log('🔍 DPlayer容器尺寸:', {
|
||||||
|
width: containerRect.width,
|
||||||
|
height: containerRect.height,
|
||||||
|
top: containerRect.top,
|
||||||
|
left: containerRect.left
|
||||||
|
})
|
||||||
|
|
||||||
|
if (containerRect.height === 0) {
|
||||||
|
console.warn('⚠️ DPlayer容器高度为0,可能影响播放')
|
||||||
|
}
|
||||||
|
|
||||||
const DPlayer = (window as any).DPlayer
|
const DPlayer = (window as any).DPlayer
|
||||||
const url = videoUrl || props.videoUrl
|
const url = videoUrl || props.videoUrl
|
||||||
|
|
||||||
@ -159,7 +208,8 @@ const initializePlayer = async (videoUrl?: string) => {
|
|||||||
player = null
|
player = null
|
||||||
}
|
}
|
||||||
|
|
||||||
player = new DPlayer({
|
// 构建DPlayer配置
|
||||||
|
const dplayerConfig: any = {
|
||||||
container: dplayerContainer.value,
|
container: dplayerContainer.value,
|
||||||
video: {
|
video: {
|
||||||
url: url,
|
url: url,
|
||||||
@ -173,12 +223,50 @@ const initializePlayer = async (videoUrl?: string) => {
|
|||||||
volume: 0.8,
|
volume: 0.8,
|
||||||
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
playbackSpeed: [0.5, 0.75, 1, 1.25, 1.5, 2],
|
||||||
loop: false,
|
loop: false,
|
||||||
|
screenshot: true, // 启用截屏功能
|
||||||
|
danmaku: {
|
||||||
|
id: 'course-video-' + Date.now(),
|
||||||
|
api: '/api/danmaku/', // 弹幕API地址
|
||||||
|
token: 'demo-token',
|
||||||
|
maximum: 1000,
|
||||||
|
// 移除有问题的addition API
|
||||||
|
// addition: ['https://api.prprpr.me/dplayer/'],
|
||||||
|
user: 'student',
|
||||||
|
bottom: '15%',
|
||||||
|
unlimited: true
|
||||||
|
},
|
||||||
contextmenu: [
|
contextmenu: [
|
||||||
|
{
|
||||||
|
text: '截屏',
|
||||||
|
click: () => takeScreenshot()
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: '关于 DPlayer',
|
text: '关于 DPlayer',
|
||||||
link: 'https://github.com/DIYGod/DPlayer'
|
link: 'https://github.com/DIYGod/DPlayer'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有多个清晰度,启用DPlayer原生清晰度切换
|
||||||
|
console.log('🔍 检查清晰度配置:', {
|
||||||
|
videoQualities: props.videoQualities,
|
||||||
|
currentQuality: props.currentQuality,
|
||||||
|
qualitiesLength: props.videoQualities?.length
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用自定义清晰度选择器,不依赖DPlayer原生quality功能
|
||||||
|
console.log('✅ 使用自定义清晰度选择器:', {
|
||||||
|
mainVideoUrl: url,
|
||||||
|
availableQualities: props.videoQualities?.length || 0
|
||||||
|
})
|
||||||
|
|
||||||
|
player = new DPlayer(dplayerConfig)
|
||||||
|
|
||||||
|
// 检查DPlayer是否正确加载了quality配置
|
||||||
|
console.log('🔍 DPlayer实例创建完成:', {
|
||||||
|
hasQuality: !!player.quality,
|
||||||
|
qualityOptions: player.quality?.options,
|
||||||
|
playerConfig: dplayerConfig
|
||||||
})
|
})
|
||||||
|
|
||||||
// 事件监听
|
// 事件监听
|
||||||
@ -198,10 +286,42 @@ const initializePlayer = async (videoUrl?: string) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
player.on('error', (error: any) => {
|
player.on('error', (error: any) => {
|
||||||
console.error('DPlayer error:', error)
|
console.error('DPlayer 播放错误:', error)
|
||||||
|
console.error('错误详情:', {
|
||||||
|
type: error.type,
|
||||||
|
message: error.message,
|
||||||
|
url: url
|
||||||
|
})
|
||||||
emit('error', error)
|
emit('error', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
player.on('loadstart', () => {
|
||||||
|
console.log('🔍 视频开始加载:', url)
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('canplay', () => {
|
||||||
|
console.log('✅ 视频可以播放:', url)
|
||||||
|
})
|
||||||
|
|
||||||
|
player.on('loadeddata', () => {
|
||||||
|
console.log('✅ 视频数据加载完成:', url)
|
||||||
|
|
||||||
|
// 在视频加载完成后,尝试手动设置清晰度选项
|
||||||
|
if (props.videoQualities && props.videoQualities.length > 1) {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('🔍 尝试手动设置清晰度选项')
|
||||||
|
if (player && player.quality) {
|
||||||
|
console.log('✅ DPlayer quality对象存在:', player.quality)
|
||||||
|
} else {
|
||||||
|
console.log('❌ DPlayer quality对象不存在,尝试其他方法')
|
||||||
|
// 尝试直接在DOM中查找清晰度按钮
|
||||||
|
const qualityBtn = dplayerContainer.value?.querySelector('.dplayer-quality-button')
|
||||||
|
console.log('🔍 查找清晰度按钮:', qualityBtn)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
playerInitialized.value = true
|
playerInitialized.value = true
|
||||||
console.log('DPlayer 初始化成功:', url)
|
console.log('DPlayer 初始化成功:', url)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -210,30 +330,7 @@ const initializePlayer = async (videoUrl?: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换视频清晰度
|
|
||||||
const changeVideoQuality = (quality: string) => {
|
|
||||||
const qualityVideo = props.videoQualities.find(q => q.value === quality)
|
|
||||||
if (qualityVideo && player) {
|
|
||||||
try {
|
|
||||||
// 尝试使用 DPlayer 的 switchVideo 方法
|
|
||||||
if (typeof player.switchVideo === 'function') {
|
|
||||||
player.switchVideo({
|
|
||||||
url: qualityVideo.url,
|
|
||||||
type: 'auto'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 如果没有 switchVideo 方法,重新初始化播放器
|
|
||||||
initializePlayer(qualityVideo.url)
|
|
||||||
}
|
|
||||||
emit('qualityChange', quality)
|
|
||||||
console.log('切换清晰度到:', quality)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('切换清晰度失败:', error)
|
|
||||||
// 重新初始化播放器作为后备方案
|
|
||||||
initializePlayer(qualityVideo.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 播放控制方法
|
// 播放控制方法
|
||||||
const play = () => {
|
const play = () => {
|
||||||
@ -254,6 +351,144 @@ const seek = (time: number) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 截屏功能
|
||||||
|
const takeScreenshot = () => {
|
||||||
|
if (player && player.video) {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
const video = player.video
|
||||||
|
|
||||||
|
canvas.width = video.videoWidth
|
||||||
|
canvas.height = video.videoHeight
|
||||||
|
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||||
|
const dataUrl = canvas.toDataURL('image/png')
|
||||||
|
|
||||||
|
// 创建下载链接
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.download = `screenshot-${Date.now()}.png`
|
||||||
|
link.href = dataUrl
|
||||||
|
link.click()
|
||||||
|
|
||||||
|
emit('screenshot', dataUrl)
|
||||||
|
console.log('截屏成功')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('截屏失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 画中画功能
|
||||||
|
const togglePictureInPicture = async () => {
|
||||||
|
if (player && player.video) {
|
||||||
|
try {
|
||||||
|
if (document.pictureInPictureElement) {
|
||||||
|
await document.exitPictureInPicture()
|
||||||
|
isPictureInPicture.value = false
|
||||||
|
} else {
|
||||||
|
await player.video.requestPictureInPicture()
|
||||||
|
isPictureInPicture.value = true
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('画中画切换失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 弹幕功能
|
||||||
|
const toggleDanmaku = () => {
|
||||||
|
danmakuEnabled.value = !danmakuEnabled.value
|
||||||
|
if (player && player.danmaku) {
|
||||||
|
if (danmakuEnabled.value) {
|
||||||
|
player.danmaku.show()
|
||||||
|
} else {
|
||||||
|
player.danmaku.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendDanmaku = () => {
|
||||||
|
if (danmakuText.value.trim() && player && player.danmaku) {
|
||||||
|
const danmaku = {
|
||||||
|
text: danmakuText.value.trim(),
|
||||||
|
color: '#ffffff',
|
||||||
|
type: 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
player.danmaku.send(danmaku)
|
||||||
|
emit('danmakuSend', danmakuText.value.trim())
|
||||||
|
danmakuText.value = ''
|
||||||
|
console.log('发送弹幕:', danmaku.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清晰度切换相关函数
|
||||||
|
const getCurrentQualityLabel = () => {
|
||||||
|
const current = props.videoQualities.find(q => q.value === props.currentQuality)
|
||||||
|
return current ? current.label : '360p'
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchQuality = (quality: any) => {
|
||||||
|
if (player && quality.url) {
|
||||||
|
try {
|
||||||
|
// 记录当前播放状态
|
||||||
|
const currentTime = player.video?.currentTime || 0
|
||||||
|
const wasPlaying = !player.video?.paused
|
||||||
|
|
||||||
|
console.log('🔄 开始切换清晰度:', {
|
||||||
|
from: getCurrentQualityLabel(),
|
||||||
|
to: quality.label,
|
||||||
|
currentTime: currentTime,
|
||||||
|
wasPlaying: wasPlaying
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暂停播放
|
||||||
|
if (wasPlaying) {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换视频源
|
||||||
|
if (typeof player.switchVideo === 'function') {
|
||||||
|
player.switchVideo({
|
||||||
|
url: quality.url,
|
||||||
|
type: 'auto'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 恢复播放状态
|
||||||
|
setTimeout(() => {
|
||||||
|
if (player && player.video) {
|
||||||
|
player.seek(currentTime)
|
||||||
|
if (wasPlaying) {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
// 重新初始化播放器
|
||||||
|
initializePlayer(quality.url).then(() => {
|
||||||
|
// 恢复播放时间
|
||||||
|
setTimeout(() => {
|
||||||
|
if (player && player.video) {
|
||||||
|
player.seek(currentTime)
|
||||||
|
if (wasPlaying) {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('qualityChange', quality.value)
|
||||||
|
console.log('✅ 切换清晰度到:', quality.label)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 切换清晰度失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const setVolume = (volume: number) => {
|
const setVolume = (volume: number) => {
|
||||||
if (player) {
|
if (player) {
|
||||||
player.volume(volume / 100)
|
player.volume(volume / 100)
|
||||||
@ -286,7 +521,12 @@ defineExpose({
|
|||||||
seek,
|
seek,
|
||||||
setVolume,
|
setVolume,
|
||||||
destroy,
|
destroy,
|
||||||
initializePlayer
|
initializePlayer,
|
||||||
|
takeScreenshot,
|
||||||
|
togglePictureInPicture,
|
||||||
|
toggleDanmaku,
|
||||||
|
sendDanmaku,
|
||||||
|
switchQuality
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -299,6 +539,8 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
destroy()
|
destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -310,7 +552,7 @@ onUnmounted(() => {
|
|||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 578px;
|
height: 100%; /* 填满父容器 */
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,83 +613,244 @@ onUnmounted(() => {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 清晰度选择器 */
|
|
||||||
.video-quality-selector {
|
|
||||||
|
/* 视频控制工具栏 - DPlayer原生风格 */
|
||||||
|
.video-controls-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 15px;
|
top: 16px;
|
||||||
right: 15px;
|
right: 16px;
|
||||||
z-index: 10;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-dropdown {
|
.video-function-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DPlayer原生风格的控制按钮 */
|
||||||
|
.dplayer-control-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dplayer-control-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dplayer-control-btn.active {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dplayer-control-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DPlayer风格的清晰度选择器 */
|
||||||
|
.dplayer-quality-selector {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-btn {
|
.dplayer-quality-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 6px 12px;
|
padding: 8px 12px;
|
||||||
background: rgba(0, 0, 0, 0.6);
|
min-width: 60px;
|
||||||
color: white;
|
height: 36px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
color: #fff;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.2s ease;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-btn:hover {
|
.dplayer-quality-btn:hover {
|
||||||
background: rgba(0, 0, 0, 0.8);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-icon {
|
.quality-dropdown-icon {
|
||||||
transition: transform 0.2s;
|
transition: transform 0.2s ease;
|
||||||
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-btn:hover .dropdown-icon {
|
.dplayer-quality-btn:hover .quality-dropdown-icon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-menu {
|
.dplayer-quality-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 100%;
|
bottom: 100%;
|
||||||
right: 0;
|
right: 0;
|
||||||
margin-top: 4px;
|
margin-bottom: 8px;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-width: 80px;
|
min-width: 80px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-option {
|
.dplayer-quality-option {
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
color: white;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s ease;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-option:hover {
|
.dplayer-quality-option:hover {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality-option.active {
|
.dplayer-quality-option.active {
|
||||||
background: #1890ff;
|
background: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹幕输入框 */
|
||||||
|
.danmaku-input-container {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 60px;
|
||||||
|
left: 16px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
color: white;
|
color: white;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input:focus {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border-color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-send-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-send-btn:hover:not(:disabled) {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-send-btn:disabled {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式设计 */
|
/* 响应式设计 */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.video-container {
|
.video-controls-overlay {
|
||||||
height: 400px;
|
top: 12px;
|
||||||
|
right: 12px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-btn {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input-container {
|
||||||
|
bottom: 50px;
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input-wrapper {
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-send-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
@media (max-width: 576px) {
|
||||||
.video-container {
|
.video-controls-overlay {
|
||||||
height: 350px;
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-btn svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input-container {
|
||||||
|
bottom: 40px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-input {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danmaku-send-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -6,6 +6,7 @@ import Home from '@/views/Home.vue'
|
|||||||
import Courses from '@/views/Courses.vue'
|
import Courses from '@/views/Courses.vue'
|
||||||
import CourseDetail from '@/views/CourseDetail.vue'
|
import CourseDetail from '@/views/CourseDetail.vue'
|
||||||
import CourseDetailEnrolled from '@/views/CourseDetailEnrolled.vue'
|
import CourseDetailEnrolled from '@/views/CourseDetailEnrolled.vue'
|
||||||
|
import CourseExchanged from '@/views/CourseExchanged.vue'
|
||||||
import CourseStudy from '@/views/CourseStudy.vue'
|
import CourseStudy from '@/views/CourseStudy.vue'
|
||||||
import Learning from '@/views/Learning.vue'
|
import Learning from '@/views/Learning.vue'
|
||||||
import Profile from '@/views/Profile.vue'
|
import Profile from '@/views/Profile.vue'
|
||||||
@ -26,6 +27,7 @@ import SpecialTraining from '@/views/SpecialTraining.vue'
|
|||||||
import SpecialTrainingDetail from '@/views/SpecialTrainingDetail.vue'
|
import SpecialTrainingDetail from '@/views/SpecialTrainingDetail.vue'
|
||||||
import HelpCenter from '@/views/HelpCenter.vue'
|
import HelpCenter from '@/views/HelpCenter.vue'
|
||||||
import LearningCenter from '@/views/LearningCenter.vue'
|
import LearningCenter from '@/views/LearningCenter.vue'
|
||||||
|
import AICompanion from '@/views/AICompanion.vue'
|
||||||
|
|
||||||
// ========== 管理员后台组件 ==========
|
// ========== 管理员后台组件 ==========
|
||||||
import AdminDashboard from '@/views/teacher/AdminDashboard.vue'
|
import AdminDashboard from '@/views/teacher/AdminDashboard.vue'
|
||||||
@ -311,6 +313,14 @@ const routes: RouteRecordRaw[] = [
|
|||||||
meta: { title: '积分中心', requiresAuth: true }
|
meta: { title: '积分中心', requiresAuth: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// AI伴学
|
||||||
|
{
|
||||||
|
path: '/ai-companion',
|
||||||
|
name: 'AICompanion',
|
||||||
|
component: AICompanion,
|
||||||
|
meta: { title: 'AI伴学' }
|
||||||
|
},
|
||||||
|
|
||||||
// 首页与课程
|
// 首页与课程
|
||||||
{
|
{
|
||||||
path: '/service-agreement',
|
path: '/service-agreement',
|
||||||
@ -344,6 +354,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: CourseDetailEnrolled,
|
component: CourseDetailEnrolled,
|
||||||
meta: { title: '课程详情 - 已报名' }
|
meta: { title: '课程详情 - 已报名' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/course/:id/exchanged',
|
||||||
|
name: 'CourseExchanged',
|
||||||
|
component: CourseExchanged,
|
||||||
|
meta: { title: '课程详情 - 已兑换', requiresAuth: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/course/study/:id',
|
path: '/course/study/:id',
|
||||||
name: 'CourseStudy',
|
name: 'CourseStudy',
|
||||||
|
@ -522,12 +522,12 @@
|
|||||||
<button @click="loadCourseSections" class="retry-btn">重试</button>
|
<button @click="loadCourseSections" class="retry-btn">重试</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="courseSections.length > 0" class="sections-list">
|
<div v-else-if="courseSections.length > 0" class="sections-list">
|
||||||
<!-- 按章节分组显示 -->
|
<!-- 按章节分组显示 - 未报名状态,灰色不可点击 -->
|
||||||
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
|
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
|
||||||
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
|
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
<span class="chapter-number">第{{ chapterIndex + 1 }}章</span>
|
<span class="chapter-title">第{{ getChapterNumber(chapterIndex + 1) }}章 {{ chapter.title
|
||||||
<span class="chapter-title">{{ chapter.title }}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
|
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||||
@ -537,46 +537,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="chapter.expanded" class="chapter-lessons">
|
<div v-if="chapter.expanded" class="chapter-lessons">
|
||||||
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
|
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
|
||||||
<div class="lesson-content" :class="{ 'unregistered': !isUserEnrolled }"
|
<!-- 未报名状态:灰色不可点击 -->
|
||||||
@click="isUserEnrolled ? handleSectionClick(section) : handleUnregisteredClick(section)">
|
<div class="lesson-content unregistered" @click="handleUnregisteredClick(section)">
|
||||||
<div class="lesson-type-badge"
|
<div class="lesson-type-badge disabled" :class="getLessonTypeBadgeClass(section)">
|
||||||
:class="[getLessonTypeBadgeClass(section), { 'disabled': !isUserEnrolled }]">
|
|
||||||
{{ getLessonTypeText(section) }}
|
{{ getLessonTypeText(section) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-info">
|
<div class="lesson-info">
|
||||||
<span class="lesson-title" :class="{ 'disabled': !isUserEnrolled }">{{ section.name
|
<span class="lesson-title disabled">{{ section.name }}</span>
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-meta">
|
<div class="lesson-meta">
|
||||||
<span v-if="isVideoLesson(section)" class="lesson-duration"
|
<span v-if="isVideoLesson(section)" class="lesson-duration disabled">{{
|
||||||
:class="{ 'disabled': !isUserEnrolled }">{{ formatLessonDuration(section) }}</span>
|
formatLessonDuration(section) }}</span>
|
||||||
<div class="lesson-actions">
|
<div class="lesson-actions">
|
||||||
<!-- 视频播放图标 -->
|
<!-- 视频播放图标 - 不可点击 -->
|
||||||
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
|
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn disabled" disabled
|
||||||
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
|
@click.stop="handleUnregisteredClick(section)">
|
||||||
@click.stop="isUserEnrolled ? handleSectionClick(section) : handleUnregisteredClick(section)">
|
|
||||||
<!-- 调试: 视频课时判断结果 -->
|
|
||||||
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
|
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
|
||||||
</button>
|
</button>
|
||||||
<!-- 下载图标 -->
|
<!-- 下载图标 - 不可点击 -->
|
||||||
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
|
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn disabled" disabled
|
||||||
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
|
@click.stop="handleUnregisteredClick(section)">
|
||||||
@click.stop="isUserEnrolled ? handleDownload(section) : handleUnregisteredClick(section)">
|
<img src="/public/images/courses/download-enroll.png" alt="资料" width="14" height="14">
|
||||||
<img src="/images/courses/download-enroll.png" alt="资料" width="14" height="14">
|
|
||||||
</button>
|
</button>
|
||||||
<!-- 作业图标 -->
|
<!-- 编辑图标(作业) - 不可点击 -->
|
||||||
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
|
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn disabled" disabled
|
||||||
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
|
@click.stop="handleUnregisteredClick(section)">
|
||||||
@click.stop="isUserEnrolled ? handleHomework(section) : handleUnregisteredClick(section)">
|
<img src="/public/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
|
||||||
<img src="/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
|
|
||||||
</button>
|
</button>
|
||||||
<!-- 考试图标 -->
|
<!-- 考试图标 - 不可点击 -->
|
||||||
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
|
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn disabled" disabled
|
||||||
:class="{ 'disabled': !isUserEnrolled }" :disabled="!isUserEnrolled"
|
@click.stop="handleUnregisteredClick(section)">
|
||||||
@click.stop="isUserEnrolled ? handleExam(section) : handleUnregisteredClick(section)">
|
<img src="/public/images/courses/examination-enroll.png" alt="考试" width="14"
|
||||||
<img src="/images/courses/examination-enroll.png" alt="考试" width="14" height="14">
|
height="14">
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -714,7 +707,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuth } from '@/composables/useAuth'
|
import { useAuth } from '@/composables/useAuth'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
@ -728,7 +721,7 @@ import RegisterModal from '@/components/auth/RegisterModal.vue'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const courseId = ref(Number(route.params.id))
|
const courseId = ref(String(route.query.courseId) || '1')
|
||||||
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
|
const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
|
||||||
// enrollCourse 暂时未使用,后续需要时再启用
|
// enrollCourse 暂时未使用,后续需要时再启用
|
||||||
|
|
||||||
@ -837,33 +830,52 @@ const generateMockSections = (): CourseSection[] => {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将章节按章分组
|
// 将章节按章分组 - 根据后端数据结构重新实现
|
||||||
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||||
const chapterTitles = [
|
console.log('🔍 开始分组章节数据:', sections)
|
||||||
'课前准备',
|
|
||||||
'程序设计基础知识',
|
|
||||||
'实战项目',
|
|
||||||
'高级应用',
|
|
||||||
'拓展学习',
|
|
||||||
'答疑与交流'
|
|
||||||
]
|
|
||||||
|
|
||||||
const groups: ChapterGroup[] = []
|
const groups: ChapterGroup[] = []
|
||||||
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] // 每章的课程数量
|
|
||||||
let sectionIndex = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < chapterTitles.length; i++) {
|
// 找出所有一级章节(level=1,这些是父章节)
|
||||||
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
|
const parentChapters = sections.filter(section => section.level === 1)
|
||||||
if (chapterSections.length > 0) {
|
console.log('🔍 找到一级章节:', parentChapters)
|
||||||
groups.push({
|
|
||||||
title: `第${i + 1}章 ${chapterTitles[i]}`,
|
// 按sortOrder降序排序一级章节(sortOrder越大越靠前)
|
||||||
sections: chapterSections,
|
parentChapters.sort((a, b) => b.sort - a.sort)
|
||||||
expanded: i === 0 // 默认展开第一章
|
|
||||||
})
|
// 为每个一级章节创建分组
|
||||||
}
|
parentChapters.forEach((parentChapter, index) => {
|
||||||
sectionIndex += sectionsPerChapter[i]
|
// 找出该章节下的所有子章节(level=2,parentId匹配)
|
||||||
|
const childSections = sections.filter(section =>
|
||||||
|
section.level === 2 && section.parentId === parentChapter.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// 按sortOrder降序排序子章节(sortOrder越大越靠前)
|
||||||
|
childSections.sort((a, b) => b.sort - a.sort)
|
||||||
|
|
||||||
|
console.log(`🔍 章节"${parentChapter.name}"的子章节:`, childSections)
|
||||||
|
|
||||||
|
// 创建章节分组
|
||||||
|
groups.push({
|
||||||
|
title: parentChapter.name, // 使用后端返回的章节名称
|
||||||
|
sections: childSections.length > 0 ? childSections : [parentChapter], // 如果有子章节就用子章节,否则用父章节本身
|
||||||
|
expanded: index === 0 // 默认展开第一章
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有找到一级章节,可能所有章节都是同级的,直接作为一个组
|
||||||
|
if (groups.length === 0 && sections.length > 0) {
|
||||||
|
console.log('🔍 没有找到层级结构,将所有章节作为一组')
|
||||||
|
// 按sortOrder降序排序(sortOrder越大越靠前)
|
||||||
|
const sortedSections = [...sections].sort((a, b) => b.sort - a.sort)
|
||||||
|
groups.push({
|
||||||
|
title: '课程章节',
|
||||||
|
sections: sortedSections,
|
||||||
|
expanded: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✅ 章节分组完成:', groups)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1089,9 +1101,9 @@ const replyToUsername = ref('')
|
|||||||
const loadCourseDetail = async () => {
|
const loadCourseDetail = async () => {
|
||||||
console.log('开始加载课程详情,课程ID:', courseId.value)
|
console.log('开始加载课程详情,课程ID:', courseId.value)
|
||||||
|
|
||||||
if (!courseId.value) courseId.value = 1
|
if (!courseId.value) courseId.value = '1'
|
||||||
|
|
||||||
if (!courseId.value || isNaN(courseId.value)) {
|
if (!courseId.value || isNaN(Number(courseId.value))) {
|
||||||
console.log('课程ID无效,使用模拟数据')
|
console.log('课程ID无效,使用模拟数据')
|
||||||
loadMockCourseData()
|
loadMockCourseData()
|
||||||
return
|
return
|
||||||
@ -1102,7 +1114,7 @@ const loadCourseDetail = async () => {
|
|||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
console.log('调用API获取课程详情...')
|
console.log('调用API获取课程详情...')
|
||||||
const response = await CourseApi.getCourseById(String(courseId.value))
|
const response = await CourseApi.getCourseById(courseId.value)
|
||||||
console.log('API响应:', response)
|
console.log('API响应:', response)
|
||||||
|
|
||||||
if (response.code === 0 || response.code === 200) {
|
if (response.code === 0 || response.code === 200) {
|
||||||
@ -1145,7 +1157,7 @@ const loadCourseDetail = async () => {
|
|||||||
|
|
||||||
// 加载课程章节列表
|
// 加载课程章节列表
|
||||||
const loadCourseSections = async () => {
|
const loadCourseSections = async () => {
|
||||||
if (!courseId.value || isNaN(courseId.value)) {
|
if (!courseId.value || isNaN(Number(courseId.value))) {
|
||||||
console.log('课程ID无效,使用模拟章节数据')
|
console.log('课程ID无效,使用模拟章节数据')
|
||||||
loadMockData()
|
loadMockData()
|
||||||
return
|
return
|
||||||
@ -1156,27 +1168,32 @@ const loadCourseSections = async () => {
|
|||||||
sectionsError.value = ''
|
sectionsError.value = ''
|
||||||
|
|
||||||
console.log('调用API获取课程章节...')
|
console.log('调用API获取课程章节...')
|
||||||
const response = await CourseApi.getCourseSections(String(courseId.value))
|
const response = await CourseApi.getCourseSections(courseId.value)
|
||||||
console.log('章节API响应:', response)
|
console.log('章节API响应:', response)
|
||||||
|
|
||||||
if (response.code === 0 || response.code === 200) {
|
if (response.code === 0 || response.code === 200) {
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||||
courseSections.value = response.data
|
console.log('✅ API返回的原始章节数据:', response.data.list)
|
||||||
groupedSections.value = groupSectionsByChapter(response.data)
|
console.log('✅ 章节数据数量:', response.data.list.length)
|
||||||
console.log('章节数据设置成功:', courseSections.value)
|
|
||||||
console.log('分组数据:', groupedSections.value)
|
courseSections.value = response.data.list
|
||||||
|
groupedSections.value = groupSectionsByChapter(response.data.list)
|
||||||
|
|
||||||
|
console.log('✅ 设置后的courseSections:', courseSections.value)
|
||||||
|
console.log('✅ 设置后的groupedSections:', groupedSections.value)
|
||||||
|
console.log('✅ groupedSections长度:', groupedSections.value.length)
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回的章节数据为空,使用模拟数据')
|
console.log('❌ API返回的章节数据为空或格式错误')
|
||||||
loadMockData()
|
console.log('❌ response.data:', response.data)
|
||||||
|
sectionsError.value = '暂无课程章节数据'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回错误,使用模拟数据')
|
console.log('API返回错误')
|
||||||
loadMockData()
|
sectionsError.value = response.message || '获取课程章节失败'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载课程章节失败:', err)
|
console.error('加载课程章节失败:', err)
|
||||||
console.log('API调用失败,使用模拟数据')
|
sectionsError.value = '网络错误,请稍后重试'
|
||||||
loadMockData()
|
|
||||||
} finally {
|
} finally {
|
||||||
sectionsLoading.value = false
|
sectionsLoading.value = false
|
||||||
}
|
}
|
||||||
@ -1252,6 +1269,12 @@ const loadMockData = () => {
|
|||||||
examProgress.value = examSections.length > 0 ? Math.round((examSections.filter(s => s.completed).length / examSections.length) * 100) : 0
|
examProgress.value = examSections.length > 0 ? Math.round((examSections.filter(s => s.completed).length / examSections.length) * 100) : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取章节编号
|
||||||
|
const getChapterNumber = (num: number) => {
|
||||||
|
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||||
|
return numbers[num - 1] || num.toString()
|
||||||
|
}
|
||||||
|
|
||||||
// 切换章节展开/折叠
|
// 切换章节展开/折叠
|
||||||
const toggleChapter = (chapterIndex: number) => {
|
const toggleChapter = (chapterIndex: number) => {
|
||||||
console.log('点击切换章节,章节索引:', chapterIndex)
|
console.log('点击切换章节,章节索引:', chapterIndex)
|
||||||
@ -1272,87 +1295,66 @@ const toggleChapter = (chapterIndex: number) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取课时类型文本
|
// 获取课程类型文本
|
||||||
const getLessonTypeText = (section: CourseSection): string => {
|
const getLessonTypeText = (section: CourseSection) => {
|
||||||
if (section.outline && section.outline.includes('ppt')) {
|
if (isVideoLesson(section)) return '视频'
|
||||||
return '资料'
|
if (isResourceLesson(section)) return '资料'
|
||||||
} else if (section.name.includes('作业') || section.name.includes('练习')) {
|
if (isHomeworkLesson(section)) return '作业'
|
||||||
return '作业'
|
if (isExamLesson(section)) return '考试'
|
||||||
} else if (section.name.includes('考试') || section.name.includes('测试')) {
|
return '视频'
|
||||||
return '考试'
|
|
||||||
}
|
|
||||||
return '视频' // 默认为视频
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化课时时长
|
// 格式化课程时长
|
||||||
const formatLessonDuration = (section: CourseSection): string => {
|
const formatLessonDuration = (section: CourseSection) => {
|
||||||
// 根据课时名称和类型生成合适的时长
|
if (!section.duration) return ''
|
||||||
const durations = [
|
return section.duration
|
||||||
'01:03:56', '00:44:05', '00:52:22', '', // 第一章时长
|
|
||||||
'00:52:22', '', '01:03:56', '', '' // 第二章时长
|
|
||||||
]
|
|
||||||
|
|
||||||
// 根据section.id获取对应时长
|
|
||||||
const durationIndex = Number(section.id) - 1
|
|
||||||
if (durationIndex >= 0 && durationIndex < durations.length) {
|
|
||||||
return durations[durationIndex] || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认时长生成
|
|
||||||
if (isVideoLesson(section)) {
|
|
||||||
const minutes = Math.floor(Math.random() * 60) + 10 // 10-70分钟
|
|
||||||
const seconds = Math.floor(Math.random() * 60)
|
|
||||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return '' // 非视频课时不显示时长
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为视频课时
|
// 课程类型判断函数 - 与CourseExchanged保持一致
|
||||||
const isVideoLesson = (section: CourseSection): boolean => {
|
const isVideoLesson = (section: CourseSection) => {
|
||||||
|
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
|
||||||
|
// 优先根据type字段判断:0=视频
|
||||||
if (!section.outline && getLessonTypeText(section) != '考试') {
|
if (section.type === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
console.log(section.outline)
|
// 如果type为null,则根据outline判断
|
||||||
// 检查常见视频格式扩展名
|
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
|
||||||
return !!(section.outline && (
|
|
||||||
section.outline.includes('.m3u8') ||
|
|
||||||
section.outline.includes('.mp4') ||
|
|
||||||
section.outline.includes('.avi') ||
|
|
||||||
section.outline.includes('.mov') ||
|
|
||||||
section.outline.includes('.wmv')
|
|
||||||
)) || section.name.includes('视频')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为资料课时
|
const isResourceLesson = (section: CourseSection) => {
|
||||||
const isResourceLesson = (section: CourseSection): boolean => {
|
// 优先根据type字段判断:1=资料
|
||||||
return !!(section.outline && section.outline.includes('ppt')) || section.name.includes('PPT')
|
if (section.type === 1) {
|
||||||
}
|
return true
|
||||||
|
|
||||||
// 判断是否为作业课时
|
|
||||||
const isHomeworkLesson = (section: CourseSection): boolean => {
|
|
||||||
return section.name.includes('作业') || section.name.includes('练习') || section.name.includes('题目') || section.name.includes('分析')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否为考试课时
|
|
||||||
const isExamLesson = (section: CourseSection): boolean => {
|
|
||||||
return section.name.includes('考试') || section.name.includes('测试') || section.name.includes('函数&循环')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取课时类型徽章样式类
|
|
||||||
const getLessonTypeBadgeClass = (section: CourseSection): string => {
|
|
||||||
if (isVideoLesson(section)) {
|
|
||||||
return 'badge-video'
|
|
||||||
} else if (isResourceLesson(section)) {
|
|
||||||
return 'badge-resource'
|
|
||||||
} else if (isHomeworkLesson(section)) {
|
|
||||||
return 'badge-homework'
|
|
||||||
} else if (isExamLesson(section)) {
|
|
||||||
return 'badge-exam'
|
|
||||||
}
|
}
|
||||||
return 'badge-video' // 默认为视频
|
// 如果type为null,则根据outline或名称判断
|
||||||
|
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHomeworkLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:3=作业
|
||||||
|
if (section.type === 3) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
|
return section.name.includes('作业') || section.name.includes('练习')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExamLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:2=考试
|
||||||
|
if (section.type === 2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
|
return section.name.includes('考试') || section.name.includes('测试')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程类型样式类
|
||||||
|
const getLessonTypeBadgeClass = (section: CourseSection) => {
|
||||||
|
if (isVideoLesson(section)) return 'badge-video'
|
||||||
|
if (isResourceLesson(section)) return 'badge-resource'
|
||||||
|
if (isHomeworkLesson(section)) return 'badge-homework'
|
||||||
|
if (isExamLesson(section)) return 'badge-exam'
|
||||||
|
return 'badge-default'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理下载操作
|
// 处理下载操作
|
||||||
@ -1582,6 +1584,16 @@ const initializeMockState = () => {
|
|||||||
isEnrolled.value = false // false=未报名状态,true=已报名状态
|
isEnrolled.value = false // false=未报名状态,true=已报名状态
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 监听路由参数变化
|
||||||
|
watch(() => route.query.courseId, (newCourseId) => {
|
||||||
|
if (newCourseId && typeof newCourseId === 'string') {
|
||||||
|
courseId.value = newCourseId
|
||||||
|
console.log('路由参数变化,重新加载课程数据,课程ID:', courseId.value)
|
||||||
|
loadCourseDetail()
|
||||||
|
loadCourseSections()
|
||||||
|
}
|
||||||
|
}, { immediate: false })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
||||||
initializeMockState() // 初始化模拟状态
|
initializeMockState() // 初始化模拟状态
|
||||||
|
@ -59,9 +59,23 @@
|
|||||||
<div class="video-player-section">
|
<div class="video-player-section">
|
||||||
<div class="video-player enrolled">
|
<div class="video-player enrolled">
|
||||||
<div class="video-container">
|
<div class="video-container">
|
||||||
<!-- CKPlayer 容器 -->
|
<!-- DPlayer 播放器 -->
|
||||||
<div v-if="currentVideoUrl" id="ckplayer_container" class="ckplayer-container">
|
<DPlayerVideo
|
||||||
</div>
|
v-if="currentVideoUrl"
|
||||||
|
ref="videoPlayerRef"
|
||||||
|
:video-url="currentVideoUrl"
|
||||||
|
:poster="course?.coverImage || course?.thumbnail"
|
||||||
|
:title="currentVideoSection?.name || '课程视频'"
|
||||||
|
:autoplay="false"
|
||||||
|
:video-qualities="videoQualities"
|
||||||
|
:current-quality="currentQuality"
|
||||||
|
@play="onVideoPlay"
|
||||||
|
@pause="onVideoPause"
|
||||||
|
@ended="onVideoEnded"
|
||||||
|
@error="onVideoError"
|
||||||
|
@screenshot="onScreenshot"
|
||||||
|
@danmaku-send="onDanmakuSend"
|
||||||
|
/>
|
||||||
<div v-else class="video-placeholder"
|
<div v-else class="video-placeholder"
|
||||||
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
:style="{ backgroundImage: course?.coverImage || course?.thumbnail ? `url(${course.coverImage || course.thumbnail})` : '' }">
|
||||||
<div class="placeholder-content">
|
<div class="placeholder-content">
|
||||||
@ -543,12 +557,12 @@
|
|||||||
<button @click="loadCourseSections" class="retry-btn">重试</button>
|
<button @click="loadCourseSections" class="retry-btn">重试</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="courseSections.length > 0" class="sections-list">
|
<div v-else-if="courseSections.length > 0" class="sections-list">
|
||||||
<!-- 按章节分组显示 -->
|
<!-- 按章节分组显示 - 已报名状态,彩色可点击 -->
|
||||||
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
|
<div v-for="(chapter, chapterIndex) in groupedSections" :key="chapterIndex" class="chapter-section">
|
||||||
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
|
<div class="chapter-header" @click="toggleChapter(chapterIndex)">
|
||||||
<div class="chapter-info">
|
<div class="chapter-info">
|
||||||
<span class="chapter-number">第{{ chapterIndex + 1 }}章</span>
|
<span class="chapter-title">第{{ getChapterNumber(chapterIndex + 1) }}章 {{ chapter.title
|
||||||
<span class="chapter-title">{{ chapter.title }}</span>
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
|
<span class="chapter-toggle" :class="{ 'expanded': chapter.expanded }">
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12">
|
<svg width="12" height="12" viewBox="0 0 12 12">
|
||||||
@ -558,40 +572,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="chapter.expanded" class="chapter-lessons">
|
<div v-if="chapter.expanded" class="chapter-lessons">
|
||||||
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
|
<div v-for="section in chapter.sections" :key="section.id" class="lesson-item">
|
||||||
<div class="lesson-content" @click="handleSectionClick(section)">
|
<!-- 已报名状态:彩色可点击 -->
|
||||||
<div class="lesson-type-badge"
|
<div class="lesson-content enrolled" @click="handleSectionClick(section)">
|
||||||
:class="[getLessonTypeBadgeClass(section), { 'disabled': !isUserEnrolled }]">
|
<div class="lesson-type-badge" :class="getLessonTypeBadgeClass(section)">
|
||||||
{{ getLessonTypeText(section) }}
|
{{ getLessonTypeText(section) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-info">
|
<div class="lesson-info">
|
||||||
<span class="lesson-title" :class="{ 'disabled': !isUserEnrolled }">{{ section.name
|
<span class="lesson-title">{{ section.name }}</span>
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="lesson-meta">
|
<div class="lesson-meta">
|
||||||
<span v-if="isVideoLesson(section)" class="lesson-duration"
|
<span v-if="isVideoLesson(section)" class="lesson-duration">{{
|
||||||
:class="{ 'disabled': !isUserEnrolled }">{{ formatLessonDuration(section) }}</span>
|
formatLessonDuration(section) }}</span>
|
||||||
<div class="lesson-actions">
|
<div class="lesson-actions">
|
||||||
<!-- 视频播放图标 -->
|
<!-- 视频播放图标 - 可点击 -->
|
||||||
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
|
<button v-if="isVideoLesson(section)" class="lesson-action-btn video-btn"
|
||||||
@click.stop="handleVideoPlay(section)">
|
@click.stop="handleVideoPlay(section)">
|
||||||
<img src="/images/courses/video-enroll.png" alt="视频" width="14" height="14">
|
<img src="/public/images/courses/video-enroll.png" alt="视频" width="14" height="14">
|
||||||
</button>
|
</button>
|
||||||
<!-- 下载图标 -->
|
<!-- 下载图标 - 可点击 -->
|
||||||
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
|
<button v-else-if="isResourceLesson(section)" class="lesson-action-btn download-btn"
|
||||||
@click.stop="handleDownload(section)">
|
@click.stop="handleDownload(section)">
|
||||||
<img src="/images/courses/download-enroll.png" alt="资料" width="14" height="14">
|
<img src="/public/images/courses/download-enroll.png" alt="资料" width="14" height="14">
|
||||||
</button>
|
</button>
|
||||||
<!-- 作业图标 -->
|
<!-- 编辑图标(作业) - 可点击 -->
|
||||||
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
|
<button v-else-if="isHomeworkLesson(section)" class="lesson-action-btn edit-btn"
|
||||||
@click.stop="handleHomework(section)">
|
@click.stop="handleHomework(section)">
|
||||||
<img src="/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
|
<img src="/public/images/courses/homework-enroll.png" alt="作业" width="14" height="14">
|
||||||
</button>
|
</button>
|
||||||
<!-- 考试图标 -->
|
<!-- 考试图标 - 可点击 -->
|
||||||
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
|
<button v-else-if="isExamLesson(section)" class="lesson-action-btn exam-btn"
|
||||||
@click.stop="handleExam(section)">
|
@click.stop="handleExam(section)">
|
||||||
<img src="/images/courses/examination-enroll.png" alt="考试" width="14" height="14">
|
<img src="/public/images/courses/examination-enroll.png" alt="考试" width="14"
|
||||||
|
height="14">
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -938,13 +951,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onActivated, nextTick } from 'vue'
|
import { ref, computed, onMounted, onActivated } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
// import { useAuth } from '@/composables/useAuth'
|
// import { useAuth } from '@/composables/useAuth'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { CourseApi } from '@/api/modules/course'
|
import { CourseApi } from '@/api/modules/course'
|
||||||
import type { Course, CourseSection } from '@/api/types'
|
import type { Course, CourseSection } from '@/api/types'
|
||||||
import QuillEditor from '@/components/common/QuillEditor.vue'
|
import QuillEditor from '@/components/common/QuillEditor.vue'
|
||||||
|
import DPlayerVideo from '@/components/course/DPlayerVideo.vue'
|
||||||
|
|
||||||
// import LoginModal from '@/components/auth/LoginModal.vue'
|
// import LoginModal from '@/components/auth/LoginModal.vue'
|
||||||
// import RegisterModal from '@/components/auth/RegisterModal.vue'
|
// import RegisterModal from '@/components/auth/RegisterModal.vue'
|
||||||
@ -953,7 +968,8 @@ import QuillEditor from '@/components/common/QuillEditor.vue'
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const courseId = ref(Number(route.params.id))
|
const message = useMessage()
|
||||||
|
const courseId = ref(String(route.params.id))
|
||||||
// const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
|
// const { loginModalVisible, registerModalVisible, handleAuthSuccess, showLoginModal } = useAuth()
|
||||||
// enrollCourse 暂时未使用,后续需要时再启用
|
// enrollCourse 暂时未使用,后续需要时再启用
|
||||||
|
|
||||||
@ -1032,74 +1048,54 @@ interface ChapterGroup {
|
|||||||
|
|
||||||
const groupedSections = ref<ChapterGroup[]>([])
|
const groupedSections = ref<ChapterGroup[]>([])
|
||||||
|
|
||||||
// 生成模拟章节数据(用于演示)
|
|
||||||
const generateMockSections = (): CourseSection[] => {
|
|
||||||
return [
|
|
||||||
// 第一章 - 课前准备 (4个)
|
|
||||||
{ id: '1', lessonId: String(courseId.value), name: '开课彩蛋:新开始新征程', outline: 'https://example.com/video1.m3u8', type: 0, parentId: '0', sort: 1, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
|
||||||
{ id: '2', lessonId: String(courseId.value), name: '课程定位与目标', outline: 'https://example.com/video2.m3u8', type: 0, parentId: '0', sort: 2, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:44:05' },
|
|
||||||
{ id: '3', lessonId: String(courseId.value), name: '教学安排及学习建议', outline: 'https://example.com/video3.m3u8', type: 0, parentId: '0', sort: 3, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
|
||||||
{ id: '4', lessonId: String(courseId.value), name: '课前准备PPT', outline: 'https://example.com/ppt1.ppt', type: 1, parentId: '0', sort: 4, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
|
|
||||||
// 第二章 - 程序设计基础知识 (5个)
|
|
||||||
{ id: '5', lessonId: String(courseId.value), name: '第一课 程序设计入门', outline: 'https://example.com/video4.m3u8', type: 0, parentId: '0', sort: 5, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '00:52:22' },
|
|
||||||
{ id: '6', lessonId: String(courseId.value), name: '操作PPT', outline: 'https://example.com/ppt2.ppt', type: 1, parentId: '0', sort: 6, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
{ id: '7', lessonId: String(courseId.value), name: '第二课 循环结构', outline: 'https://example.com/video5.m3u8', type: 0, parentId: '0', sort: 7, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: true, duration: '01:03:56' },
|
|
||||||
{ id: '8', lessonId: String(courseId.value), name: '函数&循环', outline: 'https://example.com/video5.m3u8', type: 0, parentId: '0', sort: 8, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
{ id: '9', lessonId: String(courseId.value), name: '第三课 条件结构', outline: 'https://example.com/video6.m3u8', type: 0, parentId: '0', sort: 9, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:45:30' },
|
|
||||||
|
|
||||||
// 第三章 - 实战项目 (6个)
|
// 将章节按章分组 - 根据后端数据结构重新实现
|
||||||
{ id: '10', lessonId: String(courseId.value), name: '项目一:计算器开发', outline: 'https://example.com/video7.m3u8', type: 0, parentId: '0', sort: 10, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:20:15' },
|
|
||||||
{ id: '11', lessonId: String(courseId.value), name: '项目源码下载', outline: 'https://example.com/source1.zip', type: 1, parentId: '0', sort: 11, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
{ id: '12', lessonId: String(courseId.value), name: '项目二:数据管理系统', outline: 'https://example.com/video8.m3u8', type: 0, parentId: '0', sort: 12, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:45:20' },
|
|
||||||
{ id: '13', lessonId: String(courseId.value), name: '作业:完成个人项目', outline: '', type: 3, parentId: '0', sort: 13, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
{ id: '14', lessonId: String(courseId.value), name: '项目三:Web应用开发', outline: 'https://example.com/video9.m3u8', type: 0, parentId: '0', sort: 14, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '02:10:45' },
|
|
||||||
{ id: '15', lessonId: String(courseId.value), name: '期末考试', outline: '', type: 2, parentId: '0', sort: 15, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
|
|
||||||
// 第四章 - 高级应用 (4个)
|
|
||||||
{ id: '16', lessonId: String(courseId.value), name: '高级特性介绍', outline: 'https://example.com/video10.m3u8', type: 0, parentId: '0', sort: 16, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:55:30' },
|
|
||||||
{ id: '17', lessonId: String(courseId.value), name: '性能优化技巧', outline: 'https://example.com/video11.m3u8', type: 0, parentId: '0', sort: 17, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:15:20' },
|
|
||||||
{ id: '18', lessonId: String(courseId.value), name: '部署与发布', outline: 'https://example.com/video12.m3u8', type: 0, parentId: '0', sort: 18, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:40:15' },
|
|
||||||
{ id: '19', lessonId: String(courseId.value), name: '课程总结', outline: 'https://example.com/video13.m3u8', type: 0, parentId: '0', sort: 19, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:30:10' },
|
|
||||||
|
|
||||||
// 第五章 - 拓展学习 (3个)
|
|
||||||
{ id: '20', lessonId: String(courseId.value), name: '行业发展趋势', outline: 'https://example.com/video14.m3u8', type: 0, parentId: '0', sort: 20, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:35:45' },
|
|
||||||
{ id: '21', lessonId: String(courseId.value), name: '学习资源推荐', outline: 'https://example.com/resources.pdf', type: 1, parentId: '0', sort: 21, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
{ id: '22', lessonId: String(courseId.value), name: '结业证书申请', outline: '', type: 1, parentId: '0', sort: 22, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: undefined },
|
|
||||||
|
|
||||||
// 第六章 - 答疑与交流 (2个)
|
|
||||||
{ id: '23', lessonId: String(courseId.value), name: '常见问题解答', outline: 'https://example.com/video15.m3u8', type: 0, parentId: '0', sort: 23, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '00:25:30' },
|
|
||||||
{ id: '24', lessonId: String(courseId.value), name: '在线答疑直播', outline: 'https://example.com/live1.m3u8', type: 0, parentId: '0', sort: 24, level: 1, revision: 1, createdAt: Date.now(), updatedAt: Date.now(), deletedAt: null, completed: false, duration: '01:30:00' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将章节按章分组
|
|
||||||
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
const groupSectionsByChapter = (sections: CourseSection[]) => {
|
||||||
const chapterTitles = [
|
console.log('🔍 开始分组章节数据:', sections)
|
||||||
'课前准备',
|
|
||||||
'程序设计基础知识',
|
|
||||||
'实战项目',
|
|
||||||
'高级应用',
|
|
||||||
'拓展学习',
|
|
||||||
'答疑与交流'
|
|
||||||
]
|
|
||||||
|
|
||||||
const groups: ChapterGroup[] = []
|
const groups: ChapterGroup[] = []
|
||||||
let sectionsPerChapter = [4, 5, 6, 4, 3, 2] // 每章的课程数量
|
|
||||||
let sectionIndex = 0
|
|
||||||
|
|
||||||
for (let i = 0; i < chapterTitles.length; i++) {
|
// 找出所有一级章节(level=1,这些是父章节)
|
||||||
const chapterSections = sections.slice(sectionIndex, sectionIndex + sectionsPerChapter[i])
|
const parentChapters = sections.filter(section => section.level === 1)
|
||||||
if (chapterSections.length > 0) {
|
console.log('🔍 找到一级章节:', parentChapters)
|
||||||
groups.push({
|
|
||||||
title: `第${i + 1}章 ${chapterTitles[i]}`,
|
// 按sortOrder降序排序一级章节(sortOrder越大越靠前)
|
||||||
sections: chapterSections,
|
parentChapters.sort((a, b) => b.sort - a.sort)
|
||||||
expanded: i === 0 // 默认展开第一章
|
|
||||||
})
|
// 为每个一级章节创建分组
|
||||||
}
|
parentChapters.forEach((parentChapter, index) => {
|
||||||
sectionIndex += sectionsPerChapter[i]
|
// 找出该章节下的所有子章节(level=2,parentId匹配)
|
||||||
|
const childSections = sections.filter(section =>
|
||||||
|
section.level === 2 && section.parentId === parentChapter.id
|
||||||
|
)
|
||||||
|
|
||||||
|
// 按sortOrder降序排序子章节(sortOrder越大越靠前)
|
||||||
|
childSections.sort((a, b) => b.sort - a.sort)
|
||||||
|
|
||||||
|
console.log(`🔍 章节"${parentChapter.name}"的子章节:`, childSections)
|
||||||
|
|
||||||
|
// 创建章节分组
|
||||||
|
groups.push({
|
||||||
|
title: parentChapter.name, // 使用后端返回的章节名称
|
||||||
|
sections: childSections.length > 0 ? childSections : [parentChapter], // 如果有子章节就用子章节,否则用父章节本身
|
||||||
|
expanded: index === 0 // 默认展开第一章
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果没有找到一级章节,可能所有章节都是同级的,直接作为一个组
|
||||||
|
if (groups.length === 0 && sections.length > 0) {
|
||||||
|
console.log('🔍 没有找到层级结构,将所有章节作为一组')
|
||||||
|
// 按sortOrder降序排序(sortOrder越大越靠前)
|
||||||
|
const sortedSections = [...sections].sort((a, b) => b.sort - a.sort)
|
||||||
|
groups.push({
|
||||||
|
title: '课程章节',
|
||||||
|
sections: sortedSections,
|
||||||
|
expanded: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('✅ 章节分组完成:', groups)
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1159,7 +1155,11 @@ const getVideoUrl = (section?: CourseSection) => {
|
|||||||
// 视频播放相关状态
|
// 视频播放相关状态
|
||||||
const currentVideoUrl = ref<string>('')
|
const currentVideoUrl = ref<string>('')
|
||||||
const currentVideoSection = ref<CourseSection | null>(null)
|
const currentVideoSection = ref<CourseSection | null>(null)
|
||||||
const ckplayer = ref<any>(null)
|
const currentVideo = ref<any>(null)
|
||||||
|
const videoQualities = ref<any[]>([])
|
||||||
|
const currentQuality = ref<string>('360')
|
||||||
|
const videoLoading = ref<boolean>(false)
|
||||||
|
const videoPlayerRef = ref<any>(null)
|
||||||
|
|
||||||
// 新增的响应式数据
|
// 新增的响应式数据
|
||||||
const aiActiveTab = ref('assistant')
|
const aiActiveTab = ref('assistant')
|
||||||
@ -1351,18 +1351,8 @@ const replyToUsername = ref('')
|
|||||||
const handleVideoPlay = async (section: CourseSection) => {
|
const handleVideoPlay = async (section: CourseSection) => {
|
||||||
console.log('播放视频:', section.name)
|
console.log('播放视频:', section.name)
|
||||||
|
|
||||||
// 获取视频URL
|
// 加载章节视频数据
|
||||||
const videoUrl = getVideoUrl(section)
|
await loadSectionVideo(section)
|
||||||
currentVideoUrl.value = videoUrl
|
|
||||||
currentVideoSection.value = section
|
|
||||||
|
|
||||||
console.log('使用视频源:', videoUrl)
|
|
||||||
|
|
||||||
// 等待DOM更新
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
// 初始化CKPlayer播放器
|
|
||||||
initCKPlayer(videoUrl)
|
|
||||||
|
|
||||||
// 标记为已完成
|
// 标记为已完成
|
||||||
if (!section.completed) {
|
if (!section.completed) {
|
||||||
@ -1381,134 +1371,99 @@ const handleVideoPlay = async (section: CourseSection) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化CKPlayer播放器
|
// 加载章节视频
|
||||||
const initCKPlayer = (url: string) => {
|
const loadSectionVideo = async (section: CourseSection) => {
|
||||||
// 清理之前的播放器实例
|
|
||||||
if (ckplayer.value) {
|
|
||||||
try {
|
|
||||||
ckplayer.value.remove()
|
|
||||||
} catch (e) {
|
|
||||||
console.log('清理播放器实例时出错:', e)
|
|
||||||
}
|
|
||||||
ckplayer.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查CKPlayer是否已加载
|
|
||||||
if (typeof window.ckplayer === 'undefined') {
|
|
||||||
console.error('CKPlayer not loaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 若容器暂未挂载,延迟重试一次,避免"未找到放置视频的容器"
|
|
||||||
const containerEl = document.querySelector('#ckplayer_container') as HTMLElement | null
|
|
||||||
if (!containerEl) {
|
|
||||||
console.warn('Player container not found, retrying init...')
|
|
||||||
setTimeout(() => initCKPlayer(url), 50)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断视频类型
|
|
||||||
const isMP4 = url.endsWith('.mp4')
|
|
||||||
const isHLS = url.endsWith('.m3u8')
|
|
||||||
|
|
||||||
// CKPlayer配置
|
|
||||||
const videoObject = {
|
|
||||||
container: '#ckplayer_container', // 容器ID
|
|
||||||
autoplay: false, // 自动播放
|
|
||||||
video: url, // 视频地址
|
|
||||||
volume: 0.8, // 音量
|
|
||||||
poster: course.value?.coverImage || course.value?.thumbnail || '', // 封面图
|
|
||||||
live: false, // 是否直播
|
|
||||||
// 根据视频格式选择插件
|
|
||||||
plug: isHLS ? 'hls.js' : '', // HLS使用hls.js插件,MP4不需要插件
|
|
||||||
playbackrateOpen: true, // 开启倍速选项
|
|
||||||
playbackrateList: [0.5, 0.75, 1, 1.25, 1.5, 2], // 播放速度选项
|
|
||||||
seek: 0, // 默认跳转秒数
|
|
||||||
loaded: 'loadedHandler', // 播放器加载完成回调
|
|
||||||
ended: 'endedHandler', // 播放结束回调
|
|
||||||
error: 'errorHandler', // 播放错误回调
|
|
||||||
title: currentVideoSection.value?.name || '课程视频', // 视频标题
|
|
||||||
controls: true, // 显示控制栏
|
|
||||||
webFull: true, // 启用页面全屏
|
|
||||||
screenshot: true, // 启用截图功能
|
|
||||||
timeScheduleAdjust: 1, // 允许调节播放进度
|
|
||||||
// MP4视频的额外配置
|
|
||||||
...(isMP4 && {
|
|
||||||
type: 'mp4', // 明确指定视频类型
|
|
||||||
crossOrigin: 'anonymous' // 跨域设置
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建播放器实例
|
videoLoading.value = true
|
||||||
ckplayer.value = new window.ckplayer(videoObject)
|
console.log('🔍 加载章节视频,章节ID:', section.id)
|
||||||
console.log('CKPlayer initialized successfully for:', isMP4 ? 'MP4' : 'HLS')
|
|
||||||
|
const response = await CourseApi.getSectionVideos(courseId.value, section.id)
|
||||||
|
console.log('🔍 视频API响应:', response)
|
||||||
|
|
||||||
|
if (response.code === 0 || response.code === 200) {
|
||||||
|
if (response.data && response.data.length > 0) {
|
||||||
|
const video = response.data[0] // 取第一个视频
|
||||||
|
currentVideo.value = video
|
||||||
|
videoQualities.value = video.qualities
|
||||||
|
currentQuality.value = video.defaultQuality
|
||||||
|
|
||||||
|
// 设置视频清晰度选项
|
||||||
|
videoQualities.value = video.qualities || []
|
||||||
|
currentQuality.value = video.defaultQuality || '360'
|
||||||
|
|
||||||
|
// 获取默认清晰度的URL
|
||||||
|
const defaultQualityVideo = video.qualities.find((q: any) => q.value === video.defaultQuality)
|
||||||
|
if (defaultQualityVideo) {
|
||||||
|
currentVideoUrl.value = defaultQualityVideo.url
|
||||||
|
currentVideoSection.value = section
|
||||||
|
console.log('✅ 设置视频URL:', currentVideoUrl.value)
|
||||||
|
console.log('✅ 可用清晰度:', video.qualities)
|
||||||
|
console.log('✅ 传递给DPlayer的清晰度:', videoQualities.value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ 没有找到视频数据')
|
||||||
|
// 如果没有视频数据,使用默认视频
|
||||||
|
currentVideoUrl.value = 'https://example.com/default-video.mp4'
|
||||||
|
currentVideoSection.value = section
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('❌ 获取视频失败:', response.message)
|
||||||
|
// 使用默认视频
|
||||||
|
currentVideoUrl.value = 'https://example.com/default-video.mp4'
|
||||||
|
currentVideoSection.value = section
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize CKPlayer:', error)
|
console.error('❌ 加载章节视频失败:', error)
|
||||||
|
// 使用默认视频
|
||||||
|
currentVideoUrl.value = 'https://example.com/default-video.mp4'
|
||||||
|
currentVideoSection.value = section
|
||||||
|
} finally {
|
||||||
|
videoLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载模拟课程数据
|
// DPlayer 事件处理函数
|
||||||
const loadMockCourseData = () => {
|
const onVideoPlay = () => {
|
||||||
console.log('加载模拟课程数据')
|
console.log('视频开始播放')
|
||||||
course.value = {
|
|
||||||
id: String(courseId.value || 1),
|
|
||||||
title: 'DeepSeek办公自动化职业岗位标准课程',
|
|
||||||
description: '本课程将帮助您掌握DeepSeek的基本使用方法,了解办公自动化职业岗位标准,提高教学质量和效率,获得实际工作技能。',
|
|
||||||
instructor: {
|
|
||||||
id: 1,
|
|
||||||
name: 'DeepSeek技术学院',
|
|
||||||
title: '讲师',
|
|
||||||
bio: '专注于AI技术应用与教学',
|
|
||||||
avatar: '/images/aiCompanion/AI小助手@2x.png',
|
|
||||||
rating: 4.8,
|
|
||||||
studentsCount: 1000,
|
|
||||||
coursesCount: 10,
|
|
||||||
experience: '5年教学经验',
|
|
||||||
education: ['计算机科学硕士'],
|
|
||||||
certifications: ['高级讲师认证']
|
|
||||||
},
|
|
||||||
duration: '12小时43分钟',
|
|
||||||
totalLessons: 54,
|
|
||||||
rating: 4.8,
|
|
||||||
studentsCount: 1000,
|
|
||||||
price: 0,
|
|
||||||
originalPrice: 299,
|
|
||||||
currency: 'CNY',
|
|
||||||
level: 'beginner',
|
|
||||||
language: 'zh-CN',
|
|
||||||
category: {
|
|
||||||
id: 1,
|
|
||||||
name: 'AI技术',
|
|
||||||
slug: 'ai-technology',
|
|
||||||
description: 'AI技术相关课程'
|
|
||||||
},
|
|
||||||
tags: ['AI', '办公自动化', 'DeepSeek'],
|
|
||||||
skills: ['DeepSeek', '办公自动化'],
|
|
||||||
requirements: ['基础计算机操作能力', '对AI技术感兴趣'],
|
|
||||||
objectives: ['掌握DeepSeek的基本使用方法', '了解办公自动化职业岗位标准', '提高教学质量和效率'],
|
|
||||||
thumbnail: '/images/aiCompanion/bg.png',
|
|
||||||
coverImage: '/images/aiCompanion/bg.png',
|
|
||||||
content: '课程内容详细介绍...',
|
|
||||||
status: 'published',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
ratingCount: 0,
|
|
||||||
isEnrolled: false,
|
|
||||||
progress: 0,
|
|
||||||
isFavorite: false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onVideoPause = () => {
|
||||||
|
console.log('视频暂停')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideoEnded = () => {
|
||||||
|
console.log('视频播放结束')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onVideoError = (error: Event) => {
|
||||||
|
console.error('视频播放出错:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增的事件处理函数
|
||||||
|
const onScreenshot = (dataUrl: string) => {
|
||||||
|
console.log('截屏成功:', dataUrl)
|
||||||
|
// 可以在这里添加截屏成功的提示
|
||||||
|
message.success('截屏成功!')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDanmakuSend = (text: string) => {
|
||||||
|
console.log('发送弹幕:', text)
|
||||||
|
// 可以在这里添加弹幕发送的逻辑,比如保存到数据库
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 加载课程详情
|
// 加载课程详情
|
||||||
const loadCourseDetail = async () => {
|
const loadCourseDetail = async () => {
|
||||||
console.log('开始加载课程详情,课程ID:', courseId.value)
|
console.log('开始加载课程详情,课程ID:', courseId.value)
|
||||||
|
|
||||||
if (!courseId.value) courseId.value = 1
|
if (!courseId.value) {
|
||||||
|
courseId.value = '1'
|
||||||
|
}
|
||||||
|
|
||||||
if (!courseId.value || isNaN(courseId.value)) {
|
if (!courseId.value || isNaN(Number(courseId.value))) {
|
||||||
console.log('课程ID无效,使用模拟数据')
|
console.log('课程ID无效')
|
||||||
loadMockCourseData()
|
error.value = '课程ID无效'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1516,8 +1471,8 @@ const loadCourseDetail = async () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
console.log('调用API获取课程详情...')
|
console.log('调用API获取课程详情,课程ID:', courseId.value)
|
||||||
const response = await CourseApi.getCourseById(String(courseId.value))
|
const response = await CourseApi.getCourseById(courseId.value)
|
||||||
console.log('API响应:', response)
|
console.log('API响应:', response)
|
||||||
|
|
||||||
if (response.code === 0 || response.code === 200) {
|
if (response.code === 0 || response.code === 200) {
|
||||||
@ -1546,13 +1501,12 @@ const loadCourseDetail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回错误,使用模拟数据')
|
console.log('API返回错误')
|
||||||
loadMockCourseData()
|
error.value = response.message || '获取课程详情失败'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载课程详情失败:', err)
|
console.error('加载课程详情失败:', err)
|
||||||
console.log('API调用失败,使用模拟数据')
|
error.value = '网络错误,请稍后重试'
|
||||||
loadMockCourseData()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -1560,7 +1514,7 @@ const loadCourseDetail = async () => {
|
|||||||
|
|
||||||
// 加载课程章节列表
|
// 加载课程章节列表
|
||||||
const loadCourseSections = async () => {
|
const loadCourseSections = async () => {
|
||||||
if (!courseId.value || isNaN(courseId.value)) {
|
if (!courseId.value || isNaN(Number(courseId.value))) {
|
||||||
sectionsError.value = '课程ID无效'
|
sectionsError.value = '课程ID无效'
|
||||||
console.error('课程ID无效:', courseId.value)
|
console.error('课程ID无效:', courseId.value)
|
||||||
return
|
return
|
||||||
@ -1571,52 +1525,38 @@ const loadCourseSections = async () => {
|
|||||||
sectionsError.value = ''
|
sectionsError.value = ''
|
||||||
|
|
||||||
console.log('调用API获取课程章节...')
|
console.log('调用API获取课程章节...')
|
||||||
const response = await CourseApi.getCourseSections(String(courseId.value))
|
const response = await CourseApi.getCourseSections(courseId.value)
|
||||||
console.log('章节API响应:', response)
|
console.log('章节API响应:', response)
|
||||||
|
|
||||||
if (response.code === 0 || response.code === 200) {
|
if (response.code === 0 || response.code === 200) {
|
||||||
if (response.data && Array.isArray(response.data)) {
|
if (response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||||
courseSections.value = response.data
|
console.log('✅ API返回的原始章节数据:', response.data.list)
|
||||||
groupedSections.value = groupSectionsByChapter(response.data)
|
console.log('✅ 章节数据数量:', response.data.list.length)
|
||||||
console.log('章节数据设置成功:', courseSections.value)
|
|
||||||
console.log('分组数据:', groupedSections.value)
|
courseSections.value = response.data.list
|
||||||
|
groupedSections.value = groupSectionsByChapter(response.data.list)
|
||||||
|
|
||||||
|
console.log('✅ 设置后的courseSections:', courseSections.value)
|
||||||
|
console.log('✅ 设置后的groupedSections:', groupedSections.value)
|
||||||
|
console.log('✅ groupedSections长度:', groupedSections.value.length)
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回的章节数据为空,使用模拟数据')
|
console.log('❌ API返回的章节数据为空或格式错误')
|
||||||
loadMockData()
|
console.log('❌ response.data:', response.data)
|
||||||
|
sectionsError.value = '暂无课程章节数据'
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('API返回错误,使用模拟数据')
|
console.log('API返回错误')
|
||||||
loadMockData()
|
sectionsError.value = response.message || '获取课程章节失败'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('加载课程章节失败:', err)
|
console.error('加载课程章节失败:', err)
|
||||||
console.log('API调用失败,使用模拟数据')
|
sectionsError.value = '网络错误,请稍后重试'
|
||||||
loadMockData()
|
|
||||||
} finally {
|
} finally {
|
||||||
sectionsLoading.value = false
|
sectionsLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载模拟数据
|
|
||||||
const loadMockData = () => {
|
|
||||||
console.log('加载模拟章节数据')
|
|
||||||
const mockSections = generateMockSections()
|
|
||||||
courseSections.value = mockSections
|
|
||||||
groupedSections.value = groupSectionsByChapter(mockSections)
|
|
||||||
|
|
||||||
// 计算学习进度
|
|
||||||
const completed = mockSections.filter(section => section.completed).length
|
|
||||||
completedLessons.value = completed
|
|
||||||
|
|
||||||
// 计算各类进度
|
|
||||||
const videoSections = mockSections.filter(section => isVideoLesson(section))
|
|
||||||
const exerciseSections = mockSections.filter(section => isHomeworkLesson(section))
|
|
||||||
const examSections = mockSections.filter(section => isExamLesson(section))
|
|
||||||
|
|
||||||
videoProgress.value = videoSections.length > 0 ? Math.round((videoSections.filter(s => s.completed).length / videoSections.length) * 100) : 0
|
|
||||||
exerciseProgress.value = exerciseSections.length > 0 ? Math.round((exerciseSections.filter(s => s.completed).length / exerciseSections.length) * 100) : 0
|
|
||||||
examProgress.value = examSections.length > 0 ? Math.round((examSections.filter(s => s.completed).length / examSections.length) * 100) : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换章节展开/折叠
|
// 切换章节展开/折叠
|
||||||
const toggleChapter = (chapterIndex: number) => {
|
const toggleChapter = (chapterIndex: number) => {
|
||||||
@ -1638,87 +1578,72 @@ const toggleChapter = (chapterIndex: number) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取课时类型文本
|
// 获取课程类型文本
|
||||||
const getLessonTypeText = (section: CourseSection): string => {
|
const getLessonTypeText = (section: CourseSection) => {
|
||||||
if (section.outline && section.outline.includes('ppt')) {
|
if (isVideoLesson(section)) return '视频'
|
||||||
return '资料'
|
if (isResourceLesson(section)) return '资料'
|
||||||
} else if (section.name.includes('作业') || section.name.includes('练习')) {
|
if (isHomeworkLesson(section)) return '作业'
|
||||||
return '作业'
|
if (isExamLesson(section)) return '考试'
|
||||||
} else if (section.name.includes('考试') || section.name.includes('测试')) {
|
return '视频'
|
||||||
return '考试'
|
|
||||||
}
|
|
||||||
return '视频' // 默认为视频
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化课时时长
|
// 获取章节编号
|
||||||
const formatLessonDuration = (section: CourseSection): string => {
|
const getChapterNumber = (num: number) => {
|
||||||
// 根据课时名称和类型生成合适的时长
|
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十']
|
||||||
const durations = [
|
return numbers[num - 1] || num.toString()
|
||||||
'01:03:56', '00:44:05', '00:52:22', '', // 第一章时长
|
|
||||||
'00:52:22', '', '01:03:56', '', '' // 第二章时长
|
|
||||||
]
|
|
||||||
|
|
||||||
// 根据section.id获取对应时长
|
|
||||||
const durationIndex = Number(section.id) - 1
|
|
||||||
if (durationIndex >= 0 && durationIndex < durations.length) {
|
|
||||||
return durations[durationIndex] || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认时长生成
|
|
||||||
if (isVideoLesson(section)) {
|
|
||||||
const minutes = Math.floor(Math.random() * 60) + 10 // 10-70分钟
|
|
||||||
const seconds = Math.floor(Math.random() * 60)
|
|
||||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return '' // 非视频课时不显示时长
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为视频课时
|
// 格式化课程时长
|
||||||
const isVideoLesson = (section: CourseSection): boolean => {
|
const formatLessonDuration = (section: CourseSection) => {
|
||||||
|
if (!section.duration) return ''
|
||||||
|
return section.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// 课程类型判断函数 - 与CourseDetailEnrolled保持一致
|
||||||
if (!section.outline && getLessonTypeText(section) != '考试') {
|
const isVideoLesson = (section: CourseSection) => {
|
||||||
|
console.log('检查章节类型:', section.name, 'type:', section.type, 'outline:', section.outline)
|
||||||
|
// 优先根据type字段判断:0=视频
|
||||||
|
if (section.type === 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
console.log(section.outline)
|
// 如果type为null,则根据outline判断
|
||||||
// 检查常见视频格式扩展名
|
return section.outline && (section.outline.includes('.m3u8') || section.outline.includes('.mp4'))
|
||||||
return !!(section.outline && (
|
|
||||||
section.outline.includes('.m3u8') ||
|
|
||||||
section.outline.includes('.mp4') ||
|
|
||||||
section.outline.includes('.avi') ||
|
|
||||||
section.outline.includes('.mov') ||
|
|
||||||
section.outline.includes('.wmv')
|
|
||||||
)) || section.name.includes('视频')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否为资料课时
|
const isResourceLesson = (section: CourseSection) => {
|
||||||
const isResourceLesson = (section: CourseSection): boolean => {
|
// 优先根据type字段判断:1=资料
|
||||||
return !!(section.outline && section.outline.includes('ppt')) || section.name.includes('PPT')
|
if (section.type === 1) {
|
||||||
}
|
return true
|
||||||
|
|
||||||
// 判断是否为作业课时
|
|
||||||
const isHomeworkLesson = (section: CourseSection): boolean => {
|
|
||||||
return section.name.includes('作业') || section.name.includes('练习') || section.name.includes('题目') || section.name.includes('分析')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否为考试课时
|
|
||||||
const isExamLesson = (section: CourseSection): boolean => {
|
|
||||||
return section.name.includes('考试') || section.name.includes('测试') || section.name.includes('函数&循环')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取课时类型徽章样式类
|
|
||||||
const getLessonTypeBadgeClass = (section: CourseSection): string => {
|
|
||||||
if (isVideoLesson(section)) {
|
|
||||||
return 'badge-video'
|
|
||||||
} else if (isResourceLesson(section)) {
|
|
||||||
return 'badge-resource'
|
|
||||||
} else if (isHomeworkLesson(section)) {
|
|
||||||
return 'badge-homework'
|
|
||||||
} else if (isExamLesson(section)) {
|
|
||||||
return 'badge-exam'
|
|
||||||
}
|
}
|
||||||
return 'badge-video' // 默认为视频
|
// 如果type为null,则根据outline或名称判断
|
||||||
|
return section.outline && (section.outline.includes('.pdf') || section.outline.includes('.ppt') || section.outline.includes('.zip'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHomeworkLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:3=作业
|
||||||
|
if (section.type === 3) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
|
return section.name.includes('作业') || section.name.includes('练习')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExamLesson = (section: CourseSection) => {
|
||||||
|
// 优先根据type字段判断:2=考试
|
||||||
|
if (section.type === 2) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 如果type为null,则根据名称判断
|
||||||
|
return section.name.includes('考试') || section.name.includes('测试')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取课程类型样式类
|
||||||
|
const getLessonTypeBadgeClass = (section: CourseSection) => {
|
||||||
|
if (isVideoLesson(section)) return 'badge-video'
|
||||||
|
if (isResourceLesson(section)) return 'badge-resource'
|
||||||
|
if (isHomeworkLesson(section)) return 'badge-homework'
|
||||||
|
if (isExamLesson(section)) return 'badge-exam'
|
||||||
|
return 'badge-default'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理下载操作
|
// 处理下载操作
|
||||||
@ -1766,25 +1691,41 @@ const handleExam = (section: CourseSection) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 点击课程章节标题
|
// 处理章节点击 - 已报名状态,可以正常点击
|
||||||
const handleSectionClick = (section: CourseSection) => {
|
const handleSectionClick = (section: CourseSection) => {
|
||||||
console.log('点击课程章节:', section)
|
console.log('🔍 点击课程章节:', section.name, section)
|
||||||
|
|
||||||
// 设置当前选中的章节
|
|
||||||
currentSection.value = section
|
currentSection.value = section
|
||||||
|
|
||||||
// 根据章节类型处理
|
// 检查章节类型
|
||||||
if (isVideoLesson(section)) {
|
const isVideo = isVideoLesson(section)
|
||||||
handleVideoPlay(section)
|
const isResource = isResourceLesson(section)
|
||||||
} else if (isResourceLesson(section)) {
|
const isHomework = isHomeworkLesson(section)
|
||||||
|
const isExam = isExamLesson(section)
|
||||||
|
|
||||||
|
console.log('🔍 章节类型判断结果:', {
|
||||||
|
isVideo,
|
||||||
|
isResource,
|
||||||
|
isHomework,
|
||||||
|
isExam,
|
||||||
|
type: section.type
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果是视频课程,加载视频数据
|
||||||
|
if (isVideo) {
|
||||||
|
console.log('✅ 识别为视频课程,开始加载视频数据')
|
||||||
|
loadSectionVideo(section)
|
||||||
|
} else if (isResource) {
|
||||||
|
console.log('✅ 识别为资料课程')
|
||||||
handleDownload(section)
|
handleDownload(section)
|
||||||
} else if (isHomeworkLesson(section)) {
|
} else if (isHomework) {
|
||||||
|
console.log('✅ 识别为作业课程')
|
||||||
handleHomework(section)
|
handleHomework(section)
|
||||||
} else if (isExamLesson(section)) {
|
} else if (isExam) {
|
||||||
|
console.log('✅ 识别为考试课程')
|
||||||
handleExam(section)
|
handleExam(section)
|
||||||
} else {
|
} else {
|
||||||
// 如果不是视频,显示预览
|
console.log('⚠️ 未识别的课程类型,默认当作视频处理')
|
||||||
previewSection(section)
|
loadSectionVideo(section)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2023,30 +1964,10 @@ const deleteNote = (index: number) => {
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 初始化模拟状态(用于演示)
|
|
||||||
const initializeMockState = () => {
|
|
||||||
// 模拟用户已登录
|
|
||||||
if (!userStore.isLoggedIn) {
|
|
||||||
userStore.user = {
|
|
||||||
id: 1,
|
|
||||||
username: 'testuser',
|
|
||||||
email: 'test@example.com',
|
|
||||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&auto=format&fit=crop&w=50&q=80',
|
|
||||||
role: 'student',
|
|
||||||
status: 'active',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
}
|
|
||||||
userStore.token = 'mock-token'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟用户未报名状态,可以测试完整的报名流程
|
|
||||||
isEnrolled.value = false // false=未报名状态,true=已报名状态
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
console.log('课程详情页加载完成,课程ID:', courseId.value)
|
||||||
initializeMockState() // 初始化模拟状态
|
|
||||||
loadCourseDetail()
|
loadCourseDetail()
|
||||||
loadCourseSections()
|
loadCourseSections()
|
||||||
|
|
||||||
@ -2434,8 +2355,10 @@ onActivated(() => {
|
|||||||
.video-player-section {
|
.video-player-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
overflow: hidden;
|
overflow: visible; /* 改为visible,确保底部交互区域不被裁剪 */
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-player.unregistered {
|
.video-player.unregistered {
|
||||||
@ -2445,21 +2368,26 @@ onActivated(() => {
|
|||||||
|
|
||||||
.video-player.enrolled {
|
.video-player.enrolled {
|
||||||
background: #000;
|
background: #000;
|
||||||
min-height: 400px;
|
/* 移除固定高度,让内容自适应 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 400px;
|
height: 450px; /* 使用固定高度,确保播放器能正常工作 */
|
||||||
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ckplayer-container {
|
/* DPlayer 容器样式 */
|
||||||
width: 100%;
|
.video-container :deep(.dplayer) {
|
||||||
height: 100%;
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-placeholder {
|
.video-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
@ -2654,6 +2582,9 @@ onActivated(() => {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-top: 1px solid #e5e7eb;
|
border-top: 1px solid #e5e7eb;
|
||||||
min-height: 60px;
|
min-height: 60px;
|
||||||
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
position: relative;
|
||||||
|
z-index: 10; /* 确保在最上层 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.interaction-left {
|
.interaction-left {
|
||||||
@ -5036,7 +4967,7 @@ onActivated(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.video-player-section {
|
.video-player-section {
|
||||||
height: 400px;
|
/* 移除固定高度,让内容自适应 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,11 +425,8 @@ const goToCourseDetail = async (course: Course) => {
|
|||||||
try {
|
try {
|
||||||
// 检查用户是否已登录
|
// 检查用户是否已登录
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
console.log('用户未登录,跳转到课程详情页')
|
console.log('用户未登录,跳转到AI伴学页面')
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -442,29 +439,23 @@ const goToCourseDetail = async (course: Course) => {
|
|||||||
const isEnrolled = response.data.result
|
const isEnrolled = response.data.result
|
||||||
|
|
||||||
if (isEnrolled) {
|
if (isEnrolled) {
|
||||||
// 已报名,跳转到已报名页面
|
// 已报名,跳转到已兑换页面
|
||||||
console.log('用户已报名,跳转到已报名页面')
|
console.log('用户已报名,跳转到已兑换页面')
|
||||||
router.push(`/course/${course.id}/enrolled`)
|
router.push(`/course/${course.id}/exchanged`)
|
||||||
} else {
|
} else {
|
||||||
// 未报名,跳转到课程详情页
|
// 未报名,跳转到AI伴学页面
|
||||||
console.log('用户未报名,跳转到课程详情页')
|
console.log('用户未报名,跳转到AI伴学页面')
|
||||||
router.push(`/course/${course.id}`)
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 查询失败,默认跳转到课程详情页
|
// 查询失败,默认跳转到AI伴学页面
|
||||||
console.warn('查询报名状态失败,跳转到课程详情页')
|
console.warn('查询报名状态失败,跳转到AI伴学页面')
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查报名状态时发生错误:', error)
|
console.error('检查报名状态时发生错误:', error)
|
||||||
// 发生错误时,默认跳转到课程详情页
|
// 发生错误时,默认跳转到AI伴学页面
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,8 +563,8 @@ const goToCourseDetail = async (courseId: string) => {
|
|||||||
try {
|
try {
|
||||||
// 检查用户是否已登录
|
// 检查用户是否已登录
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
console.log('用户未登录,跳转到课程详情页')
|
console.log('用户未登录,跳转到AI伴学页面')
|
||||||
router.push(`/course/${courseId}`)
|
router.push(`/ai-companion?courseId=${courseId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -577,23 +577,23 @@ const goToCourseDetail = async (courseId: string) => {
|
|||||||
const isEnrolled = response.data.result
|
const isEnrolled = response.data.result
|
||||||
|
|
||||||
if (isEnrolled) {
|
if (isEnrolled) {
|
||||||
// 已报名,跳转到已报名页面
|
// 已报名,跳转到已兑换页面
|
||||||
console.log('用户已报名,跳转到已报名页面')
|
console.log('用户已报名,跳转到已兑换页面')
|
||||||
router.push(`/course/${courseId}/enrolled`)
|
router.push(`/course/${courseId}/exchanged`)
|
||||||
} else {
|
} else {
|
||||||
// 未报名,跳转到课程详情页
|
// 未报名,跳转到AI伴学页面
|
||||||
console.log('用户未报名,跳转到课程详情页')
|
console.log('用户未报名,跳转到AI伴学页面')
|
||||||
router.push(`/course/${courseId}`)
|
router.push(`/ai-companion?courseId=${courseId}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 查询失败,默认跳转到课程详情页
|
// 查询失败,默认跳转到AI伴学页面
|
||||||
console.warn('查询报名状态失败,跳转到课程详情页')
|
console.warn('查询报名状态失败,跳转到AI伴学页面')
|
||||||
router.push(`/course/${courseId}`)
|
router.push(`/ai-companion?courseId=${courseId}`)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查报名状态时发生错误:', error)
|
console.error('检查报名状态时发生错误:', error)
|
||||||
// 发生错误时,默认跳转到课程详情页
|
// 发生错误时,默认跳转到AI伴学页面
|
||||||
router.push(`/course/${courseId}`)
|
router.push(`/ai-companion?courseId=${courseId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -563,11 +563,8 @@ const goToCourseDetail = async (course: Course) => {
|
|||||||
try {
|
try {
|
||||||
// 检查用户是否已登录
|
// 检查用户是否已登录
|
||||||
if (!userStore.isLoggedIn) {
|
if (!userStore.isLoggedIn) {
|
||||||
console.log('用户未登录,跳转到课程详情页')
|
console.log('用户未登录,跳转到AI伴学页面')
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -580,29 +577,23 @@ const goToCourseDetail = async (course: Course) => {
|
|||||||
const isEnrolled = response.data.result
|
const isEnrolled = response.data.result
|
||||||
|
|
||||||
if (isEnrolled) {
|
if (isEnrolled) {
|
||||||
// 已报名,跳转到已报名页面
|
// 已报名,跳转到已兑换页面
|
||||||
console.log('用户已报名,跳转到已报名页面')
|
console.log('用户已报名,跳转到已兑换页面')
|
||||||
router.push(`/course/${course.id}/enrolled`)
|
router.push(`/course/${course.id}/exchanged`)
|
||||||
} else {
|
} else {
|
||||||
// 未报名,跳转到课程详情页
|
// 未报名,跳转到AI伴学页面
|
||||||
console.log('用户未报名,跳转到课程详情页')
|
console.log('用户未报名,跳转到AI伴学页面')
|
||||||
router.push(`/course/${course.id}`)
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 查询失败,默认跳转到课程详情页
|
// 查询失败,默认跳转到AI伴学页面
|
||||||
console.warn('查询报名状态失败,跳转到课程详情页')
|
console.warn('查询报名状态失败,跳转到AI伴学页面')
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查报名状态时发生错误:', error)
|
console.error('检查报名状态时发生错误:', error)
|
||||||
// 发生错误时,默认跳转到课程详情页
|
// 发生错误时,默认跳转到AI伴学页面
|
||||||
router.push({
|
router.push(`/ai-companion?courseId=${course.id}`)
|
||||||
name: 'CourseDetail',
|
|
||||||
params: { id: course.id }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user