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
View File
@@ -17,6 +17,8 @@
"less": "^4.4.1", "less": "^4.4.1",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"simple-mind-map": "0.14.0-fix.1",
"simple-mind-map-plugin-themes": "^1.0.1",
"vue": "^3.5.18", "vue": "^3.5.18",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
}, },
+1035 -7
View File
File diff suppressed because it is too large. Load diff
File renamed without changes.
Binary file not shown.
+1 -1
View File
@@ -2,7 +2,7 @@
<div class="app-container" :style="containerStyle"> <div class="app-container" :style="containerStyle">
<div class="app-content" :style="scaleStyle"> <div class="app-content" :style="scaleStyle">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
<KeepAlive :include="['Action']"> <KeepAlive :include="['Action', 'Skill']">
<component :is="Component" /> <component :is="Component" />
</KeepAlive> </KeepAlive>
</router-view> </router-view>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1768210850113" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1852" id="mx_n_1768210850114" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#d4237a" p-id="1853" data-spm-anchor-id="a313x.search_index.0.i1.47a23a81k3YVnR" class="selected"></path><path d="M320.36864 346.70592h403.74272v437.39136H320.36864V346.70592z m67.29728 67.2768V716.8H656.7936V413.98272H387.6864zM421.2736 481.28h67.29728v168.22272h-67.29728V481.28z m134.59456 0h67.29728v168.22272h-67.29728V481.28zM286.72 346.70592h471.04v67.2768H286.72V346.7264zM421.29408 245.76h201.89184v67.29728H421.2736V245.76z" fill="#FFFFFF" p-id="1854"></path></svg>

After

Width:  |  Height:  |  Size: 898 B

