forked from zhongyidian/super_video
feat: 完善访谈与vlog模块页面,重组设备与图片公共资源,新增音频管理与一班对接工具
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c4c9be9d35
commit
fc9633f371
159 files changed
+4823
-2535
No files matched your search
+179
-120
@@ -2,33 +2,135 @@
|
||||
defineOptions({
|
||||
name: 'Action'
|
||||
})
|
||||
import { getBackgroundImage, IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS, getStaticImage } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { ModuleSwitcher } from './components/ModuleSwitcher'
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore, useFilmStore, useVlogStore } from '@/stores'
|
||||
import { GuideSteps, GuideStepsItem } from '@/components/Guide'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter, type RouteRecordName } from 'vue-router'
|
||||
import {
|
||||
useUserStore,
|
||||
useFilmStore,
|
||||
useVlogStore,
|
||||
useInterviewStore,
|
||||
useBasicElementsStore
|
||||
} from '@/stores'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import type { GuideStep } from '@/components/Guide/types'
|
||||
|
||||
//点击电视机按钮引导状态
|
||||
const status = ref(false)
|
||||
const status2 = ref(false)
|
||||
const status3 = ref(false)
|
||||
type GuideChildNode = {
|
||||
id: number
|
||||
label: string
|
||||
icon: string
|
||||
routeName: RouteRecordName
|
||||
}
|
||||
|
||||
const stepIndex = ref(0)
|
||||
type GuideNode = {
|
||||
id: number
|
||||
num: string
|
||||
label: string
|
||||
x: number
|
||||
y: number
|
||||
hasChildren: boolean
|
||||
routeName?: RouteRecordName
|
||||
children?: GuideChildNode[]
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
status.value = false
|
||||
const homeGuideShow = ref(true)
|
||||
const unlockedHomeGuideShow = ref(false)
|
||||
const elementGuideShow = ref(false)
|
||||
const unlockedHomeGuideCompleted = ref(false)
|
||||
|
||||
const homeGuideSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'home-guide-navigation',
|
||||
content: '点击新手导航按钮,可以展开/收起新手导航面板!',
|
||||
positionOffsetY: '50%',
|
||||
highlightSelector: '.newUserGuide',
|
||||
audioSrc: getPublicPath('audio/py_0_005.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-locked-modules',
|
||||
content: '新手导航模块全部完成后才能解锁三大进阶模块哦~',
|
||||
highlightSelector: '.newUserGuide',
|
||||
positionOffsetY: '50%',
|
||||
audioSrc: getPublicPath('audio/py_0_006.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-module-switcher',
|
||||
content: '舞台划分为三大创作板块!点击 Vlog、访谈、电影的对应区域,右侧屏幕就会切换到对应板块!',
|
||||
highlightSelector: '.module-switcher-guide',
|
||||
positionOffsetY: '85%',
|
||||
audioSrc: getPublicPath('audio/py_0_007.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-tv',
|
||||
content: '再点击右侧屏幕就会开启对应的板块学习哦!',
|
||||
highlightSelector: '.tvGuide',
|
||||
positionOffsetY: '80%',
|
||||
audioSrc: getPublicPath('audio/py_0_008.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-skill',
|
||||
content: '点击技能树,可以一键回顾新手导航和三大创作板块所有的知识点哦!',
|
||||
highlightSelector: '.skill-tree',
|
||||
positionOffsetY: '50%',
|
||||
audioSrc: getPublicPath('audio/py_0_009.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const unlockedHomeGuideSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'home-guide-welcome',
|
||||
content: '欢迎来到新手村!在这里,我会带着你解锁短视频创作的核心基本功!',
|
||||
audioSrc: getPublicPath('audio/py_1_001.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-map',
|
||||
content: '我们会读懂构图、用好光线、灵活运镜,了解基础拍摄技巧和剪映软件的基本操作。',
|
||||
audioSrc: getPublicPath('audio/py_1_002.MP3')
|
||||
},
|
||||
{
|
||||
id: 'home-guide-start-node',
|
||||
content: '选择任意节点开始学习~',
|
||||
highlightSelector: '.guide-content',
|
||||
positionOffsetY: '20%',
|
||||
audioSrc: getPublicPath('audio/py_1_003.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const elementGuideSteps: GuideStep[] = [
|
||||
{
|
||||
id: 'home-guide-start-element',
|
||||
content: '选择任意要素开始学习~',
|
||||
highlightSelector: '.btn',
|
||||
positionOffsetY: '20%',
|
||||
audioSrc: getPublicPath('audio/py_1_004.MP3')
|
||||
}
|
||||
]
|
||||
|
||||
const handleHomeGuideComplete = () => {
|
||||
homeGuideShow.value = false
|
||||
}
|
||||
|
||||
const handleUnlockedHomeGuideComplete = () => {
|
||||
unlockedHomeGuideShow.value = false
|
||||
}
|
||||
|
||||
const handleElementGuideComplete = () => {
|
||||
elementGuideShow.value = false
|
||||
unlockedHomeGuideCompleted.value = true
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const user = useUserStore()
|
||||
const film = useFilmStore()
|
||||
const vlog = useVlogStore()
|
||||
const interview = useInterviewStore()
|
||||
const basicElements = useBasicElementsStore()
|
||||
|
||||
// 响应式状态
|
||||
const guideShow = ref(false)
|
||||
const selectedModule = ref<'vlog' | 'film' | 'interview'>('film')
|
||||
const selectedModule = ref<'vlog' | 'film' | 'interview'>('vlog')
|
||||
const isImageLoading = ref(false)
|
||||
|
||||
// 预加载图片映射
|
||||
@@ -39,7 +141,7 @@ const imageMap = {
|
||||
}
|
||||
|
||||
//获取新手导航引导状态
|
||||
const isGuideNewUser = computed(() => user.isGuideNewUser)
|
||||
const isGuideNewUser = computed(() => basicElements.isGuideNewUser)
|
||||
|
||||
// 预加载所有图片
|
||||
const preloadImages = () => {
|
||||
@@ -52,19 +154,20 @@ const preloadImages = () => {
|
||||
|
||||
// 事件处理函数
|
||||
const handleOpenGuide = () => {
|
||||
status2.value = true
|
||||
guideShow.value = true
|
||||
if (!homeGuideShow.value && isGuideNewUser.value) {
|
||||
unlockedHomeGuideShow.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseGuide = () => {
|
||||
guideShow.value = false
|
||||
if (isGuideNewUser.value) {
|
||||
user.setIsGuideNewUser(false)
|
||||
basicElements.setIsGuideNewUser(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModuleChange = (module: 'vlog' | 'film' | 'interview') => {
|
||||
status.value = true
|
||||
if (selectedModule.value === module) return
|
||||
|
||||
isImageLoading.value = true
|
||||
@@ -77,7 +180,7 @@ const handleModuleChange = (module: 'vlog' | 'film' | 'interview') => {
|
||||
}
|
||||
|
||||
const handleClick = () => {
|
||||
if (selectedModule.value === 'interview') return
|
||||
// if (selectedModule.value === 'interview') return
|
||||
// console.log(selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1))
|
||||
router.push({
|
||||
//首字母大写
|
||||
@@ -90,35 +193,28 @@ const selectedImage = computed(() => {
|
||||
return getBackgroundImage(imageMap[selectedModule.value])
|
||||
})
|
||||
|
||||
const hasFinishedAllHomeGuides = computed(() => unlockedHomeGuideCompleted.value)
|
||||
const isSkillDisabled = computed(() => !hasFinishedAllHomeGuides.value)
|
||||
|
||||
const handleSkill = () => {
|
||||
if (isSkillDisabled.value) return
|
||||
|
||||
router.push({
|
||||
name: 'Skill'
|
||||
})
|
||||
}
|
||||
|
||||
// 清除所有与课程相关的缓存(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
|
||||
})
|
||||
film.resetFilmState()
|
||||
vlog.resetVlogState()
|
||||
interview.resetInterviewState()
|
||||
basicElements.resetBasicElementsState()
|
||||
user.resetUserState()
|
||||
}
|
||||
|
||||
// 新手导航数据
|
||||
const nodes = [
|
||||
const nodes: GuideNode[] = [
|
||||
{
|
||||
id: 1,
|
||||
num: '1',
|
||||
@@ -156,33 +252,31 @@ const nodes = [
|
||||
const showChildrenNodeId = ref<number | null>(null)
|
||||
|
||||
// 处理节点点击
|
||||
const handleNodeClick = (node: any, event?: MouseEvent) => {
|
||||
if (event) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
const handleNodeClick = (node: GuideNode, event?: MouseEvent) => {
|
||||
event?.stopPropagation()
|
||||
|
||||
if (node.hasChildren) {
|
||||
// 如果点击的是有子节点的节点,切换显示状态
|
||||
if (showChildrenNodeId.value === node.id) {
|
||||
showChildrenNodeId.value = null
|
||||
} else {
|
||||
showChildrenNodeId.value = node.id
|
||||
status3.value = true
|
||||
}
|
||||
} else {
|
||||
// 如果点击的是没有子节点的节点,直接跳转
|
||||
if (!node.hasChildren) {
|
||||
if (node.routeName) {
|
||||
router.push({ name: node.routeName })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (showChildrenNodeId.value === node.id) {
|
||||
showChildrenNodeId.value = null
|
||||
return
|
||||
}
|
||||
|
||||
showChildrenNodeId.value = node.id
|
||||
if (!unlockedHomeGuideShow.value && !unlockedHomeGuideCompleted.value) {
|
||||
elementGuideShow.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子节点点击
|
||||
const handleChildNodeClick = (childNode: any, event: MouseEvent) => {
|
||||
const handleChildNodeClick = (childNode: GuideChildNode, event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
if (childNode.routeName) {
|
||||
router.push({ name: childNode.routeName })
|
||||
}
|
||||
router.push({ name: childNode.routeName })
|
||||
}
|
||||
|
||||
// 点击外部关闭子节点
|
||||
@@ -194,26 +288,28 @@ const handleClickOutside = () => {
|
||||
onMounted(() => {
|
||||
preloadImages()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理工作
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<!-- 图片引导 -->
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full z-50"
|
||||
v-if="stepIndex < 4 && isGuideNewUser"
|
||||
@click="stepIndex++"
|
||||
>
|
||||
<img v-if="stepIndex === 0" :src="getStaticImage('home/1')" alt="" />
|
||||
<img v-else-if="stepIndex === 1" :src="getStaticImage('home/2')" alt="" />
|
||||
<img v-else-if="stepIndex === 2" :src="getStaticImage('home/3')" alt="" />
|
||||
<img v-else-if="stepIndex === 3" :src="getStaticImage('home/4')" alt="" />
|
||||
<div v-else class="absolute top-0 left-0 w-full h-full bg-black/50"></div>
|
||||
</div>
|
||||
<GuideSteps
|
||||
v-if="isGuideNewUser && homeGuideShow"
|
||||
:steps="homeGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleHomeGuideComplete"
|
||||
/>
|
||||
<GuideSteps
|
||||
v-if="unlockedHomeGuideShow"
|
||||
:steps="unlockedHomeGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleUnlockedHomeGuideComplete"
|
||||
/>
|
||||
<GuideSteps
|
||||
v-if="elementGuideShow"
|
||||
:steps="elementGuideSteps"
|
||||
position-offset-y="82%"
|
||||
@complete="handleElementGuideComplete"
|
||||
/>
|
||||
|
||||
<!-- 背景图片 -->
|
||||
<div
|
||||
@@ -229,11 +325,15 @@ onUnmounted(() => {
|
||||
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="module-switcher-guide absolute w-[1380px] h-[600px] top-[460px] left-[calc(50%-690px)] pointer-events-none"
|
||||
></div>
|
||||
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
|
||||
</div>
|
||||
|
||||
@@ -247,14 +347,17 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 技能树 按钮 -->
|
||||
<ImgButton
|
||||
v-if="!isGuideNewUser"
|
||||
@click="handleSkill"
|
||||
:image="IMAGE_PATHS.ACTION.SKILLS_BTN"
|
||||
class="absolute bottom-0 right-0 z-30"
|
||||
:disabled="isSkillDisabled"
|
||||
class="absolute bottom-0 right-0 z-30 skill-tree"
|
||||
/>
|
||||
|
||||
<!-- 新手指导遮罩 -->
|
||||
<div v-if="isGuideNewUser" class="absolute inset-0 bg-black/50 z-10"></div>
|
||||
<div
|
||||
v-if="isGuideNewUser"
|
||||
:class="['absolute inset-0 z-10', homeGuideShow ? 'bg-black/0' : 'bg-black/50']"
|
||||
></div>
|
||||
|
||||
<!-- 新手指导 -->
|
||||
<div
|
||||
@@ -281,7 +384,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- 新手导航内容 -->
|
||||
<div class="relative w-[880px] h-[400px] mt-20">
|
||||
<div class="w-full h-full relative" @click="handleClickOutside">
|
||||
<div class="w-full h-full relative guide-content" @click="handleClickOutside">
|
||||
<!-- 标题区域 -->
|
||||
<div class="text-center mt-10 mb-8">
|
||||
<h2 class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg">
|
||||
@@ -395,49 +498,5 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引导与高亮 -->
|
||||
<GuideSteps
|
||||
v-if="status"
|
||||
highlight-mode="gradient"
|
||||
highlight-selector=".tvGuide"
|
||||
position-offset-y="85%"
|
||||
@complete="handleComplete"
|
||||
>
|
||||
<GuideStepsItem>
|
||||
<div
|
||||
class="flex items-center justify-start"
|
||||
v-if="selectedModule === 'interview' || selectedModule === 'vlog'"
|
||||
>
|
||||
[{{
|
||||
selectedModule == 'interview'
|
||||
? '访谈-聆听与对话'
|
||||
: selectedModule == 'vlog'
|
||||
? 'vlog-创作与表达'
|
||||
: ''
|
||||
}}]模块建设中...
|
||||
</div>
|
||||
<div class="flex items-center justify-start" v-else>
|
||||
请点击<img
|
||||
class="w-20"
|
||||
:src="getStaticImage(imageMap[selectedModule])"
|
||||
alt=""
|
||||
/>,继续学习[{{
|
||||
selectedModule == 'film'
|
||||
? '电影-创作与表达'
|
||||
: selectedModule == 'vlog'
|
||||
? 'vlog-创作与表达'
|
||||
: ''
|
||||
}}]模块!
|
||||
</div>
|
||||
</GuideStepsItem>
|
||||
</GuideSteps>
|
||||
|
||||
<GuideSteps :visible="status2" highlight-selector=".btn-items" position-offset-y="20%">
|
||||
<GuideStepsItem> 选择任意节点开始学习 </GuideStepsItem>
|
||||
</GuideSteps>
|
||||
<GuideSteps :visible="status3" highlight-selector=".btn" position-offset-y="20%">
|
||||
<GuideStepsItem> 选择任意要素开始学习 </GuideStepsItem>
|
||||
</GuideSteps>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,113 @@
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { GuideSteps, ImagePreview } from '@/components'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
|
||||
const CourseFrameGuide = () => (
|
||||
<div class="flex items-center justify-start">
|
||||
点击
|
||||
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME)} alt="" />
|
||||
查看课程结构,快速了解每个模块的学习内容。
|
||||
</div>
|
||||
)
|
||||
|
||||
const OperateGuide = () => (
|
||||
<div class="flex items-center justify-start">
|
||||
<img class="w-20 inline" src={getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE)} alt="" />
|
||||
包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)
|
||||
</div>
|
||||
)
|
||||
|
||||
const StartFilmingGuide = () => (
|
||||
<div class="flex items-center justify-start">
|
||||
点击
|
||||
<img class="w-20" src={getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING)} alt="" />
|
||||
正式进入课程,开启你的短视频拍摄之旅。
|
||||
</div>
|
||||
)
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HomeView',
|
||||
setup() {
|
||||
const guideSteps = ref([
|
||||
{
|
||||
id: 'step-1',
|
||||
title: '',
|
||||
content: '欢迎来到《短视频达人养成记》网络课程的学习!',
|
||||
highlightSelector: null,
|
||||
audioSrc: getPublicPath('audio/py_0_001.MP3'),
|
||||
positionOffsetY: '80%'
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
title: '',
|
||||
content: CourseFrameGuide,
|
||||
highlightSelector: '.kecheng-wrapper',
|
||||
audioSrc: getPublicPath('audio/py_0_002.MP3'),
|
||||
positionOffsetY: '40%'
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
title: '',
|
||||
content: OperateGuide,
|
||||
highlightSelector: '.zhiying-wrapper',
|
||||
audioSrc: getPublicPath('audio/py_0_003.MP3'),
|
||||
positionOffsetY: '40%'
|
||||
},
|
||||
{
|
||||
id: 'step-4',
|
||||
title: '',
|
||||
content: StartFilmingGuide,
|
||||
highlightSelector: '.start-filming-wrapper',
|
||||
audioSrc: getPublicPath('audio/py_0_004.MP3'),
|
||||
positionOffsetY: '40%'
|
||||
}
|
||||
])
|
||||
const showGuide = ref(true)
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
|
||||
const handleComplete = () => {
|
||||
console.log('handleComplete')
|
||||
}
|
||||
|
||||
const openMindMap = () => {
|
||||
ImagePreview({
|
||||
images: [getStaticImage('maps/dt')],
|
||||
maskClosable: true,
|
||||
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||
})
|
||||
}
|
||||
|
||||
return () => (
|
||||
<div class="w-full h-full relative">
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: getBackgroundImage(IMAGE_PATHS.HOME_BG) }}
|
||||
/>
|
||||
|
||||
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
|
||||
<ImgButton image="home/action-btn" path={{ name: 'Action' }} />
|
||||
</div>
|
||||
|
||||
<div class="absolute right-20 bottom-36 kecheng-wrapper">
|
||||
<ImgButton image={IMAGE_PATHS.BUTTONS.COURSE_FRAME} onClick={openMindMap} />
|
||||
</div>
|
||||
|
||||
<div class="absolute right-20 bottom-10 zhiying-wrapper">
|
||||
<ImgButton image={IMAGE_PATHS.BUTTONS.OPERATE_GUIDE} />
|
||||
</div>
|
||||
|
||||
<GuideSteps
|
||||
visible={showGuide.value}
|
||||
steps={guideSteps.value}
|
||||
highlightPadding={12}
|
||||
highlightRadius={16}
|
||||
keyboardNavigation={true}
|
||||
highlightMode="border"
|
||||
onComplete={handleComplete}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -1,327 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { GuideSteps, ImagePreview, Overlay } from '@/components'
|
||||
import { computed, ref, h, nextTick } from 'vue'
|
||||
import MindMap from 'simple-mind-map'
|
||||
// XMind 解析工具(用于将 .xmind 文件解析为思维导图数据)
|
||||
// @ts-expect-error: 官方包未提供类型声明
|
||||
import xmindParser from 'simple-mind-map/src/parse/xmind.js'
|
||||
import { useScale } from '@/composables/useScale'
|
||||
|
||||
// 第一个步骤的内容组件 - 使用函数组件形式
|
||||
const Step1Content = () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex items-center justify-start'
|
||||
},
|
||||
[
|
||||
'点击',
|
||||
h('img', {
|
||||
class: 'w-20',
|
||||
src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
|
||||
alt: ''
|
||||
}),
|
||||
' 查看课程结构,快速了解每个模块的学习内容。'
|
||||
]
|
||||
)
|
||||
|
||||
const Step2Content = () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex items-center justify-start'
|
||||
},
|
||||
[
|
||||
h('img', {
|
||||
class: 'w-20 inline',
|
||||
src: getStaticImage(IMAGE_PATHS.BUTTONS.OPERATE_GUIDE),
|
||||
alt: ''
|
||||
}),
|
||||
'包含课程介绍和操作指南,观看指引视频,轻松上手课程。(资源建设中...)'
|
||||
]
|
||||
)
|
||||
|
||||
const Step3Content = () =>
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
class: 'flex items-center justify-start'
|
||||
},
|
||||
[
|
||||
'点击',
|
||||
h('img', {
|
||||
class: 'w-20',
|
||||
src: getStaticImage(IMAGE_PATHS.BUTTONS.START_FILMING),
|
||||
alt: ''
|
||||
}),
|
||||
' 正式进入课程,开启你的短视频拍摄之旅。'
|
||||
]
|
||||
)
|
||||
|
||||
// const Step3Content = () =>
|
||||
// h(
|
||||
// 'div',
|
||||
// {
|
||||
// class: 'flex items-center justify-start'
|
||||
// },
|
||||
// [
|
||||
// '点击',
|
||||
// h('img', {
|
||||
// class: 'w-20',
|
||||
// src: getStaticImage(IMAGE_PATHS.BUTTONS.COURSE_FRAME),
|
||||
// alt: ''
|
||||
// }),
|
||||
// ' 查看课程结构,快速了解每个模块的学习内容。'
|
||||
// ]
|
||||
// )
|
||||
|
||||
const guideSteps = ref([
|
||||
{
|
||||
id: 'step-1',
|
||||
title: '',
|
||||
content: Step1Content,
|
||||
highlightSelector: '.kecheng-wrapper'
|
||||
},
|
||||
{
|
||||
id: 'step-2',
|
||||
title: '',
|
||||
content: Step2Content,
|
||||
highlightSelector: '.zhiying-wrapper'
|
||||
},
|
||||
{
|
||||
id: 'step-3',
|
||||
title: '',
|
||||
content: Step3Content,
|
||||
highlightSelector: '.start-filming-wrapper'
|
||||
}
|
||||
])
|
||||
|
||||
const showGuide = ref(true)
|
||||
const currentGuideStep = ref(0)
|
||||
|
||||
// 当前步骤的高亮选择器
|
||||
const currentHighlightSelector = computed(() => {
|
||||
if (showGuide.value && guideSteps.value[currentGuideStep.value]) {
|
||||
return guideSteps.value[currentGuideStep.value].highlightSelector || null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const handleGuideStepChange = (step: any, index: number) => {
|
||||
currentGuideStep.value = index
|
||||
}
|
||||
|
||||
const handleComplete = () => {
|
||||
console.log('handleComplete')
|
||||
}
|
||||
|
||||
// 课程框架思维导图弹层
|
||||
const showMindMap = ref(false)
|
||||
const mindMapContainer = ref<HTMLElement | null>(null)
|
||||
let mindMapInstance: any = null
|
||||
|
||||
// 缓存解析后的思维导图数据,避免重复请求
|
||||
let cachedMindMapData: any = null
|
||||
|
||||
const initMindMap = async () => {
|
||||
if (!mindMapContainer.value) return
|
||||
|
||||
// 等待容器完全渲染并获取实际尺寸
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
console.log('容器尺寸:', {
|
||||
width: mindMapContainer.value.clientWidth,
|
||||
height: mindMapContainer.value.clientHeight
|
||||
})
|
||||
|
||||
// 确保容器有尺寸
|
||||
if (mindMapContainer.value.clientWidth === 0 || mindMapContainer.value.clientHeight === 0) {
|
||||
console.error('容器尺寸为 0,等待渲染')
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
}
|
||||
|
||||
// 初始化思维导图实例(每次都重新创建,避免 DOM 销毁问题)
|
||||
mindMapInstance = new MindMap({
|
||||
el: mindMapContainer.value,
|
||||
data: {
|
||||
data: { text: '加载中...' },
|
||||
children: []
|
||||
}
|
||||
})
|
||||
|
||||
// 创建后立即调用 resize
|
||||
if (typeof mindMapInstance.resize === 'function') {
|
||||
mindMapInstance.resize()
|
||||
}
|
||||
|
||||
try {
|
||||
// 如果有缓存数据,直接使用
|
||||
if (cachedMindMapData) {
|
||||
console.log('使用缓存数据')
|
||||
mindMapInstance.setData(cachedMindMapData)
|
||||
} else {
|
||||
// 首次加载,从服务器获取文件
|
||||
console.log('首次加载,获取 XMind 文件')
|
||||
const res = await fetch(getPublicPath('maps/dsp_kecheng.xmind'))
|
||||
if (!res.ok) {
|
||||
throw new Error(`加载 XMind 失败: ${res.status}`)
|
||||
}
|
||||
|
||||
const blob = await res.blob()
|
||||
// 将远程文件转换成 File 对象,供解析器使用
|
||||
const file = new File([blob], 'dsp_kecheng.xmind', {
|
||||
type: 'application/vnd.xmind.workbook'
|
||||
})
|
||||
|
||||
// 使用 simple-mind-map 提供的解析工具解析 xmind 文件
|
||||
const data = await (xmindParser as any).parseXmindFile(file)
|
||||
|
||||
// 缓存数据
|
||||
cachedMindMapData = data
|
||||
|
||||
// 将解析后的数据设置到思维导图实例
|
||||
mindMapInstance.setData(data)
|
||||
}
|
||||
|
||||
// 数据加载后再次调用 resize
|
||||
if (typeof mindMapInstance.resize === 'function') {
|
||||
mindMapInstance.resize()
|
||||
}
|
||||
|
||||
// 等待渲染完成后调用视图居中
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
centerMindMapView()
|
||||
} catch (error) {
|
||||
console.error('加载 XMind 文件失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 居中并缩放视图的辅助函数
|
||||
const centerMindMapView = () => {
|
||||
if (!mindMapInstance) return
|
||||
|
||||
const anyInstance = mindMapInstance as any
|
||||
console.log('尝试居中视图')
|
||||
|
||||
// 尝试多种方法来居中视图
|
||||
if (anyInstance.view) {
|
||||
// 方法1: fit - 自动缩放并居中
|
||||
if (typeof anyInstance.view.fit === 'function') {
|
||||
console.log('调用 view.fit()')
|
||||
anyInstance.view.fit()
|
||||
}
|
||||
// 方法2: setScale + translateX/Y 来手动居中
|
||||
else if (typeof anyInstance.view.setScale === 'function') {
|
||||
console.log('手动设置缩放和位置')
|
||||
anyInstance.view.setScale(1)
|
||||
anyInstance.view.translateX(0)
|
||||
anyInstance.view.translateY(0)
|
||||
}
|
||||
// 方法3: fitCenter
|
||||
else if (typeof anyInstance.view.fitCenter === 'function') {
|
||||
console.log('调用 view.fitCenter()')
|
||||
anyInstance.view.fitCenter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { scale, offsetX, offsetY } = useScale()
|
||||
const openMindMap = () => {
|
||||
ImagePreview({
|
||||
images: [getStaticImage('maps/dt')],
|
||||
maskClosable: true,
|
||||
getScaleMetrics: () => ({ scale: scale.value, offsetX: offsetX.value, offsetY: offsetY.value })
|
||||
})
|
||||
// showMindMap.value = true
|
||||
// nextTick(() => {
|
||||
// void initMindMap()
|
||||
// })
|
||||
}
|
||||
|
||||
const handleMindMapClose = () => {
|
||||
showMindMap.value = false
|
||||
|
||||
// 销毁思维导图实例,避免 DOM 销毁后引用失效
|
||||
if (mindMapInstance && typeof mindMapInstance.destroy === 'function') {
|
||||
mindMapInstance.destroy()
|
||||
}
|
||||
mindMapInstance = null
|
||||
|
||||
// 保留缓存数据,下次打开时直接使用,不需要重新请求
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative">
|
||||
<!-- 背景图片 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.HOME_BG) }"
|
||||
></div>
|
||||
<!-- 按钮 -->
|
||||
<!-- 开始拍摄 -->
|
||||
<div class="absolute right-[420px] bottom-[200px] start-filming-wrapper">
|
||||
<ImgButton image="home/action-btn" :path="{ name: 'Action' }" />
|
||||
</div>
|
||||
<!-- 课程框架 -->
|
||||
<div class="absolute right-20 bottom-36 kecheng-wrapper">
|
||||
<ImgButton :image="IMAGE_PATHS.BUTTONS.COURSE_FRAME" @click="openMindMap" />
|
||||
</div>
|
||||
<!-- 操作指引 -->
|
||||
<div class="absolute right-20 bottom-10 zhiying-wrapper">
|
||||
<ImgButton :image="IMAGE_PATHS.BUTTONS.OPERATE_GUIDE" />
|
||||
</div>
|
||||
|
||||
<GuideSteps
|
||||
:visible="showGuide"
|
||||
:steps="guideSteps"
|
||||
:highlight-selector="currentHighlightSelector"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="16"
|
||||
:keyboard-navigation="true"
|
||||
highlight-mode="border"
|
||||
position-offset-y="40%"
|
||||
@complete="handleComplete"
|
||||
@step-change="handleGuideStepChange"
|
||||
/>
|
||||
<!-- 课程框架思维导图弹层 -->
|
||||
<!-- <Overlay :visible="showMindMap" @close="handleMindMapClose">
|
||||
<div class="bg-white rounded-xl shadow-xl w-[100vw] h-[100vh] flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b flex-shrink-0">
|
||||
<div class="font-semibold text-lg">短视频达人课程框架</div>
|
||||
<button
|
||||
class="text-gray-500 hover:text-gray-700 text-2xl leading-none px-2"
|
||||
type="button"
|
||||
@click="handleMindMapClose"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div ref="mindMapContainer" class="mindmap-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mindmap-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 确保 simple-mind-map 生成的 SVG 填充整个容器 */
|
||||
.mindmap-container :deep(svg) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.mindmap-container :deep(.smm-container) {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user