Appearance
模块:Node System
唯一职责:实现 Vue Flow 自定义节点的渲染、编辑和 AI 生成交互。
边界
属于本模块:
- 通用节点框架(NodeShell):节点容器、标题栏、连接点、阴影状态
- 紧凑模式 / 编辑模式切换、悬浮面板、右键上下文菜单
- 状态机 composable(useNodeShellState):selected/hovered/connecting → 各区域可见性
- TextNode:TipTap 富文本编辑 + AI 文本生成
- ImageNode:图片上传/预览 + AI 图片生成(支持参考图)
- VideoNode:视频播放 + AI 视频生成
- Toolbar(各节点的内容操作工具,通过 NodeShell toolbar slot 悬浮显示)
- NodeActionBar + Prompt Input(模型选择、变体、生成按钮,通过 NodeToolbar 悬浮显示)
- 节点状态管理(idle → generating → done/error)
不属于本模块:
- 节点在画布上的位置管理(由 Vue Flow + flowStore 负责)
- 节点间的连线和数据流(由 Canvas Editor + State Management 负责)
- HTTP 请求发送(由 AI Service 模块负责)
- 全局连线状态(isConnecting)的 provide(由 useCanvasEditor 负责)
对外接口
Vue Flow 节点注册
typescript
// 画布页面通过 Vue Flow 的 node-types slot 注册
// types: 'text' | 'image' | 'video'
// 每种类型对应一个组件:TextNode.vue / ImageNode.vue / VideoNode.vueuseNodeShellState
typescript
// composables/useNodeShellState.ts
export const IS_CONNECTING_KEY: InjectionKey<Ref<boolean>>
export function useNodeShellState(input: {
selected: Ref<boolean>
isHovered: Ref<boolean>
isConnecting: Ref<boolean>
}): {
mode: ComputedRef<'compact' | 'edit'>
showHandles: ComputedRef<boolean>
showFloatingToolbar: ComputedRef<boolean>
showFloatingBottom: ComputedRef<boolean>
}消费的接口
| 依赖模块 | 调用的方法/属性 | 用途 |
|---|---|---|
| flowStore | updateNodeData(id, data) | 更新节点数据 |
| flowStore | removeNode(id) | 删除节点(通过右键菜单) |
| AiService | generateText(params) | AI 文本生成 |
| AiService | generateImage(params) | AI 图片生成 |
| AiService | generateVideo(params) | AI 视频生成 |
| modelsStore | getAiModels(modality) | 获取模型列表 |
| modelsStore | getDefaultModelKey(modality) | 获取默认模型 |
| useCanvasEditor | provide(IS_CONNECTING_KEY, ref) | 注入全局连线状态 |
状态机
节点生成状态(NodeStatus)
idle ──[点击生成]-→ generating ──[成功]-→ done
└──[失败]-→ error
done ──[修改 prompt/模型]-→ idle
error ──[修改重试]-→ idleNodeShell 显示模式
default (compact) ──[click]-→ selected (edit)
default ──[hover]-→ hovered ──[leave]-→ default
default ──[connectStart]-→ connect-ready ──[connectEnd]-→ default
selected ──[clickAway/Esc]-→ default不变量
- 每个节点组件接收 Vue Flow 的
NodeProps,从data读取/写入状态 - 未选中时紧凑模式:仅标题 + 内容预览,内容区有透明蒙版阻止交互
- 选中时编辑模式:上方悬浮 Toolbar(通过 NodeToolbar),下方悬浮 Prompt + ActionBar
- Handle 在 hover / selected / connecting 三种情况显示,其余隐藏(opacity + pointer-events)
- 右键上下文菜单通过 UContextMenu 包裹节点卡片实现
generating状态下生成按钮被禁用- 生成失败不丢失用户已输入的 prompt 和现有内容
- 图片上传限制:类型 JPG/PNG/WebP、大小 ≤ 10MB
- 视频上传限制:类型 MP4/WebM、大小 ≤ 50MB
错误场景
| 场景 | 模块行为 | 调用方职责 |
|---|---|---|
| 图片上传文件类型不合法(非 JPG/PNG/WebP) | 拒绝文件,显示 toast 错误提示 | 无需处理(节点内部兜底) |
| 图片上传文件超过 10MB / 视频超过 50MB | 拒绝文件,显示 toast 错误提示 | 无需处理 |
| AI 生成请求网络超时或后端返回错误 | 节点状态变为 error,保留用户已输入的 prompt 和现有内容 | 无需处理 |
| AI 生成请求被 abort(组件卸载或用户取消) | 请求取消,节点状态恢复为之前状态 | 无需处理 |
| Object URL 泄漏(组件卸载时未释放) | 组件 onUnmounted 中释放所有 Object URL 和 abort 进行中的请求 | 无需处理 |
| 模型列表未加载完成时用户点击生成 | 使用当前已选模型(或默认模型),不阻塞生成 | 无需处理 |
组件结构
UContextMenu(右键菜单包裹)
└── NodeShell(通用容器 · sf-glass 卡片)
├── NodeHandle (target) — visible=showHandles
├── NodeToolbar (position=Top) — isVisible=showTopToolbar
│ └── [slot:toolbar](TextEditorToolbar / 图片工具按钮 / 上传按钮)
├── NodeHeader(图标 + 标题,选中时可编辑)
├── [slot:default — 内容区](始终显示)
│ ├── TextNode → TextEditorContent
│ ├── ImageNode → 图片上传/预览 + ImageResultGrid
│ └── VideoNode → 视频播放器
│ └── [蒙版 v-if="!selected"](紧凑模式阻止交互)
├── NodeToolbar (position=Bottom) — isVisible=showFloatingBottom
│ ├── [slot:prompt] → NodePromptInput
│ └── NodeActionBar(模型选择 + 变体 + 生成按钮)
└── NodeHandle (source) — visible=showHandles实现位置
| 角色 | 文件路径 |
|---|---|
| 通用框架 | apps/web/app/components/nodes/NodeShell.vue |
| 状态机 composable | apps/web/app/composables/useNodeShellState.ts |
| 连接点 | apps/web/app/components/nodes/NodeHandle.vue |
| 标题栏 | apps/web/app/components/nodes/NodeHeader.vue |
| 生成控制栏 | apps/web/app/components/nodes/NodeActionBar.vue |
| Prompt 输入 | apps/web/app/components/nodes/NodePromptInput.vue |
| 文本节点 | apps/web/app/components/nodes/TextNode.vue |
| 图片节点 | apps/web/app/components/nodes/ImageNode.vue |
| 视频节点 | apps/web/app/components/nodes/VideoNode.vue |
| 编辑器 composable | apps/web/app/composables/useTextEditor.ts |
| 测试 | apps/web/test/nuxt/nodeShell.test.ts, useNodeShellState.test.ts, textNode.test.ts, videoNode.test.ts |