feat: 完善技能树功能架构与逻辑流程文档,新增视频播放与学习进度追踪功能
This commit is contained in:
1 parent
6090abf955
commit
e9a1fee27f
42 files changed
+1849
-760
No files matched your search
+277
-58
@@ -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>
|
||||
Reference in new issue
Block a user