Files
super_video/src/views/home/index.tsx
T
zhongyidianandClaude Opus 4.7 3e258b87d9 feat: 完善课程学习与技能树体验
完善课程资源、学习进度与技能树解锁体验,统一视频播放与引导相关交互。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:30:18 +08:00

247 lines
6.8 KiB
TypeScript

import { computed, defineComponent, onUnmounted, ref } from 'vue'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { CourseGuidePopup, CourseFrameworkPopup, GuideSteps, Video } from '@/components'
import Loading from './components/Loading.vue'
import { useScale } from '@/composables/useScale'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
const CourseFrameGuide = () => (
<div class="flex items-center justify-start">
点击
<img
class="w-40"
src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)}
alt=""
/>
查看课程结构,快速了解每个模块的学习内容。
</div>
)
const OperateGuide = () => (
<div class="flex items-center justify-start">
<img
class="w-40 inline"
src={getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE)}
alt=""
/>
包含课程介绍和操作指南,点击即可查看!
</div>
)
const StartFilmingGuide = () => (
<div class="flex items-center justify-start">
点击
<img
class="w-40"
src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)}
alt=""
/>
,一起我们的短视频学习之旅吧!
</div>
)
export default defineComponent({
name: 'HomeView',
setup() {
const guideSteps = ref([
{
id: 'step-1',
title: '',
content: '欢迎来到《短视频达人养成记》网络课程的学习!',
highlightSelector: null,
audioSrc: getPublicPath('audio/py_0_001.MP3'),
positionOffsetY: '80%',
},
{
id: 'step-2',
title: '',
content: CourseFrameGuide,
highlightSelector: '.kecheng-wrapper',
audioSrc: getPublicPath('audio/py_0_002.MP3'),
positionOffsetY: '40%',
},
{
id: 'step-3',
title: '',
content: OperateGuide,
highlightSelector: '.zhiying-wrapper',
audioSrc: getPublicPath('audio/py_0_003.MP3'),
positionOffsetY: '40%',
},
{
id: 'step-4',
title: '',
content: StartFilmingGuide,
highlightSelector: '.start-filming-wrapper',
audioSrc: getPublicPath('audio/py_0_004.MP3'),
positionOffsetY: '40%',
},
])
const basicElementsStore = useBasicElementsStore()
const guideKey = 'home-intro'
const isFirstVisit = !basicElementsStore.guideShown[guideKey]
const showGuide = ref(isFirstVisit)
const { scale, offsetX, offsetY } = useScale()
// 首次进入的视频播放序列
const introVideos = computed(() => [getPublicPath('video/start.mp4')])
const introVideoIndex = ref(0)
const showIntroVideo = ref(isFirstVisit)
const currentIntroSrc = computed(
() => introVideos.value[introVideoIndex.value] ?? '',
)
if (isFirstVisit) {
basicElementsStore.markGuideShown(guideKey)
}
const handleComplete = () => {
showGuide.value = false
}
const handleIntroVideoEnded = () => {
if (introVideoIndex.value < introVideos.value.length - 1) {
introVideoIndex.value++
} else {
showIntroVideo.value = false
// 引导步骤在视频播放结束后出现
}
}
// Loading 加载状态
const showLoading = ref(isFirstVisit)
const loadingProgress = ref(0)
let progressTimer: ReturnType<typeof setInterval> | null = null
let loadingFallbackTimer: ReturnType<typeof setTimeout> | null = null
if (isFirstVisit) {
progressTimer = setInterval(() => {
if (loadingProgress.value < 99) {
loadingProgress.value += 1
}
}, 60)
loadingFallbackTimer = setTimeout(() => {
finishLoading()
}, 8000)
}
onUnmounted(() => {
if (progressTimer) clearInterval(progressTimer)
if (loadingFallbackTimer) clearTimeout(loadingFallbackTimer)
})
const finishLoading = () => {
if (!showLoading.value) return
if (progressTimer) {
clearInterval(progressTimer)
progressTimer = null
}
if (loadingFallbackTimer) {
clearTimeout(loadingFallbackTimer)
loadingFallbackTimer = null
}
loadingProgress.value = 100
setTimeout(() => {
showLoading.value = false
}, 200)
}
const handleIntroVideoPlay = () => {
finishLoading()
}
const handleIntroVideoLoaded = () => {
finishLoading()
}
const openMindMap = () => {
CourseFrameworkPopup({
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
const openCourseGuide = () => {
CourseGuidePopup({
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
return () => (
<div class="w-full h-full relative">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: getBackgroundImage(IMAGE_PATHS.HOME_BG) }}
/>
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton
image="home/action-btn"
path={{ name: 'Action' }}
/>
</div>
<div class="absolute right-20 bottom-10 space-y-3">
<ImgButton
class={['kecheng-wrapper']}
image={IMAGE_PATHS.BUTTONS.COURSE_FRAME}
onClick={openMindMap}
/>
<ImgButton
class={['zhiying-wrapper']}
image={IMAGE_PATHS.BUTTONS.OPERATE_GUIDE}
onClick={openCourseGuide}
/>
</div>
{showIntroVideo.value && (
<div class="absolute inset-0 z-[10000] bg-black flex items-center justify-center">
<Loading
visible={showLoading.value}
progress={loadingProgress.value}
/>
<Video
src={currentIntroSrc.value}
autoplay={true}
muted={false}
controls={true}
volume={1}
fit="contain"
onPlay={handleIntroVideoPlay}
// onLoadedmetadata={handleIntroVideoLoaded}
// onLoadeddata={handleIntroVideoLoaded}
// onCanplay={handleIntroVideoLoaded}
onCanplaythrough={handleIntroVideoLoaded}
onPlayfailed={finishLoading}
onEnded={handleIntroVideoEnded}
/>
</div>
)}
<GuideSteps
visible={showGuide.value && !showIntroVideo.value}
steps={guideSteps.value}
highlightPadding={12}
highlightRadius={16}
keyboardNavigation={true}
highlightMode="border"
onComplete={handleComplete}
/>
</div>
)
},
})