更新:课程详情的初步代码

This commit is contained in:
2025-11-14 15:13:21 +08:00
parent e7e0597026
commit 21b03635a2
25 changed files with 4958 additions and 12 deletions

View File

@@ -0,0 +1,466 @@
<template>
<view class="chapter-detail-page">
<!-- 自定义导航栏 -->
<nav-bar :title="$t('courseDetails.chapter')" />
<!-- 页面内容 -->
<view class="page-content" :style="{ height: contentHeight }">
<!-- 视频播放器 -->
<view v-if="videoList.length > 0" class="video-section">
<VideoPlayer
ref="videoPlayerRef"
:videoList="videoList"
:currentIndex="currentVideoIndex"
:noRecored="noRecored"
@end="handleVideoEnd"
@fullscreen="handleFullscreen"
@change="handleVideoChange"
/>
</view>
<!-- 课程和章节信息 -->
<view class="info-section">
<view class="info-item">
<text class="label">{{ $t('courseDetails.courseInfo') }}</text>
<text class="value">{{ navTitle }}</text>
</view>
<view class="info-item">
<text class="label">{{ $t('courseDetails.chapterInfo') }}</text>
<text class="value">{{ chapterTitle }}</text>
</view>
</view>
<!-- 视频列表 -->
<view v-if="videoList.length > 0" class="video-list-section">
<view class="section-title">{{ $t('courseDetails.videoTeaching') }}</view>
<view class="video-list">
<view
v-for="(video, index) in videoList"
:key="video.id"
:class="['video-item', currentVideoIndex === index ? 'active' : '']"
@click="selectVideo(index)"
>
<view class="video-info">
<text class="video-title">{{ video.type == "2" ? "音频" : "视频" }}{{ index + 1 }}</text>
<!-- <wd-icon
v-if="currentVideoIndex === index"
name="play-circle"
color="#258feb"
size="14px"
/> -->
</view>
</view>
</view>
</view>
<!-- 选项卡 -->
<view v-if="tabList.length > 0" class="tabs-section">
<view class="tabs">
<view
v-for="(tab, index) in tabList"
:key="tab.id"
:class="['tab-item', currentTab === index ? 'active' : '']"
@click="switchTab(index)"
>
<text>{{ tab.name }}</text>
</view>
</view>
</view>
<!-- 选项卡内容 -->
<view class="tab-content">
<!-- 章节介绍 -->
<view v-show="currentTab === 0" class="intro-content">
<view class="section-title">{{ $t('courseDetails.chapterIntro') }}</view>
<view class="intro-wrapper">
<!-- 章节封面 -->
<image
v-if="chapterDetail?.imgUrl"
:src="chapterDetail.imgUrl"
mode="widthFix"
class="chapter-image"
@click="previewImage(chapterDetail.imgUrl)"
/>
<!-- 章节内容 -->
<view v-if="chapterDetail?.content" class="chapter-content" v-html="chapterDetail.content"></view>
</view>
<view class="copyright">
<text>{{ $t('courseDetails.copyright') }}</text>
</view>
</view>
<!-- 思考题 -->
<view v-show="currentTab === 1" class="question-content">
<view class="section-title">{{ $t('courseDetails.thinkingQuestion') }}</view>
<view v-if="chapterDetail?.questions" class="question-wrapper">
<view class="question-html" v-html="chapterDetail.questions"></view>
</view>
<view v-else class="no-question">
<wd-divider>{{ $t('courseDetails.noQuestion') }}</wd-divider>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { onLoad, onShow, onHide, onUnload } from '@dcloudio/uni-app'
import { courseApi } from '@/api/modules/course'
import VideoPlayer from '@/components/course/VideoPlayer.vue'
import NavBar from '@/components/nav-bar/nav-bar.vue'
import type { IChapterDetail, IVideo } from '@/types/course'
// 页面参数
const chapterId = ref<number>(0)
const courseId = ref<number>(0)
const navTitle = ref('')
const chapterTitle = ref('')
const noRecored = ref(false)
// 页面数据
const chapterDetail = ref<IChapterDetail | null>(null)
const videoList = ref<IVideo[]>([])
const currentVideoIndex = ref(0)
const currentTab = ref(0)
const isFullScreen = ref(false)
// 视频播放器引用
const videoPlayerRef = ref<any>(null)
// 选项卡列表
const tabList = computed(() => {
const tabs = [
{ id: '0', name: '章节介绍' }
]
// 如果有思考题,添加思考题选项卡
if (chapterDetail.value?.questions) {
tabs.push({ id: '1', name: '思考题' })
}
return tabs
})
// 内容高度(全屏时调整)
const contentHeight = computed(() => {
return isFullScreen.value ? '100vh' : 'auto'
})
/**
* 页面加载
*/
onLoad((options: any) => {
chapterId.value = parseInt(options.id)
courseId.value = parseInt(options.courseId)
navTitle.value = options.navTitle || ''
chapterTitle.value = options.title || ''
noRecored.value = options.noRecored === 'true'
loadChapterDetail()
})
/**
* 加载章节详情
*/
const loadChapterDetail = async () => {
try {
uni.showLoading({ title: '加载中...' })
const res = await courseApi.getChapterDetail(chapterId.value)
if (res.code === 0 && res.data) {
chapterDetail.value = res.data.detail
videoList.value = res.data.videos || []
// 如果有历史播放记录,定位到对应视频
if (res.data.current) {
const index = videoList.value.findIndex(v => v.id === res.data.current)
if (index !== -1) {
currentVideoIndex.value = index
}
}
}
} catch (error) {
console.error('加载章节详情失败:', error)
} finally {
uni.hideLoading()
}
}
/**
* 选择视频
*/
const selectVideo = (index: number) => {
if (index === currentVideoIndex.value) return
currentVideoIndex.value = index
}
/**
* 视频切换
*/
const handleVideoChange = (index: number) => {
currentVideoIndex.value = index
}
/**
* 视频播放结束
*/
const handleVideoEnd = () => {
// 视频播放结束的处理已在 VideoPlayer 组件中完成
}
/**
* 全屏变化
*/
const handleFullscreen = (fullscreen: boolean) => {
isFullScreen.value = fullscreen
// 全屏时锁定屏幕方向
// #ifdef APP-PLUS
if (fullscreen) {
plus.screen.unlockOrientation()
plus.screen.lockOrientation('landscape-primary')
} else {
plus.screen.unlockOrientation()
plus.screen.lockOrientation('portrait-primary')
}
// #endif
}
/**
* 切换选项卡
*/
const switchTab = (index: number) => {
currentTab.value = index
}
/**
* 预览图片
*/
const previewImage = (url: string) => {
uni.previewImage({
urls: [url],
current: url
})
}
/**
* 页面显示
*/
onShow(() => {
// 锁定竖屏
// #ifdef APP-PLUS
plus.screen.unlockOrientation()
plus.screen.lockOrientation('portrait-primary')
// #endif
})
/**
* 页面隐藏
*/
onHide(() => {
// 暂停视频
if (videoPlayerRef.value) {
videoPlayerRef.value.pause()
}
})
/**
* 页面卸载
*/
onUnload(() => {
// 停止视频
if (videoPlayerRef.value) {
videoPlayerRef.value.stop()
}
// 解锁屏幕方向
// #ifdef APP-PLUS
plus.screen.unlockOrientation()
// #endif
})
</script>
<style lang="scss" scoped>
.chapter-detail-page {
min-height: 100vh;
background-color: #f5f5f5;
}
.page-content {
padding-bottom: 100rpx;
}
.video-section {
background-color: #000;
}
.info-section {
padding: 20rpx;
background-color: #fff;
.info-item {
margin-bottom: 15rpx;
font-size: 26rpx;
color: #666;
&:last-child {
margin-bottom: 0;
}
.label {
color: #999;
}
.value {
color: #333;
}
}
}
.video-list-section {
padding: 20rpx;
background-color: #fff;
margin-top: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: 500;
color: #2979ff;
margin-bottom: 20rpx;
}
.video-list {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: 10rpx;
.video-item {
padding: 20rpx;
margin-bottom: 10rpx;
background-color: #f7f8f9;
border-radius: 8rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
&.active {
background-color: #e8f4ff;
border-color: #258feb;
}
.video-info {
display: flex;
justify-content: space-between;
align-items: center;
.video-title {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
}
}
}
.tabs-section {
background-color: #fff;
margin-top: 20rpx;
border-bottom: 2rpx solid #2979ff;
.tabs {
display: flex;
.tab-item {
flex: 1;
text-align: center;
padding: 25rpx 0;
font-size: 30rpx;
color: #666;
position: relative;
transition: all 0.3s;
&.active {
color: #2979ff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #2979ff;
border-radius: 2rpx;
}
}
}
}
}
.tab-content {
background-color: #fff;
padding: 20rpx;
.section-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.intro-content {
.intro-wrapper {
.chapter-image {
width: 100%;
display: block;
margin-bottom: 20rpx;
border-radius: 8rpx;
}
.chapter-content {
font-size: 28rpx;
line-height: 1.8;
color: #666;
text-align: justify;
word-break: break-all;
}
}
.copyright {
margin-top: 40rpx;
padding-top: 20rpx;
border-top: 1px solid #f0f0f0;
text-align: center;
text {
font-size: 24rpx;
color: #ff4444;
}
}
}
.question-content {
.question-wrapper {
.question-html {
font-size: 28rpx;
line-height: 1.8;
color: #666;
word-break: break-all;
}
}
.no-question {
padding: 80rpx 0;
text-align: center;
}
}
}
</style>