CWFrame v0.1 26/3/25 前端渲染逻辑总结 / CWFrame Frontend Rendering Architecture Review
一、技术栈概览
| 类别 | 技术 |
|---|---|
| 框架 | Vue 3 (Composition API + TypeScript) |
| 构建工具 | Vite |
| 3D 渲染 | Three.js |
| 3D 控制器 | OrbitControls (Three.js 官方示例) |
| 图论可视化 | Vue-Flow (仅引用,未深度使用) |
| 类型定义 | @shared/contract |
二、核心数据流
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ world-data.json│ │ mock-progress.ts│ │ cwframe.loader │
│ (框架图数据) │ │ (用户进度) │ │ (加载器) │
└────────┬────────┘ └────────┬─────────┘ └────────┬────────┘
│ │ │
└───────────────────────┼────────────────────────┘
▼
┌─────────────────┐
│ App.vue │
│ (状态管理) │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CWFrameGraph │ │ CWFrameNode │ │ CWFrameLabel │
│ (Three.js 3D) │ │ (Vue DOM 标签) │ │ (详情弹窗) │
└─────────────────┘ └─────────────────┘ └─────────────────┘三、模块详解
1. 数据加载层 (core/cwframe.loader.ts)
| 函数 | 作用 |
|---|---|
loadFrameMap() | 读取 world-data.json,返回 CWFrameMap |
loadProgress() | 读取 mock-progress.ts,返回 CWFrameProgress |
当前实现:直接从本地文件读取,后续将改为调用后端 API。
2. 状态计算层 (core/cwframe.status.ts)
typescript
type CWFrameNodeStatus = 'Locked' | 'Discoverable' | 'Unlocked';
// 节点状态判断逻辑
function getNodeStatus(node, progress):
- 已解锁 → Unlocked
- 未解锁但依赖全满足 → Discoverable
- 依赖未满足 → Locked
- 首次加载(无任何解锁)→ 全部隐藏为 Locked| 状态 | 表现 |
|---|---|
Locked | 3D 节点不渲染,DOM 标签不显示 |
Discoverable | 3D 节点暗淡显示,DOM 标签隐藏 |
Unlocked | 3D 节点高亮,DOM 标签显示,可点击 |
3. 进度操作层 (core/cwframe.progress.ts)
| 函数 | 作用 |
|---|---|
unlockNode(progress, node) | 将节点加入 unlockedNodes |
lockNode(progress, node) | 从 unlockedNodes 中移除 |
resetProgress(progress) | 重置所有进度 |
当前逻辑:开放式探索,用户输入匹配即解锁,不检查依赖。
4. 主组件 (App.vue)
状态定义
typescript
frameMap: CWFrameMap // 框架图数据
progress: CWFrameProgress // 用户进度
nodePositions: Map // 3D→2D 坐标映射
selectedNodeId: number|null // 当前选中的节点
inputValue: string // 用户输入
matchResult: { success, message }核心方法
| 方法 | 触发时机 | 作用 |
|---|---|---|
onMounted | 页面加载 | 调用 loader 加载数据 |
handleInputUnlock | 输入框回车/点击按钮 | 匹配标签→解锁节点 |
handleNodeClick | 点击节点 | 切换选中状态 |
handlePositionsUpdate | Three.js 每帧 | 更新 2D 坐标映射 |
handleReset | 点击重置按钮 | 清空进度 |
模板结构
App.vue
├── CWFrameGraph (Three.js 画布)
│ ├── 接收 frameMap, progress
│ └── 发出 nodeClick, positionsUpdate
├── CWFrameNode (Vue 标签覆盖层)
│ ├── v-for 未锁定的节点
│ ├── 定位依据 nodePositions
│ └── 发出 click
├── Teleport → CenterPopup (详情弹窗)
│ └── CWFrameLabel
└── InputPanel (搜索框 + 按钮)5. 3D 渲染组件 (components/CWFrameGraph.vue)
这是最复杂的组件,负责 Three.js 场景管理。
初始化流程 (initThree)
- 场景:创建
THREE.Scene,加雾效FogExp2 - 相机:
PerspectiveCamera,位置 (0, 0, 400) - 渲染器:
WebGLRenderer,透明度背景 - 控制器:
OrbitControls,支持拖拽旋转,限制距离 100-800 - 背景:创建 2000 个星点粒子
Points - 布局:调用
initLayout()构建节点和连线 - 动画循环:
requestAnimationFrame渲染
节点布局算法 (buildStableNodePositions)
┌─────────────────────────────────────────────┐
│ 输入:nodes (含 id, category, dependencies) │
├─────────────────────────────────────────────┤
│ 1. 构建深度图 (依赖链层数) │
│ 2. 按 category 分扇区 (环形分布) │
│ 3. 同 depth + category 的节点线性排列 │
│ 4. 半径 = baseRadius + depth * layerGap │
│ 5. 加入稳定噪声避免重叠 │
│ 输出:Map<nodeId, Vector3> │
└─────────────────────────────────────────────┘节点对象结构 (createNode)
THREE.Group
├── sphere (SphereGeometry, 半径8, 半透明核心)
├── glow (SphereGeometry, 半径12, 外层光晕)
└── sprite (CanvasTexture 渐变, AdditiveBlending)连线逻辑 (createLink)
- 遍历所有节点的
dependencies - 创建
THREE.Line,连接依赖节点 → 当前节点 - 透明度根据两端状态动态调整
坐标映射 (updateScreenPositions)
3D 节点位置 ──project(camera)──> 归一化坐标 (-1~1)
│
▼
转换为屏幕像素坐标
│
▼
emit('positionsUpdate', Map)触发频率:每帧最多 30ms 一次(节流),由 OrbitControls 的 change 事件驱动。
可见性更新 (updateNodeVisibility)
| 状态 | sphere | glow | sprite | 透明度 |
|---|---|---|---|---|
| Unlocked | 显示 | 显示 | 显示 | 高 |
| Discoverable | 显示 | 显示 | 显示 | 低 |
| Locked | 隐藏 | 隐藏 | 隐藏 | - |
连线的可见性:
- 两端都是 Unlocked → opacity 0.6
- 有一端 Unlocked → opacity 0.15
- 其他 → 隐藏
交互 (onMouseClick)
- 使用
Raycaster检测点击 - 只有 Unlocked 节点可点击
- 触发
emit('nodeClick', nodeId)
6. DOM 标签组件 (components/CWFrameNode.vue)
| 属性 | 说明 |
|---|---|
node | 节点数据 |
status | 状态 (Unlocked/Discoverable/Locked) |
screenX/screenY | 来自父组件的 2D 坐标 |
selected | 是否被选中 |
定位:绝对定位,left: screenX px; top: screenY px,transform: translate(-50%, -50%)
样式:
- Unlocked:显示球体 + 文字 + 发光
- Discoverable:仅显示暗淡球体,隐藏文字
- Locked:
opacity: 0,不响应点击
7. 详情弹窗组件 (components/CWFrameLabel.vue)
通过 Vue Teleport 渲染到 body 下:
html
<Teleport to="body">
<div v-if="selectedNode" class="center-popup">
<CWFrameLabel :node="selectedNode" />
</div>
</Teleport>显示内容:label + description + category
四、调用关系图
┌─────────────────────────────────────┐
│ App.vue │
│ onMounted: loadFrameMap, loadProgress │
└──────────────┬──────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ cwframe.loader │ │ cwframe.status │ │ cwframe.progress │
│ - loadFrameMap │ │ - buildStatusMap │ │ - unlockNode │
│ - loadProgress │ │ - getNodeStatus │ │ - resetProgress │
└────────┬─────────┘ └────────┬─────────┘ └──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ CWFrameGraph.vue (Three.js) │
│ - initThree: Scene/Camera/Renderer/Controls │
│ - buildStableNodePositions: 3D 布局算法 │
│ - createNode: 创建球体+光晕+sprite │
│ - createLink: 绘制依赖连线 │
│ - updateScreenPositions: 3D→2D 坐标映射 │
│ - updateNodeVisibility: 根据状态更新透明度 │
│ - onMouseClick: Raycaster 点击检测 │
└───────────────────────────┬─────────────────────────────────┘
│
@node-click, @positions-update
▼
┌─────────────────────────────────────────────────────────────┐
│ CWFrameNode.vue (DOM 标签) │
│ - 接收 screenX/Y 定位到 3D 节点对应位置 │
│ - 根据 status 渲染不同样式 │
│ - 点击触发 @click → handleNodeClick │
└───────────────────────────┬─────────────────────────────────┘
│
点击已解锁节点
▼
┌─────────────────────────────────────────────────────────────┐
│ Teleport → CenterPopup │
│ └── CWFrameLabel.vue (显示详情) │
└─────────────────────────────────────────────────────────────┘五、关键算法
1. 深度计算(拓扑排序)
typescript
function buildDepthMap():
1. memo = {}, visiting = Set()
2. 对每个节点递归计算深度
3. depth = 1 + max(所有依赖的depth)
4. 避免循环依赖导致无限递归2. 3D 布局(稳定位置)
typescript
function buildStableNodePositions():
1. 按 depth + category 分组
2. 环形分布:扇区角度 = 2π / category数量
3. 径向距离 = base + depth * layerGap
4. 加入 sin 噪声避免重叠3. 屏幕坐标投影
typescript
vec.setFromMatrixPosition(group.matrixWorld) // 世界坐标
vec.project(camera) // 归一化设备坐标 (-1~1)
screenX = (vec.x * 0.5 + 0.5) * width // 转像素
screenY = (-vec.y * 0.5 + 0.5) * height六、后续扩展方向
- 后端 API 对接:
cwframe.loader.ts改为 fetch 调用 - 依赖检查:解锁时验证依赖是否已解锁
- 持久化:解锁后调用 API 保存进度
- 更多节点样式:不同 category 使用不同颜色
- 交互优化:拖拽节点、缩放动画
七、文件索引
| 路径 | 作用 |
|---|---|
src/main.ts | Vue 入口 |
src/App.vue | 主组件,状态管理 |
src/core/cwframe.loader.ts | 数据加载 |
src/core/cwframe.status.ts | 状态计算 |
src/core/cwframe.progress.ts | 进度操作 |
src/components/CWFrameGraph.vue | Three.js 3D 场景 |
src/components/CWFrameNode.vue | DOM 标签层 |
src/components/CWFrameLabel.vue | 详情弹窗 |
src/data/world-data.json | 框架图数据 |
src/data/mock-progress.ts | 模拟进度数据 |
shared/contract.ts | TypeScript 类型定义 |