feat: 完善视频学习进度缓存

完善新手导航和三大创作模块的视频完成缓存、未完成提示与学习进度计算,确保进度按实际学习资源完成情况更新。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-11 09:05:29 +08:00
1 parent 1f8723aabd
commit 6f289959be
63 files changed
+3139 -1126

No files matched your search

+80 -36
View File
@@ -4,7 +4,7 @@ import { ImgButton } from '@/components/Button'
import { computed, ref, watch, type Ref } from 'vue'
import { ProgressBar, Overlay } from '@/components'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { useRoute, useRouter } from 'vue-router'
import { useVlogStore } from '@/stores'
import VlogMenu from './components/menu.vue'
@@ -15,32 +15,43 @@ const vlog = useVlogStore()
const visible = computed({
get: () => vlog.vlogMenuVisible,
set: value => vlog.setVlogMenuVisible(value)
set: (value) => vlog.setVlogMenuVisible(value),
})
const currentStep = computed(() => vlog.vlogUnlockProgress)
const progress = computed(() => vlog.progressPercent)
const showReturnGuide = ref(route.query.guide === 'return' && vlog.isGuideVlogReturn)
// 实战操作台一次性引导(currentStep === 4 时解锁)
const practiceGuideKey = 'vlog-practice-guide'
const hasShownPracticeGuide = computed(
() => vlog.guideShown[practiceGuideKey] === true,
)
const showPracticeGuide = computed(
() => currentStep.value === 4 && !hasShownPracticeGuide.value,
)
const showReturnGuide = ref(
route.query.guide === 'return' && vlog.isGuideVlogReturn,
)
const returnSteps: GuideStep[] = [
{
id: 'return-step1',
content: '通过前面的案例赏析,相信你已经初步了解了 Vlog 的表达方式。',
audioSrc: getPublicPath('audio/py_2_006.MP3')
audioSrc: getPublicPath('audio/py_2_006.MP3'),
},
{
id: 'return-step2',
content: '接下来,我们将一起完成一支真实主题作品——《我的志愿者手记》。',
audioSrc: getPublicPath('audio/py_2_007.MP3')
content: '接下来,我们将一起完成真实主题作品——《我的志愿者手记》。',
audioSrc: getPublicPath('audio/py_2_007.MP3'),
},
{
id: 'return-step3',
content:
'在记录社区志愿活动的过程中,学习 Vlog 从前期策划、中期拍摄到后期剪辑的完整流程,同时传递热心助人、服务社会的价值力量。',
audioSrc: getPublicPath('audio/py_2_008.MP3')
}
audioSrc: getPublicPath('audio/py_2_008.MP3'),
},
]
const handleReturnGuideComplete = () => {
@@ -58,20 +69,20 @@ const stageGuideMap = {
{
id: 'stage1-step1',
content: '一次顺利的拍摄,往往从充分准备开始。',
audioSrc: getPublicPath('audio/py_2_009.MP3')
audioSrc: getPublicPath('audio/py_2_009.MP3'),
},
{
id: 'stage1-step2',
content:
'前期阶段,我们需要完成四项关键任务:【资源筹备】【创意构想】【脚本撰写】【分镜制作】',
audioSrc: getPublicPath('audio/py_2_010.MP3')
'前期阶段,我们需要完成四项关键任务:\n【资源筹备】【创意构想】【脚本撰写】【分镜制作】',
audioSrc: getPublicPath('audio/py_2_010.MP3'),
},
{
id: 'stage1-step3',
content: '画面中高亮的按钮是我们可以展开学习的内容。点击进入学习吧!',
audioSrc: getPublicPath('audio/py_2_011.MP3')
}
]
// {
// id: 'stage1-step3',
// content: '画面中高亮的按钮是我们可以展开学习的内容。点击进入学习吧!',
// audioSrc: getPublicPath('audio/py_2_011.MP3'),
// },
],
},
2: {
flagKey: 'isGuideVlogStage2',
@@ -80,10 +91,10 @@ const stageGuideMap = {
{
id: 'stage2-step1',
content:
'拍摄过程可分为三个核心步骤:【设备准备】【实地取景】【现场拍摄】。稳妥的准备是顺利拍摄的前提。',
audioSrc: getPublicPath('audio/py_2_012.MP3')
}
]
'拍摄过程可分为三个核心步骤:\n【设备准备】【实地取景】【现场拍摄】。稳妥的准备是顺利拍摄的前提。',
audioSrc: getPublicPath('audio/py_2_012.MP3'),
},
],
},
3: {
flagKey: 'isGuideVlogStage3',
@@ -91,28 +102,30 @@ const stageGuideMap = {
steps: [
{
id: 'stage3-step1',
content: '经过一系列的准备与拍摄,现在我们来到后期剪辑阶段。',
audioSrc: getPublicPath('audio/py_2_016.MP3')
content: '经过一系列的准备与拍摄,我们来到后期剪辑阶段。',
audioSrc: getPublicPath('audio/py_2_016.MP3'),
},
{
id: 'stage3-step2',
content: '这个阶段需要做好四件事:【素材整理】【剪辑】【调色】【导出发布】。',
audioSrc: getPublicPath('audio/py_2_017.MP3')
content:
'这个阶段需要做好四件事:\n【素材整理】【剪辑】【调色】【导出发布】。',
audioSrc: getPublicPath('audio/py_2_017.MP3'),
},
{
id: 'stage3-step3',
content: '画面中高亮的按钮,是可以展开深入学习的内容,点击就能开启学习啦!',
audioSrc: getPublicPath('audio/py_2_018.MP3')
}
]
}
content:
'画面中高亮的按钮,是可以展开深入学习的内容,点击就能开启学习啦!',
audioSrc: getPublicPath('audio/py_2_018.MP3'),
},
],
},
} as const
type StageKey = keyof typeof stageGuideMap
const stageGuideStep = ref<StageKey | null>(null)
const stageGuideSteps = computed(() =>
stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : []
stageGuideStep.value ? stageGuideMap[stageGuideStep.value].steps : [],
) as Ref<GuideStep[]>
const triggerStageGuide = (step: number) => {
@@ -152,6 +165,16 @@ const handleClose = () => {
router.push({ name: 'Action' })
}
watch(
showPracticeGuide,
(val) => {
if (val) {
vlog.markGuideShown(practiceGuideKey)
}
},
{ immediate: true },
)
watch(
[visible, currentStep],
([isVisible, step]) => {
@@ -159,7 +182,7 @@ watch(
triggerStageGuide(step)
}
},
{ immediate: true }
{ immediate: true },
)
</script>
@@ -168,7 +191,9 @@ watch(
<!-- 背景图片-center -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER),
}"
></div>
<!-- 进度数据 -->
@@ -202,22 +227,41 @@ watch(
<!-- 任务取景按钮 -->
<div
class="absolute left-24 top-24"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA),
}"
>
<ImgButton
:image="currentStep === 4 ? IMAGE_PATHS.VLOG.TASK_BTN_OK : IMAGE_PATHS.VLOG.TASK_BTN"
:image="
currentStep > 0
? IMAGE_PATHS.VLOG.TASK_BTN_OK
: IMAGE_PATHS.VLOG.TASK_BTN
"
@click="handleWorkClick"
/>
</div>
<GuideSteps
:visible="showPracticeGuide"
highlight-selector=".right-section"
position-offset-y="50%"
@complete="vlog.markGuideShown(practiceGuideKey)"
>
<GuideStepsItem>点击【实战操作台】进入实战操作!</GuideStepsItem>
</GuideSteps>
<!-- 引导人物 -->
<ImgButton
v-if="currentStep !== 4"
v-show="!showReturnGuide"
class="absolute right-24 bottom-24"
:image="IMAGE_PATHS.VLOG.TASK_PERSON"
/>
<Overlay content-class="w-full h-full" :visible="visible && currentStep < 4">
<Overlay
content-class="w-full h-full"
:visible="visible && currentStep < 4"
>
<!-- 关闭按钮 -->
<ImgButton
class="absolute right-0 top-0 z-20"