forked from zhongyidian/super_video
完善课程资源、学习进度与技能树解锁体验,统一视频播放与引导相关交互。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
247 lines
6.8 KiB
TypeScript
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>
|
|
)
|
|
},
|
|
})
|