feat: 完善课程引导与实战评价体验

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.7 committed 2026-05-09 09:53:06 +08:00
1 parent 6f8252472f
commit 1f8723aabd
122 files changed
+2595 -1325

No files matched your search

@@ -2,7 +2,8 @@
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { useVlogStore } from '@/stores/modules/vlog'
@@ -25,25 +26,27 @@ const navigationSteps = ref([
label: '拍摄执行',
content: '开场建立',
videoUrl: getPublicPath('video/vlog/on-site/Vlogmk_kcjl_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'lens-language',
label: '拍摄执行',
content: '镜头语言',
videoUrl: getPublicPath('video/vlog/on-site/Vlogmk_jtyy_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'motion-capture',
label: '拍摄执行',
content: '动态跟拍与抓拍',
videoUrl: getPublicPath('video/vlog/on-site/Vlogmk_dtgpyzp_batch.mp4'),
type: 'shooting' as const
}
type: 'shooting' as const,
},
])
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const handleClose = () => {
router.back()
@@ -102,7 +105,12 @@ const handleIntroGuideComplete = () => {
title="资源列表"
@click="showStepNavigation = true"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M3 4H17M3 8H17M3 12H17M3 16H17"
stroke="currentColor"
@@ -123,9 +131,14 @@ const handleIntroGuideComplete = () => {
@close="handleStepNavigationClose"
/>
<GuideSteps v-if="showIntroGuide" position-offset-y="85%" @complete="handleIntroGuideComplete">
<GuideSteps
v-if="showIntroGuide"
position-offset-y="85%"
@complete="handleIntroGuideComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_2_015.MP3')">
接下来,我们进入最核心的实拍环节。看看一条真实 Vlog,是如何一步步被拍出来的。
接下来,我们进入最核心的实拍环节。看看一条真实
Vlog,是如何一步步被拍出来的。
</GuideStepsItem>
</GuideSteps>
</div>
@@ -1,7 +1,29 @@
<script setup lang="ts">
import { ImgButton } from '@/components/Button'
import { getStaticImage } from '@/utils'
defineEmits<{
start: []
}>()
const props = defineProps<{
scene: 'a' | 'b'
}>()
</script>
<template>
<div class="absolute bottom-0">
<div class="relative">
<img src="@/assets/images/shooting/开始取景.png" alt="" />
<img
:src="
getStaticImage(
props.scene === 'a'
? 'vlog/shooting/开始拍摄-热带植物馆.png'
: 'vlog/shooting/开始拍摄-沉水栈道.png',
)
"
alt=""
/>
<ImgButton
class="absolute bottom-[46%] left-[49%] -translate-x-1/2"
image="shooting/按钮-开始取景.png"
@@ -10,11 +32,3 @@
</div>
</div>
</template>
<script setup lang="ts">
import { ImgButton } from '@/components/Button'
defineEmits<{
start: []
}>()
</script>
@@ -35,7 +35,7 @@ const {
getTransform,
currentOffsetX,
currentOffsetY,
handleWheel
handleWheel,
} = useViewfinderA()
// 刻度盘配置常量
@@ -88,7 +88,7 @@ const dialTicks = computed<DialTick[]>(() => {
angle,
isMajor,
isInteger,
label: isInteger ? zoom + 'x' : ''
label: isInteger ? zoom + 'x' : '',
})
}
return ticks
@@ -116,7 +116,7 @@ const capturePhoto = async () => {
if (isLoading.value) return
isCapturing.value = true
await new Promise(resolve => setTimeout(resolve, FLASH_DURATION))
await new Promise((resolve) => setTimeout(resolve, FLASH_DURATION))
await capture({
width: CANVAS_WIDTH,
@@ -124,7 +124,7 @@ const capturePhoto = async () => {
scale: CANVAS_SCALE,
delay: UI_UPDATE_DELAY,
resultDelay: RESULT_DISPLAY_DELAY,
onDraw: ctx => {
onDraw: (ctx) => {
if (!photoImage.value) {
throw new Error('Photo image not found')
}
@@ -142,12 +142,12 @@ const capturePhoto = async () => {
-CANVAS_WIDTH / 2,
-CANVAS_HEIGHT / 2,
CANVAS_WIDTH,
CANVAS_HEIGHT
CANVAS_HEIGHT,
)
},
onComplete: () => {
isCapturing.value = false
}
},
})
}
</script>
@@ -156,10 +156,21 @@ const capturePhoto = async () => {
<div class="w-full h-full">
<div
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage('vlog/shooting/热带植物馆-模糊.png') }"
:style="{
backgroundImage: getBackgroundImage(
'vlog/shooting/热带植物馆-模糊.png',
),
}"
>
<ImgButton image="vlog/shooting/热带植物馆侧边.png" class="title-button absolute left-0" />
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
<ImgButton
image="vlog/shooting/热带植物馆侧边.png"
class="title-button absolute left-0"
/>
<ImgButton
image="shooting/返回.png"
class="back-button absolute right-0"
@click="back"
/>
<p
class="absolute left-1/2 -translate-x-1/2 top-[260px] text-red-500 text-4xl leading-normal font-bold"
@@ -167,10 +178,17 @@ const capturePhoto = async () => {
操作说明:在手机取景框区域,滚动鼠标滚轮可缩放画面,按住鼠标左键拖动可调整画面位置。
</p>
<StartOverlay v-if="!start" @start="start = true" />
<StartOverlay
scene="a"
v-if="!start"
@start="start = true"
/>
<!-- 手机拍照界面 -->
<div class="absolute bottom-0 w-full h-[766px]" v-else>
<div
class="absolute bottom-0 w-full h-[766px]"
v-else
>
<img
class="phone-frame absolute w-full h-[766px] z-10 pointer-events-none"
src="@/assets/images/shooting/shoot.png"
@@ -189,7 +207,7 @@ const capturePhoto = async () => {
:class="{
'transition-transform duration-300': !isDragging,
'cursor-grab': !isDragging,
'cursor-grabbing': isDragging
'cursor-grabbing': isDragging,
}"
:style="{ transform: getTransform() }"
src="@/assets/images/vlog/shooting/热带植物馆-清晰.png"
@@ -216,7 +234,9 @@ const capturePhoto = async () => {
<div
class="shooting-decor relative pointer-events-none top-[19.2%] left-[48.3%] -translate-x-1/2 w-[871px] h-[420px] bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage('shooting/拍摄装饰.png') }"
:style="{
backgroundImage: getBackgroundImage('shooting/拍摄装饰.png'),
}"
>
<!-- 仿苹果半圆盘缩放控件 -->
<div
@@ -240,13 +260,17 @@ const capturePhoto = async () => {
<!-- 圆心在容器右侧外部,形成左侧弧形 -->
<div
class="dial-wheel absolute top-1/2 right-[-480px] w-[600px] h-[600px] rounded-full bg-black/30 backdrop-blur-md border-[5px] border-white/20 transition-transform duration-100 ease-out origin-center cursor-grab active:cursor-grabbing"
:style="{ transform: `translateY(-50%) rotate(${dialRotation}deg)` }"
:style="{
transform: `translateY(-50%) rotate(${dialRotation}deg)`,
}"
>
<div
v-for="tick in dialTicks"
:key="tick.zoom"
class="absolute top-1/2 left-0 w-full h-0 flex justify-start items-center origin-center pointer-events-auto"
:style="{ transform: `translate(0, -50%) rotate(${tick.angle}deg)` }"
:style="{
transform: `translate(0, -50%) rotate(${tick.angle}deg)`,
}"
@click.stop="setZoom(tick.zoom)"
>
<!-- 刻度线本体 (位于圆盘左边缘) -->
@@ -255,17 +279,21 @@ const capturePhoto = async () => {
:class="{
'w-10 h-1 opacity-90': tick.isInteger,
'w-6 h-0.5 opacity-60': !tick.isInteger && tick.isMajor,
'w-3 h-[1px] opacity-40': !tick.isMajor
'w-3 h-[1px] opacity-40': !tick.isMajor,
}"
></div>
<!-- 刻度文字 -->
<div
v-if="tick.isInteger"
class="absolute left-[52px] text-white/90 font-bold text-lg drop-shadow-md"
:style="{ transform: `rotate(${-tick.angle - dialRotation}deg)` }"
:style="{
transform: `rotate(${-tick.angle - dialRotation}deg)`,
}"
>
<span
:class="{ 'text-yellow-400 scale-110': currentZoom === tick.zoom }"
:class="{
'text-yellow-400 scale-110': currentZoom === tick.zoom,
}"
class="block transition-all duration-200"
>
{{ tick.label }}
@@ -285,22 +313,39 @@ const capturePhoto = async () => {
</div>
<!-- 结果弹窗 -->
<Overlay :visible="showModal" @close="handleModalClose">
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
<Overlay
:visible="showModal"
@close="handleModalClose"
>
<div
class="relative w-[1000px] h-[600px] flex items-center justify-center"
>
<!-- 截图内容 -->
<div v-if="capturedImageUrl" class="bg-white p-2 rounded-md -rotate-6">
<img :src="capturedImageUrl" alt="拍摄结果" />
<div
v-if="capturedImageUrl"
class="bg-white p-2 rounded-md -rotate-6"
>
<img
:src="capturedImageUrl"
alt="拍摄结果"
/>
</div>
<!-- 操作按钮 -->
<div class="absolute bottom-1/2 -right-1/3 translate-y-1/2">
<ImgButton image="shooting/按钮-前往下一个取景地" @click="handleFinish" />
<ImgButton
image="shooting/按钮-前往下一个取景地"
@click="handleFinish"
/>
</div>
</div>
</Overlay>
</div>
<LoadingOverlay :visible="isLoading" text="正在出片中..." />
<LoadingOverlay
:visible="isLoading"
text="正在出片中..."
/>
</div>
</template>
@@ -324,7 +369,19 @@ const capturePhoto = async () => {
}
.mask-gradient {
mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
-webkit-mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent);
mask-image: linear-gradient(
to bottom,
transparent,
black 15%,
black 85%,
transparent
);
-webkit-mask-image: linear-gradient(
to bottom,
transparent,
black 15%,
black 85%,
transparent
);
}
</style>
@@ -36,14 +36,23 @@ const handleModalClose = () => {
}
// 自由取景框模式
const { viewfinderContainer, viewfinderBox, viewfinder, startDrag, startResize } = useViewfinderB()
const {
viewfinderContainer,
viewfinderBox,
viewfinder,
startDrag,
startResize,
} = useViewfinderB()
const handleStartDrag = (event: MouseEvent | TouchEvent): void => {
hideInstruction()
startDrag(event)
}
const handleStartResize = (direction: ResizeDirection, event: MouseEvent | TouchEvent): void => {
const handleStartResize = (
direction: ResizeDirection,
event: MouseEvent | TouchEvent,
): void => {
hideInstruction()
startResize(direction, event)
}
@@ -74,7 +83,7 @@ const captureScene = async (): Promise<void> => {
height: viewfinder.value.height,
scale: CANVAS_SCALE,
delay: UI_UPDATE_DELAY,
onDraw: ctx => {
onDraw: (ctx) => {
if (!backgroundImage.value) {
throw new Error('Background image not found')
}
@@ -96,7 +105,7 @@ const captureScene = async (): Promise<void> => {
0, // 目标 x
0, // 目标 y
viewfinder.value.width, // 目标 width
viewfinder.value.height // 目标 height
viewfinder.value.height, // 目标 height
)
},
onComplete: () => {
@@ -104,7 +113,7 @@ const captureScene = async (): Promise<void> => {
setTimeout(() => {
showGuide.value = true
}, GUIDE_SHOW_DELAY)
}
},
})
}
@@ -114,7 +123,7 @@ const captureScene = async (): Promise<void> => {
const handleComplete = (): void => {
showGuide.value = false
router.push({
name: 'Vlog'
name: 'Vlog',
})
}
</script>
@@ -126,11 +135,14 @@ const handleComplete = (): void => {
:style="{
backgroundImage: start
? getBackgroundImage('vlog/shooting/沉水栈道-清晰.png')
: getBackgroundImage('vlog/shooting/沉水栈道-模糊.png')
: getBackgroundImage('vlog/shooting/沉水栈道-模糊.png'),
}"
>
<!-- 标题 -->
<ImgButton image="vlog/shooting/沉水栈道侧边.png" class="title-button absolute left-0" />
<ImgButton
image="vlog/shooting/沉水栈道侧边.png"
class="title-button absolute left-0"
/>
<!-- 返回按钮 -->
<ImgButton
@@ -146,10 +158,17 @@ const handleComplete = (): void => {
操作说明:拖动取景框四角调整裁剪范围,点击“取景”键就完成啦!
</p>
<StartOverlay v-if="!start" @start="handleStart" />
<StartOverlay
scene="b"
v-if="!start"
@start="handleStart"
/>
<!-- 取景 -->
<div class="absolute bottom-0 w-full h-full" v-else>
<div
class="absolute bottom-0 w-full h-full"
v-else
>
<!-- 隐藏的背景图片,用于 Canvas 绘制 -->
<img
ref="backgroundImage"
@@ -160,14 +179,17 @@ const handleComplete = (): void => {
/>
<!-- 取景框容器 -->
<div ref="viewfinderContainer" class="relative w-full h-full">
<div
ref="viewfinderContainer"
class="relative w-full h-full"
>
<!-- 四个方向的遮罩层 -->
<!-- 上遮罩 -->
<div
class="mask-layer absolute left-0 right-0 bg-black/60 pointer-events-none z-10"
:style="{
top: 0,
height: viewfinder.y + 'px'
height: viewfinder.y + 'px',
}"
></div>
<!-- 下遮罩 -->
@@ -175,7 +197,7 @@ const handleComplete = (): void => {
class="mask-layer absolute left-0 right-0 bg-black/60 pointer-events-none z-10"
:style="{
top: viewfinder.y + viewfinder.height + 'px',
bottom: 0
bottom: 0,
}"
></div>
<!-- 左遮罩 -->
@@ -184,7 +206,7 @@ const handleComplete = (): void => {
:style="{
top: viewfinder.y + 'px',
width: viewfinder.x + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
></div>
<!-- 右遮罩 -->
@@ -193,7 +215,7 @@ const handleComplete = (): void => {
:style="{
top: viewfinder.y + 'px',
left: viewfinder.x + viewfinder.width + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
></div>
@@ -205,13 +227,15 @@ const handleComplete = (): void => {
left: viewfinder.x + 'px',
top: viewfinder.y + 'px',
width: viewfinder.width + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
@mousedown="handleStartDrag"
@touchstart="handleStartDrag"
>
<!-- 九宫格辅助线 -->
<div class="grid-lines absolute inset-0 grid grid-cols-3 grid-rows-3">
<div
class="grid-lines absolute inset-0 grid grid-cols-3 grid-rows-3"
>
<div
v-for="i in 9"
:key="i"
@@ -220,7 +244,7 @@ const handleComplete = (): void => {
'border-t-0': i <= 3,
'border-b-0': i >= 7,
'border-l-0': (i - 1) % 3 === 0,
'border-r-0': i % 3 === 0
'border-r-0': i % 3 === 0,
}"
></div>
</div>
@@ -254,19 +278,34 @@ const handleComplete = (): void => {
class="capture-button-container absolute z-20 -translate-x-1/2 -translate-y-1/2"
:style="{
left: viewfinder.x + viewfinder.width / 2 + 'px',
top: viewfinder.y + viewfinder.height + 'px'
top: viewfinder.y + viewfinder.height + 'px',
}"
>
<ImgButton class="w-36" image="shooting/按钮-取景" @click="captureScene" />
<ImgButton
class="w-36"
image="shooting/按钮-取景"
@click="captureScene"
/>
</div>
</div>
<!-- 结果弹窗 -->
<Overlay :visible="showModal" @close="handleModalClose">
<div class="relative w-[1000px] h-[600px] flex items-center justify-center">
<Overlay
:visible="showModal"
@close="handleModalClose"
>
<div
class="relative w-[1000px] h-[600px] flex items-center justify-center"
>
<!-- 截图内容 -->
<div v-if="capturedImageUrl" class="bg-white p-2 rounded-md -rotate-6">
<img :src="capturedImageUrl" alt="拍摄结果" />
<div
v-if="capturedImageUrl"
class="bg-white p-2 rounded-md -rotate-6"
>
<img
:src="capturedImageUrl"
alt="拍摄结果"
/>
</div>
<!-- 操作按钮 -->
@@ -277,11 +316,20 @@ const handleComplete = (): void => {
</Overlay>
</div>
<LoadingOverlay :visible="isLoading" text="正在出片中..." />
<LoadingOverlay
:visible="isLoading"
text="正在出片中..."
/>
<!-- 引导步骤 -->
<GuideSteps v-if="showGuide" positionOffsetY="80%" @complete="handleComplete">
<GuideStepsItem>实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~</GuideStepsItem>
<GuideSteps
v-if="showGuide"
positionOffsetY="80%"
@complete="handleComplete"
>
<GuideStepsItem>
实地取景已全部完成,现在跟着我走进现场开始正式的拍摄吧~
</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -37,7 +37,7 @@ const currentItem = ref<Btn>({
name: '',
btnImage: '',
images: [],
desc: ''
desc: '',
})
const btnList = ref<Btn[]>([
@@ -45,23 +45,23 @@ const btnList = ref<Btn[]>([
key: '1',
name: '居中构图',
btnImage: 'shooting/居中构图底板',
desc: '居中构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用',
images: ['shooting/居中构图1', 'shooting/居中构图2']
desc: '居中构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用。',
images: ['shooting/居中构图1', 'shooting/居中构图2'],
},
{
key: '2',
name: '三分法构图',
btnImage: 'shooting/三分法构图底板',
desc: '三分法构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调',
images: ['shooting/图片待定1', 'shooting/图片待定2']
desc: '三分法构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调。',
images: ['shooting/图片待定1', 'shooting/图片待定2'],
},
{
key: '3',
name: '对角线构图',
btnImage: 'shooting/对角线构图底板',
desc: '对角线构图属于引导线构图的一种,所以也是借助引导线的视觉引导效果来突出强调主体。另外对角线构图在画面中制造出对角线的走向即可。',
images: ['shooting/对角线构图1', 'shooting/对角线构图2']
}
images: ['shooting/对角线构图1', 'shooting/对角线构图2'],
},
])
const handleModalClose = () => {
@@ -96,22 +96,39 @@ const back = () => {
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage('vlog/shooting/实地取景底图') }"
:style="{
backgroundImage: getBackgroundImage('vlog/shooting/实地取景底图'),
}"
>
<div class="shooting-title">
<img :src="shootingtitle" alt="" />
<img
:src="shootingtitle"
alt=""
/>
</div>
<div class="shooting-return">
<ImgButton image="shooting/返回.png" @click="back" />
<ImgButton
image="shooting/返回.png"
@click="back"
/>
</div>
<div class="absolute left-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
<ImgButton image="vlog/shooting/热带植物馆-入口.png" @click="handleClickA" />
<ImgButton
image="vlog/shooting/热带植物馆-入口.png"
@click="handleClickA"
/>
</div>
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
<ImgButton image="vlog/shooting/沉水栈道-入口.png" @click="handleClickB" />
<ImgButton
image="vlog/shooting/沉水栈道-入口.png"
@click="handleClickB"
/>
</div>
<div class="shooting-composition btn-1">
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
<ImgButton
image="shooting/构图指南.png"
@click="showModal = true"
/>
</div>
</div>
@@ -133,7 +150,7 @@ const back = () => {
:visible="showModal"
:clickable="false"
:customStyle="{
backgroundImage: getBackgroundImage('shooting/构图指南底图')
backgroundImage: getBackgroundImage('shooting/构图指南底图'),
}"
customClass="inset-0 bg-cover bg-center bg-no-repeat"
contentClass="relative w-full h-full"
@@ -146,7 +163,10 @@ const back = () => {
/>
<!-- 构图指南 -->
<ImgButton class="absolute left-0 top-0 z-10" image="shooting/标题-构图指南" />
<ImgButton
class="absolute left-0 top-0 z-10"
image="shooting/标题-构图指南"
/>
<!-- 新手导航视频 -->
<ImgButton
@@ -175,8 +195,16 @@ const back = () => {
@close="showModal2 = false"
/>
<!-- 视频 -->
<Overlay :visible="showModal3" :clickable="false">
<Video :src="videoSrc" controls :volume="1" :muted="false"></Video>
<Overlay
:visible="showModal3"
:clickable="false"
>
<Video
:src="videoSrc"
controls
:volume="1"
:muted="false"
></Video>
<!-- 返回 -->
<ImgButton
class="absolute right-0 top-0 z-10"