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,8 +2,12 @@
import { computed, nextTick, ref } from 'vue'
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 { GuideSteps, GuideStepsItem } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
@@ -27,44 +31,48 @@ const rawSteps = [
id: 'step2',
label: '摄影基础',
content: '手机摄像头介绍',
videoUrl: getPublicPath('video/new/shooting-skills/xinshou_shoujishexiangtoujieshao.mp4'),
type: 'post' as const
videoUrl: getPublicPath(
'video/new/shooting-skills/xinshou_shoujishexiangtoujieshao.mp4',
),
type: 'post' as const,
},
{
id: 'step3',
label: '摄影基础',
content: '景别概念介绍',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_jbgnjs_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step4',
label: '摄影基础',
content: '手机慢动作',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sjmdz_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step5',
label: '摄影基础',
content: '延时摄影',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_yssy_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step6',
label: '摄影基础',
content: '神奇的微距摄影',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_sqdwjsy_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step7',
label: '摄影基础',
content: '独具魅力的夜景摄影',
videoUrl: getPublicPath('video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4'),
type: 'post' as const
}
videoUrl: getPublicPath(
'video/new/shooting-skills/xsdh_djmldyjsy_batch.mp4',
),
type: 'post' as const,
},
]
const navigationSteps = ref(rawSteps)
@@ -77,7 +85,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
// 题目数据:景别概念介绍(index 1)、手机慢动作(index 2)、独具魅力的夜景摄影(index 5)
const questions = ref<Question[]>([])
@@ -90,24 +100,25 @@ const getQuestionsForVideo = (index: number): Question[] => {
{
id: 1,
title: '',
description: '(单选)如果你想让观众“看清环境、理解空间关系”,更适合用哪种镜头?',
description:
'(单选)如果你想让观众“看清环境、理解空间关系”,更适合用哪种镜头?',
options: [
{
id: 'a',
label: '面部特写视频'
label: '面部特写视频',
},
{
id: 'b',
label: '中景人物视频'
label: '中景人物视频',
},
{
id: 'c',
label: '全景环境视频'
}
label: '全景环境视频',
},
],
correctOptionId: 'c',
explanation: '全景更适合交代故事发生在哪里。'
}
explanation: '全景更适合交代故事发生在哪里。',
},
]
}
@@ -117,30 +128,32 @@ const getQuestionsForVideo = (index: number): Question[] => {
{
id: 2,
title: '',
description: '(多选题)下面这些拍摄场景中,哪些更适合使用"慢动作"来表现?',
description:
'(多选题)下面这些拍摄场景中,哪些更适合使用"慢动作"来表现?',
options: [
{
id: 'a',
label: '城市街道上车水马龙,车辆快速穿行'
label: '城市街道上车水马龙,车辆快速穿行',
},
{
id: 'b',
label: '甩头发、转身、跳跃等人物动作瞬间'
label: '甩头发、转身、跳跃等人物动作瞬间',
},
{
id: 'c',
label: '水花飞溅、雨滴落地、浪花拍岸'
label: '水花飞溅、雨滴落地、浪花拍岸',
},
{
id: 'd',
label: '风吹动衣摆、窗帘或树叶缓慢摆动'
}
label: '风吹动衣摆、窗帘或树叶缓慢摆动',
},
],
isMultiple: true,
correctOptionIds: ['b', 'c', 'd'],
correctOptionId: '', // 多选题时此字段不使用
explanation: '更适合用慢动作表现的是 B、C、D,它们都包含了"瞬间细节"和"节奏变化"。'
}
explanation:
'更适合用慢动作表现的是 B、C、D,它们都包含了"瞬间细节"和"节奏变化"。',
},
]
}
@@ -154,20 +167,20 @@ const getQuestionsForVideo = (index: number): Question[] => {
options: [
{
id: 'a',
label: '哪里好看就拍哪里'
label: '哪里好看就拍哪里',
},
{
id: 'b',
label: '设备越贵,画面越好'
label: '设备越贵,画面越好',
},
{
id: 'c',
label: '在合适的时间,用合适的方式拍'
}
label: '在合适的时间,用合适的方式拍',
},
],
correctOptionId: 'c',
explanation: '拍摄要学会判断时间、光线和方式。'
}
explanation: '拍摄要学会判断时间、光线和方式。',
},
]
}
@@ -252,7 +265,12 @@ const handleQuestionsComplete = () => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -2,7 +2,7 @@
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 from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
// 运镜初识 - 暂时用运镜的基本方式代替
@@ -55,16 +55,20 @@ const navigationSteps = ref([
id: 'step2',
label: '视觉素养',
content: '正确的手机拍摄持机姿势',
videoUrl: getPublicPath('video/new/movement/xinshou_zhengquedeshoujipaishe.mp4'),
type: 'planning' as const
videoUrl: getPublicPath(
'video/new/movement/xinshou_zhengquedeshoujipaishe.mp4',
),
type: 'planning' as const,
},
{
id: 'step3',
label: '视觉素养',
content: '运镜的基本方式',
videoUrl: getPublicPath('video/new/movement/xinshou_yunjingdejibenfangshi.mp4'),
type: 'planning' as const
}
videoUrl: getPublicPath(
'video/new/movement/xinshou_yunjingdejibenfangshi.mp4',
),
type: 'planning' as const,
},
])
// 步骤导航相关状态
@@ -75,7 +79,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const handleVideoChange = () => {
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
currentVideoIndex.value++
@@ -137,7 +143,12 @@ const handleStepNavigationClose = () => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
+19 -7
View File
@@ -2,7 +2,7 @@
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
@@ -32,15 +32,15 @@ const navigationSteps = ref([
label: '视觉素养',
content: '如何选择构图',
videoUrl: getPublicPath('video/new/composition/xsdh_rhxzgt_batch.mp4'),
type: 'planning' as const
type: 'planning' as const,
},
{
id: 'step2',
label: '视觉素养',
content: '九种经典构图方式',
videoUrl: getPublicPath('video/new/composition/xsdh_jzgtf_batch.mp4'),
type: 'planning' as const
}
type: 'planning' as const,
},
])
// 步骤导航相关状态
@@ -51,7 +51,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const handleVideoChange = () => {
if (currentVideoIndex.value < navigationSteps.value.length - 1) {
currentVideoIndex.value++
@@ -115,7 +117,12 @@ onMounted(() => {})
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -180,7 +187,12 @@ onMounted(() => {})
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="步骤导航"
>
<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"
+32 -17
View File
@@ -2,8 +2,11 @@
import { computed, nextTick, ref } from 'vue'
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 StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps } from '@/components/Guide'
import GuideStepsItem from '@/components/Guide/GuideStepsItem.vue'
@@ -53,22 +56,22 @@ const navigationSteps = ref([
label: '视觉素养',
content: '光的基础认知',
videoUrl: getPublicPath('video/new/light/xinshou_guangdejichurenzhi.mp4'),
type: 'planning' as const
type: 'planning' as const,
},
{
id: 'step3',
label: '视觉素养',
content: '光的四大属性',
videoUrl: getPublicPath('video/new/light/xinshou_guangdesidashuxing.mp4'),
type: 'planning' as const
type: 'planning' as const,
},
{
id: 'step4',
label: '视觉素养',
content: '拍摄的黄金时间',
videoUrl: getPublicPath('video/new/light/xsdh_psdhjsj_batch.mp4'),
type: 'planning' as const
}
type: 'planning' as const,
},
])
// 步骤导航相关状态
@@ -79,7 +82,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
// 题目数据:拍摄的黄金时间(index 2)
const questions = ref<Question[]>([])
@@ -92,24 +97,25 @@ const getQuestionsForVideo = (index: number): Question[] => {
{
id: 1,
title: '',
description: '(单选)同一条街道,在不同时间拍摄,哪一张画面更适合表现"温暖、舒服的日常感"?',
description:
'(单选)同一条街道,在不同时间拍摄,哪一张画面更适合表现"温暖、舒服的日常感"?',
options: [
{
id: 'a',
label: '正午直射光,阴影很硬,路面反光明显'
label: '正午直射光,阴影很硬,路面反光明显',
},
{
id: 'b',
label: '傍晚低角度阳光,墙面被染成金色'
label: '傍晚低角度阳光,墙面被染成金色',
},
{
id: 'c',
label: '夜晚路灯照明,光源复杂、色温混杂'
}
label: '夜晚路灯照明,光源复杂、色温混杂',
},
],
correctOptionId: 'b',
explanation: '光线角度越低,画面柔和,情绪放大'
}
explanation: '光线角度越低,画面柔和,情绪放大',
},
]
}
return []
@@ -195,7 +201,12 @@ const handleQuestionsComplete = () => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -243,9 +254,13 @@ const handleQuestionsComplete = () => {
<!-- 引导与高亮 -->
<GuideSteps v-if="isGuide" position-offset-y="85%" @complete="handleComplete">
<GuideSteps
v-if="isGuide"
position-offset-y="85%"
@complete="handleComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_009.MP3')">
光是摄影的灵魂,摄影高手一定是用光的高手
光是摄影的灵魂,摄影高手一定是用光的高手。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_1_010.MP3')">
现在, 让我们一起去认识光、读懂光。
@@ -2,7 +2,8 @@
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation, GuideSteps, GuideStepsItem } 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 { useBasicElementsStore } from '@/stores/modules/basic-elements'
@@ -26,72 +27,86 @@ const rawSteps = [
id: 'step2',
label: '剪映基础操作',
content: '初识剪映专业版界面和功能',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_csjyzybjmhgn_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_csjyzybjmhgn_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step3',
label: '剪映基础操作',
content: '时间线的用法详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_sjxdyfxj_batch.mp4'),
type: 'other' as const
type: 'other' as const,
},
{
id: 'step4',
label: '剪映基础操作',
content: '转场特效贴纸功能详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_zctxtzgnxj_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_zctxtzgnxj_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step5',
label: '剪映基础操作',
content: '如何给视频快速上字幕',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhgspksszm_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_rhgspksszm_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step6',
label: '剪映基础操作',
content: '音频的功能应用和处理',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_ypdgnyyhcl_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_ypdgnyyhcl_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step7',
label: '剪映基础操作',
content: '关键帧功能用法详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_gjzgnyfxj_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_gjzgnyfxj_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step8',
label: '剪映基础操作',
content: '蒙版功能用法详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_mbgnyfxj_batch.mp4'),
type: 'other' as const
type: 'other' as const,
},
{
id: 'step9',
label: '剪映基础操作',
content: '混合模式功能用法详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_hhmsgnyfxj_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_hhmsgnyfxj_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step10',
label: '剪映基础操作',
content: '曲线变速功能用法详解',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_qxbsgnyfxj_batch.mp4'),
type: 'other' as const
videoUrl: getPublicPath(
'video/new/editing-skills/xsdh_qxbsgnyfxj_batch.mp4',
),
type: 'other' as const,
},
{
id: 'step11',
label: '剪映基础操作',
content: '如何给画面调色',
videoUrl: getPublicPath('video/new/editing-skills/xsdh_rhghmts_batch.mp4'),
type: 'other' as const
}
type: 'other' as const,
},
]
const navigationSteps = ref(rawSteps)
@@ -163,7 +178,12 @@ const handleIntroGuideComplete = () => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -2,7 +2,10 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, QuestionQuiz, type Question } from '@/components'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { useFilmStore } from '@/stores'
@@ -15,11 +18,11 @@ const handleBack = () => {
// 声音转场视频 URL(暂时用其他视频代替)
const soundTransitionVideoUrl = getPublicPath(
'video/film/editing/xianchangpaishe_shengyingzhuanchang1.mp4'
'video/film/editing/xianchangpaishe_shengyingzhuanchang1.mp4',
)
// const soundTransitionVideoUrl2 = getPublicPath('video/on-site/xianchangpaishe_yanshipaishe.mp4')
const soundTransitionVideoUrl3 = getPublicPath(
'video/film/editing/xianchangpaishe_shengyingzhuanchang3.mp4'
'video/film/editing/xianchangpaishe_shengyingzhuanchang3.mp4',
)
// 视频播放状态
@@ -39,13 +42,14 @@ const questions = ref<Question[]>([
{
id: 1,
title: '画面感受判断',
description: '播放视频,听听看,你觉得这个声音,像是用来结束一段画面,还是开启一段新的内容?',
description:
'播放视频,听听看,你觉得这个声音,像是用来结束一段画面,还是开启一段新的内容?',
options: [
{ id: 'a', label: '开始' },
{ id: 'b', label: '结束' }
{ id: 'b', label: '结束' },
],
correctOptionId: '', // 引导题,无对错
mediaSrc: soundTransitionVideoUrl
mediaSrc: soundTransitionVideoUrl,
},
{
id: 2,
@@ -54,11 +58,11 @@ const questions = ref<Question[]>([
options: [
{ id: 'a', label: '直接切画面' },
{ id: 'b', label: '用相似声音连接' },
{ id: 'c', label: '用字幕说明' }
{ id: 'c', label: '用字幕说明' },
],
correctOptionId: 'b',
mediaSrc: 'film/on-site/声音转场jh_2.png'
}
mediaSrc: 'film/on-site/声音转场jh_2.png',
},
])
// 答题完成
@@ -70,7 +74,9 @@ const handleQuestionsComplete = () => {
</script>
<template>
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
<div
class="relative w-full h-full flex items-center justify-center overflow-hidden"
>
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
@@ -113,7 +119,9 @@ const handleQuestionsComplete = () => {
class="max-w-[1100px] w-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20 p-16 space-y-10 text-white pointer-events-auto"
>
<h2 class="text-4xl font-bold mb-4 leading-relaxed">结果验证</h2>
<div class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]">
<div
class="rounded-2xl overflow-hidden border border-white/20 bg-black/60 min-h-[500px]"
>
<Video
class="w-full h-full"
:src="soundTransitionVideoUrl3"
@@ -123,7 +131,9 @@ const handleQuestionsComplete = () => {
:volume="1"
/>
</div>
<div class="mt-6 p-6 rounded-xl bg-amber-500/20 border border-amber-400/30">
<div
class="mt-6 p-6 rounded-xl bg-amber-500/20 border border-amber-400/30"
>
<div class="text-xl font-semibold text-amber-300 mb-2">提示语:</div>
<div class="text-lg text-amber-100 leading-relaxed">
<p>相似的声音,</p>
@@ -2,8 +2,15 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { QuestionQuiz, type Question } from '@/components'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
const router = useRouter()
@@ -16,14 +23,15 @@ const questions = ref<Question[]>([
{
id: 1,
title: '画面感受判断',
description: '如果你身处滨江岸边,看到一位正在打太极的老人,你会选择哪个角度拍摄?',
description:
'如果你身处滨江岸边,看到一位正在打太极的老人,你会选择哪个角度拍摄?',
options: [
{ id: 'a', label: '平视拍摄' },
{ id: 'b', label: '俯拍' },
{ id: 'c', label: '低角度仰拍' }
{ id: 'c', label: '低角度仰拍' },
],
correctOptionId: '', // 引导题,没有正确答案,任意选择都可进入下一题
mediaSrc: 'film/on-site/低角度拍摄jh_1.png'
mediaSrc: 'film/on-site/低角度拍摄jh_1.png',
},
{
id: 2,
@@ -33,11 +41,11 @@ const questions = ref<Question[]>([
{ id: 'a', label: '放大了人物的真实身高' },
{ id: 'b', label: '强化了人物动作的力量感与气势' },
{ id: 'c', label: '让背景元素显得更加丰富' },
{ id: 'd', label: '让画面信息量变得更多' }
{ id: 'd', label: '让画面信息量变得更多' },
],
correctOptionId: 'b',
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_dijiaodu.mp4')
}
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_dijiaodu.mp4'),
},
])
const showSummaryStep = ref(false)
@@ -2,7 +2,10 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, QuestionQuiz, type Question } from '@/components'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getPublicPath, IMAGE_PATHS } from '@/utils'
const router = useRouter()
@@ -12,7 +15,9 @@ const handleBack = () => {
}
// 滑轨拍摄视频 URL
const sliderVideoUrl = getPublicPath('video/film/on-site/dygmk_hgydjt_batch.mp4')
const sliderVideoUrl = getPublicPath(
'video/film/on-site/dygmk_hgydjt_batch.mp4',
)
// 视频播放状态
const videoEnded = ref(false)
@@ -34,15 +39,16 @@ const questions = ref<Question[]>([
options: [
{
id: 'a',
label: '人物正对镜头说话,背景简单,镜头静止人物正对镜头说话,背景简单,镜头静止'
label:
'人物正对镜头说话,背景简单,镜头静止人物正对镜头说话,背景简单,镜头静止',
},
{ id: 'b', label: '人物站在窗边,前景有栏杆或墙体,镜头缓慢横向移动' },
{ id: 'c', label: '人物快速奔跑,摄影师跟着移动拍摄' }
{ id: 'c', label: '人物快速奔跑,摄影师跟着移动拍摄' },
],
correctOptionId: 'b',
explanation:
'滑轨镜头的价值,不在"动",而在稳定、克制地交代空间关系。当前景—人物—背景同时存在时,缓慢的横向移动,会让画面产生层次和叙事感,而不是分散注意力。'
}
'滑轨镜头的价值,不在"动",而在稳定、克制地交代空间关系。当前景—人物—背景同时存在时,缓慢的横向移动,会让画面产生层次和叙事感,而不是分散注意力。',
},
])
// 答题完成
@@ -52,7 +58,9 @@ const handleQuestionsComplete = () => {
</script>
<template>
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
<div
class="relative w-full h-full flex items-center justify-center overflow-hidden"
>
<!-- 视频播放区域 -->
<Video
class="w-full h-full"
@@ -2,8 +2,15 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { QuestionQuiz, type Question } from '@/components'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
const router = useRouter()
@@ -19,11 +26,12 @@ const questions = ref<Question[]>([
description: '在这个白天场景中,画面里的运动变化,是否容易被直接观察到?',
options: [
{ id: 'a', label: '是,变化本身已经很清楚' },
{ id: 'b', label: '否,变化存在,但不容易被完整感知' }
{ id: 'b', label: '否,变化存在,但不容易被完整感知' },
],
correctOptionId: 'b',
mediaSrc: 'film/on-site/白天慢门jh_1.png',
explanation: '白天光照条件下,许多运动变化存在于连续时间中,并不容易被单帧画面完整呈现。'
explanation:
'白天光照条件下,许多运动变化存在于连续时间中,并不容易被单帧画面完整呈现。',
},
{
id: 2,
@@ -33,11 +41,13 @@ const questions = ref<Question[]>([
options: [
{ id: 'a', label: '提高 ISO' },
{ id: 'b', label: '加装 ND 镜片,降低快门速度' },
{ id: 'c', label: '使用连拍模式' }
{ id: 'c', label: '使用连拍模式' },
],
correctOptionId: 'b',
mediaSrc: getPublicPath('video/film/on-site/xianchangpaishe_baitianmanmen.mp4')
}
mediaSrc: getPublicPath(
'video/film/on-site/xianchangpaishe_baitianmanmen.mp4',
),
},
])
const showSummaryStep = ref(false)
@@ -2,13 +2,18 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video, QuestionQuiz, type Question } from '@/components'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
const router = useRouter()
// 延时拍摄视频 URL(占位,后续可替换)
const timeLapseVideoUrl = getPublicPath('video/film/on-site/xianchangpaishe_yanshipaishe.mp4')
const timeLapseVideoUrl = getPublicPath(
'video/film/on-site/xianchangpaishe_yanshipaishe.mp4',
)
const handleBack = () => {
router.back()
@@ -19,29 +24,31 @@ const questions = ref<Question[]>([
{
id: 1,
title: '观察判断',
description: '如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
description:
'如果你只用一条普通视频来拍这个场景,你觉得画面最难被感知的是什么?',
options: [
{ id: 'a', label: '建筑的外形结构' },
{ id: 'b', label: '天空的颜色' },
{ id: 'c', label: '时间的变化' },
{ id: 'd', label: '画面的清晰度' }
{ id: 'd', label: '画面的清晰度' },
],
correctOptionId: 'c',
mediaSrc: 'film/on-site/延时拍摄jh_1.png' // 占位:请按需替换为真实图片
mediaSrc: 'film/on-site/延时拍摄jh_1.png', // 占位:请按需替换为真实图片
},
{
id: 2,
title: '方法选择',
description: '如果希望在很短时间内,让观众明显感知“时间正在流动”,你会选择哪种拍摄方式?',
description:
'如果希望在很短时间内,让观众明显感知“时间正在流动”,你会选择哪种拍摄方式?',
options: [
{ id: 'a', label: '手持拍摄' },
{ id: 'b', label: '慢动作拍摄' },
{ id: 'c', label: '延时拍摄' },
{ id: 'd', label: '推拉镜头' }
{ id: 'd', label: '推拉镜头' },
],
correctOptionId: 'c',
mediaSrc: 'film/on-site/延时拍摄jh_1.png' // 占位:请按需替换为真实图片
}
mediaSrc: 'film/on-site/延时拍摄jh_1.png', // 占位:请按需替换为真实图片
},
])
const showObserveStep = ref(false)
@@ -79,7 +86,9 @@ const handleQuestionsComplete = () => {
<!-- 总结文本内容 -->
<div class="w-2/6 text-2xl leading-10 opacity-90 space-y-3">
<h2 class="text-4xl font-bold mb-4 leading-relaxed">总结</h2>
<p>在这个延时画面中,注意天空变化与光影移动的节奏,它们在短时间内被连续呈现出来。</p>
<p>
在这个延时画面中,注意天空变化与光影移动的节奏,它们在短时间内被连续呈现出来。
</p>
<p>
<span>延时拍摄的主要目的</span>
@@ -4,7 +4,12 @@ import { useRouter } from 'vue-router'
import { GuideSteps } from '@/components/Guide'
import { Overlay } from '@/components/Overlay'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
import videoSrc from '/video/new/composition/xsdh_jzgtf_batch.mp4'
import KnowledgeCard from '@/components/KnowledgeCard/FilmKnowledgeCard.vue'
@@ -35,26 +40,26 @@ const steps = ref([
id: '1',
audioSrc: getPublicPath('audio/py_4_015.MP3'),
content: '哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~',
highlightSelector: '.btn-1'
}
highlightSelector: '.btn-1',
},
])
const guideSteps = ref([
{
id: '1',
content: '点击框架构图,查看构图方法',
highlightSelector: '.btn-item-1'
highlightSelector: '.btn-item-1',
},
{
id: '2',
content: '点击对称构图,查看构图方法',
highlightSelector: '.btn-item-2'
highlightSelector: '.btn-item-2',
},
{
id: '3',
content: '点击引导线构图,查看构图方法',
highlightSelector: '.btn-item-3'
}
highlightSelector: '.btn-item-3',
},
])
const showModal = ref(false)
@@ -65,7 +70,7 @@ const currentItem = ref<Btn>({
name: '',
btnImage: '',
images: [],
desc: ''
desc: '',
})
const btnList = ref<Btn[]>([
@@ -74,22 +79,25 @@ const btnList = ref<Btn[]>([
name: '框架构图',
btnImage: 'film/shooting/框架构图',
desc: '框架构图是拍摄时用“框架”把主体框起来,可以让画面更加有空间感及层次感,并且使用也并不复杂,比如镜子、门窗、山洞等,也可以说是光影框等。',
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图']
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图'],
},
{
key: '2',
name: '对称构图',
btnImage: 'film/shooting/对称构图',
desc: '对称构图是通过画面中轴线(水平 / 垂直)两侧元素镜像分布的构图方式,属于平衡类构图。它借对称的视觉均衡感,让画面显规整、稳定,同时强化主体秩序与美感。拍摄可找天然对称景物(如建筑、倒影),或调整角度让元素对称;略有不对称可后期微调,适合展现建筑、对称静物等规整场景。',
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图']
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图'],
},
{
key: '3',
name: '引导线构图',
btnImage: 'film/shooting/引导线构图',
desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)',
images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图']
}
desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)。',
images: [
'film/shooting/05引导线构图_底图',
'film/shooting/06引导线构图_底图',
],
},
])
const handleModalClose = () => {
@@ -135,19 +143,36 @@ const back = () => {
></div>
<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 class="btn-2" image="film/shooting/龙美术馆入口.png" @click="handleClickA" />
<ImgButton
class="btn-2"
image="film/shooting/龙美术馆入口.png"
@click="handleClickA"
/>
</div>
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
<ImgButton class="btn-3" image="film/shooting/油罐艺术公园入口.png" @click="handleClickB" />
<ImgButton
class="btn-3"
image="film/shooting/油罐艺术公园入口.png"
@click="handleClickB"
/>
</div>
<div class="shooting-composition btn-1">
<ImgButton image="shooting/构图指南.png" @click="openCompositionGuide" />
<ImgButton
image="shooting/构图指南.png"
@click="openCompositionGuide"
/>
</div>
<!-- 引导步骤 -->
@@ -164,7 +189,7 @@ const back = () => {
:visible="showModal"
:clickable="false"
:customStyle="{
backgroundImage: getBackgroundImage('film/shooting/构图指南底图')
backgroundImage: getBackgroundImage('film/shooting/构图指南底图'),
}"
customClass="inset-0 bg-cover bg-center bg-no-repeat"
contentClass="relative w-full h-full"
@@ -177,15 +202,25 @@ const back = () => {
/>
<!-- 构图指南 -->
<ImgButton class="absolute left-0 top-0 z-10" image="shooting/标题-构图指南" />
<ImgButton
class="absolute left-0 top-0 z-10"
image="shooting/标题-构图指南"
/>
<!-- 新手导航视频 -->
<div class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2">
<ImgButton image="shooting/新手导航-视频回顾" @click="showModal3 = true" />
<ImgButton
image="shooting/新手导航-视频回顾"
@click="showModal3 = true"
/>
<div
class="absolute left-[40%] top-[42%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
>
<img class="w-32" :src="getStaticImage('work/小手')" alt="" />
<img
class="w-32"
:src="getStaticImage('work/小手')"
alt=""
/>
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
</div>
</div>
@@ -220,8 +255,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"
+103 -25
View File
@@ -4,7 +4,7 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { useRouter } from 'vue-router'
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
import BackToTopIcon from '@/assets/images/components/置顶.png'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores/modules/film'
@@ -26,7 +26,9 @@ const isLastPage = computed<boolean>(() => currentPage.value >= TOTAL_PAGES - 1)
/** 是否正在从第二页回到第一页 */
const isScrollingBackToFirst = computed<boolean>(() => {
return isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
return (
isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
)
})
/** 第一页坐着的人物是否显示 */
@@ -42,7 +44,7 @@ const guideKeyPrefix = 'film-practice-page'
const steps = ref([
'欢迎来到实战操作台! 这里的内容很丰富,滚动鼠标就能浏览到全部内容啦~',
'点击【我的创作笔记】,编写创作思路~',
'点击【查看】按钮,欣赏他人实战作品~'
'点击【查看】按钮,欣赏他人实战作品~',
])
const currentGuideKey = computed(() => `${guideKeyPrefix}-${currentPage.value}`)
@@ -75,8 +77,8 @@ const worksList = ref<WorkItem[]>([
stars: 4,
path: '/video/film/practice/film_anli1.mp4',
description:
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
}
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介',
},
])
/** 左滑动作品列表 */
@@ -168,18 +170,44 @@ const handleWheel = (event: WheelEvent): void => {
}
}
const restoreScrollPage = (): void => {
const savedPage = filmStore.practiceScrollPage
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
currentPage.value = targetPage
animatingToPage.value = targetPage
requestAnimationFrame(() => {
if (containerRef.value) {
containerRef.value.scrollTop = targetPage * PAGE_HEIGHT
}
})
}
const saveScrollPage = (): void => {
const page = containerRef.value
? Math.round(containerRef.value.scrollTop / PAGE_HEIGHT)
: currentPage.value
const targetPage = Math.max(0, Math.min(page, TOTAL_PAGES - 1))
filmStore.setPracticeScrollPage(targetPage)
}
onMounted(() => {
restoreScrollPage()
showPageGuide()
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
})
onUnmounted(() => {
saveScrollPage()
containerRef.value?.removeEventListener('wheel', handleWheel)
})
</script>
<template>
<div ref="containerRef" class="w-full h-full overflow-auto">
<div
ref="containerRef"
class="w-full h-full overflow-auto"
>
<div class="relative min-h-full overflow-hidden">
<!-- 关闭 -->
<ImgButton
@@ -188,16 +216,19 @@ onUnmounted(() => {
@click="handleClose"
/>
<!-- 实战操作台 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat relative"
:style="{ backgroundImage: getBackgroundImage('film/practice/实战操作台首页- 电影感') }"
>
<div class="w-full h-[1080px] bg-[#F5EFE6] relative">
<ImgButton
class="absolute left-40 bottom-28 z-10"
class="absolute left-40 bottom-14 z-10"
image="vlog/practice/查看任务要求"
@click="scrollToNextPage"
/>
<!-- img -->
<img
class="absolute left-0 top-0 z-10"
draggable="false"
:src="getStaticImage('film/practice/电影')"
alt=""
/>
<img
class="absolute right-0"
draggable="false"
@@ -212,12 +243,18 @@ onUnmounted(() => {
>
<ImgButton
class="absolute right-20 bottom-0 z-99"
image="vlog/practice/我的创作笔记"
image="vlog/practice/我的创作笔记btn"
path="/film/practice/my-notes"
/>
<div class="task-panel w-[757px] h-full ml-10 overflow-y-auto" @wheel.stop>
<img :src="getStaticImage('film/practice/滑动任务面板-电影感')" alt="" />
<div
class="task-panel w-[757px] h-full ml-10 overflow-y-auto"
@wheel.stop
>
<img
:src="getStaticImage('film/practice/任务要求')"
alt=""
/>
</div>
<img
@@ -239,7 +276,9 @@ onUnmounted(() => {
<!-- 他人实战作品 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat pt-44 px-16"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/他人实战作品') }"
:style="{
backgroundImage: getBackgroundImage('vlog/practice/他人实战作品'),
}"
>
<!-- 作品列表 -->
<div class="w-full overflow-hidden">
@@ -254,15 +293,27 @@ onUnmounted(() => {
>
<!-- 作品图片 -->
<div class="w-full h-[280px] rounded-2xl overflow-hidden">
<img class="w-full h-full object-cover" :src="work.image" :alt="work.title" />
<img
class="w-full h-full object-cover"
:src="work.image"
:alt="work.title"
/>
</div>
<!-- 作品信息 -->
<div class="flex flex-col items-center pt-8 space-y-4">
<h3 class="text-5xl font-bold text-gray-800 mb-2">{{ work.title }}</h3>
<h3 class="text-5xl font-bold text-gray-800 mb-2">
{{ work.title }}
</h3>
<!-- 星星评分 -->
<StarRating :score="work.stars" size="lg" readonly />
<StarRating
:score="work.stars"
size="lg"
readonly
/>
<!-- 简介 -->
<p class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed">
<p
class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed"
>
{{ work.description }}
</p>
<!-- 查看按钮 -->
@@ -275,7 +326,7 @@ onUnmounted(() => {
</div>
</div>
<!-- 指示按钮 -->
<!-- 指示按钮,暂时不要
<div class="absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-16">
<img
class="cursor-pointer hover:scale-110 transition-transform"
@@ -286,19 +337,23 @@ onUnmounted(() => {
/>
<img
class="cursor-pointer hover:scale-110 transition-transform rotate-180"
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex >= worksList.length - 3 }"
:class="{
'opacity-50 cursor-not-allowed':
worksScrollIndex >= worksList.length - 3,
}"
:src="getStaticImage('vlog/practice/滑动左')"
alt="向右"
@click="scrollWorksRight"
/>
</div>
-->
</div>
</div>
<!-- 下滑按钮 - 最后一页隐藏 -->
<ImgButton
v-show="!isLastPage"
class="absolute bottom-0 left-1/2 -translate-x-1/2"
class="absolute bottom-0 left-1/2 down-scroll-hint"
:image="'vlog/practice/下滑'"
@click="scrollToNextPage"
/>
@@ -306,13 +361,16 @@ onUnmounted(() => {
<!-- 回到顶部按钮 - 最后一页显示 -->
<img
v-show="isLastPage"
class="absolute bottom-10 right-10 cursor-pointer transition-all duration-300 hover:scale-105"
class="absolute bottom-10 left-1/2 -translate-x-1/2 cursor-pointer transition-all duration-300 hover:scale-105"
:src="BackToTopIcon"
@click="scrollToTop"
/>
<!-- 跨页人物动画 -->
<div v-show="showSittingPerson || isScrollingBackToFirst" class="character-container">
<div
v-show="showSittingPerson || isScrollingBackToFirst"
class="character-container"
>
<!-- 坐着的人物 - 第一页 -->
<Transition name="fade">
<img
@@ -335,7 +393,11 @@ onUnmounted(() => {
</Transition>
</div>
<GuideSteps v-if="visible" position-offset-y="70%" @complete="visible = false">
<GuideSteps
v-if="visible"
position-offset-y="70%"
@complete="visible = false"
>
<GuideStepsItem>{{ contentSteps }}</GuideStepsItem>
</GuideSteps>
</div>
@@ -355,6 +417,22 @@ onUnmounted(() => {
/* Chrome/Safari/Opera */
}
.down-scroll-hint {
transform: translateX(-50%);
animation: down-scroll-hint 1.4s ease-in-out infinite;
}
@keyframes down-scroll-hint {
0%,
100% {
transform: translate(-50%, 0);
}
50% {
transform: translate(-50%, 12px);
}
}
/* 第一页人物动画容器 */
.character-container {
position: fixed;
+30 -85
View File
@@ -3,8 +3,9 @@ import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { StarRating } from '@/components/Rating'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
const router = useRouter()
@@ -21,42 +22,33 @@ const handleBack = (): void => {
const disabled = computed<boolean>(() => noteContent.value.trim() === '')
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '画面氛围',
description: '画面具有明确情绪或空间气质,整体风格统一',
score: 4
score: 4,
},
{
title: '拍摄方法',
description: '拍摄方式选择合理,镜头语言服务于画面表达',
score: 2
score: 2,
},
{
title: '剪辑逻辑',
description: '剪辑节奏清晰,镜头衔接自然,有整体感',
score: 5
score: 5,
},
{
title: '创作意识',
description: '能体现主动构思,而非随拍拼接',
score: 0
score: 0,
},
{
title: '综合观感',
description: '作品具有“电影感”,完成度高',
score: 5
}
score: 5,
},
])
const handleSave = (): void => {
console.log('保存笔记')
@@ -67,7 +59,9 @@ const handleSave = (): void => {
<template>
<div
class="w-full h-full bg-no-repeat bg-cover px-8 pt-[400px]"
:style="{ backgroundImage: getBackgroundImage('film/practice/创作笔记底图-电影感') }"
:style="{
backgroundImage: getBackgroundImage('film/practice/我的创作笔记'),
}"
>
<ImgButton
class="absolute top-0 right-0"
@@ -83,53 +77,25 @@ const handleSave = (): void => {
></textarea>
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-60">
<ImgButton image="vlog/practice/保存" @click="handleSave" />
<ImgButton image="vlog/practice/自评" :disabled="disabled" @click="showModal = true" />
<ImgButton
image="vlog/practice/保存"
@click="handleSave"
/>
<ImgButton
image="vlog/practice/自评"
:disabled="disabled"
@click="showModal = true"
/>
</div>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
:style="{ backgroundImage: getBackgroundImage('film/practice/评分底板-电影感') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div class="pl-10 space-y-10 mt-10">
<p class="glow-text">
评分时要关注5个维度哦~每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~
</p>
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<p class="glow-text">
己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~
</p>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</Overlay>
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="film/practice/电影感短视频创作-作品评分表"
top-tip="评分时要关注5个维度哦~分别是画面氛围、抽摄方法、剪辑逻辑、创作意识、综合观感,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
</div>
</template>
@@ -146,25 +112,4 @@ textarea::placeholder {
color: #5d5b5b;
font-weight: 700;
}
.glow-text {
color: white;
font-size: 2rem;
font-weight: bold;
text-shadow:
/* 描边效果 - 多层阴影模拟描边 */
-1px -1px 0 #0066ff,
1px -1px 0 #0066ff,
-1px 1px 0 #0066ff,
1px 1px 0 #0066ff,
-2px -2px 0 #0066ff,
2px -2px 0 #0066ff,
-2px 2px 0 #0066ff,
2px 2px 0 #0066ff,
/* 发光效果 - 蓝色光晕 */ 0 0 10px #0066ff,
0 0 20px #0066ff,
0 0 30px #0066ff,
0 0 40px #0066ff,
0 0 50px #0066ff;
}
</style>
+36 -74
View File
@@ -2,9 +2,10 @@
import { useRouter } from 'vue-router'
import { Video } from '@/components'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
import { ref } from 'vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores/modules/film'
@@ -20,46 +21,37 @@ if (showGuide.value) {
const props = defineProps({
path: {
type: String,
required: true
}
required: true,
},
})
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
title: '画面氛围',
description: '画面具有明确情绪或空间气质,整体风格统一',
score: 4,
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
title: '拍摄方法',
description: '拍摄方式选择合理,镜头语言服务于画面表达',
score: 2,
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
title: '剪辑逻辑',
description: '剪辑节奏清晰,镜头衔接自然,有整体感',
score: 5,
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
title: '创作意识',
description: '能体现主动构思,而非随拍拼接',
score: 0,
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
title: '综合观感',
description: '作品具有“电影感”,完成度高',
score: 5,
},
])
/** 保存弹窗 */
@@ -91,49 +83,19 @@ const handleBack = (): void => {
:loop="false"
/>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[80px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div
class="w-full bg-no-repeat bg-cover bg-left py-44"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
>
<div class="pl-10 space-y-16">
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</div>
</Overlay>
<GuideSteps :visible="showGuide" :highlight-selector="'.btn-1'" position-offset-y="60%">
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="film/practice/电影感短视频创作-作品评分表"
top-tip="评分时要关注5个维度哦~分别是画面氛围、抽摄方法、剪辑逻辑、创作意识、综合观感,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
<GuideSteps
:visible="showGuide"
:highlight-selector="'.btn-1'"
position-offset-y="60%"
>
<GuideStepsItem>点击这里可以给作品评分噢~</GuideStepsItem>
</GuideSteps>
</div>
+66 -33
View File
@@ -2,8 +2,12 @@
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import QuestionQuiz, {
type Question,
} from '@/components/QuestionQuiz/QuestionQuiz.vue'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { useFilmStore } from '@/stores'
@@ -33,44 +37,44 @@ const navigationSteps = ref([
id: 'step1',
label: '后期制作',
videoUrl: getPublicPath('video/film/work/dygmk_zf_batch.mp4'),
content: '遮幅'
content: '遮幅',
},
{
id: 'step2',
label: '拍摄执行',
videoUrl: getPublicPath('video/film/work/dygmk_gx_batch.mp4'),
content: '光线'
content: '光线',
},
{
id: 'step3',
label: '拍摄执行',
videoUrl: getPublicPath('video/film/work/dygmk_xs_batch.mp4'),
content: '虚实'
content: '虚实',
},
{
id: 'step4',
label: '拍摄执行',
videoUrl: getPublicPath('video/film/work/dygmk_txjt_batch.mp4'),
content: '特写镜头'
content: '特写镜头',
},
{
id: 'step5',
label: '拍摄执行',
videoUrl: getPublicPath('video/film/work/dygmk_fjt_batch.mp4'),
content: '分镜头'
content: '分镜头',
},
{
id: 'step5',
label: '后期制作',
videoUrl: getPublicPath('video/film/work/dygmk_lj_batch.mp4'),
content: '滤镜'
content: '滤镜',
},
{
id: 'step5',
label: '拍摄执行',
videoUrl: getPublicPath('video/film/work/dygmk_tqs_batch.mp4'),
content: '同期声'
}
content: '同期声',
},
])
// 步骤导航相关状态
@@ -94,17 +98,17 @@ const getQuestionsForVideo = (index: number): Question[] => {
options: [
{
id: 'a',
label: '前景、人物、背景全部清晰'
label: '前景、人物、背景全部清晰',
},
{
id: 'b',
label: '前景虚化,人物清晰,背景有层次'
}
label: '前景虚化,人物清晰,背景有层次',
},
],
correctOptionId: 'b',
explanation:
'电影感的画面,往往会主动模糊一部分信息。这是为了把观众的注意力,留给正在发生的情绪。'
}
'电影感的画面,往往会主动模糊一部分信息。这是为了把观众的注意力,留给正在发生的情绪。',
},
]
}
// 分镜头(index 4)
@@ -117,25 +121,28 @@ const getQuestionsForVideo = (index: number): Question[] => {
options: [
{
id: 'a',
label: '特写→中景→ 全景'
label: '特写→中景→ 全景',
},
{
id: 'b',
label: '全景→ 中景→ 特写'
}
label: '全景→ 中景→ 特写',
},
],
correctOptionId: 'b',
explanation: '电影里的镜头, 往往是从环境进入人物,再进入情绪。'
}
explanation: '电影里的镜头, 往往是从环境进入人物,再进入情绪。',
},
]
}
return []
}
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const resolvedSrc = computed(() => {
if (key.value === 'case') return getPublicPath('video/film/work/dygmk_zbbwg_batch.mp4')
if (key.value === 'case')
return getPublicPath('video/film/work/dygmk_zbbwg_batch.mp4')
if (key.value === 'profession') return currentVideo.value
return ''
})
@@ -148,8 +155,9 @@ const handleVideoChange = () => {
steps.value = [
{
id: 'step1',
content: '恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!'
}
content:
'恭喜你,已完成专业剖析的学习,点击【查看脚本】回顾视频脚本吧!',
},
]
return
}
@@ -164,7 +172,7 @@ const handleClose = () => {
film.setFilmUnlockProgress(1)
router.push({
name: 'Film',
query: { guide: 'return' }
query: { guide: 'return' },
})
return
}
@@ -200,7 +208,7 @@ const handleComplete = () => {
if (key.value === 'profession') {
if (!film.isFilmUnlock) film.setIsFilmUnlock(true)
router.push({
name: 'FilmWork'
name: 'FilmWork',
})
}
}
@@ -239,7 +247,10 @@ onMounted(() => {})
</script>
<template>
<div class="w-full h-full relative bg-white/10" @click="handleClick">
<div
class="w-full h-full relative bg-white/10"
@click="handleClick"
>
<template v-if="key !== 'case' && key !== 'profession'">
<ImgButton
class="absolute right-0 top-0 z-10"
@@ -254,7 +265,12 @@ onMounted(() => {})
@click="showStepNavigation = true"
title="步骤导航"
>
<svg width="35" height="35" viewBox="0 0 20 20" fill="none">
<svg
width="35"
height="35"
viewBox="0 0 20 20"
fill="none"
>
<path
d="M3 4H17M3 8H17M3 12H17M3 16H17"
stroke="currentColor"
@@ -289,13 +305,24 @@ onMounted(() => {})
/>
</template>
</Video>
<div v-else-if="key === 'idea'" :src="image">
<img :src="getStaticImage('film/思路提点全屏')" alt="" />
<div
v-else-if="key === 'idea'"
:src="image"
>
<img
:src="getStaticImage('film/思路提点全屏')"
alt=""
/>
</div>
</div>
<!-- 引导与高亮 -->
<GuideSteps v-if="status" :steps="steps" position-offset-y="85%" @complete="handleComplete" />
<GuideSteps
v-if="status"
:steps="steps"
position-offset-y="85%"
@complete="handleComplete"
/>
<!-- 步骤导航抽屉 -->
<StepNavigation
@@ -331,7 +358,12 @@ onMounted(() => {})
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="步骤导航"
>
<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"
@@ -354,7 +386,8 @@ onMounted(() => {})
@complete="handleComplete"
>
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_007.MP3')">
一个好镜头,既要好看,又要好懂 —— 既有视觉吸引力,又能把信息清晰地传达出来。
一个好镜头,既要好看,又要好懂 ——
既有视觉吸引力,又能把信息清晰地传达出来。
</GuideStepsItem>
<GuideStepsItem :audio-src="getPublicPath('audio/py_4_008.MP3')">
当这些条件被满足,一个镜头就有了“高级感”。
+34 -15
View File
@@ -1,7 +1,12 @@
<script lang="tsx" name="FilmWork" setup>
defineOptions({ name: 'FilmWork' })
import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage, getPublicPath } from '@/utils'
import {
getBackgroundImage,
IMAGE_PATHS,
getStaticImage,
getPublicPath,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
@@ -29,28 +34,30 @@ const sideSwitcherItems = ref([
key: 'case',
name: '案例赏析',
description: '物件春秋-上海钟表文化科普馆',
image: 'film/案件赏析小屏'
image: 'film/案件赏析小屏',
},
{
key: 'profession',
name: '专业剖析',
description: '物件春秋-上海钟表文化科普馆',
image: 'film/专业剖析'
image: 'film/专业剖析',
},
{
key: 'idea',
name: '思路提点',
description: '物件春秋-上海钟表文化科普馆',
image: 'film/思路提点小屏'
}
image: 'film/思路提点小屏',
},
])
//当前选中的item
const currentItem = computed(() => {
return sideSwitcherItems.value.find(item => item.key === selected.value)
return sideSwitcherItems.value.find((item) => item.key === selected.value)
})
const currentHighlight = computed(() => (stepIndex.value === 2 ? '.work-border' : null))
const currentHighlight = computed(() =>
stepIndex.value === 2 ? '.work-border' : null,
)
const handleStepChange = (_: any, index: number) => {
stepIndex.value = index
@@ -61,14 +68,16 @@ const handleSideSwitcherChange = (value: WorkTab) => {
}
const handleImageClick = () => {
const currentItem = workSideSwitcherRef.value?.items.find(item => item.key === selected.value)
const currentItem = workSideSwitcherRef.value?.items.find(
(item) => item.key === selected.value,
)
router.push({
name: 'FilmVideo',
query: {
image: currentItem?.activeImage || '',
key: selected.value,
label: currentItem?.label || ''
}
label: currentItem?.label || '',
},
})
}
@@ -96,7 +105,11 @@ function openImages(urls: string[], startIndex = 0) {
images: urls,
startIndex,
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
</script>
@@ -131,10 +144,14 @@ function openImages(urls: string[], startIndex = 0) {
/>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
class="absolute left-[46%] top-[45%] -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>
<img
class="w-40"
:src="getStaticImage('work/小手')"
alt=""
/>
<!-- <span class="text-white text-2xl font-bold ml-5">点击进入</span> -->
</div>
</div>
<div
@@ -177,7 +194,9 @@ function openImages(urls: string[], startIndex = 0) {
<!-- 任务取景按钮 -->
<ImgButton
class="absolute right-14 bottom-10"
:image="isFilmUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
:image="
isFilmUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN
"
@click="handleWorkClick"
/>
+140 -53
View File
@@ -1,8 +1,13 @@
<script lang="ts" setup>
defineOptions({
name: 'Action'
name: 'Action',
})
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
import {
getBackgroundImage,
getPublicPath,
IMAGE_PATHS,
getStaticImage,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { ModuleSwitcher } from './components/ModuleSwitcher'
import StudyProgressPopup from './components/StudyProgressPopup.vue'
@@ -13,7 +18,7 @@ import {
useFilmStore,
useVlogStore,
useInterviewStore,
useBasicElementsStore
useBasicElementsStore,
} from '@/stores'
import { GuideSteps } from '@/components/Guide'
import type { GuideStep } from '@/components/Guide/types'
@@ -46,66 +51,67 @@ const homeGuideSteps: GuideStep[] = [
content: '点击新手导航按钮,可以展开/收起新手导航面板!',
positionOffsetY: '50%',
highlightSelector: '.newUserGuide',
audioSrc: getPublicPath('audio/py_0_005.MP3')
audioSrc: getPublicPath('audio/py_0_005.MP3'),
},
{
id: 'home-guide-locked-modules',
content: '新手导航模块全部完成后才能解锁三大进阶模块哦~',
highlightSelector: '.newUserGuide',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_006.MP3')
audioSrc: getPublicPath('audio/py_0_006.MP3'),
},
{
id: 'home-guide-module-switcher',
content: '舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
content:
'舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
highlightSelector: '.module-switcher-guide',
positionOffsetY: '85%',
audioSrc: getPublicPath('audio/py_0_007.MP3')
audioSrc: getPublicPath('audio/py_0_007.MP3'),
},
{
id: 'home-guide-tv',
content: '再点击右侧屏幕就会开启对应的板块学习哦!',
highlightSelector: '.tvGuide',
positionOffsetY: '80%',
audioSrc: getPublicPath('audio/py_0_008.MP3')
audioSrc: getPublicPath('audio/py_0_008.MP3'),
},
{
id: 'home-guide-skill',
content: '点击技能树,可以一键回顾新手导航和三大创作板块所有的知识点哦!',
highlightSelector: '.skill-tree',
positionOffsetY: '50%',
audioSrc: getPublicPath('audio/py_0_009.MP3')
}
audioSrc: getPublicPath('audio/py_0_009.MP3'),
},
]
const unlockedHomeGuideSteps: GuideStep[] = [
{
id: 'home-guide-welcome',
content: '欢迎来到新手村!在这里,我会带着你解锁短视频创作的核心基本功!',
audioSrc: getPublicPath('audio/py_1_001.MP3')
audioSrc: getPublicPath('audio/py_1_001.MP3'),
},
{
id: 'home-guide-map',
content: '我们会读懂构图、用好光线、灵活运镜,了解基础拍摄技巧和剪映软件的基本操作。',
audioSrc: getPublicPath('audio/py_1_002.MP3')
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')
}
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')
}
audioSrc: getPublicPath('audio/py_1_004.MP3'),
},
]
const handleHomeGuideComplete = () => {
@@ -137,17 +143,21 @@ const isImageLoading = ref(false)
const imageMap = {
vlog: IMAGE_PATHS.ACTION.VIDEO1,
film: IMAGE_PATHS.ACTION.VIDEO2,
interview: IMAGE_PATHS.ACTION.VIDEO3
interview: IMAGE_PATHS.ACTION.VIDEO3,
}
//获取新手导航引导状态
const isGuideNewUser = computed(() => basicElements.isGuideNewUser)
const hasFinishedActionGuide = computed(() => basicElements.guideShown[actionGuideKey] === true)
const hasFinishedActionGuide = computed(
() => basicElements.guideShown[actionGuideKey] === true,
)
// 预加载所有图片
const preloadImages = () => {
const imageUrls = Object.values(imageMap).map(path => getBackgroundImage(path))
imageUrls.forEach(url => {
const imageUrls = Object.values(imageMap).map((path) =>
getBackgroundImage(path),
)
imageUrls.forEach((url) => {
const img = new Image()
img.src = url
})
@@ -156,7 +166,11 @@ const preloadImages = () => {
// 事件处理函数
const handleOpenGuide = () => {
guideShow.value = true
if (activeActionGuide.value !== 'home' && isGuideNewUser.value && !hasFinishedActionGuide.value) {
if (
activeActionGuide.value !== 'home' &&
isGuideNewUser.value &&
!hasFinishedActionGuide.value
) {
activeActionGuide.value = 'unlocked'
}
}
@@ -185,7 +199,9 @@ const handleClick = () => {
// console.log(selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1))
router.push({
//首字母大写
name: selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1)
name:
selectedModule.value.charAt(0).toUpperCase() +
selectedModule.value.slice(1),
})
}
@@ -204,7 +220,7 @@ const handleSkill = () => {
if (isSkillDisabled.value) return
router.push({
name: 'Skill'
name: 'Skill',
})
}
@@ -227,10 +243,25 @@ const nodes: GuideNode[] = [
y: 30,
hasChildren: true,
children: [
{ id: 1, label: '构图', icon: 'home/action/新手导航构图', routeName: 'Composition' },
{ id: 2, label: '光', icon: 'home/action/新手导航光', routeName: 'Light' },
{ id: 3, label: '运镜', icon: 'home/action/新手导航运镜', routeName: 'CameraMovement' }
]
{
id: 1,
label: '构图',
icon: 'home/action/新手导航构图',
routeName: 'Composition',
},
{
id: 2,
label: '光',
icon: 'home/action/新手导航光',
routeName: 'Light',
},
{
id: 3,
label: '运镜',
icon: 'home/action/新手导航运镜',
routeName: 'CameraMovement',
},
],
},
{
id: 2,
@@ -239,7 +270,7 @@ const nodes: GuideNode[] = [
x: 50,
y: 60,
hasChildren: false,
routeName: 'BasicShootingSkills'
routeName: 'BasicShootingSkills',
},
{
id: 3,
@@ -248,8 +279,8 @@ const nodes: GuideNode[] = [
x: 80,
y: 30,
hasChildren: false,
routeName: 'ShortVideoEditingSkills'
}
routeName: 'ShortVideoEditingSkills',
},
]
// 显示子节点的节点ID
@@ -326,7 +357,10 @@ onMounted(() => {
<!-- 学习进度 -->
<div class="absolute top-0 left-0">
<ImgButton image="home/action/学习进度入口" @click="handleOpenStudyProgress" />
<ImgButton
image="home/action/学习进度入口"
@click="handleOpenStudyProgress"
/>
</div>
<StudyProgressPopup v-model="studyProgressVisible" />
@@ -344,11 +378,18 @@ onMounted(() => {
</div>
<!-- 选择模块 -->
<div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
<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" />
<ModuleSwitcher
v-model="selectedModule"
@change="handleModuleChange"
/>
</div>
<!-- 电视机 -->
@@ -372,14 +413,16 @@ onMounted(() => {
v-if="isGuideNewUser && !hasFinishedActionGuide"
:class="[
'absolute inset-0 z-10',
activeActionGuide === 'home' ? 'bg-black/0' : 'bg-black/50'
activeActionGuide === 'home' ? 'bg-black/0' : 'bg-black/50',
]"
></div>
<!-- 新手指导 -->
<div
class="absolute bottom-0 left-0 w-full h-[497px] bg-cover bg-center bg-no-repeat z-20 pointer-events-none"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BTN) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BTN),
}"
v-if="!guideShow"
>
<div
@@ -390,7 +433,9 @@ onMounted(() => {
<div
class="absolute bottom-0 left-0 w-full h-[911px] bg-cover bg-center bg-no-repeat z-40 flex flex-col items-center justify-center pt-40"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BG) }"
:style="{
backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BG),
}"
v-else
>
<ImgButton
@@ -401,13 +446,20 @@ onMounted(() => {
<!-- 新手导航内容 -->
<div class="relative w-[880px] h-[400px] mt-20">
<div class="w-full h-full relative guide-content" @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">
<h2
class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg"
>
新手导航
</h2>
<p class="text-4xl text-white font-bold drop-shadow-md">点击节点即可开始学习</p>
<p class="text-4xl text-white font-bold drop-shadow-md">
点击节点即可开始学习
</p>
</div>
<!-- 节点图表 -->
@@ -492,24 +544,37 @@ onMounted(() => {
</div>
<!-- 子节点图标(当点击三大基础要素时显示) -->
<div
v-if="node.hasChildren && showChildrenNodeId === node.id"
class="absolute -top-40 left-1/2 transform -translate-x-1/2 flex z-20 btn"
>
<Transition name="child-node-pop">
<div
v-for="childNode in node.children"
:key="childNode.id"
class="flex flex-col items-center cursor-pointer"
@click.stop="handleChildNodeClick(childNode, $event)"
v-if="node.hasChildren && showChildrenNodeId === node.id"
class="child-node-buttons btn absolute -top-48 left-1/2 z-20 flex -translate-x-1/2 flex-col items-center"
>
<!-- 图标 -->
<div
class="w-36 flex items-center justify-center transition-all duration-300 hover:scale-110"
class="relative flex rounded-[30px] bg-[rgba(8,40,58,0.82)] px-3 py-2 shadow-[0_0_0_1px_rgba(123,255,253,0.28),0_0_20px_rgba(123,255,253,0.28)] after:absolute after:left-1/2 after:top-[calc(100%-1px)] after:h-0 after:w-0 after:-translate-x-1/2 after:border-x-[12px] after:border-t-[14px] after:border-x-transparent after:border-t-[rgba(8,40,58,0.82)]"
>
<img :src="getStaticImage(childNode.icon)" :alt="childNode.label" />
<div
v-for="childNode in node.children"
:key="childNode.id"
class="group/child relative z-10 flex flex-col items-center cursor-pointer"
@click.stop="handleChildNodeClick(childNode, $event)"
>
<!-- 图标 -->
<div
class="relative w-36 flex items-center justify-center transition-all duration-300 hover:scale-110"
>
<div
class="absolute inset-x-5 inset-y-4 rounded-full bg-[#7bfffd]/12 blur-xl opacity-60 transition-opacity duration-300 group-hover/child:opacity-100"
></div>
<img
class="relative z-10 drop-shadow-[0_0_10px_rgba(123,255,253,0.45)] transition-all duration-300 group-hover/child:brightness-125 group-hover/child:drop-shadow-[0_0_18px_rgba(123,255,253,0.8)]"
:src="getStaticImage(childNode.icon)"
:alt="childNode.label"
/>
</div>
</div>
</div>
</div>
</div>
</Transition>
</div>
</div>
</div>
@@ -517,3 +582,25 @@ onMounted(() => {
</div>
</div>
</template>
<style scoped>
.child-node-pop-enter-active,
.child-node-pop-leave-active {
transition:
opacity 0.22s ease,
transform 0.22s cubic-bezier(0.2, 0.9, 0.2, 1.1);
transform-origin: bottom center;
}
.child-node-pop-enter-from,
.child-node-pop-leave-to {
opacity: 0;
transform: translateX(-50%) translateY(10px) scale(0.86);
}
.child-node-pop-enter-to,
.child-node-pop-leave-from {
opacity: 1;
transform: translateX(-50%) translateY(0) scale(1);
}
</style>
+82 -20
View File
@@ -1,30 +1,47 @@
import { defineComponent, ref } from 'vue'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { computed, defineComponent, ref } from 'vue'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, ImagePreview } from '@/components'
import { GuideSteps, ImagePreview, Video } from '@/components'
import { useScale } from '@/composables/useScale'
import { useBasicElementsStore } from '@/stores/modules/basic-elements'
const CourseFrameGuide = () => (
<div class="flex items-center justify-start">
点击
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)} alt="" />
<img
class="w-40"
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="" />
包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)
<img
class="w-40 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="" />
正式进入课程,开启你的短视频拍摄之旅。
<img
class="w-40"
src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)}
alt=""
/>
,一起我们的短视频学习之旅吧!
</div>
)
@@ -38,7 +55,7 @@ export default defineComponent({
content: '欢迎来到《短视频达人养成记》网络课程的学习!',
highlightSelector: null,
audioSrc: getPublicPath('audio/py_0_001.MP3'),
positionOffsetY: '80%'
positionOffsetY: '80%',
},
{
id: 'step-2',
@@ -46,7 +63,7 @@ export default defineComponent({
content: CourseFrameGuide,
highlightSelector: '.kecheng-wrapper',
audioSrc: getPublicPath('audio/py_0_002.MP3'),
positionOffsetY: '40%'
positionOffsetY: '40%',
},
{
id: 'step-3',
@@ -54,7 +71,7 @@ export default defineComponent({
content: OperateGuide,
highlightSelector: '.zhiying-wrapper',
audioSrc: getPublicPath('audio/py_0_003.MP3'),
positionOffsetY: '40%'
positionOffsetY: '40%',
},
{
id: 'step-4',
@@ -62,15 +79,27 @@ export default defineComponent({
content: StartFilmingGuide,
highlightSelector: '.start-filming-wrapper',
audioSrc: getPublicPath('audio/py_0_004.MP3'),
positionOffsetY: '40%'
}
positionOffsetY: '40%',
},
])
const basicElementsStore = useBasicElementsStore()
const guideKey = 'home-intro'
const showGuide = ref(!basicElementsStore.guideShown[guideKey])
const isFirstVisit = !basicElementsStore.guideShown[guideKey]
const showGuide = ref(isFirstVisit)
const { scale, offsetX, offsetY } = useScale()
if (showGuide.value) {
// 首次进入的视频播放序列
const introVideos = computed(() => [
getPublicPath('video/campus life.mp4'),
getPublicPath('video/new/xsdh_CapCut_Tutorial.mp4'),
])
const introVideoIndex = ref(0)
const showIntroVideo = ref(isFirstVisit)
const currentIntroSrc = computed(
() => introVideos.value[introVideoIndex.value] ?? '',
)
if (isFirstVisit) {
basicElementsStore.markGuideShown(guideKey)
}
@@ -78,11 +107,24 @@ export default defineComponent({
showGuide.value = false
}
const handleIntroVideoEnded = () => {
if (introVideoIndex.value < introVideos.value.length - 1) {
introVideoIndex.value++
} else {
showIntroVideo.value = false
// 引导步骤在视频播放结束后出现
}
}
const openMindMap = () => {
ImagePreview({
images: [getStaticImage('maps/dt')],
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
@@ -94,19 +136,39 @@ export default defineComponent({
/>
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton image="home/action-btn" path={{ name: 'Action' }} />
<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} />
<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>
{showIntroVideo.value && (
<div class="absolute inset-0 z-[10000] bg-black flex items-center justify-center">
<Video
src={currentIntroSrc.value}
autoplay={true}
muted={false}
controls={true}
volume={1}
fit="contain"
onEnded={handleIntroVideoEnded}
/>
</div>
)}
<GuideSteps
visible={showGuide.value}
visible={showGuide.value && !showIntroVideo.value}
steps={guideSteps.value}
highlightPadding={12}
highlightRadius={16}
@@ -116,5 +178,5 @@ export default defineComponent({
/>
</div>
)
}
},
})
+11 -6
View File
@@ -2,7 +2,7 @@
import { nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { StepNavigation } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { IMAGE_PATHS } from '@/utils'
import { Video } from '@/components'
@@ -19,20 +19,20 @@ const navigationSteps = [
id: 'step1',
label: '后期制作',
content: '声画剪辑流程',
type: 'post' as const
type: 'post' as const,
},
{
id: 'step2',
label: '后期制作',
content: 'J-cut/L-cut运用',
type: 'post' as const
type: 'post' as const,
},
{
id: 'step3',
label: '后期制作',
content: '【Q3】节奏剪辑技巧',
type: 'post' as const
}
type: 'post' as const,
},
]
const currentVideoIndex = ref(0)
@@ -83,7 +83,12 @@ const handleClose = () => {
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"
+54 -31
View File
@@ -16,61 +16,62 @@ const category = ref([
{
label: '前期准备',
value: 1,
icon: getStaticImage('midterm/准备图标.png')
icon: getStaticImage('midterm/准备图标.png'),
},
{
label: '中期拍摄',
value: 2,
icon: getStaticImage('midterm/拍摄图标.png')
icon: getStaticImage('midterm/拍摄图标.png'),
},
{
label: '后期剪辑',
value: 3,
icon: getStaticImage('midterm/剪辑图标.png')
}
icon: getStaticImage('midterm/剪辑图标.png'),
},
])
const menuItems = ref([
{
label: '资源筹备',
routeName: 'InterviewResource',
parentValue: 1,
disabled: false
disabled: false,
},
{
label: '创意构想',
routeName: 'InterviewCreative',
parentValue: 1,
disabled: false
disabled: false,
},
{
label: '脚本撰写',
routeName: 'InterviewScriptWriting',
parentValue: 1,
disabled: false
disabled: false,
},
{
label: '分镜制作',
routeName: 'InterviewStoryboard',
parentValue: 1,
disabled: false
disabled: false,
},
{
label: '设备准备',
routeName: 'InterviewEquipment',
parentValue: 2,
disabled: false
disabled: false,
},
{
label: '实地取景',
routeName: 'InterviewShooting',
parentValue: 2,
disabled: false
disabled: false,
},
{
label: '现场拍摄',
routeName: 'InterviewOnsiteShooting',
parentValue: 2,
disabled: false
disabled: false,
},
// {
// label: '素材管理',
@@ -82,26 +83,26 @@ const menuItems = ref([
label: '剪辑',
routeName: 'InterviewEditing',
parentValue: 3,
disabled: false
disabled: false,
},
{
label: '调色',
routeName: 'InterviewColor',
parentValue: 3,
disabled: false
disabled: false,
},
{
label: '音频',
routeName: 'InterviewAudio',
parentValue: 3,
disabled: false
disabled: false,
},
{
label: '导出发布',
routeName: 'InterviewExportPublish',
parentValue: 3,
disabled: false
}
disabled: false,
},
])
const props = defineProps<{
@@ -119,28 +120,36 @@ const currentParentValue = computed<number>(() => {
return props.currentStep
}) // 当前选中的父级菜单项
const currentItemValue = computed<string>(() => {
return router.currentRoute.value.name?.toString() ?? ''
})
const currentItemValue = ref<string>('') // 当前选中的子级菜单项
const getMenuItems = computed(() => {
return menuItems.value.filter(item => item.parentValue === currentParentValue.value)
return menuItems.value.filter(
(item) => item.parentValue === currentParentValue.value,
)
})
const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
if (item.disabled) return
currentItemValue.value = item.routeName
router.push({ name: item.routeName })
}
</script>
<template>
<div class="absolute left-20 top-0 bottom-0 w-[520px] pt-28" v-if="currentStep">
<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>
<img class="filter-blue" :src="category[currentParentValue - 1].icon" alt="" />
<img
class="filter-blue"
:src="category[currentParentValue - 1].icon"
alt=""
/>
</div>
<!-- 背景图片 -->
<div
@@ -148,12 +157,17 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
:style="{ backgroundImage: getBackgroundImage('components/menu-bg.png') }"
></div>
<div class="flex items-center flex-col pl-[60px]">
<template v-for="(item, index) in getMenuItems" :key="item.routeName">
<template
v-for="(item, index) in getMenuItems"
:key="item.routeName"
>
<div
class="relative w-[373px] h-[143px] pl-14 flex items-center justify-center group text-4xl font-black transition-colors"
:class="[
currentItemValue === item.routeName ? 'text-blue-400' : 'text-white',
item.disabled ? 'opacity-20 cursor-not-allowed' : 'cursor-pointer'
currentItemValue === item.routeName
? 'text-blue-400'
: 'text-white',
item.disabled ? 'opacity-20 cursor-not-allowed' : 'cursor-pointer',
]"
:style="{ marginLeft: -index * 40 + 'px' }"
@click="handleItemClick(item)"
@@ -161,13 +175,22 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
<div
v-if="!item.disabled"
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') }"
:style="{
backgroundImage: getBackgroundImage('components/menu-hover.png'),
}"
></div>
<span class="flex items-center gap-2" :class="[!item.disabled ? 'menu-glow-text' : '']">
<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"
class="w-10"
:src="
isViewed(item.routeName)
? '/src/assets/images/midterm/完成.png'
: '/src/assets/images/midterm/未完成.png'
"
alt=""
/>
</span>
@@ -179,8 +202,8 @@ const handleItemClick = (item: { routeName: string; disabled: boolean }) => {
<style scoped>
.filter-blue {
filter: brightness(0) saturate(100%) invert(79%) sepia(29%) saturate(1190%) hue-rotate(330deg)
brightness(102%) contrast(98%);
filter: brightness(0) saturate(100%) invert(79%) sepia(29%) saturate(1190%)
hue-rotate(330deg) brightness(102%) contrast(98%);
}
.menu-glow-text {
@@ -2,7 +2,7 @@
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton, Video } from '@/components'
import { StepNavigation } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
const router = useRouter()
@@ -16,30 +16,32 @@ const navigationSteps = ref([
label: '中期拍摄',
content: '素材怎么拍',
videoUrl: getPublicPath('video/interview/on-site/ftmk_sczmp_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'q1-required-shots',
label: '中期拍摄',
content: '必拍镜头有哪些',
videoUrl: getPublicPath('video/interview/on-site/ftmk_bpjtynx_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'q2-moving-shot',
label: '中期拍摄',
content: '移动镜头的作用',
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'q2-close-up',
label: '中期拍摄',
content: '特写镜头的作用',
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 back = () => {
router.back()
@@ -92,7 +94,12 @@ const handleEnded = () => {
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"
@@ -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'
? 'interview/shooting/开始拍摄-操场.png'
: 'interview/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,18 +156,31 @@ 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('interview/shooting/操场-模糊.png') }"
:style="{
backgroundImage: getBackgroundImage('interview/shooting/操场-模糊.png'),
}"
>
<ImgButton
image="interview/shooting/操场-侧边标题.png"
class="title-button absolute left-0"
/>
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
<ImgButton
image="shooting/返回.png"
class="back-button absolute right-0"
@click="back"
/>
<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"
@@ -186,7 +199,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/interview/shooting/操场-清晰.png"
@@ -213,7 +226,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
@@ -237,13 +252,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)"
>
<!-- 刻度线本体 (位于圆盘左边缘) -->
@@ -252,17 +271,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 }}
@@ -282,22 +305,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>
@@ -321,7 +361,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>
@@ -24,7 +24,13 @@ const handleModalClose = () => {
}
// 自由取景框模式
const { viewfinderContainer, viewfinderBox, viewfinder, startDrag, startResize } = useViewfinderB()
const {
viewfinderContainer,
viewfinderBox,
viewfinder,
startDrag,
startResize,
} = useViewfinderB()
const showGuide = ref(false)
const backgroundImage = ref<HTMLImageElement | null>(null)
@@ -50,7 +56,7 @@ const captureScene = async () => {
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')
}
@@ -72,7 +78,7 @@ const captureScene = async () => {
0, // 目标 x
0, // 目标 y
viewfinder.value.width, // 目标 width
viewfinder.value.height // 目标 height
viewfinder.value.height, // 目标 height
)
},
onComplete: () => {
@@ -80,7 +86,7 @@ const captureScene = async () => {
setTimeout(() => {
showGuide.value = true
}, GUIDE_SHOW_DELAY)
}
},
})
}
@@ -90,7 +96,7 @@ const captureScene = async () => {
const handleComplete = () => {
showGuide.value = false
router.push({
name: 'Interview'
name: 'Interview',
})
}
</script>
@@ -102,7 +108,7 @@ const handleComplete = () => {
:style="{
backgroundImage: start
? getBackgroundImage('interview/shooting/教学楼-清晰.png')
: getBackgroundImage('interview/shooting/教学楼-模糊.png')
: getBackgroundImage('interview/shooting/教学楼-模糊.png'),
}"
>
<!-- 标题 -->
@@ -118,10 +124,17 @@ const handleComplete = () => {
@click="back"
/>
<StartOverlay v-if="!start" @start="start = true" />
<StartOverlay
scene="b"
v-if="!start"
@start="start = true"
/>
<!-- 取景 -->
<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"
@@ -132,14 +145,17 @@ const handleComplete = () => {
/>
<!-- 取景框容器 -->
<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>
<!-- 下遮罩 -->
@@ -147,7 +163,7 @@ const handleComplete = () => {
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>
<!-- 左遮罩 -->
@@ -156,7 +172,7 @@ const handleComplete = () => {
:style="{
top: viewfinder.y + 'px',
width: viewfinder.x + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
></div>
<!-- 右遮罩 -->
@@ -165,7 +181,7 @@ const handleComplete = () => {
:style="{
top: viewfinder.y + 'px',
left: viewfinder.x + viewfinder.width + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
></div>
@@ -177,13 +193,15 @@ const handleComplete = () => {
left: viewfinder.x + 'px',
top: viewfinder.y + 'px',
width: viewfinder.width + 'px',
height: viewfinder.height + 'px'
height: viewfinder.height + 'px',
}"
@mousedown="startDrag"
@touchstart="startDrag"
>
<!-- 九宫格辅助线 -->
<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"
@@ -192,7 +210,7 @@ const handleComplete = () => {
'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>
@@ -226,19 +244,34 @@ const handleComplete = () => {
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>
<!-- 操作按钮 -->
@@ -249,11 +282,20 @@ const handleComplete = () => {
</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>
@@ -7,7 +7,7 @@ import { ImgButton } from '@/components/Button'
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'
import KnowledgeCard from '@/components/KnowledgeCard/InterviewKnowledgeCard.vue'
import { useInterviewStore } from '@/stores/modules/interview'
type Btn = {
@@ -36,31 +36,31 @@ const currentItem = ref<Btn>({
name: '',
btnImage: '',
images: [],
desc: ''
desc: '',
})
const btnList = ref<Btn[]>([
{
key: '1',
name: '居中构图',
btnImage: 'shooting/居中构图底板',
desc: '居中构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用',
images: ['shooting/居中构图1', 'shooting/居中构图2']
name: '前景构图',
btnImage: 'interview/composition/前景构图',
desc: '前景构图就是在拍摄前选择一个物体作为前景,通常会将前景虚化,通过前景和主体的虚实对比突出主体,前景构图可以增加画面的层次感和信息量。',
images: ['interview/composition/前景1', 'interview/composition/前景2'],
},
{
key: '2',
name: '三分法构图',
btnImage: 'shooting/三分法构图底板',
desc: '三分法构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调',
images: ['shooting/图片待定1', 'shooting/图片待定2']
name: '留白构图',
btnImage: 'interview/composition/留白构图',
desc: '留白构图法也可以称为大构图,借用了中国书法、绘画的留白概念,简单来说就是给画面做减法,减少主体在画面中的占比,会让画面更加的干净并且能更加突出主体,会让照片更有故事感。',
images: ['interview/composition/留白1', 'interview/composition/留白2'],
},
{
key: '3',
name: '对角线构图',
btnImage: 'shooting/对角线构图底板',
desc: '对角线构图属于引导线构图的一种,所以也是借助引导线的视觉引导效果来突出强调主体。另外对角线构图在画面中制造出对角线的走向即可。',
images: ['shooting/对角线构图1', 'shooting/对角线构图2']
}
name: '三角构图',
btnImage: 'interview/composition/三角构图',
desc: '分为正三角形、倒三角和斜三角形,这种构图具有一种极强的稳定感和向上的冲击力,也是人像摄影中一种典型的构图形式,拍摄时可利用人物主体的肢体或动作姿态来组合构成三角形。',
images: ['interview/composition/三角1', 'interview/composition/三角2'],
},
])
const handleModalClose = () => {
@@ -95,22 +95,40 @@ const back = () => {
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage('interview/shooting/实地取景底图') }"
:style="{
backgroundImage: getBackgroundImage('interview/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="interview/shooting/操场-入口.png" @click="handleClickA" />
<ImgButton
image="interview/shooting/操场-入口.png"
@click="handleClickA"
/>
</div>
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
<ImgButton image="interview/shooting/教学楼-入口.png" @click="handleClickB" />
<ImgButton
image="interview/shooting/教学楼-入口.png"
@click="handleClickB"
/>
</div>
<div class="shooting-composition">
<ImgButton class="mybutton" image="shooting/构图指南.png" @click="showModal = true" />
<ImgButton
class="mybutton"
image="shooting/构图指南.png"
@click="showModal = true"
/>
</div>
</div>
@@ -132,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"
@@ -145,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
@@ -174,8 +195,14 @@ const back = () => {
@close="showModal2 = false"
/>
<!-- 视频 -->
<Overlay :visible="showModal3" :clickable="false">
<video :src="videoSrc" controls></video>
<Overlay
:visible="showModal3"
:clickable="false"
>
<video
:src="videoSrc"
controls
></video>
<!-- 返回 -->
<ImgButton
class="absolute right-0 top-0 z-10"
+113 -28
View File
@@ -4,7 +4,8 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { useRouter } from 'vue-router'
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
import BackToTopIcon from '@/assets/images/components/置顶.png'
import { useInterviewStore } from '@/stores/modules/interview'
const router = useRouter()
@@ -12,6 +13,8 @@ const TOTAL_PAGES = 3
const PAGE_HEIGHT = 1080
const SCROLL_DURATION = 1000
const interviewStore = useInterviewStore()
const currentPage = ref<number>(0)
const worksScrollIndex = ref<number>(0)
const animatingToPage = ref<number>(0)
@@ -23,7 +26,9 @@ const isLastPage = computed<boolean>(() => currentPage.value >= TOTAL_PAGES - 1)
/** 是否正在从第二页回到第一页 */
const isScrollingBackToFirst = computed<boolean>(() => {
return isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
return (
isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
)
})
/** 第一页坐着的人物是否显示 */
@@ -45,13 +50,13 @@ interface WorkItem {
const worksList = ref<WorkItem[]>([
{
id: 1,
image: 'https://picsum.photos/400/300?random=1',
title: '作品名称',
image: getStaticImage('interview/practice/访谈-匠心育人润物无声'),
title: '匠心育人润物无声',
stars: 4,
path: '/video/interview/practice/fangtan_cace1_batch.mp4',
path: 'video/interview/practice/fangtan_cace1_batch.mp4',
description:
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
}
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介',
},
// {
// id: 2,
// image: 'https://picsum.photos/400/300?random=2',
@@ -150,17 +155,43 @@ const handleWheel = (event: WheelEvent): void => {
}
}
const restoreScrollPage = (): void => {
const savedPage = interviewStore.practiceScrollPage
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
currentPage.value = targetPage
animatingToPage.value = targetPage
requestAnimationFrame(() => {
if (containerRef.value) {
containerRef.value.scrollTop = targetPage * PAGE_HEIGHT
}
})
}
const saveScrollPage = (): void => {
const page = containerRef.value
? Math.round(containerRef.value.scrollTop / PAGE_HEIGHT)
: currentPage.value
const targetPage = Math.max(0, Math.min(page, TOTAL_PAGES - 1))
interviewStore.setPracticeScrollPage(targetPage)
}
onMounted(() => {
restoreScrollPage()
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
})
onUnmounted(() => {
saveScrollPage()
containerRef.value?.removeEventListener('wheel', handleWheel)
})
</script>
<template>
<div ref="containerRef" class="w-full h-full overflow-auto">
<div
ref="containerRef"
class="w-full h-full overflow-auto"
>
<div class="relative min-h-full overflow-hidden">
<!-- 关闭 -->
<ImgButton
@@ -169,16 +200,19 @@ onUnmounted(() => {
@click="handleClose"
/>
<!-- 实战操作台 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat relative"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/实战操作台首页') }"
>
<div class="w-full h-[1080px] bg-[#F5EFE6] relative">
<ImgButton
class="absolute left-40 bottom-28 z-10"
class="absolute left-40 bottom-12 z-10"
image="vlog/practice/查看任务要求"
@click="scrollToNextPage"
/>
<!-- img -->
<img
class="absolute left-0 top-0 z-10"
draggable="false"
:src="getStaticImage('interview/practice/访谈')"
alt=""
/>
<img
class="absolute right-0"
draggable="false"
@@ -193,12 +227,18 @@ onUnmounted(() => {
>
<ImgButton
class="absolute right-20 bottom-0 z-99"
image="vlog/practice/我的创作笔记"
path="/practice/my-notes"
image="vlog/practice/我的创作笔记btn"
path="/interview/practice/my-notes"
/>
<div class="task-panel w-[757px] h-full ml-10 overflow-y-auto" @wheel.stop>
<img :src="getStaticImage('vlog/practice/滑动任务面板')" alt="" />
<div
class="task-panel w-[757px] h-full ml-10 overflow-y-auto"
@wheel.stop
>
<img
:src="getStaticImage('interview/practice/任务要求')"
alt=""
/>
</div>
<img
@@ -220,7 +260,9 @@ onUnmounted(() => {
<!-- 他人实战作品 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat pt-44 px-16"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/他人实战作品') }"
:style="{
backgroundImage: getBackgroundImage('vlog/practice/他人实战作品'),
}"
>
<!-- 作品列表 -->
<div class="w-full overflow-hidden">
@@ -235,28 +277,40 @@ onUnmounted(() => {
>
<!-- 作品图片 -->
<div class="w-full h-[280px] rounded-2xl overflow-hidden">
<img class="w-full h-full object-cover" :src="work.image" :alt="work.title" />
<img
class="w-full h-full object-cover"
:src="work.image"
:alt="work.title"
/>
</div>
<!-- 作品信息 -->
<div class="flex flex-col items-center pt-8 space-y-4">
<h3 class="text-5xl font-bold text-gray-800 mb-2">{{ work.title }}</h3>
<h3 class="text-5xl font-bold text-gray-800 mb-2">
{{ work.title }}
</h3>
<!-- 星星评分 -->
<StarRating :score="work.stars" size="lg" readonly />
<StarRating
:score="work.stars"
size="lg"
readonly
/>
<!-- 简介 -->
<p class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed">
<p
class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed"
>
{{ work.description }}
</p>
<!-- 查看按钮 -->
<ImgButton
image="vlog/practice/查看"
:path="`/practice/video/${encodeURIComponent(work.path)}`"
:path="`/interview/practice/video/${encodeURIComponent(work.path)}`"
/>
</div>
</div>
</div>
</div>
<!-- 指示按钮 -->
<!-- 指示按钮,暂时不要
<div class="absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-16">
<img
class="cursor-pointer hover:scale-110 transition-transform"
@@ -267,19 +321,23 @@ onUnmounted(() => {
/>
<img
class="cursor-pointer hover:scale-110 transition-transform rotate-180"
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex >= worksList.length - 3 }"
:class="{
'opacity-50 cursor-not-allowed':
worksScrollIndex >= worksList.length - 3,
}"
:src="getStaticImage('vlog/practice/滑动左')"
alt="向右"
@click="scrollWorksRight"
/>
</div>
-->
</div>
</div>
<!-- 下滑按钮 - 最后一页隐藏 -->
<ImgButton
v-show="!isLastPage"
class="absolute bottom-0 left-1/2 -translate-x-1/2"
class="absolute bottom-0 left-1/2 down-scroll-hint"
:image="'vlog/practice/下滑'"
@click="scrollToNextPage"
/>
@@ -287,13 +345,24 @@ onUnmounted(() => {
<!-- 回到顶部按钮 - 最后一页显示 -->
<img
v-show="isLastPage"
class="absolute bottom-10 right-10 cursor-pointer transition-all duration-300 hover:scale-105"
class="absolute bottom-10 left-1/2 -translate-x-1/2 cursor-pointer transition-all duration-300 hover:scale-105"
:src="BackToTopIcon"
@click="scrollToTop"
/>
<!-- 老师点评按钮 - 最后一页显示 -->
<ImgButton
v-show="isLastPage"
class="absolute right-2 bottom-10 z-30"
image="components/教师点评按钮"
path="/interview/practice/teacher-comment"
/>
<!-- 跨页人物动画 -->
<div v-show="showSittingPerson || isScrollingBackToFirst" class="character-container">
<div
v-show="showSittingPerson || isScrollingBackToFirst"
class="character-container"
>
<!-- 坐着的人物 - 第一页 -->
<Transition name="fade">
<img
@@ -332,6 +401,22 @@ onUnmounted(() => {
/* Chrome/Safari/Opera */
}
.down-scroll-hint {
transform: translateX(-50%);
animation: down-scroll-hint 1.4s ease-in-out infinite;
}
@keyframes down-scroll-hint {
0%,
100% {
transform: translate(-50%, 0);
}
50% {
transform: translate(-50%, 12px);
}
}
/* 第一页人物动画容器 */
.character-container {
position: fixed;
+45 -73
View File
@@ -3,8 +3,9 @@ import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { StarRating } from '@/components/Rating'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
const router = useRouter()
@@ -21,42 +22,38 @@ const handleBack = (): void => {
const disabled = computed<boolean>(() => noteContent.value.trim() === '')
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
title: '人物表达',
description: '能清晰介绍人物身份、特点或故事,让观众认识人物',
score: 4,
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
title: '素材拍摄',
description: '画面包含必要人物镜头、环境镜头与细节素材,内容较丰富',
score: 2,
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
title: '镜头语言',
description: '合理使用移动镜头、特写、跟拍等方式增强表现力',
score: 5,
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
title: '剪辑结构',
description: '结构完整清晰,段落顺畅,内容有起承转合',
score: 0,
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
title: '音画关系',
description: '画面与配音/环境音基本匹配,能辅助理解内容',
score: 5,
},
{
title: '综合观感',
description: '作品完整,有真实感与感染力,具备观看吸引力',
score: 5,
},
])
const handleSave = (): void => {
console.log('保存笔记')
@@ -67,7 +64,9 @@ const handleSave = (): void => {
<template>
<div
class="w-full h-full bg-no-repeat bg-cover px-8 pt-[400px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/创作笔记底图') }"
:style="{
backgroundImage: getBackgroundImage('interview/practice/我的创作笔记'),
}"
>
<ImgButton
class="absolute top-0 right-0"
@@ -83,52 +82,25 @@ const handleSave = (): void => {
></textarea>
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-60">
<ImgButton image="vlog/practice/保存" @click="handleSave" />
<ImgButton image="vlog/practice/自评" :disabled="disabled" @click="showModal = true" />
<ImgButton
image="vlog/practice/保存"
@click="handleSave"
/>
<ImgButton
image="vlog/practice/自评"
:disabled="disabled"
@click="showModal = true"
/>
</div>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div
class="w-full bg-no-repeat bg-cover bg-left py-44"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
>
<div class="pl-10 space-y-16">
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</div>
</Overlay>
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="interview/practice/人物访谈短片创作-作品评分表"
top-tip="评分时要关注6个维度哦~分别是人物表达、素材拍摄、镜头语言、剪辑结构、音画关系、综合观感每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
</div>
</template>
@@ -0,0 +1,26 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
const router = useRouter()
const handleBack = (): void => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-x-hidden">
<ImgButton
class="absolute right-0 top-0 z-50"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
<img
src="@/assets/images/interview/practice/教师点评.png"
alt=""
/>
</div>
</template>
+37 -74
View File
@@ -2,9 +2,10 @@
import { useRouter } from 'vue-router'
import { Video } from '@/components'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage } from '@/utils'
import { Overlay } from '@/components/Overlay'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
import { ref } from 'vue'
const router = useRouter()
@@ -12,46 +13,42 @@ const router = useRouter()
const props = defineProps({
path: {
type: String,
required: true
}
required: true,
},
})
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
title: '人物表达',
description: '能清晰介绍人物身份、特点或故事,让观众认识人物',
score: 4,
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
title: '素材拍摄',
description: '画面包含必要人物镜头、环境镜头与细节素材,内容较丰富',
score: 2,
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
title: '镜头语言',
description: '合理使用移动镜头、特写、跟拍等方式增强表现力',
score: 5,
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
title: '剪辑结构',
description: '结构完整清晰,段落顺畅,内容有起承转合',
score: 0,
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
title: '音画关系',
description: '画面与配音/环境音基本匹配,能辅助理解内容',
score: 5,
},
{
title: '综合观感',
description: '作品完整,有真实感与感染力,具备观看吸引力',
score: 5,
},
])
/** 保存弹窗 */
@@ -76,54 +73,20 @@ const handleBack = (): void => {
@click="showModal = true"
/>
<Video
:src="decodeURIComponent(props.path)"
:src="getPublicPath(decodeURIComponent(props.path))"
:volume="1"
:muted="false"
:controls="true"
:loop="false"
/>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[80px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div
class="w-full bg-no-repeat bg-cover bg-left py-44"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
>
<div class="pl-10 space-y-16">
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</div>
</Overlay>
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="interview/practice/人物访谈短片创作-作品评分表"
top-tip="评分时要关注6个维度哦~分别是人物表达、素材拍摄、镜头语言、剪辑结构、音画关系、综合观感每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
</div>
</template>
+53 -23
View File
@@ -2,7 +2,8 @@
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, StepNavigation, type GuideStep } from '@/components/Guide'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { useInterviewStore } from '@/stores'
@@ -21,8 +22,9 @@ const steps = ref<GuideStep[]>([
{
id: 'step1',
audioSrc: getPublicPath('audio/py_3_006.MP3'),
content: '刚才我们学习了访谈作品的核心拍摄技能,现在通过这张思维导图来回顾一下吧!'
}
content:
'刚才我们学习了访谈作品的核心拍摄技能,现在通过这张思维导图来回顾一下吧!',
},
])
const introGuideSteps = computed<GuideStep[]>(() => {
@@ -31,8 +33,8 @@ const introGuideSteps = computed<GuideStep[]>(() => {
{
id: 'case-intro',
audioSrc: getPublicPath('audio/py_3_003.MP3'),
content: '现在,让我们通过一段访谈案例,去看看同龄人心中的职业蓝图吧!'
}
content: '现在,让我们通过一段访谈案例,去看看同龄人心中的职业蓝图吧!',
},
]
}
@@ -41,13 +43,14 @@ const introGuideSteps = computed<GuideStep[]>(() => {
{
id: 'profession-intro1',
audioSrc: getPublicPath('audio/py_3_004.MP3'),
content: '看完作品后,你可能已经感受到了,一条访谈视频,好像并不是“坐着讲话”这么简单。'
content:
'看完作品后,你可能已经感受到了,一条访谈视频,好像并不是“坐着讲话”这么简单。',
},
{
id: 'profession-intro2',
audioSrc: getPublicPath('audio/py_3_005.MP3'),
content: '那么,让我们一起来拆解访谈作品的核心技能吧!'
}
content: '那么,让我们一起来拆解访谈作品的核心技能吧!',
},
]
}
@@ -67,29 +70,29 @@ const navigationSteps = ref([
label: '拍摄执行',
content: '机位设置',
videoUrl: getPublicPath('video/interview/work/ftmk_jwsz_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'step2',
label: '拍摄执行',
content: '轴线原则',
videoUrl: getPublicPath('video/interview/work/ftmk_zxyz_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'step3',
label: '内容策划',
content: '镜头组接逻辑',
videoUrl: getPublicPath('video/interview/work/ftmk_jtzjlj_batch.mp4'),
type: 'planning' as const
type: 'planning' as const,
},
{
id: 'step4',
label: '拍摄执行',
content: '声音同步与收录',
videoUrl: getPublicPath('video/interview/work/ftmk_sytbysl_batch.mp4'),
type: 'shooting' as const
}
type: 'shooting' as const,
},
])
// 步骤导航相关状态
@@ -100,7 +103,9 @@ const currentNavigationStep = ref(1) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const resolvedSrc = computed(() => {
if (key.value === 'case') return '/video/interview/work/ft_zylt.mp4'
if (key.value === 'profession') return currentVideo.value
@@ -131,7 +136,7 @@ const handleClose = () => {
interview.setInterviewUnlockProgress(1)
router.push({
name: 'Interview',
query: { guide: 'return' }
query: { guide: 'return' },
})
return
}
@@ -153,7 +158,7 @@ const handleComplete = () => {
if (key.value === 'profession') {
if (!interview.isInterviewUnlock) interview.setIsInterviewUnlock(true)
router.push({
name: 'InterviewWork'
name: 'InterviewWork',
})
}
}
@@ -179,7 +184,10 @@ onMounted(() => {
</script>
<template>
<div class="w-full h-full relative bg-white/10" @click="handleClick">
<div
class="w-full h-full relative bg-white/10"
@click="handleClick"
>
<template v-if="key !== 'case' && key !== 'profession'">
<ImgButton
class="absolute right-0 top-0 z-10"
@@ -194,7 +202,12 @@ onMounted(() => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -204,8 +217,14 @@ onMounted(() => {
</svg>
</button>
<div v-if="key === 'vlog'" :src="image">
<img :src="getStaticImage('work/vlog背景全屏待定')" alt="" />
<div
v-if="key === 'vlog'"
:src="image"
>
<img
:src="getStaticImage('work/vlog背景全屏待定')"
alt=""
/>
</div>
<Video
v-else-if="key === 'case' || key === 'profession'"
@@ -226,8 +245,14 @@ onMounted(() => {
/>
</template>
</Video>
<div v-else-if="key === 'idea'" :src="image">
<img :src="getStaticImage('interview/work/思路提点全屏')" alt="" />
<div
v-else-if="key === 'idea'"
:src="image"
>
<img
:src="getStaticImage('interview/work/思路提点全屏')"
alt=""
/>
</div>
</div>
@@ -241,7 +266,12 @@ onMounted(() => {
/>
<!-- 引导与高亮 -->
<GuideSteps v-if="status" :steps="steps" position-offset-y="85%" @complete="handleComplete" />
<GuideSteps
v-if="status"
:steps="steps"
position-offset-y="85%"
@complete="handleComplete"
/>
<!-- 步骤导航抽屉 -->
<StepNavigation
+40 -20
View File
@@ -1,7 +1,12 @@
<script lang="tsx" name="Work" setup>
defineOptions({ name: 'Work' })
import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage, getPublicPath } from '@/utils'
import {
getBackgroundImage,
IMAGE_PATHS,
getStaticImage,
getPublicPath,
} from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps } from '@/components/Guide'
@@ -28,14 +33,14 @@ const steps = [
id: 'step1',
audioSrc: getPublicPath('audio/py_3_001.MP3'),
content: '我们常常会思考,未来自己想成为什么样的人呢?',
highlightSelector: '.img-switch-item--case'
highlightSelector: '.img-switch-item--case',
},
{
id: 'step2',
audioSrc: getPublicPath('audio/py_3_002.MP3'),
content: '如果把这份思考勇敢地说出来,被镜头记录下来,你会如何表达呢?',
highlightSelector: '.work-border'
}
highlightSelector: '.work-border',
},
]
//切换图片
@@ -43,28 +48,28 @@ const sideSwitcherItems = ref([
{
key: 'vlog',
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
image: 'interview/work/案件赏析小屏',
},
{
key: 'case',
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
image: 'interview/work/案件赏析小屏',
},
{
key: 'profession',
description: '我的职业蓝图',
image: 'interview/work/案件赏析小屏'
image: 'interview/work/案件赏析小屏',
},
{
key: 'idea',
description: '我的职业蓝图',
image: 'interview/work/思路提点小屏'
}
image: 'interview/work/思路提点小屏',
},
])
//当前选中的item
const currentItem = computed(() => {
return sideSwitcherItems.value.find(item => item.key === selected.value)
return sideSwitcherItems.value.find((item) => item.key === selected.value)
})
const handleSideSwitcherChange = (value: WorkTab) => {
@@ -76,10 +81,11 @@ const handleImageClick = () => {
name: 'InterviewVideo',
query: {
image:
workSideSwitcherRef.value?.items.find(item => item.key === selected.value)?.activeImage ||
'',
key: selected.value
}
workSideSwitcherRef.value?.items.find(
(item) => item.key === selected.value,
)?.activeImage || '',
key: selected.value,
},
})
}
@@ -106,7 +112,11 @@ function openImages(urls: string[], startIndex = 0) {
images: urls,
startIndex,
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
</script>
@@ -116,7 +126,9 @@ function openImages(urls: string[], startIndex = 0) {
<!-- 背景图片-center -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage('interview/work/任务取景框背景') }"
:style="{
backgroundImage: getBackgroundImage('interview/work/任务取景框背景'),
}"
></div>
<!-- 关闭 -->
@@ -133,10 +145,14 @@ function openImages(urls: string[], startIndex = 0) {
>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
class="absolute left-[46%] top-[45%] -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>
<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"
@@ -169,7 +185,11 @@ function openImages(urls: string[], startIndex = 0) {
<!-- 任务取景按钮 -->
<ImgButton
class="absolute right-14 bottom-10"
:image="isInterviewUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
:image="
isInterviewUnlock
? IMAGE_PATHS.WORK.TASK_BTN_OK
: IMAGE_PATHS.WORK.TASK_BTN
"
@click="handleWorkClick"
/>
+21 -9
View File
@@ -2,7 +2,8 @@
import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { getPublicPath, IMAGE_PATHS } from '@/utils'
import { Video } from '@/components'
import { useVlogStore } from '@/stores/modules/vlog'
@@ -22,32 +23,34 @@ const navigationSteps = ref([
label: '后期制作',
content: '剪辑起点',
videoUrl: getPublicPath('video/vlog/editing/Vlogmk_jjqd_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step2',
label: '后期制作',
content: '音画匹配',
videoUrl: getPublicPath('video/vlog/editing/Vlogmk_yhpp_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step3',
label: '后期制作',
content: '剪辑节奏',
videoUrl: getPublicPath('video/vlog/editing/Vlogmk_jjjz_batch.mp4'),
type: 'post' as const
type: 'post' as const,
},
{
id: 'step4',
label: '后期制作',
content: '声音与转场',
videoUrl: getPublicPath('video/vlog/editing/Vlogmk_syyzc_batch.mp4'),
type: 'post' as const
}
type: 'post' as const,
},
])
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const currentVideoIndex = ref(0)
const playing = ref(false)
@@ -109,7 +112,12 @@ const handleEndingGuideComplete = () => {
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"
@@ -127,7 +135,11 @@ const handleEndingGuideComplete = () => {
@close="showStepNavigation = false"
/>
<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_019.MP3')">
剪辑不是简单拼接素材,而是在重新组织故事、控制节奏、强化表达。
</GuideStepsItem>
@@ -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"
+113 -27
View File
@@ -4,7 +4,8 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { useRouter } from 'vue-router'
import BackToTopIcon from '@/assets/svg/back-to-top.svg'
import BackToTopIcon from '@/assets/images/components/置顶.png'
import { useVlogStore } from '@/stores/modules/vlog'
const router = useRouter()
@@ -12,6 +13,8 @@ const TOTAL_PAGES = 3
const PAGE_HEIGHT = 1080
const SCROLL_DURATION = 1000
const vlogStore = useVlogStore()
const currentPage = ref<number>(0)
const worksScrollIndex = ref<number>(0)
const animatingToPage = ref<number>(0)
@@ -23,7 +26,9 @@ const isLastPage = computed<boolean>(() => currentPage.value >= TOTAL_PAGES - 1)
/** 是否正在从第二页回到第一页 */
const isScrollingBackToFirst = computed<boolean>(() => {
return isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
return (
isAnimating.value && currentPage.value === 1 && animatingToPage.value === 0
)
})
/** 第一页坐着的人物是否显示 */
@@ -45,21 +50,22 @@ interface WorkItem {
const worksList = ref<WorkItem[]>([
{
id: 1,
image: 'https://picsum.photos/400/300?random=1',
title: '作品名称',
image: getStaticImage('vlog/practice/vlog-熙熙攘攘我们的劳动'),
title: '熙熙攘攘我们的劳动',
stars: 4,
path: '/video/vlog/practice/vlog_case1_batch.mp4',
description:
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介',
},
{
id: 2,
image: 'https://picsum.photos/400/300?random=2',
title: '作品名称',
image: getStaticImage('vlog/practice/vlog-匠心精神劳动承匠心'),
title: '匠心精神劳动承匠心',
stars: 5,
path: '/video/vlog/practice/vlog_case2_batch.mp4',
description: '作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介'
}
description:
'作品简介作品简介作品简介作品简介作品简介作品简介作品简介作品简介',
},
])
/** 左滑动作品列表 */
@@ -150,17 +156,43 @@ const handleWheel = (event: WheelEvent): void => {
}
}
const restoreScrollPage = (): void => {
const savedPage = vlogStore.practiceScrollPage
const targetPage = Math.max(0, Math.min(savedPage, TOTAL_PAGES - 1))
currentPage.value = targetPage
animatingToPage.value = targetPage
requestAnimationFrame(() => {
if (containerRef.value) {
containerRef.value.scrollTop = targetPage * PAGE_HEIGHT
}
})
}
const saveScrollPage = (): void => {
const page = containerRef.value
? Math.round(containerRef.value.scrollTop / PAGE_HEIGHT)
: currentPage.value
const targetPage = Math.max(0, Math.min(page, TOTAL_PAGES - 1))
vlogStore.setPracticeScrollPage(targetPage)
}
onMounted(() => {
restoreScrollPage()
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
})
onUnmounted(() => {
saveScrollPage()
containerRef.value?.removeEventListener('wheel', handleWheel)
})
</script>
<template>
<div ref="containerRef" class="w-full h-full overflow-auto">
<div
ref="containerRef"
class="w-full h-full overflow-auto"
>
<div class="relative min-h-full overflow-hidden">
<!-- 关闭 -->
<ImgButton
@@ -169,16 +201,19 @@ onUnmounted(() => {
@click="handleClose"
/>
<!-- 实战操作台 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat relative"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/实战操作台首页') }"
>
<div class="w-full h-[1080px] bg-[#F5EFE6] relative">
<ImgButton
class="absolute left-40 bottom-28 z-10"
image="vlog/practice/查看任务要求"
@click="scrollToNextPage"
/>
<!-- img -->
<img
class="absolute left-0 top-0 z-10"
draggable="false"
:src="getStaticImage('vlog/practice/vlog')"
alt=""
/>
<img
class="absolute right-0"
draggable="false"
@@ -193,12 +228,18 @@ onUnmounted(() => {
>
<ImgButton
class="absolute right-20 bottom-0 z-99"
image="vlog/practice/我的创作笔记"
image="vlog/practice/我的创作笔记btn"
path="/vlog/practice/my-notes"
/>
<div class="task-panel w-[757px] h-full ml-10 overflow-y-auto" @wheel.stop>
<img :src="getStaticImage('vlog/practice/滑动任务面板')" alt="" />
<div
class="task-panel w-[757px] h-full ml-10 overflow-y-auto"
@wheel.stop
>
<img
:src="getStaticImage('vlog/practice/任务要求')"
alt=""
/>
</div>
<img
@@ -220,7 +261,9 @@ onUnmounted(() => {
<!-- 他人实战作品 -->
<div
class="w-full h-[1080px] bg-cover bg-no-repeat pt-44 px-16"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/他人实战作品') }"
:style="{
backgroundImage: getBackgroundImage('vlog/practice/他人实战作品'),
}"
>
<!-- 作品列表 -->
<div class="w-full overflow-hidden">
@@ -235,15 +278,27 @@ onUnmounted(() => {
>
<!-- 作品图片 -->
<div class="w-full h-[280px] rounded-2xl overflow-hidden">
<img class="w-full h-full object-cover" :src="work.image" :alt="work.title" />
<img
class="w-full h-full object-cover"
:src="work.image"
:alt="work.title"
/>
</div>
<!-- 作品信息 -->
<div class="flex flex-col items-center pt-8 space-y-4">
<h3 class="text-5xl font-bold text-gray-800 mb-2">{{ work.title }}</h3>
<h3 class="text-5xl font-bold text-gray-800 mb-2">
{{ work.title }}
</h3>
<!-- 星星评分 -->
<StarRating :score="work.stars" size="lg" readonly />
<StarRating
:score="work.stars"
size="lg"
readonly
/>
<!-- 简介 -->
<p class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed">
<p
class="h-[110px] text-2xl text-gray-600 mb-3 line-clamp-3 leading-relaxed"
>
{{ work.description }}
</p>
<!-- 查看按钮 -->
@@ -256,7 +311,7 @@ onUnmounted(() => {
</div>
</div>
<!-- 指示按钮 -->
<!-- 指示按钮,暂时不要
<div class="absolute bottom-5 left-1/2 -translate-x-1/2 flex gap-16">
<img
class="cursor-pointer hover:scale-110 transition-transform"
@@ -267,19 +322,23 @@ onUnmounted(() => {
/>
<img
class="cursor-pointer hover:scale-110 transition-transform rotate-180"
:class="{ 'opacity-50 cursor-not-allowed': worksScrollIndex >= worksList.length - 3 }"
:class="{
'opacity-50 cursor-not-allowed':
worksScrollIndex >= worksList.length - 3,
}"
:src="getStaticImage('vlog/practice/滑动左')"
alt="向右"
@click="scrollWorksRight"
/>
</div>
-->
</div>
</div>
<!-- 下滑按钮 - 最后一页隐藏 -->
<ImgButton
v-show="!isLastPage"
class="absolute bottom-0 left-1/2 -translate-x-1/2"
class="absolute bottom-0 left-1/2 down-scroll-hint"
:image="'vlog/practice/下滑'"
@click="scrollToNextPage"
/>
@@ -287,13 +346,24 @@ onUnmounted(() => {
<!-- 回到顶部按钮 - 最后一页显示 -->
<img
v-show="isLastPage"
class="absolute bottom-10 right-10 cursor-pointer transition-all duration-300 hover:scale-105"
class="absolute bottom-10 left-1/2 -translate-x-1/2 cursor-pointer transition-all duration-300 hover:scale-105"
:src="BackToTopIcon"
@click="scrollToTop"
/>
<!-- 老师点评按钮 - 最后一页显示 -->
<ImgButton
v-show="isLastPage"
class="absolute right-2 bottom-10 z-30"
image="components/教师点评按钮"
path="/vlog/practice/teacher-comment"
/>
<!-- 跨页人物动画 -->
<div v-show="showSittingPerson || isScrollingBackToFirst" class="character-container">
<div
v-show="showSittingPerson || isScrollingBackToFirst"
class="character-container"
>
<!-- 坐着的人物 - 第一页 -->
<Transition name="fade">
<img
@@ -332,6 +402,22 @@ onUnmounted(() => {
/* Chrome/Safari/Opera */
}
.down-scroll-hint {
transform: translateX(-50%);
animation: down-scroll-hint 1.4s ease-in-out infinite;
}
@keyframes down-scroll-hint {
0%,
100% {
transform: translate(-50%, 0);
}
50% {
transform: translate(-50%, 12px);
}
}
/* 第一页人物动画容器 */
.character-container {
position: fixed;
+45 -94
View File
@@ -3,8 +3,9 @@ import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { Overlay } from '@/components/Overlay'
import { StarRating } from '@/components/Rating'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
const router = useRouter()
@@ -21,42 +22,39 @@ const handleBack = (): void => {
const disabled = computed<boolean>(() => noteContent.value.trim() === '')
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
title: '开场建立',
description: '能清晰交代时间、地点与事件开端,观众易于进入情境',
score: 4,
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
title: '拍摄表达',
description:
'合理运用镜头语言(如推、拉、摇、跟拍等),能够表现空间关系或记录真实过程',
score: 2,
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
title: '真实捕捉',
description: '能捕捉到具有现场感的瞬间(如互动、行动、细节),画面不生硬',
score: 5,
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
title: '剪辑结构',
description: '结构清晰,内容有基本顺序(如开场—过程—收束),画面衔接自然',
score: 0,
},
{
title: '音画关系',
description: '画面与配音/环境音基本匹配,能辅助理解内容',
score: 5,
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
description: '视频完整、有连贯观看体验,能表达出一次真实记录或个人感受',
score: 5,
},
])
const handleSave = (): void => {
console.log('保存笔记')
@@ -67,7 +65,9 @@ const handleSave = (): void => {
<template>
<div
class="w-full h-full bg-no-repeat bg-cover px-8 pt-[400px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/创作笔记底图') }"
:style="{
backgroundImage: getBackgroundImage('vlog/practice/我的创作笔记'),
}"
>
<ImgButton
class="absolute top-0 right-0"
@@ -83,53 +83,25 @@ const handleSave = (): void => {
></textarea>
<div class="absolute bottom-24 left-1/2 -translate-x-1/2 flex gap-60">
<ImgButton image="vlog/practice/保存" @click="handleSave" />
<ImgButton image="vlog/practice/自评" :disabled="disabled" @click="showModal = true" />
<ImgButton
image="vlog/practice/保存"
@click="handleSave"
/>
<ImgButton
image="vlog/practice/自评"
:disabled="disabled"
@click="showModal = true"
/>
</div>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[100px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div class="pl-10 space-y-10 mt-10">
<p class="glow-text">
评分时要关注5个维度哦~每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~
</p>
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<p class="glow-text">
己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~
</p>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</Overlay>
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="vlog/practice/Vlog作品评分表"
top-tip="评分时要关注6个维度哦~分别是开场建立、拍摄表达、真实捕捉、剪辑结构、音画关系、综合感受,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
</div>
</template>
@@ -146,25 +118,4 @@ textarea::placeholder {
color: #5d5b5b;
font-weight: 700;
}
.glow-text {
color: white;
font-size: 2rem;
font-weight: bold;
text-shadow:
/* 描边效果 - 多层阴影模拟描边 */
-1px -1px 0 #0066ff,
1px -1px 0 #0066ff,
-1px 1px 0 #0066ff,
1px 1px 0 #0066ff,
-2px -2px 0 #0066ff,
2px -2px 0 #0066ff,
-2px 2px 0 #0066ff,
2px 2px 0 #0066ff,
/* 发光效果 - 蓝色光晕 */ 0 0 10px #0066ff,
0 0 20px #0066ff,
0 0 30px #0066ff,
0 0 40px #0066ff,
0 0 50px #0066ff;
}
</style>
@@ -0,0 +1,26 @@
<script lang="ts" setup>
import { useRouter } from 'vue-router'
import { IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
const router = useRouter()
const handleBack = (): void => {
router.back()
}
</script>
<template>
<div class="relative w-full h-full overflow-x-hidden">
<ImgButton
class="absolute right-0 top-0 z-50"
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
<img
src="@/assets/images/vlog/practice/教师点评.png"
alt=""
/>
</div>
</template>
+36 -72
View File
@@ -2,9 +2,10 @@
import { useRouter } from 'vue-router'
import { Video } from '@/components'
import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay'
import PracticeRatingModal, {
type PracticeRatingItem,
} from '@/components/PracticeRatingModal'
import { IMAGE_PATHS, getPublicPath } from '@/utils'
import { ref } from 'vue'
const router = useRouter()
@@ -12,46 +13,43 @@ const router = useRouter()
const props = defineProps({
path: {
type: String,
required: true
}
required: true,
},
})
interface EvaluationItem {
/** 标题 */
title: string
/** 描述 */
description: string
/** 评分 */
score: number
}
/** 评估项目列表 */
const evaluationItems = ref<EvaluationItem[]>([
const evaluationItems = ref<PracticeRatingItem[]>([
{
title: '主题与内容',
description: '主题明确聚焦"身边美好",内容真实、有感染力,能让观众感受到日常闪光',
score: 3
title: '开场建立',
description: '能清晰交代时间、地点与事件开端,观众易于进入情境',
score: 4,
},
{
title: '拍摄技巧',
description: '运镜自然、光线运用合理、画面清晰饱和度、能辅助主题表达(至少1种运镜+1种光线)',
score: 4
title: '拍摄表达',
description:
'合理运用镜头语言(如推、拉、摇、跟拍等),能够表现空间关系或记录真实过程',
score: 2,
},
{
title: '剪辑与包装',
description: '素材拼接流畅,调色/滤镜适配、字幕清晰、音乐不盖人声、整体风格统一',
score: 5
title: '真实捕捉',
description: '能捕捉到具有现场感的瞬间(如互动、行动、细节),画面不生硬',
score: 5,
},
{
title: '创意与独特性',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”,避免平淡复刻',
score: 4
title: '剪辑结构',
description: '结构清晰,内容有基本顺序(如开场—过程—收束),画面衔接自然',
score: 0,
},
{
title: '音画关系',
description: '画面与配音/环境音基本匹配,能辅助理解内容',
score: 5,
},
{
title: '综合感受',
description: '视角新颖、有个人观察,或用特殊细节/情节展现“美好”避免平淡复刻',
score: 5
}
description: '视频完整、有连贯观看体验,能表达出一次真实记录或个人感受',
score: 5,
},
])
/** 保存弹窗 */
@@ -83,47 +81,13 @@ const handleBack = (): void => {
:loop="false"
/>
<!-- 保存图片弹窗 -->
<Overlay :visible="showModal" :clickable="false">
<div
class="w-[1795px] h-[987px] bg-no-repeat bg-cover p-10 pt-[80px]"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分底板') }"
>
<ImgButton
class="absolute -top-8 -right-14 z-10"
:image="IMAGE_PATHS.COMPONENTS.CLOSE"
@click="showModal = false"
/>
<div class="relative h-full overflow-y-scroll px-8">
<div
class="w-full bg-no-repeat bg-cover bg-left py-44"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/滑动表内文字') }"
>
<div class="pl-10 space-y-16">
<div
v-for="(item, index) in evaluationItems"
:key="index"
class="relative w-full h-[182px] bg-no-repeat bg-cover px-12 py-3 flex justify-between"
:style="{ backgroundImage: getBackgroundImage('vlog/practice/评分条') }"
>
<div class="pr-8 space-y-10">
<h4 class="text-3xl font-bold text-white mb-3">
{{ `0${index + 1}` }} {{ item.title }}
</h4>
<p class="text-3xl text-gray-800 font-bold leading-relaxed">
{{ item.description }}
</p>
</div>
<StarRating class="absolute -bottom-5 right-10" :score="item.score" size="lg" />
</div>
<div class="w-full flex justify-center">
<ImgButton image="vlog/practice/保存" @click="showModal = false" />
</div>
</div>
</div>
</div>
</div>
</Overlay>
<PracticeRatingModal
:visible="showModal"
:items="evaluationItems"
background-image="vlog/practice/Vlog作品评分表"
top-tip="评分时要关注6个维度哦~分别是开场建立、拍摄表达、真实捕捉、剪辑结构、音画关系、综合感受,每个维度满分5分,能打1-5分,记得对照作品表现来打分呀~"
bottom-tip="己评价的实战作品,从封面入口就能直接看到具体评分!打了多少分一目了然,不用再点进详情页翻找啦~"
@close="showModal = false"
/>
</div>
</template>
+59 -25
View File
@@ -2,9 +2,15 @@
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, StepNavigation } from '@/components/Guide'
import { GuideSteps } from '@/components/Guide'
import StepNavigation from '@/components/Guide/StepNavigation.vue'
import { Video } from '@/components'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import {
getBackgroundImage,
getPublicPath,
getStaticImage,
IMAGE_PATHS,
} from '@/utils'
import { useVlogStore } from '@/stores'
const router = useRouter()
@@ -21,20 +27,22 @@ const ideaSteps = [
{
id: 'idea-step1',
content: '一条优秀的 Vlog,往往不是记录"大事",而是认真记录生活里的小事。',
audioSrc: getPublicPath('audio/py_2_004.MP3')
audioSrc: getPublicPath('audio/py_2_004.MP3'),
},
{
id: 'idea-step2',
content: '试着把刚才学到的方法用起来,你会发现,原来平凡的日子里,也藏着许多值得被记录的瞬间。',
audioSrc: getPublicPath('audio/py_2_005.MP3')
}
content:
'试着把刚才学到的方法用起来,你会发现,原来平凡的日子里,也藏着许多值得被记录的瞬间。',
audioSrc: getPublicPath('audio/py_2_005.MP3'),
},
]
const steps = ref<{ id: string; content: string; audioSrc?: string }[]>([
{
id: 'step1',
content: '试着用这些方法记录身边的小事,你会发现,原来平凡的日子里有这么多值得捕捉的瞬间。'
}
content:
'试着用这些方法记录身边的小事,你会发现,原来平凡的日子里有这么多值得捕捉的瞬间。',
},
])
// 步骤导航数据
@@ -44,36 +52,36 @@ const navigationSteps = ref([
label: '拍摄执行',
content: '基础景别应用',
videoUrl: getPublicPath('video/vlog/work/Vlogmk_jcjbyy_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'step2',
label: '内容策划',
content: '简单脚本策划',
videoUrl: getPublicPath('video/vlog/work/Vlogmk_jdjbch_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'step3',
label: '后期制作',
content: '素材导入、剪辑、拼接',
videoUrl: getPublicPath('video/vlog/work/Vlogmk_scdrjjpj_batch.mp4'),
type: 'planning' as const
type: 'planning' as const,
},
{
id: 'step4',
label: '后期制作',
content: '添加背景音乐',
videoUrl: getPublicPath('video/vlog/work/Vlogmk_tjbjyy_batch.mp4'),
type: 'shooting' as const
type: 'shooting' as const,
},
{
id: 'step5',
label: '拍摄执行',
content: '用视听语言初步表达观点',
videoUrl: getPublicPath('video/vlog/work/Vlogmk_ystyycbbdgd_batch.mp4'),
type: 'post' as const
}
type: 'post' as const,
},
])
// 步骤导航相关状态
@@ -84,7 +92,9 @@ const currentNavigationStep = ref(0) // 对应视频索引,从0开始
const currentVideoIndex = ref(0)
//按顺序播放
const currentVideo = computed(() => navigationSteps.value[currentVideoIndex.value].videoUrl)
const currentVideo = computed(
() => navigationSteps.value[currentVideoIndex.value].videoUrl,
)
const resolvedSrc = computed(() => {
if (key.value === 'case')
return getPublicPath('video/vlog/work/anlishangxi_meishi_xiaoyuanzi.mp4')
@@ -116,7 +126,7 @@ const handleClose = () => {
if (vlog.vlogUnlockProgress === 0) vlog.setVlogUnlockProgress(1)
router.push({
name: 'Vlog',
query: { guide: 'return' }
query: { guide: 'return' },
})
return
}
@@ -147,7 +157,7 @@ const handleComplete = () => {
if (key.value === 'profession') {
if (!vlog.isVlogUnlock) vlog.setIsVlogUnlock(true)
router.push({
name: 'VlogWork'
name: 'VlogWork',
})
}
}
@@ -179,12 +189,14 @@ onMounted(() => {
<div
:class="[
'absolute left-4 top-4 z-10 w-[359px] h-[96px] bg-cover bg-center',
'flex justify-center items-center'
'flex justify-center items-center',
]"
:style="{ backgroundImage: getBackgroundImage(image) }"
>
<span
:class="['ml-6 text-white [text-shadow:5px_5px_5px_rgba(0,0,0,0.4)] text-5xl font-bold']"
:class="[
'ml-6 text-white [text-shadow:5px_5px_5px_rgba(0,0,0,0.4)] text-5xl font-bold',
]"
>
{{ label }}
</span>
@@ -203,7 +215,12 @@ onMounted(() => {
@click="showStepNavigation = true"
title="步骤导航"
>
<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"
@@ -213,8 +230,14 @@ onMounted(() => {
</svg>
</button>
<div v-if="key === 'vlog'" :src="image">
<img :src="getStaticImage('work/vlog背景全屏待定')" alt="" />
<div
v-if="key === 'vlog'"
:src="image"
>
<img
:src="getStaticImage('work/vlog背景全屏待定')"
alt=""
/>
</div>
<Video
v-else-if="key === 'case' || key === 'profession'"
@@ -277,13 +300,24 @@ onMounted(() => {
/>
</template>
</Video>
<div v-else-if="key === 'idea'" :src="image">
<img :src="getStaticImage('work/思路提点全屏')" alt="" />
<div
v-else-if="key === 'idea'"
:src="image"
>
<img
:src="getStaticImage('work/思路提点全屏')"
alt=""
/>
</div>
</div>
<!-- 引导与高亮 -->
<GuideSteps v-if="status" :steps="steps" position-offset-y="85%" @complete="handleComplete" />
<GuideSteps
v-if="status"
:steps="steps"
position-offset-y="85%"
@complete="handleComplete"
/>
<!-- 步骤导航抽屉 -->
<StepNavigation
+36 -19
View File
@@ -1,7 +1,12 @@
<script lang="tsx" name="VlogWork" setup>
defineOptions({ name: 'VlogWork' })
import { ref, computed } from 'vue'
import { getBackgroundImage, getPublicPath, 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 } from '@/components/Guide'
@@ -28,22 +33,22 @@ const steps = [
id: 'step1',
content: '现在我们要一起走进 Vlog 的世界。',
audioSrc: getPublicPath('audio/py_2_001.MP3'),
highlightSelector: '.work-border'
highlightSelector: '.work-border',
},
{
id: 'step2',
content:
'Vlog 是用视频记录生活的一种方式。它像一本会动的日记,能把日常里的小事、真实的情绪,以及那些稍纵即逝的瞬间,用镜头生动地保存下来。',
audioSrc: getPublicPath('audio/py_2_002.MP3'),
highlightSelector: '.work-border'
highlightSelector: '.work-border',
},
{
id: 'step3',
content:
'接下来,我们将通过一个具体案例,一起看看 Vlog 是如何发现生活中的美好、记录生活中的温度。',
audioSrc: getPublicPath('audio/py_2_003.MP3'),
highlightSelector: '.img-switch-item--case'
}
highlightSelector: '.img-switch-item--case',
},
]
//切换图片
@@ -51,28 +56,28 @@ const sideSwitcherItems = ref([
{
key: 'vlog',
description: '记录身边的美好',
image: 'work/vlog背景全屏待定'
image: 'work/vlog背景全屏待定',
},
{
key: 'case',
description: '案例赏析-桂花酒酿小圆子',
image: 'work/案例背景待定'
image: 'work/案例背景待定',
},
{
key: 'profession',
description: '专业剖析-桂花酒酿小圆子',
image: 'work/专业剖析小屏'
image: 'work/专业剖析小屏',
},
{
key: 'idea',
description: '思路提点-桂花酒酿小圆子',
image: 'work/思路提点小屏'
}
image: 'work/思路提点小屏',
},
])
//当前选中的item
const currentItem = computed(() => {
return sideSwitcherItems.value.find(item => item.key === selected.value)
return sideSwitcherItems.value.find((item) => item.key === selected.value)
})
const handleSideSwitcherChange = (value: WorkTab) => {
@@ -80,14 +85,16 @@ const handleSideSwitcherChange = (value: WorkTab) => {
}
const handleImageClick = () => {
const currentItem = workSideSwitcherRef.value?.items.find(item => item.key === selected.value)
const currentItem = workSideSwitcherRef.value?.items.find(
(item) => item.key === selected.value,
)
router.push({
name: 'VlogVideo',
query: {
image: currentItem?.activeImage || '',
key: selected.value,
label: currentItem?.label || ''
}
label: currentItem?.label || '',
},
})
}
@@ -114,7 +121,11 @@ function openImages(urls: string[], startIndex = 0) {
images: urls,
startIndex,
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
getScaleMetrics: () => ({
scale: scale.value,
offsetX: offsetX.value,
offsetY: offsetY.value,
}),
})
}
</script>
@@ -141,10 +152,14 @@ function openImages(urls: string[], startIndex = 0) {
>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
class="absolute left-[46%] top-[45%] -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>
<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"
@@ -177,7 +192,9 @@ function openImages(urls: string[], startIndex = 0) {
<!-- 任务取景按钮 -->
<ImgButton
class="absolute right-14 bottom-10"
:image="isVlogUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN"
:image="
isVlogUnlock ? IMAGE_PATHS.WORK.TASK_BTN_OK : IMAGE_PATHS.WORK.TASK_BTN
"
@click="handleWorkClick"
/>