444 lines
13 KiB
Vue
444 lines
13 KiB
Vue
<script lang="ts" setup>
|
||
defineOptions({
|
||
name: 'Action'
|
||
})
|
||
import { getBackgroundImage, 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'
|
||
|
||
//点击电视机按钮引导状态
|
||
const status = ref(false)
|
||
const status2 = ref(false)
|
||
const status3 = ref(false)
|
||
|
||
const stepIndex = ref(0)
|
||
|
||
const handleComplete = () => {
|
||
status.value = false
|
||
}
|
||
|
||
const router = useRouter()
|
||
const user = useUserStore()
|
||
const film = useFilmStore()
|
||
const vlog = useVlogStore()
|
||
|
||
// 响应式状态
|
||
const guideShow = ref(false)
|
||
const selectedModule = ref<'vlog' | 'film' | 'interview'>('film')
|
||
const isImageLoading = ref(false)
|
||
|
||
// 预加载图片映射
|
||
const imageMap = {
|
||
vlog: IMAGE_PATHS.ACTION.VIDEO1,
|
||
film: IMAGE_PATHS.ACTION.VIDEO2,
|
||
interview: IMAGE_PATHS.ACTION.VIDEO3
|
||
}
|
||
|
||
//获取新手导航引导状态
|
||
const isGuideNewUser = computed(() => user.isGuideNewUser)
|
||
|
||
// 预加载所有图片
|
||
const preloadImages = () => {
|
||
const imageUrls = Object.values(imageMap).map(path => getBackgroundImage(path))
|
||
imageUrls.forEach(url => {
|
||
const img = new Image()
|
||
img.src = url
|
||
})
|
||
}
|
||
|
||
// 事件处理函数
|
||
const handleOpenGuide = () => {
|
||
status2.value = true
|
||
guideShow.value = true
|
||
}
|
||
|
||
const handleCloseGuide = () => {
|
||
guideShow.value = false
|
||
if (isGuideNewUser.value) {
|
||
user.setIsGuideNewUser(false)
|
||
}
|
||
}
|
||
|
||
const handleModuleChange = (module: 'vlog' | 'film' | 'interview') => {
|
||
status.value = true
|
||
if (selectedModule.value === module) return
|
||
|
||
isImageLoading.value = true
|
||
selectedModule.value = module
|
||
|
||
// 模拟图片加载完成
|
||
setTimeout(() => {
|
||
isImageLoading.value = false
|
||
}, 100)
|
||
}
|
||
|
||
const handleClick = () => {
|
||
if (selectedModule.value === 'interview') return
|
||
// console.log(selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1))
|
||
router.push({
|
||
//首字母大写
|
||
name: selectedModule.value.charAt(0).toUpperCase() + selectedModule.value.slice(1)
|
||
})
|
||
}
|
||
|
||
// 计算属性
|
||
const selectedImage = computed(() => {
|
||
return getBackgroundImage(imageMap[selectedModule.value])
|
||
})
|
||
|
||
const handleSkill = () => {
|
||
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
|
||
})
|
||
}
|
||
|
||
// 新手导航数据
|
||
const nodes = [
|
||
{
|
||
id: 1,
|
||
num: '1',
|
||
label: '三大基础要素',
|
||
x: 20,
|
||
y: 30,
|
||
hasChildren: true,
|
||
children: [
|
||
{ id: 1, label: '构图', icon: 'home/action/新手导航构图', routeName: 'Composition' },
|
||
{ id: 2, label: '光', icon: 'home/action/新手导航光', routeName: 'Light' },
|
||
{ id: 3, label: '运镜', icon: 'home/action/新手导航运镜', routeName: 'CameraMovement' }
|
||
]
|
||
},
|
||
{
|
||
id: 2,
|
||
num: '2',
|
||
label: '基础拍摄技巧',
|
||
x: 50,
|
||
y: 60,
|
||
hasChildren: false,
|
||
routeName: 'BasicShootingSkills'
|
||
},
|
||
{
|
||
id: 3,
|
||
num: '3',
|
||
label: '短视频剪辑技巧',
|
||
x: 80,
|
||
y: 30,
|
||
hasChildren: false,
|
||
routeName: 'ShortVideoEditingSkills'
|
||
}
|
||
]
|
||
|
||
// 显示子节点的节点ID
|
||
const showChildrenNodeId = ref<number | null>(null)
|
||
|
||
// 处理节点点击
|
||
const handleNodeClick = (node: any, event?: MouseEvent) => {
|
||
if (event) {
|
||
event.stopPropagation()
|
||
}
|
||
|
||
if (node.hasChildren) {
|
||
// 如果点击的是有子节点的节点,切换显示状态
|
||
if (showChildrenNodeId.value === node.id) {
|
||
showChildrenNodeId.value = null
|
||
} else {
|
||
showChildrenNodeId.value = node.id
|
||
status3.value = true
|
||
}
|
||
} else {
|
||
// 如果点击的是没有子节点的节点,直接跳转
|
||
if (node.routeName) {
|
||
router.push({ name: node.routeName })
|
||
}
|
||
}
|
||
}
|
||
|
||
// 处理子节点点击
|
||
const handleChildNodeClick = (childNode: any, event: MouseEvent) => {
|
||
event.stopPropagation()
|
||
if (childNode.routeName) {
|
||
router.push({ name: childNode.routeName })
|
||
}
|
||
}
|
||
|
||
// 点击外部关闭子节点
|
||
const handleClickOutside = () => {
|
||
showChildrenNodeId.value = null
|
||
}
|
||
|
||
// 生命周期
|
||
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>
|
||
|
||
<!-- 背景图片 -->
|
||
<div
|
||
:class="['absolute inset-0 bg-cover bg-center bg-no-repeat']"
|
||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION_BG) }"
|
||
></div>
|
||
|
||
<div
|
||
class="absolute top-4 left-4 flex items-center text-white text-xl bg-black/10 px-4 py-2 rounded-xl"
|
||
>
|
||
!测试阶段友情提示:课程全部完成后,若需重新观看之前的内容,请按
|
||
<img
|
||
class="w-10 cursor-pointer mx-2"
|
||
src="@/assets/svg/clear.svg"
|
||
@click="handleClearCache"
|
||
/>清空缓存。
|
||
</div>
|
||
|
||
<!-- 选择模块 -->
|
||
<div :class="['absolute left-0 top-[-140px] inset-0 z-[8] pointer-events-none']">
|
||
<ModuleSwitcher v-model="selectedModule" @change="handleModuleChange" />
|
||
</div>
|
||
|
||
<!-- 电视机 -->
|
||
<div
|
||
class="absolute w-[765px] h-[768px] right-0 top-0 z-[5] transition-all duration-300 hover:scale-105 hover:brightness-110 cursor-pointer tvGuide"
|
||
:class="{ 'opacity-50': isImageLoading }"
|
||
:style="{ backgroundImage: selectedImage }"
|
||
@click.stop="handleClick"
|
||
></div>
|
||
|
||
<!-- 技能树 按钮 -->
|
||
<ImgButton
|
||
v-if="!isGuideNewUser"
|
||
@click="handleSkill"
|
||
:image="IMAGE_PATHS.ACTION.SKILLS_BTN"
|
||
class="absolute bottom-0 right-0 z-30"
|
||
/>
|
||
|
||
<!-- 新手指导遮罩 -->
|
||
<div v-if="isGuideNewUser" class="absolute inset-0 bg-black/50 z-10"></div>
|
||
|
||
<!-- 新手指导 -->
|
||
<div
|
||
class="absolute bottom-0 left-0 w-full h-[497px] bg-cover bg-center bg-no-repeat z-20 pointer-events-none"
|
||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BTN) }"
|
||
v-if="!guideShow"
|
||
>
|
||
<div
|
||
class="absolute bottom-20 right-[calc(50%-20px)] translate-x-1/2 w-20 h-20 cursor-pointer pointer-events-auto newUserGuide"
|
||
@click="handleOpenGuide"
|
||
></div>
|
||
</div>
|
||
|
||
<div
|
||
class="absolute bottom-0 left-0 w-full h-[911px] bg-cover bg-center bg-no-repeat z-40 flex flex-col items-center justify-center pt-40"
|
||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.ACTION.FOOTER_BG) }"
|
||
v-else
|
||
>
|
||
<ImgButton
|
||
:image="IMAGE_PATHS.ACTION.DOWN"
|
||
class="absolute top-60 right-1/2 translate-x-1/2 z-50"
|
||
@click="handleCloseGuide"
|
||
/>
|
||
|
||
<!-- 新手导航内容 -->
|
||
<div class="relative w-[880px] h-[400px] mt-20">
|
||
<div class="w-full h-full relative" @click="handleClickOutside">
|
||
<!-- 标题区域 -->
|
||
<div class="text-center mt-10 mb-8">
|
||
<h2 class="text-5xl font-bold mb-3 text-[#7bfffd] tracking-wider drop-shadow-lg">
|
||
新手导航
|
||
</h2>
|
||
<p class="text-4xl text-white font-bold drop-shadow-md">点击节点即可开始学习</p>
|
||
</div>
|
||
|
||
<!-- 节点图表 -->
|
||
<div class="absolute inset-0 top-[60px] btn-items">
|
||
<!-- 连线 (SVG) -->
|
||
<svg class="absolute inset-0 w-full h-full pointer-events-none">
|
||
<!-- 左侧装饰线 -> 节点1 -->
|
||
<line
|
||
x1="0%"
|
||
y1="45%"
|
||
:x2="nodes[0].x + '%'"
|
||
:y2="nodes[0].y + '%'"
|
||
stroke="#7bfffd"
|
||
stroke-width="5"
|
||
stroke-linecap="round"
|
||
/>
|
||
<!-- 节点1 -> 节点2 -->
|
||
<line
|
||
:x1="nodes[0].x + '%'"
|
||
:y1="nodes[0].y + '%'"
|
||
:x2="nodes[1].x + '%'"
|
||
:y2="nodes[1].y + '%'"
|
||
stroke="#7bfffd"
|
||
stroke-width="5"
|
||
stroke-linecap="round"
|
||
/>
|
||
<!-- 节点2 -> 节点3 -->
|
||
<line
|
||
:x1="nodes[1].x + '%'"
|
||
:y1="nodes[1].y + '%'"
|
||
:x2="nodes[2].x + '%'"
|
||
:y2="nodes[2].y + '%'"
|
||
stroke="#7bfffd"
|
||
stroke-width="5"
|
||
stroke-linecap="round"
|
||
/>
|
||
<!-- 节点3 -> 右侧装饰线 -->
|
||
<line
|
||
:x1="nodes[2].x + '%'"
|
||
:y1="nodes[2].y + '%'"
|
||
x2="100%"
|
||
y2="45%"
|
||
stroke="#7bfffd"
|
||
stroke-width="5"
|
||
stroke-linecap="round"
|
||
/>
|
||
</svg>
|
||
|
||
<!-- 节点 -->
|
||
<div
|
||
v-for="node in nodes"
|
||
:key="node.id"
|
||
class="absolute transform -translate-x-1/2 -translate-y-1/2"
|
||
:style="{ left: node.x + '%', top: node.y + '%' }"
|
||
@click="handleNodeClick(node, $event)"
|
||
>
|
||
<div
|
||
class="cursor-pointer flex flex-col items-center group transition-all duration-300 hover:scale-110"
|
||
>
|
||
<!-- 圆圈 -->
|
||
<div
|
||
class="w-24 h-24 rounded-full flex items-center justify-center text-5xl font-bold text-white z-10 relative transition-all duration-300 group-hover:bg-[#ff4d8c]"
|
||
:class="
|
||
showChildrenNodeId === node.id
|
||
? 'bg-[#ff4d8c] shadow-[0_0_20px_rgba(255,77,140,0.6)]'
|
||
: 'bg-[#60c8db] shadow-[0_0_20px_rgba(96, 200, 219,0.6)]'
|
||
"
|
||
>
|
||
{{ node.num }}
|
||
<!-- 选中状态的光晕 -->
|
||
<div
|
||
v-if="showChildrenNodeId === node.id"
|
||
class="absolute inset-0 rounded-full border-4 border-white/20 animate-pulse"
|
||
></div>
|
||
</div>
|
||
<!-- 标签 -->
|
||
<div
|
||
class="mt-2 text-4xl font-bold text-white tracking-wider drop-shadow-[0_2px_4px_rgba(0,0,0,0.5)] whitespace-nowrap"
|
||
>
|
||
{{ node.label }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 子节点图标(当点击三大基础要素时显示) -->
|
||
<div
|
||
v-if="node.hasChildren && showChildrenNodeId === node.id"
|
||
class="absolute -top-40 left-1/2 transform -translate-x-1/2 flex z-20 btn"
|
||
>
|
||
<div
|
||
v-for="childNode in node.children"
|
||
:key="childNode.id"
|
||
class="flex flex-col items-center cursor-pointer"
|
||
@click.stop="handleChildNodeClick(childNode, $event)"
|
||
>
|
||
<!-- 图标 -->
|
||
<div
|
||
class="w-36 flex items-center justify-center transition-all duration-300 hover:scale-110"
|
||
>
|
||
<img :src="getStaticImage(childNode.icon)" :alt="childNode.label" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</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>
|