feat: 完善访谈与vlog模块页面,重组设备与图片公共资源,新增音频管理与一班对接工具

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-05 15:41:41 +08:00
1 parent c4c9be9d35
commit fc9633f371
159 files changed
+4823 -2535

No files matched your search

@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getStaticImage, IMAGE_PATHS } from '@/utils'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
// 视频导入
// 手机摄像头介绍
import video1 from '/video/new/xinshou_shoujishexiangtoujieshao.mp4'
@@ -146,7 +146,7 @@ const getQuestionsForVideo = (index: number): Question[] => {
return [
{
id: 3,
title: '互动题 4',
title: '',
description: '(单选)你觉得下面哪句话,更接近“新手拍视频的正确思路”?',
options: [
{
@@ -223,6 +223,12 @@ const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
// 答题完成后的处理
const handleQuestionsComplete = () => {
showQuiz.value = false
@@ -291,12 +297,16 @@ const handleQuestionsComplete = () => {
@close="handleStepNavigationClose"
/>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>欢迎来到基础拍摄训练区!</GuideStepsItem>
<GuideStepsItem
>接下来,我会带你依次认识手机摄像头、看懂景别、用好慢动作与延时技法。</GuideStepsItem
>
<GuideStepsItem>还会教你如何拍出更稳的微距和更亮的夜景。</GuideStepsItem>
<GuideSteps :visible="true" 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>
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import { Video } from '@/components'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
// 运镜初识 - 暂时用运镜的基本方式代替
import video1 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
import video2 from '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
@@ -20,6 +20,23 @@ const props = defineProps<{
const playing = ref(false)
const status = ref(false)
const showCameraMovementGuide = ref(false)
const cameraMovementGuideShown = ref(false)
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
const handleCameraMovementGuideComplete = () => {
showCameraMovementGuide.value = false
cameraMovementGuideShown.value = true
nextTick(() => {
playing.value = true
})
}
// 步骤导航数据
const navigationSteps = ref([
// {
@@ -60,6 +77,14 @@ const handleVideoChange = () => {
status.value = true
return
}
// 切到“运镜的基本方式”(index 1)时,先暂停并展示引导对话
if (currentVideoIndex.value === 1 && !cameraMovementGuideShown.value) {
playing.value = false
showCameraMovementGuide.value = true
return
}
nextTick(() => {
// 使用受控播放来触发下一个视频自动播放
playing.value = true
@@ -145,9 +170,26 @@ const handleStepNavigationClose = () => {
@close="handleStepNavigationClose"
/>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>想拍出好的视频效果,画面稳定性是最基本的要求。</GuideStepsItem>
<GuideStepsItem>接下来,我们来学习正确的持机方式和拍摄姿势。</GuideStepsItem>
<GuideSteps :visible="true" position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_014.MP3')">
想拍出好的视频效果,画面稳定性是最基本的要求。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_015.MP3')">
接下来,我们来学习正确的持机方式和拍摄姿势。
</GuideStepsItem>
</GuideSteps>
<GuideSteps
v-if="showCameraMovementGuide"
position-offset-y="85%"
@complete="handleCameraMovementGuideComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_016.MP3')">
当我们将画面稳定之后,就来到了镜头语言。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_017.MP3')">
同一个场景,用不同运镜,表达就不一样。跟着我,去了解5种基础运镜吧~
</GuideStepsItem>
</GuideSteps>
</div>
</template>
+22 -6
View File
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import { Video } from '@/components'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import video1 from '/video/new/xsdh_rhxzgt_batch.mp4'
import video2 from '/video/new/xsdh_jzgtf_batch.mp4'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
@@ -75,6 +75,8 @@ const onEnded = () => {
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
currentNavigationStep.value = index
// 先重置播放状态,确保 v-model 触发 watcher
playing.value = false
nextTick(() => {
playing.value = true
})
@@ -84,6 +86,12 @@ const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
onMounted(() => {})
</script>
@@ -145,15 +153,23 @@ onMounted(() => {})
:highlight-selector="guideStepIndex === 3 ? '.step-navigation-btn' : ''"
position-offset-y="85%"
@step-change="(_: any, index: number) => (guideStepIndex = index)"
@complete="handleIntroGuideComplete"
>
<GuideStepsItem>你有没有过这样的感觉?画面里什么都有,却不知道重点在哪里?</GuideStepsItem>
<GuideStepsItem>很多时候,不是内容不够好,而是需要有构图思维。</GuideStepsItem>
<GuideStepsItem>所谓构图,就是在画面中对元素进行选择、取舍和安排</GuideStepsItem>
<GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_005.MP3')">
你有没有过这样的感觉?画面里什么都有,却不知道重点在哪里?
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_006.MP3')">
很多时候,不是内容不够好,而是需要有构图思维。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_007.MP3')">
所谓构图,就是在画面中对元素进行选择、取舍和安排
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_008.MP3')">
<div class="flex flex-wrap items-center justify-start">
<div>一起来看看吧~</div>
<div class="text-yellow-400">
点击<button
点击
<button
class="w-10 h-10 bg-blue-500/80 hover:bg-blue-500 rounded-full inline-flex items-center justify-center text-white transition-all duration-200"
title="步骤导航"
>
+44 -2
View File
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getStaticImage, IMAGE_PATHS } from '@/utils'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps } from '@/components/Guide'
// 光初识 - 暂时用光的基础认知代替
import video1 from '/video/new/xinshou_guangdejichurenzhi.mp4'
@@ -21,12 +21,25 @@ const showQuiz = ref(false) // 控制是否显示答题窗口
const isGuide = ref(true)
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
})
}
// 步骤导航数据
@@ -103,6 +116,14 @@ const handleVideoChange = () => {
status.value = true
return
}
// 切到“拍摄的黄金时间”(index 2)时,先暂停并展示引导对话
if (currentVideoIndex.value === 2 && !goldenHourGuideShown.value) {
playing.value = false
showGoldenHourGuide.value = true
return
}
nextTick(() => {
// 使用受控播放来触发下一个视频自动播放
playing.value = true
@@ -215,7 +236,28 @@ const handleQuestionsComplete = () => {
<!-- 引导与高亮 -->
<GuideSteps v-if="isGuide" position-offset-y="85%" @complete="handleComplete">
<GuideStepsItem>光是摄影的灵魂,摄影高手一定是用光的高手</GuideStepsItem>
<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>
@@ -2,10 +2,9 @@
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import { StepNavigation, GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video } from '@/components'
import { getStaticImage, IMAGE_PATHS } from '@/utils'
import video from '/video/new/xsdh_CapCut_Tutorial.mp4'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
@@ -18,55 +17,71 @@ const rawSteps = [
{
id: 'step2',
label: '剪映基础操作',
content: '认识剪映剪辑界面',
content: '初识剪映专业版界面和功能',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step3',
label: '剪映基础操作',
content: '素材导入及调整',
content: '时间线的用法详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step4',
label: '剪映基础操作',
content: '比例及背景设置',
content: '转场特效贴纸功能详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step5',
label: '剪映基础操作',
content: '内容裁剪及组合',
content: '如何给视频快速上字幕',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step6',
label: '剪映基础操作',
content: '音频添加及调整',
content: '音频的功能应用和处理',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step7',
label: '剪映基础操作',
content: '添加字幕和贴纸',
content: '关键帧功能用法详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step8',
label: '剪映基础操作',
content: '添加特效和滤镜',
content: '蒙版功能用法详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step9',
label: '剪映基础操作',
content: '添加转场和动画',
content: '混合模式功能用法详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step10',
label: '剪映基础操作',
content: '封面和视频导出',
content: '曲线变速功能用法详解',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
},
{
id: 'step11',
label: '剪映基础操作',
content: '如何给画面调色',
videoUrl: getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
type: 'other' as const
}
]
@@ -78,13 +93,13 @@ const showStepNavigation = ref(false)
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
// 视频列表
const videoList = [video, video, video, video, video, video, video, video, video]
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => videoList[currentVideoIndex.value])
const currentVideo = computed(() => rawSteps[currentVideoIndex.value].videoUrl)
const handleVideoChange = () => {
if (currentVideoIndex.value < videoList.length - 1) {
if (currentVideoIndex.value < rawSteps.length - 1) {
currentVideoIndex.value++
currentNavigationStep.value = currentVideoIndex.value
} else {
@@ -119,6 +134,12 @@ const handleStepClick = (index: number) => {
const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
</script>
<template>
@@ -174,6 +195,21 @@ const handleStepNavigationClose = () => {
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps :visible="true" position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_021.MP3')">
拍摄工作完成以后还需要剪辑来赋予节奏与表达。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_022.MP3')">
接下来,我们一起进入剪映专业版,开启剪映软件的操作学习。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_023.MP3')">
从界面认识到时间线操作,从字幕添加到音频处理,再到调色与特效制作,你将逐步掌握剪映软件的基础技能。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_024.MP3')">
准备好了吗?让零散的素材在你的手中拼接成精彩作品!
</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -21,10 +21,10 @@ const back = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/调色-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
<!-- 内容 -->
<div class="relative h-full pt-28 overflow-y-scroll">
@@ -1,15 +1,12 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, GuideSteps, GuideStepsItem } from '@/components'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { useFilmStore } from '@/stores'
const router = useRouter()
const film = useFilmStore()
const handleBack = () => {
router.back()
}
@@ -25,10 +22,6 @@ const showGuide = ref(true)
const handleComplete = () => {
showGuide.value = false
}
onMounted(() => {
film.setFilmUnlockProgress(4)
})
</script>
<template>
@@ -60,7 +53,7 @@ onMounted(() => {
/>
<GuideSteps v-if="showGuide" position-offset-y="85%" @complete="handleComplete">
<GuideStepsItem> 现在一起来看看导出后的影片效果吧! </GuideStepsItem>
<GuideStepsItem>现在一起来看看导出后的影片效果吧!</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -21,10 +21,10 @@ const back = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/音效-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/音效-标题.png')" alt="" />
<!-- 内容 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] -->
<div class="relative h-full pt-28 overflow-y-scroll">
+27 -10
View File
@@ -3,6 +3,7 @@ import { getStaticImage } from '@/utils'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage } from '@/utils'
import { useFilmStore } from '@/stores'
defineOptions({ name: 'FilmMenu' })
/**
@@ -71,12 +72,12 @@ const menuItems = ref([
parentValue: 2,
disabled: false
},
{
label: '素材管理',
routeName: 'Film素材管理',
parentValue: 3,
disabled: true
},
// {
// label: '素材管理',
// routeName: 'Film素材管理',
// parentValue: 3,
// disabled: true
// },
{
label: '剪辑',
routeName: 'FilmEditing',
@@ -109,6 +110,11 @@ const props = defineProps<{
}>()
const router = useRouter()
const film = useFilmStore()
const isViewed = (routeName: string) => {
return film.viewed?.[routeName] === true
}
const currentParentValue = computed<number>(() => {
return props.currentStep
@@ -131,9 +137,9 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
<div class="absolute left-20 top-0 bottom-0 w-[520px] pt-28" v-if="currentStep">
<!-- 分类名称 -->
<div class="absolute top-3 left-24 flex items-center space-x-2">
<span class="text-4xl text-[#fbbf24] font-black">{{
category[currentParentValue - 1].label
}}</span>
<span class="text-4xl text-[#fbbf24] font-black">
{{ category[currentParentValue - 1].label }}
</span>
<img class="filter-blue" :src="category[currentParentValue - 1].icon" alt="" />
</div>
<!-- 背景图片 -->
@@ -157,7 +163,18 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 group-hover:opacity-100 transition-opacity duration-300"
:style="{ backgroundImage: getBackgroundImage('components/menu-hover.png') }"
></div>
<span :class="[!item.disabled ? 'menu-glow-text' : '']">{{ item.label }}</span>
<span class="flex items-center gap-2" :class="[!item.disabled ? 'menu-glow-text' : '']">
<span>{{ item.label }}</span>
<img
class="w-10"
:src="
isViewed(item.routeName)
? '/src/assets/images/midterm/完成.png'
: '/src/assets/images/midterm/未完成.png'
"
alt=""
/>
</span>
</div>
</template>
</div>
+2 -2
View File
@@ -32,10 +32,10 @@ const handleOpenImages = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/创意构想-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/创意构想-标题.png')" alt="" />
<!-- 内容 -->
<div class="relative h-full pt-28 overflow-y-scroll">
+7 -10
View File
@@ -17,9 +17,7 @@ const status = ref(true)
const currentStep = computed(() => film.FilmUnlockProgress)
const progress = computed(() => {
return Math.round((currentStep.value / 4) * 100)
})
const progress = computed(() => film.progressPercent)
const guideIndex = ref<number>(0)
const showTaskGuide = computed(
@@ -130,7 +128,7 @@ watch(
highlight-selector=".right-section"
position-offset-y="50%"
>
<GuideStepsItem> 点击这里进入实战操作台 </GuideStepsItem>
<GuideStepsItem>点击这里进入实战操作台</GuideStepsItem>
</GuideSteps>
<!-- 引导任务 -->
@@ -167,7 +165,7 @@ watch(
<ProgressBar
module-name="film"
:current-step="currentStep"
:completed-step="2"
:completed-step="film.completedStep"
@step-change="handleStepChange"
/>
</div>
@@ -177,6 +175,7 @@ watch(
<ProgressBar
module-name="film"
:current-step="currentStep"
:completed-step="film.completedStep"
@complete="handleComplete"
@step-change="handleStepChange"
/>
@@ -191,11 +190,9 @@ watch(
>
<GuideStepsItem>
<div class="flex items-center justify-start">
请点击<img
class="w-20"
:src="getStaticImage('film/icon')"
alt=""
/>进入任务取景框开始学习!
请点击
<img class="w-20" :src="getStaticImage('film/icon')" alt="" />
进入任务取景框开始学习!
</div>
</GuideStepsItem>
</GuideSteps>
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps } from '@/components/Guide'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -28,42 +28,100 @@ const steps = ref([
highlightSelector: '.plate'
}
])
const stepIndex = ref<number>(0)
const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
const currentHighlight = computed(() => {
return steps.value[stepIndex.value ?? 0]?.highlightSelector
})
type EquipmentCategoryId = 'camera' | 'slider' | 'tripod' | 'stabilizer' | 'light' | 'filter'
interface EquipmentProduct {
img: string
}
interface EquipmentCategory {
id: EquipmentCategoryId
name: string
disabled: boolean
titleImg: string
contentImg: string
products: EquipmentProduct[]
}
// 菜单项数据
const menuItems = ref([
{ id: 'phone', name: '相机/摄像机/微单', disabled: false },
{ id: 'phone-stand', name: '滑轨', disabled: true },
{ id: 'bluetooth-remote', name: '三脚架', disabled: true },
{ id: 'stabilizer', name: '稳定器', disabled: true },
{ id: 'props', name: '灯光', disabled: true },
{ id: 'phone-lens', name: '滤镜', disabled: true }
// 菜单项数据 + 对应资源(资源都在 equipment)
const categories = ref<EquipmentCategory[]>([
{
id: 'camera',
name: '相机/摄像机/微单',
disabled: false,
titleImg: getStaticImage('equipment/相机标题.png'),
contentImg: getStaticImage('equipment/相机内容.png'),
products: [
{ img: getStaticImage('equipment/相机产品.png') },
{ img: getStaticImage('equipment/摄像机产品.png') },
{ img: getStaticImage('equipment/微单产品.png') }
]
},
{
id: 'slider',
name: '滑轨',
disabled: false,
titleImg: getStaticImage('equipment/滑轨标题.png'),
contentImg: getStaticImage('equipment/滑轨内容.png'),
products: [
{ img: getStaticImage('equipment/便携滑轨产品.png') },
{ img: getStaticImage('equipment/脚架式长滑轨产品.png') }
]
},
{
id: 'tripod',
name: '三脚架',
disabled: false,
titleImg: getStaticImage('equipment/三脚架标题.png'),
contentImg: getStaticImage('equipment/三脚架内容.png'),
products: [{ img: getStaticImage('equipment/三角架产品.png') }]
},
{
id: 'stabilizer',
name: '稳定器',
disabled: false,
titleImg: getStaticImage('equipment/稳定器标题.png'),
contentImg: getStaticImage('equipment/稳定器内容.png'),
products: [
{ img: getStaticImage('equipment/手机稳定器.png') },
{ img: getStaticImage('equipment/相机稳定器.png') }
]
},
{
id: 'light',
name: '灯光',
disabled: false,
titleImg: getStaticImage('equipment/灯标题.png'),
contentImg: getStaticImage('equipment/灯内容.png'),
products: [
{ img: getStaticImage('equipment/led补光灯.png') },
{ img: getStaticImage('equipment/环形灯产品.png') }
]
},
{
id: 'filter',
name: '滤镜',
disabled: false,
titleImg: getStaticImage('equipment/滤镜标题.png'),
contentImg: getStaticImage('equipment/滤镜内容.png'),
products: [{ img: getStaticImage('equipment/滤镜产品.png') }]
}
])
const phoneList = ref([
{ phoneImg: getStaticImage('film/equipment/相机产品.png') },
{ phoneImg: getStaticImage('film/equipment/摄像机产品.png') },
{ phoneImg: getStaticImage('film/equipment/微单产品.png') }
])
// 有资源的都解锁:如果某个类目没有任何产品资源,则自动禁用
categories.value = categories.value.map(c => ({
...c,
disabled: (c.products?.length ?? 0) === 0
}))
const platetitleList = ref([
{ title: getStaticImage('film/equipment/相机标题.png') },
{ title: getStaticImage('film/equipment/相机标题.png') },
{ title: getStaticImage('film/equipment/相机标题.png') }
])
const platetextList = ref([
{ text: getStaticImage('film/equipment/相机内容.png') },
{ text: getStaticImage('film/equipment/相机内容.png') },
{ text: getStaticImage('film/equipment/相机内容.png') }
])
const currentCategory = computed(
() => categories.value[selectedMenuIndex.value] ?? categories.value[0]
)
const currentProducts = computed(() => currentCategory.value?.products ?? [])
const currentTitleImg = computed(() => currentCategory.value?.titleImg ?? '')
const currentContentImg = computed(() => currentCategory.value?.contentImg ?? '')
const hiddenbarclick = () => {
hiddenbar.value = !hiddenbar.value
}
@@ -71,15 +129,15 @@ const hiddenbarclick = () => {
// 选择菜单项
const selectMenuItem = (index: number, item: { id: string; name: string; disabled: boolean }) => {
if (item.disabled) {
console.log(`菜单项 "${item.name}" 已被禁用`)
return
}
selectedMenuIndex.value = index
currentIndex.value = 0
}
// 下一张图片
const nextPhone = () => {
if (currentIndex.value < phoneList.value.length - 1) {
if (currentIndex.value < currentProducts.value.length - 1) {
currentIndex.value++
}
}
@@ -90,13 +148,9 @@ const prevPhone = () => {
currentIndex.value--
}
}
const handleStepChange = (_: any, index: number) => {
stepIndex.value = index
}
</script>
<template>
<div class="equipment-index">
<div class="equipment-index overflow-hidden">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat equipment-bg"
@@ -109,7 +163,7 @@ const handleStepChange = (_: any, index: number) => {
<div class="equipment-item" :hidden="hiddenbar">
<div class="menu-list">
<div
v-for="(item, index) in menuItems"
v-for="(item, index) in categories"
:key="item.id"
class="menu-item"
:class="{
@@ -155,12 +209,12 @@ const handleStepChange = (_: any, index: number) => {
<img src="@/assets/images/equipment/按钮-左滑.png" alt="" />
</div>
<div class="equipment-phoneImg">
<img :src="phoneList[currentIndex].phoneImg" alt="" />
<img :src="currentProducts[currentIndex]?.img" alt="" />
</div>
<div
class="equipment-rightbutton"
@click="nextPhone"
:style="{ visibility: currentIndex < phoneList.length - 1 ? 'visible' : 'hidden' }"
:style="{ visibility: currentIndex < currentProducts.length - 1 ? 'visible' : 'hidden' }"
>
<img src="@/assets/images/equipment/按钮-右滑.png" alt="" />
</div>
@@ -171,11 +225,11 @@ const handleStepChange = (_: any, index: number) => {
</div>
<div class="title">
<img :src="platetitleList[currentIndex].title" alt="" />
<img :src="currentTitleImg" alt="" />
</div>
<div class="plate-content">
<div class="text">
<img :src="platetextList[currentIndex].text" alt="" />
<img :src="currentContentImg" alt="" />
</div>
</div>
</div>
@@ -193,10 +247,8 @@ const handleStepChange = (_: any, index: number) => {
<GuideSteps
:visible="true"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
/>
</div>
@@ -3,14 +3,10 @@ import { useRouter } from 'vue-router'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const film = useFilmStore()
const router = useRouter()
const handleClose = () => {
film.setFilmUnlockProgress(3)
router.back()
}
@@ -9,7 +9,6 @@ import StartOverlay from '../components/StartOverlay.vue'
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useFilmStore } from '@/stores'
const router = useRouter()
const start = ref(false)
@@ -114,7 +113,6 @@ const captureScene = async (): Promise<void> => {
*/
const handleComplete = () => {
showGuide.value = false
useFilmStore().setFilmUnlockProgress(3)
router.push({
name: 'Film'
})
@@ -281,7 +279,7 @@ const handleComplete = () => {
<!-- 引导步骤 -->
<GuideSteps v-if="showGuide" positionOffsetY="80%" @complete="handleComplete">
<GuideStepsItem> 实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~ </GuideStepsItem>
<GuideStepsItem>实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -41,15 +41,6 @@ const steps = ref([
}
])
const currentIdx = ref<number>(0)
const handleStepChange = (_: any, index: number) => {
currentIdx.value = index
}
const currentHighlight = computed(() => {
return steps.value[currentIdx.value]?.highlightSelector
})
const guideSteps = ref([
{
id: '1',
@@ -67,14 +58,6 @@ const guideSteps = ref([
highlightSelector: '.btn-item-3'
}
])
const currentIndex = ref<number>(0)
const handleGuideStepChange = (_: any, index: number) => {
currentIndex.value = index
}
const currentHighlightSelector = computed(() => {
return guideSteps.value[currentIndex.value ?? 0]?.highlightSelector
})
const showModal = ref(false)
const showModal2 = ref(false)
@@ -134,7 +117,6 @@ const handleClickB = () => {
router.push({ name: 'FilmFilmingLocationB' })
}
const back = () => {
film.setFilmUnlockProgress(3)
// 跳转设备准备
router.back()
}
@@ -167,11 +149,9 @@ const back = () => {
<GuideSteps
:visible="true"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
position-offset-y="20%"
@step-change="handleStepChange"
/>
<!-- 保存图片弹窗 -->
@@ -219,12 +199,9 @@ const back = () => {
:visible="true"
:steps="guideSteps"
:keyboard-navigation="true"
:highlight-selector="currentHighlightSelector"
:highlight-padding="12"
:highlight-radius="16"
position-offset-y="85%"
@step-change="handleGuideStepChange"
@complete="currentIndex = 0"
/>
<!-- 知识卡片 -->
+2 -3
View File
@@ -3,7 +3,6 @@ import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { ref } from 'vue'
const router = useRouter()
@@ -20,10 +19,10 @@ const back = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/资源筹备-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/资源筹备-标题.png')" alt="" />
<!-- 内容 -->
<div class="relative h-full pt-28 overflow-y-scroll">
+2 -2
View File
@@ -33,10 +33,10 @@ const handleOpenImages = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/脚本编写-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/脚本编写-标题.png')" alt="" />
<!-- 内容 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] -->
<div class="relative h-full pt-28 overflow-y-scroll">
+11 -17
View File
@@ -1,34 +1,30 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale'
import { useFilmStore } from '@/stores'
const router = useRouter()
const showDoubao = ref(false)
const film = useFilmStore()
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
{
id: 'step1',
content: '先认识 Vlog:点击右侧「1.vlog初识」继续',
highlightSelector: '.img-switch-item--vlog'
},
{
id: 'step2',
content: '这是主要内容区域的边框',
highlightSelector: '.work-border'
}
]
const stepIndex = ref(1)
const back = () => {
router.back()
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
const clickdoubao = () => {
film.setFilmUnlockProgress(2)
router.push({
name: 'Film'
})
@@ -105,10 +101,8 @@ onMounted(() => {
<GuideSteps
v-if="showDoubao"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem>
+2 -6
View File
@@ -5,16 +5,12 @@ import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const { scale, offsetX, offsetY } = useScale()
const router = useRouter()
const film = useFilmStore()
const back = () => {
film.setFilmUnlockProgress(2)
router.back()
}
@@ -36,10 +32,10 @@ const handleOpenImages = () => {
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('film/img/背景.png')" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('film/img/分镜制作-标题.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/分镜制作-标题.png')" alt="" />
<!-- 内容 -->
<div class="relative h-full pt-28 overflow-y-scroll">
+4 -8
View File
@@ -209,11 +209,6 @@ const handleComplete = () => {
router.push({
name: 'FilmWork'
})
} else {
// if (film.FilmUnlockProgress == 0) film.setFilmUnlockProgress(1)
// router.push({
// name: 'Film'
// })
}
}
@@ -332,7 +327,8 @@ onMounted(() => {
<div class="flex flex-wrap items-center justify-start">
<div>接下来,让我们一起看看打造电影感的几种关键技巧吧。</div>
<div class="text-yellow-400">
点击<button
点击
<button
class="w-10 h-10 bg-blue-500/80 hover:bg-blue-500 rounded-full inline-flex items-center justify-center text-white transition-all duration-200"
title="步骤导航"
>
@@ -361,8 +357,8 @@ onMounted(() => {
<GuideStepsItem>
一个好镜头,既要好看,又要好懂 —— 既有视觉吸引力,又能把信息清晰地传达出来。
</GuideStepsItem>
<GuideStepsItem> 当这些条件被满足,一个镜头就有了“高级感”。 </GuideStepsItem>
<GuideStepsItem> 而优秀的短视频,就是靠无数这样的好镜头累积而成的。 </GuideStepsItem>
<GuideStepsItem>当这些条件被满足,一个镜头就有了“高级感”。</GuideStepsItem>
<GuideStepsItem>而优秀的短视频,就是靠无数这样的好镜头累积而成的。</GuideStepsItem>
<GuideStepsItem>
接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧!
</GuideStepsItem>
+7 -9
View File
@@ -1,5 +1,5 @@
<script lang="tsx" name="Work" setup>
defineOptions({ name: 'Work' })
<script lang="tsx" name="FilmWork" setup>
defineOptions({ name: 'FilmWork' })
import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
@@ -146,8 +146,8 @@ function openImages(urls: string[], startIndex = 0) {
<span
class="mt-24 text-white font-bold text-6xl [text-shadow:0_2px_4px_rgba(0,0,0,0.2),0_0_6px_rgba(0,207,255,0.9),0_0_14px_rgba(0,207,255,0.7)]"
>
{{ currentItem?.description }}</span
>
{{ currentItem?.description }}
</span>
</div>
<!-- <div class="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
@@ -199,11 +199,9 @@ function openImages(urls: string[], startIndex = 0) {
</GuideStepsItem>
<GuideStepsItem>
<div class="flex items-center justify-start">
请点击 &nbsp;<img
class="w-16"
:src="getStaticImage('film/sx')"
alt=""
/>&nbsp;,进入案例赏析!
请点击 &nbsp;
<img class="w-16" :src="getStaticImage('film/sx')" alt="" />
&nbsp;,进入案例赏析!
</div>
</GuideStepsItem>
</GuideSteps>
+179 -120
View File
@@ -2,33 +2,135 @@
defineOptions({
name: 'Action'
})
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { ModuleSwitcher } from './components/ModuleSwitcher'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore, useFilmStore, useVlogStore } from '@/stores'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { computed, ref, onMounted } from 'vue'
import { useRouter, type RouteRecordName } from 'vue-router'
import {
useUserStore,
useFilmStore,
useVlogStore,
useInterviewStore,
useBasicElementsStore
} from '@/stores'
import { GuideSteps } from '@/components/Guide'
import type { GuideStep } from '@/components/Guide/types'
//点击电视机按钮引导状态
const status = ref(false)
const status2 = ref(false)
const status3 = ref(false)
type GuideChildNode = {
id: number
label: string
icon: string
routeName: RouteRecordName
}
const stepIndex = ref(0)
type GuideNode = {
id: number
num: string
label: string
x: number
y: number
hasChildren: boolean
routeName?: RouteRecordName
children?: GuideChildNode[]
}
const handleComplete = () => {
status.value = false
const homeGuideShow = ref(true)
const unlockedHomeGuideShow = ref(false)
const elementGuideShow = ref(false)
const unlockedHomeGuideCompleted = ref(false)
const homeGuideSteps: GuideStep[] = [
{
id: 'home-guide-navigation',
content: '点击新手导航按钮,可以展开/收起新手导航面板!',
positionOffsetY: '50%',
highlightSelector: '.newUserGuide',
audioSrc: getPublicPath('audio/py_0_005.MP3')
},
{
id: 'home-guide-locked-modules',
content: '新手导航模块全部完成后才能解锁三大进阶模块哦~',
highlightSelector: '.newUserGuide',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_006.MP3')
},
{
id: 'home-guide-module-switcher',
content: '舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
highlightSelector: '.module-switcher-guide',
positionOffsetY: '85%',
audioSrc: getPublicPath('audio/py_0_007.MP3')
},
{
id: 'home-guide-tv',
content: '再点击右侧屏幕就会开启对应的板块学习哦!',
highlightSelector: '.tvGuide',
positionOffsetY: '80%',
audioSrc: getPublicPath('audio/py_0_008.MP3')
},
{
id: 'home-guide-skill',
content: '点击技能树,可以一键回顾新手导航和三大创作板块所有的知识点哦!',
highlightSelector: '.skill-tree',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_009.MP3')
}
]
const unlockedHomeGuideSteps: GuideStep[] = [
{
id: 'home-guide-welcome',
content: '欢迎来到新手村!在这里,我会带着你解锁短视频创作的核心基本功!',
audioSrc: getPublicPath('audio/py_1_001.MP3')
},
{
id: 'home-guide-map',
content: '我们会读懂构图、用好光线、灵活运镜,了解基础拍摄技巧和剪映软件的基本操作。',
audioSrc: getPublicPath('audio/py_1_002.MP3')
},
{
id: 'home-guide-start-node',
content: '选择任意节点开始学习~',
highlightSelector: '.guide-content',
positionOffsetY: '20%',
audioSrc: getPublicPath('audio/py_1_003.MP3')
}
]
const elementGuideSteps: GuideStep[] = [
{
id: 'home-guide-start-element',
content: '选择任意要素开始学习~',
highlightSelector: '.btn',
positionOffsetY: '20%',
audioSrc: getPublicPath('audio/py_1_004.MP3')
}
]
const handleHomeGuideComplete = () => {
homeGuideShow.value = false
}
const handleUnlockedHomeGuideComplete = () => {
unlockedHomeGuideShow.value = false
}
const handleElementGuideComplete = () => {
elementGuideShow.value = false
unlockedHomeGuideCompleted.value = true
}
const router = useRouter()
const user = useUserStore()
const film = useFilmStore()
const vlog = useVlogStore()
const interview = useInterviewStore()
const basicElements = useBasicElementsStore()
// 响应式状态
const guideShow = ref(false)
const selectedModule = ref<'vlog' | 'film' | 'interview'>('film')
const selectedModule = ref<'vlog' | 'film' | 'interview'>('vlog')
const isImageLoading = ref(false)
// 预加载图片映射
@@ -39,7 +141,7 @@ const imageMap = {
}
//获取新手导航引导状态
const isGuideNewUser = computed(() => user.isGuideNewUser)
const isGuideNewUser = computed(() => basicElements.isGuideNewUser)
// 预加载所有图片
const preloadImages = () => {
@@ -52,19 +154,20 @@ const preloadImages = () => {
// 事件处理函数
const handleOpenGuide = () => {
status2.value = true
guideShow.value = true
if (!homeGuideShow.value && isGuideNewUser.value) {
unlockedHomeGuideShow.value = true
}
}
const handleCloseGuide = () => {
guideShow.value = false
if (isGuideNewUser.value) {
user.setIsGuideNewUser(false)
basicElements.setIsGuideNewUser(false)
}
}
const handleModuleChange = (module: 'vlog' | 'film' | 'interview') => {
status.value = true
if (selectedModule.value === module) return
isImageLoading.value = true
@@ -77,7 +180,7 @@ const handleModuleChange = (module: 'vlog' | 'film' | 'interview') => {
}
const handleClick = () => {
if (selectedModule.value === 'interview') return
// if (selectedModule.value === 'interview') return
// console.log(selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1))
router.push({
//首字母大写
@@ -90,35 +193,28 @@ const selectedImage = computed(() => {
return getBackgroundImage(imageMap[selectedModule.value])
})
const hasFinishedAllHomeGuides = computed(() => unlockedHomeGuideCompleted.value)
const isSkillDisabled = computed(() => !hasFinishedAllHomeGuides.value)
const handleSkill = () => {
if (isSkillDisabled.value) return
router.push({
name: 'Skill'
})
}
// 清除所有与课程相关的缓存(Film / Vlog / User)
// 清除所有与课程相关的缓存
const handleClearCache = () => {
// 直接通过 $patch 重置各模块的状态,避免热更新导致的方法缺失问题
film.$patch({
isGuideFilmTask: true,
hasShownGuideFilmTask: false,
isFilmUnlock: false,
FilmUnlockProgress: 0
})
vlog.$patch({
isGuideVlogTask: true,
isVlogUnlock: false,
vlogUnlockProgress: 0
})
user.$patch({
isGuideNewUser: true
})
film.resetFilmState()
vlog.resetVlogState()
interview.resetInterviewState()
basicElements.resetBasicElementsState()
user.resetUserState()
}
// 新手导航数据
const nodes = [
const nodes: GuideNode[] = [
{
id: 1,
num: '1',
@@ -156,33 +252,31 @@ const nodes = [
const showChildrenNodeId = ref<number | null>(null)
// 处理节点点击
const handleNodeClick = (node: any, event?: MouseEvent) => {
if (event) {
event.stopPropagation()
}
const handleNodeClick = (node: GuideNode, event?: MouseEvent) => {
event?.stopPropagation()
if (node.hasChildren) {
// 如果点击的是有子节点的节点,切换显示状态
if (showChildrenNodeId.value === node.id) {
showChildrenNodeId.value = null
} else {
showChildrenNodeId.value = node.id
status3.value = true
}
} else {
// 如果点击的是没有子节点的节点,直接跳转
if (!node.hasChildren) {
if (node.routeName) {
router.push({ name: node.routeName })
}
return
}
if (showChildrenNodeId.value === node.id) {
showChildrenNodeId.value = null
return
}
showChildrenNodeId.value = node.id
if (!unlockedHomeGuideShow.value && !unlockedHomeGuideCompleted.value) {
elementGuideShow.value = true
}
}
// 处理子节点点击
const handleChildNodeClick = (childNode: any, event: MouseEvent) => {
const handleChildNodeClick = (childNode: GuideChildNode, event: MouseEvent) => {
event.stopPropagation()
if (childNode.routeName) {
router.push({ name: childNode.routeName })
}
router.push({ name: childNode.routeName })
}
// 点击外部关闭子节点
@@ -194,26 +288,28 @@ const handleClickOutside = () => {
onMounted(() => {
preloadImages()
})
onUnmounted(() => {
// 清理工作
})
</script>
<template>
<div class="w-full h-full relative">
<!-- 图片引导 -->
<div
class="absolute top-0 left-0 w-full h-full z-50"
v-if="stepIndex < 4 && isGuideNewUser"
@click="stepIndex++"
>
<img v-if="stepIndex === 0" :src="getStaticImage('home/1')" alt="" />
<img v-else-if="stepIndex === 1" :src="getStaticImage('home/2')" alt="" />
<img v-else-if="stepIndex === 2" :src="getStaticImage('home/3')" alt="" />
<img v-else-if="stepIndex === 3" :src="getStaticImage('home/4')" alt="" />
<div v-else class="absolute top-0 left-0 w-full h-full bg-black/50"></div>
</div>
<GuideSteps
v-if="isGuideNewUser && homeGuideShow"
:steps="homeGuideSteps"
position-offset-y="82%"
@complete="handleHomeGuideComplete"
/>
<GuideSteps
v-if="unlockedHomeGuideShow"
:steps="unlockedHomeGuideSteps"
position-offset-y="82%"
@complete="handleUnlockedHomeGuideComplete"
/>
<GuideSteps
v-if="elementGuideShow"
:steps="elementGuideSteps"
position-offset-y="82%"
@complete="handleElementGuideComplete"
/>
<!-- 背景图片 -->
<div
@@ -229,11 +325,15 @@ onUnmounted(() => {
class="w-10 cursor-pointer mx-2"
src="@/assets/svg/clear.svg"
@click="handleClearCache"
/>清空缓存。
/>
清空缓存。
</div>
<!-- 选择模块 -->
<div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
<div
class="module-switcher-guide absolute w-[1380px] h-[600px] top-[460px] left-[calc(50%-690px)] pointer-events-none"
></div>
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
</div>
@@ -247,14 +347,17 @@ onUnmounted(() => {
<!-- 技能树 按钮 -->
<ImgButton
v-if="!isGuideNewUser"
@click="handleSkill"
:image="IMAGE_PATHS.ACTION.SKILLS_BTN"
class="absolute bottom-0 right-0 z-30"
:disabled="isSkillDisabled"
class="absolute bottom-0 right-0 z-30 skill-tree"
/>
<!-- 新手指导遮罩 -->
<div v-if="isGuideNewUser" class="absolute inset-0 bg-black/50 z-10"></div>
<div
v-if="isGuideNewUser"
:class="['absolute inset-0 z-10', homeGuideShow ? 'bg-black/0' : 'bg-black/50']"
></div>
<!-- 新手指导 -->
<div
@@ -281,7 +384,7 @@ onUnmounted(() => {
<!-- 新手导航内容 -->
<div class="relative w-[880px] h-[400px] mt-20">
<div class="w-full h-full relative" @click="handleClickOutside">
<div class="w-full h-full relative guide-content" @click="handleClickOutside">
<!-- 标题区域 -->
<div class="text-center mt-10 mb-8">
<h2 class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg">
@@ -395,49 +498,5 @@ onUnmounted(() => {
</div>
</div>
</div>
<!-- 引导与高亮 -->
<GuideSteps
v-if="status"
highlight-mode="gradient"
highlight-selector=".tvGuide"
position-offset-y="85%"
@complete="handleComplete"
>
<GuideStepsItem>
<div
class="flex items-center justify-start"
v-if="selectedModule === 'interview' || selectedModule === 'vlog'"
>
[{{
selectedModule == 'interview'
? '访谈-聆听与对话'
: selectedModule == 'vlog'
? 'vlog-创作与表达'
: ''
}}]模块建设中...
</div>
<div class="flex items-center justify-start" v-else>
请点击<img
class="w-20"
:src="getStaticImage(imageMap[selectedModule])"
alt=""
/>,继续学习[{{
selectedModule == 'film'
? '电影-创作与表达'
: selectedModule == 'vlog'
? 'vlog-创作与表达'
: ''
}}]模块!
</div>
</GuideStepsItem>
</GuideSteps>
<GuideSteps :visible="status2" highlight-selector=".btn-items" position-offset-y="20%">
<GuideStepsItem> 选择任意节点开始学习 </GuideStepsItem>
</GuideSteps>
<GuideSteps :visible="status3" highlight-selector=".btn" position-offset-y="20%">
<GuideStepsItem> 选择任意要素开始学习 </GuideStepsItem>
</GuideSteps>
</div>
</template>
+113
View File
@@ -0,0 +1,113 @@
import { defineComponent, ref } from 'vue'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale'
const CourseFrameGuide = () => (
<div class="flex items-center justify-start">
点击
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)} alt="" />
查看课程结构,快速了解每个模块的学习内容。
</div>
)
const OperateGuide = () => (
<div class="flex items-center justify-start">
<img class="w-20 inline" src={getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE)} alt="" />
包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)
</div>
)
const StartFilmingGuide = () => (
<div class="flex items-center justify-start">
点击
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)} alt="" />
正式进入课程,开启你的短视频拍摄之旅。
</div>
)
export default defineComponent({
name: 'HomeView',
setup() {
const guideSteps = ref([
{
id: 'step-1',
title: '',
content: '欢迎来到《短视频达人养成记》网络课程的学习!',
highlightSelector: null,
audioSrc: getPublicPath('audio/py_0_001.MP3'),
positionOffsetY: '80%'
},
{
id: 'step-2',
title: '',
content: CourseFrameGuide,
highlightSelector: '.kecheng-wrapper',
audioSrc: getPublicPath('audio/py_0_002.MP3'),
positionOffsetY: '40%'
},
{
id: 'step-3',
title: '',
content: OperateGuide,
highlightSelector: '.zhiying-wrapper',
audioSrc: getPublicPath('audio/py_0_003.MP3'),
positionOffsetY: '40%'
},
{
id: 'step-4',
title: '',
content: StartFilmingGuide,
highlightSelector: '.start-filming-wrapper',
audioSrc: getPublicPath('audio/py_0_004.MP3'),
positionOffsetY: '40%'
}
])
const showGuide = ref(true)
const { scale, offsetX, offsetY } = useScale()
const handleComplete = () => {
console.log('handleComplete')
}
const openMindMap = () => {
ImagePreview({
images: [getStaticImage('maps/dt')],
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
})
}
return () => (
<div class="w-full h-full relative">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: getBackgroundImage(IMAGE_PATHS.HOME_BG) }}
/>
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton image="home/action-btn" path={{ name: 'Action' }} />
</div>
<div class="absolute right-20 bottom-36 kecheng-wrapper">
<ImgButton image={IMAGE_PATHS.BUTTONS.COURSE_FRAME} onClick={openMindMap} />
</div>
<div class="absolute right-20 bottom-10 zhiying-wrapper">
<ImgButton image={IMAGE_PATHS.BUTTONS.OPERATE_GUIDE} />
</div>
<GuideSteps
visible={showGuide.value}
steps={guideSteps.value}
highlightPadding={12}
highlightRadius={16}
keyboardNavigation={true}
highlightMode="border"
onComplete={handleComplete}
/>
</div>
)
}
})
-327
View File
@@ -1,327 +0,0 @@
<script lang="ts" setup>
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, ImagePreview, Overlay } from '@/components'
import { computed, ref, h, nextTick } from 'vue'
import MindMap from 'simple-mind-map'
// XMind 解析工具(用于将 .xmind 文件解析为思维导图数据)
// @ts-expect-error: 官方包未提供类型声明
import xmindParser from 'simple-mind-map/src/parse/xmind.js'
import { useScale } from '@/composables/useScale'
// 第一个步骤的内容组件 - 使用函数组件形式
const Step1Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
'点击',
h('img', {
class: 'w-20',
src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
alt: ''
}),
' 查看课程结构,快速了解每个模块的学习内容。'
]
)
const Step2Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
h('img', {
class: 'w-20 inline',
src: getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE),
alt: ''
}),
'包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)'
]
)
const Step3Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
'点击',
h('img', {
class: 'w-20',
src: getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING),
alt: ''
}),
' 正式进入课程,开启你的短视频拍摄之旅。'
]
)
// const Step3Content = () =>
// h(
// 'div',
// {
// class: 'flex items-center justify-start'
// },
// [
// '点击',
// h('img', {
// class: 'w-20',
// src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
// alt: ''
// }),
// ' 查看课程结构,快速了解每个模块的学习内容。'
// ]
// )
const guideSteps = ref([
{
id: 'step-1',
title: '',
content: Step1Content,
highlightSelector: '.kecheng-wrapper'
},
{
id: 'step-2',
title: '',
content: Step2Content,
highlightSelector: '.zhiying-wrapper'
},
{
id: 'step-3',
title: '',
content: Step3Content,
highlightSelector: '.start-filming-wrapper'
}
])
const showGuide = ref(true)
const currentGuideStep = ref(0)
// 当前步骤的高亮选择器
const currentHighlightSelector = computed(() => {
if (showGuide.value && guideSteps.value[currentGuideStep.value]) {
return guideSteps.value[currentGuideStep.value].highlightSelector || null
}
return null
})
const handleGuideStepChange = (step: any, index: number) => {
currentGuideStep.value = index
}
const handleComplete = () => {
console.log('handleComplete')
}
// 课程框架思维导图弹层
const showMindMap = ref(false)
const mindMapContainer = ref<HTMLElement | null>(null)
let mindMapInstance: any = null
// 缓存解析后的思维导图数据,避免重复请求
let cachedMindMapData: any = null
const initMindMap = async () => {
if (!mindMapContainer.value) return
// 等待容器完全渲染并获取实际尺寸
await new Promise(resolve => setTimeout(resolve, 100))
console.log('容器尺寸:', {
width: mindMapContainer.value.clientWidth,
height: mindMapContainer.value.clientHeight
})
// 确保容器有尺寸
if (mindMapContainer.value.clientWidth === 0 || mindMapContainer.value.clientHeight === 0) {
console.error('容器尺寸为 0,等待渲染')
await new Promise(resolve => setTimeout(resolve, 200))
}
// 初始化思维导图实例(每次都重新创建,避免 DOM 销毁问题)
mindMapInstance = new MindMap({
el: mindMapContainer.value,
data: {
data: { text: '加载中...' },
children: []
}
})
// 创建后立即调用 resize
if (typeof mindMapInstance.resize === 'function') {
mindMapInstance.resize()
}
try {
// 如果有缓存数据,直接使用
if (cachedMindMapData) {
console.log('使用缓存数据')
mindMapInstance.setData(cachedMindMapData)
} else {
// 首次加载,从服务器获取文件
console.log('首次加载,获取 XMind 文件')
const res = await fetch(getPublicPath('maps/dsp_kecheng.xmind'))
if (!res.ok) {
throw new Error(`加载 XMind 失败: ${res.status}`)
}
const blob = await res.blob()
// 将远程文件转换成 File 对象,供解析器使用
const file = new File([blob], 'dsp_kecheng.xmind', {
type: 'application/vnd.xmind.workbook'
})
// 使用 simple-mind-map 提供的解析工具解析 xmind 文件
const data = await (xmindParser as any).parseXmindFile(file)
// 缓存数据
cachedMindMapData = data
// 将解析后的数据设置到思维导图实例
mindMapInstance.setData(data)
}
// 数据加载后再次调用 resize
if (typeof mindMapInstance.resize === 'function') {
mindMapInstance.resize()
}
// 等待渲染完成后调用视图居中
await new Promise(resolve => setTimeout(resolve, 100))
centerMindMapView()
} catch (error) {
console.error('加载 XMind 文件失败:', error)
}
}
// 居中并缩放视图的辅助函数
const centerMindMapView = () => {
if (!mindMapInstance) return
const anyInstance = mindMapInstance as any
console.log('尝试居中视图')
// 尝试多种方法来居中视图
if (anyInstance.view) {
// 方法1: fit - 自动缩放并居中
if (typeof anyInstance.view.fit === 'function') {
console.log('调用 view.fit()')
anyInstance.view.fit()
}
// 方法2: setScale + translateX/Y 来手动居中
else if (typeof anyInstance.view.setScale === 'function') {
console.log('手动设置缩放和位置')
anyInstance.view.setScale(1)
anyInstance.view.translateX(0)
anyInstance.view.translateY(0)
}
// 方法3: fitCenter
else if (typeof anyInstance.view.fitCenter === 'function') {
console.log('调用 view.fitCenter()')
anyInstance.view.fitCenter()
}
}
}
const { scale, offsetX, offsetY } = useScale()
const openMindMap = () => {
ImagePreview({
images: [getStaticImage('maps/dt')],
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
})
// showMindMap.value = true
// nextTick(() => {
// void initMindMap()
// })
}
const handleMindMapClose = () => {
showMindMap.value = false
// 销毁思维导图实例,避免 DOM 销毁后引用失效
if (mindMapInstance && typeof mindMapInstance.destroy === 'function') {
mindMapInstance.destroy()
}
mindMapInstance = null
// 保留缓存数据,下次打开时直接使用,不需要重新请求
}
</script>
<template>
<div class="w-full h-full relative">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.HOME_BG) }"
></div>
<!-- 按钮 -->
<!-- 开始拍摄 -->
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton image="home/action-btn" :path="{ name: 'Action' }" />
</div>
<!-- 课程框架 -->
<div class="absolute right-20 bottom-36 kecheng-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" @click="openMindMap" />
</div>
<!-- 操作指引 -->
<div class="absolute right-20 bottom-10 zhiying-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.OPERATE_GUIDE" />
</div>
<GuideSteps
:visible="showGuide"
:steps="guideSteps"
:highlight-selector="currentHighlightSelector"
:highlight-padding="12"
:highlight-radius="16"
:keyboard-navigation="true"
highlight-mode="border"
position-offset-y="40%"
@complete="handleComplete"
@step-change="handleGuideStepChange"
/>
<!-- 课程框架思维导图弹层 -->
<!-- <Overlay :visible="showMindMap" @close="handleMindMapClose">
<div class="bg-white rounded-xl shadow-xl w-[100vw] h-[100vh] flex flex-col overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b flex-shrink-0">
<div class="font-semibold text-lg">短视频达人课程框架</div>
<button
class="text-gray-500 hover:text-gray-700 text-2xl leading-none px-2"
type="button"
@click="handleMindMapClose"
>
×
</button>
</div>
<div class="flex-1 overflow-hidden">
<div ref="mindMapContainer" class="mindmap-container"></div>
</div>
</div>
</Overlay> -->
</div>
</template>
<style scoped>
.mindmap-container {
width: 100%;
height: 100%;
position: relative;
}
/* 确保 simple-mind-map 生成的 SVG 填充整个容器 */
.mindmap-container :deep(svg) {
width: 100% !important;
height: 100% !important;
}
.mindmap-container :deep(.smm-container) {
width: 100% !important;
height: 100% !important;
}
</style>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video } from '@/components'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
const router = useRouter()
const playing = ref(false)
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
class="w-full h-full"
:src="getPublicPath('video/editing/yingxiao_batch.mp4')"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
/>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -0,0 +1,38 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/调色-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('interview/img/调色实践.png')" alt="" />
<ImgButton
class="absolute top-[1180px] left-[51.5%] -translate-x-1/2"
image="after-editing/color/调色实践"
@click="router.push({ name: 'InterviewColor' })"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+12 -7
View File
@@ -5,7 +5,6 @@ import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { ref } from 'vue'
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
import { useInterviewStore } from '@/stores'
const selected = ref<number | undefined>(1)
const btnState = ref(false)
@@ -25,11 +24,11 @@ const adjustmentList = ref([
const colorBarList = ref([
{ id: 1, name: '原图', filter: 'none' },
{ id: 2, name: '暖阳', filter: 'sepia(0.3) contrast(1.1) brightness(1.1)' },
{ id: 2, name: '暖阳', filter: 'saturate(2.5) contrast(2.1)' },
{ id: 3, name: '冷调', filter: 'hue-rotate(180deg) contrast(0.9)' },
{ id: 4, name: '复古', filter: 'sepia(0.6) contrast(1.2) brightness(0.9)' },
{ id: 5, name: '黑白', filter: 'grayscale(1)' },
{ id: 6, name: '鲜艳', filter: 'saturate(1.5) contrast(1.1)' }
{ id: 6, name: '鲜艳', filter: 'sepia(0.1) saturate(2.5) contrast(2.1) brightness(1)' }
])
// 获取指定参数的值
@@ -117,10 +116,8 @@ const handleTryOther = () => {
}
const handleFinish = () => {
useInterviewStore().setInterviewUnlockProgress(4)
router.push({
name: 'Interview'
name: 'Film'
})
}
@@ -157,7 +154,7 @@ const changeColor = (item: { id: number; name: string }) => {
class="relative w-[1273px] h-[732px] bg-no-repeat bg-cover mt-10 p-2 pb-[26px] pr-[26px]"
:style="{ backgroundImage: getBackgroundImage('after-editing/color/图片框.png') }"
>
<ImageFilter ref="imageFilterRef" :src="getStaticImage('after-editing/color/图片')" />
<ImageFilter ref="imageFilterRef" :src="getStaticImage('after-editing/color/film_img')" />
<!-- 开启对比 -->
<ImgButton
@@ -258,6 +255,14 @@ const changeColor = (item: { id: number; name: string }) => {
<!-- title -->
<img class="absolute top-0 left-0" :src="getStaticImage('after-editing/color/标题-调色实践')" />
<!-- 回到顶部按钮 - 最后一页显示 -->
<p class="absolute left-40 bottom-10 flex items-center">
<img class="rotate-0 w-28 mr-4" src="@/assets/svg/deng.svg" />
<span class="text-white text-2xl font-bold">
推荐参数:亮度 90、对比度 100、饱和度 100、曝光 - 90、色调 0、锐化 100、模糊 0
</span>
</p>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" @close="handleModalClose">
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
+71 -66
View File
@@ -1,69 +1,63 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
// import { GuideSteps } from '@/components/Guide'
import { StepNavigation } from '@/components/Guide'
import { IMAGE_PATHS } from '@/utils'
import { Video } from '@/components'
const router = useRouter()
// 导入视频文件
import videoBasic from '/video/vlog_zhizuobianjiaojing_jichujianji.mp4'
import videoSoundeffect from '/video/vlog_renwuqujing_yingxiaochuli.mp4'
import videoPackaging from '/video/vlog_renwuqujing_baozhuang.mp4'
import videoCopyright from '/video/vlog_renwuqujing_banquanguifan.mp4'
const videoList = [videoBasic, videoSoundeffect, videoPackaging, videoCopyright]
const videoRef = ref<HTMLVideoElement>()
const navigationSteps = [
{
id: 'step1',
label: '后期制作',
content: '声画剪辑流程',
type: 'post' as const
},
{
id: 'step2',
label: '后期制作',
content: 'J-cut/L-cut运用',
type: 'post' as const
},
{
id: 'step3',
label: '后期制作',
content: '【Q3】节奏剪辑技巧',
type: 'post' as const
}
]
const currentVideoIndex = ref(0)
const playing = ref(false)
const showStepNavigation = ref(false)
onMounted(() => {
// 确保视频元素加载并自动播放
if (videoRef.value) {
videoRef.value.src = videoList[currentVideoIndex.value]
videoRef.value.load()
// 尝试自动播放
const playPromise = videoRef.value.play()
if (playPromise !== undefined) {
playPromise.catch(error => {
// 自动播放失败,可能是浏览器策略阻止,需要用户交互
console.log('自动播放被阻止:', error)
})
}
// 监听视频结束事件
videoRef.value.addEventListener('ended', playNextVideo)
}
})
// 播放下一个视频
const playNextVideo = () => {
if (currentVideoIndex.value < videoList.length - 1) {
currentVideoIndex.value++
if (videoRef.value) {
videoRef.value.src = videoList[currentVideoIndex.value]
videoRef.value.load()
// 自动播放下一个视频
const playPromise = videoRef.value.play()
if (playPromise !== undefined) {
playPromise.catch(error => {
console.log('自动播放被阻止:', error)
})
}
}
if (currentVideoIndex.value >= videoList.length - 1) {
playing.value = false
return
}
currentVideoIndex.value++
nextTick(() => {
playing.value = true
})
}
// 监听当前视频索引变化
watch(currentVideoIndex, newIndex => {
if (videoRef.value) {
videoRef.value.src = videoList[newIndex]
videoRef.value.load()
}
})
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
// showStepNavigation.value = false
nextTick(() => {
playing.value = true
})
}
const handleClose = () => {
router.back()
@@ -71,29 +65,40 @@ const handleClose = () => {
</script>
<template>
<div>
<!--使用动态绑定和ref-->
<video
ref="videoRef"
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
:src="videoList[currentVideoIndex]"
type="video/mp4"
controls
preload="auto"
class="video-player"
autoplay
></video>
v-model="playing"
:muted="false"
:volume="1"
@ended="playNextVideo"
/>
<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 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-blue-500/80 text-white transition-all duration-200 hover:bg-blue-500"
title="步骤导航"
@click="showStepNavigation = true"
>
<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>
<StepNavigation
:visible="showStepNavigation"
:background-image="''"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
@step-click="handleStepClick"
@close="showStepNavigation = false"
/>
</div>
</template>
<style scoped>
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
@@ -0,0 +1,33 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video } from '@/components'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
const router = useRouter()
const playing = ref(false)
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
class="w-full h-full"
:src="getPublicPath('video/on-site/dygmk_zlsxhbj.mp4')"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
/>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
+43 -23
View File
@@ -3,7 +3,8 @@ import { getStaticImage } from '@/utils'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage } from '@/utils'
defineOptions({ name: 'VlogMenu' })
import { useInterviewStore } from '@/stores'
defineOptions({ name: 'InterviewMenu' })
/**
* 菜单项 (前期准备、中期拍摄、后期剪辑)
@@ -33,13 +34,13 @@ const menuItems = ref([
label: '资源筹备',
routeName: 'InterviewResource',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '创意构想',
routeName: 'InterviewCreative',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '脚本撰写',
@@ -51,32 +52,32 @@ const menuItems = ref([
label: '分镜制作',
routeName: 'InterviewStoryboard',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '设备准备',
routeName: 'InterviewMidtermEquipment',
routeName: 'InterviewEquipment',
parentValue: 2,
disabled: false
},
{
label: '实地取景',
routeName: 'InterviewMidtermShooting',
routeName: 'InterviewShooting',
parentValue: 2,
disabled: false
},
{
label: '现场拍摄',
routeName: 'Interview现场拍摄',
routeName: 'InterviewOnsiteShooting',
parentValue: 2,
disabled: true
},
{
label: '素材管理',
routeName: 'Interview素材管理',
parentValue: 3,
disabled: true
disabled: false
},
// {
// label: '素材管理',
// routeName: 'Interview素材管理',
// parentValue: 3,
// disabled: true
// },
{
label: '剪辑',
routeName: 'InterviewEditing',
@@ -85,15 +86,21 @@ const menuItems = ref([
},
{
label: '调色',
routeName: 'InterviewColor',
routeName: 'InterviewColorPractice',
parentValue: 3,
disabled: false
},
{
label: '音频',
routeName: 'InterviewAudio',
parentValue: 3,
disabled: false
},
{
label: '导出发布',
routeName: 'Interview导出发布',
routeName: 'InterviewExportPublish',
parentValue: 3,
disabled: true
disabled: false
}
])
@@ -102,12 +109,19 @@ const props = defineProps<{
}>()
const router = useRouter()
const interview = useInterviewStore()
const isViewed = (routeName: string) => {
return interview.viewed?.[routeName] === true
}
const currentParentValue = computed<number>(() => {
return props.currentStep
}) // 当前选中的父级菜单项
const currentItemValue = ref<string>('') // 当前选中的子级菜单项
const currentItemValue = computed<string>(() => {
return router.currentRoute.value.name?.toString() ?? ''
})
const getMenuItems = computed(() => {
return menuItems.value.filter(item => item.parentValue === currentParentValue.value)
@@ -115,7 +129,6 @@ const getMenuItems = computed(() => {
const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
if (item.disabled) return
currentItemValue.value = item.routeName
router.push({ name: item.routeName })
}
</script>
@@ -124,9 +137,9 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
<div class="absolute left-20 top-0 bottom-0 w-[520px] pt-28" v-if="currentStep">
<!-- 分类名称 -->
<div class="absolute top-3 left-24 flex items-center space-x-2">
<span class="text-4xl text-[#fbbf24] font-black">{{
category[currentParentValue - 1].label
}}</span>
<span class="text-4xl text-[#fbbf24] font-black">
{{ category[currentParentValue - 1].label }}
</span>
<img class="filter-blue" :src="category[currentParentValue - 1].icon" alt="" />
</div>
<!-- 背景图片 -->
@@ -150,7 +163,14 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 group-hover:opacity-100 transition-opacity duration-300"
:style="{ backgroundImage: getBackgroundImage('components/menu-hover.png') }"
></div>
<span :class="[!item.disabled ? 'menu-glow-text' : '']">{{ item.label }}</span>
<span class="flex items-center gap-2" :class="[!item.disabled ? 'menu-glow-text' : '']">
<span>{{ item.label }}</span>
<img
:class="['w-10', isViewed(item.routeName) ? 'brightness-90' : 'brightness-50']"
src="@/assets/images/midterm/完成.png"
alt=""
/>
</span>
</div>
</template>
</div>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/创意构想-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('interview/img/创意构想.png')" alt="" />
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+26 -8
View File
@@ -1,8 +1,9 @@
<script lang="tsx" setup>
<script lang="tsx" name="Interview" setup>
defineOptions({ name: 'Interview' })
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { ProgressBar, Overlay } from '@/components'
import { useRouter } from 'vue-router'
import { useInterviewStore } from '@/stores'
@@ -12,7 +13,8 @@ const router = useRouter()
const interview = useInterviewStore()
const visible = ref(false)
const currentStep = ref(interview.InterviewUnlockProgress)
const currentStep = computed(() => interview.InterviewUnlockProgress)
const progress = computed(() => interview.progressPercent)
const handleComplete = () => {
// console.log('完成前置条件')
@@ -21,8 +23,7 @@ const handleComplete = () => {
}
const handleStepChange = (step: number) => {
// 处理步骤变化逻辑
currentStep.value = step
interview.setInterviewUnlockProgress(step)
visible.value = true
}
@@ -48,9 +49,19 @@ onMounted(() => {
<!-- 背景图片-center -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BG_CENTER) }"
:style="{ backgroundImage: getBackgroundImage('interview/访谈背景.png') }"
></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"
@@ -66,7 +77,7 @@ onMounted(() => {
<!-- 标题 -->
<div class="absolute top-2 left-1/2 -translate-x-1/2">
<ImgButton :image="IMAGE_PATHS.VLOG.TITLE" />
<ImgButton :image="'interview/访谈-聆听与对话.png'" />
</div>
<!-- 任务取景按钮 -->
@@ -96,13 +107,20 @@ onMounted(() => {
<!-- 进度条 -->
<div class="absolute left-0 bottom-0 w-full">
<ProgressBar :current-step="currentStep" @step-change="handleStepChange" />
<ProgressBar
module-name="interview"
:current-step="currentStep"
:completed-step="interview.completedStep"
@step-change="handleStepChange"
/>
</div>
</Overlay>
<div class="absolute bottom-0 w-full">
<ProgressBar
module-name="interview"
:current-step="currentStep"
:completed-step="interview.completedStep"
@complete="handleComplete"
@step-change="handleStepChange"
/>
@@ -1,7 +1,7 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed } from 'vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -12,38 +12,59 @@ const back = () => {
router.back()
}
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
{
id: 'step1',
content: '先认识 Vlog:点击右侧「1.vlog初识」继续',
highlightSelector: '.img-switch-item--vlog'
},
{
id: 'step2',
content: '这是主要内容区域的边框',
highlightSelector: '.work-border'
}
]
const stepIndex = ref(0)
const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
const currentIndex = ref(0)
const selectedMenuIndex = ref(0)
// 菜单项数据
const menuItems = ref([
{ id: 'phone', name: '手机', disabled: false },
{ id: 'phone-stand', name: '手机支架', disabled: true },
{ id: 'bluetooth-remote', name: '蓝牙遥控器', disabled: true },
{ id: 'stabilizer', name: '稳定器', disabled: true },
{ id: 'props', name: '常用道具', disabled: true },
{ id: 'phone-lens', name: '手机辅助箱头', disabled: true }
])
const equipmentItems = [
{
id: 'camera',
name: '相机 / 摄像机 / 微单',
productImages: [
getStaticImage('equipment/相机产品.png'),
getStaticImage('equipment/摄像机产品.png'),
getStaticImage('equipment/微单产品.png')
],
title: getStaticImage('equipment/相机标题.png'),
text: getStaticImage('equipment/相机内容.png')
},
{
id: 'tripod',
name: '三脚架',
productImages: [getStaticImage('equipment/三角架产品.png')],
title: getStaticImage('equipment/三脚架标题.png'),
text: getStaticImage('equipment/三脚架内容.png')
},
{
id: 'light',
name: '灯光',
productImages: [
getStaticImage('equipment/led补光灯.png'),
getStaticImage('equipment/环形灯产品.png')
],
title: getStaticImage('equipment/灯标题.png'),
text: getStaticImage('equipment/灯内容.png')
}
]
const phoneList = ref([
{ phoneImg: getStaticImage('equipment/设备-手机1.png') },
{ phoneImg: getStaticImage('equipment/设备-手机2.png') }
])
const menuItems = ref(
equipmentItems.map(item => ({ id: item.id, name: item.name, disabled: false }))
)
const platetitleList = ref([
{ title: getStaticImage('equipment/标题-苹果.png') },
{ title: getStaticImage('equipment/标题-安卓小米.png') }
])
const currentEquipment = computed(() => equipmentItems[selectedMenuIndex.value])
const currentProductImages = computed(() => currentEquipment.value.productImages)
const platetextList = ref([
{ text: getStaticImage('equipment/内容-苹果.png') },
{ text: getStaticImage('equipment/内容-安卓小米.png') }
])
const hiddenbarclick = () => {
hiddenbar.value = !hiddenbar.value
}
@@ -55,11 +76,12 @@ const selectMenuItem = (index: number, item: { id: string; name: string; disable
return
}
selectedMenuIndex.value = index
currentIndex.value = 0
}
// 下一张图片
const nextPhone = () => {
if (currentIndex.value < phoneList.value.length - 1) {
if (currentIndex.value < currentProductImages.value.length - 1) {
currentIndex.value++
}
}
@@ -70,14 +92,6 @@ const prevPhone = () => {
currentIndex.value--
}
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
</script>
<template>
<div class="equipment-index">
@@ -139,12 +153,14 @@ const currentHighlight = computed(() =>
<img src="@/assets/images/equipment/按钮-左滑.png" alt="" />
</div>
<div class="equipment-phoneImg">
<img :src="phoneList[currentIndex].phoneImg" alt="" />
<img :src="currentProductImages[currentIndex]" alt="" />
</div>
<div
class="equipment-rightbutton"
@click="nextPhone"
:style="{ visibility: currentIndex < phoneList.length - 1 ? 'visible' : 'hidden' }"
:style="{
visibility: currentIndex < currentProductImages.length - 1 ? 'visible' : 'hidden'
}"
>
<img src="@/assets/images/equipment/按钮-右滑.png" alt="" />
</div>
@@ -155,11 +171,11 @@ const currentHighlight = computed(() =>
</div>
<div class="title">
<img :src="platetitleList[currentIndex].title" alt="" />
<img :src="currentEquipment.title" alt="" />
</div>
<div class="plate-content">
<div class="text">
<img :src="platetextList[currentIndex].text" alt="" />
<img :src="currentEquipment.text" alt="" />
</div>
</div>
</div>
@@ -176,10 +192,8 @@ const currentHighlight = computed(() =>
<!-- 引导步骤 -->
<GuideSteps
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem>
@@ -0,0 +1,121 @@
<script lang="ts" setup>
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton, Video } from '@/components'
import { StepNavigation } from '@/components/Guide'
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
const router = useRouter()
const playing = ref(false)
const showStepNavigation = ref(false)
const currentVideoIndex = ref(0)
const navigationSteps = [
{
id: 'q1-shoot-material',
label: '中期拍摄',
content: '素材怎么拍',
type: 'shooting' as const
},
{
id: 'q1-required-shots',
label: '中期拍摄',
content: '必拍镜头有哪些',
type: 'shooting' as const
},
{
id: 'q2-moving-shot',
label: '中期拍摄',
content: '移动镜头的作用',
type: 'shooting' as const
},
{
id: 'q2-close-up',
label: '中期拍摄',
content: '特写镜头的作用',
type: 'shooting' as const
}
]
const videoList = [
getPublicPath('video/on-site/dygmk_mmps_batch.mp4'),
getPublicPath('video/on-site/dygmk_hgydjt_batch.mp4'),
getPublicPath('video/on-site/dygmk_jzxjj_batch.mp4'),
getPublicPath('video/on-site/dygmk_czsjx_batch.mp4')
]
const currentVideo = computed(() => videoList[currentVideoIndex.value])
const back = () => {
router.back()
}
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
// showStepNavigation.value = false
nextTick(() => {
playing.value = true
})
}
const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const handleEnded = () => {
if (currentVideoIndex.value >= videoList.length - 1) {
playing.value = false
return
}
currentVideoIndex.value++
nextTick(() => {
playing.value = true
})
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
:src="currentVideo"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
@ended="handleEnded"
>
<template #overlay>
<ImgButton
class="absolute right-0 top-0 z-20 pointer-events-auto"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<button
class="absolute right-4 top-1/2 z-20 -translate-y-1/2 w-12 h-12 rounded-full bg-blue-500/80 text-white transition-all duration-200 hover:bg-blue-500 pointer-events-auto flex items-center justify-center"
title="资源列表"
@click="showStepNavigation = true"
>
<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>
</template>
</Video>
<StepNavigation
:visible="showStepNavigation"
:background-image="getStaticImage('work/vlog背景全屏待定')"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
</div>
</template>
@@ -9,7 +9,6 @@ import StartOverlay from '../components/StartOverlay.vue'
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useInterviewStore } from '@/stores'
const router = useRouter()
const start = ref(false)
@@ -90,7 +89,6 @@ const captureScene = async () => {
*/
const handleComplete = () => {
showGuide.value = false
useInterviewStore().setInterviewUnlockProgress(3)
router.push({
name: 'Interview'
})
@@ -252,7 +250,7 @@ const handleComplete = () => {
<!-- 引导步骤 -->
<GuideSteps v-if="showGuide" positionOffsetY="80%" @complete="handleComplete">
<GuideStepsItem> 实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~ </GuideStepsItem>
<GuideStepsItem>实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~</GuideStepsItem>
</GuideSteps>
</div>
</template>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/资源筹备-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('interview/img/资源筹备.png')" alt="" />
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
Whitespace-only changes.
+20 -64
View File
@@ -1,88 +1,44 @@
<script lang="ts" setup>
import { IMAGE_PATHS } from '@/utils'
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { ref } from 'vue'
import sample from './sample.vue'
import writing from './writing.vue'
import { useRouter } from 'vue-router'
import { getStaticImage } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video } from '@/components'
const router = useRouter()
const writingtext = ref('撰写格式')
const hiddenbar = ref(false)
const formatClick = () => {
writingtext.value = '撰写格式'
}
const sampleanClick = () => {
writingtext.value = '样例展示'
// router.push({ name: 'sample' })
}
const hiddenbarclick = () => {
hiddenbar.value = !hiddenbar.value
}
const back = () => {
// 跳转设备准备
router.back()
}
</script>
<template>
<div class="relative w-full h-full">
<!-- 返回按钮 -->
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-10"
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<!-- 侧边栏 -->
<div
:class="[
'absolute h-full left-0 top-0 transition-transform duration-300 z-10',
hiddenbar ? 'translate-x-[-400px]' : 'translate-x-[0px]'
]"
>
<img class="absolute inset-0 -z-10" src="@/assets/images/equipment/侧边导航栏.png" alt="" />
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 脚本撰写 -->
<img class="" :src="getStaticImage(IMAGE_PATHS.VLOG.SCRIPTINGTITLE)" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/脚本编写-标题.png')" alt="" />
<img
class="absolute bottom-4 left-6"
src="@/assets/images/vlog/制作变焦镜/左边导航栏/ai分镜工具.png"
alt=""
/>
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('interview/img/脚本编写.png')" alt="" />
<div class="absolute left-0 top-36">
<ImgButton
:image="
writingtext == '撰写格式' ? IMAGE_PATHS.VLOG.FORMATLIANG : IMAGE_PATHS.VLOG.FORMATAN
"
@click="formatClick"
/>
<ImgButton
:image="
writingtext == '样例展示' ? IMAGE_PATHS.VLOG.SAMPLELANG : IMAGE_PATHS.VLOG.SAMPLEAN
"
@click="sampleanClick"
<div class="absolute top-[250px] left-[8%] w-[960px]">
<Video
:src="getPublicPath('video/editing/color_jianji_batch.mp4')"
playsinline
controls
:muted="false"
:volume="1"
/>
</div>
</div>
<div
:class="[
'absolute transition-transform duration-200 top-1/2 -translate-y-1/2 z-20',
hiddenbar ? ' translate-x-[-38px]' : 'absolute translate-x-[270px]'
]"
@click="hiddenbarclick()"
>
<ImgButton :image="hiddenbar ? 'equipment/按钮-展开' : 'equipment/按钮-收缩'" />
</div>
<div class="absolute w-full h-full left-0 top-0">
<writing v-if="writingtext == '撰写格式'"></writing>
<sample v-else-if="writingtext == '样例展示'"></sample>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -1,175 +0,0 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale'
import { useInterviewStore } from '@/stores'
const router = useRouter()
const showDoubao = ref(false)
const interview = useInterviewStore()
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
]
const stepIndex = ref(1)
const back = () => {
router.back()
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
const clickdoubao = () => {
interview.setInterviewUnlockProgress(2)
router.push({
name: 'Interview'
})
}
function openImages(urls: string[], startIndex = 0) {
ImagePreview({
images: urls,
startIndex,
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value }),
onClose: () => {
showDoubao.value = true
}
})
}
const { scale, offsetX, offsetY } = useScale()
onMounted(() => {
openImages([getStaticImage('vlog/脚本切图/我的志愿者手记拍摄脚本.png')])
})
</script>
<template>
<div class="writing-index">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat writing-bg"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BIAOGEBG) }"
>
<div>
<div class="writing-title">
<img src="@/assets/images/vlog/标题栏-中间.png" alt="" class="titledibu" />
<img
src="@/assets/images/vlog/制作变焦镜/文字标题/《我的志愿者手记》分镜头脚本.png"
alt=""
class="titletext"
/>
</div>
<div class="doubao" @click="clickdoubao">
<img
src="@/assets/images/vlog/制作变焦镜/ai脚本视频占位图.png"
alt=""
class="doubao-img"
/>
</div>
<div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div>
</div>
<!-- <div v-else>
<div class="writing-title">
<img src="@/assets/images/vlog/标题栏-中间.png" alt="" class="titledibu" />
<img
src="@/assets/images/vlog/制作变焦镜/文字标题/《我的志愿者手记》分镜头脚本.png"
alt=""
class="titletext"
/>
</div>
<div class="biaogehome" @click="deletebiaoge()">
<img
src="@/assets/images/vlog/脚本切图/我的志愿者手记拍摄脚本.png"
alt=""
class="biaoge-img"
/>
<img src="@/assets/images/midterm/叉.png" alt="" class="biaoge-x" />
</div>
<div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div>
</div> -->
<!-- 引导步骤 -->
<GuideSteps
v-if="showDoubao"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem>
<img src="@/assets/images/components/引导/ai脚本工具引导.png" alt="" />
</GuideStepsItem>
</GuideSteps>
</div>
</div>
</template>
<style lang="less" scoped>
.writing-index {
width: 100%;
height: 100%;
}
.shooting-return {
position: absolute;
right: -90%;
top: 0;
width: 50%;
height: 50%;
}
.doubao .doubao-img {
width: 100%;
height: 100%;
}
.biaoge-x {
position: absolute;
top: -20px;
right: 0px;
width: 70px;
cursor: pointer;
z-index: 9999999;
}
.writing-title {
.titledibu {
position: absolute;
top: 17px;
left: 30%;
}
.titletext {
position: absolute;
top: 30px;
left: 40%;
}
}
.biaogehome {
position: absolute;
top: 165px;
left: 5%;
width: 94%;
}
.biaogehome .biaoge-img {
height: 896px !important;
width: 1800px;
display: block;
}
</style>
@@ -1,61 +0,0 @@
<template>
<div class="writing-index">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat writing-bg"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.SCRIPTING) }"
>
<div class="geshi-title">
<img src="@/assets/images/vlog/标题栏-中间.png" alt="" class="titledibu" />
<img
src="@/assets/images/vlog/制作变焦镜/文字标题/脚本的概念和基本要素.png"
alt=""
class="titletext"
/>
</div>
<video :src="fourth" controls></video>
<!-- <div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div> -->
<!-- 引导步骤 -->
<!-- <GuideSteps :steps="steps" :highlight-selector="currentHighlight" :highlight-padding="12"
:highlight-radius="20" @step-change="handleStepChange" position-offset-y="85%" hidden:false>
<GuideStepsItem>
<img src="@/assets/images/equipment/设备准备引导1.png" alt="" />
</GuideStepsItem>
</GuideSteps> -->
</div>
</div>
</template>
<script lang="tsx" setup>
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import fourth from '/video/vlog_renwuqujing_jiaobencehua.mp4'
</script>
<style lang="less" scoped>
.writing-index {
width: 100%;
height: 100%;
}
.geshi-title {
.titledibu {
position: absolute;
top: 17px;
left: 30%;
}
.titletext {
position: absolute;
top: 30px;
left: 45%;
}
}
.shooting-return {
position: absolute;
right: -90%;
top: 0;
width: 100%;
height: 100%;
}
</style>
+33
View File
@@ -0,0 +1,33 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<img class="absolute top-5 z-10" :src="getStaticImage('img/分镜制作-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('interview/img/分镜制作.png')" alt="" />
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+8 -11
View File
@@ -99,6 +99,13 @@ const handleVideoChange = () => {
}
const handleClose = () => {
if (key.value === 'idea') {
interview.setInterviewUnlockProgress(1)
router.push({
name: 'Interview'
})
return
}
router.back()
}
@@ -119,11 +126,6 @@ const handleComplete = () => {
router.push({
name: 'InterviewWork'
})
} else {
if (interview.InterviewUnlockProgress == 0) interview.setInterviewUnlockProgress(1)
router.push({
name: 'Interview'
})
}
}
@@ -150,7 +152,6 @@ onMounted(() => {
<template>
<div class="w-full h-full relative bg-white/10" @click="handleClick">
<template v-if="key !== 'case' && key !== 'profession'">
<ImgButton class="absolute left-4 top-4 z-10" :image="image"></ImgButton>
<ImgButton
class="absolute right-0 top-0 z-10"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@@ -189,10 +190,6 @@ onMounted(() => {
@ended="onEnded"
>
<template v-slot:overlay>
<ImgButton
class="absolute left-4 top-4 z-10 pointer-events-auto"
:image="image"
></ImgButton>
<ImgButton
class="absolute right-0 top-0 z-10 pointer-events-auto"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@@ -201,7 +198,7 @@ onMounted(() => {
</template>
</Video>
<div v-else-if="key === 'idea'" :src="image">
<img :src="getStaticImage('work/思路提点全屏')" alt="" />
<img :src="getStaticImage('interview/work/思路提点全屏')" alt="" />
</div>
</div>
+28 -28
View File
@@ -4,7 +4,7 @@ import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import { GuideSteps } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
@@ -17,7 +17,6 @@ const interview = useInterviewStore()
type WorkTab = 'vlog' | 'case' | 'profession' | 'idea'
const selected = ref<WorkTab>('case')
const stepIndex = ref(0)
const workSideSwitcherRef = ref<InstanceType<typeof WorkSideSwitcher>>()
const isGuideInterviewTask = computed(() => interview.isGuideInterviewTask)
@@ -28,32 +27,37 @@ const steps = [
{
id: 'step1',
content:
'现在我们要一起走进 Vlog 的世界。简单说,Vlog 就是用视频记录生活的一种方式 —— 它像一本会动的日记,能把我们身边的小事、瞬间的心情,通过镜头变得生动又具体。'
'现在我们要一起走进 Vlog 的世界。简单说,Vlog 就是用视频记录生活的一种方式 —— 它像一本会动的日记,能把我们身边的小事、瞬间的心情,通过镜头变得生动又具体。',
highlightSelector: '.img-switch-item--case'
},
{ id: 'step2', content: '接下来,我们就通过一个具体的案例,看看 Vlog 是怎么“捕捉美好” 的吧。' }
{
id: 'step2',
content: '接下来,我们就通过一个具体的案例,看看 Vlog 是怎么“捕捉美好” 的吧。',
highlightSelector: '.work-border'
}
]
//切换图片
const sideSwitcherItems = ref([
{
key: 'vlog',
description: '记录身边的美好',
image: 'work/vlog背景全屏待定'
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
},
{
key: 'case',
description: '案例赏析-桂花酒酿小圆子',
image: 'work/案例背景待定'
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
},
{
key: 'profession',
description: '专业剖析-桂花酒酿小圆子',
image: 'work/专业剖析小屏'
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
},
{
key: 'idea',
description: '思路提点-桂花酒酿小圆子',
image: 'work/思路提点小屏'
description: '我的职业蓝图',
image: 'interview/work/思路提点小屏'
}
])
@@ -62,21 +66,13 @@ const currentItem = computed(() => {
return sideSwitcherItems.value.find(item => item.key === selected.value)
})
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
)
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const handleSideSwitcherChange = (value: WorkTab) => {
if (selected.value === value) return
}
const handleImageClick = () => {
router.push({
name: 'FilmVideo',
name: 'InterviewVideo',
query: {
image:
workSideSwitcherRef.value?.items.find(item => item.key === selected.value)?.activeImage ||
@@ -87,7 +83,7 @@ const handleImageClick = () => {
}
const handleClose = () => {
router.push({ name: 'Film' })
router.push({ name: 'Interview' })
}
const handleWorkClick = () => {
@@ -99,7 +95,6 @@ const handleGuideComplete = () => {
}
const handleOpenGuide = () => {
stepIndex.value = 0
interview.setIsGuideInterviewTask(true)
}
@@ -120,7 +115,7 @@ function openImages(urls: string[], startIndex = 0) {
<!-- 背景图片-center -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BG) }"
:style="{ backgroundImage: getBackgroundImage('interview/work/任务取景框背景') }"
></div>
<!-- 关闭 -->
@@ -135,6 +130,13 @@ function openImages(urls: string[], startIndex = 0) {
class="work-border absolute top-[140px] left-24 inset-0 bg-cover bg-center bg-no-repeat z-10 w-[1360px] h-[800px] p-10"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BORDER) }"
>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
>
<img class="w-40" :src="getStaticImage('work/小手')" alt="" />
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
</div>
<div
class="flex items-center pl-16 w-[1357px] h-[265px] absolute -bottom-16 left-0 bg-cover bg-center bg-no-repeat z-20"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.TITLE) }"
@@ -142,8 +144,8 @@ function openImages(urls: string[], startIndex = 0) {
<span
class="mt-24 text-white font-bold text-6xl [text-shadow:0_2px_4px_rgba(0,0,0,0.2),0_0_6px_rgba(0,207,255,0.9),0_0_14px_rgba(0,207,255,0.7)]"
>
{{ currentItem?.description }}</span
>
{{ currentItem?.description }}
</span>
</div>
<img
@click="handleImageClick"
@@ -174,10 +176,8 @@ function openImages(urls: string[], startIndex = 0) {
<GuideSteps
v-if="isGuideInterviewTask"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
@complete="handleGuideComplete"
@skip="handleGuideComplete"
position-offset-y="85%"
+23 -57
View File
@@ -4,7 +4,8 @@ import {
getAllModules,
getTagsByModule,
getSubTagsByTag,
getTagResourceCount
getTagResourceCount,
getSubTagIcon
} from '../resourceData'
import { getStaticImage, getTagColor } from '@/utils'
@@ -135,7 +136,7 @@ const getResourceCount = (moduleId: string, tagId: string, tagName: string) => {
// 获取标签颜色类 - 根据标签类型返回不同的颜色(匹配目标效果图)
const getTagColorClass = (tagName: string, isSelected: boolean) => {
if (isSelected) {
return getTagColor(tagName) + ' brightness-125'
return getTagColor(tagName) + ' brightness-110'
}
return getTagColor(tagName)
@@ -149,34 +150,6 @@ const handleSubTagClick = (moduleId: string, tagId: string, subTagId: string) =>
emit('subTagClick', moduleId, tagId, subTagId)
}
// 获取子标签的图标
const getSubTagIcon = (subTagName: string) => {
// 图标文件名映射
const iconMap: Record<string, string> = {
构图: 'skill/icon/构图.png',
光线: 'skill/icon/光线.png',
运镜: 'skill/icon/运镜.png',
设备准备: 'skill/icon/设备准备.png',
脚本制作: 'skill/icon/脚本与分镜制作.png',
光线运用: 'skill/icon/光线运用.png',
运镜技巧: 'skill/icon/运镜技巧.png',
选题与构思: 'skill/icon/选题与构思.png',
脚本与分镜制作: 'skill/icon/脚本与分镜制作.png',
拍摄技巧: 'skill/icon/拍摄技巧.png',
剪辑技巧: 'skill/icon/剪辑技巧.png',
包装: 'skill/icon/包装.png',
调色: 'skill/icon/调色.png',
音效: 'skill/icon/音效.png',
素材管理: 'skill/icon/素材管理.png',
蒙太奇剪辑: 'skill/icon/蒙太奇剪辑.png',
版权意识: 'skill/icon/版权意识.png',
ai技术应用: 'skill/icon/ai技术应用.png',
思路提点: 'skill/icon/思路提点.png',
案例合集: 'skill/icon/案例合集.png'
}
return iconMap[subTagName] || 'skill/icon/构图.png' // 默认图标
}
// 获取技能树标题
const getTreeTitle = () => {
if (!props.selectedModuleId) {
@@ -252,7 +225,15 @@ const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
:style="getTagPosition(tagIndex)"
>
<!-- 资源标签节点 -->
<div class="relative">
<div
:class="[
'relative transition-all',
isTagSelected(tag.moduleId, tag.tagId) ? 'scale-110 z-30' : '',
props.selectedTagId && !isTagSelected(tag.moduleId, tag.tagId)
? 'opacity-35 brightness-75 saturate-50'
: ''
]"
>
<!-- 标签主体容器 - 用于精确对齐定位层 -->
<div class="relative">
<!-- 标签主体 -->
@@ -261,7 +242,7 @@ const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
'relative py-5 cursor-pointer min-w-[240px] max-w-[220px] transition-all hover:scale-105',
'border-4 rounded-full shadow-lg',
getTagColorClass(tag.tagName, isTagSelected(tag.moduleId, tag.tagId)),
isTagSelected(tag.moduleId, tag.tagId) ? 'ring-2 ring-white/60' : ''
isTagSelected(tag.moduleId, tag.tagId) ? 'ring-2 ring-white/80 shadow-2xl' : ''
]"
@click="handleTagClick(tag.moduleId, tag.tagId)"
>
@@ -323,35 +304,20 @@ const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
</template>
</div>
<!-- 点击之后的蒙版 - 使用径向渐变,从中心透明到边缘半透明黑色 -->
<transition
enter-active-class="transition-opacity duration-300 ease-out"
leave-active-class="transition-opacity duration-300 ease-in"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="props.selectedTagId"
class="absolute inset-0 z-10 pointer-events-auto"
style="
<!-- 点击之后的蒙版 - 暂时关闭 -->
<!-- <transition enter-active-class="transition-opacity duration-300 ease-out"
leave-active-class="transition-opacity duration-300 ease-in" enter-from-class="opacity-0"
enter-to-class="opacity-100" leave-from-class="opacity-100" leave-to-class="opacity-0">
<div v-if="props.selectedTagId" class="absolute inset-0 z-10 pointer-events-auto" style="
background: radial-gradient(
circle at center,
rgba(46, 141, 218, 0.8) 0%,
rgba(46, 141, 218, 0.4) 30%,
rgba(46, 141, 218, 0.3) 60%,
rgba(46, 141, 218, 0.95) 0%,
rgba(46, 141, 218, 0.65) 30%,
rgba(46, 141, 218, 0.5) 60%,
transparent 70%
);
"
></div>
<!-- <img
:src="getStaticImage('skill/树蒙版.png')"
alt=""
class="absolute w-full h-full inset-0 z-20"
v-if="props.selectedTagId"
/> -->
</transition>
"></div>
</transition> -->
<!-- 右侧标签 -->
<img class="absolute right-4 top-0 z-30" :src="getStaticImage('skill/技能树标题.png')" alt="" />
+154 -64
View File
@@ -2,7 +2,13 @@
defineOptions({
name: 'Skill'
})
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
getTagColor,
IMAGE_PATHS
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { ImagePreview } from '@/components'
import { useRouter } from 'vue-router'
@@ -14,6 +20,8 @@ import {
getResourcesByTag,
getResourcesBySubTag,
getSubTagsByTag,
getSubTagsByTagName,
getSubTagIcon,
type ResourceItem
} from './resourceData'
import SearchTags from './components/SearchTags.vue'
@@ -67,7 +75,6 @@ const guideSteps = ref([
highlightSelector: '.resource-list-container'
}
])
const currentGuideStep = ref(0)
/**
* 模块列表
@@ -81,22 +88,26 @@ const moduleList = ref([
{
id: 'module-1',
name: '新手导航',
image: 'skill/新手导航-选中.png'
selectedImage: 'skill/新手导航-选中.png',
unselectedImage: 'skill/新手导航-未选中.png'
},
{
id: 'module-2',
name: 'vlog发现与记录',
image: 'skill/vlog-选中.png'
selectedImage: 'skill/vlog-选中.png',
unselectedImage: 'skill/vlog-未选中.png'
},
{
id: 'module-3',
name: '访谈聆听与对话',
image: 'skill/访谈-选中.png'
selectedImage: 'skill/访谈-选中.png',
unselectedImage: 'skill/访谈-未选中.png'
},
{
id: 'module-4',
name: '电影创作与表达',
image: 'skill/电影-选中.png'
selectedImage: 'skill/电影-选中.png',
unselectedImage: 'skill/电影-未选中.png'
}
])
@@ -152,7 +163,9 @@ const searchTags = computed<SearchTag[]>(() => {
if (selectedSubTagId.value) {
const subTagResource = getAllResources().find(
r => r.tagId === selectedTagId.value && r.subTagId === selectedSubTagId.value
r =>
r.tagName === currentSelectedTagName.value &&
(r.subTagId === selectedSubTagId.value || r.subTagName === selectedSubTagId.value)
)
if (subTagResource && subTagResource.subTagName) {
tags.push({ type: 'subTag', name: subTagResource.subTagName })
@@ -164,17 +177,55 @@ const searchTags = computed<SearchTag[]>(() => {
return tags
})
const hasActiveFilter = computed(
() => searchTags.value.length > 0 || search.value.trim().length > 0
)
const currentFilterText = computed(() => {
if (searchTags.value.length > 0) {
return searchTags.value.map(tag => tag.name).join(' / ')
}
return search.value.trim()
})
const currentSelectedTagName = computed(() => {
if (!selectedTagId.value) {
return ''
}
return getAllResources().find(r => r.tagId === selectedTagId.value)?.tagName || ''
})
const availableSubTags = computed(() => {
if (!selectedTagId.value) {
return []
}
if (selectedModuleId.value) {
return getSubTagsByTag(selectedModuleId.value, selectedTagId.value)
}
return getSubTagsByTagName(currentSelectedTagName.value)
})
// 过滤后的资源列表
const filteredResources = computed<ResourceItem[]>(() => {
let resources: ResourceItem[] = []
// 如果选择了子标签,返回子标签的资源
if (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
resources = getResourcesBySubTag(
selectedModuleId.value,
selectedTagId.value,
selectedSubTagId.value
)
if (selectedTagId.value && selectedSubTagId.value) {
if (selectedModuleId.value) {
resources = getResourcesBySubTag(
selectedModuleId.value,
selectedTagId.value,
selectedSubTagId.value
)
} else {
resources = getAllResources().filter(
r => r.tagName === currentSelectedTagName.value && r.subTagName === selectedSubTagId.value
)
}
}
// 如果选择了资源标签
else if (selectedTagId.value) {
@@ -183,9 +234,8 @@ const filteredResources = computed<ResourceItem[]>(() => {
resources = getResourcesByTag(selectedModuleId.value, selectedTagId.value)
} else {
// 总览模式:返回所有模块中该标签名称的资源
const tagName = getAllResources().find(r => r.tagId === selectedTagId.value)?.tagName
if (tagName) {
resources = getAllResources().filter(r => r.tagName === tagName)
if (currentSelectedTagName.value) {
resources = getAllResources().filter(r => r.tagName === currentSelectedTagName.value)
}
}
}
@@ -231,47 +281,28 @@ const handleTagClick = (moduleId: string, tagId: string) => {
return
}
// 检查是否有子标签
const subTags = getSubTagsByTag(moduleId, tagId)
const hasSubTags = subTags.length > 0
const isModuleMode = selectedModuleId.value !== null
if (hasSubTags) {
// 有子标签:选择新标签并自动展开
// 在总览模式下,不设置 selectedModuleId,保持总览状态
if (selectedModuleId.value !== null) {
selectedModuleId.value = moduleId
}
selectedTagId.value = tagId
selectedSubTagId.value = null
// 自动展开
expandedTags.value.add(tagId)
} else {
// 无子标签:高亮并搜索,不隐藏其他标签
// 在总览模式下,不设置 selectedModuleId,保持总览状态
if (selectedModuleId.value !== null) {
selectedModuleId.value = moduleId
}
selectedTagId.value = tagId
selectedSubTagId.value = null
expandedTags.value.clear()
if (isModuleMode) {
selectedModuleId.value = moduleId
}
selectedTagId.value = tagId
selectedSubTagId.value = null
expandedTags.value.clear()
}
// 处理子标签点击
const handleSubTagClick = (moduleId: string, tagId: string, subTagId: string) => {
// 如果点击的是已选中的子标签,则取消选择
if (
selectedModuleId.value === moduleId &&
selectedTagId.value === tagId &&
selectedSubTagId.value === subTagId
) {
if (selectedTagId.value === tagId && selectedSubTagId.value === subTagId) {
selectedSubTagId.value = null
} else {
selectedModuleId.value = moduleId
if (selectedModuleId.value !== null) {
selectedModuleId.value = moduleId
}
selectedTagId.value = tagId
selectedSubTagId.value = subTagId
// 确保父标签是展开的
expandedTags.value.add(tagId)
}
}
@@ -280,6 +311,14 @@ const handleModuleListItemClick = (moduleId: string) => {
handleModuleClick(moduleId === selectedModuleId.value ? null : moduleId)
}
const clearAllFilters = () => {
search.value = ''
selectedModuleId.value = null
selectedTagId.value = null
selectedSubTagId.value = null
expandedTags.value.clear()
}
// 处理删除搜索标签
const handleRemoveTag = (tagType: 'module' | 'tag' | 'subTag') => {
if (tagType === 'subTag') {
@@ -339,19 +378,6 @@ const handleGuideComplete = () => {
const handleGuideSkip = () => {
showGuide.value = false
}
// 处理引导步骤变化
const handleGuideStepChange = (step: any, index: number) => {
currentGuideStep.value = index
}
// 当前步骤的高亮选择器
const currentHighlightSelector = computed(() => {
if (showGuide.value && guideSteps.value[currentGuideStep.value]) {
return guideSteps.value[currentGuideStep.value].highlightSelector || null
}
return null
})
</script>
<template>
@@ -392,6 +418,20 @@ const currentHighlightSelector = computed(() => {
/>
</div>
<div
v-if="hasActiveFilter"
class="absolute left-10 top-[108px] z-20 flex items-center gap-4 rounded-full border-2 border-white/80 bg-[#1b8fd8]/85 px-6 py-3 text-white shadow-lg backdrop-blur-sm"
>
<span class="text-2xl font-bold">当前查看:{{ currentFilterText }}</span>
<span class="text-xl opacity-90">共 {{ filteredResources.length }} 个资源</span>
<button
class="rounded-full bg-white/25 px-4 py-1 text-xl font-bold transition-all hover:bg-white/40"
@click="clearAllFilters"
>
清除筛选
</button>
</div>
<!-- 模块列表 -->
<div
class="module-list-container absolute left-[55%] -translate-x-1/2 top-10 bg-cover bg-center"
@@ -404,14 +444,66 @@ const currentHighlightSelector = computed(() => {
:class="{ 'opacity-80': selectedModuleId === module.id }"
@click="handleModuleListItemClick(module.id)"
>
<img class="absolute inset-0" :src="getStaticImage(module.image)" alt="" />
<img
class="absolute inset-0"
:src="
getStaticImage(
selectedModuleId === module.id ? module.selectedImage : module.unselectedImage
)
"
alt=""
/>
</div>
</div>
</div>
<!-- 资源列表 -->
<div class="resource-list-container absolute left-10 bottom-20 w-[740px] h-[800px]">
<ResourceList :resources="filteredResources" @resource-click="handleResourceClick" />
<div
class="resource-list-container absolute left-10 bottom-20 flex h-[800px] w-[740px] flex-col"
>
<div v-if="selectedTagId" class="mb-4">
<div
class="flex h-[72px] items-center rounded-br-full rounded-tl-full border-4 px-8 shadow-lg"
:class="getTagColor(currentSelectedTagName)"
>
<span class="mr-8 text-5xl leading-none">•</span>
<span class="text-4xl font-bold tracking-[0.25em]">{{ currentSelectedTagName }}</span>
</div>
<div
v-if="availableSubTags.length > 0"
class="mt-4 rounded-3xl bg-white/25 p-4 backdrop-blur-sm"
>
<!-- <div class="mb-3 flex justify-end">
<button class="rounded-full px-4 py-1 text-xl font-bold text-white transition-all"
:class="selectedSubTagId === null ? 'bg-white/30' : 'bg-white/15 hover:bg-white/30'"
@click="selectedSubTagId = null">
全部
</button>
</div> -->
<div class="flex gap-3 pb-1 scrollbar-hide">
<button
v-for="subTag in availableSubTags"
:key="subTag.id"
class="flex h-[116px] w-[104px] shrink-0 flex-col items-center justify-center rounded-2xl border-2 transition-all hover:scale-105"
:class="
selectedSubTagId === subTag.id || selectedSubTagId === subTag.name
? 'border-white bg-white/35 shadow-lg'
: 'border-white/45 bg-[#1597d7]/45'
"
@click="handleSubTagClick(selectedModuleId || '', selectedTagId || '', subTag.id)"
>
<img
class="object-contain"
:src="getStaticImage(getSubTagIcon(subTag.name))"
:alt="subTag.name"
/>
</button>
</div>
</div>
</div>
<div class="min-h-0 flex-1">
<ResourceList :resources="filteredResources" @resource-click="handleResourceClick" />
</div>
</div>
<!-- 技能树-资源树 -->
@@ -471,13 +563,11 @@ const currentHighlightSelector = computed(() => {
v-if="showGuide"
:steps="guideSteps"
:keyboard-navigation="true"
:highlight-selector="currentHighlightSelector"
:highlight-padding="12"
:highlight-radius="16"
position-offset-y="85%"
@complete="handleGuideComplete"
@skip="handleGuideSkip"
@step-change="handleGuideStepChange"
/>
</div>
</template>
+41 -2
View File
@@ -577,7 +577,7 @@ export const resourceData: ResourceItem[] = [
videoUrl: '/video/anlishangxi_meishi_xiaoyuanzi.mp4'
},
{
id: 'res-38',
id: 'res-48',
title: 'Vlog 案例一',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
@@ -587,7 +587,7 @@ export const resourceData: ResourceItem[] = [
videoUrl: '/video/practice/vlog_anli1.mp4'
},
{
id: 'res-39',
id: 'res-49',
title: 'Vlog 案例二',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
@@ -667,6 +667,45 @@ export function getSubTagsByTag(
return Array.from(subTags.entries()).map(([id, name]) => ({ id, name }))
}
export function getSubTagsByTagName(tagName: string): Array<{ id: string; name: string }> {
const subTags = new Map<string, string>()
resourceData
.filter(resource => resource.tagName === tagName && resource.subTagName)
.forEach(resource => {
const subTagName = resource.subTagName || ''
if (!subTags.has(subTagName)) {
subTags.set(subTagName, subTagName)
}
})
return Array.from(subTags.entries()).map(([id, name]) => ({ id, name }))
}
export function getSubTagIcon(subTagName: string): string {
const iconMap: Record<string, string> = {
构图: 'skill/icon/构图.png',
光线: 'skill/icon/光线.png',
运镜: 'skill/icon/运镜.png',
设备准备: 'skill/icon/设备准备.png',
脚本制作: 'skill/icon/脚本与分镜制作.png',
光线运用: 'skill/icon/光线运用.png',
运镜技巧: 'skill/icon/运镜技巧.png',
选题与构思: 'skill/icon/选题与构思.png',
脚本与分镜制作: 'skill/icon/脚本与分镜制作.png',
拍摄技巧: 'skill/icon/拍摄技巧.png',
剪辑技巧: 'skill/icon/剪辑技巧.png',
包装: 'skill/icon/包装.png',
调色: 'skill/icon/调色.png',
音效: 'skill/icon/音效.png',
素材管理: 'skill/icon/素材管理.png',
蒙太奇剪辑: 'skill/icon/蒙太奇剪辑.png',
版权意识: 'skill/icon/版权意识.png',
ai技术应用: 'skill/icon/ai技术应用.png',
思路提点: 'skill/icon/思路提点.png',
案例合集: 'skill/icon/案例合集.png'
}
return iconMap[subTagName] || 'skill/icon/构图.png'
}
// 获取标签的资源数量
export function getTagResourceCount(moduleId: string, tagId: string): number {
return resourceData.filter(resource => resource.moduleId === moduleId && resource.tagId === tagId)
+3 -4
View File
@@ -1,11 +1,12 @@
<script lang="ts" setup>
import router from '@/router'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { ref } from 'vue'
import ImageFilter from '@/components/ImageFilter/ImageFilter.vue'
import { useVlogStore } from '@/stores'
const router = useRouter()
const selected = ref<number | undefined>(1)
const btnState = ref(false)
@@ -117,8 +118,6 @@ const handleTryOther = () => {
}
const handleFinish = () => {
useVlogStore().setVlogUnlockProgress(4)
router.push({
name: 'Vlog'
})
+107 -67
View File
@@ -1,99 +1,139 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue'
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
// import { GuideSteps } from '@/components/Guide'
import { IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { getPublicPath, IMAGE_PATHS } from '@/utils'
import { Video } from '@/components'
const router = useRouter()
// 导入视频文件
import videoBasic from '/video/vlog_zhizuobianjiaojing_jichujianji.mp4'
import videoSoundeffect from '/video/vlog_renwuqujing_yingxiaochuli.mp4'
import videoPackaging from '/video/vlog_renwuqujing_baozhuang.mp4'
import videoCopyright from '/video/vlog_renwuqujing_banquanguifan.mp4'
const videoList = [videoBasic, videoSoundeffect, videoPackaging, videoCopyright]
const videoRef = ref<HTMLVideoElement>()
const navigationSteps = [
{
id: 'step1',
label: '后期制作',
content: '剪辑起点',
type: 'post' as const
},
{
id: 'step2',
label: '后期制作',
content: '音画匹配',
type: 'post' as const
},
{
id: 'step3',
label: '后期制作',
content: '剪辑节奏',
type: 'post' as const
},
{
id: 'step4',
label: '后期制作',
content: '声音与转场',
type: 'post' as const
}
]
const currentVideoIndex = ref(0)
const playing = ref(false)
const showStepNavigation = ref(false)
const showEndingGuide = ref(false)
onMounted(() => {
// 确保视频元素加载并自动播放
if (videoRef.value) {
videoRef.value.src = videoList[currentVideoIndex.value]
videoRef.value.load()
// 尝试自动播放
const playPromise = videoRef.value.play()
if (playPromise !== undefined) {
playPromise.catch(error => {
// 自动播放失败,可能是浏览器策略阻止,需要用户交互
console.log('自动播放被阻止:', error)
})
}
// 监听视频结束事件
videoRef.value.addEventListener('ended', playNextVideo)
}
})
// 播放下一个视频
const playNextVideo = () => {
if (currentVideoIndex.value < videoList.length - 1) {
currentVideoIndex.value++
if (videoRef.value) {
videoRef.value.src = videoList[currentVideoIndex.value]
videoRef.value.load()
// 自动播放下一个视频
const playPromise = videoRef.value.play()
if (playPromise !== undefined) {
playPromise.catch(error => {
console.log('自动播放被阻止:', error)
})
}
}
if (currentVideoIndex.value >= videoList.length - 1) {
playing.value = false
showEndingGuide.value = true
return
}
currentVideoIndex.value++
nextTick(() => {
playing.value = true
})
}
// 监听当前视频索引变化
watch(currentVideoIndex, newIndex => {
if (videoRef.value) {
videoRef.value.src = videoList[newIndex]
videoRef.value.load()
}
})
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
// showStepNavigation.value = false
nextTick(() => {
playing.value = true
})
}
const handleClose = () => {
router.back()
}
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
const handleEndingGuideComplete = () => {
showEndingGuide.value = false
}
</script>
<template>
<div>
<!--使用动态绑定和ref-->
<video
ref="videoRef"
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
:src="videoList[currentVideoIndex]"
type="video/mp4"
controls
preload="auto"
class="video-player"
autoplay
></video>
v-model="playing"
:muted="false"
:volume="1"
@ended="playNextVideo"
/>
<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 flex h-12 w-12 -translate-y-1/2 items-center justify-center rounded-full bg-blue-500/80 text-white transition-all duration-200 hover:bg-blue-500"
title="步骤导航"
@click="showStepNavigation = true"
>
<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>
<StepNavigation
:visible="showStepNavigation"
:background-image="''"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
@step-click="handleStepClick"
@close="showStepNavigation = false"
/>
<GuideSteps position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_019.MP3')">
剪辑不是简单拼接素材,而是在重新组织故事、控制节奏、强化表达。
</GuideStepsItem>
</GuideSteps>
<GuideSteps
v-if="showEndingGuide"
position-offset-y="85%"
@complete="handleEndingGuideComplete"
@skip="handleEndingGuideComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_020.MP3')">
当内容结构完成后,我们还可以通过调色与包装,让画面更加统一、更具观感。
</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
.video-player {
width: 100%;
height: 100%;
object-fit: contain;
}
</style>
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, GuideSteps, GuideStepsItem } from '@/components'
import { getPublicPath, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const videoUrl = getPublicPath('video/vlog_renwuqujing_baozhuang.mp4')
const videoPlaying = ref(false)
const showGuide = ref(true)
const handleBack = () => {
router.back()
}
const handleComplete = () => {
showGuide.value = false
nextTick(() => {
videoPlaying.value = true
})
}
</script>
<template>
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
<Video
class="w-full h-full"
:src="videoUrl"
v-model="videoPlaying"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
/>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
<GuideSteps v-if="showGuide" position-offset-y="85%" @complete="handleComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_021.MP3')">
最后一步,就是将作品正式输出。一起看看最终效果吧!
</GuideStepsItem>
</GuideSteps>
</div>
</template>
+34 -17
View File
@@ -3,7 +3,8 @@ import { getStaticImage } from '@/utils'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage } from '@/utils'
defineOptions({ name: 'InterviewMenu' })
import { useVlogStore } from '@/stores'
defineOptions({ name: 'VlogMenu' })
/**
* 菜单项 (前期准备、中期拍摄、后期剪辑)
@@ -34,13 +35,13 @@ const menuItems = ref([
label: '资源筹备',
routeName: 'VlogResource',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '创意构想',
routeName: 'VlogCreative',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '脚本撰写',
@@ -52,7 +53,7 @@ const menuItems = ref([
label: '分镜制作',
routeName: 'VlogStoryboard',
parentValue: 1,
disabled: true
disabled: false
},
{
label: '设备准备',
@@ -70,14 +71,14 @@ const menuItems = ref([
label: '现场拍摄',
routeName: 'VlogShooting',
parentValue: 2,
disabled: true
},
{
label: '素材管理',
routeName: 'Vlog素材管理',
parentValue: 3,
disabled: true
disabled: false
},
// {
// label: '素材管理',
// routeName: 'Vlog素材管理',
// parentValue: 3,
// disabled: true
// },
{
label: '剪辑',
routeName: 'VlogEditing',
@@ -92,9 +93,9 @@ const menuItems = ref([
},
{
label: '导出发布',
routeName: 'Vlog导出发布',
routeName: 'VlogExportPublish',
parentValue: 3,
disabled: true
disabled: false
}
])
@@ -103,6 +104,11 @@ const props = defineProps<{
}>()
const router = useRouter()
const vlog = useVlogStore()
const isViewed = (routeName: string) => {
return vlog.viewed?.[routeName] === true
}
const currentParentValue = computed<number>(() => {
return props.currentStep
@@ -125,9 +131,9 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
<div class="absolute left-20 top-0 bottom-0 w-[520px] pt-28" v-if="currentStep">
<!-- 分类名称 -->
<div class="absolute top-3 left-24 flex items-center space-x-2">
<span class="text-4xl text-[#fbbf24] font-black">{{
category[currentParentValue - 1].label
}}</span>
<span class="text-4xl text-[#fbbf24] font-black">
{{ category[currentParentValue - 1].label }}
</span>
<img class="filter-blue" :src="category[currentParentValue - 1].icon" alt="" />
</div>
<!-- 背景图片 -->
@@ -151,7 +157,18 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
class="absolute inset-0 bg-cover bg-center bg-no-repeat opacity-0 group-hover:opacity-100 transition-opacity duration-300"
:style="{ backgroundImage: getBackgroundImage('components/menu-hover.png') }"
></div>
<span :class="[!item.disabled ? 'menu-glow-text' : '']">{{ item.label }}</span>
<span class="flex items-center gap-2" :class="[!item.disabled ? 'menu-glow-text' : '']">
<span>{{ item.label }}</span>
<img
class="w-10"
:src="
isViewed(item.routeName)
? '/src/assets/images/midterm/完成.png'
: '/src/assets/images/midterm/未完成.png'
"
alt=""
/>
</span>
</div>
</template>
</div>
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('img/创意构想-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('vlog/img/创意构想.png')" alt="" />
</div>
<!-- <GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps> -->
</div>
</template>
+173 -22
View File
@@ -1,19 +1,135 @@
<script lang="tsx" setup>
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { computed, onMounted, ref, watch } from 'vue'
import { computed, ref, watch, type Ref } from 'vue'
import { ProgressBar, Overlay } from '@/components'
import { useRouter } from 'vue-router'
import { GuideSteps, 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 = ref(false)
const visible = computed({
get: () => vlog.vlogMenuVisible,
set: value => vlog.setVlogMenuVisible(value)
})
const currentStep = computed(() => vlog.vlogUnlockProgress)
const progress = computed(() => vlog.progressPercent)
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:
'前期阶段,我们需要完成四项关键任务:【资源筹备】【创意构想】【脚本撰写】【分镜制作】',
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:
'拍摄过程可分为三个核心步骤:【设备准备】【实地取景】【现场拍摄】。稳妥的准备是顺利拍摄的前提。',
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: '这个阶段需要做好四件事:【素材整理】【剪辑】【调色】【导出发布】。',
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('完成前置条件')
// 处理完成逻辑
@@ -27,7 +143,7 @@ const handleStepChange = (step: number) => {
}
const handleWorkClick = () => {
console.log('任务取景按钮点击')
// console.log('任务取景按钮点击')
// 处理任务取景按钮点击逻辑
router.push({ name: 'VlogWork' })
}
@@ -36,20 +152,15 @@ const handleClose = () => {
router.push({ name: 'Action' })
}
// watch(
// () => currentStep.value,
// newVal => {
// if (newVal !== 0 && newVal !== 4) {
// visible.value = true
// }
// }
// )
onMounted(() => {
if (currentStep.value !== 0 && currentStep.value !== 4) {
visible.value = true
}
})
watch(
[visible, currentStep],
([isVisible, step]) => {
if (isVisible && step < 4) {
triggerStageGuide(step)
}
},
{ immediate: true }
)
</script>
<template>
@@ -60,6 +171,16 @@ onMounted(() => {
: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"
@@ -89,8 +210,12 @@ onMounted(() => {
/>
</div>
<!-- 引导任务 -->
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
<!-- 引导人物 -->
<ImgButton
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">
<!-- 关闭按钮 -->
@@ -105,16 +230,42 @@ onMounted(() => {
<!-- 进度条 -->
<div class="absolute left-0 bottom-0 w-full">
<ProgressBar :current-step="currentStep" @step-change="handleStepChange" />
<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>
@@ -1,7 +1,7 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed } from 'vue'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps } from '@/components/Guide'
import { onBeforeUnmount, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -11,39 +11,102 @@ const back = () => {
// 跳转设备准备
router.back()
}
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
]
const stepIndex = ref(0)
const steps = ref([
{
id: 'step1',
content:
'设备准备妥当后,我们就该走进真实场景,开启实地取景的环节啦,一起去捕捉那些适合拍摄的画面吧!',
audioSrc: getPublicPath('audio/py_2_013.MP3')
}
])
const showGuide = ref(false)
const guideTriggered = ref(false)
const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
const viewedMenuKeys = ref<Set<string>>(new Set())
const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
// 菜单项数据
const menuItems = ref([
{ id: 'phone', name: '手机', disabled: false },
{ id: 'phone-stand', name: '手机支架', disabled: true },
{ id: 'bluetooth-remote', name: '蓝牙遥控器', disabled: true },
{ id: 'stabilizer', name: '稳定器', disabled: true },
{ id: 'props', name: '常用道具', disabled: true },
{ id: 'phone-lens', name: '手机辅助箱头', disabled: true }
type EquipmentCategoryId = 'phone' | 'phone-stabilizer' | 'wireless-lavalier-mic'
interface EquipmentProduct {
img: string
}
interface EquipmentCategory {
id: EquipmentCategoryId
name: string
disabled: boolean
titleImg: string
contentImg: string
products: EquipmentProduct[]
}
const categories = ref<EquipmentCategory[]>([
{
id: 'phone',
name: '手机',
disabled: false,
titleImg: getStaticImage('equipment/标题-苹果.png'),
contentImg: getStaticImage('equipment/内容-苹果.png'),
products: [
{ img: getStaticImage('equipment/设备-手机1.png') },
{ img: getStaticImage('equipment/设备-手机2.png') }
]
},
{
id: 'phone-stabilizer',
name: '手机稳定器',
disabled: false,
titleImg: getStaticImage('equipment/手机稳定器标题.png'),
contentImg: getStaticImage('equipment/手机稳定器图文.png'),
products: [{ img: getStaticImage('equipment/手机稳定器.png') }]
},
{
id: 'wireless-lavalier-mic',
name: '无线领夹麦克风',
disabled: false,
titleImg: getStaticImage('equipment/无线领夹麦克风标题.png'),
contentImg: getStaticImage('equipment/无线领夹麦克风图文.png'),
products: [{ img: getStaticImage('equipment/无线领夹麦克风.png') }]
}
])
const phoneList = ref([
{ phoneImg: getStaticImage('equipment/设备-手机1.png') },
{ phoneImg: getStaticImage('equipment/设备-手机2.png') }
])
categories.value = categories.value.map(c => ({
...c,
disabled: (c.products?.length ?? 0) === 0
}))
const platetitleList = ref([
{ title: getStaticImage('equipment/标题-苹果.png') },
{ title: getStaticImage('equipment/标题-安卓小米.png') }
])
// 默认选中的菜单也算作已查看
if (categories.value[0] && !categories.value[0].disabled) {
viewedMenuKeys.value.add(categories.value[0].id)
}
const platetextList = ref([
{ text: getStaticImage('equipment/内容-苹果.png') },
{ text: getStaticImage('equipment/内容-安卓小米.png') }
])
const maybeTriggerGuide = () => {
if (guideTriggered.value) return
const enabledIds = categories.value.filter(c => !c.disabled).map(c => c.id)
if (enabledIds.length === 0) return
const allViewed = enabledIds.every(id => viewedMenuKeys.value.has(id))
if (!allViewed) return
guideTriggered.value = true
guideTimer.value = setTimeout(() => {
showGuide.value = true
guideTimer.value = null
}, 3000)
}
onBeforeUnmount(() => {
if (guideTimer.value) {
clearTimeout(guideTimer.value)
guideTimer.value = null
}
})
const currentCategory = computed(
() => categories.value[selectedMenuIndex.value] ?? categories.value[0]
)
const currentProducts = computed(() => currentCategory.value?.products ?? [])
const currentTitleImg = computed(() => currentCategory.value?.titleImg ?? '')
const currentContentImg = computed(() => currentCategory.value?.contentImg ?? '')
const hiddenbarclick = () => {
hiddenbar.value = !hiddenbar.value
}
@@ -51,15 +114,17 @@ const hiddenbarclick = () => {
// 选择菜单项
const selectMenuItem = (index: number, item: { id: string; name: string; disabled: boolean }) => {
if (item.disabled) {
console.log(`菜单项 "${item.name}" 已被禁用`)
return
}
selectedMenuIndex.value = index
currentIndex.value = 0
viewedMenuKeys.value.add(item.id)
maybeTriggerGuide()
}
// 下一张图片
const nextPhone = () => {
if (currentIndex.value < phoneList.value.length - 1) {
if (currentIndex.value < currentProducts.value.length - 1) {
currentIndex.value++
}
}
@@ -70,17 +135,9 @@ const prevPhone = () => {
currentIndex.value--
}
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
</script>
<template>
<div class="equipment-index">
<div class="equipment-index overflow-hidden">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat equipment-bg"
@@ -93,7 +150,7 @@ const currentHighlight = computed(() =>
<div class="equipment-item" :hidden="hiddenbar">
<div class="menu-list">
<div
v-for="(item, index) in menuItems"
v-for="(item, index) in categories"
:key="item.id"
class="menu-item"
:class="{
@@ -118,7 +175,10 @@ const currentHighlight = computed(() =>
<div class="equipment-bt">
<img src="@/assets/images/equipment/按钮选中-设备准备.png" alt="" />
</div>
<div :class="hiddenbar ? 'equipment-unfold1' : 'equipment-unfold2'" @click="hiddenbarclick()">
<div
:class="['cursor-pointer', hiddenbar ? 'equipment-unfold1' : 'equipment-unfold2']"
@click="hiddenbarclick()"
>
<img
:src="
hiddenbar
@@ -139,12 +199,12 @@ const currentHighlight = computed(() =>
<img src="@/assets/images/equipment/按钮-左滑.png" alt="" />
</div>
<div class="equipment-phoneImg">
<img :src="phoneList[currentIndex].phoneImg" alt="" />
<img :src="currentProducts[currentIndex]?.img" alt="" />
</div>
<div
class="equipment-rightbutton"
@click="nextPhone"
:style="{ visibility: currentIndex < phoneList.length - 1 ? 'visible' : 'hidden' }"
:style="{ visibility: currentIndex < currentProducts.length - 1 ? 'visible' : 'hidden' }"
>
<img src="@/assets/images/equipment/按钮-右滑.png" alt="" />
</div>
@@ -155,11 +215,11 @@ const currentHighlight = computed(() =>
</div>
<div class="title">
<img :src="platetitleList[currentIndex].title" alt="" />
<img :src="currentTitleImg" alt="" />
</div>
<div class="plate-content">
<div class="text">
<img :src="platetextList[currentIndex].text" alt="" />
<img :src="currentContentImg" alt="" />
</div>
</div>
</div>
@@ -175,17 +235,12 @@ const currentHighlight = computed(() =>
<!-- 引导步骤 -->
<GuideSteps
:visible="showGuide"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem>
<img src="@/assets/images/equipment/设备准备引导1.png" alt="" />
</GuideStepsItem>
</GuideSteps>
/>
</div>
</template>
<style lang="less" scoped>
+107 -85
View File
@@ -1,113 +1,135 @@
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const playing = ref(false)
const showStepNavigation = ref(false)
const currentVideoIndex = ref(0)
const navigationSteps = [
{
id: 'opening-shot',
label: '拍摄执行',
content: '开场建立',
type: 'shooting' as const
},
{
id: 'lens-language',
label: '拍摄执行',
content: '镜头语言',
type: 'shooting' as const
},
{
id: 'motion-capture',
label: '拍摄执行',
content: '动态跟拍与抓拍',
type: 'shooting' as const
}
]
const videoList = [
getPublicPath('video/vlog_renwuqujing_huamiangoutu.mp4'),
getPublicPath('video/vlog_renwuqujing_jichujingbieyunyong.mp4'),
getPublicPath('video/vlog_renwuqujing_zhuapai.mp4')
]
const currentVideo = computed(() => videoList[currentVideoIndex.value])
const handleClose = () => {
router.back()
}
const goTimeLapse = () => {
router.push({ name: 'VlogOnsiteTimeLapse' })
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
nextTick(() => {
playing.value = true
})
}
const goSlider = () => {
router.push({ name: 'VlogOnsiteSlider' })
const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const goSlowShutterDay = () => {
router.push({ name: 'VlogOnsiteSlowShutterDay' })
const handleEnded = () => {
if (currentVideoIndex.value >= videoList.length - 1) {
playing.value = false
return
}
currentVideoIndex.value++
nextTick(() => {
playing.value = true
})
}
const goSlowShutterNight = () => {
router.push({ name: 'VlogOnsiteSlowShutterNight' })
}
const goLowAngle = () => {
router.push({ name: 'VlogOnsiteLowAngle' })
}
const goRandom = () => {
router.push({ name: 'VlogOnsiteRandom' })
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
:src="currentVideo"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
:anchors="[]"
@ended="handleEnded"
>
<template #overlay>
<ImgButton
class="absolute right-0 top-0 z-20 pointer-events-auto"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<button
class="absolute right-4 top-1/2 z-20 -translate-y-1/2 w-12 h-12 rounded-full bg-blue-500/80 text-white transition-all duration-200 hover:bg-blue-500 pointer-events-auto flex items-center justify-center"
title="资源列表"
@click="showStepNavigation = true"
>
<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>
</template>
</Video>
<!-- 内容卡片 + 按钮 -->
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-6xl w-full bg-black/35 backdrop-blur-md rounded-3xl border border-white/15 p-10 space-y-8 text-white pointer-events-auto"
>
<div class="flex items-baseline justify-between gap-6">
<h1 class="text-5xl font-extrabold tracking-wide">现场拍摄</h1>
<span class="text-xl opacity-80">从不同维度练习你的镜头语言</span>
</div>
<p class="text-xl leading-8 opacity-90">
选择下面任意一个练习模块,进入对应的现场拍摄场景,按提示完成小任务,逐步建立自己的拍摄手感和创作节奏。
</p>
<div class="grid grid-cols-3 gap-6">
<button type="button" class="onsite-btn" @click="goTimeLapse">
<span class="onsite-btn-title">延时拍摄</span>
<span class="onsite-btn-desc">记录时间流动,让平凡场景变得有节奏、有变化。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlider">
<span class="onsite-btn-title">滑轨拍摄</span>
<span class="onsite-btn-desc">通过平稳移动镜头,营造空间感和叙事推进感。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlowShutterDay">
<span class="onsite-btn-title">慢门拍摄 · 白天</span>
<span class="onsite-btn-desc">在白天环境下练习慢门,营造水流、车流等运动轨迹。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlowShutterNight">
<span class="onsite-btn-title">慢门拍摄 · 夜晚</span>
<span class="onsite-btn-desc">利用城市灯光,拍出具有节奏感和氛围感的夜景画面。</span>
</button>
<button type="button" class="onsite-btn" @click="goLowAngle">
<span class="onsite-btn-title">低角度拍摄</span>
<span class="onsite-btn-desc">从低视角观察世界,突出主体气势,改变日常视角。</span>
</button>
<button type="button" class="onsite-btn" @click="goRandom">
<span class="onsite-btn-title">创作随机性</span>
<span class="onsite-btn-desc">跟随直觉自由取景,捕捉那些意料之外的画面惊喜。</span>
</button>
</div>
</div>
</div>
<!-- 关闭 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
<StepNavigation
:visible="showStepNavigation"
:background-image="getStaticImage('work/vlog背景全屏待定')"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_015.MP3')">
接下来,我们进入最核心的实拍环节。看看一条真实 Vlog,是如何一步步被拍出来的。
</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
.onsite-btn {
@apply relative flex flex-col items-start justify-between rounded-2xl px-6 py-5 text-left bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/30 transition-all duration-200 cursor-pointer;
}
.onsite-btn-title {
@apply text-2xl font-bold mb-2;
}
.onsite-btn-desc {
@apply text-base leading-7 opacity-80;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">低角度拍摄</h1>
<p class="text-lg leading-8 opacity-90">
尝试把镜头放低,接近地面或桌面,从下往上看主体,观察画面气势和空间的变化。
</p>
<p class="text-base leading-7 opacity-80">
你可以拍摄人物、建筑或生活小物件,注意前景、主体与背景的组合,让画面更具故事感和张力。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">创作随机性</h1>
<p class="text-lg leading-8 opacity-90">
不预设太多规则,带着手机走一圈,只要看到有感觉的光线、人物或场景,就果断按下录制键。
</p>
<p class="text-base leading-7 opacity-80">
可以从“声音”“光线”“动作”这三个维度去寻找灵感,把这些零散片段记录下来,之后再在剪辑中重新组合,形成属于你的故事。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">滑轨拍摄</h1>
<p class="text-lg leading-8 opacity-90">
使用滑轨或模拟滑轨移动(例如沿桌面平移手机),练习横向、前后、环绕等不同方向的移动镜头。
</p>
<p class="text-base leading-7 opacity-80">
重点关注移动速度的稳定性,以及前景、主体和背景在画面中的关系,尝试拍出有空间层次感的镜头。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">慢门拍摄 · 白天</h1>
<p class="text-lg leading-8 opacity-90">
在白天选择有连续运动元素的场景(如喷泉、水流、人群、车流等),尝试使用慢门制造“运动模糊”的效果。
</p>
<p class="text-base leading-7 opacity-80">
建议:固定机位,尽量降低感光度,必要时使用减光配件;注意避免画面过曝,同时保留主体的清晰度与背景的动感。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">慢门拍摄 · 夜晚</h1>
<p class="text-lg leading-8 opacity-90">
在夜晚的城市路口、桥上或高处,尝试记录车流灯轨、行人轨迹等富有节奏感的光线运动。
</p>
<p class="text-base leading-7 opacity-80">
建议:寻找安全稳定的拍摄点,将设备固定好,控制好曝光时间与
ISO,避免过度抖动,让光轨清晰流畅。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,157 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
// 延时拍摄视频 URL(占位,后续可替换)
const timeLapseVideoUrl =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
const handleBack = () => {
router.back()
}
// 交互一 & 交互二:选择题
const questions = ref<Question[]>([
{
id: 1,
title: '交互一|观察判断(引导问题)',
description:
'画面状态:展示一张「江边建筑 + 天空」的静态画面截图(不播放视频)。如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
options: [
{ id: 'a', label: '建筑的外形结构' },
{ id: 'b', label: '天空的颜色' },
{ id: 'c', label: '时间的变化' },
{ id: 'd', label: '画面的清晰度' }
],
correctOptionId: 'c',
mediaSrc: 'shooting/延时场景静帧.png' // 占位:请按需替换为真实图片
},
{
id: 2,
title: '交互二|方法选择(技术判断)',
description:
'画面状态:同一场景的短视频首帧(仍未播放)。如果希望在很短时间内,让观众明显感知"时间正在流动",你会选择哪种拍摄方式?',
options: [
{ id: 'a', label: '手持拍摄' },
{ id: 'b', label: '慢动作拍摄' },
{ id: 'c', label: '延时拍摄' },
{ id: 'd', label: '推拉镜头' }
],
correctOptionId: 'c',
mediaSrc: 'shooting/延时场景静帧.png' // 占位:请按需替换为真实图片
}
])
const showObserveStep = ref(false)
const showSummary = ref(false) // 是否显示交互四总结
const videoPlaying = ref(false) // 视频播放状态
// 答题完成,进入交互三(视频)
const handleQuestionsComplete = () => {
showObserveStep.value = true
showSummary.value = false // 先显示交互三(视频)
}
// 视频播放完成,进入交互四总结
const handleVideoEnded = () => {
showSummary.value = true
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<!-- 交互一 & 交互二:选择题 -->
<QuestionQuiz
v-if="!showObserveStep"
:questions="questions"
@complete="handleQuestionsComplete"
/>
<!-- 交互三:观察型 - 播放视频 -->
<div
v-if="showObserveStep && !showSummary"
class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none"
>
<div
class="max-w-7xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-12 space-y-8 text-white pointer-events-auto"
>
<div>
<h2 class="text-2xl font-bold mb-4">交互三|观察型(无选项)</h2>
<p class="text-xl leading-7 opacity-90 mb-5">
播放延时拍摄视频(延时拍摄 .mp4)。在这个延时画面中,请注意天空变化与光影移动的节奏 ——
它们在短时间内被连续表现出来,这就是延时拍摄让「时间流动」变得可见的关键。
</p>
<div class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]">
<Video
:src="timeLapseVideoUrl"
v-model="videoPlaying"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
fit="contain"
@ended="handleVideoEnded"
/>
</div>
</div>
</div>
</div>
<!-- 交互四:总结 - 显示文本内容 -->
<div
v-if="showObserveStep && showSummary"
class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none"
>
<div
class="max-w-6xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-12 space-y-6 text-white pointer-events-auto"
>
<div>
<h2 class="text-2xl font-bold mb-5">交互四|总结</h2>
<!-- 总结文本内容 -->
<div class="space-y-3">
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">延时拍摄的主要目的</span>,<span class="px-1 rounded"
>是交代时间流逝与环境状态</span
>。
</p>
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">这里的拍摄对象是江边的建筑群与天空</span>。<span
class="px-1 rounded"
>通过固定机位的连续拍摄</span
>,<span class="px-1 rounded">将原本需要较长时间才能观察到的</span
><span class="px-1 rounded">天空变化和光影移动</span>,<span class="px-1 rounded"
>压缩到一个较短的镜头中呈现</span
>。
</p>
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">这种处理方式</span>,<span class="px-1 rounded"
>非常适合放置在视频片头阶段</span
>,<span class="px-1 rounded">用于快速建立时间感与空间环境</span>,<span
class="px-1 rounded"
>而不会占用过多时长</span
>。
</p>
</div>
</div>
</div>
</div>
<!-- 返回按钮 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -9,7 +9,6 @@ import StartOverlay from '../components/StartOverlay.vue'
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useVlogStore } from '@/stores'
const router = useRouter()
const start = ref(false)
@@ -114,7 +113,6 @@ const captureScene = async (): Promise<void> => {
*/
const handleComplete = (): void => {
showGuide.value = false
useVlogStore().setVlogUnlockProgress(3)
router.push({
name: 'Vlog'
})
@@ -283,7 +281,7 @@ const handleComplete = (): void => {
<!-- 引导步骤 -->
<GuideSteps v-if="showGuide" positionOffsetY="80%" @complete="handleComplete">
<GuideStepsItem> 实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~ </GuideStepsItem>
<GuideStepsItem>实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Overlay } from '@/components/Overlay'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
import videoSrc from '/video/xinshou_jiuzhonggoutufa.mp4'
import KnowledgeCard from '@/components/KnowledgeCard/KnowledgeCard.vue'
@@ -102,18 +102,24 @@ const back = () => {
<ImgButton image="shooting/按钮-取景地B.png" @click="handleClickB" />
</div>
<div class="shooting-composition">
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
<ImgButton
class="shooting-composition-btn"
image="shooting/构图指南.png"
@click="showModal = true"
/>
</div>
</div>
<!-- 引导步骤 -->
<GuideSteps
:highlight-selector="'.mybutton'"
:highlight-selector="'.shooting-composition-btn'"
:highlight-padding="12"
:highlight-radius="20"
position-offset-y="85%"
position-offset-y="60%"
>
<GuideStepsItem> 在走进拍摄场景之前,我们先来查看一下构图指南。 </GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_014.MP3')">
在走进拍摄场景之前,我们先来回顾一下三种常见的构图方法。
</GuideStepsItem>
</GuideSteps>
<!-- 保存图片弹窗 -->
+35
View File
@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('img/资源筹备-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('vlog/img/资源筹备.png')" alt="" />
</div>
<!-- <GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps> -->
</div>
</template>
+25 -128
View File
@@ -1,110 +1,63 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale'
import { useVlogStore } from '@/stores'
const router = useRouter()
const showDoubao = ref(false)
const vlog = useVlogStore()
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
{
id: 'step1',
content: '先认识 Vlog:点击右侧「1.vlog初识」继续',
highlightSelector: '.img-switch-item--vlog'
},
{
id: 'step2',
content: '这是主要内容区域的边框',
highlightSelector: '.work-border'
}
]
const stepIndex = ref(1)
const back = () => {
router.back()
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
const clickdoubao = () => {
vlog.setVlogUnlockProgress(2)
router.push({
name: 'Vlog'
})
}
const { scale, offsetX, offsetY } = useScale()
// const { scale, offsetX, offsetY } = useScale()
onMounted(() => {
ImagePreview({
images: [getStaticImage('vlog/脚本切图/我的志愿者手记拍摄脚本.png')],
startIndex: 0,
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value }),
onClose: () => {
showDoubao.value = true
}
})
// ImagePreview({
// images: [getStaticImage('vlog/脚本切图/我的志愿者手记拍摄脚本.png')],
// startIndex: 0,
// maskClosable: true,
// getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value }),
// onClose: () => {
// showDoubao.value = true
// }
// })
})
</script>
<template>
<div class="writing-index">
<div class="relative w-full h-full overflow-hidden">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat writing-bg"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.VLOG.BIAOGEBG) }"
class="absolute inset-0 bg-cover bg-center bg-no-repeat overflow-y-auto"
:style="{ backgroundImage: getBackgroundImage('img/背景.png') }"
>
<div>
<div class="writing-title">
<img src="@/assets/images/vlog/标题栏-中间.png" alt="" class="titledibu" />
<img
src="@/assets/images/vlog/制作变焦镜/文字标题/《我的志愿者手记》分镜头脚本.png"
alt=""
class="titletext"
/>
</div>
<div class="doubao" @click="clickdoubao">
<img
src="@/assets/images/vlog/制作变焦镜/ai脚本视频占位图.png"
alt=""
class="doubao-img"
/>
</div>
<div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div>
</div>
<!-- <div v-else>
<div class="writing-title">
<img src="@/assets/images/vlog/标题栏-中间.png" alt="" class="titledibu" />
<img
src="@/assets/images/vlog/制作变焦镜/文字标题/《我的志愿者手记》分镜头脚本.png"
alt=""
class="titletext"
/>
</div>
<div class="biaogehome" @click="deletebiaoge()">
<img
src="@/assets/images/vlog/脚本切图/我的志愿者手记拍摄脚本.png"
alt=""
class="biaoge-img"
/>
<img src="@/assets/images/midterm/叉.png" alt="" class="biaoge-x" />
</div>
<div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div>
</div> -->
<img class="absolute top-40" :src="getStaticImage('vlog/img/脚本编写.png')" />
<!-- 引导步骤 -->
<GuideSteps
v-if="showDoubao"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
hidden:false
>
@@ -115,59 +68,3 @@ onMounted(() => {
</div>
</div>
</template>
<style lang="less" scoped>
.writing-index {
width: 100%;
height: 100%;
position: absolute;
}
.shooting-return {
position: absolute;
right: -90%;
top: 0;
width: 50%;
height: 50%;
}
.doubao .doubao-img {
width: 100%;
height: 100%;
}
.biaoge-x {
position: absolute;
top: -20px;
right: 0px;
width: 70px;
cursor: pointer;
z-index: 9999999;
}
.writing-title {
.titledibu {
position: absolute;
top: 17px;
left: 30%;
}
.titletext {
position: absolute;
top: 30px;
left: 40%;
}
}
.biaogehome {
position: absolute;
top: 165px;
left: 5%;
width: 94%;
}
.biaogehome .biaoge-img {
height: 896px !important;
width: 1800px;
display: block;
}
</style>
+2 -11
View File
@@ -13,23 +13,14 @@
class="titletext"
/>
</div>
<video :src="fourth" controls></video>
<!-- <div class="shooting-return">
<img src="@/assets/images/shooting/返回.png" alt="" @click="back" />
</div> -->
<!-- 引导步骤 -->
<!-- <GuideSteps :steps="steps" :highlight-selector="currentHighlight" :highlight-padding="12"
:highlight-radius="20" @step-change="handleStepChange" position-offset-y="85%" hidden:false>
<GuideStepsItem>
<img src="@/assets/images/equipment/设备准备引导1.png" alt="" />
</GuideStepsItem>
</GuideSteps> -->
<Video :src="fourth" controls :muted="false" :volume="1"></Video>
</div>
</div>
</template>
<script lang="tsx" setup>
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import fourth from '/video/vlog_renwuqujing_jiaobencehua.mp4'
import { Video } from '@/components'
</script>
<style lang="less">
.writing-index {
+34
View File
@@ -0,0 +1,34 @@
<script lang="ts" setup>
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
// import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const back = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="back"
/>
<!-- 背景 -->
<img class="absolute inset-0" :src="getStaticImage('img/背景.png')" alt="" />
<!-- 标题 -->
<img class="absolute top-5 z-10" :src="getStaticImage('img/资源筹备-标题.png')" alt="" />
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('vlog/img/分镜制作.png')" alt="" />
</div>
<!-- <GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps> -->
</div>
</template>
+31 -19
View File
@@ -22,7 +22,20 @@ const playing = ref(false)
const status = ref(false)
//引导步骤
const steps = ref([
const ideaSteps = [
{
id: 'idea-step1',
content: '一条优秀的 Vlog,往往不是记录"大事",而是认真记录生活里的小事。',
audioSrc: getPublicPath('audio/py_2_004.MP3')
},
{
id: 'idea-step2',
content: '试着把刚才学到的方法用起来,你会发现,原来平凡的日子里,也藏着许多值得被记录的瞬间。',
audioSrc: getPublicPath('audio/py_2_005.MP3')
}
]
const steps = ref<{ id: string; content: string; audioSrc?: string }[]>([
{
id: 'step1',
content: '试着用这些方法记录身边的小事,你会发现,原来平凡的日子里有这么多值得捕捉的瞬间。'
@@ -83,13 +96,13 @@ 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(() => {
@@ -99,6 +112,14 @@ const handleVideoChange = () => {
}
const handleClose = () => {
if (key.value === 'idea') {
if (vlog.vlogUnlockProgress === 0) vlog.setVlogUnlockProgress(1)
router.push({
name: 'Vlog',
query: { guide: 'return' }
})
return
}
router.back()
}
@@ -106,10 +127,6 @@ const image = computed(() => route.query.image as string)
const key = computed(() => route.query.key as string)
const label = computed(() => route.query.label as string)
const handleClick = () => {}
const handlePlay = () => {}
// 示例:简单锚点配置(可按需替换为真实题目数据)
// const anchors = ref([
// {
@@ -130,12 +147,7 @@ const handleComplete = () => {
if (key.value === 'profession') {
if (!vlog.isVlogUnlock) vlog.setIsVlogUnlock(true)
router.push({
name: 'Work'
})
} else {
if (vlog.vlogUnlockProgress == 0) vlog.setVlogUnlockProgress(1)
router.push({
name: 'Vlog'
name: 'VlogWork'
})
}
}
@@ -155,6 +167,7 @@ const handleStepNavigationClose = () => {
onMounted(() => {
if (key.value === 'idea') {
steps.value = ideaSteps
status.value = true
}
})
@@ -212,7 +225,6 @@ onMounted(() => {
:controls="true"
:hideNativeControls="true"
:anchors="[]"
@play="handlePlay"
@ended="onEnded"
>
<!-- <template #anchor="{ anchor, resume, unmute }">
+30 -21
View File
@@ -1,10 +1,10 @@
<script lang="tsx" name="Work" setup>
defineOptions({ name: 'Work' })
<script lang="tsx" name="VlogWork" setup>
defineOptions({ name: 'VlogWork' })
import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import { GuideSteps } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
@@ -17,7 +17,6 @@ const vlog = useVlogStore()
type WorkTab = 'vlog' | 'case' | 'profession' | 'idea'
const selected = ref<WorkTab>('case')
const stepIndex = ref(0)
const workSideSwitcherRef = ref<InstanceType<typeof WorkSideSwitcher>>()
const isGuideVlogTask = computed(() => vlog.isGuideVlogTask)
@@ -27,10 +26,24 @@ const isVlogUnlock = computed(() => vlog.isVlogUnlock)
const steps = [
{
id: 'step1',
content:
'现在我们要一起走进 Vlog 的世界。简单说,Vlog 就是用视频记录生活的一种方式 —— 它像一本会动的日记,能把我们身边的小事、瞬间的心情,通过镜头变得生动又具体。'
content: '现在我们要一起走进 Vlog 的世界。',
audioSrc: getPublicPath('audio/py_2_001.MP3'),
highlightSelector: '.work-border'
},
{ id: 'step2', content: '接下来,我们就通过一个具体的案例,看看 Vlog 是怎么“捕捉美好” 的吧。' }
{
id: 'step2',
content:
'Vlog 是用视频记录生活的一种方式。它像一本会动的日记,能把日常里的小事、真实的情绪,以及那些稍纵即逝的瞬间,用镜头生动地保存下来。',
audioSrc: getPublicPath('audio/py_2_002.MP3'),
highlightSelector: '.work-border'
},
{
id: 'step3',
content:
'接下来,我们将通过一个具体案例,一起看看 Vlog 是如何发现生活中的美好、记录生活中的温度。',
audioSrc: getPublicPath('audio/py_2_003.MP3'),
highlightSelector: '.img-switch-item--case'
}
]
//切换图片
@@ -62,14 +75,6 @@ const currentItem = computed(() => {
return sideSwitcherItems.value.find(item => item.key === selected.value)
})
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
)
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const handleSideSwitcherChange = (value: WorkTab) => {
if (selected.value === value) return
}
@@ -99,7 +104,6 @@ const handleGuideComplete = () => {
}
const handleOpenGuide = () => {
stepIndex.value = 0
vlog.setIsGuideVlogTask(true)
}
@@ -135,6 +139,13 @@ function openImages(urls: string[], startIndex = 0) {
class="work-border absolute top-[140px] left-24 inset-0 bg-cover bg-center bg-no-repeat z-10 w-[1360px] h-[800px] p-10"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BORDER) }"
>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
>
<img class="w-40" :src="getStaticImage('work/小手')" alt="" />
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
</div>
<div
class="flex items-center pl-16 w-[1357px] h-[265px] absolute -bottom-16 left-0 bg-cover bg-center bg-no-repeat z-20"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.TITLE) }"
@@ -142,8 +153,8 @@ function openImages(urls: string[], startIndex = 0) {
<span
class="mt-24 text-white font-bold text-6xl [text-shadow:0_2px_4px_rgba(0,0,0,0.2),0_0_6px_rgba(0,207,255,0.9),0_0_14px_rgba(0,207,255,0.7)]"
>
{{ currentItem?.description }}</span
>
{{ currentItem?.description }}
</span>
</div>
<img
@click="handleImageClick"
@@ -174,10 +185,8 @@ function openImages(urls: string[], startIndex = 0) {
<GuideSteps
v-if="isGuideVlogTask"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
@complete="handleGuideComplete"
@skip="handleGuideComplete"
position-offset-y="85%"