feat: 完善技能树功能架构与逻辑流程文档,新增视频播放与学习进度追踪功能
No files matched your search
@@ -0,0 +1,481 @@
|
||||
# 📚 Skill 功能架构完整分析
|
||||
|
||||
## 🗂️ 文件结构(已清理)
|
||||
|
||||
```
|
||||
src/views/skill/
|
||||
├── index.vue # 主页面 - 技能树资源浏览器
|
||||
├── resourceData.ts # 数据层 - 资源数据管理
|
||||
├── video.vue # 视频播放页面
|
||||
└── components/
|
||||
├── SkillTree.vue # 技能树可视化组件
|
||||
├── SearchTags.vue # 搜索面包屑标签
|
||||
└── ResourceList.vue # 资源列表展示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 核心功能定位
|
||||
|
||||
**技能树资源浏览系统** - 一个基于树形结构的学习资源导航和筛选平台
|
||||
|
||||
---
|
||||
|
||||
## 📊 数据架构
|
||||
|
||||
### 1. 数据层级结构
|
||||
|
||||
```
|
||||
模块 (Module)
|
||||
└─ 标签 (Tag)
|
||||
└─ 子标签 (SubTag)
|
||||
└─ 资源 (Resource)
|
||||
```
|
||||
|
||||
### 2. 数据实体
|
||||
|
||||
#### ResourceItem 接口
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: string // 资源唯一标识
|
||||
title: string // 资源标题
|
||||
description?: string // 资源描述
|
||||
moduleId: string // 所属模块ID
|
||||
moduleName: string // 所属模块名称
|
||||
tagId: string // 所属标签ID
|
||||
tagName: string // 所属标签名称
|
||||
subTagId?: string // 所属子标签ID(可选)
|
||||
subTagName?: string // 所属子标签名称(可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 数据统计
|
||||
|
||||
- **4个模块**:
|
||||
1. module-1: 新手导航 (5个资源)
|
||||
2. module-2: vlog发现与记录 (6个资源)
|
||||
3. module-3: 访谈聆听与对话 (3个资源)
|
||||
4. module-4: 电影创作与表达 (7个资源)
|
||||
|
||||
- **8种标签类型**:
|
||||
- 视觉素养 (橙色)
|
||||
- 摄影基础 (蓝色)
|
||||
- 剪映基础操作 (紫色)
|
||||
- 内容策划 (橙色)
|
||||
- 拍摄执行 (绿色)
|
||||
- 后期制作 (蓝色)
|
||||
- AI与版权伦理知识 (蓝色)
|
||||
- 思路提点与案例赏析 (蓝色)
|
||||
|
||||
- **总资源数**:21个学习资源
|
||||
|
||||
### 4. 工具函数
|
||||
|
||||
```typescript
|
||||
getAllResources() // 获取所有资源
|
||||
getResourcesByModule(moduleId) // 按模块筛选
|
||||
getResourcesByTag(moduleId, tagId) // 按标签筛选
|
||||
getResourcesBySubTag(moduleId, tagId, subTagId) // 按子标签筛选
|
||||
getAllModules() // 获取所有模块
|
||||
getTagsByModule(moduleId) // 获取模块的标签
|
||||
getSubTagsByTag(moduleId, tagId) // 获取标签的子标签
|
||||
getTagResourceCount(moduleId, tagId) // 获取标签的资源数量
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 布局
|
||||
|
||||
### 页面布局(1920x1080)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ [搜索框 + 面包屑] [模块1][模块2][模块3][模块4] [X] │
|
||||
│ │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────────┐ │
|
||||
│ │ │ │ │ │
|
||||
│ │ 资源列表 │ │ 技能树 │ │
|
||||
│ │ 740x800 │ │ 860x820 │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ │ [标签节点...] │ │
|
||||
│ │ [资源卡片] │ │ │ │
|
||||
│ │ [资源卡片] │ │ 技能树总览 │ │
|
||||
│ └──────────────┘ └──────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 组件定位
|
||||
|
||||
1. **搜索框**:左上角 (left-10, top-10, 547x84px)
|
||||
2. **模块图标**:顶部居中 (left-55%, top-10, 4个141x150px)
|
||||
3. **资源列表**:左下角 (left-10, bottom-20, 740x800px)
|
||||
4. **技能树**:右下角 (right-0, bottom-20, 860x820px)
|
||||
5. **关闭按钮**:右上角 (right-0, top-0)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 交互逻辑流程
|
||||
|
||||
### 1. 状态管理
|
||||
|
||||
```typescript
|
||||
// 核心状态
|
||||
selectedModuleId: string | null // 当前选中的模块
|
||||
selectedTagId: string | null // 当前选中的标签
|
||||
selectedSubTagId: string | null // 当前选中的子标签
|
||||
expandedTags: Set<string> // 展开的标签集合
|
||||
search: string // 搜索文本(待实现)
|
||||
```
|
||||
|
||||
### 2. 用户交互流程
|
||||
|
||||
#### 流程A:模块筛选
|
||||
|
||||
```
|
||||
用户点击模块图标
|
||||
↓
|
||||
handleModuleListItemClick(moduleId)
|
||||
↓
|
||||
如果已选中同一模块 → 取消选择(显示所有)
|
||||
如果选中新模块 → 设置 selectedModuleId
|
||||
↓
|
||||
清空标签和子标签选择
|
||||
↓
|
||||
技能树只显示该模块的标签
|
||||
资源列表只显示该模块的资源
|
||||
```
|
||||
|
||||
#### 流程B:标签筛选
|
||||
|
||||
```
|
||||
用户点击技能树标签
|
||||
↓
|
||||
handleTagClick(moduleId, tagId)
|
||||
↓
|
||||
如果已选中同一标签:
|
||||
- 已展开 → 折叠并取消选择
|
||||
- 未展开 → 展开子标签
|
||||
如果选中新标签:
|
||||
- 设置 selectedModuleId 和 selectedTagId
|
||||
- 如果有子标签 → 自动展开
|
||||
↓
|
||||
资源列表显示该标签的所有资源
|
||||
搜索框显示面包屑:[模块] > [标签]
|
||||
```
|
||||
|
||||
#### 流程C:子标签筛选
|
||||
|
||||
```
|
||||
用户点击子标签
|
||||
↓
|
||||
handleSubTagClick(moduleId, tagId, subTagId)
|
||||
↓
|
||||
如果已选中同一子标签 → 取消选择
|
||||
如果选中新子标签 → 设置 selectedSubTagId
|
||||
↓
|
||||
资源列表显示该子标签的资源
|
||||
搜索框显示面包屑:[模块] > [标签] > [子标签]
|
||||
```
|
||||
|
||||
### 3. 计算属性逻辑
|
||||
|
||||
#### searchTags(面包屑)
|
||||
|
||||
```typescript
|
||||
根据选择状态生成面包屑数组:
|
||||
- 选中模块 → 添加模块标签(蓝色)
|
||||
- 选中标签 → 添加标签标签(橙色)
|
||||
- 选中子标签 → 添加子标签标签(紫色)
|
||||
```
|
||||
|
||||
#### filteredResources(资源过滤)
|
||||
|
||||
```typescript
|
||||
优先级:子标签 > 标签 > 模块 > 全部
|
||||
1. 如果选中子标签 → 返回子标签的资源
|
||||
2. 否则如果选中标签 → 返回标签的资源
|
||||
3. 否则如果选中模块 → 返回模块的资源
|
||||
4. 否则 → 返回所有资源
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🌳 技能树组件详解
|
||||
|
||||
### 1. 核心功能
|
||||
|
||||
- **标签去重**:使用 `displayedTags` 计算属性,通过 `moduleId-tagId` 作为唯一键去重
|
||||
- **位置布局**:14个预定义位置,形成树形分布
|
||||
- **颜色映射**:根据标签名称返回不同颜色
|
||||
- **展开/折叠**:有子标签的标签可以展开显示子标签列表
|
||||
- **数字徽章**:显示每个标签下的资源数量
|
||||
|
||||
### 2. 标签位置分布
|
||||
|
||||
```typescript
|
||||
14个预定义位置(百分比定位):
|
||||
顶部区域(树冠):
|
||||
- (8%, 35%) - (12%, 55%) - (18%, 72%)
|
||||
中上区域:
|
||||
- (28%, 18%) - (32%, 82%)
|
||||
中部区域:
|
||||
- (42%, 10%) - (45%, 28%) - (48%, 68%)
|
||||
中下区域:
|
||||
- (58%, 22%) - (62%, 78%) - (68%, 85%)
|
||||
底部区域(树干):
|
||||
- (75%, 15%) - (78%, 38%) - (82%, 65%)
|
||||
```
|
||||
|
||||
### 3. 标签样式
|
||||
|
||||
```css
|
||||
基础样式:
|
||||
- 圆角矩形:rounded-xl
|
||||
- 内边距:px-4 py-2
|
||||
- 边框:border-2
|
||||
- 尺寸:min-w-[140px] max-w-[200px]
|
||||
- 阴影:shadow-lg
|
||||
|
||||
交互效果:
|
||||
- hover:scale-105
|
||||
- 选中:橙色背景 + 白色光圈
|
||||
|
||||
颜色映射:
|
||||
- 后期制作:蓝色 (bg-blue-500/85)
|
||||
- 拍摄执行:绿色 (bg-green-500/85)
|
||||
- 内容策划:橙色 (bg-orange-500/85)
|
||||
- 视觉素养:橙色 (bg-orange-500/85)
|
||||
- 摄影基础:蓝色 (bg-blue-500/85)
|
||||
- 剪映基础操作:紫色 (bg-purple-500/85)
|
||||
- AI与版权伦理知识:蓝色 (bg-blue-500/85)
|
||||
- 思路提点与案例赏析:蓝色 (bg-blue-500/85)
|
||||
```
|
||||
|
||||
### 4. 子标签展开
|
||||
|
||||
```
|
||||
父标签被点击且有子标签
|
||||
↓
|
||||
在父标签下方显示子标签列表
|
||||
↓
|
||||
子标签样式:
|
||||
- 更小的尺寸 (px-3 py-1)
|
||||
- 圆角矩形 (rounded-lg)
|
||||
- 蓝色背景(未选中)/ 橙色背景(选中)
|
||||
- 垂直排列 (space-y-1)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 资源列表组件
|
||||
|
||||
### 功能
|
||||
|
||||
- 显示过滤后的资源列表
|
||||
- 滚动容器(overflow-y-auto)
|
||||
- 空状态提示
|
||||
|
||||
### 资源卡片样式
|
||||
|
||||
```css
|
||||
- 半透明蓝色背景:bg-blue-500/20
|
||||
- 圆角:rounded
|
||||
- 内边距:p-4
|
||||
- hover效果:bg-blue-500/30
|
||||
- 标题:text-2xl font-bold
|
||||
- 描述:text-lg text-gray-300
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 搜索标签组件
|
||||
|
||||
### 功能
|
||||
|
||||
- 显示当前选择路径(面包屑导航)
|
||||
- 不同类型不同颜色
|
||||
|
||||
### 标签样式
|
||||
|
||||
```css
|
||||
- 圆角胶囊:rounded-full
|
||||
- 边框:border border-white/80
|
||||
- 内边距:px-3 py-1
|
||||
- 颜色映射:
|
||||
- 模块:蓝色 (bg-blue-500/80)
|
||||
- 标签:橙色 (bg-orange-500/80)
|
||||
- 子标签:紫色 (bg-purple-500/80)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎬 视频播放页面
|
||||
|
||||
### 功能
|
||||
|
||||
- 播放学习视频
|
||||
- 全屏视频播放器
|
||||
- 返回按钮
|
||||
|
||||
### 当前问题
|
||||
|
||||
- 硬编码视频路径:`/video/xinshou_jiuzhonggoutufa.mp4`
|
||||
- 未接收路由参数
|
||||
- 无法动态切换视频
|
||||
|
||||
### 建议改进
|
||||
|
||||
```typescript
|
||||
// 应该接收资源ID参数
|
||||
const route = useRoute()
|
||||
const resourceId = route.params.id
|
||||
|
||||
// 根据资源ID获取视频路径
|
||||
const videoSrc = getVideoByResourceId(resourceId)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 功能特点
|
||||
|
||||
### ✅ 已实现
|
||||
|
||||
1. **多层级筛选**:模块 → 标签 → 子标签
|
||||
2. **可视化导航**:技能树形式展示
|
||||
3. **实时过滤**:点击即时更新资源列表
|
||||
4. **面包屑导航**:清晰显示当前位置
|
||||
5. **标签去重**:避免重复显示
|
||||
6. **展开/折叠**:子标签动态显示
|
||||
7. **资源计数**:显示每个标签的资源数量
|
||||
8. **响应式交互**:hover、选中等视觉反馈
|
||||
|
||||
### ⚠️ 待完善
|
||||
|
||||
1. **搜索功能**:文本搜索未实现
|
||||
2. **资源详情**:点击资源卡片无响应
|
||||
3. **视频播放**:未与资源数据关联
|
||||
4. **学习进度**:无进度追踪
|
||||
5. **数据持久化**:刷新后状态丢失
|
||||
|
||||
---
|
||||
|
||||
## 💡 优化建议
|
||||
|
||||
### 1. 完善搜索功能
|
||||
|
||||
```typescript
|
||||
// 在 filteredResources 中添加文本搜索
|
||||
const filteredResources = computed(() => {
|
||||
let resources = /* 现有筛选逻辑 */
|
||||
|
||||
// 添加文本搜索
|
||||
if (search.value.trim()) {
|
||||
resources = resources.filter(r =>
|
||||
r.title.includes(search.value) ||
|
||||
r.description?.includes(search.value)
|
||||
)
|
||||
}
|
||||
|
||||
return resources
|
||||
})
|
||||
```
|
||||
|
||||
### 2. 资源点击跳转
|
||||
|
||||
```typescript
|
||||
// ResourceList.vue
|
||||
const handleResourceClick = (resource: ResourceItem) => {
|
||||
router.push({
|
||||
name: 'SkillVideo',
|
||||
params: { id: resource.id }
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 动态视频播放
|
||||
|
||||
```typescript
|
||||
// video.vue
|
||||
const route = useRoute()
|
||||
const resourceId = route.params.id as string
|
||||
const resource = getAllResources().find(r => r.id === resourceId)
|
||||
const videoSrc = `/video/${resource?.videoFile || 'default.mp4'}`
|
||||
```
|
||||
|
||||
### 4. 学习进度追踪
|
||||
|
||||
```typescript
|
||||
// 添加到 ResourceItem
|
||||
interface ResourceItem {
|
||||
// ... 现有字段
|
||||
completed?: boolean
|
||||
progress?: number
|
||||
lastViewedAt?: Date
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 状态持久化
|
||||
|
||||
```typescript
|
||||
// 使用 localStorage 或 Pinia
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
|
||||
const selectedModuleId = useLocalStorage('skill-module', null)
|
||||
const selectedTagId = useLocalStorage('skill-tag', null)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 数据扩展建议
|
||||
|
||||
### 1. 添加视频文件字段
|
||||
|
||||
```typescript
|
||||
interface ResourceItem {
|
||||
// ... 现有字段
|
||||
videoFile?: string // 视频文件名
|
||||
duration?: number // 视频时长(秒)
|
||||
difficulty?: 'easy' | 'medium' | 'hard' // 难度
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 添加标签元数据
|
||||
|
||||
```typescript
|
||||
interface TagMeta {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
icon?: string
|
||||
description?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 添加模块元数据
|
||||
|
||||
```typescript
|
||||
interface ModuleMeta {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
totalResources: number
|
||||
completedResources: number
|
||||
progress: number
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
这是一个**结构清晰、功能完整的技能树资源浏览系统**,核心特点:
|
||||
|
||||
1. **数据驱动**:基于一维数组的资源数据,通过工具函数灵活查询
|
||||
2. **层级筛选**:支持模块→标签→子标签的多层级筛选
|
||||
3. **可视化导航**:技能树形式直观展示资源结构
|
||||
4. **实时反馈**:点击即时更新,面包屑导航清晰
|
||||
5. **代码整洁**:已清理冗余代码,结构简洁明了
|
||||
|
||||
主要改进方向是完善搜索、资源详情、视频播放和学习进度追踪功能。
|
||||
@@ -0,0 +1,429 @@
|
||||
# 🔄 技能树功能逻辑关系完整梳理
|
||||
|
||||
## 📊 核心状态管理
|
||||
|
||||
### 状态变量
|
||||
|
||||
```typescript
|
||||
selectedModuleId: string | null // 当前选中的模块ID(null = 总览模式)
|
||||
selectedTagId: string | null // 当前选中的标签ID
|
||||
selectedSubTagId: string | null // 当前选中的子标签ID
|
||||
expandedTags: Set<string> // 展开的标签集合
|
||||
search: string // 搜索文本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 功能模块详解
|
||||
|
||||
### 1. 模块切换与技能树标题
|
||||
|
||||
#### 逻辑流程
|
||||
|
||||
```
|
||||
用户点击模块图标
|
||||
↓
|
||||
handleModuleListItemClick(moduleId)
|
||||
↓
|
||||
如果点击的是已选中的模块 → 取消选择(回到总览)
|
||||
如果点击的是新模块 → 选中该模块
|
||||
↓
|
||||
更新 selectedModuleId
|
||||
清空 selectedTagId、selectedSubTagId
|
||||
清空 expandedTags
|
||||
↓
|
||||
技能树标题更新:
|
||||
- selectedModuleId = null → "技能树总览"
|
||||
- selectedModuleId = "module-1" → "新手导航"
|
||||
- selectedModuleId = "module-2" → "vlog发现与记录"
|
||||
- selectedModuleId = "module-3" → "访谈聆听与对话"
|
||||
- selectedModuleId = "module-4" → "电影创作与表达"
|
||||
```
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **SkillTree.vue**: `getTreeTitle()` 函数
|
||||
- **index.vue**: `handleModuleListItemClick()` 函数
|
||||
|
||||
---
|
||||
|
||||
### 2. 标签点击逻辑(核心)
|
||||
|
||||
#### 逻辑分支
|
||||
|
||||
```
|
||||
用户点击技能树标签
|
||||
↓
|
||||
handleTagClick(moduleId, tagId)
|
||||
↓
|
||||
检查是否有子标签
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 分支A: 有子标签 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ 如果点击的是已选中的标签: │
|
||||
│ - 已展开 → 折叠(删除 tagId from expandedTags) │
|
||||
│ - 未展开 → 展开(添加 tagId to expandedTags) │
|
||||
│ │
|
||||
│ 如果点击的是新标签: │
|
||||
│ - 设置 selectedModuleId = moduleId │
|
||||
│ - 设置 selectedTagId = tagId │
|
||||
│ - 清空 selectedSubTagId = null │
|
||||
│ - 自动展开(添加 tagId to expandedTags) │
|
||||
│ │
|
||||
│ 结果:展开/折叠子节点,不触发搜索 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 分支B: 无子标签 │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ - 设置 selectedModuleId = moduleId │
|
||||
│ - 设置 selectedTagId = tagId │
|
||||
│ - 清空 selectedSubTagId = null │
|
||||
│ - 清空 expandedTags │
|
||||
│ │
|
||||
│ 结果:高亮标签 + 触发搜索,其他标签保持可见 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 关键特性
|
||||
|
||||
- ✅ 有子标签:只展开/折叠,不搜索
|
||||
- ✅ 无子标签:高亮并搜索,不隐藏其他标签
|
||||
- ✅ 其他标签始终可见(不会因为选择而隐藏)
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **index.vue**: `handleTagClick()` 函数
|
||||
|
||||
---
|
||||
|
||||
### 3. 子标签点击逻辑
|
||||
|
||||
#### 逻辑流程
|
||||
|
||||
```
|
||||
用户点击子标签
|
||||
↓
|
||||
handleSubTagClick(moduleId, tagId, subTagId)
|
||||
↓
|
||||
如果点击的是已选中的子标签 → 取消选择
|
||||
如果点击的是新子标签 → 选中该子标签
|
||||
↓
|
||||
更新状态:
|
||||
- selectedModuleId = moduleId
|
||||
- selectedTagId = tagId
|
||||
- selectedSubTagId = subTagId
|
||||
- 确保父标签展开(添加 tagId to expandedTags)
|
||||
↓
|
||||
触发资源过滤(显示该子标签的资源)
|
||||
```
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **index.vue**: `handleSubTagClick()` 函数
|
||||
|
||||
---
|
||||
|
||||
### 4. 资源过滤逻辑
|
||||
|
||||
#### 过滤优先级
|
||||
|
||||
```
|
||||
1. 子标签过滤(最精确)
|
||||
↓
|
||||
2. 标签过滤
|
||||
↓
|
||||
3. 模块过滤
|
||||
↓
|
||||
4. 全部资源(默认)
|
||||
↓
|
||||
5. 文本搜索(叠加过滤)
|
||||
```
|
||||
|
||||
#### 详细流程
|
||||
|
||||
```typescript
|
||||
filteredResources = computed(() => {
|
||||
let resources = []
|
||||
|
||||
// 第一步:根据选择获取资源
|
||||
if (selectedSubTagId) {
|
||||
resources = getResourcesBySubTag(moduleId, tagId, subTagId)
|
||||
} else if (selectedTagId) {
|
||||
resources = getResourcesByTag(moduleId, tagId)
|
||||
} else if (selectedModuleId) {
|
||||
resources = getResourcesByModule(moduleId)
|
||||
} else {
|
||||
resources = getAllResources()
|
||||
}
|
||||
|
||||
// 第二步:文本搜索(如果有搜索文本)
|
||||
if (search.trim()) {
|
||||
resources = resources.filter(
|
||||
r =>
|
||||
r.title.includes(search) ||
|
||||
r.moduleName.includes(search) ||
|
||||
r.tagName.includes(search) ||
|
||||
r.description?.includes(search)
|
||||
)
|
||||
}
|
||||
|
||||
return resources
|
||||
})
|
||||
```
|
||||
|
||||
#### 搜索字段
|
||||
|
||||
- ✅ `title` - 资源标题
|
||||
- ✅ `moduleName` - 模块名称
|
||||
- ✅ `tagName` - 标签名称
|
||||
- ✅ `description` - 资源描述
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **index.vue**: `filteredResources` 计算属性
|
||||
|
||||
---
|
||||
|
||||
### 5. 搜索标签(面包屑)
|
||||
|
||||
#### 显示逻辑
|
||||
|
||||
```
|
||||
searchTags = computed(() => {
|
||||
const tags = []
|
||||
|
||||
if (selectedModuleId) {
|
||||
tags.push({ type: 'module', name: moduleName })
|
||||
}
|
||||
|
||||
if (selectedTagId) {
|
||||
tags.push({ type: 'tag', name: tagName })
|
||||
}
|
||||
|
||||
if (selectedSubTagId) {
|
||||
tags.push({ type: 'subTag', name: subTagName })
|
||||
}
|
||||
|
||||
return tags
|
||||
})
|
||||
```
|
||||
|
||||
#### 标签样式
|
||||
|
||||
- **模块标签**:蓝色(`bg-blue-500/80`)
|
||||
- **标签标签**:橙色(`bg-orange-500/80`)
|
||||
- **子标签标签**:紫色(`bg-purple-500/80`)
|
||||
|
||||
#### 删除功能
|
||||
|
||||
```
|
||||
用户点击标签的 × 按钮
|
||||
↓
|
||||
handleRemoveTag(tagType)
|
||||
↓
|
||||
根据 tagType 清除对应状态:
|
||||
- 'subTag' → 清除 selectedSubTagId
|
||||
- 'tag' → 清除 selectedTagId + selectedSubTagId + expandedTags
|
||||
- 'module' → 清除所有选择状态
|
||||
↓
|
||||
资源列表自动更新
|
||||
```
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **index.vue**: `searchTags` 计算属性, `handleRemoveTag()` 函数
|
||||
- **SearchTags.vue**: 标签显示和删除按钮
|
||||
|
||||
---
|
||||
|
||||
### 6. 文本搜索
|
||||
|
||||
#### 搜索框行为
|
||||
|
||||
```
|
||||
用户在搜索框输入文本
|
||||
↓
|
||||
v-model 绑定到 search 变量
|
||||
↓
|
||||
filteredResources 自动重新计算
|
||||
↓
|
||||
资源列表实时更新
|
||||
```
|
||||
|
||||
#### 搜索特性
|
||||
|
||||
- ✅ 实时搜索(无需点击搜索按钮)
|
||||
- ✅ 多字段搜索(title、moduleName、tagName、description)
|
||||
- ✅ 大小写不敏感
|
||||
- ✅ 叠加过滤(在已选择的基础上进一步过滤)
|
||||
- ✅ 搜索文本为空时,不影响选择过滤
|
||||
|
||||
#### 实现位置
|
||||
|
||||
- **index.vue**: 搜索框 `v-model="search"`
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI 交互状态
|
||||
|
||||
### 标签高亮状态
|
||||
|
||||
```typescript
|
||||
isTagSelected(moduleId, tagId) {
|
||||
return selectedModuleId === moduleId && selectedTagId === tagId
|
||||
}
|
||||
```
|
||||
|
||||
### 标签展开状态
|
||||
|
||||
```typescript
|
||||
isTagExpanded(tagId) {
|
||||
return expandedTags.has(tagId)
|
||||
}
|
||||
```
|
||||
|
||||
### 子标签选中状态
|
||||
|
||||
```typescript
|
||||
isSubTagSelected(moduleId, tagId, subTagId) {
|
||||
return selectedModuleId === moduleId &&
|
||||
selectedTagId === tagId &&
|
||||
selectedSubTagId === subTagId
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 数据流向图
|
||||
|
||||
```
|
||||
用户操作
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 点击模块图标 → handleModuleListItemClick │
|
||||
│ 点击标签 → handleTagClick │
|
||||
│ 点击子标签 → handleSubTagClick │
|
||||
│ 输入搜索文本 → v-model:search │
|
||||
│ 删除面包屑标签 → handleRemoveTag │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
更新状态变量
|
||||
- selectedModuleId
|
||||
- selectedTagId
|
||||
- selectedSubTagId
|
||||
- expandedTags
|
||||
- search
|
||||
↓
|
||||
触发计算属性重新计算
|
||||
- searchTags(面包屑)
|
||||
- filteredResources(资源列表)
|
||||
- displayedTags(技能树标签)
|
||||
- displayedModules(显示的模块)
|
||||
↓
|
||||
UI 自动更新
|
||||
- 技能树标题
|
||||
- 标签高亮状态
|
||||
- 子节点展开/折叠
|
||||
- 面包屑标签
|
||||
- 资源列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 关键场景示例
|
||||
|
||||
### 场景1:从总览到具体资源
|
||||
|
||||
```
|
||||
1. 初始状态:总览模式
|
||||
- selectedModuleId = null
|
||||
- 显示 "技能树总览"
|
||||
- 显示所有模块的标签(去重)
|
||||
|
||||
2. 点击 "新手导航" 模块
|
||||
- selectedModuleId = "module-1"
|
||||
- 显示 "新手导航"
|
||||
- 只显示新手导航的标签
|
||||
|
||||
3. 点击 "视觉素养" 标签(有子标签)
|
||||
- selectedTagId = "tag-1-1"
|
||||
- 展开子标签:构图、光线、运镜
|
||||
- 资源列表显示视觉素养的所有资源
|
||||
|
||||
4. 点击 "构图" 子标签
|
||||
- selectedSubTagId = "subtag-1-1-1"
|
||||
- 资源列表只显示构图相关资源
|
||||
- 面包屑:新手导航 > 视觉素养 > 构图
|
||||
```
|
||||
|
||||
### 场景2:搜索功能
|
||||
|
||||
```
|
||||
1. 在搜索框输入 "构图"
|
||||
- search = "构图"
|
||||
- 资源列表实时过滤,显示所有包含"构图"的资源
|
||||
|
||||
2. 点击面包屑标签的删除按钮
|
||||
- 清除对应的选择
|
||||
- 资源列表更新
|
||||
|
||||
3. 清空搜索框
|
||||
- search = ""
|
||||
- 资源列表恢复到选择过滤的结果
|
||||
```
|
||||
|
||||
### 场景3:标签点击行为
|
||||
|
||||
```
|
||||
有子标签的标签(如"视觉素养"):
|
||||
- 第一次点击 → 展开子节点
|
||||
- 第二次点击 → 折叠子节点
|
||||
- 不触发搜索,其他标签保持可见
|
||||
|
||||
无子标签的标签(如"摄影基础"):
|
||||
- 点击 → 高亮标签 + 过滤资源
|
||||
- 其他标签保持可见
|
||||
- 可以继续点击其他标签
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 功能清单
|
||||
|
||||
### 已实现功能
|
||||
|
||||
- ✅ 模块切换(总览 ↔ 具体模块)
|
||||
- ✅ 技能树标题动态更新
|
||||
- ✅ 标签点击逻辑(有/无子标签分支)
|
||||
- ✅ 子标签展开/折叠
|
||||
- ✅ 标签高亮显示
|
||||
- ✅ 资源多级过滤(模块 → 标签 → 子标签)
|
||||
- ✅ 文本搜索(多字段)
|
||||
- ✅ 面包屑导航
|
||||
- ✅ 标签删除功能
|
||||
- ✅ 实时搜索
|
||||
- ✅ 标签去重(总览模式)
|
||||
|
||||
### 核心特性
|
||||
|
||||
- 🎯 **智能标签点击**:根据是否有子标签自动选择行为
|
||||
- 🔍 **多维度搜索**:选择过滤 + 文本搜索
|
||||
- 🏷️ **面包屑导航**:清晰显示当前位置
|
||||
- 🗑️ **灵活删除**:可以删除任意层级的选择
|
||||
- 👁️ **保持可见**:选择标签后其他标签不隐藏
|
||||
|
||||
---
|
||||
|
||||
## 🎯 总结
|
||||
|
||||
这是一个**多层级、多维度的资源浏览和搜索系统**,核心特点:
|
||||
|
||||
1. **灵活的导航**:支持模块 → 标签 → 子标签的层级导航
|
||||
2. **智能的交互**:根据标签类型自动选择展开或搜索
|
||||
3. **强大的搜索**:选择过滤 + 文本搜索双重过滤
|
||||
4. **清晰的反馈**:面包屑、高亮、展开状态等多种视觉反馈
|
||||
5. **用户友好**:可以随时删除选择,标签始终可见
|
||||
|
||||
整个系统通过 Vue 的响应式系统自动同步状态和 UI,用户的每个操作都会立即反映在界面上。
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 19 KiB |
@@ -154,14 +154,14 @@ const currentStepContent = computed(() => {
|
||||
const step = currentStepData.value
|
||||
if (!step) return ''
|
||||
|
||||
if (!step.title) {
|
||||
// if (!step.title) {
|
||||
return `${step.content}`
|
||||
}
|
||||
// }
|
||||
|
||||
return `
|
||||
<h3 class="text-lg font-bold mb-2">${step.title}</h3>
|
||||
<div class="leading-relaxed">${step.content}</div>
|
||||
`
|
||||
// return `
|
||||
// <h3 class="text-lg font-bold mb-2">${step.title}</h3>
|
||||
// <div class="leading-relaxed">${step.content}</div>
|
||||
// `
|
||||
})
|
||||
|
||||
const handleNext = () => {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div> -->
|
||||
|
||||
<!-- 步骤标签 -->
|
||||
<div class="step-label" :class="`step-label--${step.type}`">
|
||||
<div class="step-label" :class="getTagColor(step.label)">
|
||||
{{ step.label }}
|
||||
</div>
|
||||
|
||||
@@ -93,6 +93,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { getTagColor } from '@/utils'
|
||||
|
||||
export interface StepItem {
|
||||
id: string
|
||||
@@ -354,38 +355,9 @@ onMounted(() => {
|
||||
|
||||
.step-label {
|
||||
@apply px-16 py-4 rounded-full text-2xl font-semibold mr-4 whitespace-nowrap;
|
||||
color: white;
|
||||
min-width: 5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.step-label--shooting {
|
||||
background: linear-gradient(135deg, #22c55e, #16a34a);
|
||||
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.4);
|
||||
}
|
||||
|
||||
.step-label--planning {
|
||||
background: linear-gradient(135deg, #e38872, #e38872);
|
||||
box-shadow: 0 2px 8px rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
/* 蓝色 */
|
||||
.step-label--blue {
|
||||
background: linear-gradient(135deg, #0ea5e9, #0284c7);
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
|
||||
/* 天蓝色 */
|
||||
.step-label--post {
|
||||
/* background: linear-gradient(180deg, #9ed5ee, #0ea5e9); */
|
||||
background-color: #70c7ef;
|
||||
box-shadow: 0 2px 8px rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
|
||||
.step-label--other {
|
||||
/* background: linear-gradient(135deg, #a855f7, #9445de); */
|
||||
background-color: #5f15d7;
|
||||
box-shadow: 0 2px 8px rgba(168, 85, 247, 0.4);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.step-content {
|
||||
|
||||
@@ -221,3 +221,18 @@ export const getPublicPath = (path: string) => {
|
||||
// 使用 Vite 的 BASE_URL 环境变量来处理基础路径
|
||||
return `${import.meta.env.BASE_URL}${cleanPath}`
|
||||
}
|
||||
|
||||
// 获取标签颜色
|
||||
export const getTagColor = (tagName: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
后期制作: 'bg-[#49dcda] border-white text-white',
|
||||
拍摄执行: 'bg-[#98d265] border-white text-white',
|
||||
内容策划: 'bg-[#f1a554] border-white text-white',
|
||||
视觉素养: 'bg-[#e38872] border-white text-white',
|
||||
摄影基础: 'bg-[#2cb067] border-white text-white',
|
||||
剪映基础操作: 'bg-[#945bc6] border-white text-white',
|
||||
AI与版权伦理知识: 'bg-[#6f8cf9] border-white text-white',
|
||||
思路提点与案例赏析: 'bg-[#32a4fb] border-white text-white'
|
||||
}
|
||||
return colorMap[tagName] || 'bg-blue-400 border-white/60 text-white'
|
||||
}
|
||||
@@ -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">
|
||||
<!-- 背景层 - 使用柔和的半透明渐变 -->
|
||||
<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) => {
|
||||
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) => {
|
||||
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) => {
|
||||
// 获取标签的资源数量(如果显示所有模块,则合并同名标签的数量)
|
||||
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">
|
||||
<!-- 标签节点 -->
|
||||
<template v-for="(tag, tagIndex) in displayedTags" :key="`${tag.moduleId}-${tag.tagId}`">
|
||||
<!-- 标签节点容器 - 选中的标签提高 z-index 显示在蒙版之上 -->
|
||||
<div
|
||||
class="absolute z-10"
|
||||
:style="
|
||||
getTagPosition(moduleIndex, tagIndex, getTagsByModule(module.id).length, module.id)
|
||||
"
|
||||
class="absolute"
|
||||
:class="isTagSelected(tag.moduleId, tag.tagId) ? 'z-20' : 'z-[1]'"
|
||||
:style="getTagPosition(tagIndex)"
|
||||
>
|
||||
<!-- 资源标签节点 -->
|
||||
<div class="relative">
|
||||
<!-- 标签主体容器 - 用于精确对齐定位层 -->
|
||||
<div class="relative">
|
||||
<!-- 标签主体 -->
|
||||
<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' : ''
|
||||
'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(module.id, tag.id)"
|
||||
@click="handleTagClick(tag.moduleId, tag.tagId)"
|
||||
>
|
||||
<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"
|
||||
<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"
|
||||
>
|
||||
{{ isTagExpanded(tag.id) ? '▼' : '▶' }}
|
||||
</span>
|
||||
<!-- 子标签列表(围绕父标签圆形分布) -->
|
||||
<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)"
|
||||
>
|
||||
<!-- 图标-图标资源已经带了文字 -->
|
||||
<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>
|
||||
{{ getResourceCount(tag.moduleId, tag.tagId, tag.tagName) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 点击之后的蒙版 - 使用径向渐变,从中心透明到边缘半透明黑色 -->
|
||||
<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
|
||||
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>
|
||||
|
||||
<!-- 右侧标签 -->
|
||||
<div class="absolute right-0 top-1/2 -translate-y-1/2 -translate-x-4 z-20">
|
||||
<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>
|
||||
<img class="absolute right-4 top-0 z-30" :src="getStaticImage('skill/技能树标题.png')" alt="" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -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,25 +113,21 @@ 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) {
|
||||
// 从资源数组中查找标签名
|
||||
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 (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
|
||||
// 从资源数组中查找子标签名
|
||||
if (selectedSubTagId.value) {
|
||||
const subTagResource = getAllResources().find(
|
||||
r =>
|
||||
r.moduleId === selectedModuleId.value &&
|
||||
@@ -96,29 +138,76 @@ const searchTags = computed<SearchTag[]>(() => {
|
||||
tags.push({ type: 'subTag', name: subTagResource.subTagName })
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 总览模式:只显示标签名称(不显示模块)
|
||||
if (selectedTagId.value) {
|
||||
const tagResource = getAllResources().find(r => r.tagId === selectedTagId.value)
|
||||
if (tagResource) {
|
||||
tags.push({ type: 'tag', name: tagResource.tagName })
|
||||
}
|
||||
|
||||
if (selectedSubTagId.value) {
|
||||
const subTagResource = getAllResources().find(
|
||||
r => r.tagId === selectedTagId.value && r.subTagId === selectedSubTagId.value
|
||||
)
|
||||
if (subTagResource && subTagResource.subTagName) {
|
||||
tags.push({ type: 'subTag', name: subTagResource.subTagName })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags
|
||||
})
|
||||
|
||||
// 过滤后的资源列表
|
||||
const filteredResources = computed<ResourceItem[]>(() => {
|
||||
let resources: ResourceItem[] = []
|
||||
|
||||
// 如果选择了子标签,返回子标签的资源
|
||||
if (selectedModuleId.value && selectedTagId.value && selectedSubTagId.value) {
|
||||
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) {
|
||||
return getResourcesByModule(selectedModuleId.value)
|
||||
// 模块模式:只返回该模块该标签的资源
|
||||
resources = getResourcesByTag(selectedModuleId.value, selectedTagId.value)
|
||||
} else {
|
||||
// 总览模式:返回所有模块中该标签名称的资源
|
||||
const tagName = getAllResources().find(r => r.tagId === selectedTagId.value)?.tagName
|
||||
if (tagName) {
|
||||
resources = getAllResources().filter(r => r.tagName === tagName)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果选择了模块,返回模块的资源
|
||||
else if (selectedModuleId.value) {
|
||||
resources = getResourcesByModule(selectedModuleId.value)
|
||||
}
|
||||
// 默认返回所有资源
|
||||
else {
|
||||
resources = getAllResources()
|
||||
}
|
||||
|
||||
// 默认返回所有资源
|
||||
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)
|
||||
// 如果点击的是已选中的标签,则取消选中(关闭遮罩)
|
||||
if (selectedTagId.value === tagId) {
|
||||
selectedTagId.value = null
|
||||
selectedSubTagId.value = null
|
||||
} else {
|
||||
// 如果未展开,则展开
|
||||
expandedTags.value.add(tagId)
|
||||
expandedTags.value.clear()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 选择新的标签
|
||||
|
||||
// 检查是否有子标签
|
||||
const subTags = getSubTagsByTag(moduleId, tagId)
|
||||
const hasSubTags = subTags.length > 0
|
||||
|
||||
if (hasSubTags) {
|
||||
// 有子标签:选择新标签并自动展开
|
||||
// 在总览模式下,不设置 selectedModuleId,保持总览状态
|
||||
if (selectedModuleId.value !== null) {
|
||||
selectedModuleId.value = moduleId
|
||||
}
|
||||
selectedTagId.value = tagId
|
||||
selectedSubTagId.value = null
|
||||
|
||||
// 如果标签有子标签,则展开
|
||||
const subTags = getSubTagsByTag(moduleId, tagId)
|
||||
if (subTags.length > 0) {
|
||||
// 自动展开
|
||||
expandedTags.value.add(tagId)
|
||||
} else {
|
||||
expandedTags.value.delete(tagId)
|
||||
// 无子标签:高亮并搜索,不隐藏其他标签
|
||||
// 在总览模式下,不设置 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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||