Files
super_video/src/views/basic-elements/light/index.vue
T
2026-09-21 10:30:13 +08:00

352 lines
9.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue'
import { onBeforeRouteLeave, useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import UnfinishedLearningPrompt from '@/components/UnfinishedLearningPrompt'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getPublicPath, IMAGE_PATHS, getVideoPoster } from '@/utils'
import { GuideSteps } from '@/components/Guide'
import GuideStepsItem from '@/components/Guide/GuideStepsItem.vue'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
const router = useRouter()
const basicElementsStore = useBasicElementsStore()
const introGuideKey = 'basic-light-intro'
const showIntroGuide = ref(!basicElementsStore.guideShown[introGuideKey])
if (showIntroGuide.value) {
basicElementsStore.markGuideShown(introGuideKey)
}
// 受控播放开关(用于切换到下一个视频时自动播放)
const playing = ref(false)
const status = ref(false)
const showQuiz = ref(false) // 控制是否显示答题窗口
const isGuide = showIntroGuide
const currentGuideStep = ref(0)
const showGoldenHourGuide = ref(false)
const goldenHourGuideShown = ref(false)
const handleSkip = (index: number) => {
currentGuideStep.value = index
}
const handleComplete = () => {
isGuide.value = false
nextTick(() => {
playing.value = true
})
}
const handleGoldenHourGuideComplete = () => {
showGoldenHourGuide.value = false
goldenHourGuideShown.value = true
nextTick(() => {
playing.value = true
})
}
// 步骤导航数据
const navigationSteps = ref([
{
id: 'step2',
label: '视觉素养',
content: '光的基础认知',
videoUrl: getPublicPath('video/new/light/xinshou_guangdejichurenzhi.mp4'),
type: 'planning' as const,
},
{
id: 'step3',
label: '视觉素养',
content: '光的四大属性',
videoUrl: getPublicPath('video/new/light/xinshou_guangdesidashuxing.mp4'),
type: 'planning' as const,
},
{
id: 'step4',
label: '视觉素养',
content: '拍摄的黄金时间',
videoUrl: getPublicPath('video/new/light/xsdh_psdhjsj_batch.mp4'),
type: 'planning' as const,
},
])
// 步骤导航相关状态
const showStepNavigation = ref(false)
const showUnfinishedPrompt = ref(false)
const pendingLeavePath = ref<string | null>(null)
const allowRouteLeave = ref(false)
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
// 视频列表
const currentVideoIndex = ref(0)
const getLearningVideoKey = (index: number) =>
`basic-light:${navigationSteps.value[index]?.id ?? index}`
const completedNavigationSteps = computed(() =>
navigationSteps.value.map(
(_, index) =>
basicElementsStore.learningVideosWatched[getLearningVideoKey(index)] ===
true,
),
)
const hasUnfinishedLearningVideos = computed(() =>
completedNavigationSteps.value.some((completed) => !completed),
)
const markCurrentLearningVideoWatched = () => {
basicElementsStore.markLearningVideoWatched(
getLearningVideoKey(currentVideoIndex.value),
)
}
const currentVideoWatched = computed(
() =>
basicElementsStore.learningVideosWatched[
getLearningVideoKey(currentVideoIndex.value)
] === true,
)
//按顺序播放
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
// 题目数据:拍摄的黄金时间(index 2)
const questions = ref<Question[]>([])
// 根据当前视频索引获取对应的题目
const getQuestionsForVideo = (index: number): Question[] => {
// 拍摄的黄金时间(index 2)
if (index === 2) {
return [
{
id: 1,
title: '',
description:
'【单选题】同一条街道,在不同时间拍摄,哪一张画面更适合表现"温暖、舒服的日常感"?',
options: [
{
id: 'a',
label: '正午直射光,阴影很硬,路面反光明显',
},
{
id: 'b',
label: '傍晚低角度阳光,墙面被染成金色',
},
{
id: 'c',
label: '夜晚路灯照明,光源复杂、色温混杂',
},
],
correctOptionId: 'b',
explanation: '光线角度越低,画面柔和,情绪放大',
},
]
}
return []
}
//按顺序播放
const handleVideoChange = () => {
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
currentVideoIndex.value++
currentNavigationStep.value = currentVideoIndex.value
} else {
status.value = true
return
}
// 切到“拍摄的黄金时间”(index 2)时,先暂停并展示引导对话
if (currentVideoIndex.value === 2 && !goldenHourGuideShown.value) {
playing.value = false
showGoldenHourGuide.value = true
return
}
playing.value = true
}
const handleClose = () => {
router.back()
}
const handlePlay = () => {
isGuide.value = false
}
const onEnded = () => {
markCurrentLearningVideoWatched()
// 检查是否是"拍摄的黄金时间"(index 2)
if (currentVideoIndex.value === 2) {
// 暂停播放并显示答题窗口
playing.value = false
questions.value = getQuestionsForVideo(currentVideoIndex.value)
showQuiz.value = true
} else {
// 其他视频正常切换
handleVideoChange()
}
}
// 步骤导航事件处理
const handleStepClick = (index: number) => {
// 关闭答题窗口(如果打开)
showQuiz.value = false
currentVideoIndex.value = index
currentNavigationStep.value = index
playing.value = true
}
const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
// 答题完成后的处理
const handleQuestionsComplete = () => {
showQuiz.value = false
// 答题完成后继续播放下一个视频
handleVideoChange()
}
const handleContinueLearning = () => {
showUnfinishedPrompt.value = false
pendingLeavePath.value = null
}
const handleConfirmLeave = () => {
if (!pendingLeavePath.value) return
allowRouteLeave.value = true
showUnfinishedPrompt.value = false
router.push(pendingLeavePath.value)
}
onBeforeRouteLeave((to) => {
if (allowRouteLeave.value || !hasUnfinishedLearningVideos.value) return true
pendingLeavePath.value = to.fullPath
showUnfinishedPrompt.value = true
return false
})
</script>
<template>
<div class="w-full h-full relative bg-white/10">
<ImgButton
class="absolute right-0 top-0 z-10"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<!-- 步骤导航按钮 -->
<button
class="absolute right-4 top-1/2 z-10 -translate-y-1/2 w-16 h-16 bg-blue-500/80 hover:bg-blue-500 rounded-full flex items-center justify-center text-white transition-all duration-200"
@click="showStepNavigation = true"
title="步骤导航"
>
<svg
width="35"
height="35"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M3 4H17M3 8H17M3 12H17M3 16H17"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
<Video
:src="currentVideo"
:poster="getVideoPoster(currentVideo)"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
:anchors="[]"
:seekable="currentVideoWatched"
@play="handlePlay"
@ended="onEnded"
>
<template v-slot:overlay>
<ImgButton
class="absolute right-0 top-0 z-10 pointer-events-auto"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<QuestionQuiz
v-if="showQuiz"
:show-question-number="false"
:questions="questions"
@complete="handleQuestionsComplete"
/>
</template>
</Video>
<!-- 步骤导航抽屉 -->
<StepNavigation
:visible="showStepNavigation"
:steps="navigationSteps"
:current-step-index="currentNavigationStep"
:completed-steps="completedNavigationSteps"
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<UnfinishedLearningPrompt
:visible="showUnfinishedPrompt"
@continue="handleContinueLearning"
@leave="handleConfirmLeave"
/>
<!-- 引导与高亮 -->
<GuideSteps
v-if="isGuide"
position-offset-y="85%"
@complete="handleComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_009.MP3')">
光是摄影的灵魂,摄影高手一定是用光的高手。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_010.MP3')">
现在, 让我们一起去认识光、读懂光。
</GuideStepsItem>
</GuideSteps>
<GuideSteps
v-if="showGoldenHourGuide"
position-offset-y="85%"
@complete="handleGoldenHourGuideComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_011.MP3')">
同一个地点,不同季节拍出来的感觉为什么完全不一样?
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_012.MP3')">
同一条街,早上、下午、傍晚各拍一张,你会发现世界仿佛在悄悄换装。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_013.MP3')">
接下来,我们一起揭开四季与时间的秘密,找一找一年365天中最容易出片的时刻。
</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
video {
width: 100%;
height: 100%;
/* 核心:保持视频比例,多余部分被裁剪 */
object-fit: cover;
}
</style>