| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- <template>
- <!-- ======================================== -->
- <!-- 手机端:完整布局 -->
- <!-- ======================================== -->
- <div class="mobile-container" :class="{ 'fullscreen-active': isFullscreen }">
- <!-- 手机端:顶部导航栏 - 悬浮在视频上方 -->
- <div class="mobile-header" :class="{ 'header-hidden': headerHidden }">
- <button class="mobile-back-btn" @click="goBack">
- <el-icon><ArrowLeftBold /></el-icon>
- </button>
- <span class="mobile-header-title">{{ courseInfo.title }}</span>
- <div class="mobile-header-placeholder"></div>
- </div>
- <!-- 手机端:视频播放区域 -->
- <div class="mobile-video-area">
- <!-- 手机端视频播放器 -->
- <VideoPlayer
- v-if="currentCourse && currentCourse.courseContentType === 'video'"
- :key="'mobile-' + (currentCourse?.id || currentChapterId)"
- :contentType="currentCourse.courseContentType"
- :videoPath="currentCourse.courseVideoPath"
- :courseId="currentCourse.id || ''"
- :typeId="typeId"
- :courseConfigList="currentCourse.courseConfigList || []"
- :allIndices="chapterIds"
- :currentIndex="currentChapterId"
- :isFullscreen="isFullscreen"
- @switchVideo="handleSelect"
- @saveProgress="handleSaveProgress"
- @videoPlay="onVideoPlay"
- @videoPause="onVideoPause"
- @fullscreenChange="handleFullscreenChange"
- />
- </div>
- <!-- 手机端:搜索框 -->
- <div class="mobile-search-container">
- <el-input
- v-model="searchKeyword"
- placeholder="搜索课程内容..."
- class="mobile-search-input"
- clearable
- @keyup.enter="handleSearchSubmit"
- >
- <template #prefix>
- <el-icon><Search /></el-icon>
- </template>
- </el-input>
- </div>
- <!-- 手机端:章节列表 -->
- <div class="mobile-chapter-list">
- <div
- v-for="(chapter, index) in filteredTitles"
- :key="chapter.id"
- class="mobile-chapter-item"
- :class="{ active: currentChapterId === chapter.id }"
- @click="playChapter(chapter)"
- >
- <div class="mobile-chapter-info">
- <div class="mobile-chapter-title-row">
- <span class="mobile-chapter-number">{{ String(index + 1).padStart(2, '0') }}.</span>
- <span class="mobile-chapter-title">{{ videoPathMap[chapter.id]?.courseName || '' }}</span>
- </div>
- <div class="mobile-chapter-meta">
- <span class="mobile-chapter-type">视频时长</span>
- <span class="mobile-chapter-divider">|</span>
- <span class="mobile-chapter-duration">{{ formatDuration(videoPathMap[chapter.id]?.courseTime || 0) }}</span>
- </div>
- </div>
- <div class="mobile-chapter-play-icon">
- <el-icon
- v-if="currentChapterId === chapter.id && isPlaying"
- class="playing-icon"
- >
- <div class="audio-wave">
- <div class="wave-bar"></div>
- <div class="wave-bar"></div>
- <div class="wave-bar"></div>
- </div>
- </el-icon>
- <el-icon
- v-else
- class="play-icon"
- >
- <CaretRight />
- </el-icon>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed, onMounted, onUnmounted } from 'vue'
- import { useRouter, useRoute } from 'vue-router'
- import {
- Search,
- ArrowLeftBold,
- CaretRight
- } from '@element-plus/icons-vue'
- import VideoPlayer from '@/components/videopage/VideoPlayer.vue'
- import classImages from '@/assets/icon/class.png'
- import { ClassType } from '@/api/class.js'
- import { Message } from '@/utils/message/Message.js'
- import { saveRecord } from '@/api/personalized/index.js'
- import { globalState } from '@/utils/globalState.js'
- const router = useRouter()
- const route = useRoute()
- const searchKeyword = ref('')
- const currentChapterId = ref('')
- const isPlaying = ref(false)
- // 强制设为移动端,不再检测设备类型
- const isMobile = ref(true)
- const headerHidden = ref(false)
- const courseInfo = ref({
- title: '',
- subtitle: '',
- coverImage: '',
- progress: 0
- })
- // 跟踪是否处于全屏状态
- const isFullscreen = ref(false)
- const chapters = ref([])
- const videoPathMap = ref({})
- const gradeId = ref('')
- const typeId = ref('')
- const currentCourse = ref({})
- const chapterIds = computed(() => {
- return chapters.value.map(c => c.id)
- })
- const filteredTitles = computed(() => {
- if (!searchKeyword.value) {
- return chapters.value
- }
- return chapters.value.filter(chapter =>
- chapter.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
- )
- })
- const querySearch = (queryString, cb) => {
- const results = queryString
- ? chapters.value.filter(item => item.title.toLowerCase().includes(queryString.toLowerCase()))
- : chapters.value
- cb(results)
- }
- const handleSaveProgress = async (type, progress) => {
- if (!progress) return
- const saveProgressData = {
- brpNjId: gradeId.value,
- brpType: type,
- brpProgress: progress,
- brpCtId: typeId.value,
- brpCourseId: currentCourse.value.id
- }
- try {
- await saveRecord(saveProgressData)
- } catch (error) {
- console.error('保存视频进度失败:', error)
- }
- }
- const goBack = () => {
- router.go(-1)
- }
- const handleSelect = (index) => {
- if (videoPathMap.value[index]) {
- currentCourse.value = videoPathMap.value[index]
- currentChapterId.value = index
- } else {
- Message().notifyWarning('视频不存在!', true)
- }
- }
- const handleSearchSelect = (item) => {
- handleSelect(item.id)
- searchKeyword.value = ''
- }
- const handleSearchSubmit = () => {
- if (searchKeyword.value.trim()) {
- // 可以在这里实现具体的搜索逻辑
- console.log('搜索关键词:', searchKeyword.value)
- // 例如:可以触发课程内容搜索、跳转到搜索结果页等
- Message().success(`搜索: ${searchKeyword.value}`, true)
- }
- }
- const playPreviousVideo = () => {
- const currentIndex = chapterIds.value.indexOf(currentChapterId.value)
- if (currentIndex > 0) {
- handleSelect(chapterIds.value[currentIndex - 1])
- }
- }
- const playNextVideo = () => {
- const currentIndex = chapterIds.value.indexOf(currentChapterId.value)
- if (currentIndex !== -1 && currentIndex < chapterIds.value.length - 1) {
- handleSelect(chapterIds.value[currentIndex + 1])
- }
- }
- const playChapter = (chapter) => {
- handleSelect(chapter.id)
- isPlaying.value = true
- }
- const togglePlay = () => {
- isPlaying.value = !isPlaying.value
- }
- const onVideoPlay = () => {
- isPlaying.value = true
- // 视频播放时隐藏头部导航栏
- headerHidden.value = true
- }
- const onVideoPause = () => {
- isPlaying.value = false
- // 视频暂停时显示头部导航栏
- headerHidden.value = false
- }
- const handlePageClick = () => {}
- const formatDuration = (seconds) => {
- const mins = Math.floor(seconds / 60)
- const secs = Math.floor(seconds % 60)
- return `${mins.toString().padStart(2, '0')}分${secs.toString().padStart(2, '0')}秒`
- }
- const handleFullscreenChange = (isFullscreenNow) => {
- isFullscreen.value = isFullscreenNow
- }
- // 监听设备方向变化
- const handleOrientationChange = () => {
- // 在全屏状态下,设备方向改变时强制重新调整视频容器尺寸
- if (isFullscreen.value) {
- // 使用setTimeout确保DOM更新完成后再执行
- setTimeout(() => {
- // 触发一次强制重绘
- const videoArea = document.querySelector('.mobile-video-area');
- if (videoArea) {
- videoArea.style.display = 'none';
- void videoArea.offsetWidth; // 强制重排
- videoArea.style.display = '';
- }
- }, 100);
- }
- };
- onMounted(async () => {
- // 监听设备方向变化事件
- window.addEventListener('orientationchange', handleOrientationChange);
-
- const typeIdParam = history.state?.typeId || router.currentRoute.value.query.typeId
- if (typeIdParam) {
- typeId.value = typeIdParam
- try {
- const res = await ClassType(typeIdParam)
- const processedData = res.data.map(course => {
- if (course.courseConfigList && Array.isArray(course.courseConfigList)) {
- const validConfigList = course.courseConfigList.filter(config =>
- config.ccTime !== undefined && config.ccTime !== null && config.ccTime > 0
- )
- return {
- ...course,
- courseConfigList: validConfigList
- }
- }
- return course
- })
- const videoCourses = processedData.filter(course => course.courseContentType === 'video')
- videoCourses.forEach((course, index) => {
- const menuIndex = String(index + 1).padStart(2, '0')
- course.key = menuIndex
- videoPathMap.value[menuIndex] = course
- const menuItem = {
- id: menuIndex,
- key: menuIndex,
- title: course.courseName
- }
- chapters.value.push(menuItem)
- if (index === 0) {
- currentCourse.value = course
- currentChapterId.value = menuIndex
- }
- })
- } catch (error) {
- console.error('获取课程数据失败:', error)
- }
- }
- const title = history.state?.typeName || router.currentRoute.value.query.typeName
- if (title) {
- courseInfo.value.title = String(title)
- }
- gradeId.value = globalState.initGradeId()
- })
- onUnmounted(() => {
- // 移除设备方向变化事件监听器
- window.removeEventListener('orientationchange', handleOrientationChange);
- });
- </script>
- <style scoped lang="scss">
- @use 'sass:math';
- @function rpx($px) {
- @return math.div($px, 750) * 100vw;
- }
- /* ======================================== */
- /* 手机端样式 */
- /* ======================================== */
- .mobile-container {
- display: flex;
- flex-direction: column;
- width: 100% !important;
- min-width: 100% !important;
- max-width: 100% !important;
- min-height: 100vh;
- background: #f5f5f5;
- padding: 0 !important;
- margin: 0 !important;
- box-sizing: border-box !important;
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
- overflow-y: auto !important;
- }
- .mobile-header {
- width: 100% !important;
- height: 44px;
- background: linear-gradient(135deg, #44449c80, #7F70C880); /* 半透明背景 */
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 12px;
- box-sizing: border-box;
- position: fixed; /* 固定定位,悬浮在顶部 */
- top: 0;
- left: 0;
- right: 0;
- z-index: 1000; /* 提高层级确保在最顶层 */
- transition: transform 0.3s ease; /* 添加过渡动画 */
- }
- .mobile-header.header-hidden {
- transform: translateY(-100%); /* 向上隐藏 */
- }
- .mobile-back-btn {
- width: 32px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 50%;
- border: none;
- color: white;
- font-size: 16px;
- cursor: pointer;
- flex-shrink: 0;
- }
- .mobile-header-title {
- flex: 1;
- text-align: center;
- color: white;
- font-size: 16px;
- font-weight: 500;
- padding: 0 40px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .mobile-header-placeholder {
- width: 32px;
- flex-shrink: 0;
- }
- .mobile-video-area {
- width: 100% !important;
- min-width: 100% !important;
- max-width: 100% !important;
- height: auto !important;
- background: #000;
- margin-top: 44px; /* 为固定在顶部的导航栏预留空间 */
- position: relative;
- padding: 0 !important;
- margin: 0 !important;
- box-sizing: border-box !important;
- flex-shrink: 0;
- border-radius: 0 !important;
- overflow: hidden;
- }
- /* 修复全屏模式下的视频显示问题 */
- .mobile-container.fullscreen-active .mobile-video-area {
- position: fixed !important;
- top: 0 !important;
- left: 0 !important;
- right: 0 !important;
- bottom: 0 !important;
- z-index: 9999 !important;
- margin-top: 0 !important;
- /* 使用视口单位确保在设备方向改变时适应屏幕 */
- width: 100vw !important;
- height: 100vh !important;
- min-height: 100vh !important;
- max-height: 100vh !important;
- /* 确保视频容器占据全部可用空间 */
- display: flex !important;
- align-items: center !important;
- justify-content: center !important;
- }
- .mobile-container.fullscreen-active .mobile-header {
- z-index: 10000;
- }
- .mobile-container.fullscreen-active .mobile-search-container,
- .mobile-container.fullscreen-active .mobile-chapter-list {
- display: none;
- }
- /* 手机端搜索框容器 */
- .mobile-search-container {
- width: 100%;
- padding: rpx(16);
- background: #f5f5f5;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- }
- .mobile-search-input {
- width: 100%;
- height: rpx(64);
- }
- .mobile-search-input :deep(.el-input__wrapper) {
- border-radius: rpx(32);
- width: 100%;
- height: 100%;
- background: #fff;
- border: none;
- box-shadow: none;
- }
- .mobile-search-input :deep(.el-input__inner) {
- height: 100%;
- line-height: rpx(64);
- font-size: rpx(28);
- color: #999;
- }
- /* 手机端视频播放器去圆角 */
- .mobile-video-area :deep(.d-player-wrap),
- .mobile-video-area :deep(.full-box-video),
- .mobile-video-area :deep(.ppt-container) {
- border-radius: 0 !important;
- width: 100% !important;
- height: 100% !important;
- max-width: 100% !important;
- max-height: 100% !important;
- }
- .mobile-video-area :deep(*) {
- width: 100% !important;
- height: 100% !important;
- max-width: 100% !important;
- max-height: 100% !important;
- object-fit: contain !important; /* 使用contain确保视频完全可见 */
- border-radius: 0 !important;
- }
- /* 特别处理全屏模式下的视频播放器 */
- .mobile-container.fullscreen-active .mobile-video-area :deep(.d-player-wrap),
- .mobile-container.fullscreen-active .mobile-video-area :deep(.full-box-video),
- .mobile-container.fullscreen-active .mobile-video-area :deep(.ppt-container) {
- width: 100% !important;
- height: 100% !important;
- max-width: 100% !important;
- max-height: 100% !important;
- position: relative !important;
- }
- .mobile-container.fullscreen-active .mobile-video-area :deep(video) {
- object-fit: contain !important;
- width: 100% !important;
- height: 100% !important;
- max-width: 100% !important;
- max-height: 100% !important;
- }
- .mobile-chapter-list {
- flex: 1;
- padding: 0;
- background: #fff;
- margin-top: rpx(10); /* 与视频信息区域分开 */
- }
- .mobile-chapter-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px;
- border-bottom: 1px solid #f0f0f0;
- cursor: pointer;
- transition: background-color 0.2s ease;
- background: #fff;
- }
- .mobile-chapter-item:last-child {
- border-bottom: none;
- }
- .mobile-chapter-item.active {
- background: #fff;
- }
- .mobile-chapter-item.locked {
- opacity: 0.6;
- }
- .mobile-chapter-info {
- flex: 1;
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .mobile-chapter-title-row {
- display: flex;
- align-items: flex-start;
- gap: 6px;
- }
- .mobile-chapter-number {
- font-size: 15px;
- font-weight: 600;
- color: #333;
- flex-shrink: 0;
- }
- .mobile-chapter-title {
- font-size: 15px;
- font-weight: 500;
- color: #333;
- line-height: 1.4;
- }
- .mobile-chapter-meta {
- font-size: 12px;
- color: #999;
- display: flex;
- align-items: center;
- gap: 8px;
- padding-left: 28px;
- }
- .mobile-chapter-type {
- color: #999;
- }
- .mobile-chapter-divider {
- color: #ddd;
- }
- .mobile-chapter-duration {
- color: #999;
- }
- .mobile-chapter-play-icon {
- flex-shrink: 0;
- margin-left: 12px;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .mobile-chapter-play-icon .el-icon {
- font-size: 20px;
- color: #409EFF;
- }
- .mobile-chapter-play-icon .playing-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .audio-wave {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 2px;
- height: 20px;
- }
- .wave-bar {
- width: 2px;
- height: 8px;
- background-color: #409EFF;
- border-radius: 1px;
- animation: wave-animation 1.2s ease-in-out infinite;
- }
- .wave-bar:nth-child(1) {
- animation-delay: 0s;
- }
- .wave-bar:nth-child(2) {
- animation-delay: 0.2s;
- }
- .wave-bar:nth-child(3) {
- animation-delay: 0.4s;
- }
- @keyframes wave-animation {
- 0%, 100% {
- transform: scaleY(1);
- opacity: 0.7;
- }
- 50% {
- transform: scaleY(1.8);
- opacity: 1;
- }
- }
- .mobile-chapter-action {
- width: 40px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .mobile-lock-icon {
- color: #ccc;
- font-size: 16px;
- }
- /* 处理设备方向变化时的全屏视频 */
- @media screen and (orientation: landscape) {
- .mobile-container.fullscreen-active .mobile-video-area {
- width: 100vw !important;
- height: 100vh !important;
- min-height: 100vh !important;
- max-height: 100vh !important;
- }
- }
- @media screen and (orientation: portrait) {
- .mobile-container.fullscreen-active .mobile-video-area {
- width: 100vw !important;
- height: 100vh !important;
- min-height: 100vh !important;
- max-height: 100vh !important;
- }
- }
- </style>
|