+10 -3
View File
@@ -37,7 +37,7 @@
</div> </div>
<!-- 进度指示器 --> <!-- 进度指示器 -->
<div class="progress-indicator"> <div class="progress-indicator flex items-center gap-6">
<div class="progress-dots"> <div class="progress-dots">
<span <span
v-for="i in totalSteps" v-for="i in totalSteps"
@@ -46,6 +46,12 @@
:class="{ active: i <= currentStep }" :class="{ active: i <= currentStep }"
></span> ></span>
</div> </div>
<!-- v-if="currentStep !== totalSteps" -->
<ImgButton
:image="IMAGE_PATHS.BUTTONS.CONTINUE"
class="continue-button"
@click="handleContinue"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -126,7 +132,7 @@ const positionStyle = computed(() => {
<style scoped> <style scoped>
.guide-dragon-container { .guide-dragon-container {
@apply w-full h-full relative overflow-hidden; @apply w-full h-full relative overflow-hidden;
pointer-events: none; pointer-events: auto;
z-index: 100002; /* 高于遮罩 */ z-index: 100002; /* 高于遮罩 */
} }
@@ -145,6 +151,7 @@ const positionStyle = computed(() => {
height: 279px; height: 279px;
padding: 75px 30px 30px 120px; padding: 75px 30px 30px 120px;
background-color: rgba(0, 0, 0, 0); /* 背景完全透明 */ background-color: rgba(0, 0, 0, 0); /* 背景完全透明 */
cursor: pointer;
} }
.guide-header { .guide-header {
@@ -192,7 +199,7 @@ const positionStyle = computed(() => {
} }
.progress-indicator { .progress-indicator {
@apply absolute bottom-6 right-10; @apply absolute bottom-6 right-7;
} }
.progress-dots { .progress-dots {
+47 -14
View File
@@ -37,7 +37,10 @@
:total-steps="totalSteps" :total-steps="totalSteps"
@continue="handleNext" @continue="handleNext"
> >
{{ currentStepContent }} <!-- 支持组件形式的 content -->
<component v-if="isCurrentStepComponent" :is="currentStepContentComponent" />
<!-- 支持字符串形式的 content -->
<div v-else v-html="currentStepContent"></div>
</GuideStepsItem> </GuideStepsItem>
</template> </template>
</GuideDragon> </GuideDragon>
@@ -48,6 +51,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, useSlots, onMounted, onUnmounted, reactive } from 'vue' import { ref, computed, useSlots, onMounted, onUnmounted, reactive } from 'vue'
import type { Component } from 'vue'
import SpotlightOverlay from './SpotlightOverlay.vue' import SpotlightOverlay from './SpotlightOverlay.vue'
import GuideDragon from './GuideDragon.vue' import GuideDragon from './GuideDragon.vue'
import GuideStepsItem from './GuideStepsItem.vue' import GuideStepsItem from './GuideStepsItem.vue'
@@ -55,7 +59,7 @@ import GuideStepsItem from './GuideStepsItem.vue'
interface GuideStep { interface GuideStep {
id: string id: string
title?: string title?: string
content: string content: string | Component
nextText?: string nextText?: string
} }
@@ -78,6 +82,8 @@ interface Props {
highlightMode?: 'border' | 'gradient' highlightMode?: 'border' | 'gradient'
/** 垂直偏移(仅Y轴),支持 number(px) 或 '10%'/'120px' */ /** 垂直偏移(仅Y轴),支持 number(px) 或 '10%'/'120px' */
positionOffsetY?: number | string positionOffsetY?: number | string
/** 是否可见,用于外部控制显示/隐藏 */
visible?: boolean
} }
interface Emits { interface Emits {
@@ -95,14 +101,21 @@ const props = withDefaults(defineProps<Props>(), {
highlightPadding: 8, highlightPadding: 8,
highlightRadius: 16, highlightRadius: 16,
highlightMode: 'border', highlightMode: 'border',
positionOffsetY: '50%' positionOffsetY: '50%',
visible: true
}) })
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
const slots = useSlots() const slots = useSlots()
const show = ref(true) const internalShow = ref(true)
const currentStepIndex = ref(0) const currentStepIndex = ref(0)
// 合并外部 visible 和内部 show 状态
// 只有当外部 visible 为 true 且内部 internalShow 也为 true 时才显示
const show = computed(() => {
return props.visible && internalShow.value
})
const alignStyle = reactive<{ const alignStyle = reactive<{
left: string left: string
top: string top: string
@@ -116,7 +129,12 @@ const alignStyle = reactive<{
width: '1920px', width: '1920px',
height: '1080px' height: '1080px'
}) })
const highlightSelector = computed(() => props.highlightSelector || null) // 验证并清理选择器,空字符串或只包含空格的选择器视为无效
const highlightSelector = computed(() => {
if (!props.highlightSelector) return null
const trimmed = props.highlightSelector.trim()
return trimmed.length > 0 ? trimmed : null
})
const highlightPadding = computed(() => props.highlightPadding || 0) const highlightPadding = computed(() => props.highlightPadding || 0)
const highlightRadius = computed(() => props.highlightRadius || 0) const highlightRadius = computed(() => props.highlightRadius || 0)
@@ -148,20 +166,34 @@ const currentStepComponent = computed(() => {
return slotSteps.value[currentStepIndex.value] || null return slotSteps.value[currentStepIndex.value] || null
}) })
// 判断当前步骤的 content 是否为组件
const isCurrentStepComponent = computed(() => {
if (useSlotMode.value) return false
const step = currentStepData.value
if (!step) return false
return typeof step.content !== 'string'
})
// 当前步骤的内容(字符串形式)
const currentStepContent = computed(() => { const currentStepContent = computed(() => {
if (useSlotMode.value) return '' if (useSlotMode.value) return ''
if (isCurrentStepComponent.value) return ''
const step = currentStepData.value const step = currentStepData.value
if (!step) return '' if (!step) return ''
// if (!step.title) { return typeof step.content === 'string' ? step.content : ''
return `${step.content}` })
// }
// return ` // 当前步骤的内容(组件形式)
// <h3 class="text-lg font-bold mb-2">${step.title}</h3> const currentStepContentComponent = computed(() => {
// <div class="leading-relaxed">${step.content}</div> if (useSlotMode.value) return null
// ` if (!isCurrentStepComponent.value) return null
const step = currentStepData.value
if (!step) return null
return typeof step.content !== 'string' ? step.content : null
}) })
const handleNext = () => { const handleNext = () => {
@@ -184,7 +216,7 @@ const handleSkip = () => {
} }
const handleComplete = () => { const handleComplete = () => {
show.value = false internalShow.value = false
emit('complete') emit('complete')
} }
@@ -205,7 +237,7 @@ const goToStep = (index: number) => {
const resetGuide = () => { const resetGuide = () => {
currentStepIndex.value = 0 currentStepIndex.value = 0
show.value = true internalShow.value = true
} }
// 键盘导航 // 键盘导航
@@ -309,6 +341,7 @@ defineExpose({
inset: 0; inset: 0;
z-index: 10001; z-index: 10001;
pointer-events: none; pointer-events: none;
font-family: 'DingTalkJinBuTi';
} }
.guide-align { .guide-align {
+1 -7
View File
@@ -1,11 +1,6 @@
<template> <template>
<span class="guide-steps-item"> <span class="guide-steps-item">
<slot /> <slot />
<ImgButton
v-if="currentStep !== totalSteps"
:image="IMAGE_PATHS.BUTTONS.CONTINUE"
class="continue-button"
/>
</span> </span>
</template> </template>
@@ -36,9 +31,8 @@ defineEmits<{
@apply inline; @apply inline;
} }
.continue-button { .continue-button {
@apply inline-block ml-3; @apply absolute bottom-4 right-6 hover:scale-105;
vertical-align: middle; vertical-align: middle;
transform: scale(0.8);
} }
/* .continue-button:hover { /* .continue-button:hover {
+164 -11
View File
@@ -12,7 +12,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue' import { reactive, computed, watch, onMounted, onBeforeUnmount, ref } from 'vue'
interface Props { interface Props {
/** 是否可见 */ /** 是否可见 */
@@ -39,23 +39,175 @@ const props = withDefaults(defineProps<Props>(), {
}) })
const rect = reactive({ top: 0, left: 0, width: 0, height: 0 }) const rect = reactive({ top: 0, left: 0, width: 0, height: 0 })
const isElementReady = ref(false)
// 重试相关变量
let retryCount = 0
const MAX_RETRY_COUNT = 100 // 最大重试次数(约 1.6 秒,60fps)
const RETRY_TIMEOUT = 2000 // 超时时间(毫秒)
let retryTimer: number | null = null
let mutationObserver: MutationObserver | null = null
let retryStartTime = 0
// 验证 CSS 选择器是否有效
// 主要检查:不能以纯数字开头(除非是 ID 选择器 #1)
const isValidSelector = (selector: string): boolean => {
if (!selector || selector.trim().length === 0) return false
const trimmed = selector.trim()
// CSS 选择器不能以数字开头(除非是 ID 选择器 #)
// 如果以数字开头且不是以 # 开头,则无效
if (/^\d/.test(trimmed) && !trimmed.startsWith('#')) {
return false
}
return true
}
// 检查元素是否存在且尺寸有效
const checkElement = (selector: string): HTMLElement | null => {
try {
const el = document.querySelector(selector) as HTMLElement | null
if (!el) return null
const r = el.getBoundingClientRect()
// 只有当元素有有效尺寸时才返回
if (r.width > 0 && r.height > 0) {
return el
}
return null
} catch (error) {
return null
}
}
const measure = () => { const measure = () => {
if (!props.visible || !props.targetSelector) return if (!props.visible || !props.targetSelector) {
const el = document.querySelector(props.targetSelector) as HTMLElement | null isElementReady.value = false
if (!el) return rect.width = 0
const r = el.getBoundingClientRect() rect.height = 0
const pad = Math.max(0, props.padding) return
// 使用视口坐标(fixed层),不叠加滚动偏移 }
rect.top = r.top - pad
rect.left = r.left - pad // 验证选择器是否有效(不能是空字符串或只包含空格)
rect.width = r.width + pad * 2 const selector = props.targetSelector.trim()
rect.height = r.height + pad * 2 if (!selector || selector.length === 0) {
isElementReady.value = false
rect.width = 0
rect.height = 0
return
}
// 先进行基本格式验证,避免调用 querySelector 时抛出错误
if (!isValidSelector(selector)) {
isElementReady.value = false
rect.width = 0
rect.height = 0
return
}
const el = checkElement(selector)
if (el) {
// 元素已找到且尺寸有效
const r = el.getBoundingClientRect()
const pad = Math.max(0, props.padding)
// 使用视口坐标(fixed层),不叠加滚动偏移
rect.top = r.top - pad
rect.left = r.left - pad
rect.width = r.width + pad * 2
rect.height = r.height + pad * 2
isElementReady.value = true
// 停止重试
stopRetry()
} else {
// 元素未找到或尺寸无效,启动重试机制
isElementReady.value = false
rect.width = 0
rect.height = 0
startRetry(selector)
}
}
// 启动重试机制
const startRetry = (selector: string) => {
// 如果已经在重试中,不重复启动
if (retryTimer !== null || mutationObserver !== null) {
return
}
retryCount = 0
retryStartTime = Date.now()
// 使用 MutationObserver 监听 DOM 变化
mutationObserver = new MutationObserver(() => {
retryCheck(selector)
})
// 监听整个文档的变化
mutationObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: false
})
// 同时使用 requestAnimationFrame 轮询检查(作为备用机制)
retryCheck(selector)
}
// 重试检查
const retryCheck = (selector: string) => {
// 检查超时
if (Date.now() - retryStartTime > RETRY_TIMEOUT) {
stopRetry()
return
}
// 检查重试次数
if (retryCount >= MAX_RETRY_COUNT) {
stopRetry()
return
}
retryCount++
const el = checkElement(selector)
if (el) {
// 元素已找到,更新 rect
const r = el.getBoundingClientRect()
const pad = Math.max(0, props.padding)
rect.top = r.top - pad
rect.left = r.left - pad
rect.width = r.width + pad * 2
rect.height = r.height + pad * 2
isElementReady.value = true
stopRetry()
} else {
// 继续重试
retryTimer = requestAnimationFrame(() => {
retryTimer = null
retryCheck(selector)
})
}
}
// 停止重试
const stopRetry = () => {
if (mutationObserver) {
mutationObserver.disconnect()
mutationObserver = null
}
if (retryTimer !== null) {
cancelAnimationFrame(retryTimer)
retryTimer = null
}
retryCount = 0
} }
watch( watch(
() => [props.visible, props.targetSelector, props.padding], () => [props.visible, props.targetSelector, props.padding],
() => { () => {
stopRetry() // 停止之前的重试
requestAnimationFrame(measure) requestAnimationFrame(measure)
}, },
{ immediate: true } { immediate: true }
@@ -73,6 +225,7 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', onResize) window.removeEventListener('resize', onResize)
window.removeEventListener('scroll', onScroll) window.removeEventListener('scroll', onScroll)
stopRetry()
}) })
// 计算渐变遮罩样式 // 计算渐变遮罩样式
+1 -1
View File
@@ -99,7 +99,7 @@ export interface StepItem {
id: string id: string
label: string label: string
content: string content: string
type: 'shooting' | 'planning' | 'post' | 'other' type?: 'shooting' | 'planning' | 'post' | 'other'
} }
interface Props { interface Props {
+4 -2
View File
@@ -1,8 +1,10 @@
import type { Component } from 'vue'
// 引导步骤类型定义 // 引导步骤类型定义
export interface GuideStep { export interface GuideStep {
id: string id: string
title: string title?: string
content: string content: string | Component
nextText?: string nextText?: string
} }
@@ -28,9 +28,9 @@ const getImage = computed(() => {
// 覆盖图映射:根据构图类型和图片索引获取对应的覆盖图 // 覆盖图映射:根据构图类型和图片索引获取对应的覆盖图
const getOverlayImage = (index: number) => { const getOverlayImage = (index: number) => {
const overlayMap: Record<string, string[]> = { const overlayMap: Record<string, string[]> = {
'1': ['film/shooting/05引导线构图_覆盖', 'film/shooting/06引导线构图_覆盖'], // 框架构图 '1': ['film/shooting/01框架构图_覆盖', 'film/shooting/02框架构图_覆盖'], // 框架构图
'2': ['film/shooting/03对称构图_覆盖', 'film/shooting/04对称构图_覆盖'], // 对称构图 '2': ['film/shooting/03对称构图_覆盖', 'film/shooting/04对称构图_覆盖'], // 对称构图
'3': ['film/shooting/01框架构图_覆盖', 'film/shooting/02框架构图_覆盖'] // 引导线构图 '3': ['film/shooting/05引导线构图_覆盖', 'film/shooting/06引导线构图_覆盖'] // 引导线构图
} }
const images = overlayMap[props.compositionKey] || [] const images = overlayMap[props.compositionKey] || []
return images[index] || '' return images[index] || ''
+34 -16
View File
@@ -8,9 +8,9 @@
<!-- 主进度条 --> <!-- 主进度条 -->
<div class="progress-bar"> <div class="progress-bar">
<!-- 中间步骤指示器 --> <!-- 中间步骤指示器 -->
<div class="steps-section" v-if="currentStep > 0"> <div class="steps-section" v-if="currentStep > 0 || completedStep > 0">
<!-- 标题区域 --> <!-- 标题区域 -->
<div class="absolute top-0 left-40" v-if="currentStep > 0"> <div class="absolute top-0 left-40" v-if="currentStep > 0 || completedStep > 0">
<img <img
v-if="moduleName === 'vlog'" v-if="moduleName === 'vlog'"
:src="getStaticImage(IMAGE_PATHS.PROGRESS_BAR.TITLE)" :src="getStaticImage(IMAGE_PATHS.PROGRESS_BAR.TITLE)"
@@ -51,11 +51,7 @@
:src="currentStep === index ? steps[index].nameSelect : steps[index].nameselectNot" :src="currentStep === index ? steps[index].nameSelect : steps[index].nameselectNot"
alt="" alt=""
/> --> /> -->
<div <div class="flex items-center cursor-pointer" @click="handleStepClick(index + 1)">
class="flex items-center"
:class="canComplete ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleStepClick(index + 1)"
>
<span <span
:class="[ :class="[
'font-bold text-4xl ', 'font-bold text-4xl ',
@@ -63,7 +59,8 @@
]" ]"
>{{ step.name }}</span >{{ step.name }}</span
> >
<img v-if="canComplete" src="@/assets/images/midterm/完成.png" alt="" /> <!-- 图标 -->
<img v-if="isStepCompleted(index)" src="@/assets/images/midterm/完成.png" alt="" />
<img <img
v-else v-else
:class="['ml-2', currentStep === index + 1 && 'filter-blue']" :class="['ml-2', currentStep === index + 1 && 'filter-blue']"
@@ -72,9 +69,10 @@
/> />
</div> </div>
<!-- 连接线 -->
<template v-if="index < 2"> <template v-if="index < 2">
<img <img
v-if="canComplete" v-if="isStepCompleted(index + 1)"
class="step-ellipsis" class="step-ellipsis"
src="@/assets/images/midterm/进度装饰(已完成.png" src="@/assets/images/midterm/进度装饰(已完成.png"
alt="" alt=""
@@ -125,7 +123,9 @@ import { getStaticImage, IMAGE_PATHS } from '@/utils'
// 定义组件属性 // 定义组件属性
interface Props { interface Props {
/** 当前步骤索引 (0-3) */ /** 已完成的最大步骤编号 (1-4),表示已完成到第几步。1=前期准备, 2=中期拍摄, 3=后期剪辑, 4=实战操作台 */
completedStep?: number
/** 当前选中的步骤索引 (1-3),对应进度条显示的三个步骤 */
currentStep?: number currentStep?: number
moduleName?: 'film' | 'vlog' | 'interview' moduleName?: 'film' | 'vlog' | 'interview'
} }
@@ -138,19 +138,34 @@ interface Emits {
stepChange: [number] stepChange: [number]
/** v-model:currentStep 双向绑定时触发 */ /** v-model:currentStep 双向绑定时触发 */
'update:currentStep': [number] 'update:currentStep': [number]
/** v-model:completedStep 双向绑定时触发 */
'update:completedStep': [number]
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
currentStep: 0, completedStep: 0,
currentStep: 1,
moduleName: 'vlog' moduleName: 'vlog'
}) })
const [currentStep] = useModel(props, 'currentStep') const [currentStep] = useModel(props, 'currentStep')
const [completedStep] = useModel(props, 'completedStep')
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
// 计算属性:是否可以完成(当前在最后一步) // 计算属性:是否可以完成(前3步都已完成,解锁实战操作台)
const canComplete = computed(() => props.currentStep > steps.length) // 步骤:1=前期准备, 2=中期拍摄, 3=后期剪辑, 4=实战操作台
// 当 completedStep >= 3(即完成第3步)时,解锁右侧完成按钮
const canComplete = computed(() => props.completedStep >= 3)
// 判断步骤是否已完成
const isStepCompleted = (stepIndex: number) => {
// stepIndex 是数组索引 (0, 1, 2),对应步骤编号 (1, 2, 3)
// completedStep 是已完成的最大步骤编号 (1-4)
// 如果 completedStep >= stepIndex + 1,说明该步骤已完成
const stepNumber = stepIndex + 1
return props.completedStep >= stepNumber
}
// 处理完成按钮点击 // 处理完成按钮点击
const handleComplete = () => { const handleComplete = () => {
@@ -160,7 +175,6 @@ const handleComplete = () => {
} }
const handleStepClick = (index: number) => { const handleStepClick = (index: number) => {
if (canComplete.value) return
currentStep.value = index currentStep.value = index
emit('stepChange', index) emit('stepChange', index)
} }
@@ -189,8 +203,12 @@ const steps = [
// 获取步骤状态 // 获取步骤状态
const getStepStatus = (stepIndex: number) => { const getStepStatus = (stepIndex: number) => {
if (stepIndex < props.currentStep) return 'completed' const stepNumber = stepIndex + 1 // 转换为步骤编号 (1, 2, 3)
if (stepIndex === props.currentStep) return 'current' // 如果已完成,返回 completed
if (props.completedStep >= stepNumber) return 'completed'
// 如果当前选中,返回 current
if (props.currentStep === stepNumber) return 'current'
// 否则返回 locked
return 'locked' return 'locked'
} }
</script> </script>
+1 -1
View File
@@ -152,7 +152,7 @@ const handleSubmit = () => {
<template> <template>
<div class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none z-30"> <div class="absolute inset-0 flex items-center justify-center px-20 pointer-events-none z-30">
<div <div
class="bg-black/40 backdrop-blur-sm rounded-3xl border border-white/20 p-16 space-y-10 text-white pointer-events-auto" class="bg-black/80 backdrop-blur-lg rounded-3xl border border-white/20 p-16 space-y-10 text-white pointer-events-auto"
> >
<!-- 顶部:题号 --> <!-- 顶部:题号 -->
<div v-if="showQuestionNumber" class="flex items-center justify-between mb-2"> <div v-if="showQuestionNumber" class="flex items-center justify-between mb-2">
+17 -1
View File
@@ -11,6 +11,7 @@ export const useFilmStore = defineStore(
//Film-任务取景框引导 //Film-任务取景框引导
const isGuideFilmTask = ref<boolean>(true) const isGuideFilmTask = ref<boolean>(true)
const hasShownGuideFilmTask = ref<boolean>(false)
//Film-是否解锁完成前置(观看视频) //Film-是否解锁完成前置(观看视频)
const isFilmUnlock = ref<boolean>(false) const isFilmUnlock = ref<boolean>(false)
//Film-发现与记录解锁进度 //Film-发现与记录解锁进度
@@ -29,15 +30,30 @@ export const useFilmStore = defineStore(
FilmUnlockProgress.value = value FilmUnlockProgress.value = value
} }
function setHasShownGuideFilmTask(value: boolean): void {
hasShownGuideFilmTask.value = value
}
// Reset all Film-related persistent state
function resetFilmState(): void {
isGuideFilmTask.value = true
hasShownGuideFilmTask.value = false
isFilmUnlock.value = false
FilmUnlockProgress.value = 0
}
return { return {
//State //State
setIsGuideFilmTask, setIsGuideFilmTask,
setIsFilmUnlock, setIsFilmUnlock,
setFilmUnlockProgress, setFilmUnlockProgress,
setHasShownGuideFilmTask,
resetFilmState,
//Actions //Actions
isGuideFilmTask, isGuideFilmTask,
isFilmUnlock, isFilmUnlock,
FilmUnlockProgress FilmUnlockProgress,
hasShownGuideFilmTask
} }
}, },
{ {
+6
View File
@@ -14,8 +14,14 @@ export const useUserStore = defineStore(
isGuideNewUser.value = value isGuideNewUser.value = value
} }
// Reset all User-related persistent state
function resetUserState(): void {
isGuideNewUser.value = true
}
return { return {
setIsGuideNewUser, setIsGuideNewUser,
resetUserState,
isGuideNewUser isGuideNewUser
} }
}, },
+8
View File
@@ -29,11 +29,19 @@ export const useVlogStore = defineStore(
vlogUnlockProgress.value = value vlogUnlockProgress.value = value
} }
// Reset all Vlog-related persistent state
function resetVlogState(): void {
isGuideVlogTask.value = true
isVlogUnlock.value = false
vlogUnlockProgress.value = 0
}
return { return {
//State //State
setIsGuideVlogTask, setIsGuideVlogTask,
setIsVlogUnlock, setIsVlogUnlock,
setVlogUnlockProgress, setVlogUnlockProgress,
resetVlogState,
//Actions //Actions
isGuideVlogTask, isGuideVlogTask,
isVlogUnlock, isVlogUnlock,
+2 -1
View File
@@ -6,7 +6,7 @@
/* 定义字体 */ /* 定义字体 */
@font-face { @font-face {
font-family: 'DingTalkJinBuTi'; font-family: 'DingTalkJinBuTi';
src: url('/fonts/DingTalk-JinBuTi.otf') format('opentype'); src: url('/fonts/DingTalk-JinBuTi.ttf') format('opentype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
@@ -17,6 +17,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
font-family: 'DingTalkJinBuTi', sans-serif;
} }
body { body {
+1
View File
@@ -0,0 +1 @@
declare module 'simple-mind-map-plugin-themes'
@@ -2,7 +2,7 @@
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import ImgButton from '@/components/Button/ImgButton.vue' 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 { Video, QuestionQuiz, type Question } from '@/components'
import { getStaticImage, IMAGE_PATHS } from '@/utils' import { getStaticImage, IMAGE_PATHS } from '@/utils'
// 视频导入 // 视频导入
@@ -290,6 +290,14 @@ const handleQuestionsComplete = () => {
@step-click="handleStepClick" @step-click="handleStepClick"
@close="handleStepNavigationClose" @close="handleStepNavigationClose"
/> />
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>欢迎来到基础拍摄训练区!</GuideStepsItem>
<GuideStepsItem
>接下来,我会带你依次认识手机摄像头、看懂景别、用好慢动作与延时技法。</GuideStepsItem
>
<GuideStepsItem>还会教你如何拍出更稳的微距和更亮的夜景。</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
@@ -9,6 +9,7 @@ import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import video1 from '/video/new/xinshou_yunjingdejibenfangshi.mp4' import video1 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
import video2 from '/video/new/xinshou_zhengquedeshoujipaishe.mp4' import video2 from '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
import video3 from '/video/new/xinshou_yunjingdejibenfangshi.mp4' import video3 from '/video/new/xinshou_yunjingdejibenfangshi.mp4'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
const props = defineProps<{ const props = defineProps<{
@@ -143,6 +144,11 @@ const handleStepNavigationClose = () => {
@step-click="handleStepClick" @step-click="handleStepClick"
@close="handleStepNavigationClose" @close="handleStepNavigationClose"
/> />
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>想拍出好的视频效果,画面稳定性是最基本的要求。</GuideStepsItem>
<GuideStepsItem>接下来,我们来学习正确的持机方式和拍摄姿势。</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+36 -2
View File
@@ -7,7 +7,7 @@ import { Video } from '@/components'
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import video1 from '/video/new/xsdh_rhxzgt_batch.mp4' import video1 from '/video/new/xsdh_rhxzgt_batch.mp4'
import video2 from '/video/new/xsdh_jzgtf_batch.mp4' import video2 from '/video/new/xsdh_jzgtf_batch.mp4'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
const props = defineProps<{ const props = defineProps<{
label: string label: string
@@ -17,6 +17,8 @@ const props = defineProps<{
const playing = ref(false) const playing = ref(false)
const status = ref(false) const status = ref(false)
const guideStepIndex = ref(0)
// 步骤导航数据 // 步骤导航数据
const navigationSteps = ref([ const navigationSteps = ref([
{ {
@@ -94,7 +96,7 @@ onMounted(() => {})
/> />
<!-- 步骤导航按钮 --> <!-- 步骤导航按钮 -->
<button <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" @click="showStepNavigation = true"
title="步骤导航" title="步骤导航"
> >
@@ -137,6 +139,38 @@ onMounted(() => {})
@step-click="handleStepClick" @step-click="handleStepClick"
@close="handleStepNavigationClose" @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> </div>
</template> </template>
@@ -4,6 +4,7 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Video } from '@/components' import { Video } from '@/components'
import { getPublicPath } from '@/utils' import { getPublicPath } from '@/utils'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
@@ -45,5 +46,9 @@ const back = () => {
@click="router.push({ name: 'FilmColor' })" @click="router.push({ name: 'FilmColor' })"
/> />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
-2
View File
@@ -117,8 +117,6 @@ const handleTryOther = () => {
} }
const handleFinish = () => { const handleFinish = () => {
useFilmStore().setFilmUnlockProgress(4)
router.push({ router.push({
name: 'Film' name: 'Film'
}) })
+37 -6
View File
@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { useRouter } from 'vue-router' 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 { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Video } from '@/components' import { Video } from '@/components'
const router = useRouter() const router = useRouter()
@@ -53,11 +54,18 @@ const goContinuity = () => {
<!-- 背景 --> <!-- 背景 -->
<div <div
class="absolute inset-0 bg-cover bg-center bg-no-repeat" 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> ></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"> <div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
<Video <Video
class="w-full h-full" class="w-full h-full"
@@ -72,10 +80,10 @@ const goContinuity = () => {
@ended="handleVideoEnded" @ended="handleVideoEnded"
/> />
</div> </div>
</div> </div> -->
<!-- 内容卡片 + 按钮 --> <!-- 内容卡片 + 按钮 -->
<div <!-- <div
v-if="videoEnded" v-if="videoEnded"
class="absolute inset-0 flex items-center justify-center px-16 pointer-events-none" 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"> <div class="flex items-baseline justify-between gap-6">
<h1 class="text-5xl font-extrabold tracking-wide">剪辑</h1> <h1 class="text-5xl font-extrabold tracking-wide">剪辑</h1>
<!-- <span class="text-3xl opacity-80">从不同维度练习你的剪辑技巧</span> -->
</div> </div>
<p class="text-2xl leading-8 opacity-90"> <p class="text-2xl leading-8 opacity-90">
选择下面任意一个练习模块,进入对应的剪辑场景,按提示完成小任务,逐步建立自己的剪辑手感和创作节奏。 选择下面任意一个练习模块,进入对应的剪辑场景,按提示完成小任务,逐步建立自己的剪辑手感和创作节奏。
@@ -121,6 +128,26 @@ const goContinuity = () => {
</button> </button>
</div> </div>
</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> </div>
<!-- 关闭 --> <!-- 关闭 -->
@@ -129,6 +156,10 @@ const goContinuity = () => {
:image="IMAGE_PATHS.COMPONENTS.BACK" :image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleClose" @click="handleClose"
/> />
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
@@ -1,21 +1,34 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { Video } from '@/components' import { Video, GuideSteps, GuideStepsItem } from '@/components'
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils' import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
import { useFilmStore } from '@/stores'
const router = useRouter() const router = useRouter()
const film = useFilmStore()
const handleBack = () => { const handleBack = () => {
router.back() router.back()
} }
// 创作随机性视频 URL(暂时用其他视频代替) // 创作随机性视频 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 videoPlaying = ref(false)
const showGuide = ref(true)
const handleComplete = () => {
showGuide.value = false
}
onMounted(() => {
film.setFilmUnlockProgress(4)
})
</script> </script>
<template> <template>
@@ -36,7 +49,6 @@ const videoPlaying = ref(false)
:volume="1" :volume="1"
:controls="true" :controls="true"
:hideNativeControls="true" :hideNativeControls="true"
:autoplay="true"
/> />
</div> </div>
@@ -46,5 +58,9 @@ const videoPlaying = ref(false)
:image="IMAGE_PATHS.COMPONENTS.BACK" :image="IMAGE_PATHS.COMPONENTS.BACK"
@click="handleBack" @click="handleBack"
/> />
<GuideSteps v-if="showGuide" position-offset-y="85%" @complete="handleComplete">
<GuideStepsItem> 现在一起来看看导出后的影片效果吧! </GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
@@ -3,6 +3,7 @@ import { IMAGE_PATHS, getPublicPath, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { Video } from '@/components' import { Video } from '@/components'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
@@ -59,5 +60,9 @@ const back = () => {
/> />
</div> </div>
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+4
View File
@@ -4,6 +4,7 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale' import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
@@ -45,5 +46,8 @@ const handleOpenImages = () => {
@click="handleOpenImages" @click="handleOpenImages"
/> />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+58 -6
View File
@@ -2,7 +2,7 @@
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { ProgressBar, Overlay } from '@/components' import { ProgressBar, Overlay } from '@/components'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useFilmStore } from '@/stores' import { useFilmStore } from '@/stores'
@@ -21,6 +21,19 @@ const progress = computed(() => {
return Math.round((currentStep.value / 4) * 100) 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 = () => { const handleComplete = () => {
// console.log('完成前置条件') // console.log('完成前置条件')
// 处理完成逻辑 // 处理完成逻辑
@@ -44,10 +57,25 @@ const handleClose = () => {
} }
onMounted(() => { onMounted(() => {
if (film.hasShownGuideFilmTask) {
film.setIsGuideFilmTask(false)
}
if (currentStep.value !== 0 && currentStep.value !== 4) { if (currentStep.value !== 0 && currentStep.value !== 4) {
visible.value = true 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> </script>
<template> <template>
@@ -61,7 +89,7 @@ onMounted(() => {
<!-- 进度数据 --> <!-- 进度数据 -->
<div class="absolute top-[140px] left-[calc(50%)] -translate-x-1/2"> <div class="absolute top-[140px] left-[calc(50%)] -translate-x-1/2">
<div <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-6xl">学习进度:</span>
<span class="text-7xl">{{ progress }}%</span> <span class="text-7xl">{{ progress }}%</span>
@@ -83,7 +111,7 @@ onMounted(() => {
<!-- 标题 --> <!-- 标题 -->
<div class="absolute top-2 left-1/2 -translate-x-1/2"> <div class="absolute top-2 left-1/2 -translate-x-1/2">
<ImgButton image="film/电影-创作与表达" /> <img :src="getStaticImage('film/电影-创作与表达.png')" alt="" />
</div> </div>
<!-- 任务取景按钮 --> <!-- 任务取景按钮 -->
@@ -97,8 +125,20 @@ onMounted(() => {
/> />
</div> </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"> <Overlay content-class="w-full h-full" :visible="visible && currentStep < 4">
<!-- 关闭按钮 --> <!-- 关闭按钮 -->
@@ -108,6 +148,17 @@ onMounted(() => {
@click="visible = false" @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" /> <FilmMenu :current-step="currentStep" />
@@ -116,6 +167,7 @@ onMounted(() => {
<ProgressBar <ProgressBar
module-name="film" module-name="film"
:current-step="currentStep" :current-step="currentStep"
:completed-step="2"
@step-change="handleStepChange" @step-change="handleStepChange"
/> />
</div> </div>
@@ -131,8 +183,8 @@ onMounted(() => {
</div> </div>
<GuideSteps <GuideSteps
v-if="status" :visible="status && currentStep === 0"
highlight-selector="" highlight-selector="1"
highlight-mode="gradient" highlight-mode="gradient"
position-offset-y="85%" position-offset-y="85%"
@complete="() => {}" @complete="() => {}"
@@ -11,15 +11,32 @@ const back = () => {
// 跳转设备准备 // 跳转设备准备
router.back() router.back()
} }
const steps = [ const steps = ref([
{ id: 'step1', content: '先认识 Vlog:点击右侧「1.vlog初识」继续' }, {
{ id: 'step2', content: '这是主要内容区域的边框' } id: 'step1',
] content:
const stepIndex = ref(0) '拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。接下来,我们就一起看看在这个模块中会用到哪些常见、实用的拍摄工具。'
},
{
id: 'step2',
content: '点击右滑按钮,查看下一张拍摄工具',
highlightSelector: '.equipment-rightbutton'
},
{
id: 'step3',
content: '滚动鼠标滚轮,查看拍摄工具详细介绍',
highlightSelector: '.plate'
}
])
const stepIndex = ref<number>(0)
const hiddenbar = ref(false) const hiddenbar = ref(false)
const currentIndex = ref(0) // 当前显示的图片索引 const currentIndex = ref(0) // 当前显示的图片索引
const selectedMenuIndex = ref(0) // 当前选中的菜单项索引 const selectedMenuIndex = ref(0) // 当前选中的菜单项索引
const currentHighlight = computed(() => {
return steps.value[stepIndex.value ?? 0]?.highlightSelector
})
// 菜单项数据 // 菜单项数据
const menuItems = ref([ const menuItems = ref([
{ id: 'phone', name: '相机/摄像机/微单', disabled: false }, { id: 'phone', name: '相机/摄像机/微单', disabled: false },
@@ -74,18 +91,9 @@ const prevPhone = () => {
} }
} }
interface SimpleStep { const handleStepChange = (_: any, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
)
</script> </script>
<template> <template>
<div class="equipment-index"> <div class="equipment-index">
@@ -183,18 +191,14 @@ const currentHighlight = computed(() =>
<!-- 引导步骤 --> <!-- 引导步骤 -->
<GuideSteps <GuideSteps
:visible="true"
:steps="steps" :steps="steps"
:highlight-selector="currentHighlight" :highlight-selector="currentHighlight"
:highlight-padding="12" :highlight-padding="12"
:highlight-radius="20" :highlight-radius="20"
@step-change="handleStepChange" @step-change="handleStepChange"
position-offset-y="85%" position-offset-y="85%"
> />
<GuideStepsItem
>拍摄工具本身不分好坏,关键在于是否适合当下的拍摄内容和使用习惯。
接下来,我们就一起看看在这个模块中会用到哪些常见、实用的拍摄工具。
</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
@@ -1,11 +1,16 @@
<script setup lang="ts"> <script setup lang="ts">
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getBackgroundImage, IMAGE_PATHS } from '@/utils' import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const film = useFilmStore()
const router = useRouter() const router = useRouter()
const handleClose = () => { const handleClose = () => {
film.setFilmUnlockProgress(3)
router.back() router.back()
} }
@@ -39,11 +44,46 @@ const goRandom = () => {
<!-- 背景 --> <!-- 背景 -->
<div <div
class="absolute inset-0 bg-cover bg-center bg-no-repeat" 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> ></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 <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" 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> </button>
</div> </div>
</div> </div>
</div> </div> -->
<!-- 关闭 --> <!-- 关闭 -->
<ImgButton <ImgButton
@@ -96,6 +136,10 @@ const goRandom = () => {
@click="handleClose" @click="handleClose"
/> />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>选择任意图片按钮,进入对应拍摄场景!</GuideStepsItem>
</GuideSteps>
</template> </template>
<style scoped> <style scoped>
@@ -158,7 +158,7 @@ const capturePhoto = async () => {
class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg" class="relative w-full h-full bg-cover bg-center bg-no-repeat shooting-bg"
:style="{ backgroundImage: getBackgroundImage('film/shooting/龙美术馆模糊底图.png') }" :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" /> <ImgButton image="shooting/返回.png" class="back-button absolute right-0" @click="back" />
<p <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 <ImgButton
@@ -153,7 +153,7 @@ const handleComplete = () => {
<!-- 隐藏的背景图片,用于 Canvas 绘制 --> <!-- 隐藏的背景图片,用于 Canvas 绘制 -->
<img <img
ref="backgroundImage" 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')" :src="getStaticImage('film/shooting/油罐艺术公园清晰底图.png')"
alt="" alt=""
crossorigin="anonymous" crossorigin="anonymous"
@@ -4,10 +4,14 @@ import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide' import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { Overlay } from '@/components/Overlay' import { Overlay } from '@/components/Overlay'
import { ImgButton } from '@/components/Button' 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 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 KnowledgeCard from '@/components/KnowledgeCard/FilmKnowledgeCard.vue'
import { Video } from '@/components'
import { useFilmStore } from '@/stores'
const film = useFilmStore()
type Btn = { type Btn = {
key: string key: string
@@ -19,6 +23,58 @@ type Btn = {
const router = useRouter() 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 showModal = ref(false)
const showModal2 = ref(false) const showModal2 = ref(false)
@@ -34,24 +90,24 @@ const currentItem = ref<Btn>({
const btnList = ref<Btn[]>([ const btnList = ref<Btn[]>([
{ {
key: '1', key: '1',
name: '引导线构图', name: '框架构图',
btnImage: 'film/shooting/框架构图', btnImage: 'film/shooting/框架构图',
desc: '引导线构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用', desc: '框架构图是拍摄时用“框架”把主体框起来,可以让画面更加有空间感及层次感,并且使用也并不复杂,比如镜子、门窗、山洞等,也可以说是光影框等。',
images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图'] images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图']
}, },
{ {
key: '2', key: '2',
name: '对称构图', name: '对称构图',
btnImage: 'film/shooting/对称构图', btnImage: 'film/shooting/对称构图',
desc: '对称构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调', desc: '对称构图是通过画面中轴线(水平 / 垂直)两侧元素镜像分布的构图方式,属于平衡类构图。它借对称的视觉均衡感,让画面显规整、稳定,同时强化主体秩序与美感。拍摄可找天然对称景物(如建筑、倒影),或调整角度让元素对称;略有不对称可后期微调,适合展现建筑、对称静物等规整场景。',
images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图'] images: ['film/shooting/03对称构图_底图', 'film/shooting/04对称构图_底图']
}, },
{ {
key: '3', key: '3',
name: '框架构图', name: '引导线构图',
btnImage: 'film/shooting/引导线构图', btnImage: 'film/shooting/引导线构图',
desc: '框架构图属于引导线构图的一种,所以也是借助引导线的视觉引导效果来突出强调主体。另外对角线构图在画面中制造出对角线的走向即可。', desc: '引导线构图的关键在于引导线,借助环境中的引导线,把观者注意力集中到引导线上,营造画面纵深效果,我们日常拍摄中常见的引导线有:马路及其延伸(马路线、栏杆、走廊)',
images: ['film/shooting/01框架构图_底图', 'film/shooting/02框架构图_底图'] images: ['film/shooting/05引导线构图_底图', 'film/shooting/06引导线构图_底图']
} }
]) ])
@@ -78,6 +134,7 @@ const handleClickB = () => {
router.push({ name: 'FilmFilmingLocationB' }) router.push({ name: 'FilmFilmingLocationB' })
} }
const back = () => { const back = () => {
film.setFilmUnlockProgress(3)
// 跳转设备准备 // 跳转设备准备
router.back() router.back()
} }
@@ -87,7 +144,7 @@ const back = () => {
<!-- 背景图片 --> <!-- 背景图片 -->
<div <div
class="absolute inset-0 bg-cover bg-center bg-no-repeat shooting-bg" 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>
<div class="shooting-title"> <div class="shooting-title">
@@ -97,24 +154,25 @@ const back = () => {
<ImgButton image="shooting/返回.png" @click="back" /> <ImgButton image="shooting/返回.png" @click="back" />
</div> </div>
<div class="absolute left-[12%] top-1/2 -translate-y-[calc(50%-70px)]"> <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>
<div class="absolute right-[12%] top-1/2 -translate-y-[calc(50%-70px)]"> <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>
<div class="shooting-composition"> <div class="shooting-composition btn-1">
<ImgButton image="shooting/构图指南.png" @click="showModal = true" /> <ImgButton image="shooting/构图指南.png" @click="showModal = true" />
</div> </div>
<!-- 引导步骤 --> <!-- 引导步骤 -->
<GuideSteps <GuideSteps
:highlight-selector="'.mybutton'" :visible="true"
:steps="steps"
:highlight-selector="currentHighlight"
:highlight-padding="12" :highlight-padding="12"
:highlight-radius="20" :highlight-radius="20"
position-offset-y="85%" position-offset-y="20%"
> @step-change="handleStepChange"
<GuideStepsItem> 哇,徐汇滨江真的超适合取景!一起看看可以运用哪些构图方法吧~ </GuideStepsItem> />
</GuideSteps>
<!-- 保存图片弹窗 --> <!-- 保存图片弹窗 -->
<Overlay <Overlay
@@ -137,21 +195,38 @@ 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 <div class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2">
class="absolute left-28 top-[calc(50%-40px)] -translate-y-1/2" <ImgButton image="shooting/新手导航-视频回顾" @click="showModal3 = true" />
image="shooting/新手导航-视频回顾" <div
@click="showModal3 = true" 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"> <div class="absolute top-[30%] right-10">
<ImgButton <ImgButton
v-for="item in btnList" v-for="item in btnList"
:key="item.key" :key="item.key"
:class="`btn-item-${item.key}`"
:image="item.btnImage" :image="item.btnImage"
@click="handleClick(item)" @click="handleClick(item)"
/> />
</div> </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 <KnowledgeCard
v-if="showModal2 && currentItem.key" v-if="showModal2 && currentItem.key"
@@ -164,7 +239,7 @@ const back = () => {
/> />
<!-- 视频 --> <!-- 视频 -->
<Overlay :visible="showModal3" :clickable="false"> <Overlay :visible="showModal3" :clickable="false">
<video :src="videoSrc" controls></video> <Video :src="videoSrc" controls :volume="1" :muted="false"></Video>
<!-- 返回 --> <!-- 返回 -->
<ImgButton <ImgButton
class="absolute right-0 top-0 z-10" 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 { StarRating } from '@/components/Rating'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import BackToTopIcon from '@/assets/svg/back-to-top.svg' import BackToTopIcon from '@/assets/svg/back-to-top.svg'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
@@ -32,6 +33,19 @@ const showSittingPerson = computed<boolean>(() => {
return currentPage.value === 0 && !isScrollingBackToFirst.value return currentPage.value === 0 && !isScrollingBackToFirst.value
}) })
const visible = ref<boolean>(false)
//每一页的引导词
const steps = ref([
'欢迎来到实战操作台! 这里的内容很丰富,滚动鼠标就能浏览到全部内容啦~',
'点击【我的创作笔记】,编写创作思路~',
'点击【查看】按钮,欣赏他人实战作品~'
])
const contentSteps = computed(() => {
return steps.value[currentPage.value]
})
/** 他人实战作品列表 */ /** 他人实战作品列表 */
interface WorkItem { interface WorkItem {
id: number id: number
@@ -45,8 +59,8 @@ interface WorkItem {
const worksList = ref<WorkItem[]>([ const worksList = ref<WorkItem[]>([
{ {
id: 1, id: 1,
image: 'https://picsum.photos/400/300?random=1', image: getStaticImage('film/practice/学生案例'),
title: '作品名称', title: '学生案例',
stars: 4, stars: 4,
path: '/video/practice/film_anli1.mp4', path: '/video/practice/film_anli1.mp4',
description: description:
@@ -101,6 +115,7 @@ const smoothScrollTo = (toPage: number): void => {
} else { } else {
isAnimating.value = false isAnimating.value = false
currentPage.value = toPage currentPage.value = toPage
visible.value = true
} }
} }
@@ -143,6 +158,7 @@ const handleWheel = (event: WheelEvent): void => {
} }
onMounted(() => { onMounted(() => {
visible.value = true
containerRef.value?.addEventListener('wheel', handleWheel, { passive: false }) containerRef.value?.addEventListener('wheel', handleWheel, { passive: false })
}) })
@@ -307,6 +323,10 @@ onUnmounted(() => {
/> />
</Transition> </Transition>
</div> </div>
<GuideSteps v-if="visible" position-offset-y="70%" @complete="visible = false">
<GuideStepsItem>{{ contentSteps }}</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+7 -4
View File
@@ -3,10 +3,10 @@ import { useRouter } from 'vue-router'
import { Video } from '@/components' import { Video } from '@/components'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating' import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage } from '@/utils' import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay' import { Overlay } from '@/components/Overlay'
import { ref } from 'vue' import { ref } from 'vue'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
const props = defineProps({ const props = defineProps({
@@ -71,12 +71,12 @@ const handleBack = (): void => {
@click="handleBack" @click="handleBack"
/> />
<ImgButton <ImgButton
class="absolute right-0 bottom-10 z-50" class="absolute right-0 bottom-10 z-50 btn-1"
image="vlog/practice/评分" image="vlog/practice/评分"
@click="showModal = true" @click="showModal = true"
/> />
<Video <Video
:src="decodeURIComponent(props.path)" :src="getPublicPath(decodeURIComponent(props.path))"
:volume="1" :volume="1"
:muted="false" :muted="false"
:controls="true" :controls="true"
@@ -125,5 +125,8 @@ const handleBack = (): void => {
</div> </div>
</div> </div>
</Overlay> </Overlay>
<GuideSteps :visible="true" :highlight-selector="'.btn-1'" position-offset-y="60%">
<GuideStepsItem>点击这里可以给作品评分噢~</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+6
View File
@@ -2,6 +2,8 @@
import { IMAGE_PATHS, getStaticImage } from '@/utils' import { IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { ref } from 'vue'
const router = useRouter() const router = useRouter()
@@ -27,5 +29,9 @@ const back = () => {
<div class="relative h-full pt-28 overflow-y-scroll"> <div class="relative h-full pt-28 overflow-y-scroll">
<img :src="getStaticImage('film/img/资源筹备.png')" alt="" /> <img :src="getStaticImage('film/img/资源筹备.png')" alt="" />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+4
View File
@@ -5,6 +5,7 @@ import { Video } from '@/components'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
import { useScale } from '@/composables/useScale' import { useScale } from '@/composables/useScale'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
const router = useRouter() const router = useRouter()
@@ -56,5 +57,8 @@ const handleOpenImages = () => {
@click="handleOpenImages" @click="handleOpenImages"
/> />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+2 -7
View File
@@ -1,6 +1,6 @@
<script lang="tsx" setup> <script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' 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 { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back() router.back()
} }
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+8
View File
@@ -4,12 +4,17 @@ import { ImgButton } from '@/components/Button'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale' import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
import { useFilmStore } from '@/stores'
const { scale, offsetX, offsetY } = useScale() const { scale, offsetX, offsetY } = useScale()
const router = useRouter() const router = useRouter()
const film = useFilmStore()
const back = () => { const back = () => {
film.setFilmUnlockProgress(2)
router.back() router.back()
} }
@@ -45,5 +50,8 @@ const handleOpenImages = () => {
@click="handleOpenImages" @click="handleOpenImages"
/> />
</div> </div>
<GuideSteps :visible="true" position-offset-y="85%">
<GuideStepsItem>这份长图文干货满满,滚动鼠标查看完整内容吧</GuideStepsItem>
</GuideSteps>
</div> </div>
</template> </template>
+50 -37
View File
@@ -5,11 +5,6 @@ import ImgButton from '@/components/Button/ImgButton.vue'
import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide' import { GuideSteps, GuideStepsItem, StepNavigation } from '@/components/Guide'
import { Video, QuestionQuiz, type Question } from '@/components' import { Video, QuestionQuiz, type Question } from '@/components'
import { getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils' 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' import { useFilmStore } from '@/stores'
const router = useRouter() const router = useRouter()
@@ -21,6 +16,9 @@ const visibleGuide = ref(true)
const visibleGuide2 = ref(true) const visibleGuide2 = ref(true)
// 引导步骤索引
const guideStepIndex = ref(0)
// 受控播放开关(用于切换到下一个视频时自动播放) // 受控播放开关(用于切换到下一个视频时自动播放)
const playing = ref(false) const playing = ref(false)
const status = ref(false) const status = ref(false)
@@ -39,53 +37,54 @@ const navigationSteps = ref([
{ {
id: 'step1', id: 'step1',
label: '后期制作', label: '后期制作',
content: '遮幅', content: '遮幅'
type: 'post' as const
}, },
{ {
id: 'step2', id: 'step2',
label: '拍摄执行', label: '拍摄执行',
content: '光线', content: '光线'
type: 'post' as const
}, },
{ {
id: 'step3', id: 'step3',
label: '拍摄执行', label: '拍摄执行',
content: '虚实', content: '虚实'
type: 'shooting' as const
}, },
{ {
id: 'step4', id: 'step4',
label: '拍摄执行', label: '拍摄执行',
content: '特写镜头', content: '特写镜头'
type: 'post' as const
}, },
{ {
id: 'step5', id: 'step5',
label: '拍摄执行', label: '拍摄执行',
content: '分镜头', content: '分镜头'
type: 'post' as const
}, },
{ {
id: 'step5', id: 'step5',
label: '后期制作', label: '后期制作',
content: '滤镜', content: '滤镜'
type: 'post' as const
}, },
{ {
id: 'step5', id: 'step5',
label: '拍摄执行', label: '拍摄执行',
content: '同期声', content: '同期声'
type: 'post' as const
} }
]) ])
// 步骤导航相关状态 // 步骤导航相关状态
const showStepNavigation = ref(false) 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) const currentVideoIndex = ref(0)
// 题目数据:虚实(index 2)和分镜头(index 4) // 题目数据:虚实(index 2)和分镜头(index 4)
@@ -98,8 +97,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
return [ return [
{ {
id: 1, id: 1,
title: '交互题 1', title: '',
description: '下面两个画面,哪一个更具电影叙事感?', description: '(单选题)下面两个画面,哪一个更具电影叙事感?',
options: [ options: [
{ {
id: 'a', id: 'a',
@@ -121,8 +120,8 @@ const getQuestionsForVideo = (index: number): Question[] => {
return [ return [
{ {
id: 2, id: 2,
title: '交互题2', title: '',
description: '下面哪一组镜头顺序,更符合电影叙事逻辑?', description: '(单选题)下面哪一组镜头顺序,更符合电影叙事逻辑?',
options: [ options: [
{ {
id: 'a', id: 'a',
@@ -144,7 +143,7 @@ const getQuestionsForVideo = (index: number): Question[] => {
//按顺序播放 //按顺序播放
const currentVideo = computed(() => videoList[currentVideoIndex.value]) const currentVideo = computed(() => videoList[currentVideoIndex.value])
const resolvedSrc = computed(() => { 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 if (key.value === 'profession') return currentVideo.value
return '' return ''
}) })
@@ -169,6 +168,13 @@ const handleVideoChange = () => {
} }
const handleClose = () => { const handleClose = () => {
if (key.value === 'idea') {
film.setFilmUnlockProgress(1)
router.push({
name: 'Film'
})
return
}
router.back() router.back()
} }
@@ -204,10 +210,10 @@ const handleComplete = () => {
name: 'FilmWork' name: 'FilmWork'
}) })
} else { } else {
if (film.FilmUnlockProgress == 0) film.setFilmUnlockProgress(1) // if (film.FilmUnlockProgress == 0) film.setFilmUnlockProgress(1)
router.push({ // router.push({
name: 'Film' // name: 'Film'
}) // })
} }
} }
@@ -252,7 +258,7 @@ onMounted(() => {
<!-- 步骤导航按钮 --> <!-- 步骤导航按钮 -->
<button <button
v-if="key === 'profession'" 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" @click="showStepNavigation = true"
title="步骤导航" title="步骤导航"
> >
@@ -297,7 +303,7 @@ onMounted(() => {
</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 <StepNavigation
@@ -311,9 +317,10 @@ onMounted(() => {
<!-- 引导与高亮 --> <!-- 引导与高亮 -->
<GuideSteps <GuideSteps
highlight-selector="" :highlight-selector="guideStepIndex === 2 ? '.step-navigation-btn' : ''"
v-if="visibleGuide && key === 'profession'" v-if="visibleGuide && key === 'profession'"
position-offset-y="85%" position-offset-y="85%"
@step-change="(_: any, index: number) => (guideStepIndex = index)"
> >
<GuideStepsItem> <GuideStepsItem>
从刚才的案例可以看出,电影感画面自带一种独特的艺术魅力——或大气恢弘,扑朔迷离或充满故事感。 从刚才的案例可以看出,电影感画面自带一种独特的艺术魅力——或大气恢弘,扑朔迷离或充满故事感。
@@ -345,13 +352,19 @@ onMounted(() => {
</GuideSteps> </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>
<GuideStepsItem> <GuideStepsItem>
而优秀的短视频,就是靠无数这样的好镜头累积而成的。接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧! 接下来,我们就以徐汇滨江为例,一起动手练习如何拍出属于它的意义与故事吧!
</GuideStepsItem> </GuideStepsItem>
</GuideSteps> </GuideSteps>
</template> </template>
+19 -13
View File
@@ -35,7 +35,7 @@ const sideSwitcherItems = ref([
key: 'profession', key: 'profession',
name: '专业剖析', name: '专业剖析',
description: '物件春秋-上海钟表文化科普馆', description: '物件春秋-上海钟表文化科普馆',
image: 'film/案件赏析小屏' image: 'film/专业剖析'
}, },
{ {
key: 'idea', key: 'idea',
@@ -54,11 +54,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border' stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
) )
interface SimpleStep { const handleStepChange = (_: any, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
@@ -84,7 +80,7 @@ const handleClose = () => {
const handleWorkClick = () => { const handleWorkClick = () => {
if (!isFilmUnlock.value) return if (!isFilmUnlock.value) return
openImages([getStaticImage('work/桂花酒酿小圆子')]) openImages([getStaticImage('film/paishejiaoben')])
} }
const handleGuideComplete = () => { const handleGuideComplete = () => {
film.setIsGuideFilmTask(false) 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" 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) }" :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 <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" 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) }" :style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.WORK.TITLE) }"
@@ -137,12 +149,6 @@ function openImages(urls: string[], startIndex = 0) {
{{ currentItem?.description }}</span {{ currentItem?.description }}</span
> >
</div> </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 class="absolute inset-0 flex items-center justify-center pointer-events-none z-10">
<div <div
+61 -19
View File
@@ -7,13 +7,15 @@ import { ImgButton } from '@/components/Button'
import { ModuleSwitcher } from './components/ModuleSwitcher' import { ModuleSwitcher } from './components/ModuleSwitcher'
import { computed, ref, onMounted, onUnmounted } from 'vue' import { computed, ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores' import { useUserStore, useFilmStore, useVlogStore } from '@/stores'
import { GuideSteps, GuideStepsItem } from '@/components/Guide' import { GuideSteps, GuideStepsItem } from '@/components/Guide'
//点击电视机按钮引导状态 //点击电视机按钮引导状态
const status = ref(false) const status = ref(false)
//点击新手导航节点引导状态 const status2 = ref(false)
const status2 = ref(true) const status3 = ref(false)
const stepIndex = ref(0)
const handleComplete = () => { const handleComplete = () => {
status.value = false status.value = false
@@ -21,6 +23,8 @@ const handleComplete = () => {
const router = useRouter() const router = useRouter()
const user = useUserStore() const user = useUserStore()
const film = useFilmStore()
const vlog = useVlogStore()
// 响应式状态 // 响应式状态
const guideShow = ref(false) const guideShow = ref(false)
@@ -48,6 +52,7 @@ const preloadImages = () => {
// 事件处理函数 // 事件处理函数
const handleOpenGuide = () => { const handleOpenGuide = () => {
status2.value = true
guideShow.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 = [ const nodes = [
{ {
@@ -141,6 +167,7 @@ const handleNodeClick = (node: any, event?: MouseEvent) => {
showChildrenNodeId.value = null showChildrenNodeId.value = null
} else { } else {
showChildrenNodeId.value = node.id showChildrenNodeId.value = node.id
status3.value = true
} }
} else { } else {
// 如果点击的是没有子节点的节点,直接跳转 // 如果点击的是没有子节点的节点,直接跳转
@@ -175,12 +202,36 @@ onUnmounted(() => {
<template> <template>
<div class="w-full h-full relative"> <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 <div
:class="['absolute inset-0 bg-cover bg-center bg-no-repeat']" :class="['absolute inset-0 bg-cover bg-center bg-no-repeat']"
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION_BG) }" :style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION_BG) }"
></div> ></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']"> <div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" /> <ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
@@ -240,7 +291,7 @@ onUnmounted(() => {
</div> </div>
<!-- 节点图表 --> <!-- 节点图表 -->
<div class="absolute inset-0 top-[60px]"> <div class="absolute inset-0 top-[60px] btn-items">
<!-- 连线 (SVG) --> <!-- 连线 (SVG) -->
<svg class="absolute inset-0 w-full h-full pointer-events-none"> <svg class="absolute inset-0 w-full h-full pointer-events-none">
<!-- 左侧装饰线 -> 节点1 --> <!-- 左侧装饰线 -> 节点1 -->
@@ -323,7 +374,7 @@ onUnmounted(() => {
<!-- 子节点图标(当点击三大基础要素时显示) --> <!-- 子节点图标(当点击三大基础要素时显示) -->
<div <div
v-if="node.hasChildren && showChildrenNodeId === node.id" 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 <div
v-for="childNode in node.children" v-for="childNode in node.children"
@@ -382,20 +433,11 @@ onUnmounted(() => {
</GuideStepsItem> </GuideStepsItem>
</GuideSteps> </GuideSteps>
<GuideSteps <GuideSteps :visible="status2" highlight-selector=".btn-items" position-offset-y="20%">
v-if="status2" <GuideStepsItem> 选择任意节点开始学习 </GuideStepsItem>
highlight-mode="gradient" </GuideSteps>
highlight-selector=".newUserGuide" <GuideSteps :visible="status3" highlight-selector=".btn" position-offset-y="20%">
position-offset-y="50%" <GuideStepsItem> 选择任意要素开始学习 </GuideStepsItem>
@complete="handleComplete"
>
<GuideStepsItem>
<div class="flex items-center justify-start">
欢迎来到《短视频达人养成记》课程,请点击下方
<img class="w-20" :src="getStaticImage('home/action/top')" alt="" />
开始学习!
</div>
</GuideStepsItem>
</GuideSteps> </GuideSteps>
</div> </div>
</template> </template>
@@ -7,7 +7,10 @@
{ selected: isSelected } { 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> </div>
</template> </template>
+306 -6
View File
@@ -1,6 +1,256 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getBackgroundImage, IMAGE_PATHS } from '@/utils' import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
import { ImgButton } from '@/components/Button' 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> </script>
<template> <template>
@@ -12,16 +262,66 @@ import { ImgButton } from '@/components/Button'
></div> ></div>
<!-- 按钮 --> <!-- 按钮 -->
<!-- 开始拍摄 --> <!-- 开始拍摄 -->
<div class="absolute right-[320px] bottom-[100px]"> <div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.START_FILMING" :path="{ name: 'Action' }" /> <ImgButton image="home/action-btn" :path="{ name: 'Action' }" />
</div> </div>
<!-- 课程框架 --> <!-- 课程框架 -->
<div class="absolute right-20 bottom-36"> <div class="absolute right-20 bottom-36 kecheng-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" /> <ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" @click="openMindMap" />
</div> </div>
<!-- 操作指引 --> <!-- 操作指引 -->
<div class="absolute right-20 bottom-10"> <div class="absolute right-20 bottom-10 zhiying-wrapper">
<ImgButton :image="IMAGE_PATHS.BUTTONS.OPERATE_GUIDE" /> <ImgButton :image="IMAGE_PATHS.BUTTONS.OPERATE_GUIDE" />
</div> </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> </div>
</template> </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> <script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' 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 { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
@@ -71,12 +71,7 @@ const prevPhone = () => {
} }
} }
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
@@ -1,6 +1,6 @@
<script lang="tsx" setup> <script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' 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 { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back() router.back()
} }
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+2 -6
View File
@@ -4,7 +4,7 @@ import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils' import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue' import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps } from '@/components/Guide' import { GuideSteps, type GuideStep } from '@/components/Guide'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale' import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
@@ -66,11 +66,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border' stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
) )
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+1 -1
View File
@@ -16,7 +16,7 @@ const handleResourceClick = (resource: ResourceItem) => {
<template> <template>
<div <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 <div
v-if="props.resources.length === 0" v-if="props.resources.length === 0"
+9 -3
View File
@@ -338,13 +338,19 @@ const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
style=" style="
background: radial-gradient( background: radial-gradient(
circle at center, circle at center,
rgba(0, 0, 0, 0.5) 0%, rgba(46, 141, 218, 0.8) 0%,
rgba(0, 0, 0, 0.4) 20%, rgba(46, 141, 218, 0.4) 30%,
rgba(0, 0, 0, 0.3) 40%, rgba(46, 141, 218, 0.3) 60%,
transparent 70% transparent 70%
); );
" "
></div> ></div>
<!-- <img
:src="getStaticImage('skill/树蒙版.png')"
alt=""
class="absolute w-full h-full inset-0 z-20"
v-if="props.selectedTagId"
/> -->
</transition> </transition>
<!-- 右侧标签 --> <!-- 右侧标签 -->
+6 -3
View File
@@ -1,5 +1,8 @@
<script lang="ts" setup> <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 { ImgButton } from '@/components/Button'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -301,7 +304,7 @@ const handleClose = () => {
const handleResourceClick = (resource: ResourceItem) => { const handleResourceClick = (resource: ResourceItem) => {
if (resource.type === 'video' && resource.videoUrl) { if (resource.type === 'video' && resource.videoUrl) {
// 视频类型:打开视频播放弹窗 // 视频类型:打开视频播放弹窗
currentVideoUrl.value = resource.videoUrl currentVideoUrl.value = getPublicPath(resource.videoUrl) as string
showVideoModal.value = true showVideoModal.value = true
} else if (resource.type === 'page' && resource.routeName) { } 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) { } else if (resource.type === 'image' && resource.images && resource.images.length > 0) {
// 图文类型:使用 ImagePreview 展示 // 图文类型:使用 ImagePreview 展示
ImagePreview({ ImagePreview({
images: resource.images, images: resource.images.map(image => getStaticImage(image) as string),
startIndex: 0, startIndex: 0,
maskClosable: true, maskClosable: true,
getScaleMetrics: () => ({ getScaleMetrics: () => ({
+467 -270
View File
@@ -22,11 +22,10 @@ export interface ResourceItem {
// 一维数组格式的资源数据 // 一维数组格式的资源数据
export const resourceData: ResourceItem[] = [ export const resourceData: ResourceItem[] = [
// 新手导航模块 - 保持原有标签 // 新手导航模块 - 视觉素养 - 构图
{ {
id: 'res-1', id: 'res-1',
title: '九种构图法', title: '如何选择构图',
description: '学习九种基础构图技巧',
moduleId: 'module-1', moduleId: 'module-1',
moduleName: '新手导航', moduleName: '新手导航',
tagId: 'tag-1-1', tagId: 'tag-1-1',
@@ -34,12 +33,24 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-1', subTagId: 'subtag-1-1-1',
subTagName: '构图', subTagName: '构图',
type: 'video', type: 'video',
videoUrl: '/video/构图法.mp4' videoUrl: '/video/new/xsdh_rhxzgt_batch.mp4'
}, },
{ {
id: 'res-2', id: 'res-2',
title: '光线运用基础', title: '九种经典构图方式',
description: '掌握基础光线运用技巧', 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', moduleId: 'module-1',
moduleName: '新手导航', moduleName: '新手导航',
tagId: 'tag-1-1', tagId: 'tag-1-1',
@@ -47,12 +58,36 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-2', subTagId: 'subtag-1-1-2',
subTagName: '光线', subTagName: '光线',
type: 'video', type: 'video',
videoUrl: '/video/光线运用.mp4' videoUrl: '/video/new/xinshou_guangdejichurenzhi.mp4'
}, },
{ {
id: 'res-3', id: 'res-4',
title: '运镜技巧入门', title: '光的四大属性',
description: '学习基础运镜方法', 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', moduleId: 'module-1',
moduleName: '新手导航', moduleName: '新手导航',
tagId: 'tag-1-1', tagId: 'tag-1-1',
@@ -60,344 +95,506 @@ export const resourceData: ResourceItem[] = [
subTagId: 'subtag-1-1-3', subTagId: 'subtag-1-1-3',
subTagName: '运镜', subTagName: '运镜',
type: 'video', type: 'video',
videoUrl: '/video/运镜技巧.mp4' videoUrl: '/video/new/xinshou_zhengquedeshoujipaishe.mp4'
}, },
{ {
id: 'res-4', id: 'res-7',
title: '摄影基础教程', title: '运镜的基本方式',
description: '摄影基础知识讲解', 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', moduleId: 'module-1',
moduleName: '新手导航', moduleName: '新手导航',
tagId: 'tag-1-2', tagId: 'tag-1-2',
tagName: '摄影基础', 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', type: 'video',
videoUrl: '/video/剪映入门.mp4' videoUrl: '/video/new/xinshou_shoujishexiangtoujieshao.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', id: 'res-9',
title: 'vlog构图技巧', title: '景别概念介绍',
description: 'vlog拍摄构图方法', moduleId: 'module-1',
moduleId: 'module-2', moduleName: '新手导航',
moduleName: 'vlog发现与记录', tagId: 'tag-1-2',
tagId: 'tag-2-2', tagName: '摄影基础',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-1',
subTagName: '构图',
type: 'video', type: 'video',
videoUrl: '/video/vlog构图技巧.mp4' videoUrl: '/video/new/xsdh_jbgnjs_batch.mp4'
}, },
{ {
id: 'res-10', id: 'res-10',
title: 'vlog光线控制', title: '手机慢动作',
description: 'vlog拍摄光线运用', moduleId: 'module-1',
moduleId: 'module-2', moduleName: '新手导航',
moduleName: 'vlog发现与记录', tagId: 'tag-1-2',
tagId: 'tag-2-2', tagName: '摄影基础',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-2',
subTagName: '光线运用',
type: 'video', type: 'video',
videoUrl: '/video/vlog光线控制.mp4' videoUrl: '/video/new/xsdh_sjmdz_batch.mp4'
}, },
{ {
id: 'res-11', id: 'res-11',
title: 'vlog运镜方法', title: '延时摄影',
description: 'vlog拍摄运镜技巧', moduleId: 'module-1',
moduleId: 'module-2', moduleName: '新手导航',
moduleName: 'vlog发现与记录', tagId: 'tag-1-2',
tagId: 'tag-2-2', tagName: '摄影基础',
tagName: '拍摄执行',
subTagId: 'subtag-2-2-3',
subTagName: '运镜技巧',
type: 'video', type: 'video',
videoUrl: '/video/vlog运镜方法.mp4' videoUrl: '/video/new/xsdh_yssy_batch.mp4'
}, },
{ {
id: 'res-12', id: 'res-12',
title: 'vlog剪辑技巧', title: '神奇的微距摄影',
description: 'vlog视频剪辑方法', moduleId: 'module-1',
moduleId: 'module-2', moduleName: '新手导航',
moduleName: 'vlog发现与记录', tagId: 'tag-1-2',
tagId: 'tag-2-3', tagName: '摄影基础',
tagName: '后期制作',
subTagId: 'subtag-2-3-1',
subTagName: '剪辑技巧',
type: 'video', type: 'video',
videoUrl: '/video/vlog剪辑技巧.mp4' videoUrl: '/video/new/xinshou_shenqideweijusheying.mp4'
}, },
{ {
id: 'res-13', id: 'res-13',
title: 'vlog调色技巧', title: '独具魅力的夜景摄影',
description: 'vlog视频调色方法', moduleId: 'module-1',
moduleId: 'module-2', moduleName: '新手导航',
moduleName: 'vlog发现与记录', tagId: 'tag-1-2',
tagId: 'tag-2-3', tagName: '摄影基础',
tagName: '后期制作',
subTagId: 'subtag-2-3-2',
subTagName: '调色',
type: 'video', 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', id: 'res-14',
title: 'vlog音效处理', title: '拍摄前的信息整理',
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', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-1', tagId: 'tag-4-1',
tagName: '内容策划', tagName: '内容策划',
subTagId: 'subtag-4-1-1', subTagId: 'subtag-4-1-1',
subTagName: '设备准备', subTagName: '选题与构思',
type: 'page', type: 'page',
routeName: 'FilmResource' routeName: 'FilmResource'
}, },
{ {
id: 'res-25', id: 'res-15',
title: '电影选题构思', title: '徐汇滨江短视频构思过程',
description: '电影创作选题方法', 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', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-1', tagId: 'tag-4-1',
tagName: '内容策划', tagName: '内容策划',
subTagId: 'subtag-4-1-2', subTagId: 'subtag-4-1-2',
subTagName: '选题与构思', subTagName: '脚本与分镜制作',
type: 'video', type: 'page',
videoUrl: '/video/电影选题构思.mp4' routeName: 'FilmScriptWriting'
}, },
{ {
id: 'res-26', id: 'res-17',
title: '电影脚本分镜', title: '分镜制作',
description: '电影脚本和分镜制作',
moduleId: 'module-4', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-1', tagId: 'tag-4-1',
tagName: '内容策划', tagName: '内容策划',
subTagId: 'subtag-4-1-3', subTagId: 'subtag-4-1-2',
subTagName: '脚本与分镜制作', subTagName: '脚本与分镜制作',
type: 'video', type: 'page',
videoUrl: '/video/电影脚本分镜.mp4' routeName: 'FilmStoryboard'
}, },
// 电影创作与表达模块 - 拍摄执行
{ {
id: 'res-27', id: 'res-18',
title: '电影拍摄执行', title: '设备准备',
description: '电影拍摄执行技巧',
moduleId: 'module-4', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-2', tagId: 'tag-4-2',
tagName: '拍摄执行', tagName: '拍摄执行',
type: 'video', subTagId: 'subtag-4-2-1',
videoUrl: '/video/电影拍摄执行.mp4' subTagName: '设备准备',
type: 'page',
routeName: 'FilmMidtermEquipment'
}, },
{ {
id: 'res-28', id: 'res-19',
title: '电影后期制作', title: '构图指南',
description: '电影后期剪辑与特效', 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', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-3', tagId: 'tag-4-3',
tagName: '后期制作', tagName: '后期制作',
subTagId: 'subtag-4-3-1',
subTagName: '素材管理',
type: 'video', 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', id: 'res-29',
title: '电影AI工具应用', title: '声音转场',
description: 'AI工具在电影创作中的应用',
moduleId: 'module-4', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-4', tagId: 'tag-4-3',
tagName: 'AI与版权伦理知识', tagName: '后期制作',
subTagId: 'subtag-4-3-2',
subTagName: '剪辑技巧',
type: 'video', type: 'video',
videoUrl: '/video/电影AI工具应用.mp4' videoUrl: '/video/on-site/dygmk_kq_batch.mp4'
}, },
{ {
id: 'res-30', id: 'res-30',
title: '电影创作思路与案例', title: '快切',
description: '电影创作思路和案例分析', 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', moduleId: 'module-4',
moduleName: '电影创作与表达', moduleName: '电影创作与表达',
tagId: 'tag-4-5', tagId: 'tag-4-5',
tagName: '思路提点与案例赏析', tagName: '思路提点与案例赏析',
type: 'video', 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> <script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' 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 { ref, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
@@ -71,12 +71,7 @@ const prevPhone = () => {
} }
} }
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+2 -2
View File
@@ -3,7 +3,7 @@ import { useRouter } from 'vue-router'
import { Video } from '@/components' import { Video } from '@/components'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import { StarRating } from '@/components/Rating' import { StarRating } from '@/components/Rating'
import { IMAGE_PATHS, getBackgroundImage } from '@/utils' import { IMAGE_PATHS, getBackgroundImage, getPublicPath } from '@/utils'
import { Overlay } from '@/components/Overlay' import { Overlay } from '@/components/Overlay'
import { ref } from 'vue' import { ref } from 'vue'
@@ -76,7 +76,7 @@ const handleBack = (): void => {
@click="showModal = true" @click="showModal = true"
/> />
<Video <Video
:src="decodeURIComponent(props.path)" :src="getPublicPath(decodeURIComponent(props.path))"
:volume="1" :volume="1"
:muted="false" :muted="false"
:controls="true" :controls="true"
+2 -7
View File
@@ -1,6 +1,6 @@
<script lang="tsx" setup> <script lang="tsx" setup>
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils' 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 { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
@@ -19,12 +19,7 @@ const back = () => {
router.back() router.back()
} }
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+3 -3
View File
@@ -65,7 +65,7 @@ const navigationSteps = ref([
// 步骤导航相关状态 // 步骤导航相关状态
const showStepNavigation = ref(false) const showStepNavigation = ref(false)
const currentNavigationStep = ref(1) // 对应视频索引,从0开始 const currentNavigationStep = ref(0) // 对应视频索引,从0开始
// 视频列表 // 视频列表
const videoList = [second, third, fourth, five, six] const videoList = [second, third, fourth, five, six]
@@ -239,7 +239,7 @@ onMounted(() => {
</div> </div>
</template> --> </template> -->
<template v-slot:overlay> <template v-slot:overlay>
<div <!-- <div
:class="[ :class="[
'absolute left-4 top-4 z-10 pointer-events-auto w-[359px] h-[96px] bg-cover bg-center', 'absolute left-4 top-4 z-10 pointer-events-auto w-[359px] h-[96px] bg-cover bg-center',
'flex justify-center items-center' 'flex justify-center items-center'
@@ -253,7 +253,7 @@ onMounted(() => {
> >
{{ label }} {{ label }}
</span> </span>
</div> </div> -->
<!-- <ImgButton <!-- <ImgButton
class="absolute left-4 top-4 z-10 pointer-events-auto" class="absolute left-4 top-4 z-10 pointer-events-auto"
:image="image" :image="image"
+2 -6
View File
@@ -4,7 +4,7 @@ import { ref, computed } from 'vue'
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils' import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
import { ImgButton } from '@/components/Button' import { ImgButton } from '@/components/Button'
import WorkSideSwitcher from './components/WorkSideSwitcher.vue' import WorkSideSwitcher from './components/WorkSideSwitcher.vue'
import { GuideSteps } from '@/components/Guide' import { GuideSteps, type GuideStep } from '@/components/Guide'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useScale } from '@/composables/useScale' import { useScale } from '@/composables/useScale'
import { ImagePreview } from '@/components' import { ImagePreview } from '@/components'
@@ -66,11 +66,7 @@ const currentHighlight = computed(() =>
stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border' stepIndex.value === 0 ? '.img-switch-item--case' : '.work-border'
) )
interface SimpleStep { const handleStepChange = (_step: GuideStep | null, index: number) => {
id: string
content: string
}
const handleStepChange = (_step: SimpleStep | null, index: number) => {
stepIndex.value = index stepIndex.value = index
} }
+17
View File
@@ -1 +1,18 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
// src/typings/mind-map.d.ts
declare module 'simple-mind-map' {
class MindMap {
constructor(options: any)
render(): void
destroy(): void
setData(data: any): void
export(type: string, isDownload: boolean, fileName?: string): Promise<string>
// 根据需要在此处补充常用方法
}
export default MindMap
}
declare module 'simple-mind-map/src/plugins/*.js' {
const plugin: any
export default plugin
}
+3
View File
@@ -31,6 +31,9 @@ export default defineConfig(({ mode }) => {
} }
} }
} }
},
optimizeDeps: {
include: ['simple-mind-map']
} }
} }
}) })