484 lines
14 KiB
Vue
484 lines
14 KiB
Vue
<script lang="ts" setup>
|
||
defineOptions({
|
||
name: 'Skill'
|
||
})
|
||
import { getBackgroundImage, getPublicPath, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||
import { ImgButton } from '@/components/Button'
|
||
import { ImagePreview } from '@/components'
|
||
import { useRouter } from 'vue-router'
|
||
import { ref, computed } from 'vue'
|
||
import { useScale } from '@/composables/useScale'
|
||
import {
|
||
getAllResources,
|
||
getResourcesByModule,
|
||
getResourcesByTag,
|
||
getResourcesBySubTag,
|
||
getSubTagsByTag,
|
||
type ResourceItem
|
||
} from './resourceData'
|
||
import SearchTags from './components/SearchTags.vue'
|
||
import ResourceList from './components/ResourceList.vue'
|
||
import SkillTree from './components/SkillTree.vue'
|
||
import GuideSteps from '@/components/Guide/GuideSteps.vue'
|
||
|
||
const router = useRouter()
|
||
const { scale, offsetX, offsetY } = useScale()
|
||
|
||
// 搜索框文本(保留用于用户输入搜索)
|
||
const search = ref('')
|
||
|
||
// 视频播放弹窗状态
|
||
const showVideoModal = ref(false)
|
||
const currentVideoUrl = ref('')
|
||
|
||
// 操作引导状态
|
||
const showGuide = ref(true)
|
||
const guideSteps = ref([
|
||
{
|
||
id: 'step-1',
|
||
title: '',
|
||
content:
|
||
'点击顶部模块卡片,选择您想要学习的模块(新手导航、vlog发现与记录、访谈聆听与对话、电影创作与表达)',
|
||
highlightSelector: '.module-list-container'
|
||
},
|
||
{
|
||
id: 'step-2',
|
||
title: '',
|
||
content:
|
||
'在右侧技能树中,点击标签可以查看该技能下的资源。有子标签的标签可以展开查看更多细分技能',
|
||
highlightSelector: '.skill-tree-container'
|
||
},
|
||
{
|
||
id: 'step-3',
|
||
title: '',
|
||
content: '左侧会显示当前选中标签下的所有资源,包括视频、图文和页面链接',
|
||
highlightSelector: '.resource-list-container'
|
||
},
|
||
{
|
||
id: 'step-4',
|
||
title: '',
|
||
content: '使用顶部搜索框可以快速搜索资源,支持按标题、模块、标签等关键词搜索',
|
||
highlightSelector: '.search-box-container'
|
||
},
|
||
{
|
||
id: 'step-5',
|
||
title: '',
|
||
content: '点击资源列表中的任意资源,可以查看视频、图文或跳转到相关页面',
|
||
highlightSelector: '.resource-list-container'
|
||
}
|
||
])
|
||
const currentGuideStep = ref(0)
|
||
|
||
/**
|
||
* 模块列表
|
||
* 新手导航:新手导航-选中.png
|
||
* vlog发现与记录:vlog发现与记录-选中.png
|
||
* 访谈聆听与对话:访谈聆听与对话-选中.png
|
||
* 电影创作与表达:电影创作与表达-选中.png
|
||
*/
|
||
|
||
const moduleList = ref([
|
||
{
|
||
id: 'module-1',
|
||
name: '新手导航',
|
||
image: 'skill/新手导航-选中.png'
|
||
},
|
||
{
|
||
id: 'module-2',
|
||
name: 'vlog发现与记录',
|
||
image: 'skill/vlog-选中.png'
|
||
},
|
||
{
|
||
id: 'module-3',
|
||
name: '访谈聆听与对话',
|
||
image: 'skill/访谈-选中.png'
|
||
},
|
||
{
|
||
id: 'module-4',
|
||
name: '电影创作与表达',
|
||
image: 'skill/电影-选中.png'
|
||
}
|
||
])
|
||
|
||
// 状态管理
|
||
const selectedModuleId = ref<string | null>(null)
|
||
const selectedTagId = ref<string | null>(null)
|
||
const selectedSubTagId = ref<string | null>(null)
|
||
const expandedTags = ref<Set<string>>(new Set())
|
||
|
||
// 搜索标签数组
|
||
interface SearchTag {
|
||
type: 'module' | 'tag' | 'subTag'
|
||
name: string
|
||
}
|
||
|
||
const searchTags = computed<SearchTag[]>(() => {
|
||
const tags: SearchTag[] = []
|
||
|
||
if (selectedModuleId.value) {
|
||
// 模块模式:显示完整路径
|
||
const moduleResource = getAllResources().find(r => r.moduleId === selectedModuleId.value)
|
||
if (moduleResource) {
|
||
tags.push({ type: 'module', name: moduleResource.moduleName })
|
||
}
|
||
|
||
if (selectedTagId.value) {
|
||
const tagResource = getAllResources().find(
|
||
r => r.moduleId === selectedModuleId.value && r.tagId === selectedTagId.value
|
||
)
|
||
if (tagResource) {
|
||
tags.push({ type: 'tag', name: tagResource.tagName })
|
||
}
|
||
|
||
if (selectedSubTagId.value) {
|
||
const subTagResource = getAllResources().find(
|
||
r =>
|
||
r.moduleId === selectedModuleId.value &&
|
||
r.tagId === selectedTagId.value &&
|
||
r.subTagId === selectedSubTagId.value
|
||
)
|
||
if (subTagResource && subTagResource.subTagName) {
|
||
tags.push({ type: 'subTag', name: subTagResource.subTagName })
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 总览模式:只显示标签名称(不显示模块)
|
||
if (selectedTagId.value) {
|
||
const tagResource = getAllResources().find(r => r.tagId === selectedTagId.value)
|
||
if (tagResource) {
|
||
tags.push({ type: 'tag', name: tagResource.tagName })
|
||
}
|
||
|
||
if (selectedSubTagId.value) {
|
||
const subTagResource = getAllResources().find(
|
||
r => r.tagId === selectedTagId.value && r.subTagId === selectedSubTagId.value
|
||
)
|
||
if (subTagResource && subTagResource.subTagName) {
|
||
tags.push({ type: 'subTag', name: subTagResource.subTagName })
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return tags
|
||
})
|
||
|
||
// 过滤后的资源列表
|
||
const filteredResources = computed<ResourceItem[]>(() => {
|
||
let resources: ResourceItem[] = []
|
||
|
||
// 如果选择了子标签,返回子标签的资源
|
||
if (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
|
||
resources = getResourcesBySubTag(
|
||
selectedModuleId.value,
|
||
selectedTagId.value,
|
||
selectedSubTagId.value
|
||
)
|
||
}
|
||
// 如果选择了资源标签
|
||
else if (selectedTagId.value) {
|
||
if (selectedModuleId.value) {
|
||
// 模块模式:只返回该模块该标签的资源
|
||
resources = getResourcesByTag(selectedModuleId.value, selectedTagId.value)
|
||
} else {
|
||
// 总览模式:返回所有模块中该标签名称的资源
|
||
const tagName = getAllResources().find(r => r.tagId === selectedTagId.value)?.tagName
|
||
if (tagName) {
|
||
resources = getAllResources().filter(r => r.tagName === tagName)
|
||
}
|
||
}
|
||
}
|
||
// 如果选择了模块,返回模块的资源
|
||
else if (selectedModuleId.value) {
|
||
resources = getResourcesByModule(selectedModuleId.value)
|
||
}
|
||
// 默认返回所有资源
|
||
else {
|
||
resources = getAllResources()
|
||
}
|
||
|
||
// 如果有搜索文本,进行文本搜索
|
||
if (search.value.trim()) {
|
||
const searchText = search.value.trim().toLowerCase()
|
||
resources = resources.filter(
|
||
r =>
|
||
r.title.toLowerCase().includes(searchText) ||
|
||
r.moduleName.toLowerCase().includes(searchText) ||
|
||
r.tagName.toLowerCase().includes(searchText) ||
|
||
r.description?.toLowerCase().includes(searchText)
|
||
)
|
||
}
|
||
|
||
return resources
|
||
})
|
||
|
||
// 处理模块点击
|
||
const handleModuleClick = (moduleId: string | null) => {
|
||
selectedModuleId.value = moduleId
|
||
selectedTagId.value = null
|
||
selectedSubTagId.value = null
|
||
expandedTags.value.clear()
|
||
}
|
||
|
||
// 处理资源标签点击
|
||
const handleTagClick = (moduleId: string, tagId: string) => {
|
||
// 如果点击的是已选中的标签,则取消选中(关闭遮罩)
|
||
if (selectedTagId.value === tagId) {
|
||
selectedTagId.value = null
|
||
selectedSubTagId.value = null
|
||
expandedTags.value.clear()
|
||
return
|
||
}
|
||
|
||
// 检查是否有子标签
|
||
const subTags = getSubTagsByTag(moduleId, tagId)
|
||
const hasSubTags = subTags.length > 0
|
||
|
||
if (hasSubTags) {
|
||
// 有子标签:选择新标签并自动展开
|
||
// 在总览模式下,不设置 selectedModuleId,保持总览状态
|
||
if (selectedModuleId.value !== null) {
|
||
selectedModuleId.value = moduleId
|
||
}
|
||
selectedTagId.value = tagId
|
||
selectedSubTagId.value = null
|
||
// 自动展开
|
||
expandedTags.value.add(tagId)
|
||
} else {
|
||
// 无子标签:高亮并搜索,不隐藏其他标签
|
||
// 在总览模式下,不设置 selectedModuleId,保持总览状态
|
||
if (selectedModuleId.value !== null) {
|
||
selectedModuleId.value = moduleId
|
||
}
|
||
selectedTagId.value = tagId
|
||
selectedSubTagId.value = null
|
||
expandedTags.value.clear()
|
||
}
|
||
}
|
||
|
||
// 处理子标签点击
|
||
const handleSubTagClick = (moduleId: string, tagId: string, subTagId: string) => {
|
||
// 如果点击的是已选中的子标签,则取消选择
|
||
if (
|
||
selectedModuleId.value === moduleId &&
|
||
selectedTagId.value === tagId &&
|
||
selectedSubTagId.value === subTagId
|
||
) {
|
||
selectedSubTagId.value = null
|
||
} else {
|
||
selectedModuleId.value = moduleId
|
||
selectedTagId.value = tagId
|
||
selectedSubTagId.value = subTagId
|
||
// 确保父标签是展开的
|
||
expandedTags.value.add(tagId)
|
||
}
|
||
}
|
||
|
||
// 处理模块列表点击
|
||
const handleModuleListItemClick = (moduleId: string) => {
|
||
handleModuleClick(moduleId === selectedModuleId.value ? null : moduleId)
|
||
}
|
||
|
||
// 处理删除搜索标签
|
||
const handleRemoveTag = (tagType: 'module' | 'tag' | 'subTag') => {
|
||
if (tagType === 'subTag') {
|
||
selectedSubTagId.value = null
|
||
} else if (tagType === 'tag') {
|
||
selectedTagId.value = null
|
||
selectedSubTagId.value = null
|
||
expandedTags.value.clear()
|
||
} else if (tagType === 'module') {
|
||
selectedModuleId.value = null
|
||
selectedTagId.value = null
|
||
selectedSubTagId.value = null
|
||
expandedTags.value.clear()
|
||
}
|
||
}
|
||
|
||
const handleClose = () => {
|
||
router.push({ name: 'Action' })
|
||
}
|
||
|
||
// 处理资源点击
|
||
const handleResourceClick = (resource: ResourceItem) => {
|
||
if (resource.type === 'video' && resource.videoUrl) {
|
||
// 视频类型:打开视频播放弹窗
|
||
currentVideoUrl.value = getPublicPath(resource.videoUrl) as string
|
||
showVideoModal.value = true
|
||
} else if (resource.type === 'page' && resource.routeName) {
|
||
// 页面类型:跳转到对应路由
|
||
router.push({ name: resource.routeName })
|
||
} else if (resource.type === 'image' && resource.images && resource.images.length > 0) {
|
||
// 图文类型:使用 ImagePreview 展示
|
||
ImagePreview({
|
||
images: resource.images.map(image => getStaticImage(image) as string),
|
||
startIndex: 0,
|
||
maskClosable: true,
|
||
getScaleMetrics: () => ({
|
||
scale: scale.value,
|
||
offsetX: offsetX.value,
|
||
offsetY: offsetY.value
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// 关闭视频弹窗
|
||
const closeVideoModal = () => {
|
||
showVideoModal.value = false
|
||
currentVideoUrl.value = ''
|
||
}
|
||
|
||
// 处理引导完成
|
||
const handleGuideComplete = () => {
|
||
showGuide.value = false
|
||
}
|
||
|
||
// 处理引导跳过
|
||
const handleGuideSkip = () => {
|
||
showGuide.value = false
|
||
}
|
||
|
||
// 处理引导步骤变化
|
||
const handleGuideStepChange = (step: any, index: number) => {
|
||
currentGuideStep.value = index
|
||
}
|
||
|
||
// 当前步骤的高亮选择器
|
||
const currentHighlightSelector = computed(() => {
|
||
if (showGuide.value && guideSteps.value[currentGuideStep.value]) {
|
||
return guideSteps.value[currentGuideStep.value].highlightSelector || null
|
||
}
|
||
return null
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div
|
||
class="w-full h-full bg-cover bg-center"
|
||
:style="{
|
||
backgroundImage: getBackgroundImage('skill/背景.png'),
|
||
backgroundSize: 'cover'
|
||
}"
|
||
>
|
||
<!-- 关闭 -->
|
||
<ImgButton
|
||
class="absolute right-0 top-0 z-20"
|
||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||
@click="handleClose"
|
||
/>
|
||
|
||
<!-- 搜索框 -->
|
||
<div
|
||
class="search-box-container absolute left-10 top-10 w-[547px] h-[84px] bg-cover bg-center pl-[130px] pr-[30px] pt-[20px] pb-[21px] flex items-center gap-2 overflow-hidden"
|
||
:style="{
|
||
backgroundImage: getBackgroundImage('skill/搜索框-已搜索.png'),
|
||
backgroundSize: 'cover'
|
||
}"
|
||
>
|
||
<!-- 搜索标签显示 -->
|
||
<div class="flex-shrink-0 overflow-x-auto scrollbar-hide">
|
||
<SearchTags :tags="searchTags" @remove="handleRemoveTag" />
|
||
</div>
|
||
<!-- 搜索文本输入(可选,用于额外搜索) -->
|
||
<input
|
||
v-model="search"
|
||
type="text"
|
||
name="skill"
|
||
:placeholder="searchTags.length === 0 ? '请搜索检索' : ''"
|
||
maxlength="14"
|
||
class="flex-1 min-w-0 h-full bg-transparent border-none placeholder:text-white placeholder:text-2xl outline-none text-white text-2xl"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 模块列表 -->
|
||
<div
|
||
class="module-list-container absolute left-[55%] -translate-x-1/2 top-10 bg-cover bg-center"
|
||
>
|
||
<div class="flex gap-10 -mt-7">
|
||
<div
|
||
v-for="module in moduleList"
|
||
:key="module.id"
|
||
class="relative w-[141px] h-[150px] cursor-pointer"
|
||
:class="{ 'opacity-80': selectedModuleId === module.id }"
|
||
@click="handleModuleListItemClick(module.id)"
|
||
>
|
||
<img class="absolute inset-0" :src="getStaticImage(module.image)" alt="" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 资源列表 -->
|
||
<div class="resource-list-container absolute left-10 bottom-20 w-[740px] h-[800px]">
|
||
<ResourceList :resources="filteredResources" @resource-click="handleResourceClick" />
|
||
</div>
|
||
|
||
<!-- 技能树-资源树 -->
|
||
<div class="skill-tree-container absolute right-0 bottom-20 w-[860px] h-[820px]">
|
||
<SkillTree
|
||
:selected-module-id="selectedModuleId"
|
||
:expanded-tags="expandedTags"
|
||
:selected-tag-id="selectedTagId"
|
||
:selected-sub-tag-id="selectedSubTagId"
|
||
@tag-click="handleTagClick"
|
||
@sub-tag-click="handleSubTagClick"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 视频播放弹窗 -->
|
||
<transition
|
||
enter-active-class="transition-opacity duration-300"
|
||
leave-active-class="transition-opacity duration-300"
|
||
enter-from-class="opacity-0"
|
||
enter-to-class="opacity-100"
|
||
leave-from-class="opacity-100"
|
||
leave-to-class="opacity-0"
|
||
>
|
||
<div
|
||
v-if="showVideoModal"
|
||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
|
||
@click="closeVideoModal"
|
||
>
|
||
<div
|
||
class="relative w-[90%] max-w-[1200px] h-[80%] bg-black rounded-2xl overflow-hidden shadow-2xl"
|
||
@click.stop
|
||
>
|
||
<!-- 关闭按钮 -->
|
||
<button
|
||
@click="closeVideoModal"
|
||
class="absolute top-4 right-4 z-10 w-12 h-12 flex items-center justify-center bg-white/20 hover:bg-white/30 rounded-full text-white text-3xl transition-all"
|
||
>
|
||
×
|
||
</button>
|
||
|
||
<!-- 视频播放器 -->
|
||
<video
|
||
v-if="currentVideoUrl"
|
||
:src="currentVideoUrl"
|
||
controls
|
||
autoplay
|
||
class="w-full h-full object-contain"
|
||
>
|
||
您的浏览器不支持视频播放
|
||
</video>
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
|
||
<!-- 操作引导 -->
|
||
<GuideSteps
|
||
v-if="showGuide"
|
||
:steps="guideSteps"
|
||
:keyboard-navigation="true"
|
||
:highlight-selector="currentHighlightSelector"
|
||
:highlight-padding="12"
|
||
:highlight-radius="16"
|
||
position-offset-y="85%"
|
||
@complete="handleGuideComplete"
|
||
@skip="handleGuideSkip"
|
||
@step-change="handleGuideStepChange"
|
||
/>
|
||
</div>
|
||
</template>
|