Files
super_video/src/views/basic-elements/basic-shooting-skills/index.vue
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

402 lines
11 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 { GuideSteps, GuideStepsItem } from '@/components/Guide'
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,
getStaticImage,
IMAGE_PATHS,
getVideoPoster,
} from '@/utils'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
const router = useRouter()
const basicElementsStore = useBasicElementsStore()
const introGuideKey = 'basic-shooting-skills-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) // 控制是否显示答题窗口
// 步骤导航数据 - 确保content字段不包含(一)、【Q2】等标记
const rawSteps = [
{
id: 'step2',
label: '摄影基础',
content: '手机摄像头介绍',
videoUrl: getPublicPath(
'video/new/shooting-skills/xinshou_shoujishexiangtoujieshao.mp4',
),
type: 'post' as const,
},
{
id: 'step3',
label: '摄影基础',
content: '景别概念介绍',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_jbgnjs_batch.mp4'),
type: 'post' as const,
},
{
id: 'step4',
label: '摄影基础',
content: '手机慢动作',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sjmdz_batch.mp4'),
type: 'post' as const,
},
{
id: 'step5',
label: '摄影基础',
content: '延时摄影',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_yssy_batch.mp4'),
type: 'post' as const,
},
{
id: 'step6',
label: '摄影基础',
content: '神奇的微距摄影',
videoUrl: getPublicPath(
'video/new/shooting-skills/xinshou_shenqideweijusheying.mp4',
),
type: 'post' as const,
},
{
id: 'step7',
label: '摄影基础',
content: '独具魅力的夜景摄影',
videoUrl: getPublicPath(
'video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4',
),
type: 'post' as const,
},
]
const navigationSteps = ref(rawSteps)
// 步骤导航相关状态
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-shooting-skills:${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 currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
// 题目数据:景别概念介绍(index 1)、手机慢动作(index 2)、独具魅力的夜景摄影(index 5)
const questions = ref<Question[]>([])
// 根据当前视频索引获取对应的题目
const getQuestionsForVideo = (index: number): Question[] => {
// 景别概念介绍(index 1)
if (index === 1) {
return [
{
id: 1,
title: '',
description:
'【单选题】如果你想让观众“看清环境、理解空间关系”,更适合用哪种镜头?',
options: [
{
id: 'a',
label: '面部特写视频',
},
{
id: 'b',
label: '中景人物视频',
},
{
id: 'c',
label: '全景环境视频',
},
],
correctOptionId: 'c',
explanation: '全景更适合交代故事发生在哪里。',
},
]
}
// 手机慢动作(index 2)
if (index === 2) {
return [
{
id: 2,
title: '',
description:
'【多选题】下面这些拍摄场景中,哪些更适合使用"慢动作"来表现?',
options: [
{
id: 'a',
label: '城市街道上车水马龙,车辆快速穿行',
},
{
id: 'b',
label: '甩头发、转身、跳跃等人物动作瞬间',
},
{
id: 'c',
label: '水花飞溅、雨滴落地、浪花拍岸',
},
{
id: 'd',
label: '风吹动衣摆、窗帘或树叶缓慢摆动',
},
],
isMultiple: true,
correctOptionIds: ['b', 'c', 'd'],
correctOptionId: '', // 多选题时此字段不使用
explanation:
'更适合用慢动作表现的是 B、C、D,它们都包含了"瞬间细节"和"节奏变化"。',
},
]
}
// 独具魅力的夜景摄影(index 5)
if (index === 5) {
return [
{
id: 3,
title: '',
description:
'【单选题】你觉得下面哪句话,更接近“新手拍视频的正确思路”?',
options: [
{
id: 'a',
label: '哪里好看就拍哪里',
},
{
id: 'b',
label: '设备越贵,画面越好',
},
{
id: 'c',
label: '在合适的时间,用合适的方式拍',
},
],
correctOptionId: 'c',
explanation: '拍摄要学会判断时间、光线和方式。',
},
]
}
return []
}
//按顺序播放
const handleVideoChange = () => {
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
currentVideoIndex.value++
currentNavigationStep.value = currentVideoIndex.value
} else {
status.value = true
return
}
playing.value = true
}
const handleClose = () => {
router.back()
}
const handlePlay = () => {}
const onEnded = () => {
markCurrentLearningVideoWatched()
// 在指定视频结束时弹出答题
if (
currentVideoIndex.value === 1 ||
currentVideoIndex.value === 2 ||
currentVideoIndex.value === 5
) {
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 handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
// 答题完成后的处理
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-12 h-12 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="20"
height="20"
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
v-if="currentVideo"
:src="currentVideo"
:poster="getVideoPoster(currentVideo)"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
:anchors="[]"
@play="handlePlay"
@ended="onEnded"
>
<template #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
:visible="showIntroGuide"
position-offset-y="85%"
@complete="handleIntroGuideComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_018.MP3')">
欢迎来到基础拍摄训练区!
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_019.MP3')">
我会带你认识手机摄像头、看懂景别、了解慢动作与延迟摄影。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_020.MP3')">
还会教你拍出更稳的微距和更亮的夜景。
</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
video {
width: 100%;
height: 100%;
/* 核心:保持视频比例,多余部分被裁剪 */
object-fit: cover;
}
</style>