feat: 完善视频学习进度缓存
完善新手导航和三大创作模块的视频完成缓存、未完成提示与学习进度计算,确保进度按实际学习资源完成情况更新。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1f8723aabd
commit
6f289959be
63 files changed
+3139
-1126
No files matched your search
@@ -1,10 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { onBeforeRouteLeave, useRouter, useRoute } from 'vue-router'
|
||||
import ImgButton from '@/components/Button/ImgButton.vue'
|
||||
import { GuideSteps, type GuideStep } 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 } from '@/utils'
|
||||
import { useInterviewStore } from '@/stores'
|
||||
|
||||
@@ -16,6 +20,84 @@ const interview = useInterviewStore()
|
||||
// 受控播放开关(用于切换到下一个视频时自动播放)
|
||||
const playing = ref(false)
|
||||
const status = ref(false)
|
||||
const showQuiz = ref(false) // 控制是否显示答题窗口
|
||||
|
||||
// 题目数据
|
||||
const questions = ref<Question[]>([])
|
||||
|
||||
// 根据当前视频索引获取对应的题目
|
||||
const getQuestionsForVideo = (index: number): Question[] => {
|
||||
// 机位设置(index 0)
|
||||
if (index === 0) {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
title: '',
|
||||
description:
|
||||
'【单选题】关于访谈拍摄中正面机位的中心对称构图,下列说法错误的是?',
|
||||
options: [
|
||||
{
|
||||
id: 'a',
|
||||
label: '中心对称构图能让画面更平稳,传递平等交流的访谈氛围',
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
label: '拍摄时应让人物居中,头顶预留 3–4 指宽度,避免切头',
|
||||
},
|
||||
{ id: 'c', label: '为突出人物,应让人物紧贴背景,减少画面留白' },
|
||||
{ id: 'd', label: '建议使用中长焦段拍摄,避免广角镜头的边缘畸变' },
|
||||
],
|
||||
correctOptionId: 'c',
|
||||
explanation:
|
||||
'中心对称构图拍摄时,需要让人物与背景保留一定距离,让画面更通透,更好地突出主体;紧贴背景会压缩画面空间,影响视觉效果,因此 C 选项错误。',
|
||||
},
|
||||
]
|
||||
}
|
||||
// 轴线原则(index 1)
|
||||
if (index === 1) {
|
||||
return [
|
||||
{
|
||||
id: 2,
|
||||
title: '',
|
||||
description: '【多选题】关于影视拍摄中的轴线原则,下列说法正确的有?',
|
||||
options: [
|
||||
{ id: 'a', label: '轴线是拍摄对象的视线、运动或关系形成的假想线' },
|
||||
{
|
||||
id: 'b',
|
||||
label: '拍摄对话场景时,机位通常需保持在轴线同侧 180 度以内',
|
||||
},
|
||||
{ id: 'c', label: '越轴会导致画面空间关系混乱,出现视线错位等问题' },
|
||||
{ id: 'd', label: '骑轴机位(轴线之上)不属于合规的拍摄机位' },
|
||||
],
|
||||
correctOptionId: '',
|
||||
correctOptionIds: ['a', 'b', 'c'],
|
||||
isMultiple: true,
|
||||
explanation:
|
||||
'轴线原则中,机位可保持在轴线同侧 180 度以内,也包含轴线之上的骑轴机位,因此 D 选项错误;A、B、C 均为轴线原则的核心内容。',
|
||||
},
|
||||
]
|
||||
}
|
||||
// 声音同步与收录(index 3)
|
||||
if (index === 3) {
|
||||
return [
|
||||
{
|
||||
id: 3,
|
||||
title: '',
|
||||
description: '【单选题】关于访谈拍摄的声音收录,下列做法不规范的是?',
|
||||
options: [
|
||||
{ id: 'a', label: '领夹麦夹在衣领,收音头朝向嘴部,距离 15–20 厘米' },
|
||||
{ id: 'b', label: '拍摄前让被采访者试音,监听是否有爆音、底噪' },
|
||||
{ id: 'c', label: '多机位拍摄时,开拍前拍手一次,方便后期对齐音画' },
|
||||
{ id: 'd', label: '为节省时间,无需录制环境音,直接用后期音效填补' },
|
||||
],
|
||||
correctOptionId: 'd',
|
||||
explanation:
|
||||
'条件允许时,应额外录制 5–10 秒现场环境音,用于填补剪辑间隙,让音频过渡更自然;直接用后期音效会破坏现场真实感,因此 D 选项不规范。',
|
||||
},
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
//引导步骤
|
||||
const steps = ref<GuideStep[]>([
|
||||
@@ -97,11 +179,46 @@ const navigationSteps = ref([
|
||||
|
||||
// 步骤导航相关状态
|
||||
const showStepNavigation = ref(false)
|
||||
const currentNavigationStep = ref(1) // 对应视频索引,从0开始
|
||||
const showUnfinishedPrompt = ref(false)
|
||||
const pendingLeavePath = ref<string | null>(null)
|
||||
const allowRouteLeave = ref(false)
|
||||
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
|
||||
|
||||
// 视频列表
|
||||
const currentVideoIndex = ref(0)
|
||||
|
||||
const professionVideoKeys = [
|
||||
'interview-profession-jiweishezhi',
|
||||
'interview-profession-zhouxianyuanze',
|
||||
'interview-profession-jingtouzujie',
|
||||
'interview-profession-shengyintongbu',
|
||||
] as const
|
||||
|
||||
const completedNavigationSteps = computed(() =>
|
||||
navigationSteps.value.map(
|
||||
(_, index) =>
|
||||
interview.professionVideosWatched[professionVideoKeys[index]] === true,
|
||||
),
|
||||
)
|
||||
|
||||
const hasUnfinishedLearningVideos = computed(
|
||||
() =>
|
||||
key.value === 'profession' &&
|
||||
completedNavigationSteps.value.some((completed) => !completed),
|
||||
)
|
||||
|
||||
const markCurrentProfessionVideoWatched = () => {
|
||||
const videoKey = professionVideoKeys[currentVideoIndex.value]
|
||||
if (!videoKey) return
|
||||
|
||||
interview.markProfessionVideoWatched(videoKey)
|
||||
const isAllWatched = professionVideoKeys.every(
|
||||
(key) =>
|
||||
key === videoKey || interview.professionVideosWatched[key] === true,
|
||||
)
|
||||
if (isAllWatched) interview.markWorkVideoWatched('profession')
|
||||
}
|
||||
|
||||
//按顺序播放
|
||||
const currentVideo = computed(
|
||||
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
|
||||
@@ -116,13 +233,14 @@ const handleVideoChange = () => {
|
||||
currentVideoIndex.value++
|
||||
currentNavigationStep.value = currentVideoIndex.value
|
||||
} else {
|
||||
// status.value = true
|
||||
// steps.value = [
|
||||
// {
|
||||
// id: 'step1',
|
||||
// content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
|
||||
// }
|
||||
// ]
|
||||
status.value = true
|
||||
steps.value = [
|
||||
{
|
||||
id: 'step1',
|
||||
content:
|
||||
'恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!',
|
||||
},
|
||||
]
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
@@ -133,6 +251,7 @@ const handleVideoChange = () => {
|
||||
|
||||
const handleClose = () => {
|
||||
if (key.value === 'idea') {
|
||||
interview.markWorkVideoWatched('idea')
|
||||
interview.setInterviewUnlockProgress(1)
|
||||
router.push({
|
||||
name: 'Interview',
|
||||
@@ -151,11 +270,32 @@ const handleClick = () => {}
|
||||
const handlePlay = () => {}
|
||||
|
||||
const onEnded = () => {
|
||||
if (key.value === 'profession') handleVideoChange()
|
||||
if (key.value === 'case') {
|
||||
interview.markWorkVideoWatched('case')
|
||||
return
|
||||
}
|
||||
|
||||
if (key.value === 'profession') {
|
||||
// 机位设置(index 0)、轴线原则(index 1)、声音同步与收录(index 3)先出题,答完才算完成
|
||||
if (
|
||||
currentVideoIndex.value === 0 ||
|
||||
currentVideoIndex.value === 1 ||
|
||||
currentVideoIndex.value === 3
|
||||
) {
|
||||
playing.value = false
|
||||
questions.value = getQuestionsForVideo(currentVideoIndex.value)
|
||||
showQuiz.value = true
|
||||
return
|
||||
}
|
||||
|
||||
markCurrentProfessionVideoWatched()
|
||||
handleVideoChange()
|
||||
}
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
if (key.value === 'profession') {
|
||||
interview.markWorkVideoWatched('profession')
|
||||
if (!interview.isInterviewUnlock) interview.setIsInterviewUnlock(true)
|
||||
router.push({
|
||||
name: 'InterviewWork',
|
||||
@@ -163,8 +303,16 @@ const handleComplete = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 答题完成后的处理
|
||||
const handleQuestionsComplete = () => {
|
||||
markCurrentProfessionVideoWatched()
|
||||
showQuiz.value = false
|
||||
handleVideoChange()
|
||||
}
|
||||
|
||||
// 步骤导航事件处理
|
||||
const handleStepClick = (index: number) => {
|
||||
showQuiz.value = false
|
||||
currentVideoIndex.value = index
|
||||
currentNavigationStep.value = index
|
||||
nextTick(() => {
|
||||
@@ -176,6 +324,25 @@ const handleStepNavigationClose = () => {
|
||||
showStepNavigation.value = false
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (key.value === 'idea') {
|
||||
status.value = true
|
||||
@@ -243,6 +410,12 @@ onMounted(() => {
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleClose"
|
||||
/>
|
||||
<QuestionQuiz
|
||||
v-if="showQuiz && key === 'profession'"
|
||||
:show-question-number="false"
|
||||
:questions="questions"
|
||||
@complete="handleQuestionsComplete"
|
||||
/>
|
||||
</template>
|
||||
</Video>
|
||||
<div
|
||||
@@ -279,9 +452,16 @@ onMounted(() => {
|
||||
:background-image="getStaticImage('work/vlog背景全屏待定')"
|
||||
:steps="navigationSteps"
|
||||
:current-step-index="currentNavigationStep"
|
||||
:completed-steps="completedNavigationSteps"
|
||||
@step-click="handleStepClick"
|
||||
@close="handleStepNavigationClose"
|
||||
/>
|
||||
|
||||
<UnfinishedLearningPrompt
|
||||
:visible="showUnfinishedPrompt"
|
||||
@continue="handleContinueLearning"
|
||||
@leave="handleConfirmLeave"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in new issue
Block a user