feat: 添加思维导图支持,更新引导步骤组件,优化视频播放与学习进度追踪功能

This commit is contained in:
zhongyidian committed 2026-01-20 17:54:05 +08:00
1 parent e9a1fee27f
commit c4c9be9d35
98 files changed
+3178 -551

No files matched your search

@@ -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, GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getStaticImage, IMAGE_PATHS } from '@/utils'
// 视频导入
@@ -290,6 +290,14 @@ const handleQuestionsComplete = () => {
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>欢迎来到基础拍摄训练区!</GuideStepsItem>
<GuideStepsItem
>接下来,我会带你依次认识手机摄像头、看懂景别、用好慢动作与延时技法。</GuideStepsItem
>
<GuideStepsItem>还会教你如何拍出更稳的微距和更亮的夜景。</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -9,6 +9,7 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import video1 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
import video2 from '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
import video3 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const props = defineProps<{
@@ -143,6 +144,11 @@ const handleStepNavigationClose = () => {
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>想拍出好的视频效果,画面稳定性是最基本的要求。</GuideStepsItem>
<GuideStepsItem>接下来,我们来学习正确的持机方式和拍摄姿势。</GuideStepsItem>
</GuideSteps>
</div>
</template>
+36 -2
View File
@@ -7,7 +7,7 @@ import { Video } from '@/components'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import video1 from '/video/new/xsdh_rhxzgt_batch.mp4'
import video2 from '/video/new/xsdh_jzgtf_batch.mp4'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const props = defineProps<{
label: string
@@ -17,6 +17,8 @@ const props = defineProps<{
const playing = ref(false)
const status = ref(false)
const guideStepIndex = ref(0)
// 步骤导航数据
const navigationSteps = ref([
{
@@ -94,7 +96,7 @@ onMounted(() => {})
/>
<!-- 步骤导航按钮 -->
<button
class="absolute right-4 top-1/2 z-10 -translate-y-1/2 w-12 h-12 bg-blue-500/80 hover:bg-blue-500 rounded-full flex items-center justify-center text-white transition-all duration-200"
class="absolute right-4 top-1/2 z-10 -translate-y-1/2 w-12 h-12 bg-blue-500/80 hover:bg-blue-500 rounded-full flex items-center justify-center text-white transition-all duration-200 step-navigation-btn"
@click="showStepNavigation = true"
title="步骤导航"
>
@@ -137,6 +139,38 @@ onMounted(() => {})
@step-click="handleStepClick"
@close="handleStepNavigationClose"
/>
<GuideSteps
:visible="true"
:highlight-selector="guideStepIndex === 3 ? '.step-navigation-btn' : ''"
position-offset-y="85%"
@step-change="(_: any, index: number) => (guideStepIndex = index)"
>
<GuideStepsItem>你有没有过这样的感觉?画面里什么都有,却不知道重点在哪里?</GuideStepsItem>
<GuideStepsItem>很多时候,不是内容不够好,而是需要有构图思维。</GuideStepsItem>
<GuideStepsItem>所谓构图,就是在画面中对元素进行选择、取舍和安排</GuideStepsItem>
<GuideStepsItem>
<div class="flex flex-wrap items-center justify-start">
<div>一起来看看吧~</div>
<div class="text-yellow-400">
点击<button
class="w-10 h-10 bg-blue-500/80 hover:bg-blue-500 rounded-full inline-flex items-center justify-center text-white transition-all duration-200"
title="步骤导航"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M3 4H17M3 8H17M3 12H17M3 16H17"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</svg>
</button>
可以切换不同的学习内容!
</div>
</div>
</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -4,6 +4,7 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { Video } from '@/components'
import { getPublicPath } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
@@ -45,5 +46,9 @@ const back = () => {
@click="router.push({ name: 'FilmColor' })"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
-2
View File
@@ -117,8 +117,6 @@ const handleTryOther = () => {
}
const handleFinish = () => {
useFilmStore().setFilmUnlockProgress(4)
router.push({
name: 'Film'
})
+37 -6
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video } from '@/components'
const router = useRouter()
@@ -53,11 +54,18 @@ const goContinuity = () => {
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
:style="{ backgroundImage: getBackgroundImage('film/after-editing/剪辑底图') }"
></div>
<!-- 标题 -->
<img
class="absolute left-0 top-6"
:src="getStaticImage('film/after-editing/剪辑标题')"
alt=""
/>
<!-- 视频播放区域 -->
<div v-if="!videoEnded" class="absolute inset-0 flex items-center justify-center">
<!-- <div v-if="!videoEnded" class="absolute inset-0 flex items-center justify-center">
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
<Video
class="w-full h-full"
@@ -72,10 +80,10 @@ const goContinuity = () => {
@ended="handleVideoEnded"
/>
</div>
</div>
</div> -->
<!-- 内容卡片 + 按钮 -->
<div
<!-- <div
v-if="videoEnded"
class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none"
>
@@ -84,7 +92,6 @@ const goContinuity = () => {
>
<div class="flex items-baseline justify-between gap-6">
<h1 class="text-5xl font-extrabold tracking-wide">剪辑</h1>
<!-- <span class="text-3xl opacity-80">从不同维度练习你的剪辑技巧</span> -->
</div>
<p class="text-2xl leading-8 opacity-90">
选择下面任意一个练习模块,进入对应的剪辑场景,按提示完成小任务,逐步建立自己的剪辑手感和创作节奏。
@@ -121,6 +128,26 @@ const goContinuity = () => {
</button>
</div>
</div>
</div> -->
<div class="absolute left-[0px] bottom-[42%] translate-y-1/2 bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/剪辑初始'" @click="goStart" />
</div>
<div class="absolute right-[0px] top-[160px] bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/节奏型剪辑'" @click="goRhythm" />
</div>
<div class="absolute right-[42px] top-[410px] bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/声音转场'" @click="goSoundTransition" />
</div>
<div class="absolute right-[520px] top-[580px] bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/快切'" @click="goCut" />
</div>
<div class="absolute right-[42px] bottom-[270px] bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/关联剪辑'" @click="goAssociation" />
</div>
<div class="absolute right-[42px] bottom-[16px] bg-cover bg-center bg-no-repeat">
<ImgButton :image="'film/after-editing/连续剪辑'" @click="goContinuity" />
</div>
<!-- 关闭 -->
@@ -129,6 +156,10 @@ const goContinuity = () => {
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose"
/>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -1,21 +1,34 @@
<script setup lang="ts">
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
import { Video } from '@/components'
import { Video, GuideSteps, GuideStepsItem } from '@/components'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { useFilmStore } from '@/stores'
const router = useRouter()
const film = useFilmStore()
const handleBack = () => {
router.back()
}
// 创作随机性视频 URL(暂时用其他视频代替)
const randomVideoUrl = getPublicPath('video/on-site/dygmk_czsjx_batch.mp4')
const randomVideoUrl = getPublicPath('video/on-site/dygmk_zlsxhbj.mp4')
// 视频播放状态
const videoPlaying = ref(false)
const showGuide = ref(true)
const handleComplete = () => {
showGuide.value = false
}
onMounted(() => {
film.setFilmUnlockProgress(4)
})
</script>
<template>
@@ -36,7 +49,6 @@ const videoPlaying = ref(false)
:volume="1"
:controls="true"
:hideNativeControls="true"
:autoplay="true"
/>
</div>
@@ -46,5 +58,9 @@ const videoPlaying = ref(false)
:image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack"
/>
<GuideSteps v-if="showGuide" position-offset-y="85%" @complete="handleComplete">
<GuideStepsItem> 现在一起来看看导出后的影片效果吧! </GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -3,6 +3,7 @@ import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { Video } from '@/components'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
@@ -59,5 +60,9 @@ const back = () => {
/>
</div>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+4
View File
@@ -4,6 +4,7 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
@@ -45,5 +46,8 @@ const handleOpenImages = () => {
@click="handleOpenImages"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+58 -6
View File
@@ -2,7 +2,7 @@
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { ProgressBar, Overlay } from '@/components'
import { useRouter } from 'vue-router'
import { useFilmStore } from '@/stores'
@@ -21,6 +21,19 @@ const progress = computed(() => {
return Math.round((currentStep.value / 4) * 100)
})
const guideIndex = ref<number>(0)
const showTaskGuide = computed(
() => currentStep.value === 1 && guideIndex.value < 2 && film.isGuideFilmTask
)
const handleGuideClick = () => {
// Mark guide as completed after the second click and hide permanently
if (guideIndex.value >= 1) {
film.setIsGuideFilmTask(false)
}
guideIndex.value++
}
const handleComplete = () => {
// console.log('完成前置条件')
// 处理完成逻辑
@@ -44,10 +57,25 @@ const handleClose = () => {
}
onMounted(() => {
if (film.hasShownGuideFilmTask) {
film.setIsGuideFilmTask(false)
}
if (currentStep.value !== 0 && currentStep.value !== 4) {
visible.value = true
}
})
// Cache the guide after it pops up once so it does not re-appear
watch(
showTaskGuide,
val => {
if (val && !film.hasShownGuideFilmTask) {
film.setHasShownGuideFilmTask(true)
}
},
{ immediate: true }
)
</script>
<template>
@@ -61,7 +89,7 @@ onMounted(() => {
<!-- 进度数据 -->
<div class="absolute top-[140px] left-[calc(50%)] -translate-x-1/2">
<div
class="flex items-center justify-center text-white [font-family:DingTalkJinBuTi] font-[1000] [text-shadow:_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1]"
class="flex items-center justify-center text-white font-[1000] [text-shadow:_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1,_6px_6px_4px_#2b80c1]"
>
<span class="text-6xl">学习进度:</span>
<span class="text-7xl">{{ progress }}%</span>
@@ -83,7 +111,7 @@ onMounted(() => {
<!-- 标题 -->
<div class="absolute top-2 left-1/2 -translate-x-1/2">
<ImgButton image="film/电影-创作与表达" />
<img :src="getStaticImage('film/电影-创作与表达.png')" alt="" />
</div>
<!-- 任务取景按钮 -->
@@ -97,8 +125,20 @@ onMounted(() => {
/>
</div>
<GuideSteps
:visible="currentStep === 4"
highlight-selector=".right-section"
position-offset-y="50%"
>
<GuideStepsItem> 点击这里进入实战操作台 </GuideStepsItem>
</GuideSteps>
<!-- 引导任务 -->
<ImgButton class="absolute right-24 bottom-24" :image="IMAGE_PATHS.VLOG.TASK_PERSON" />
<ImgButton
v-if="currentStep !== 4"
class="absolute right-24 bottom-24"
:image="IMAGE_PATHS.VLOG.TASK_PERSON"
/>
<Overlay content-class="w-full h-full" :visible="visible && currentStep < 4">
<!-- 关闭按钮 -->
@@ -108,6 +148,17 @@ onMounted(() => {
@click="visible = false"
/>
<!-- 引导与高亮 -->
<div
class="absolute top-0 left-0 w-full h-full z-40 pointer-events-none"
v-if="showTaskGuide"
>
<div class="absolute pointer-events-auto w-full h-full" @click="handleGuideClick">
<img v-if="guideIndex === 0" :src="getStaticImage('film/on-site/1.png')" alt="" />
<img v-if="guideIndex === 1" :src="getStaticImage('film/on-site/2.png')" alt="" />
</div>
</div>
<!-- 菜单 -->
<FilmMenu :current-step="currentStep" />
@@ -116,6 +167,7 @@ onMounted(() => {
<ProgressBar
module-name="film"
:current-step="currentStep"
:completed-step="2"
@step-change="handleStepChange"
/>
</div>
@@ -131,8 +183,8 @@ onMounted(() => {
</div>
<GuideSteps
v-if="status"
highlight-selector=""
:visible="status && currentStep === 0"
highlight-selector="1"
highlight-mode="gradient"
position-offset-y="85%"
@complete="() => {}"
@@ -11,15 +11,32 @@ const back = () => {
// 跳转设备准备
router.back()
}
const steps = [
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' },
{ id: 'step2', content: '这是主要内容区域的边框' }
]
const stepIndex = ref(0)
const steps = ref([
{
id: 'step1',
content:
'拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。接下来,我们就一起看看在这个模块中会用到哪些常见、实用的拍摄工具。'
},
{
id: 'step2',
content: '点击右滑按钮,查看下一张拍摄工具',
highlightSelector: '.equipment-rightbutton'
},
{
id: 'step3',
content: '滚动鼠标滚轮,查看拍摄工具详细介绍',
highlightSelector: '.plate'
}
])
const stepIndex = ref<number>(0)
const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
const currentHighlight = computed(() => {
return steps.value[stepIndex.value ?? 0]?.highlightSelector
})
// 菜单项数据
const menuItems = ref([
{ id: 'phone', name: '相机/摄像机/微单', disabled: false },
@@ -74,18 +91,9 @@ const prevPhone = () => {
}
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_: any, index: number) => {
stepIndex.value = index
}
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
</script>
<template>
<div class="equipment-index">
@@ -183,18 +191,14 @@ const currentHighlight = computed(() =>
<!-- 引导步骤 -->
<GuideSteps
:visible="true"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
@step-change="handleStepChange"
position-offset-y="85%"
>
<GuideStepsItem
>拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。
接下来,我们就一起看看在这个模块中会用到哪些常见、实用的拍摄工具。
</GuideStepsItem>
</GuideSteps>
/>
</div>
</template>
<style lang="less" scoped>
@@ -1,11 +1,16 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const film = useFilmStore()
const router = useRouter()
const handleClose = () => {
film.setFilmUnlockProgress(3)
router.back()
}
@@ -39,11 +44,46 @@ const goRandom = () => {
<!-- 背景 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
:style="{ backgroundImage: getBackgroundImage('film/on-site/现场拍摄') }"
></div>
<!-- 标题 -->
<img class="absolute left-0 top-0" :src="getStaticImage('film/on-site/标题.png')" alt="" />
<div
class="absolute left-[100px] top-[210px] w-[446px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/延时拍摄'" @click="goTimeLapse" />
</div>
<div
class="absolute left-[640px] top-[240px] w-[541px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/滑轨拍摄'" @click="goSlider" />
</div>
<div
class="absolute right-[0px] top-[220px] w-[541px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/慢门拍摄-白天'" @click="goSlowShutterDay" />
</div>
<div
class="absolute right-[0px] bottom-[160px] w-[541px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/慢门拍摄-夜间.png'" @click="goSlowShutterNight" />
</div>
<div
class="absolute left-[100px] bottom-[80px] w-[446px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/低角度拍摄'" @click="goLowAngle" />
</div>
<div
class="absolute left-[700px] bottom-[40px] w-[446px] h-[273px] bg-cover bg-center bg-no-repeat"
>
<ImgButton :image="'film/on-site/创作随机性'" @click="goRandom" />
</div>
<!-- 内容卡片 + 按钮 -->
<div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<!-- <div class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none">
<div
class="max-w-screen-2xl w-full bg-black/35 rounded-3xl border border-white/15 p-16 space-y-10 text-white pointer-events-auto"
>
@@ -87,7 +127,7 @@ const goRandom = () => {
</button>
</div>
</div>
</div>
</div> -->
<!-- 关闭 -->
<ImgButton
@@ -96,6 +136,10 @@ const goRandom = () => {
@click="handleClose"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
</GuideSteps>
</template>
<style scoped>
@@ -158,7 +158,7 @@ const capturePhoto = async () => {
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage('film/shooting/龙美术馆模糊底图.png') }"
>
<ImgButton image="shooting/标题-取景地a.png" class="title-button absolute left-0" />
<ImgButton image="film/shooting/标题-龙美术馆.png" class="title-button absolute left-0" />
<ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
<p
@@ -130,7 +130,7 @@ const handleComplete = () => {
}"
>
<!-- 标题 -->
<ImgButton image="shooting/标题-取景地b.png" class="title-button absolute left-0" />
<ImgButton image="film/shooting/标题-油罐艺术公园.png" class="title-button absolute left-0" />
<!-- 返回按钮 -->
<ImgButton
@@ -153,7 +153,7 @@ const handleComplete = () => {
<!-- 隐藏的背景图片,用于 Canvas 绘制 -->
<img
ref="backgroundImage"
class="absolute inset-0 w-full h-full object-cover pointer-events-none opacity-0"
class="absolute inset-0 w-full h-full object-cover pointer-events-none"
:src="getStaticImage('film/shooting/油罐艺术公园清晰底图.png')"
alt=""
crossorigin="anonymous"
@@ -4,10 +4,14 @@ import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Overlay } from '@/components/Overlay'
import { ImgButton } from '@/components/Button'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import shootingtitle from '@/assets/images/shooting/标题-实地取景.png'
import videoSrc from '/video/xinshou_jiuzhonggoutufa.mp4'
import videoSrc from '/video/new/xsdh_jzgtf_batch.mp4'
import KnowledgeCard from '@/components/KnowledgeCard/FilmKnowledgeCard.vue'
import { Video } from '@/components'
import { useFilmStore } from '@/stores'
const film = useFilmStore()
type Btn = {
key: string
@@ -19,6 +23,58 @@ type Btn = {
const router = useRouter()
const steps = ref([
{
id: '1',
content: '哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~',
highlightSelector: '.btn-1'
},
{
id: '2',
content: '跟随箭头指引,一键解锁取景地',
highlightSelector: '.btn-2'
},
{
id: '3',
content: '跟随箭头指引,一键解锁取景地',
highlightSelector: '.btn-3'
}
])
const currentIdx = ref<number>(0)
const handleStepChange = (_: any, index: number) => {
currentIdx.value = index
}
const currentHighlight = computed(() => {
return steps.value[currentIdx.value]?.highlightSelector
})
const guideSteps = ref([
{
id: '1',
content: '点击框架构图,查看构图方法',
highlightSelector: '.btn-item-1'
},
{
id: '2',
content: '点击对称构图,查看构图方法',
highlightSelector: '.btn-item-2'
},
{
id: '3',
content: '点击引导线构图,查看构图方法',
highlightSelector: '.btn-item-3'
}
])
const currentIndex = ref<number>(0)
const handleGuideStepChange = (_: any, index: number) => {
currentIndex.value = index
}
const currentHighlightSelector = computed(() => {
return guideSteps.value[currentIndex.value ?? 0]?.highlightSelector
})
const showModal = ref(false)
const showModal2 = ref(false)
@@ -34,24 +90,24 @@ const currentItem = ref<Btn>({
const btnList = ref<Btn[]>([
{
key: '1',
name: '引导线构图',
name: '框架构图',
btnImage: 'film/shooting/框架构图',
desc: '引导线构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用',
images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图']
desc: '框架构图是拍摄时用“框架”把主体框起来,可以让画面更加有空间感及层次感,并且使用也并不复杂,比如镜子、门窗、山洞等,也可以说是光影框等。',
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图']
},
{
key: '2',
name: '对称构图',
btnImage: 'film/shooting/对称构图',
desc: '对称构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调',
desc: '对称构图是通过画面中轴线(水平 / 垂直)两侧元素镜像分布的构图方式,属于平衡类构图。它借对称的视觉均衡感,让画面显规整、稳定,同时强化主体秩序与美感。拍摄可找天然对称景物(如建筑、倒影),或调整角度让元素对称;略有不对称可后期微调,适合展现建筑、对称静物等规整场景。',
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图']
},
{
key: '3',
name: '框架构图',
name: '引导线构图',
btnImage: 'film/shooting/引导线构图',
desc: '框架构图属于引导线构图的一种,所以也是借助引导线的视觉引导效果来突出强调主体。另外对角线构图在画面中制造出对角线的走向即可。',
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图']
desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)',
images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图']
}
])
@@ -78,6 +134,7 @@ const handleClickB = () => {
router.push({ name: 'FilmFilmingLocationB' })
}
const back = () => {
film.setFilmUnlockProgress(3)
// 跳转设备准备
router.back()
}
@@ -87,7 +144,7 @@ const back = () => {
<!-- 背景图片 -->
<div
class="absolute inset-0 bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
:style="{ backgroundImage: getBackgroundImage('film/shooting/底图') }"
></div>
<div class="shooting-title">
@@ -97,24 +154,25 @@ const back = () => {
<ImgButton image="shooting/返回.png" @click="back" />
</div>
<div class="absolute left-[12%] top-1/2 -translate-y-[calc(50%-70px)]">
<ImgButton 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 image="film/shooting/油罐艺术公园入口.png" @click="handleClickB" />
<ImgButton class="btn-3" image="film/shooting/油罐艺术公园入口.png" @click="handleClickB" />
</div>
<div class="shooting-composition">
<div class="shooting-composition btn-1">
<ImgButton image="shooting/构图指南.png" @click="showModal = true" />
</div>
<!-- 引导步骤 -->
<GuideSteps
:highlight-selector="'.mybutton'"
:visible="true"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12"
:highlight-radius="20"
position-offset-y="85%"
>
<GuideStepsItem> 哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~ </GuideStepsItem>
</GuideSteps>
position-offset-y="20%"
@step-change="handleStepChange"
/>
<!-- 保存图片弹窗 -->
<Overlay
@@ -137,21 +195,38 @@ const back = () => {
<ImgButton class="absolute left-0 top-0 z-10" image="shooting/标题-构图指南" />
<!-- 新手导航视频 -->
<ImgButton
class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2"
image="shooting/新手导航-视频回顾"
@click="showModal3 = true"
/>
<div class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2">
<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="" />
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
</div>
</div>
<div class="absolute top-[30%] right-10">
<ImgButton
v-for="item in btnList"
:key="item.key"
:class="`btn-item-${item.key}`"
:image="item.btnImage"
@click="handleClick(item)"
/>
</div>
<GuideSteps
:visible="true"
:steps="guideSteps"
:keyboard-navigation="true"
:highlight-selector="currentHighlightSelector"
:highlight-padding="12"
:highlight-radius="16"
position-offset-y="85%"
@step-change="handleGuideStepChange"
@complete="currentIndex = 0"
/>
<!-- 知识卡片 -->
<KnowledgeCard
v-if="showModal2 && currentItem.key"
@@ -164,7 +239,7 @@ const back = () => {
/>
<!-- 视频 -->
<Overlay :visible="showModal3" :clickable="false">
<video :src="videoSrc" controls></video>
<Video :src="videoSrc" controls :volume="1" :muted="false"></Video>
<!-- 返回 -->
<ImgButton
class="absolute right-0 top-0 z-10"
+22 -2
View File
@@ -5,6 +5,7 @@ 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 { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
@@ -32,6 +33,19 @@ const showSittingPerson = computed<boolean>(() => {
return currentPage.value === 0 && !isScrollingBackToFirst.value
})
const visible = ref<boolean>(false)
//每一页的引导词
const steps = ref([
'欢迎来到实战操作台! 这里的内容很丰富,滚动鼠标就能浏览到全部内容啦~',
'点击【我的创作笔记】,编写创作思路~',
'点击【查看】按钮,欣赏他人实战作品~'
])
const contentSteps = computed(() => {
return steps.value[currentPage.value]
})
/** 他人实战作品列表 */
interface WorkItem {
id: number
@@ -45,8 +59,8 @@ interface WorkItem {
const worksList = ref<WorkItem[]>([
{
id: 1,
image: 'https://picsum.photos/400/300?random=1',
title: '作品名称',
image: getStaticImage('film/practice/学生案例'),
title: '学生案例',
stars: 4,
path: '/video/practice/film_anli1.mp4',
description:
@@ -101,6 +115,7 @@ const smoothScrollTo = (toPage: number): void => {
} else {
isAnimating.value = false
currentPage.value = toPage
visible.value = true
}
}
@@ -143,6 +158,7 @@ const handleWheel = (event: WheelEvent): void => {
}
onMounted(() => {
visible.value = true
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
})
@@ -307,6 +323,10 @@ onUnmounted(() => {
/>
</Transition>
</div>
<GuideSteps v-if="visible" position-offset-y="70%" @complete="visible = false">
<GuideStepsItem>{{ contentSteps }}</GuideStepsItem>
</GuideSteps>
</div>
</template>
+7 -4
View File
@@ -3,10 +3,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 { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay'
import { ref } from 'vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
const props = defineProps({
@@ -71,12 +71,12 @@ const handleBack = (): void => {
@click="handleBack"
/>
<ImgButton
class="absolute right-0 bottom-10 z-50"
class="absolute right-0 bottom-10 z-50 btn-1"
image="vlog/practice/评分"
@click="showModal = true"
/>
<Video
:src="decodeURIComponent(props.path)"
:src="getPublicPath(decodeURIComponent(props.path))"
:volume="1"
:muted="false"
:controls="true"
@@ -125,5 +125,8 @@ const handleBack = (): void => {
</div>
</div>
</Overlay>
<GuideSteps :visible="true" :highlight-selector="'.btn-1'" position-offset-y="60%">
<GuideStepsItem>点击这里可以给作品评分噢~</GuideStepsItem>
</GuideSteps>
</div>
</template>
+6
View File
@@ -2,6 +2,8 @@
import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { ref } from 'vue'
const router = useRouter()
@@ -27,5 +29,9 @@ const back = () => {
<div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('film/img/资源筹备.png')" alt="" />
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+4
View File
@@ -5,6 +5,7 @@ import { Video } from '@/components'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter()
@@ -56,5 +57,8 @@ const handleOpenImages = () => {
@click="handleOpenImages"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+2 -7
View File
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back()
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
+8
View File
@@ -4,12 +4,17 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const { scale, offsetX, offsetY } = useScale()
const router = useRouter()
const film = useFilmStore()
const back = () => {
film.setFilmUnlockProgress(2)
router.back()
}
@@ -45,5 +50,8 @@ const handleOpenImages = () => {
@click="handleOpenImages"
/>
</div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div>
</template>
+50 -37
View File
@@ -5,11 +5,6 @@ import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import second from '/video/vlog_renwuqujing_jichujingbieyunyong.mp4'
import third from '/video/vlog_renwuqujing_huamiangoutu.mp4'
import fourth from '/video/vlog_renwuqujing_jiaobencehua.mp4'
import five from '/video/vlog_renwuqujing_zhuapai.mp4'
import six from '/video/vlog_renwuqujing_jianjipingjie.mp4'
import { useFilmStore } from '@/stores'
const router = useRouter()
@@ -21,6 +16,9 @@ const visibleGuide = ref(true)
const visibleGuide2 = ref(true)
// 引导步骤索引
const guideStepIndex = ref(0)
// 受控播放开关(用于切换到下一个视频时自动播放)
const playing = ref(false)
const status = ref(false)
@@ -39,53 +37,54 @@ const navigationSteps = ref([
{
id: 'step1',
label: '后期制作',
content: '遮幅',
type: 'post' as const
content: '遮幅'
},
{
id: 'step2',
label: '拍摄执行',
content: '光线',
type: 'post' as const
content: '光线'
},
{
id: 'step3',
label: '拍摄执行',
content: '虚实',
type: 'shooting' as const
content: '虚实'
},
{
id: 'step4',
label: '拍摄执行',
content: '特写镜头',
type: 'post' as const
content: '特写镜头'
},
{
id: 'step5',
label: '拍摄执行',
content: '分镜头',
type: 'post' as const
content: '分镜头'
},
{
id: 'step5',
label: '后期制作',
content: '滤镜',
type: 'post' as const
content: '滤镜'
},
{
id: 'step5',
label: '拍摄执行',
content: '同期声',
type: 'post' as const
content: '同期声'
}
])
// 步骤导航相关状态
const showStepNavigation = ref(false)
const currentNavigationStep = ref(1) // 对应视频索引,从0开始
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
// 视频列表
const videoList = [second, third, fourth, five, six, five, six]
const videoList = [
getPublicPath('video/film/dygmk_zf_batch.mp4'),
getPublicPath('video/film/dygmk_gx_batch.mp4'),
getPublicPath('video/film/dygmk_xs_batch.mp4'),
getPublicPath('video/film/dygmk_txjt_batch.mp4'),
getPublicPath('video/film/dygmk_fjt_batch.mp4'),
getPublicPath('video/film/dygmk_lj_batch.mp4'),
getPublicPath('video/film/dygmk_tqs_batch.mp4')
]
const currentVideoIndex = ref(0)
// 题目数据:虚实(index 2)和分镜头(index 4)
@@ -98,8 +97,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
return [
{
id: 1,
title: '交互题 1',
description: '下面两个画面,哪一个更具电影叙事感?',
title: '',
description: '(单选题)下面两个画面,哪一个更具电影叙事感?',
options: [
{
id: 'a',
@@ -121,8 +120,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
return [
{
id: 2,
title: '交互题2',
description: '下面哪一组镜头顺序,更符合电影叙事逻辑?',
title: '',
description: '(单选题)下面哪一组镜头顺序,更符合电影叙事逻辑?',
options: [
{
id: 'a',
@@ -144,7 +143,7 @@ const getQuestionsForVideo = (index: number): Question[] => {
//按顺序播放
const currentVideo = computed(() => videoList[currentVideoIndex.value])
const resolvedSrc = computed(() => {
if (key.value === 'case') return getPublicPath('video/anlishangxi_meishi_xiaoyuanzi.mp4')
if (key.value === 'case') return getPublicPath('video/film/dygmk_zbbwg_batch.mp4')
if (key.value === 'profession') return currentVideo.value
return ''
})
@@ -169,6 +168,13 @@ const handleVideoChange = () => {
}
const handleClose = () => {
if (key.value === 'idea') {
film.setFilmUnlockProgress(1)
router.push({
name: 'Film'
})
return
}
router.back()
}
@@ -204,10 +210,10 @@ const handleComplete = () => {
name: 'FilmWork'
})
} else {
if (film.FilmUnlockProgress == 0) film.setFilmUnlockProgress(1)
router.push({
name: 'Film'
})
// if (film.FilmUnlockProgress == 0) film.setFilmUnlockProgress(1)
// router.push({
// name: 'Film'
// })
}
}
@@ -252,7 +258,7 @@ onMounted(() => {
<!-- 步骤导航按钮 -->
<button
v-if="key === 'profession'"
class="absolute right-4 top-1/2 z-10 -translate-y-1/2 w-20 h-20 bg-blue-500/80 hover:bg-blue-500 rounded-full flex items-center justify-center text-white transition-all duration-200"
class="step-navigation-btn absolute right-4 top-1/2 z-10 -translate-y-1/2 w-20 h-20 bg-blue-500/80 hover:bg-blue-500 rounded-full flex items-center justify-center text-white transition-all duration-200"
@click="showStepNavigation = true"
title="步骤导航"
>
@@ -297,7 +303,7 @@ onMounted(() => {
</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
@@ -311,9 +317,10 @@ onMounted(() => {
<!-- 引导与高亮 -->
<GuideSteps
highlight-selector=""
:highlight-selector="guideStepIndex === 2 ? '.step-navigation-btn' : ''"
v-if="visibleGuide && key === 'profession'"
position-offset-y="85%"
@step-change="(_: any, index: number) => (guideStepIndex = index)"
>
<GuideStepsItem>
从刚才的案例可以看出,电影感画面自带一种独特的艺术魅力——或大气恢弘,扑朔迷离或充满故事感。
@@ -345,13 +352,19 @@ onMounted(() => {
</GuideSteps>
<!-- 引导与高亮 -->
<GuideSteps highlight-selector="" v-if="visibleGuide2 && key === 'idea'" position-offset-y="85%">
<GuideSteps
highlight-selector=""
v-if="visibleGuide2 && key === 'idea'"
position-offset-y="85%"
@complete="handleComplete"
>
<GuideStepsItem>
一个好镜头,既要好看,又要好懂 ——
既有视觉吸引力,又能把信息清晰地传达出来。当这些条件被满足,一个镜头就有了“高级感”。
一个好镜头,既要好看,又要好懂 —— 既有视觉吸引力,又能把信息清晰地传达出来。
</GuideStepsItem>
<GuideStepsItem> 当这些条件被满足,一个镜头就有了“高级感”。 </GuideStepsItem>
<GuideStepsItem> 而优秀的短视频,就是靠无数这样的好镜头累积而成的。 </GuideStepsItem>
<GuideStepsItem>
而优秀的短视频,就是靠无数这样的好镜头累积而成的。接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧!
接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧!
</GuideStepsItem>
</GuideSteps>
</template>
+19 -13
View File
@@ -35,7 +35,7 @@ const sideSwitcherItems = ref([
key: 'profession',
name: '专业剖析',
description: '物件春秋-上海钟表文化科普馆',
image: 'film/案件赏析小屏'
image: 'film/专业剖析'
},
{
key: 'idea',
@@ -54,11 +54,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
)
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_: any, index: number) => {
stepIndex.value = index
}
@@ -84,7 +80,7 @@ const handleClose = () => {
const handleWorkClick = () => {
if (!isFilmUnlock.value) return
openImages([getStaticImage('work/桂花酒酿小圆子')])
openImages([getStaticImage('film/paishejiaoben')])
}
const handleGuideComplete = () => {
film.setIsGuideFilmTask(false)
@@ -127,6 +123,22 @@ function openImages(urls: string[], startIndex = 0) {
class="work-border absolute top-[140px] left-24 inset-0 bg-cover bg-center bg-no-repeat z-10 w-[1360px] h-[800px] p-10"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.BORDER) }"
>
<div class="relative w-full h-full">
<!-- 图片 -->
<img
@click="handleImageClick"
class="rounded-[62px] cursor-pointer"
:src="getStaticImage(currentItem?.image || '')"
alt=""
/>
<!-- 手 -->
<div
class="absolute left-[46%] top-[40%] -translate-x-1/2 -translate-y-1/2 z-10 animate-bounce pointer-events-none"
>
<img class="w-40" :src="getStaticImage('work/小手')" alt="" />
<span class="text-white text-2xl font-bold ml-5">点击进入</span>
</div>
</div>
<div
class="flex items-center pl-16 w-[1357px] h-[265px] absolute -bottom-16 left-0 bg-cover bg-center bg-no-repeat z-20"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.TITLE) }"
@@ -137,12 +149,6 @@ function openImages(urls: string[], startIndex = 0) {
{{ currentItem?.description }}</span
>
</div>
<img
@click="handleImageClick"
class="rounded-[62px] cursor-pointer"
:src="getStaticImage(currentItem?.image || '')"
alt=""
/>
<!-- <div class="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
<div
+61 -19
View File
@@ -7,13 +7,15 @@ import { ImgButton } from '@/components/Button'
import { ModuleSwitcher } from './components/ModuleSwitcher'
import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores'
import { useUserStore, useFilmStore, useVlogStore } from '@/stores'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
//点击电视机按钮引导状态
const status = ref(false)
//点击新手导航节点引导状态
const status2 = ref(true)
const status2 = ref(false)
const status3 = ref(false)
const stepIndex = ref(0)
const handleComplete = () => {
status.value = false
@@ -21,6 +23,8 @@ const handleComplete = () => {
const router = useRouter()
const user = useUserStore()
const film = useFilmStore()
const vlog = useVlogStore()
// 响应式状态
const guideShow = ref(false)
@@ -48,6 +52,7 @@ const preloadImages = () => {
// 事件处理函数
const handleOpenGuide = () => {
status2.value = true
guideShow.value = true
}
@@ -91,6 +96,27 @@ const handleSkill = () => {
})
}
// 清除所有与课程相关的缓存(Film / Vlog / User)
const handleClearCache = () => {
// 直接通过 $patch 重置各模块的状态,避免热更新导致的方法缺失问题
film.$patch({
isGuideFilmTask: true,
hasShownGuideFilmTask: false,
isFilmUnlock: false,
FilmUnlockProgress: 0
})
vlog.$patch({
isGuideVlogTask: true,
isVlogUnlock: false,
vlogUnlockProgress: 0
})
user.$patch({
isGuideNewUser: true
})
}
// 新手导航数据
const nodes = [
{
@@ -141,6 +167,7 @@ const handleNodeClick = (node: any, event?: MouseEvent) => {
showChildrenNodeId.value = null
} else {
showChildrenNodeId.value = node.id
status3.value = true
}
} else {
// 如果点击的是没有子节点的节点,直接跳转
@@ -175,12 +202,36 @@ onUnmounted(() => {
<template>
<div class="w-full h-full relative">
<!-- 图片引导 -->
<div
class="absolute top-0 left-0 w-full h-full z-50"
v-if="stepIndex < 4 && isGuideNewUser"
@click="stepIndex++"
>
<img v-if="stepIndex === 0" :src="getStaticImage('home/1')" alt="" />
<img v-else-if="stepIndex === 1" :src="getStaticImage('home/2')" alt="" />
<img v-else-if="stepIndex === 2" :src="getStaticImage('home/3')" alt="" />
<img v-else-if="stepIndex === 3" :src="getStaticImage('home/4')" alt="" />
<div v-else class="absolute top-0 left-0 w-full h-full bg-black/50"></div>
</div>
<!-- 背景图片 -->
<div
:class="['absolute inset-0 bg-cover bg-center bg-no-repeat']"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION_BG) }"
></div>
<div
class="absolute top-4 left-4 flex items-center text-white text-xl bg-black/10 px-4 py-2 rounded-xl"
>
!测试阶段友情提示:课程全部完成后,若需重新观看之前的内容,请按
<img
class="w-10 cursor-pointer mx-2"
src="@/assets/svg/clear.svg"
@click="handleClearCache"
/>清空缓存。
</div>
<!-- 选择模块 -->
<div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
@@ -240,7 +291,7 @@ onUnmounted(() => {
</div>
<!-- 节点图表 -->
<div class="absolute inset-0 top-[60px]">
<div class="absolute inset-0 top-[60px] btn-items">
<!-- 连线 (SVG) -->
<svg class="absolute inset-0 w-full h-full pointer-events-none">
<!-- 左侧装饰线 -> 节点1 -->
@@ -323,7 +374,7 @@ onUnmounted(() => {
<!-- 子节点图标(当点击三大基础要素时显示) -->
<div
v-if="node.hasChildren && showChildrenNodeId === node.id"
class="absolute -top-40 left-1/2 transform -translate-x-1/2 flex z-20"
class="absolute -top-40 left-1/2 transform -translate-x-1/2 flex z-20 btn"
>
<div
v-for="childNode in node.children"
@@ -382,20 +433,11 @@ onUnmounted(() => {
</GuideStepsItem>
</GuideSteps>
<GuideSteps
v-if="status2"
highlight-mode="gradient"
highlight-selector=".newUserGuide"
position-offset-y="50%"
@complete="handleComplete"
>
<GuideStepsItem>
<div class="flex items-center justify-start">
欢迎来到《短视频达人养成记》课程,请点击下方
<img class="w-20" :src="getStaticImage('home/action/top')" alt="" />
开始学习!
</div>
</GuideStepsItem>
<GuideSteps :visible="status2" highlight-selector=".btn-items" position-offset-y="20%">
<GuideStepsItem> 选择任意节点开始学习 </GuideStepsItem>
</GuideSteps>
<GuideSteps :visible="status3" highlight-selector=".btn" position-offset-y="20%">
<GuideStepsItem> 选择任意要素开始学习 </GuideStepsItem>
</GuideSteps>
</div>
</template>
@@ -7,7 +7,10 @@
{ selected: isSelected }
]"
>
<div :class="['btn-mask', module.maskConfig.className]" @click="$emit('click')"></div>
<div
:class="['btn-mask', `btn-mask-${module.type}`, module.maskConfig.className]"
@click="$emit('click')"
></div>
</div>
</template>
+306 -6
View File
@@ -1,6 +1,256 @@
<script lang="ts" setup>
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { GuideSteps, ImagePreview, Overlay } from '@/components'
import { computed, ref, h, nextTick } from 'vue'
import MindMap from 'simple-mind-map'
// XMind 解析工具(用于将 .xmind 文件解析为思维导图数据)
// @ts-expect-error: 官方包未提供类型声明
import xmindParser from 'simple-mind-map/src/parse/xmind.js'
import { useScale } from '@/composables/useScale'
// 第一个步骤的内容组件 - 使用函数组件形式
const Step1Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
'点击',
h('img', {
class: 'w-20',
src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
alt: ''
}),
' 查看课程结构,快速了解每个模块的学习内容。'
]
)
const Step2Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
h('img', {
class: 'w-20 inline',
src: getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE),
alt: ''
}),
'包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)'
]
)
const Step3Content = () =>
h(
'div',
{
class: 'flex items-center justify-start'
},
[
'点击',
h('img', {
class: 'w-20',
src: getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING),
alt: ''
}),
' 正式进入课程,开启你的短视频拍摄之旅。'
]
)
// const Step3Content = () =>
// h(
// 'div',
// {
// class: 'flex items-center justify-start'
// },
// [
// '点击',
// h('img', {
// class: 'w-20',
// src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
// alt: ''
// }),
// ' 查看课程结构,快速了解每个模块的学习内容。'
// ]
// )
const guideSteps = ref([
{
id: 'step-1',
title: '',
content: Step1Content,
highlightSelector: '.kecheng-wrapper'
},
{
id: 'step-2',
title: '',
content: Step2Content,
highlightSelector: '.zhiying-wrapper'
},
{
id: 'step-3',
title: '',
content: Step3Content,
highlightSelector: '.start-filming-wrapper'
}
])
const showGuide = ref(true)
const currentGuideStep = ref(0)
// 当前步骤的高亮选择器
const currentHighlightSelector = computed(() => {
if (showGuide.value && guideSteps.value[currentGuideStep.value]) {
return guideSteps.value[currentGuideStep.value].highlightSelector || null
}
return null
})
const handleGuideStepChange = (step: any, index: number) => {
currentGuideStep.value = index
}
const handleComplete = () => {
console.log('handleComplete')
}
// 课程框架思维导图弹层
const showMindMap = ref(false)
const mindMapContainer = ref<HTMLElement | null>(null)
let mindMapInstance: any = null
// 缓存解析后的思维导图数据,避免重复请求
let cachedMindMapData: any = null
const initMindMap = async () => {
if (!mindMapContainer.value) return
// 等待容器完全渲染并获取实际尺寸
await new Promise(resolve => setTimeout(resolve, 100))
console.log('容器尺寸:', {
width: mindMapContainer.value.clientWidth,
height: mindMapContainer.value.clientHeight
})
// 确保容器有尺寸
if (mindMapContainer.value.clientWidth === 0 || mindMapContainer.value.clientHeight === 0) {
console.error('容器尺寸为 0,等待渲染')
await new Promise(resolve => setTimeout(resolve, 200))
}
// 初始化思维导图实例(每次都重新创建,避免 DOM 销毁问题)
mindMapInstance = new MindMap({
el: mindMapContainer.value,
data: {
data: { text: '加载中...' },
children: []
}
})
// 创建后立即调用 resize
if (typeof mindMapInstance.resize === 'function') {
mindMapInstance.resize()
}
try {
// 如果有缓存数据,直接使用
if (cachedMindMapData) {
console.log('使用缓存数据')
mindMapInstance.setData(cachedMindMapData)
} else {
// 首次加载,从服务器获取文件
console.log('首次加载,获取 XMind 文件')
const res = await fetch(getPublicPath('maps/dsp_kecheng.xmind'))
if (!res.ok) {
throw new Error(`加载 XMind 失败: ${res.status}`)
}
const blob = await res.blob()
// 将远程文件转换成 File 对象,供解析器使用
const file = new File([blob], 'dsp_kecheng.xmind', {
type: 'application/vnd.xmind.workbook'
})
// 使用 simple-mind-map 提供的解析工具解析 xmind 文件
const data = await (xmindParser as any).parseXmindFile(file)
// 缓存数据
cachedMindMapData = data
// 将解析后的数据设置到思维导图实例
mindMapInstance.setData(data)
}
// 数据加载后再次调用 resize
if (typeof mindMapInstance.resize === 'function') {
mindMapInstance.resize()
}
// 等待渲染完成后调用视图居中
await new Promise(resolve => setTimeout(resolve, 100))
centerMindMapView()
} catch (error) {
console.error('加载 XMind 文件失败:', error)
}
}
// 居中并缩放视图的辅助函数
const centerMindMapView = () => {
if (!mindMapInstance) return
const anyInstance = mindMapInstance as any
console.log('尝试居中视图')
// 尝试多种方法来居中视图
if (anyInstance.view) {
// 方法1: fit - 自动缩放并居中
if (typeof anyInstance.view.fit === 'function') {
console.log('调用 view.fit()')
anyInstance.view.fit()
}
// 方法2: setScale + translateX/Y 来手动居中
else if (typeof anyInstance.view.setScale === 'function') {
console.log('手动设置缩放和位置')
anyInstance.view.setScale(1)
anyInstance.view.translateX(0)
anyInstance.view.translateY(0)
}
// 方法3: fitCenter
else if (typeof anyInstance.view.fitCenter === 'function') {
console.log('调用 view.fitCenter()')
anyInstance.view.fitCenter()
}
}
}
const { scale, offsetX, offsetY } = useScale()
const openMindMap = () => {
ImagePreview({
images: [getStaticImage('maps/dt')],
maskClosable: true,
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
})
// showMindMap.value = true
// nextTick(() => {
// void initMindMap()
// })
}
const handleMindMapClose = () => {
showMindMap.value = false
// 销毁思维导图实例,避免 DOM 销毁后引用失效
if (mindMapInstance && typeof mindMapInstance.destroy === 'function') {
mindMapInstance.destroy()
}
mindMapInstance = null
// 保留缓存数据,下次打开时直接使用,不需要重新请求
}
</script>
<template>
@@ -12,16 +262,66 @@ import { ImgButton } from '@/components/Button'
></div>
<!-- 按钮 -->
<!-- 开始拍摄 -->
<div class="absolute right-[320px] bottom-[100px]">
<ImgButton :image="IMAGE_PATHS.BUTTONS.START_FILMING" :path="{ name: 'Action' }" />
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton image="home/action-btn" :path="{ name: 'Action' }" />
</div>
<!-- 课程框架 -->
<div class="absolute right-20 bottom-36">
<ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" />
<div class="absolute right-20 bottom-36 kecheng-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" @click="openMindMap" />
</div>
<!-- 操作指引 -->
<div class="absolute right-20 bottom-10">
<div class="absolute right-20 bottom-10 zhiying-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.OPERATE_GUIDE" />
</div>
<GuideSteps
:visible="showGuide"
:steps="guideSteps"
:highlight-selector="currentHighlightSelector"
:highlight-padding="12"
:highlight-radius="16"
:keyboard-navigation="true"
highlight-mode="border"
position-offset-y="40%"
@complete="handleComplete"
@step-change="handleGuideStepChange"
/>
<!-- 课程框架思维导图弹层 -->
<!-- <Overlay :visible="showMindMap" @close="handleMindMapClose">
<div class="bg-white rounded-xl shadow-xl w-[100vw] h-[100vh] flex flex-col overflow-hidden">
<div class="flex items-center justify-between px-4 py-3 border-b flex-shrink-0">
<div class="font-semibold text-lg">短视频达人课程框架</div>
<button
class="text-gray-500 hover:text-gray-700 text-2xl leading-none px-2"
type="button"
@click="handleMindMapClose"
>
×
</button>
</div>
<div class="flex-1 overflow-hidden">
<div ref="mindMapContainer" class="mindmap-container"></div>
</div>
</div>
</Overlay> -->
</div>
</template>
<style scoped>
.mindmap-container {
width: 100%;
height: 100%;
position: relative;
}
/* 确保 simple-mind-map 生成的 SVG 填充整个容器 */
.mindmap-container :deep(svg) {
width: 100% !important;
height: 100% !important;
}
.mindmap-container :deep(.smm-container) {
width: 100% !important;
height: 100% !important;
}
</style>
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -71,12 +71,7 @@ const prevPhone = () => {
}
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back()
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
+2 -6
View File
@@ -4,7 +4,7 @@ import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps } from '@/components/Guide'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
@@ -66,11 +66,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
)
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
+1 -1
View File
@@ -16,7 +16,7 @@ const handleResourceClick = (resource: ResourceItem) => {
<template>
<div
class="w-full h-full overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/30 scrollbar-track-transparent"
class="w-full h-full overflow-y-scroll pr-2 scrollbar-thin scrollbar-thumb-white/30 scrollbar-track-transparent"
>
<div
v-if="props.resources.length === 0"
+9 -3
View File
@@ -338,13 +338,19 @@ const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
style="
background: radial-gradient(
circle at center,
rgba(0, 0, 0, 0.5) 0%,
rgba(0, 0, 0, 0.4) 20%,
rgba(0, 0, 0, 0.3) 40%,
rgba(46, 141, 218, 0.8) 0%,
rgba(46, 141, 218, 0.4) 30%,
rgba(46, 141, 218, 0.3) 60%,
transparent 70%
);
"
></div>
<!-- <img
:src="getStaticImage('skill/树蒙版.png')"
alt=""
class="absolute w-full h-full inset-0 z-20"
v-if="props.selectedTagId"
/> -->
</transition>
<!-- 右侧标签 -->
+6 -3
View File
@@ -1,5 +1,8 @@
<script lang="ts" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
defineOptions({
name: 'Skill'
})
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button'
import { ImagePreview } from '@/components'
import { useRouter } from 'vue-router'
@@ -301,7 +304,7 @@ const handleClose = () => {
const handleResourceClick = (resource: ResourceItem) => {
if (resource.type === 'video' && resource.videoUrl) {
// 视频类型:打开视频播放弹窗
currentVideoUrl.value = resource.videoUrl
currentVideoUrl.value = getPublicPath(resource.videoUrl) as string
showVideoModal.value = true
} else if (resource.type === 'page' && resource.routeName) {
// 页面类型:跳转到对应路由
@@ -309,7 +312,7 @@ const handleResourceClick = (resource: ResourceItem) => {
} else if (resource.type === 'image' && resource.images && resource.images.length > 0) {
// 图文类型:使用 ImagePreview 展示
ImagePreview({
images: resource.images,
images: resource.images.map(image => getStaticImage(image) as string),
startIndex: 0,
maskClosable: true,
getScaleMetrics: () => ({
+467 -270
View File
@@ -22,11 +22,10 @@ export interface ResourceItem {
// 一维数组格式的资源数据
export const resourceData: ResourceItem[] = [
// 新手导航模块 - 保持原有标签
// 新手导航模块 - 视觉素养 - 构图
{
id: 'res-1',
title: '九种构图法',
description: '学习九种基础构图技巧',
title: '如何选择构图',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
@@ -34,12 +33,24 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-1',
subTagName: '构图',
type: 'video',
videoUrl: '/video/构图法.mp4'
videoUrl: '/video/new/xsdh_rhxzgt_batch.mp4'
},
{
id: 'res-2',
title: '光线运用基础',
description: '掌握基础光线运用技巧',
title: '九种经典构图方式',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-1',
subTagName: '构图',
type: 'video',
videoUrl: '/video/new/xsdh_jzgtf_batch.mp4'
},
// 新手导航模块 - 视觉素养 - 光线
{
id: 'res-3',
title: '光的基础认知',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
@@ -47,12 +58,36 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-2',
subTagName: '光线',
type: 'video',
videoUrl: '/video/光线运用.mp4'
videoUrl: '/video/new/xinshou_guangdejichurenzhi.mp4'
},
{
id: 'res-3',
title: '运镜技巧入门',
description: '学习基础运镜方法',
id: 'res-4',
title: '光的四大属性',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-2',
subTagName: '光线',
type: 'video',
videoUrl: '/video/new/xinshou_guangdesidashuxing.mp4'
},
{
id: 'res-5',
title: '拍摄的黄金时间',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-2',
subTagName: '光线',
type: 'video',
videoUrl: '/video/new/xsdh_psdhjsj_batch.mp4'
},
// 新手导航模块 - 视觉素养 - 运镜
{
id: 'res-6',
title: '正确的手机拍摄持机姿势',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
@@ -60,344 +95,506 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-3',
subTagName: '运镜',
type: 'video',
videoUrl: '/video/运镜技巧.mp4'
videoUrl: '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
},
{
id: 'res-4',
title: '摄影基础教程',
description: '摄影基础知识讲解',
id: 'res-7',
title: '运镜的基本方式',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-3',
subTagName: '运镜',
type: 'video',
videoUrl: '/video/new/xinshou_yunjingdejibenfangshi.mp4'
},
// 新手导航模块 - 摄影基础
{
id: 'res-8',
title: '手机摄像头介绍',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'image',
images: ['/images/skill/摄影基础1.png', '/images/skill/摄影基础2.png']
},
{
id: 'res-5',
title: '剪映入门',
description: '剪映基础操作教程',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-3',
tagName: '剪映基础操作',
type: 'video',
videoUrl: '/video/剪映入门.mp4'
},
// vlog发现与记录模块 - 使用统一标签体系
{
id: 'res-6',
title: 'vlog设备准备',
description: 'vlog拍摄设备准备指南',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-1',
subTagName: '设备准备',
type: 'video',
videoUrl: '/video/vlog设备准备.mp4'
},
{
id: 'res-7',
title: 'vlog选题构思',
description: 'vlog选题与创意构思',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/vlog选题构思.mp4'
},
{
id: 'res-8',
title: 'vlog脚本撰写',
description: '如何撰写vlog脚本',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-3',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/vlog脚本撰写.mp4'
videoUrl: '/video/new/xinshou_shoujishexiangtoujieshao.mp4'
},
{
id: 'res-9',
title: 'vlog构图技巧',
description: 'vlog拍摄构图方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-1',
subTagName: '构图',
title: '景别概念介绍',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'video',
videoUrl: '/video/vlog构图技巧.mp4'
videoUrl: '/video/new/xsdh_jbgnjs_batch.mp4'
},
{
id: 'res-10',
title: 'vlog光线控制',
description: 'vlog拍摄光线运用',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-2',
subTagName: '光线运用',
title: '手机慢动作',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'video',
videoUrl: '/video/vlog光线控制.mp4'
videoUrl: '/video/new/xsdh_sjmdz_batch.mp4'
},
{
id: 'res-11',
title: 'vlog运镜方法',
description: 'vlog拍摄运镜技巧',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-3',
subTagName: '运镜技巧',
title: '延时摄影',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'video',
videoUrl: '/video/vlog运镜方法.mp4'
videoUrl: '/video/new/xsdh_yssy_batch.mp4'
},
{
id: 'res-12',
title: 'vlog剪辑技巧',
description: 'vlog视频剪辑方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-1',
subTagName: '剪辑技巧',
title: '神奇的微距摄影',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'video',
videoUrl: '/video/vlog剪辑技巧.mp4'
videoUrl: '/video/new/xinshou_shenqideweijusheying.mp4'
},
{
id: 'res-13',
title: 'vlog调色技巧',
description: 'vlog视频调色方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-2',
subTagName: '调色',
title: '独具魅力的夜景摄影',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'video',
videoUrl: '/video/vlog调色技巧.mp4'
videoUrl: '/video/new/xsdh_djmldyjsy_batch.mp4'
},
// 电影创作与表达模块
{
id: 'res-35',
title: '思维导图',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-5',
tagName: '思路提点与案例赏析',
type: 'image',
images: ['film/思路提点全屏.png']
},
{
id: 'res-14',
title: 'vlog音效处理',
description: 'vlog音效添加与处理',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-3',
subTagName: '音效',
type: 'video',
videoUrl: '/video/vlog音效处理.mp4'
},
{
id: 'res-15',
title: 'vlog版权规范',
description: 'vlog创作版权注意事项',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-4',
tagName: 'AI与版权伦理知识',
type: 'image',
images: [
'/images/skill/版权规范1.png',
'/images/skill/版权规范2.png',
'/images/skill/版权规范3.png'
]
},
{
id: 'res-16',
title: 'vlog案例赏析',
description: '优秀vlog作品分析',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/vlog案例赏析.mp4'
},
// 访谈聆听与对话模块 - 使用统一标签体系
{
id: 'res-17',
title: '访谈设备准备',
description: '访谈拍摄设备清单',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-1',
subTagName: '设备准备',
type: 'video',
videoUrl: '/video/访谈设备准备.mp4'
},
{
id: 'res-18',
title: '访谈选题方法',
description: '如何选择访谈主题',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/访谈选题方法.mp4'
},
{
id: 'res-19',
title: '访谈脚本制作',
description: '访谈脚本撰写技巧',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-3',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/访谈脚本制作.mp4'
},
{
id: 'res-20',
title: '访谈拍摄技巧',
description: '访谈视频拍摄方法',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-2',
tagName: '拍摄执行',
type: 'video',
videoUrl: '/video/访谈拍摄技巧.mp4'
},
{
id: 'res-21',
title: '访谈后期剪辑',
description: '访谈视频后期制作',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-3',
tagName: '后期制作',
type: 'video',
videoUrl: '/video/访谈后期剪辑.mp4'
},
{
id: 'res-22',
title: '访谈版权规范',
description: '访谈创作版权注意事项',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-4',
tagName: 'AI与版权伦理知识',
type: 'video',
videoUrl: '/video/访谈版权规范.mp4'
},
{
id: 'res-23',
title: '访谈案例赏析',
description: '优秀访谈作品分析',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/访谈案例赏析.mp4'
},
// 电影创作与表达模块 - 使用统一标签体系
{
id: 'res-24',
title: '电影设备准备',
description: '电影拍摄设备清单',
title: '拍摄前的信息整理',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-1',
subTagName: '设备准备',
subTagName: '选题与构思',
type: 'page',
routeName: 'FilmResource'
},
{
id: 'res-25',
title: '电影选题构思',
description: '电影创作选题方法',
id: 'res-15',
title: '徐汇滨江短视频构思过程',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-1',
subTagName: '选题与构思',
type: 'page',
routeName: 'FilmCreative'
},
{
id: 'res-16',
title: '脚本制作',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/电影选题构思.mp4'
subTagName: '脚本与分镜制作',
type: 'page',
routeName: 'FilmScriptWriting'
},
{
id: 'res-26',
title: '电影脚本分镜',
description: '电影脚本和分镜制作',
id: 'res-17',
title: '分镜制作',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-3',
subTagId: 'subtag-4-1-2',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/电影脚本分镜.mp4'
type: 'page',
routeName: 'FilmStoryboard'
},
// 电影创作与表达模块 - 拍摄执行
{
id: 'res-27',
title: '电影拍摄执行',
description: '电影拍摄执行技巧',
id: 'res-18',
title: '设备准备',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
type: 'video',
videoUrl: '/video/电影拍摄执行.mp4'
subTagId: 'subtag-4-2-1',
subTagName: '设备准备',
type: 'page',
routeName: 'FilmMidtermEquipment'
},
{
id: 'res-28',
title: '电影后期制作',
description: '电影后期剪辑与特效',
id: 'res-19',
title: '构图指南',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-2',
subTagName: '画面构图',
type: 'page',
routeName: 'FilmMidtermShooting'
},
{
id: 'res-20',
title: '实地取景',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-2',
subTagName: '画面构图',
type: 'page',
routeName: 'FilmMidtermShooting'
},
{
id: 'res-21',
title: '延时拍摄',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'page',
routeName: 'FilmOnsiteTimeLapse'
},
{
id: 'res-22',
title: '滑轨移动镜头',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-4',
subTagName: '运镜技巧',
type: 'video',
videoUrl: '/video/on-site/dygmk_hgydjt_batch.mp4'
},
{
id: 'res-23',
title: '慢门拍摄(白天)',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'page',
routeName: 'FilmOnsiteSlowShutterDay'
},
{
id: 'res-24',
title: '慢门拍摄(夜间)',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'video',
videoUrl: '/video/on-site/dygmk_mmps_batch.mp4'
},
{
id: 'res-25',
title: '低角度拍摄',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'page',
routeName: 'FilmOnsiteLowAngle'
},
{
id: 'res-26',
title: '创作随机性',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-5',
subTagName: '光线运用',
type: 'video',
videoUrl: '/video/on-site/dygmk_czsjx_batch.mp4'
},
// 电影创作与表达模块 - 后期制作
{
id: 'res-27',
title: '剪辑从哪里开始',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-1',
subTagName: '素材管理',
type: 'video',
videoUrl: '/video/电影后期制作.mp4'
videoUrl: '/video/on-site/dygmk_jjcnlks_batch.mp4'
},
{
id: 'res-28',
title: '节奏型剪辑',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-2',
subTagName: '剪辑技巧',
type: 'video',
videoUrl: '/video/on-site/dygmk_jzxjj_batch.mp4'
},
{
id: 'res-29',
title: '电影AI工具应用',
description: 'AI工具在电影创作中的应用',
title: '声音转场',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-4',
tagName: 'AI与版权伦理知识',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-2',
subTagName: '剪辑技巧',
type: 'video',
videoUrl: '/video/电影AI工具应用.mp4'
videoUrl: '/video/on-site/dygmk_kq_batch.mp4'
},
{
id: 'res-30',
title: '电影创作思路与案例',
description: '电影创作思路和案例分析',
title: '快切',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-3',
subTagName: '蒙太奇剪辑',
type: 'video',
videoUrl: '/video/on-site/dygmk_gljj_batch.mp4'
},
{
id: 'res-31',
title: '关联剪辑',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-3',
subTagName: '蒙太奇剪辑',
type: 'video',
videoUrl: '/video/on-site/dygmk_lxjj_batch.mp4'
},
{
id: 'res-32',
title: '连续(叙事)剪辑',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-3',
subTagName: '蒙太奇剪辑',
type: 'video',
videoUrl: '/video/on-site/dygmk_lxjj_batch.mp4'
},
{
id: 'res-33',
title: '调色',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-4',
subTagName: '调色',
type: 'page',
routeName: 'FilmColor'
},
{
id: 'res-34',
title: '音效',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-5',
subTagName: '音效',
type: 'page',
routeName: 'FilmSoundEffect'
},
// 电影创作与表达模块 - 新增资源
{
id: 'res-38',
title: '钟表博物馆视频',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/电影创作思路与案例.mp4'
videoUrl: '/video/film/dygmk_zbbwg_batch.mp4'
},
{
id: 'res-39',
title: '遮幅',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-6',
subTagName: '包装',
type: 'video',
videoUrl: '/video/film/dygmk_zf_batch.mp4'
},
{
id: 'res-40',
title: '光线',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-5',
subTagName: '光线运用',
type: 'video',
videoUrl: '/video/film/dygmk_gx_batch.mp4'
},
{
id: 'res-41',
title: '虚实',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'video',
videoUrl: '/video/film/dygmk_xs_batch.mp4'
},
{
id: 'res-42',
title: '特写镜头',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'video',
videoUrl: '/video/film/dygmk_txjt_batch.mp4'
},
{
id: 'res-43',
title: '分镜头',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-4',
subTagName: '运镜技巧',
type: 'video',
videoUrl: '/video/film/dygmk_fjt_batch.mp4'
},
{
id: 'res-44',
title: '滤镜',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
subTagId: 'subtag-4-3-4',
subTagName: '调色',
type: 'video',
videoUrl: '/video/film/dygmk_lj_batch.mp4'
},
{
id: 'res-45',
title: '同期声',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
subTagId: 'subtag-4-2-3',
subTagName: '拍摄技巧',
type: 'video',
videoUrl: '/video/film/dygmk_tqs_batch.mp4'
},
{
id: 'res-46',
title: '这里是徐汇滨江',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/on-site/dygmk_zlsxhbj.mp4'
},
{
id: 'res-47',
title: '电影感 案例一',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/practice/film_anli1.mp4'
},
// vlog发现与记录模块 - 思路提点与案例赏析
{
id: 'res-36',
title: '思维导图',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'image',
images: ['work/思路提点全屏.png']
},
{
id: 'res-37',
title: '美食小确幸:桂花酒酿小圆子',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/anlishangxi_meishi_xiaoyuanzi.mp4'
},
{
id: 'res-38',
title: 'Vlog 案例一',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/practice/vlog_anli1.mp4'
},
{
id: 'res-39',
title: 'Vlog 案例二',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/practice/vlog_anli2.mp4'
}
]
+477
View File
@@ -0,0 +1,477 @@
// 资源数据结构定义 - 一维数组格式
export interface ResourceItem {
id: string
title: string
description?: string
// 用于检索的属性
moduleId: string
moduleName: string
tagId: string
tagName: string
subTagId?: string
subTagName?: string
// 资源数量(用于技能树显示)
resourceCount?: number
// 资源类型和链接
type: 'video' | 'page' | 'image' // video: 视频播放, page: 页面跳转, image: 图文展示
videoUrl?: string // 视频URL(type为video时使用)
routeName?: string // 路由名称(type为page时使用)
images?: string[] // 图片URL数组(type为image时使用)
}
// 一维数组格式的资源数据
export const resourceData: ResourceItem[] = [
// 新手导航模块 - 保持原有标签
{
id: 'res-1',
title: '九种构图法',
description: '学习九种基础构图技巧',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-1',
subTagName: '构图',
type: 'video',
videoUrl: '/video/构图法.mp4'
},
{
id: 'res-2',
title: '光线运用基础',
description: '掌握基础光线运用技巧',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-2',
subTagName: '光线',
type: 'video',
videoUrl: '/video/光线运用.mp4'
},
{
id: 'res-3',
title: '运镜技巧入门',
description: '学习基础运镜方法',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-1',
tagName: '视觉素养',
subTagId: 'subtag-1-1-3',
subTagName: '运镜',
type: 'video',
videoUrl: '/video/运镜技巧.mp4'
},
{
id: 'res-4',
title: '摄影基础教程',
description: '摄影基础知识讲解',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-2',
tagName: '摄影基础',
type: 'image',
images: ['/images/skill/摄影基础1.png', '/images/skill/摄影基础2.png']
},
{
id: 'res-5',
title: '剪映入门',
description: '剪映基础操作教程',
moduleId: 'module-1',
moduleName: '新手导航',
tagId: 'tag-1-3',
tagName: '剪映基础操作',
type: 'video',
videoUrl: '/video/剪映入门.mp4'
},
// vlog发现与记录模块 - 使用统一标签体系
{
id: 'res-6',
title: 'vlog设备准备',
description: 'vlog拍摄设备准备指南',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-1',
subTagName: '设备准备',
type: 'video',
videoUrl: '/video/vlog设备准备.mp4'
},
{
id: 'res-7',
title: 'vlog选题构思',
description: 'vlog选题与创意构思',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/vlog选题构思.mp4'
},
{
id: 'res-8',
title: 'vlog脚本撰写',
description: '如何撰写vlog脚本',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-1',
tagName: '内容策划',
subTagId: 'subtag-2-1-3',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/vlog脚本撰写.mp4'
},
{
id: 'res-9',
title: 'vlog构图技巧',
description: 'vlog拍摄构图方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-1',
subTagName: '构图',
type: 'video',
videoUrl: '/video/vlog构图技巧.mp4'
},
{
id: 'res-10',
title: 'vlog光线控制',
description: 'vlog拍摄光线运用',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-2',
subTagName: '光线运用',
type: 'video',
videoUrl: '/video/vlog光线控制.mp4'
},
{
id: 'res-11',
title: 'vlog运镜方法',
description: 'vlog拍摄运镜技巧',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-2',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-3',
subTagName: '运镜技巧',
type: 'video',
videoUrl: '/video/vlog运镜方法.mp4'
},
{
id: 'res-12',
title: 'vlog剪辑技巧',
description: 'vlog视频剪辑方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-1',
subTagName: '剪辑技巧',
type: 'video',
videoUrl: '/video/vlog剪辑技巧.mp4'
},
{
id: 'res-13',
title: 'vlog调色技巧',
description: 'vlog视频调色方法',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-2',
subTagName: '调色',
type: 'video',
videoUrl: '/video/vlog调色技巧.mp4'
},
{
id: 'res-14',
title: 'vlog音效处理',
description: 'vlog音效添加与处理',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-3',
tagName: '后期制作',
subTagId: 'subtag-2-3-3',
subTagName: '音效',
type: 'video',
videoUrl: '/video/vlog音效处理.mp4'
},
{
id: 'res-15',
title: 'vlog版权规范',
description: 'vlog创作版权注意事项',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-4',
tagName: 'AI与版权伦理知识',
type: 'image',
images: [
'/images/skill/版权规范1.png',
'/images/skill/版权规范2.png',
'/images/skill/版权规范3.png'
]
},
{
id: 'res-16',
title: 'vlog案例赏析',
description: '优秀vlog作品分析',
moduleId: 'module-2',
moduleName: 'vlog发现与记录',
tagId: 'tag-2-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/vlog案例赏析.mp4'
},
// 访谈聆听与对话模块 - 使用统一标签体系
{
id: 'res-17',
title: '访谈设备准备',
description: '访谈拍摄设备清单',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-1',
subTagName: '设备准备',
type: 'video',
videoUrl: '/video/访谈设备准备.mp4'
},
{
id: 'res-18',
title: '访谈选题方法',
description: '如何选择访谈主题',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/访谈选题方法.mp4'
},
{
id: 'res-19',
title: '访谈脚本制作',
description: '访谈脚本撰写技巧',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-1',
tagName: '内容策划',
subTagId: 'subtag-3-1-3',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/访谈脚本制作.mp4'
},
{
id: 'res-20',
title: '访谈拍摄技巧',
description: '访谈视频拍摄方法',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-2',
tagName: '拍摄执行',
type: 'video',
videoUrl: '/video/访谈拍摄技巧.mp4'
},
{
id: 'res-21',
title: '访谈后期剪辑',
description: '访谈视频后期制作',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-3',
tagName: '后期制作',
type: 'video',
videoUrl: '/video/访谈后期剪辑.mp4'
},
{
id: 'res-22',
title: '访谈版权规范',
description: '访谈创作版权注意事项',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-4',
tagName: 'AI与版权伦理知识',
type: 'video',
videoUrl: '/video/访谈版权规范.mp4'
},
{
id: 'res-23',
title: '访谈案例赏析',
description: '优秀访谈作品分析',
moduleId: 'module-3',
moduleName: '访谈聆听与对话',
tagId: 'tag-3-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/访谈案例赏析.mp4'
},
// 电影创作与表达模块 - 使用统一标签体系
{
id: 'res-24',
title: '电影设备准备',
description: '电影拍摄设备清单',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-1',
subTagName: '设备准备',
type: 'page',
routeName: 'FilmResource'
},
{
id: 'res-25',
title: '电影选题构思',
description: '电影创作选题方法',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-2',
subTagName: '选题与构思',
type: 'video',
videoUrl: '/video/电影选题构思.mp4'
},
{
id: 'res-26',
title: '电影脚本分镜',
description: '电影脚本和分镜制作',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-1',
tagName: '内容策划',
subTagId: 'subtag-4-1-3',
subTagName: '脚本与分镜制作',
type: 'video',
videoUrl: '/video/电影脚本分镜.mp4'
},
{
id: 'res-27',
title: '电影拍摄执行',
description: '电影拍摄执行技巧',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-2',
tagName: '拍摄执行',
type: 'video',
videoUrl: '/video/电影拍摄执行.mp4'
},
{
id: 'res-28',
title: '电影后期制作',
description: '电影后期剪辑与特效',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-3',
tagName: '后期制作',
type: 'video',
videoUrl: '/video/电影后期制作.mp4'
},
{
id: 'res-29',
title: '电影AI工具应用',
description: 'AI工具在电影创作中的应用',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-4',
tagName: 'AI与版权伦理知识',
type: 'video',
videoUrl: '/video/电影AI工具应用.mp4'
},
{
id: 'res-30',
title: '电影创作思路与案例',
description: '电影创作思路和案例分析',
moduleId: 'module-4',
moduleName: '电影创作与表达',
tagId: 'tag-4-5',
tagName: '思路提点与案例赏析',
type: 'video',
videoUrl: '/video/电影创作思路与案例.mp4'
}
]
// 从一维数组获取所有资源
export function getAllResources(): ResourceItem[] {
return resourceData
}
// 根据模块ID获取资源
export function getResourcesByModule(moduleId: string): ResourceItem[] {
return resourceData.filter(resource => resource.moduleId === moduleId)
}
// 根据资源标签ID获取资源
export function getResourcesByTag(moduleId: string, tagId: string): ResourceItem[] {
return resourceData.filter(resource => resource.moduleId === moduleId && resource.tagId === tagId)
}
// 根据子标签ID获取资源
export function getResourcesBySubTag(
moduleId: string,
tagId: string,
subTagId: string
): ResourceItem[] {
return resourceData.filter(
resource =>
resource.moduleId === moduleId && resource.tagId === tagId && resource.subTagId === subTagId
)
}
// 获取所有模块
export function getAllModules(): Array<{ id: string; name: string }> {
const modules = new Map<string, string>()
resourceData.forEach(resource => {
if (!modules.has(resource.moduleId)) {
modules.set(resource.moduleId, resource.moduleName)
}
})
return Array.from(modules.entries()).map(([id, name]) => ({ id, name }))
}
// 获取模块下的所有标签
export function getTagsByModule(moduleId: string): Array<{ id: string; name: string }> {
const tags = new Map<string, string>()
resourceData
.filter(resource => resource.moduleId === moduleId)
.forEach(resource => {
if (!tags.has(resource.tagId)) {
tags.set(resource.tagId, resource.tagName)
}
})
return Array.from(tags.entries()).map(([id, name]) => ({ id, name }))
}
// 获取标签下的所有子标签
export function getSubTagsByTag(
moduleId: string,
tagId: string
): Array<{ id: string; name: string }> {
const subTags = new Map<string, string>()
resourceData
.filter(
resource => resource.moduleId === moduleId && resource.tagId === tagId && resource.subTagId
)
.forEach(resource => {
if (resource.subTagId && !subTags.has(resource.subTagId)) {
subTags.set(resource.subTagId, resource.subTagName || '')
}
})
return Array.from(subTags.entries()).map(([id, name]) => ({ id, name }))
}
// 获取标签的资源数量
export function getTagResourceCount(moduleId: string, tagId: string): number {
return resourceData.filter(resource => resource.moduleId === moduleId && resource.tagId === tagId)
.length
}
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button'
@@ -71,12 +71,7 @@ const prevPhone = () => {
}
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
+2 -2
View File
@@ -3,7 +3,7 @@ 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 { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay'
import { ref } from 'vue'
@@ -76,7 +76,7 @@ const handleBack = (): void => {
@click="showModal = true"
/>
<Video
:src="decodeURIComponent(props.path)"
:src="getPublicPath(decodeURIComponent(props.path))"
:volume="1"
:muted="false"
:controls="true"
+2 -7
View File
@@ -1,6 +1,6 @@
<script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { GuideSteps, GuideStepsItem, type GuideStep } from '@/components/Guide'
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back()
}
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}
+3 -3
View File
@@ -65,7 +65,7 @@ const navigationSteps = ref([
// 步骤导航相关状态
const showStepNavigation = ref(false)
const currentNavigationStep = ref(1) // 对应视频索引,从0开始
const currentNavigationStep = ref(0) // 对应视频索引,从0开始
// 视频列表
const videoList = [second, third, fourth, five, six]
@@ -239,7 +239,7 @@ onMounted(() => {
</div>
</template> -->
<template v-slot:overlay>
<div
<!-- <div
:class="[
'absolute left-4 top-4 z-10 pointer-events-auto w-[359px] h-[96px] bg-cover bg-center',
'flex justify-center items-center'
@@ -253,7 +253,7 @@ onMounted(() => {
>
{{ label }}
</span>
</div>
</div> -->
<!-- <ImgButton
class="absolute left-4 top-4 z-10 pointer-events-auto"
:image="image"
+2 -6
View File
@@ -4,7 +4,7 @@ import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps } from '@/components/Guide'
import { GuideSteps, type GuideStep } from '@/components/Guide'
import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components'
@@ -66,11 +66,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
)
interface SimpleStep {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
const handleStepChange = (_step: GuideStep | null, index: number) => {
stepIndex.value = index
}