forked from zhongyidian/super_video
feat: 完善访谈与vlog模块页面,重组设备与图片公共资源,新增音频管理与一班对接工具
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
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>
|
||||
|
||||
@@ -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="步骤导航"
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<!-- 知识卡片 -->
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
请点击 <img
|
||||
class="w-16"
|
||||
:src="getStaticImage('film/sx')"
|
||||
alt=""
|
||||
/> ,进入案例赏析!
|
||||
请点击
|
||||
<img class="w-16" :src="getStaticImage('film/sx')" alt="" />
|
||||
,进入案例赏析!
|
||||
</div>
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
+179
-120
@@ -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>
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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%"
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<!-- 保存图片弹窗 -->
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }">
|
||||
|
||||
@@ -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%"
|
||||
|
||||
Reference in new issue
Block a user