feat: 完善技能树功能架构与逻辑流程文档,新增视频播放与学习进度追踪功能
This commit is contained in:
1 parent
6090abf955
commit
e9a1fee27f
42 files changed
+1849
-760
No files matched your search
@@ -30,37 +30,37 @@ const showQuiz = ref(false) // 控制是否显示答题窗口
|
||||
const rawSteps = [
|
||||
{
|
||||
id: 'step2',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '手机摄像头介绍',
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '景别概念介绍',
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '手机慢动作',
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step5',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '延时摄影',
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step6',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '神奇的微距摄影',
|
||||
type: 'post' as const
|
||||
},
|
||||
{
|
||||
id: 'step7',
|
||||
label: '拍摄基础',
|
||||
label: '摄影基础',
|
||||
content: '独具魅力的夜景摄影',
|
||||
type: 'post' as const
|
||||
}
|
||||
|
||||
@@ -33,19 +33,19 @@ const handleComplete = () => {
|
||||
const navigationSteps = ref([
|
||||
{
|
||||
id: 'step2',
|
||||
label: '光的基础',
|
||||
label: '视觉素养',
|
||||
content: '光的基础认知',
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step3',
|
||||
label: '光的属性',
|
||||
label: '视觉素养',
|
||||
content: '光的四大属性',
|
||||
type: 'planning' as const
|
||||
},
|
||||
{
|
||||
id: 'step4',
|
||||
label: '拍摄技巧',
|
||||
label: '视觉素养',
|
||||
content: '拍摄的黄金时间',
|
||||
type: 'planning' as const
|
||||
}
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
<script lang="ts" setup></script>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ImgButton } from '@/components/Button'
|
||||
import { Video } from '@/components'
|
||||
import { getBackgroundImage, getPublicPath, IMAGE_PATHS } from '@/utils'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 创作随机性视频 URL(暂时用其他视频代替)
|
||||
const randomVideoUrl = getPublicPath('video/on-site/dygmk_czsjx_batch.mp4')
|
||||
|
||||
// 视频播放状态
|
||||
const videoPlaying = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
<div class="relative w-full h-full flex items-center justify-center overflow-hidden">
|
||||
<!-- 背景 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-cover bg-center bg-no-repeat -z-10"
|
||||
:style="{ backgroundImage: getBackgroundImage(IMAGE_PATHS.SHOOTING.BG) }"
|
||||
></div>
|
||||
|
||||
<!-- 视频播放区域 -->
|
||||
<div class="w-full h-full bg-black/40 backdrop-blur-md rounded-3xl border border-white/20">
|
||||
<Video
|
||||
class="w-full h-full"
|
||||
:src="randomVideoUrl"
|
||||
v-model="videoPlaying"
|
||||
:muted="false"
|
||||
:volume="1"
|
||||
:controls="true"
|
||||
:hideNativeControls="true"
|
||||
:autoplay="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 返回按钮 -->
|
||||
<ImgButton
|
||||
class="absolute right-0 top-0 z-20"
|
||||
:image="IMAGE_PATHS.COMPONENTS.BACK"
|
||||
@click="handleBack"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,212 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
type CategoryName = '拍摄基础' | '内容策划' | '拍摄执行' | '后期制作' | '版权伦理意识'
|
||||
|
||||
const props = defineProps<{
|
||||
status: boolean
|
||||
sideStatus: boolean
|
||||
selectedCategory: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'select', name: CategoryName): void }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-show="props.sideStatus || props.selectedCategory === '拍摄基础'"
|
||||
:class="[
|
||||
'module module-left-bottom module-vertical',
|
||||
{ active: props.selectedCategory === '拍摄基础' }
|
||||
]"
|
||||
@click="emit('select', '拍摄基础')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'module-text blue-text',
|
||||
props.status ? 'text-white' : 'text-[#969696e6_!important]'
|
||||
]"
|
||||
>
|
||||
拍摄基础
|
||||
</div>
|
||||
<div :class="['module-percent', props.status ? 'red-text' : 'blue-text']">0%</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="props.sideStatus || props.selectedCategory === '内容策划'"
|
||||
:class="[
|
||||
'module module-left-middle module-vertical',
|
||||
{ active: props.selectedCategory === '内容策划' }
|
||||
]"
|
||||
@click="emit('select', '内容策划')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'module-text blue-text',
|
||||
props.status ? 'text-white' : 'text-[#969696e6_!important]'
|
||||
]"
|
||||
>
|
||||
内容策划
|
||||
</div>
|
||||
<div :class="['module-percent', props.status ? 'red-text' : 'blue-text']">0%</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="props.sideStatus || props.selectedCategory === '拍摄执行'"
|
||||
:class="[
|
||||
'module module-right-middle module-vertical',
|
||||
{ active: props.selectedCategory === '拍摄执行' }
|
||||
]"
|
||||
@click="emit('select', '拍摄执行')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'module-text blue-text',
|
||||
props.status ? 'text-white' : 'text-[#969696e6_!important]'
|
||||
]"
|
||||
>
|
||||
拍摄执行
|
||||
</div>
|
||||
<div :class="['module-percent', props.status ? 'red-text' : 'blue-text']">0%</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="props.sideStatus || props.selectedCategory === '后期制作'"
|
||||
:class="[
|
||||
'module module-right-upper module-vertical',
|
||||
{ active: props.selectedCategory === '后期制作' }
|
||||
]"
|
||||
@click="emit('select', '后期制作')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'module-text blue-text',
|
||||
props.status ? 'text-white' : 'text-[#969696e6_!important]'
|
||||
]"
|
||||
>
|
||||
后期制作
|
||||
</div>
|
||||
<div :class="['module-percent', props.status ? 'red-text' : 'blue-text']">0%</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="props.sideStatus || props.selectedCategory === '版权伦理意识'"
|
||||
:class="[
|
||||
'module module-right-far module-vertical',
|
||||
{ active: props.selectedCategory === '版权伦理意识' }
|
||||
]"
|
||||
@click="emit('select', '版权伦理意识')"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'module-text blue-text',
|
||||
props.status ? 'text-white' : 'text-[#969696e6_!important]'
|
||||
]"
|
||||
>
|
||||
版权伦理意识
|
||||
</div>
|
||||
<div :class="['module-percent', props.status ? 'red-text' : 'blue-text']">0%</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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 {
|
||||
font-size: 36px;
|
||||
letter-spacing: 8px;
|
||||
}
|
||||
|
||||
.module-percent {
|
||||
color: #ffffff;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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.active {
|
||||
transform: scale(1.15);
|
||||
filter: brightness(1.3);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.module.active .module-text {
|
||||
text-shadow:
|
||||
0 0 4px rgba(255, 255, 255, 1),
|
||||
0 0 10px rgba(79, 241, 255, 1),
|
||||
0 0 20px rgba(79, 241, 255, 0.9),
|
||||
0 0 40px rgba(0, 199, 255, 0.8);
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
const props = defineProps<{
|
||||
positions: { top: string; left: string }[]
|
||||
displayedIcons: string[]
|
||||
sideStatus: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ (e: 'iconClick', icon: string): void }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ImgButton
|
||||
v-for="(icon, index) in props.displayedIcons"
|
||||
:key="index"
|
||||
@click="emit('iconClick', icon)"
|
||||
class="absolute z-10"
|
||||
:style="{ top: props.positions[index]?.top, left: props.positions[index]?.left }"
|
||||
:image="icon"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ImgButton } from '@/components/Button'
|
||||
|
||||
const props = defineProps<{ moduleName: string }>()
|
||||
const emit = defineEmits<{ (e: 'prev'): void; (e: 'next'): void }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ImgButton
|
||||
@click="emit('prev')"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2"
|
||||
:image="'skill/左滑'"
|
||||
/>
|
||||
|
||||
<ImgButton
|
||||
@click="emit('next')"
|
||||
class="absolute right-0 top-1/2 -translate-y-1/2"
|
||||
:image="'skill/右滑'"
|
||||
/>
|
||||
|
||||
<div class="absolute bottom-28 left-1/2 -translate-x-1/2 z-10">
|
||||
<span class="blue-text tracking-[18px] text-5xl">{{ props.moduleName }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
@@ -4,26 +4,103 @@ import type { ResourceItem } from '../resourceData'
|
||||
const props = defineProps<{
|
||||
resources: ResourceItem[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'resourceClick', resource: ResourceItem): void
|
||||
}>()
|
||||
|
||||
const handleResourceClick = (resource: ResourceItem) => {
|
||||
emit('resourceClick', resource)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto">
|
||||
<div v-if="props.resources.length === 0" class="text-white text-2xl text-center mt-10">
|
||||
<div
|
||||
class="w-full h-full overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-white/30 scrollbar-track-transparent"
|
||||
>
|
||||
<div
|
||||
v-if="props.resources.length === 0"
|
||||
class="text-white text-2xl text-center mt-10 bg-black/40 backdrop-blur-sm rounded-2xl p-8"
|
||||
>
|
||||
暂无资源
|
||||
</div>
|
||||
<div v-else class="space-y-4">
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
v-for="resource in props.resources"
|
||||
:key="resource.id"
|
||||
class="text-white text-2xl p-4 bg-blue-500/20 rounded cursor-pointer hover:bg-blue-500/30 transition"
|
||||
class="group relative overflow-hidden rounded-2xl cursor-pointer transition-all duration-300 hover:scale-[1.02] hover:shadow-2xl"
|
||||
@click="handleResourceClick(resource)"
|
||||
>
|
||||
<div class="font-bold">{{ resource.title }}</div>
|
||||
<div v-if="resource.description" class="text-lg text-gray-300 mt-2">
|
||||
{{ resource.description }}
|
||||
<!-- 背景层 - 使用柔和的半透明渐变 -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-white/70 via-blue-50/60 to-cyan-50/60 backdrop-blur-md"
|
||||
></div>
|
||||
|
||||
<!-- 左侧装饰条 -->
|
||||
<div
|
||||
class="absolute left-0 top-0 bottom-0 w-1.5 bg-gradient-to-b from-cyan-400 via-blue-500 to-purple-500 group-hover:w-2 transition-all duration-300"
|
||||
></div>
|
||||
|
||||
<!-- 内容层 -->
|
||||
<div class="relative px-6 py-4">
|
||||
<!-- 标题 -->
|
||||
<div
|
||||
class="font-bold text-3xl text-gray-800 group-hover:text-blue-600 transition-colors duration-300 mb-2"
|
||||
>
|
||||
{{ resource.title }}
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div v-if="resource.description" class="text-2xl text-gray-600 leading-relaxed mb-3">
|
||||
{{ resource.description }}
|
||||
</div>
|
||||
|
||||
<!-- 标签信息 -->
|
||||
<div class="flex items-center gap-3 mt-2 text-2xl">
|
||||
<span class="px-3 py-1 rounded-full bg-blue-100 text-blue-700 font-medium">
|
||||
{{ resource.moduleName }}
|
||||
</span>
|
||||
<span class="px-3 py-1 rounded-full bg-purple-100 text-purple-700 font-medium">
|
||||
{{ resource.tagName }}
|
||||
</span>
|
||||
<span
|
||||
v-if="resource.subTagName"
|
||||
class="px-3 py-1 rounded-full bg-green-100 text-green-700 font-medium"
|
||||
>
|
||||
{{ resource.subTagName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬停时的光效 -->
|
||||
<div
|
||||
class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 自定义滚动条样式 */
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { getTagColor } from '@/utils'
|
||||
|
||||
interface SearchTag {
|
||||
type: 'module' | 'tag' | 'subTag'
|
||||
name: string
|
||||
@@ -7,6 +9,26 @@ interface SearchTag {
|
||||
const props = defineProps<{
|
||||
tags: SearchTag[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'remove', type: 'module' | 'tag' | 'subTag'): void
|
||||
}>()
|
||||
|
||||
const handleRemove = (type: 'module' | 'tag' | 'subTag') => {
|
||||
emit('remove', type)
|
||||
}
|
||||
|
||||
// 获取标签的背景颜色类
|
||||
const getTagBgClass = (tag: SearchTag) => {
|
||||
if (tag.type === 'module') {
|
||||
return 'bg-blue-500/80'
|
||||
} else if (tag.type === 'tag') {
|
||||
// 使用 getTagColor 获取标签颜色
|
||||
return getTagColor(tag.name)
|
||||
} else {
|
||||
return 'bg-purple-500/80'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -14,14 +36,18 @@ const props = defineProps<{
|
||||
<div
|
||||
v-for="(tag, index) in props.tags"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-3 py-1 rounded-full text-white text-base font-medium whitespace-nowrap border border-white/80"
|
||||
:class="{
|
||||
'bg-blue-500/80': tag.type === 'module',
|
||||
'bg-orange-500/80': tag.type === 'tag',
|
||||
'bg-purple-500/80': tag.type === 'subTag'
|
||||
}"
|
||||
class="inline-flex items-center gap-2 px-3 py-1 rounded-full text-white text-base font-medium whitespace-nowrap border border-white/80 group hover:border-white transition-all"
|
||||
:class="getTagBgClass(tag)"
|
||||
>
|
||||
{{ tag.name }}
|
||||
<span>{{ tag.name }}</span>
|
||||
<!-- 删除按钮 -->
|
||||
<button
|
||||
@click="handleRemove(tag.type)"
|
||||
class="flex items-center justify-center w-4 h-4 rounded-full bg-white/20 hover:bg-white/40 transition-all"
|
||||
title="删除"
|
||||
>
|
||||
<span class="text-xs leading-none">×</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getSubTagsByTag,
|
||||
getTagResourceCount
|
||||
} from '../resourceData'
|
||||
import { getStaticImage, getTagColor } from '@/utils'
|
||||
|
||||
interface Props {
|
||||
selectedModuleId: string | null
|
||||
@@ -32,6 +33,54 @@ const displayedModules = computed(() => {
|
||||
return allModules.value
|
||||
})
|
||||
|
||||
// 获取所有要显示的标签(按标签名称去重,并按固定顺序排列)
|
||||
const displayedTags = computed(() => {
|
||||
const tagMap = new Map<string, { moduleId: string; tagId: string; tagName: string }>()
|
||||
|
||||
displayedModules.value.forEach(module => {
|
||||
const tags = getTagsByModule(module.id)
|
||||
tags.forEach(tag => {
|
||||
// 使用标签名称作为唯一键,避免同名标签重复显示
|
||||
if (!tagMap.has(tag.name)) {
|
||||
tagMap.set(tag.name, {
|
||||
moduleId: module.id,
|
||||
tagId: tag.id,
|
||||
tagName: tag.name
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 定义固定的标签顺序(与位置数组对应)
|
||||
const tagOrder = [
|
||||
'视觉素养',
|
||||
'摄影基础',
|
||||
'剪映基础操作',
|
||||
'内容策划',
|
||||
'拍摄执行',
|
||||
'后期制作',
|
||||
'AI与版权伦理知识',
|
||||
'思路提点与案例赏析'
|
||||
]
|
||||
|
||||
// 按照固定顺序返回标签
|
||||
const orderedTags: Array<{ moduleId: string; tagId: string; tagName: string }> = []
|
||||
tagOrder.forEach(tagName => {
|
||||
if (tagMap.has(tagName)) {
|
||||
orderedTags.push(tagMap.get(tagName)!)
|
||||
}
|
||||
})
|
||||
|
||||
// 添加任何不在固定顺序中的标签
|
||||
tagMap.forEach((tag, tagName) => {
|
||||
if (!tagOrder.includes(tagName)) {
|
||||
orderedTags.push(tag)
|
||||
}
|
||||
})
|
||||
|
||||
return orderedTags
|
||||
})
|
||||
|
||||
// 检查标签是否展开
|
||||
const isTagExpanded = (tagId: string) => {
|
||||
return props.expandedTags.has(tagId)
|
||||
@@ -39,42 +88,57 @@ const isTagExpanded = (tagId: string) => {
|
||||
|
||||
// 检查标签是否选中
|
||||
const isTagSelected = (moduleId: string, tagId: string) => {
|
||||
return props.selectedModuleId === moduleId && props.selectedTagId === tagId
|
||||
if (props.selectedModuleId) {
|
||||
// 模块模式:需要模块和标签都匹配
|
||||
return props.selectedModuleId === moduleId && props.selectedTagId === tagId
|
||||
} else {
|
||||
// 总览模式:只需要标签匹配(因为总览模式下 selectedModuleId 为 null)
|
||||
return props.selectedTagId === tagId
|
||||
}
|
||||
}
|
||||
|
||||
// 检查子标签是否选中
|
||||
const isSubTagSelected = (moduleId: string, tagId: string, subTagId: string) => {
|
||||
return (
|
||||
props.selectedModuleId === moduleId &&
|
||||
props.selectedTagId === tagId &&
|
||||
props.selectedSubTagId === subTagId
|
||||
)
|
||||
if (props.selectedModuleId) {
|
||||
// 模块模式:需要模块、标签和子标签都匹配
|
||||
return (
|
||||
props.selectedModuleId === moduleId &&
|
||||
props.selectedTagId === tagId &&
|
||||
props.selectedSubTagId === subTagId
|
||||
)
|
||||
} else {
|
||||
// 总览模式:只需要标签和子标签匹配
|
||||
return props.selectedTagId === tagId && props.selectedSubTagId === subTagId
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签的资源数量
|
||||
const getResourceCount = (moduleId: string, tagId: string) => {
|
||||
return getTagResourceCount(moduleId, tagId)
|
||||
// 获取标签的资源数量(如果显示所有模块,则合并同名标签的数量)
|
||||
const getResourceCount = (moduleId: string, tagId: string, tagName: string) => {
|
||||
if (props.selectedModuleId) {
|
||||
// 如果选中了模块,只计算该模块的资源数量
|
||||
return getTagResourceCount(moduleId, tagId)
|
||||
} else {
|
||||
// 如果显示所有模块,计算所有模块中同名标签的资源总数
|
||||
let totalCount = 0
|
||||
displayedModules.value.forEach(module => {
|
||||
const tags = getTagsByModule(module.id)
|
||||
tags.forEach(tag => {
|
||||
if (tag.name === tagName) {
|
||||
totalCount += getTagResourceCount(module.id, tag.id)
|
||||
}
|
||||
})
|
||||
})
|
||||
return totalCount
|
||||
}
|
||||
}
|
||||
|
||||
// 获取标签颜色类 - 根据标签类型返回不同的颜色
|
||||
// 获取标签颜色类 - 根据标签类型返回不同的颜色(匹配目标效果图)
|
||||
const getTagColorClass = (tagName: string, isSelected: boolean) => {
|
||||
if (isSelected) {
|
||||
return 'bg-orange-500 border-white text-white'
|
||||
return getTagColor(tagName) + ' brightness-125'
|
||||
}
|
||||
|
||||
// 根据标签名称返回不同的颜色
|
||||
const colorMap: Record<string, string> = {
|
||||
后期制作: 'bg-blue-400 border-blue-300 text-white',
|
||||
拍摄执行: 'bg-green-400 border-green-300 text-white',
|
||||
内容策划: 'bg-orange-400 border-orange-300 text-white',
|
||||
视觉素养: 'bg-orange-400 border-orange-300 text-white',
|
||||
摄影基础: 'bg-blue-400 border-blue-300 text-white',
|
||||
剪映基础操作: 'bg-purple-400 border-purple-300 text-white',
|
||||
AI与版权伦理知识: 'bg-blue-400 border-blue-300 text-white',
|
||||
思路提点与案例赏析: 'bg-blue-400 border-blue-300 text-white'
|
||||
}
|
||||
|
||||
return colorMap[tagName] || 'bg-blue-400 border-blue-300 text-white'
|
||||
return getTagColor(tagName)
|
||||
}
|
||||
|
||||
const handleTagClick = (moduleId: string, tagId: string) => {
|
||||
@@ -85,166 +149,206 @@ const handleSubTagClick = (moduleId: string, tagId: string, subTagId: string) =>
|
||||
emit('subTagClick', moduleId, tagId, subTagId)
|
||||
}
|
||||
|
||||
// 计算标签节点的位置 - 使用更自然的树形分布
|
||||
const getTagPosition = (
|
||||
moduleIndex: number,
|
||||
tagIndex: number,
|
||||
totalTags: number,
|
||||
moduleId: string
|
||||
) => {
|
||||
// 计算所有标签的总数(用于全局定位)
|
||||
const modules = displayedModules.value
|
||||
let globalTagIndex = 0
|
||||
for (let i = 0; i < moduleIndex; i++) {
|
||||
globalTagIndex += getTagsByModule(modules[i].id).length
|
||||
// 获取子标签的图标
|
||||
const getSubTagIcon = (subTagName: string) => {
|
||||
// 图标文件名映射
|
||||
const iconMap: Record<string, string> = {
|
||||
构图: 'skill/icon/构图.png',
|
||||
光线: 'skill/icon/光线.png',
|
||||
运镜: 'skill/icon/运镜.png',
|
||||
设备准备: 'skill/icon/设备准备.png',
|
||||
脚本制作: 'skill/icon/脚本与分镜制作.png',
|
||||
光线运用: 'skill/icon/光线运用.png',
|
||||
运镜技巧: 'skill/icon/运镜技巧.png',
|
||||
选题与构思: 'skill/icon/选题与构思.png',
|
||||
脚本与分镜制作: 'skill/icon/脚本与分镜制作.png',
|
||||
拍摄技巧: 'skill/icon/拍摄技巧.png',
|
||||
剪辑技巧: 'skill/icon/剪辑技巧.png',
|
||||
包装: 'skill/icon/包装.png',
|
||||
调色: 'skill/icon/调色.png',
|
||||
音效: 'skill/icon/音效.png',
|
||||
素材管理: 'skill/icon/素材管理.png',
|
||||
蒙太奇剪辑: 'skill/icon/蒙太奇剪辑.png',
|
||||
版权意识: 'skill/icon/版权意识.png',
|
||||
ai技术应用: 'skill/icon/ai技术应用.png',
|
||||
思路提点: 'skill/icon/思路提点.png',
|
||||
案例合集: 'skill/icon/案例合集.png'
|
||||
}
|
||||
globalTagIndex += tagIndex
|
||||
return iconMap[subTagName] || 'skill/icon/构图.png' // 默认图标
|
||||
}
|
||||
|
||||
// 计算总标签数
|
||||
const totalAllTags = modules.reduce((sum, m) => sum + getTagsByModule(m.id).length, 0)
|
||||
// 获取技能树标题
|
||||
const getTreeTitle = () => {
|
||||
if (!props.selectedModuleId) {
|
||||
return '技能树总览'
|
||||
}
|
||||
const module = allModules.value.find(m => m.id === props.selectedModuleId)
|
||||
return module?.name || '技能树总览'
|
||||
}
|
||||
|
||||
// 垂直位置:从树根向上分布,形成树形结构
|
||||
const baseTop = 85 // 树根位置(底部)
|
||||
const maxTop = 15 // 最顶部位置
|
||||
const verticalRange = baseTop - maxTop
|
||||
// 计算标签节点的位置 - 根据目标效果图1的布局
|
||||
const getTagPosition = (tagIndex: number) => {
|
||||
// 定义所有标签的具体位置(根据目标效果图1)
|
||||
// 标签顺序:视觉素养、摄影基础、剪映基础操作、内容策划、拍摄执行、后期制作、AI与版权伦理知识、思路提点与案例赏析
|
||||
const positions = [
|
||||
{ top: 38, left: 33 }, // 0: 视觉素养 - 顶部左侧
|
||||
{ top: 51, left: 10 }, // 1: 摄影基础 - 顶部右侧偏左
|
||||
{ top: 51, left: 58 }, // 2: 剪映基础操作 - 顶部右侧
|
||||
{ top: 32, left: 4 }, // 3: 内容策划 - 中部左侧
|
||||
{ top: 18, left: 14 }, // 4: 拍摄执行 - 中部右侧(部分在外)
|
||||
{ top: 5, left: 35 }, // 5: 后期制作 - 中下部左侧
|
||||
{ top: 22, left: 48 }, // 6: AI与版权伦理知识 - 中下部中间
|
||||
{ top: 35, left: 64 } // 7: 思路提点与案例赏析 - 中下部右侧
|
||||
]
|
||||
|
||||
// 根据全局索引计算垂直位置,形成扇形分布
|
||||
const verticalRatio = totalAllTags > 1 ? globalTagIndex / (totalAllTags - 1) : 0
|
||||
const top = baseTop - verticalRange * verticalRatio * 0.75 // 使用0.75让标签更集中在树的上半部分
|
||||
|
||||
// 水平位置:左右交替分布,形成树形
|
||||
const isLeft = globalTagIndex % 2 === 0
|
||||
const horizontalSpread = 28 // 水平展开范围
|
||||
const horizontalOffset = isLeft ? -horizontalSpread : horizontalSpread
|
||||
|
||||
// 根据垂直位置调整水平偏移,形成扇形(越往上,水平偏移越小)
|
||||
const verticalFactor = Math.max(0, (top - baseTop) / (maxTop - baseTop))
|
||||
const left = 50 + horizontalOffset * (1 - verticalFactor * 0.4)
|
||||
// 如果标签数量超过预定义位置,使用循环
|
||||
const position = positions[tagIndex % positions.length]
|
||||
|
||||
return {
|
||||
top: `${Math.max(maxTop, Math.min(baseTop, top))}%`,
|
||||
left: `${Math.max(5, Math.min(95, left))}%`
|
||||
top: `${position.top}%`,
|
||||
left: `${position.left}%`
|
||||
}
|
||||
}
|
||||
|
||||
// 计算连接线的起点和终点
|
||||
const getConnectionLine = (
|
||||
moduleIndex: number,
|
||||
tagIndex: number,
|
||||
totalTags: number,
|
||||
moduleId: string
|
||||
) => {
|
||||
const tagPos = getTagPosition(moduleIndex, tagIndex, totalTags, moduleId)
|
||||
const rootX = 50 // 树根中心X位置
|
||||
const rootY = 92 // 树根中心Y位置(底部)
|
||||
// 计算子标签围绕父标签的位置(圆形分布)
|
||||
const getSubTagPosition = (subTagIndex: number, totalSubTags: number) => {
|
||||
// 圆形半径(单位:像素)- 根据子标签数量调整半径
|
||||
// 使用更大的半径确保子标签不会挤在一起
|
||||
const baseRadius = 160 // 基础半径180px
|
||||
const radius = totalSubTags <= 3 ? baseRadius : baseRadius + (totalSubTags - 3) * 30
|
||||
|
||||
// 计算角度:均匀分布在360度圆周上
|
||||
// 从顶部开始(-90度),顺时针分布
|
||||
const angleStep = (360 / totalSubTags) * (Math.PI / 180) // 转换为弧度
|
||||
const angle = -Math.PI / 2 + subTagIndex * angleStep // -90度开始
|
||||
|
||||
// 计算子标签的位置(相对于父标签中心,使用像素单位)
|
||||
const offsetX = radius * Math.cos(angle)
|
||||
const offsetY = radius * Math.sin(angle)
|
||||
|
||||
return {
|
||||
x1: rootX,
|
||||
y1: rootY,
|
||||
x2: parseFloat(tagPos.left),
|
||||
y2: parseFloat(tagPos.top) + 3 // 标签中心位置
|
||||
transform: `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY + 30}px))` // 让子标签以自身中心定位,并应用偏移
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<div class="relative w-full h-full select-none">
|
||||
<!-- 技能树背景容器 -->
|
||||
<div class="absolute inset-0">
|
||||
<!-- 树根节点 - 底部居中 -->
|
||||
<!-- 模块名称 - 底部居中 -->
|
||||
<div
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 px-8 py-4 bg-gradient-to-r from-blue-500/90 to-blue-400/90 border-2 border-blue-300 rounded-xl text-white text-2xl font-bold shadow-2xl shadow-blue-500/60 z-10"
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 text-white text-5xl font-bold [text-shadow:_0px_0px_20px_#3fcdd6,_0px_0px_20px_#3fcdd6,_0px_0px_20px_#3fcdd6,_0px_0px_20px_#3fcdd6] z-30"
|
||||
>
|
||||
技能树总览
|
||||
{{ getTreeTitle() }}
|
||||
</div>
|
||||
|
||||
<!-- 连接线容器 -->
|
||||
<svg class="absolute inset-0 pointer-events-none z-0" style="overflow: visible">
|
||||
<template v-for="(module, moduleIndex) in displayedModules" :key="`lines-${module.id}`">
|
||||
<template v-for="(tag, tagIndex) in getTagsByModule(module.id)" :key="`line-${tag.id}`">
|
||||
<line
|
||||
:x1="`${getConnectionLine(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id).x1}%`"
|
||||
:y1="`${getConnectionLine(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id).y1}%`"
|
||||
:x2="`${getConnectionLine(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id).x2}%`"
|
||||
:y2="`${getConnectionLine(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id).y2}%`"
|
||||
stroke="rgba(255, 255, 255, 0.4)"
|
||||
stroke-width="2"
|
||||
class="drop-shadow-sm"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</svg>
|
||||
|
||||
<!-- 模块和标签节点 -->
|
||||
<template v-for="(module, moduleIndex) in displayedModules" :key="module.id">
|
||||
<template v-for="(tag, tagIndex) in getTagsByModule(module.id)" :key="tag.id">
|
||||
<!-- 标签节点 -->
|
||||
<div
|
||||
class="absolute z-10"
|
||||
:style="
|
||||
getTagPosition(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id)
|
||||
"
|
||||
>
|
||||
<!-- 资源标签节点 -->
|
||||
<div
|
||||
:class="[
|
||||
'relative px-4 py-2.5 rounded-lg border-2 cursor-pointer transition-all shadow-lg hover:scale-110 min-w-[140px] max-w-[180px]',
|
||||
getTagColorClass(tag.name, isTagSelected(module.id, tag.id)),
|
||||
isTagSelected(module.id, tag.id) ? 'ring-2 ring-orange-300 z-20' : ''
|
||||
]"
|
||||
@click="handleTagClick(module.id, tag.id)"
|
||||
>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-sm font-semibold truncate">{{ tag.name }}</span>
|
||||
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||
<span
|
||||
class="flex items-center justify-center w-5 h-5 bg-white/30 rounded-full text-white text-xs font-bold"
|
||||
<template v-for="(tag, tagIndex) in displayedTags" :key="`${tag.moduleId}-${tag.tagId}`">
|
||||
<!-- 标签节点容器 - 选中的标签提高 z-index 显示在蒙版之上 -->
|
||||
<div
|
||||
class="absolute"
|
||||
:class="isTagSelected(tag.moduleId, tag.tagId) ? 'z-20' : 'z-[1]'"
|
||||
:style="getTagPosition(tagIndex)"
|
||||
>
|
||||
<!-- 资源标签节点 -->
|
||||
<div class="relative">
|
||||
<!-- 标签主体容器 - 用于精确对齐定位层 -->
|
||||
<div class="relative">
|
||||
<!-- 标签主体 -->
|
||||
<div
|
||||
:class="[
|
||||
'relative py-5 cursor-pointer min-w-[240px] max-w-[220px] transition-all hover:scale-105',
|
||||
'border-4 rounded-full shadow-lg',
|
||||
getTagColorClass(tag.tagName, isTagSelected(tag.moduleId, tag.tagId)),
|
||||
isTagSelected(tag.moduleId, tag.tagId) ? 'ring-2 ring-white/60' : ''
|
||||
]"
|
||||
@click="handleTagClick(tag.moduleId, tag.tagId)"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-2 relative z-10">
|
||||
<span class="text-2xl font-bold truncate">{{ tag.tagName }}</span>
|
||||
<!-- 展开箭头 -->
|
||||
<!-- <span
|
||||
v-if="getSubTagsByTag(tag.moduleId, tag.tagId).length > 0"
|
||||
class="text-white text-xs flex-shrink-0"
|
||||
>
|
||||
{{ getResourceCount(module.id, tag.id) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="getSubTagsByTag(module.id, tag.id).length > 0"
|
||||
class="text-white text-xs"
|
||||
{{ isTagExpanded(tag.tagId) ? '▼' : '▶' }}
|
||||
</span> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子标签定位层 - 使用 inset-0 与标签主体完全对齐(包括 border 和 padding) -->
|
||||
<div
|
||||
v-if="
|
||||
getSubTagsByTag(tag.moduleId, tag.tagId).length > 0 && isTagExpanded(tag.tagId)
|
||||
"
|
||||
class="absolute inset-0 pointer-events-none"
|
||||
>
|
||||
<!-- 子标签列表(围绕父标签圆形分布) -->
|
||||
<div
|
||||
v-for="(subTag, subTagIndex) in getSubTagsByTag(tag.moduleId, tag.tagId)"
|
||||
:key="subTag.id"
|
||||
class="absolute z-30 pointer-events-auto"
|
||||
style="top: 50%; left: 50%"
|
||||
:style="
|
||||
getSubTagPosition(subTagIndex, getSubTagsByTag(tag.moduleId, tag.tagId).length)
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'flex flex-col items-center justify-center cursor-pointer transition-all hover:scale-105'
|
||||
// isSubTagSelected(tag.moduleId, tag.tagId, subTag.id)
|
||||
// ? 'border-orange-400 ring-2 ring-orange-300'
|
||||
// : 'border-white/50'
|
||||
]"
|
||||
@click.stop="handleSubTagClick(tag.moduleId, tag.tagId, subTag.id)"
|
||||
>
|
||||
{{ isTagExpanded(tag.id) ? '▼' : '▶' }}
|
||||
</span>
|
||||
<!-- 图标-图标资源已经带了文字 -->
|
||||
<div class="w-30">
|
||||
<img :src="getStaticImage(getSubTagIcon(subTag.name))" :alt="subTag.name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 子标签列表(展开时显示) -->
|
||||
<!-- 数字徽章 - 右上角外侧 -->
|
||||
<div
|
||||
v-if="getSubTagsByTag(module.id, tag.id).length > 0 && isTagExpanded(tag.id)"
|
||||
class="absolute top-full left-0 mt-2 space-y-1.5 z-30"
|
||||
class="absolute -top-4 -right-4 flex items-center justify-center w-12 h-12 bg-[#5287c1] rounded-full text-white text-3xl font-bold shadow-lg"
|
||||
>
|
||||
<div
|
||||
v-for="subTag in getSubTagsByTag(module.id, tag.id)"
|
||||
:key="subTag.id"
|
||||
:class="[
|
||||
'px-3 py-1.5 rounded-lg border-2 cursor-pointer transition-all shadow-md hover:scale-105 whitespace-nowrap',
|
||||
isSubTagSelected(module.id, tag.id, subTag.id)
|
||||
? 'bg-orange-400 border-white text-white ring-1 ring-orange-300'
|
||||
: 'bg-blue-300/80 border-blue-200 text-white'
|
||||
]"
|
||||
@click.stop="handleSubTagClick(module.id, tag.id, subTag.id)"
|
||||
>
|
||||
<span class="text-xs font-medium">{{ subTag.name }}</span>
|
||||
</div>
|
||||
{{ getResourceCount(tag.moduleId, tag.tagId, tag.tagName) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 右侧标签 -->
|
||||
<div class="absolute right-0 top-1/2 -translate-y-1/2 -translate-x-4 z-20">
|
||||
<!-- 点击之后的蒙版 - 使用径向渐变,从中心透明到边缘半透明黑色 -->
|
||||
<transition
|
||||
enter-active-class="transition-opacity duration-300 ease-out"
|
||||
leave-active-class="transition-opacity duration-300 ease-in"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="writing-vertical text-white text-2xl font-bold bg-blue-500/40 px-3 py-6 rounded-lg border border-blue-300/60 shadow-lg"
|
||||
>
|
||||
<span class="inline-block mb-2">●</span>
|
||||
<span>技能树</span>
|
||||
</div>
|
||||
</div>
|
||||
v-if="props.selectedTagId"
|
||||
class="absolute inset-0 z-10 pointer-events-auto"
|
||||
style="
|
||||
background: radial-gradient(
|
||||
circle at center,
|
||||
rgba(0, 0, 0, 0.5) 0%,
|
||||
rgba(0, 0, 0, 0.4) 20%,
|
||||
rgba(0, 0, 0, 0.3) 40%,
|
||||
transparent 70%
|
||||
);
|
||||
"
|
||||
></div>
|
||||
</transition>
|
||||
|
||||
<!-- 右侧标签 -->
|
||||
<img class="absolute right-4 top-0 z-30" :src="getStaticImage('skill/技能树标题.png')" alt="" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+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>
|
||||
+212
-49
@@ -13,11 +13,16 @@ export interface ResourceItem {
|
||||
subTagName?: string
|
||||
// 资源数量(用于技能树显示)
|
||||
resourceCount?: number
|
||||
// 资源类型和链接
|
||||
type: 'video' | 'page' | 'image' // video: 视频播放, page: 页面跳转, image: 图文展示
|
||||
videoUrl?: string // 视频URL(type为video时使用)
|
||||
routeName?: string // 路由名称(type为page时使用)
|
||||
images?: string[] // 图片URL数组(type为image时使用)
|
||||
}
|
||||
|
||||
// 一维数组格式的资源数据
|
||||
export const resourceData: ResourceItem[] = [
|
||||
// 新手导航模块
|
||||
// 新手导航模块 - 保持原有标签
|
||||
{
|
||||
id: 'res-1',
|
||||
title: '九种构图法',
|
||||
@@ -27,7 +32,9 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-1-1',
|
||||
tagName: '视觉素养',
|
||||
subTagId: 'subtag-1-1-1',
|
||||
subTagName: '构图'
|
||||
subTagName: '构图',
|
||||
type: 'video',
|
||||
videoUrl: '/video/构图法.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-2',
|
||||
@@ -38,7 +45,9 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-1-1',
|
||||
tagName: '视觉素养',
|
||||
subTagId: 'subtag-1-1-2',
|
||||
subTagName: '光线'
|
||||
subTagName: '光线',
|
||||
type: 'video',
|
||||
videoUrl: '/video/光线运用.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-3',
|
||||
@@ -49,7 +58,9 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-1-1',
|
||||
tagName: '视觉素养',
|
||||
subTagId: 'subtag-1-1-3',
|
||||
subTagName: '运镜'
|
||||
subTagName: '运镜',
|
||||
type: 'video',
|
||||
videoUrl: '/video/运镜技巧.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-4',
|
||||
@@ -58,7 +69,9 @@ export const resourceData: ResourceItem[] = [
|
||||
moduleId: 'module-1',
|
||||
moduleName: '新手导航',
|
||||
tagId: 'tag-1-2',
|
||||
tagName: '摄影基础'
|
||||
tagName: '摄影基础',
|
||||
type: 'image',
|
||||
images: ['/images/skill/摄影基础1.png', '/images/skill/摄影基础2.png']
|
||||
},
|
||||
{
|
||||
id: 'res-5',
|
||||
@@ -67,33 +80,53 @@ export const resourceData: ResourceItem[] = [
|
||||
moduleId: 'module-1',
|
||||
moduleName: '新手导航',
|
||||
tagId: 'tag-1-3',
|
||||
tagName: '剪映基础操作'
|
||||
tagName: '剪映基础操作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/剪映入门.mp4'
|
||||
},
|
||||
// vlog发现与记录模块
|
||||
|
||||
// vlog发现与记录模块 - 使用统一标签体系
|
||||
{
|
||||
id: 'res-6',
|
||||
title: '设备准备指南',
|
||||
description: 'vlog拍摄设备准备',
|
||||
title: 'vlog设备准备',
|
||||
description: 'vlog拍摄设备准备指南',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-2-1-1',
|
||||
subTagName: '设备准备'
|
||||
subTagName: '设备准备',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog设备准备.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-7',
|
||||
title: 'vlog选题构思',
|
||||
description: 'vlog选题与创意构思',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-2-1-2',
|
||||
subTagName: '选题与构思',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog选题构思.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-8',
|
||||
title: 'vlog脚本撰写',
|
||||
description: '如何撰写vlog脚本',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-2-1-2',
|
||||
subTagName: '脚本制作'
|
||||
subTagId: 'subtag-2-1-3',
|
||||
subTagName: '脚本与分镜制作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog脚本撰写.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-8',
|
||||
id: 'res-9',
|
||||
title: 'vlog构图技巧',
|
||||
description: 'vlog拍摄构图方法',
|
||||
moduleId: 'module-2',
|
||||
@@ -101,10 +134,12 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-2-2',
|
||||
tagName: '拍摄执行',
|
||||
subTagId: 'subtag-2-2-1',
|
||||
subTagName: '构图'
|
||||
subTagName: '构图',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog构图技巧.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-9',
|
||||
id: 'res-10',
|
||||
title: 'vlog光线控制',
|
||||
description: 'vlog拍摄光线运用',
|
||||
moduleId: 'module-2',
|
||||
@@ -112,10 +147,12 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-2-2',
|
||||
tagName: '拍摄执行',
|
||||
subTagId: 'subtag-2-2-2',
|
||||
subTagName: '光线运用'
|
||||
subTagName: '光线运用',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog光线控制.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-10',
|
||||
id: 'res-11',
|
||||
title: 'vlog运镜方法',
|
||||
description: 'vlog拍摄运镜技巧',
|
||||
moduleId: 'module-2',
|
||||
@@ -123,52 +160,164 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-2-2',
|
||||
tagName: '拍摄执行',
|
||||
subTagId: 'subtag-2-2-3',
|
||||
subTagName: '运镜技巧'
|
||||
subTagName: '运镜技巧',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog运镜方法.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-11',
|
||||
title: 'vlog后期剪辑',
|
||||
description: 'vlog视频后期制作',
|
||||
id: 'res-12',
|
||||
title: 'vlog剪辑技巧',
|
||||
description: 'vlog视频剪辑方法',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-3',
|
||||
tagName: '后期制作'
|
||||
tagName: '后期制作',
|
||||
subTagId: 'subtag-2-3-1',
|
||||
subTagName: '剪辑技巧',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog剪辑技巧.mp4'
|
||||
},
|
||||
// 访谈聆听与对话模块
|
||||
{
|
||||
id: 'res-12',
|
||||
id: 'res-13',
|
||||
title: 'vlog调色技巧',
|
||||
description: 'vlog视频调色方法',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-3',
|
||||
tagName: '后期制作',
|
||||
subTagId: 'subtag-2-3-2',
|
||||
subTagName: '调色',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog调色技巧.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-14',
|
||||
title: 'vlog音效处理',
|
||||
description: 'vlog音效添加与处理',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-3',
|
||||
tagName: '后期制作',
|
||||
subTagId: 'subtag-2-3-3',
|
||||
subTagName: '音效',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog音效处理.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-15',
|
||||
title: 'vlog版权规范',
|
||||
description: 'vlog创作版权注意事项',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-4',
|
||||
tagName: 'AI与版权伦理知识',
|
||||
type: 'image',
|
||||
images: [
|
||||
'/images/skill/版权规范1.png',
|
||||
'/images/skill/版权规范2.png',
|
||||
'/images/skill/版权规范3.png'
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'res-16',
|
||||
title: 'vlog案例赏析',
|
||||
description: '优秀vlog作品分析',
|
||||
moduleId: 'module-2',
|
||||
moduleName: 'vlog发现与记录',
|
||||
tagId: 'tag-2-5',
|
||||
tagName: '思路提点与案例赏析',
|
||||
type: 'video',
|
||||
videoUrl: '/video/vlog案例赏析.mp4'
|
||||
},
|
||||
|
||||
// 访谈聆听与对话模块 - 使用统一标签体系
|
||||
{
|
||||
id: 'res-17',
|
||||
title: '访谈设备准备',
|
||||
description: '访谈拍摄设备清单',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-3-1-1',
|
||||
subTagName: '设备准备',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈设备准备.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-18',
|
||||
title: '访谈选题方法',
|
||||
description: '如何选择访谈主题',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-3-1-1',
|
||||
subTagName: '选题与构思'
|
||||
subTagId: 'subtag-3-1-2',
|
||||
subTagName: '选题与构思',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈选题方法.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-13',
|
||||
id: 'res-19',
|
||||
title: '访谈脚本制作',
|
||||
description: '访谈脚本撰写技巧',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-3-1-2',
|
||||
subTagName: '脚本与分镜制作'
|
||||
subTagId: 'subtag-3-1-3',
|
||||
subTagName: '脚本与分镜制作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈脚本制作.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-14',
|
||||
id: 'res-20',
|
||||
title: '访谈拍摄技巧',
|
||||
description: '访谈视频拍摄方法',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-2',
|
||||
tagName: '拍摄执行'
|
||||
tagName: '拍摄执行',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈拍摄技巧.mp4'
|
||||
},
|
||||
// 电影创作与表达模块
|
||||
{
|
||||
id: 'res-15',
|
||||
id: 'res-21',
|
||||
title: '访谈后期剪辑',
|
||||
description: '访谈视频后期制作',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-3',
|
||||
tagName: '后期制作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈后期剪辑.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-22',
|
||||
title: '访谈版权规范',
|
||||
description: '访谈创作版权注意事项',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-4',
|
||||
tagName: 'AI与版权伦理知识',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈版权规范.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-23',
|
||||
title: '访谈案例赏析',
|
||||
description: '优秀访谈作品分析',
|
||||
moduleId: 'module-3',
|
||||
moduleName: '访谈聆听与对话',
|
||||
tagId: 'tag-3-5',
|
||||
tagName: '思路提点与案例赏析',
|
||||
type: 'video',
|
||||
videoUrl: '/video/访谈案例赏析.mp4'
|
||||
},
|
||||
|
||||
// 电影创作与表达模块 - 使用统一标签体系
|
||||
{
|
||||
id: 'res-24',
|
||||
title: '电影设备准备',
|
||||
description: '电影拍摄设备清单',
|
||||
moduleId: 'module-4',
|
||||
@@ -176,10 +325,12 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-4-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-4-1-1',
|
||||
subTagName: '设备准备'
|
||||
subTagName: '设备准备',
|
||||
type: 'page',
|
||||
routeName: 'FilmResource'
|
||||
},
|
||||
{
|
||||
id: 'res-16',
|
||||
id: 'res-25',
|
||||
title: '电影选题构思',
|
||||
description: '电影创作选题方法',
|
||||
moduleId: 'module-4',
|
||||
@@ -187,10 +338,12 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-4-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-4-1-2',
|
||||
subTagName: '选题与构思'
|
||||
subTagName: '选题与构思',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影选题构思.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-17',
|
||||
id: 'res-26',
|
||||
title: '电影脚本分镜',
|
||||
description: '电影脚本和分镜制作',
|
||||
moduleId: 'module-4',
|
||||
@@ -198,43 +351,53 @@ export const resourceData: ResourceItem[] = [
|
||||
tagId: 'tag-4-1',
|
||||
tagName: '内容策划',
|
||||
subTagId: 'subtag-4-1-3',
|
||||
subTagName: '脚本与分镜制作'
|
||||
subTagName: '脚本与分镜制作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影脚本分镜.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-18',
|
||||
id: 'res-27',
|
||||
title: '电影拍摄执行',
|
||||
description: '电影拍摄执行技巧',
|
||||
moduleId: 'module-4',
|
||||
moduleName: '电影创作与表达',
|
||||
tagId: 'tag-4-2',
|
||||
tagName: '拍摄执行'
|
||||
tagName: '拍摄执行',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影拍摄执行.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-19',
|
||||
id: 'res-28',
|
||||
title: '电影后期制作',
|
||||
description: '电影后期剪辑与特效',
|
||||
moduleId: 'module-4',
|
||||
moduleName: '电影创作与表达',
|
||||
tagId: 'tag-4-3',
|
||||
tagName: '后期制作'
|
||||
tagName: '后期制作',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影后期制作.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-20',
|
||||
title: 'AI与版权伦理',
|
||||
description: 'AI工具使用与版权规范',
|
||||
id: 'res-29',
|
||||
title: '电影AI工具应用',
|
||||
description: 'AI工具在电影创作中的应用',
|
||||
moduleId: 'module-4',
|
||||
moduleName: '电影创作与表达',
|
||||
tagId: 'tag-4-4',
|
||||
tagName: 'AI与版权伦理知识'
|
||||
tagName: 'AI与版权伦理知识',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影AI工具应用.mp4'
|
||||
},
|
||||
{
|
||||
id: 'res-21',
|
||||
title: '创作思路与案例',
|
||||
id: 'res-30',
|
||||
title: '电影创作思路与案例',
|
||||
description: '电影创作思路和案例分析',
|
||||
moduleId: 'module-4',
|
||||
moduleName: '电影创作与表达',
|
||||
tagId: 'tag-4-5',
|
||||
tagName: '思路提点与案例赏析'
|
||||
tagName: '思路提点与案例赏析',
|
||||
type: 'video',
|
||||
videoUrl: '/video/电影创作思路与案例.mp4'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// 分类配置
|
||||
interface CategoryConfig {
|
||||
icons: string[]
|
||||
positions: { top: string; left: string }[]
|
||||
}
|
||||
|
||||
// 模块配置
|
||||
interface ModuleConfig {
|
||||
name: string
|
||||
value: number
|
||||
categories: Record<string, CategoryConfig>
|
||||
}
|
||||
|
||||
// 知识卡片按钮数据结构(与 shooting/index.vue 对齐)
|
||||
export type KnowledgeBtn = {
|
||||
key: string
|
||||
name: string
|
||||
btnImage: string
|
||||
images: string[]
|
||||
desc: string
|
||||
}
|
||||
|
||||
export function useSkillState() {
|
||||
// 模块/分类/图标层级状态(仅保留必要业务状态)
|
||||
const selectedModule = ref(1)
|
||||
const selectedCategory = ref('')
|
||||
const showSubIcons = ref(false)
|
||||
const selectedPrimaryIcon = ref<string | null>(null)
|
||||
|
||||
const moduleList = ref<ModuleConfig[]>([
|
||||
{
|
||||
name: '技能树-新手启航',
|
||||
value: 1,
|
||||
categories: {
|
||||
拍摄基础: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
内容策划: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
拍摄执行: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
后期制作: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
版权伦理意识: {
|
||||
icons: [],
|
||||
positions: []
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '技能树-vlog',
|
||||
value: 2,
|
||||
categories: {
|
||||
拍摄基础: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
内容策划: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
拍摄执行: {
|
||||
icons: ['skill/画面构图未选中', 'skill/景别', 'skill/运镜技巧', 'skill/光线运用'],
|
||||
positions: [
|
||||
{ top: '36%', left: '23%' },
|
||||
{ top: '15%', left: '34%' },
|
||||
{ top: '20%', left: '45%' },
|
||||
{ top: '28%', left: '58%' }
|
||||
]
|
||||
},
|
||||
后期制作: {
|
||||
icons: [],
|
||||
positions: []
|
||||
},
|
||||
版权伦理意识: {
|
||||
icons: [],
|
||||
positions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const currentModule = computed(() => {
|
||||
return moduleList.value.find(item => item.value === selectedModule.value) || moduleList.value[0]
|
||||
})
|
||||
|
||||
const nextModule = () => {
|
||||
if (selectedModule.value === moduleList.value.length) {
|
||||
selectedModule.value = 1
|
||||
} else {
|
||||
selectedModule.value++
|
||||
}
|
||||
}
|
||||
|
||||
const prevModule = () => {
|
||||
if (selectedModule.value === 1) {
|
||||
selectedModule.value = moduleList.value.length
|
||||
} else {
|
||||
selectedModule.value--
|
||||
}
|
||||
}
|
||||
|
||||
const handleCategoryClick = (categoryName: string) => {
|
||||
selectedCategory.value = categoryName
|
||||
showSubIcons.value = false
|
||||
selectedPrimaryIcon.value = null
|
||||
}
|
||||
|
||||
const currentCategoryConfig = computed(() => {
|
||||
if (!selectedCategory.value) return null
|
||||
return currentModule.value.categories[selectedCategory.value] || null
|
||||
})
|
||||
|
||||
const displayedIcons = computed(() => {
|
||||
const cfg = currentCategoryConfig.value
|
||||
if (!cfg) return [] as string[]
|
||||
if (showSubIcons.value && selectedPrimaryIcon.value) {
|
||||
return ['skill/画面构图选中', 'skill/三分法构图', 'skill/居中构图', 'skill/对角线构图']
|
||||
}
|
||||
return cfg.icons
|
||||
})
|
||||
|
||||
type IconAction =
|
||||
| { type: 'navigate'; payload: { name: string } }
|
||||
| { type: 'expand' }
|
||||
| { type: 'openKnowledge'; payload: { name: '三分法构图' | '居中构图' | '对角线构图' } }
|
||||
| null
|
||||
|
||||
const getIconAction = (icon: string): IconAction => {
|
||||
if (icon === 'skill/画面构图未选中' && selectedCategory.value === '拍摄执行') {
|
||||
if (selectedModule.value === 1) return { type: 'navigate', payload: { name: 'SkillVdieo' } }
|
||||
if (selectedModule.value === 2) return { type: 'expand' }
|
||||
}
|
||||
if (showSubIcons.value && selectedModule.value === 2 && selectedCategory.value === '拍摄执行') {
|
||||
const map: Record<string, '三分法构图' | '居中构图' | '对角线构图'> = {
|
||||
'skill/三分法构图': '三分法构图',
|
||||
'skill/居中构图': '居中构图',
|
||||
'skill/对角线构图': '对角线构图'
|
||||
}
|
||||
if (icon in map) return { type: 'openKnowledge', payload: { name: map[icon] } }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
// module switcher
|
||||
currentModule,
|
||||
nextModule,
|
||||
prevModule,
|
||||
// category & icons
|
||||
selectedCategory,
|
||||
handleCategoryClick,
|
||||
currentCategoryConfig,
|
||||
displayedIcons,
|
||||
selectedModule,
|
||||
showSubIcons,
|
||||
selectedPrimaryIcon,
|
||||
getIconAction
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user