Files
super_video/src/views/vlog/index.vue
T
zhongyidianandClaude Opus 4.7 6f289959be feat: 完善视频学习进度缓存
完善新手导航和三大创作模块的视频完成缓存、未完成提示与学习进度计算,确保进度按实际学习资源完成情况更新。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:05:29 +08:00

316 lines
8.8 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="tsx" setup>
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { computed, ref, watch, type Ref } from 'vue'
import { ProgressBar, Overlay } from '@/components'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { useRoute, useRouter } from 'vue-router'
import { useVlogStore } from '@/stores'
import VlogMenu from './components/menu.vue'
const route = useRoute()
const router = useRouter()
const vlog = useVlogStore()
const visible = computed({
get: () => vlog.vlogMenuVisible,
set: (value) => vlog.setVlogMenuVisible(value),
})
const currentStep = computed(() => vlog.vlogUnlockProgress)
const progress = computed(() => vlog.progressPercent)
// 实战操作台一次性引导(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'),
},
{
id: 'return-step2',
content: '接下来,我们将一起完成真实主题作品——《我的志愿者手记》。',
audioSrc: getPublicPath('audio/py_2_007.MP3'),
},
{
id: 'return-step3',
content:
'在记录社区志愿活动的过程中,学习 Vlog 从前期策划、中期拍摄到后期剪辑的完整流程,同时传递热心助人、服务社会的价值力量。',
audioSrc: getPublicPath('audio/py_2_008.MP3'),
},
]
const handleReturnGuideComplete = () => {
showReturnGuide.value = false
vlog.setIsGuideVlogReturn(false)
router.replace({ name: 'Vlog' })
}
// 三个阶段菜单的一次性引导
const stageGuideMap = {
1: {
flagKey: 'isGuideVlogStage1',
setter: 'setIsGuideVlogStage1',
steps: [
{
id: 'stage1-step1',
content: '一次顺利的拍摄,往往从充分准备开始。',
audioSrc: getPublicPath('audio/py_2_009.MP3'),
},
{
id: 'stage1-step2',
content:
'前期阶段,我们需要完成四项关键任务:\n【资源筹备】【创意构想】【脚本撰写】【分镜制作】',
audioSrc: getPublicPath('audio/py_2_010.MP3'),
},
// {
// id: 'stage1-step3',
// content: '画面中高亮的按钮是我们可以展开学习的内容。点击进入学习吧!',
// audioSrc: getPublicPath('audio/py_2_011.MP3'),
// },
],
},
2: {
flagKey: 'isGuideVlogStage2',
setter: 'setIsGuideVlogStage2',
steps: [
{
id: 'stage2-step1',
content:
'拍摄过程可分为三个核心步骤:\n【设备准备】【实地取景】【现场拍摄】。稳妥的准备是顺利拍摄的前提。',
audioSrc: getPublicPath('audio/py_2_012.MP3'),
},
],
},
3: {
flagKey: 'isGuideVlogStage3',
setter: 'setIsGuideVlogStage3',
steps: [
{
id: 'stage3-step1',
content: '经过一系列的准备与拍摄,我们来到后期剪辑阶段。',
audioSrc: getPublicPath('audio/py_2_016.MP3'),
},
{
id: 'stage3-step2',
content:
'这个阶段需要做好四件事:\n【素材整理】【剪辑】【调色】【导出发布】。',
audioSrc: getPublicPath('audio/py_2_017.MP3'),
},
{
id: 'stage3-step3',
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 : [],
) as Ref<GuideStep[]>
const triggerStageGuide = (step: number) => {
if (step !== 1 && step !== 2 && step !== 3) return
const stage = step as StageKey
const config = stageGuideMap[stage]
if (!(vlog as any)[config.flagKey]) return
stageGuideStep.value = stage
}
const handleStageGuideComplete = () => {
if (!stageGuideStep.value) return
const config = stageGuideMap[stageGuideStep.value]
;(vlog as any)[config.setter](false)
stageGuideStep.value = null
}
const handleComplete = () => {
// console.log('完成前置条件')
// 处理完成逻辑
router.push({ name: 'VlogPractice' })
}
const handleStepChange = (step: number) => {
// 处理步骤变化逻辑
vlog.setVlogUnlockProgress(step)
visible.value = true
}
const handleWorkClick = () => {
// console.log('任务取景按钮点击')
// 处理任务取景按钮点击逻辑
router.push({ name: 'VlogWork' })
}
const handleClose = () => {
router.push({ name: 'Action' })
}
watch(
showPracticeGuide,
(val) => {
if (val) {
vlog.markGuideShown(practiceGuideKey)
}
},
{ immediate: true },
)
watch(
[visible, currentStep],
([isVisible, step]) => {
if (isVisible && step < 4) {
triggerStageGuide(step)
}
},
{ immediate: true },
)
</script>
<template>
<div class="w-full h-full relative">
<!-- 背景图片-center -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER),
}"
></div>
<!-- 进度数据 -->
<div class="absolute top-[140px] left-[calc(50%)] -translate-x-1/2">
<div
class="flex items-center justify-center text-white font-[1000] [text-shadow:_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1]"
>
<span class="text-6xl">学习进度:</span>
<span class="text-7xl">{{ progress }}%</span>
</div>
</div>
<!-- 关闭 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="handleClose"
/>
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG) }"
></div>
<!-- 标题 -->
<div class="absolute top-2 left-1/2 -translate-x-1/2">
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
</div>
<!-- 任务取景按钮 -->
<div
class="absolute left-24 top-24"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.TASK_CAMERA),
}"
>
<ImgButton
: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"
>
<!-- 关闭按钮 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="visible = false"
/>
<!-- 菜单 -->
<VlogMenu :current-step="currentStep" />
<!-- 进度条 -->
<div class="absolute left-0 bottom-0 w-full">
<ProgressBar
module-name="vlog"
:current-step="currentStep"
:completed-step="vlog.completedStep"
@complete="handleComplete"
@step-change="handleStepChange"
/>
</div>
</Overlay>
<div class="absolute bottom-0 w-full">
<ProgressBar
module-name="vlog"
:current-step="currentStep"
:completed-step="vlog.completedStep"
@complete="handleComplete"
@step-change="handleStepChange"
/>
</div>
<!-- 返回 Vlog 首页后的一次性引导 -->
<GuideSteps
v-if="showReturnGuide"
:steps="returnSteps"
position-offset-y="85%"
@complete="handleReturnGuideComplete"
@skip="handleReturnGuideComplete"
/>
<!-- 阶段菜单一次性引导 -->
<GuideSteps
v-if="stageGuideStep"
:steps="stageGuideSteps"
position-offset-y="85%"
@complete="handleStageGuideComplete"
@skip="handleStageGuideComplete"
/>
</div>
</template>