Files
super_video/src/views/skill/index.vue
T

484 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>