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

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

No files matched your search

@@ -1,7 +1,7 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed } from 'vue'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps } from '@/components/Guide'
import { onBeforeUnmount, ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -11,39 +11,102 @@ const back = () => {
// 跳转设备准备
router.back()
}
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
]
const stepIndex = ref(0)
const steps = ref([
{
id: 'step1',
content:
'设备准备妥当后,我们就该走进真实场景,开启实地取景的环节啦,一起去捕捉那些适合拍摄的画面吧!',
audioSrc: getPublicPath('audio/py_2_013.MP3')
}
])
const showGuide = ref(false)
const guideTriggered = ref(false)
const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
const viewedMenuKeys = ref<Set<string>>(new Set())
const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
// 菜单项数据
const menuItems = ref([
{ id: 'phone', name: '手机', disabled: false },
{ id: 'phone-stand', name: '手机支架', disabled: true },
{ id: 'bluetooth-remote', name: '蓝牙遥控器', disabled: true },
{ id: 'stabilizer', name: '稳定器', disabled: true },
{ id: 'props', name: '常用道具', disabled: true },
{ id: 'phone-lens', name: '手机辅助箱头', disabled: true }
type EquipmentCategoryId = 'phone' | 'phone-stabilizer' | 'wireless-lavalier-mic'
interface EquipmentProduct {
img: string
}
interface EquipmentCategory {
id: EquipmentCategoryId
name: string
disabled: boolean
titleImg: string
contentImg: string
products: EquipmentProduct[]
}
const categories = ref<EquipmentCategory[]>([
{
id: 'phone',
name: '手机',
disabled: false,
titleImg: getStaticImage('equipment/标题-苹果.png'),
contentImg: getStaticImage('equipment/内容-苹果.png'),
products: [
{ img: getStaticImage('equipment/设备-手机1.png') },
{ img: getStaticImage('equipment/设备-手机2.png') }
]
},
{
id: 'phone-stabilizer',
name: '手机稳定器',
disabled: false,
titleImg: getStaticImage('equipment/手机稳定器标题.png'),
contentImg: getStaticImage('equipment/手机稳定器图文.png'),
products: [{ img: getStaticImage('equipment/手机稳定器.png') }]
},
{
id: 'wireless-lavalier-mic',
name: '无线领夹麦克风',
disabled: false,
titleImg: getStaticImage('equipment/无线领夹麦克风标题.png'),
contentImg: getStaticImage('equipment/无线领夹麦克风图文.png'),
products: [{ img: getStaticImage('equipment/无线领夹麦克风.png') }]
}
])
const phoneList = ref([
{ phoneImg: getStaticImage('equipment/设备-手机1.png') },
{ phoneImg: getStaticImage('equipment/设备-手机2.png') }
])
categories.value = categories.value.map(c => ({
...c,
disabled: (c.products?.length ?? 0) === 0
}))
const platetitleList = ref([
{ title: getStaticImage('equipment/标题-苹果.png') },
{ title: getStaticImage('equipment/标题-安卓小米.png') }
])
// 默认选中的菜单也算作已查看
if (categories.value[0] && !categories.value[0].disabled) {
viewedMenuKeys.value.add(categories.value[0].id)
}
const platetextList = ref([
{ text: getStaticImage('equipment/内容-苹果.png') },
{ text: getStaticImage('equipment/内容-安卓小米.png') }
])
const maybeTriggerGuide = () => {
if (guideTriggered.value) return
const enabledIds = categories.value.filter(c => !c.disabled).map(c => c.id)
if (enabledIds.length === 0) return
const allViewed = enabledIds.every(id => viewedMenuKeys.value.has(id))
if (!allViewed) return
guideTriggered.value = true
guideTimer.value = setTimeout(() => {
showGuide.value = true
guideTimer.value = null
}, 3000)
}
onBeforeUnmount(() => {
if (guideTimer.value) {
clearTimeout(guideTimer.value)
guideTimer.value = null
}
})
const currentCategory = computed(
() => categories.value[selectedMenuIndex.value] ?? categories.value[0]
)
const currentProducts = computed(() => currentCategory.value?.products ?? [])
const currentTitleImg = computed(() => currentCategory.value?.titleImg ?? '')
const currentContentImg = computed(() => currentCategory.value?.contentImg ?? '')
const hiddenbarclick = () => {
hiddenbar.value = !hiddenbar.value
}
@@ -51,15 +114,17 @@ const hiddenbarclick = () => {
// 选择菜单项
const selectMenuItem = (index: number, item: { id: string; name: string; disabled: boolean }) => {
if (item.disabled) {
console.log(`菜单项 "${item.name}" 已被禁用`)
return
}
selectedMenuIndex.value = index
currentIndex.value = 0
viewedMenuKeys.value.add(item.id)
maybeTriggerGuide()
}
// 下一张图片
const nextPhone = () => {
if (currentIndex.value < phoneList.value.length - 1) {
if (currentIndex.value < currentProducts.value.length - 1) {
currentIndex.value++
}
}
@@ -70,17 +135,9 @@ const prevPhone = () => {
currentIndex.value--
}
}
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
</script>
<template>
<div class="equipment-index">
<div class="equipment-index overflow-hidden">
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat equipment-bg"
@@ -93,7 +150,7 @@ const currentHighlight = computed(() =>
<div class="equipment-item" :hidden="hiddenbar">
<div class="menu-list">
<div
v-for="(item, index) in menuItems"
v-for="(item, index) in categories"
:key="item.id"
class="menu-item"
:class="{
@@ -118,7 +175,10 @@ const currentHighlight = computed(() =>
<div class="equipment-bt">
<img src="@/assets/images/equipment/按钮选中-设备准备.png" alt="" />
</div>
<div :class="hiddenbar ? 'equipment-unfold1' : 'equipment-unfold2'" @click="hiddenbarclick()">
<div
:class="['cursor-pointer', hiddenbar ? 'equipment-unfold1' : 'equipment-unfold2']"
@click="hiddenbarclick()"
>
<img
:src="
hiddenbar
@@ -139,12 +199,12 @@ const currentHighlight = computed(() =>
<img src="@/assets/images/equipment/按钮-左滑.png" alt="" />
</div>
<div class="equipment-phoneImg">
<img :src="phoneList[currentIndex].phoneImg" alt="" />
<img :src="currentProducts[currentIndex]?.img" alt="" />
</div>
<div
class="equipment-rightbutton"
@click="nextPhone"
:style="{ visibility: currentIndex < phoneList.length - 1 ? 'visible' : 'hidden' }"
:style="{ visibility: currentIndex < currentProducts.length - 1 ? 'visible' : 'hidden' }"
>
<img src="@/assets/images/equipment/按钮-右滑.png" alt="" />
</div>
@@ -155,11 +215,11 @@ const currentHighlight = computed(() =>
</div>
<div class="title">
<img :src="platetitleList[currentIndex].title" alt="" />
<img :src="currentTitleImg" alt="" />
</div>
<div class="plate-content">
<div class="text">
<img :src="platetextList[currentIndex].text" alt="" />
<img :src="currentContentImg" alt="" />
</div>
</div>
</div>
@@ -175,17 +235,12 @@ const currentHighlight = computed(() =>
<!-- 引导步骤 -->
<GuideSteps
:visible="showGuide"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem>
<img src="@/assets/images/equipment/设备准备引导1.png" alt="" />
</GuideStepsItem>
</GuideSteps>
/>
</div>
</template>
<style lang="less" scoped>
+107 -85
View File
@@ -1,113 +1,135 @@
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const playing = ref(false)
const showStepNavigation = ref(false)
const currentVideoIndex = ref(0)
const navigationSteps = [
{
id: 'opening-shot',
label: '拍摄执行',
content: '开场建立',
type: 'shooting' as const
},
{
id: 'lens-language',
label: '拍摄执行',
content: '镜头语言',
type: 'shooting' as const
},
{
id: 'motion-capture',
label: '拍摄执行',
content: '动态跟拍与抓拍',
type: 'shooting' as const
}
]
const videoList = [
getPublicPath('video/vlog_renwuqujing_huamiangoutu.mp4'),
getPublicPath('video/vlog_renwuqujing_jichujingbieyunyong.mp4'),
getPublicPath('video/vlog_renwuqujing_zhuapai.mp4')
]
const currentVideo = computed(() => videoList[currentVideoIndex.value])
const handleClose = () => {
router.back()
}
const goTimeLapse = () => {
router.push({ name: 'VlogOnsiteTimeLapse' })
const handleStepClick = (index: number) => {
currentVideoIndex.value = index
nextTick(() => {
playing.value = true
})
}
const goSlider = () => {
router.push({ name: 'VlogOnsiteSlider' })
const handleStepNavigationClose = () => {
showStepNavigation.value = false
}
const goSlowShutterDay = () => {
router.push({ name: 'VlogOnsiteSlowShutterDay' })
const handleEnded = () => {
if (currentVideoIndex.value >= videoList.length - 1) {
playing.value = false
return
}
currentVideoIndex.value++
nextTick(() => {
playing.value = true
})
}
const goSlowShutterNight = () => {
router.push({ name: 'VlogOnsiteSlowShutterNight' })
}
const goLowAngle = () => {
router.push({ name: 'VlogOnsiteLowAngle' })
}
const goRandom = () => {
router.push({ name: 'VlogOnsiteRandom' })
const handleIntroGuideComplete = () => {
nextTick(() => {
playing.value = true
})
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="relative w-full h-full overflow-hidden bg-black">
<Video
:src="currentVideo"
v-model="playing"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
:anchors="[]"
@ended="handleEnded"
>
<template #overlay>
<ImgButton
class="absolute right-0 top-0 z-20 pointer-events-auto"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<button
class="absolute right-4 top-1/2 z-20 -translate-y-1/2 w-12 h-12 rounded-full bg-blue-500/80 text-white transition-all duration-200 hover:bg-blue-500 pointer-events-auto flex items-center justify-center"
title="资源列表"
@click="showStepNavigation = true"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M3 4H17M3 8H17M3 12H17M3 16H17"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
</template>
</Video>
<!-- 内容卡片 + 按钮 -->
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-6xl w-full bg-black/35 backdrop-blur-md rounded-3xl border border-white/15 p-10 space-y-8 text-white pointer-events-auto"
>
<div class="flex items-baseline justify-between gap-6">
<h1 class="text-5xl font-extrabold tracking-wide">现场拍摄</h1>
<span class="text-xl opacity-80">从不同维度练习你的镜头语言</span>
</div>
<p class="text-xl leading-8 opacity-90">
选择下面任意一个练习模块,进入对应的现场拍摄场景,按提示完成小任务,逐步建立自己的拍摄手感和创作节奏。
</p>
<div class="grid grid-cols-3 gap-6">
<button type="button" class="onsite-btn" @click="goTimeLapse">
<span class="onsite-btn-title">延时拍摄</span>
<span class="onsite-btn-desc">记录时间流动,让平凡场景变得有节奏、有变化。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlider">
<span class="onsite-btn-title">滑轨拍摄</span>
<span class="onsite-btn-desc">通过平稳移动镜头,营造空间感和叙事推进感。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlowShutterDay">
<span class="onsite-btn-title">慢门拍摄 · 白天</span>
<span class="onsite-btn-desc">在白天环境下练习慢门,营造水流、车流等运动轨迹。</span>
</button>
<button type="button" class="onsite-btn" @click="goSlowShutterNight">
<span class="onsite-btn-title">慢门拍摄 · 夜晚</span>
<span class="onsite-btn-desc">利用城市灯光,拍出具有节奏感和氛围感的夜景画面。</span>
</button>
<button type="button" class="onsite-btn" @click="goLowAngle">
<span class="onsite-btn-title">低角度拍摄</span>
<span class="onsite-btn-desc">从低视角观察世界,突出主体气势,改变日常视角。</span>
</button>
<button type="button" class="onsite-btn" @click="goRandom">
<span class="onsite-btn-title">创作随机性</span>
<span class="onsite-btn-desc">跟随直觉自由取景,捕捉那些意料之外的画面惊喜。</span>
</button>
</div>
</div>
</div>
<!-- 关闭 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
<StepNavigation
:visible="showStepNavigation"
:background-image="getStaticImage('work/vlog背景全屏待定')"
:steps="navigationSteps"
:current-step-index="currentVideoIndex"
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_015.MP3')">
接下来,我们进入最核心的实拍环节。看看一条真实 Vlog,是如何一步步被拍出来的。
</GuideStepsItem>
</GuideSteps>
</div>
</template>
<style scoped>
.onsite-btn {
@apply relative flex flex-col items-start justify-between rounded-2xl px-6 py-5 text-left bg-white/5 hover:bg-white/10 border border-white/10 hover:border-white/30 transition-all duration-200 cursor-pointer;
}
.onsite-btn-title {
@apply text-2xl font-bold mb-2;
}
.onsite-btn-desc {
@apply text-base leading-7 opacity-80;
video {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">低角度拍摄</h1>
<p class="text-lg leading-8 opacity-90">
尝试把镜头放低,接近地面或桌面,从下往上看主体,观察画面气势和空间的变化。
</p>
<p class="text-base leading-7 opacity-80">
你可以拍摄人物、建筑或生活小物件,注意前景、主体与背景的组合,让画面更具故事感和张力。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">创作随机性</h1>
<p class="text-lg leading-8 opacity-90">
不预设太多规则,带着手机走一圈,只要看到有感觉的光线、人物或场景,就果断按下录制键。
</p>
<p class="text-base leading-7 opacity-80">
可以从“声音”“光线”“动作”这三个维度去寻找灵感,把这些零散片段记录下来,之后再在剪辑中重新组合,形成属于你的故事。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">滑轨拍摄</h1>
<p class="text-lg leading-8 opacity-90">
使用滑轨或模拟滑轨移动(例如沿桌面平移手机),练习横向、前后、环绕等不同方向的移动镜头。
</p>
<p class="text-base leading-7 opacity-80">
重点关注移动速度的稳定性,以及前景、主体和背景在画面中的关系,尝试拍出有空间层次感的镜头。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,40 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">慢门拍摄 · 白天</h1>
<p class="text-lg leading-8 opacity-90">
在白天选择有连续运动元素的场景(如喷泉、水流、人群、车流等),尝试使用慢门制造“运动模糊”的效果。
</p>
<p class="text-base leading-7 opacity-80">
建议:固定机位,尽量降低感光度,必要时使用减光配件;注意避免画面过曝,同时保留主体的清晰度与背景的动感。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,41 +0,0 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
const handleBack = () => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-5xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-10 space-y-6 text-white pointer-events-auto"
>
<h1 class="text-4xl font-extrabold tracking-wide">慢门拍摄 · 夜晚</h1>
<p class="text-lg leading-8 opacity-90">
在夜晚的城市路口、桥上或高处,尝试记录车流灯轨、行人轨迹等富有节奏感的光线运动。
</p>
<p class="text-base leading-7 opacity-80">
建议:寻找安全稳定的拍摄点,将设备固定好,控制好曝光时间与
ISO,避免过度抖动,让光轨清晰流畅。
</p>
</div>
</div>
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -1,157 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
const router = useRouter()
// 延时拍摄视频 URL(占位,后续可替换)
const timeLapseVideoUrl =
'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
const handleBack = () => {
router.back()
}
// 交互一 & 交互二:选择题
const questions = ref<Question[]>([
{
id: 1,
title: '交互一|观察判断(引导问题)',
description:
'画面状态:展示一张「江边建筑 + 天空」的静态画面截图(不播放视频)。如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
options: [
{ id: 'a', label: '建筑的外形结构' },
{ id: 'b', label: '天空的颜色' },
{ id: 'c', label: '时间的变化' },
{ id: 'd', label: '画面的清晰度' }
],
correctOptionId: 'c',
mediaSrc: 'shooting/延时场景静帧.png' // 占位:请按需替换为真实图片
},
{
id: 2,
title: '交互二|方法选择(技术判断)',
description:
'画面状态:同一场景的短视频首帧(仍未播放)。如果希望在很短时间内,让观众明显感知"时间正在流动",你会选择哪种拍摄方式?',
options: [
{ id: 'a', label: '手持拍摄' },
{ id: 'b', label: '慢动作拍摄' },
{ id: 'c', label: '延时拍摄' },
{ id: 'd', label: '推拉镜头' }
],
correctOptionId: 'c',
mediaSrc: 'shooting/延时场景静帧.png' // 占位:请按需替换为真实图片
}
])
const showObserveStep = ref(false)
const showSummary = ref(false) // 是否显示交互四总结
const videoPlaying = ref(false) // 视频播放状态
// 答题完成,进入交互三(视频)
const handleQuestionsComplete = () => {
showObserveStep.value = true
showSummary.value = false // 先显示交互三(视频)
}
// 视频播放完成,进入交互四总结
const handleVideoEnded = () => {
showSummary.value = true
}
</script>
<template>
<div class="relative w-full h-full overflow-hidden">
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
></div>
<!-- 交互一 & 交互二:选择题 -->
<QuestionQuiz
v-if="!showObserveStep"
:questions="questions"
@complete="handleQuestionsComplete"
/>
<!-- 交互三:观察型 - 播放视频 -->
<div
v-if="showObserveStep && !showSummary"
class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none"
>
<div
class="max-w-7xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-12 space-y-8 text-white pointer-events-auto"
>
<div>
<h2 class="text-2xl font-bold mb-4">交互三|观察型(无选项)</h2>
<p class="text-xl leading-7 opacity-90 mb-5">
播放延时拍摄视频(延时拍摄 .mp4)。在这个延时画面中,请注意天空变化与光影移动的节奏 ——
它们在短时间内被连续表现出来,这就是延时拍摄让「时间流动」变得可见的关键。
</p>
<div class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]">
<Video
:src="timeLapseVideoUrl"
v-model="videoPlaying"
:muted="false"
:volume="1"
:controls="true"
:hideNativeControls="true"
fit="contain"
@ended="handleVideoEnded"
/>
</div>
</div>
</div>
</div>
<!-- 交互四:总结 - 显示文本内容 -->
<div
v-if="showObserveStep && showSummary"
class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none"
>
<div
class="max-w-6xl w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-12 space-y-6 text-white pointer-events-auto"
>
<div>
<h2 class="text-2xl font-bold mb-5">交互四|总结</h2>
<!-- 总结文本内容 -->
<div class="space-y-3">
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">延时拍摄的主要目的</span>,<span class="px-1 rounded"
>是交代时间流逝与环境状态</span
>。
</p>
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">这里的拍摄对象是江边的建筑群与天空</span>。<span
class="px-1 rounded"
>通过固定机位的连续拍摄</span
>,<span class="px-1 rounded">将原本需要较长时间才能观察到的</span
><span class="px-1 rounded">天空变化和光影移动</span>,<span class="px-1 rounded"
>压缩到一个较短的镜头中呈现</span
>。
</p>
<p class="text-xl leading-7 opacity-90">
<span class="px-1 rounded">这种处理方式</span>,<span class="px-1 rounded"
>非常适合放置在视频片头阶段</span
>,<span class="px-1 rounded">用于快速建立时间感与空间环境</span>,<span
class="px-1 rounded"
>而不会占用过多时长</span
>。
</p>
</div>
</div>
</div>
</div>
<!-- 返回按钮 -->
<ImgButton
class="absolute right-0 top-0 z-20"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
</div>
</template>
@@ -9,7 +9,6 @@ import StartOverlay from '../components/StartOverlay.vue'
import LoadingOverlay from '@/components/Loading/LoadingOverlay.vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useVlogStore } from '@/stores'
const router = useRouter()
const start = ref(false)
@@ -114,7 +113,6 @@ const captureScene = async (): Promise<void> => {
*/
const handleComplete = (): void => {
showGuide.value = false
useVlogStore().setVlogUnlockProgress(3)
router.push({
name: 'Vlog'
})
@@ -283,7 +281,7 @@ const handleComplete = (): void => {
<!-- 引导步骤 -->
<GuideSteps v-if="showGuide" positionOffsetY="80%" @complete="handleComplete">
<GuideStepsItem> 实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~ </GuideStepsItem>
<GuideStepsItem>实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Overlay } from '@/components/Overlay'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
import videoSrc from '/video/xinshou_jiuzhonggoutufa.mp4'
import KnowledgeCard from '@/components/KnowledgeCard/KnowledgeCard.vue'
@@ -102,18 +102,24 @@ const back = () => {
<ImgButton image="shooting/按钮-取景地B.png" @click="handleClickB" />
</div>
<div class="shooting-composition">
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
<ImgButton
class="shooting-composition-btn"
image="shooting/构图指南.png"
@click="showModal = true"
/>
</div>
</div>
<!-- 引导步骤 -->
<GuideSteps
:highlight-selector="'.mybutton'"
:highlight-selector="'.shooting-composition-btn'"
:highlight-padding="12"
:highlight-radius="20"
position-offset-y="85%"
position-offset-y="60%"
>
<GuideStepsItem> 在走进拍摄场景之前,我们先来查看一下构图指南。 </GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_014.MP3')">
在走进拍摄场景之前,我们先来回顾一下三种常见的构图方法。
</GuideStepsItem>
</GuideSteps>
<!-- 保存图片弹窗 -->