feat: 完善技能树功能架构与逻辑流程文档,新增视频播放与学习进度追踪功能

This commit is contained in:
zhongyidian committed 2026-01-03 17:31:24 +08:00
1 parent 6090abf955
commit e9a1fee27f
42 files changed
+1849 -760

No files matched your search

+277 -58
View File
@@ -1,8 +1,10 @@
<script lang="ts" setup>
import { getBackgroundImage, 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,
@@ -14,12 +16,56 @@ import {
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
@@ -67,33 +113,48 @@ 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 (selectedModuleId.value && 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 (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 (selectedModuleId.value && selectedTagId.value && 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 })
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 })
}
}
}
}
@@ -102,23 +163,51 @@ const searchTags = computed<SearchTag[]>(() => {
// 过滤后的资源列表
const filteredResources = computed<ResourceItem[]>(() => {
let resources: ResourceItem[] = []
// 如果选择了子标签,返回子标签的资源
if (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
return getResourcesBySubTag(selectedModuleId.value, selectedTagId.value, selectedSubTagId.value)
resources = getResourcesBySubTag(
selectedModuleId.value,
selectedTagId.value,
selectedSubTagId.value
)
}
// 如果选择了资源标签,返回标签的资源
if (selectedModuleId.value && selectedTagId.value) {
return getResourcesByTag(selectedModuleId.value, selectedTagId.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)
}
}
}
// 如果选择了模块,返回模块的资源
if (selectedModuleId.value) {
return getResourcesByModule(selectedModuleId.value)
else if (selectedModuleId.value) {
resources = getResourcesByModule(selectedModuleId.value)
}
// 默认返回所有资源
else {
resources = getAllResources()
}
// 默认返回所有资源
return 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
})
// 处理模块点击
@@ -131,30 +220,37 @@ const handleModuleClick = (moduleId: string | null) => {
// 处理资源标签点击
const handleTagClick = (moduleId: string, tagId: string) => {
// 如果点击的是已选中的标签,则折叠
if (selectedModuleId.value === moduleId && selectedTagId.value === tagId) {
if (expandedTags.value.has(tagId)) {
// 如果已展开,则折叠并取消选择
expandedTags.value.delete(tagId)
selectedTagId.value = null
selectedSubTagId.value = null
} else {
// 如果未展开,则展开
expandedTags.value.add(tagId)
// 如果点击的是已选中的标签,则取消选中(关闭遮罩)
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
}
} else {
// 选择新的标签
selectedModuleId.value = moduleId
selectedTagId.value = tagId
selectedSubTagId.value = null
// 如果标签有子标签,则展开
const subTags = getSubTagsByTag(moduleId, tagId)
if (subTags.length > 0) {
expandedTags.value.add(tagId)
} else {
expandedTags.value.delete(tagId)
// 自动展开
expandedTags.value.add(tagId)
} else {
// 无子标签:高亮并搜索,不隐藏其他标签
// 在总览模式下,不设置 selectedModuleId,保持总览状态
if (selectedModuleId.value !== null) {
selectedModuleId.value = moduleId
}
selectedTagId.value = tagId
selectedSubTagId.value = null
expandedTags.value.clear()
}
}
@@ -181,9 +277,78 @@ 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 = resource.videoUrl
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,
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>
@@ -201,11 +366,9 @@ const handleClose = () => {
@click="handleClose"
/>
<img class="absolute right-5 top-44" :src="getStaticImage('skill/技能树.png')" alt="" />
<!-- 搜索框 -->
<div
class="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"
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'
@@ -213,7 +376,7 @@ const handleClose = () => {
>
<!-- 搜索标签显示 -->
<div class="flex-shrink-0 overflow-x-auto scrollbar-hide">
<SearchTags :tags="searchTags" />
<SearchTags :tags="searchTags" @remove="handleRemoveTag" />
</div>
<!-- 搜索文本输入(可选,用于额外搜索) -->
<input
@@ -227,7 +390,9 @@ const handleClose = () => {
</div>
<!-- 模块列表 -->
<div class="absolute left-[55%] -translate-x-1/2 top-10 bg-cover bg-center">
<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"
@@ -242,12 +407,12 @@ const handleClose = () => {
</div>
<!-- 资源列表 -->
<div class="absolute left-10 bottom-20 w-[740px] h-[800px]">
<ResourceList :resources="filteredResources" />
<div class="resource-list-container absolute left-10 bottom-20 w-[740px] h-[800px]">
<ResourceList :resources="filteredResources" @resource-click="handleResourceClick" />
</div>
<!-- 技能树-资源树 -->
<div class="absolute right-28 bottom-20 w-[740px] h-[820px]">
<div class="skill-tree-container absolute right-0 bottom-20 w-[860px] h-[820px]">
<SkillTree
:selected-module-id="selectedModuleId"
:expanded-tags="expandedTags"
@@ -257,5 +422,59 @@ const handleClose = () => {
@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>