feat: 技能树
This commit is contained in:
1 parent
6ab98c4764
commit
b980f2993b
31 files changed
+861
-292
No files matched your search
+233
-292
@@ -1,320 +1,261 @@
|
||||
<script lang="tsx" setup>
|
||||
defineOptions({ name: 'SkillPage' })
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getBackgroundImage, IMAGE_PATHS } from '@/utils'
|
||||
<script lang="ts" setup>
|
||||
import { getBackgroundImage, getStaticImage, IMAGE_PATHS } from '@/utils'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import ModuleSwitcher from './components/ModuleSwitcher.vue'
|
||||
import CategoryTags from './components/CategoryTags.vue'
|
||||
import IconLayer from './components/IconLayer.vue'
|
||||
import { GuideSteps } from '@/components/Guide'
|
||||
import KnowledgeCard from '@/components/KnowledgeCard/KnowledgeCard.vue'
|
||||
import { useSkillState, type KnowledgeBtn } from './useSkillState'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
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'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 仅从 hook 获取必要的业务状态与意图判断
|
||||
const {
|
||||
currentModule,
|
||||
nextModule,
|
||||
prevModule,
|
||||
selectedCategory,
|
||||
handleCategoryClick,
|
||||
currentCategoryConfig,
|
||||
displayedIcons,
|
||||
showSubIcons,
|
||||
selectedPrimaryIcon,
|
||||
getIconAction
|
||||
} = useSkillState()
|
||||
// 搜索框文本(保留用于用户输入搜索)
|
||||
const search = ref('')
|
||||
|
||||
// 页面 UI 状态(留在组件内)
|
||||
const status = ref(false)
|
||||
setTimeout(() => {
|
||||
status.value = true
|
||||
}, 1000)
|
||||
const sideStatus = ref(true)
|
||||
/**
|
||||
* 模块列表
|
||||
* 新手导航:新手导航-选中.png
|
||||
* vlog发现与记录:vlog发现与记录-选中.png
|
||||
* 访谈聆听与对话:访谈聆听与对话-选中.png
|
||||
* 电影创作与表达:电影创作与表达-选中.png
|
||||
*/
|
||||
|
||||
// 引导
|
||||
const stepIndex = ref(0)
|
||||
type SimpleStep = { id: string; content: string }
|
||||
const steps: SimpleStep[] = [{ id: 'step1', content: '可以点击左右箭头切换到对应模块的知识树哦' }]
|
||||
const currentHighlight = computed(() =>
|
||||
stepIndex.value === 0 ? '.img-switch-item--vlog' : '.work-border'
|
||||
)
|
||||
const handleStepChange = (_step: SimpleStep | null, index: number) => {
|
||||
stepIndex.value = index
|
||||
}
|
||||
|
||||
// 模块切换(本地完成联动重置)
|
||||
const onNextModule = () => {
|
||||
nextModule()
|
||||
if (selectedCategory.value) {
|
||||
selectedCategory.value = ''
|
||||
sideStatus.value = true
|
||||
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'
|
||||
}
|
||||
showSubIcons.value = false
|
||||
selectedPrimaryIcon.value = null
|
||||
])
|
||||
|
||||
// 状态管理
|
||||
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 onPrevModule = () => {
|
||||
prevModule()
|
||||
if (selectedCategory.value) {
|
||||
selectedCategory.value = ''
|
||||
sideStatus.value = true
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
showSubIcons.value = false
|
||||
selectedPrimaryIcon.value = null
|
||||
}
|
||||
|
||||
// 分类选择(保持原交互)
|
||||
const onCategorySelect = (categoryName: string) => {
|
||||
handleCategoryClick(categoryName)
|
||||
// 如果已解锁(status=true),则不隐藏侧边栏;否则隐藏
|
||||
sideStatus.value = status.value ? true : false
|
||||
showSubIcons.value = false
|
||||
selectedPrimaryIcon.value = null
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
// 返回按钮相关
|
||||
const backButtonTooltip = computed(() => {
|
||||
if (showSubIcons.value) return '返回上一步'
|
||||
return selectedCategory.value ? '返回主视图' : '退出页面'
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
return tags
|
||||
})
|
||||
|
||||
// 过滤后的资源列表
|
||||
const filteredResources = computed<ResourceItem[]>(() => {
|
||||
// 如果选择了子标签,返回子标签的资源
|
||||
if (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
|
||||
return getResourcesBySubTag(selectedModuleId.value, selectedTagId.value, selectedSubTagId.value)
|
||||
}
|
||||
|
||||
// 如果选择了资源标签,返回标签的资源
|
||||
if (selectedModuleId.value && selectedTagId.value) {
|
||||
return getResourcesByTag(selectedModuleId.value, selectedTagId.value)
|
||||
}
|
||||
|
||||
// 如果选择了模块,返回模块的资源
|
||||
if (selectedModuleId.value) {
|
||||
return getResourcesByModule(selectedModuleId.value)
|
||||
}
|
||||
|
||||
// 默认返回所有资源
|
||||
return getAllResources()
|
||||
})
|
||||
|
||||
// 处理模块点击
|
||||
const handleModuleClick = (moduleId: string | null) => {
|
||||
selectedModuleId.value = moduleId
|
||||
selectedTagId.value = null
|
||||
selectedSubTagId.value = null
|
||||
expandedTags.value.clear()
|
||||
}
|
||||
|
||||
// 处理资源标签点击
|
||||
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)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子标签点击
|
||||
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 handleClose = () => {
|
||||
// if (showSubIcons.value) {
|
||||
// showSubIcons.value = false
|
||||
// selectedPrimaryIcon.value = null
|
||||
// return
|
||||
// }
|
||||
// if (selectedCategory.value) {
|
||||
// handleBackToMain()
|
||||
// } else {
|
||||
router.back()
|
||||
// }
|
||||
}
|
||||
|
||||
// 知识卡片(本地维护)
|
||||
const showKnowledgeCard = ref(false)
|
||||
const currentKnowledge = ref<KnowledgeBtn | null>(null)
|
||||
const knowledgeMap: Record<'三分法构图' | '居中构图' | '对角线构图', KnowledgeBtn> = {
|
||||
居中构图: {
|
||||
key: '1',
|
||||
name: '居中构图',
|
||||
btnImage: 'shooting/居中构图底板',
|
||||
desc: '居中构图就是主体放在画面中心,让画面更稳定主体更突出,拍摄时需避免选取杂乱的背景,可以用大光圈或者长焦距让背景虚化使主体从背景中跳出来适合拍摄单个主体,操作比较简单,非常适合新手使用',
|
||||
images: ['shooting/居中构图1', 'shooting/居中构图2']
|
||||
},
|
||||
三分法构图: {
|
||||
key: '2',
|
||||
name: '三分法构图',
|
||||
btnImage: 'shooting/三分法构图底板',
|
||||
desc: '三分法构图又称九宫格构图,就是把拍摄主体置于九宫格的任意一条三分位置,三分构图法是比较常见的构图法可以视觉上让整体画面更加生动,使用三分构图法拍摄人像时,尽量占据画面的下2格比例和重心会更协调',
|
||||
images: ['shooting/图片待定1', 'shooting/图片待定2']
|
||||
},
|
||||
对角线构图: {
|
||||
key: '3',
|
||||
name: '对角线构图',
|
||||
btnImage: 'shooting/对角线构图底板',
|
||||
desc: '对角线构图属于引导线构图的一种,所以也是借助引导线的视觉引导效果来突出强调主体。另外对角线构图在画面中制造出对角线的走向即可。',
|
||||
images: ['shooting/对角线构图1', 'shooting/对角线构图2']
|
||||
}
|
||||
}
|
||||
const openKnowledge = (name: '三分法构图' | '居中构图' | '对角线构图') => {
|
||||
currentKnowledge.value = knowledgeMap[name]
|
||||
showKnowledgeCard.value = true
|
||||
}
|
||||
const currentCompositionKey = computed(() => {
|
||||
const k = currentKnowledge.value?.key
|
||||
if (k === '1' || k === '2' || k === '3') return k
|
||||
return '1'
|
||||
})
|
||||
|
||||
// 图标点击(基于 hook 的意图判断)
|
||||
const handleIconClick = (icon: string) => {
|
||||
const action = getIconAction(icon)
|
||||
if (!action) return
|
||||
if (action.type === 'navigate') {
|
||||
router.push(action.payload)
|
||||
return
|
||||
}
|
||||
if (action.type === 'expand') {
|
||||
showSubIcons.value = true
|
||||
selectedPrimaryIcon.value = 'skill/画面构图选中'
|
||||
return
|
||||
}
|
||||
if (action.type === 'openKnowledge') {
|
||||
openKnowledge(action.payload.name)
|
||||
}
|
||||
router.push({ name: 'Action' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full relative font-semibold">
|
||||
<!-- 背景图片-center -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
:style="{ backgroundImage: getBackgroundImage('skill/技能树背景') }"
|
||||
></div>
|
||||
|
||||
<ModuleSwitcher :module-name="currentModule.name" @prev="onPrevModule" @next="onNextModule" />
|
||||
|
||||
<!-- 周围模块标签(静态绘制) -->
|
||||
<CategoryTags
|
||||
:status="status"
|
||||
:side-status="sideStatus"
|
||||
:selected-category="selectedCategory"
|
||||
@select="onCategorySelect"
|
||||
/>
|
||||
|
||||
<!-- 动态显示分类对应的icon -->
|
||||
<IconLayer
|
||||
v-if="currentCategoryConfig"
|
||||
:positions="currentCategoryConfig.positions"
|
||||
:displayed-icons="displayedIcons"
|
||||
:side-status="sideStatus"
|
||||
@iconClick="handleIconClick"
|
||||
/>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<div
|
||||
class="w-full h-full bg-cover bg-center"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('skill/背景.png'),
|
||||
backgroundSize: 'cover'
|
||||
}"
|
||||
>
|
||||
<!-- 关闭 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0"
|
||||
class="absolute right-0 top-0 z-20"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
:title="backButtonTooltip"
|
||||
@click="handleClose"
|
||||
/>
|
||||
|
||||
<!-- 树 -->
|
||||
<div
|
||||
class="absolute top-28 left-[calc(50%-30px)] -translate-x-1/2 w-[786px] h-[875px] bg-cover bg-center bg-no-repeat"
|
||||
:style="{
|
||||
backgroundImage: status
|
||||
? getBackgroundImage('skill/技能树-亮')
|
||||
: getBackgroundImage('skill/技能树-灰')
|
||||
}"
|
||||
></div>
|
||||
<img class="absolute right-5 top-44" :src="getStaticImage('skill/技能树.png')" alt="" />
|
||||
|
||||
<div class="absolute bottom-10 left-1/2 -translate-x-1/2">
|
||||
<span class="red-text text-4xl">{{
|
||||
!status ? '当前尚未收集任何知识点,快去学习收集知识点吧' : '当前已全部解锁'
|
||||
}}</span>
|
||||
<!-- 搜索框 -->
|
||||
<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"
|
||||
:style="{
|
||||
backgroundImage: getBackgroundImage('skill/搜索框-已搜索.png'),
|
||||
backgroundSize: 'cover'
|
||||
}"
|
||||
>
|
||||
<!-- 搜索标签显示 -->
|
||||
<div class="flex-shrink-0 overflow-x-auto scrollbar-hide">
|
||||
<SearchTags :tags="searchTags" />
|
||||
</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>
|
||||
|
||||
<!-- 引导与高亮 -->
|
||||
<GuideSteps
|
||||
v-if="status"
|
||||
:steps="steps"
|
||||
:highlight-selector="currentHighlight"
|
||||
:highlight-padding="12"
|
||||
:highlight-radius="20"
|
||||
@step-change="handleStepChange"
|
||||
position-offset-y="85%"
|
||||
/>
|
||||
<!-- 模块列表 -->
|
||||
<div class="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>
|
||||
|
||||
<!-- 知识卡片 -->
|
||||
<KnowledgeCard
|
||||
v-if="currentKnowledge"
|
||||
:visible="showKnowledgeCard"
|
||||
:title="currentKnowledge.name"
|
||||
:desc="currentKnowledge.desc"
|
||||
:images="currentKnowledge.images"
|
||||
:compositionKey="currentCompositionKey"
|
||||
@close="showKnowledgeCard = false"
|
||||
/>
|
||||
<!-- 资源列表 -->
|
||||
<div class="absolute left-10 bottom-20 w-[740px] h-[800px]">
|
||||
<ResourceList :resources="filteredResources" />
|
||||
</div>
|
||||
|
||||
<!-- 技能树-资源树 -->
|
||||
<div class="absolute right-28 bottom-20 w-[740px] 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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 强发光蓝绿主标题(图2风格) */
|
||||
.blue-text {
|
||||
/* 文字本体采用蓝绿渐变并保持白色高亮视觉 */
|
||||
background: linear-gradient(180deg, #cfffff 0%, #9ff3ff 40%, #6ee7ff 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
/* 多层发光,柔边外扩 */
|
||||
text-shadow:
|
||||
0 0 2px rgba(255, 255, 255, 0.9),
|
||||
0 0 6px rgba(79, 241, 255, 0.9),
|
||||
0 0 14px rgba(79, 241, 255, 0.8),
|
||||
0 0 28px rgba(0, 199, 255, 0.6),
|
||||
0 0 46px rgba(0, 199, 255, 0.45);
|
||||
}
|
||||
|
||||
/* 强发光白粉副标题(图2风格) */
|
||||
.red-text {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #ffe9e9 55%, #ffd1d1 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
text-shadow:
|
||||
0 0 2px rgba(255, 255, 255, 0.9),
|
||||
0 0 8px rgba(255, 135, 135, 0.85),
|
||||
0 0 18px rgba(255, 135, 135, 0.65),
|
||||
0 0 32px rgba(255, 110, 110, 0.55);
|
||||
}
|
||||
|
||||
/* 模块标签基础样式 */
|
||||
.module {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.module:hover {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* 竖排布局 */
|
||||
.module-vertical {
|
||||
cursor: pointer;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.module-vertical .module-text {
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
}
|
||||
|
||||
.module-vertical .module-percent {
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.module-text {
|
||||
/* color: rgba(150, 150, 150, 0.9); */
|
||||
font-size: 36px;
|
||||
letter-spacing: 8px;
|
||||
}
|
||||
|
||||
.module-percent {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* 位置近似图2布局 */
|
||||
.module-left-bottom {
|
||||
left: 10%;
|
||||
bottom: 22%;
|
||||
}
|
||||
|
||||
.module-left-middle {
|
||||
left: 25%;
|
||||
top: 48%;
|
||||
}
|
||||
|
||||
.module-right-middle {
|
||||
right: 26%;
|
||||
top: 52%;
|
||||
}
|
||||
|
||||
.module-right-upper {
|
||||
right: 18%;
|
||||
top: 42%;
|
||||
}
|
||||
|
||||
.module-right-far {
|
||||
right: 8%;
|
||||
top: 50%;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user