Appearance
Feature: 通用节点框架(NodeShell)
一句话目标
为三种节点类型提供统一的容器,通过选中/未选中两种模式切换紧凑展示与完整编辑体验。
行为约束
约束 1:未选中态 — 紧凑展示
前置条件: 节点未被选中且非 hover 状态 行为: 节点只渲染 NodeHeader(标题 + 类型图标)和内容区(default slot)。Toolbar slot、Prompt slot、NodeActionBar 均不渲染。Handle 隐藏。 后置条件: 节点呈紧凑卡片形态
约束 2:选中态 — 完整编辑
前置条件: 节点被单选(仅一个节点选中)且未在拖拽中 行为: 上方悬浮渲染 toolbar slot(内容操作工具),下方依次渲染 prompt slot 和 NodeActionBar。Handle 显示。内容区可交互(编辑文本、查看图片等)。多选、拖拽中、或框选进行中时,浮动面板(toolbar/prompt/ActionBar)不渲染,仅 Handle 显示。 后置条件: 节点进入编辑模式,用户可输入 prompt 并触发生成
约束 3:连接点显示
前置条件: 节点在画布中 行为: Handle 在以下情况显示:节点被选中、鼠标 hover 节点、画布中正在进行连线拖拽。其余时候隐藏。 后置条件: 左侧 target handle、右侧 source handle 可见
约束 4:点击与拖拽分离
前置条件: 节点未选中 行为: 单击 → 选中节点;按住拖拽 → 移动节点,不改变选中状态 后置条件: 两种交互互不干扰
约束 5:标题编辑
前置条件: 节点被选中 行为: 标题区可编辑,变更通过事件回写 后置条件: 标题持久化到节点数据
约束 6:悬浮面板定位
前置条件: 节点被选中 行为: Toolbar 悬浮于节点卡片上方,Prompt + ActionBar 悬浮于节点卡片下方,定位相对于节点边界 后置条件: 悬浮面板不遮挡节点内容,跟随节点移动/缩放
状态机
状态定义
| 状态 | Handle | Toolbar | Prompt + ActionBar | 内容区 |
|---|---|---|---|---|
| default | 隐藏 | 隐藏 | 隐藏 | 只读展示 |
| hovered | 显示 | 隐藏 | 隐藏 | 只读展示 |
| selected(单选) | 显示 | 悬浮显示 | 悬浮显示 | 可交互 |
| selected(多选) | 显示 | 隐藏 | 隐藏 | 只读展示 |
| selected + dragging | 显示 | 隐藏 | 隐藏 | 只读展示 |
| connect-ready | 显示 | 隐藏 | 隐藏 | 只读展示 |
转换规则
| 从 | 事件 | 到 | 备注 |
|---|---|---|---|
| default | mouseEnter | hovered | |
| default | click | selected | |
| default | connectStart (全局) | connect-ready | 画布开始连线拖拽 |
| hovered | mouseLeave | default | |
| hovered | click | selected | |
| hovered | connectStart (全局) | connect-ready | |
| selected | clickAway / Escape | default | 点击画布空白或按 Esc |
| selected | connectStart (全局) | connect-ready | 连线期间临时退出编辑态 |
| connect-ready | connectEnd (全局) | default | 恢复到连线前的状态 |
约束
selected与hovered不叠加 — 选中态已包含 hovered 的所有可见元素connect-ready是全局事件驱动,与节点自身交互无关- 多选(≥2 个节点选中)时,所有节点的浮动面板隐藏,仅 Handle 显示
- 框选进行中(选框拖拽期间),浮动面板隐藏
- 拖拽中浮动面板隐藏,拖拽结束后恢复
Acceptance Criteria
紧凑模式(default 状态)
- [x] AC-01:未选中且非 hover 时,仅显示 NodeHeader 和内容区(default slot)
- [x] AC-02:未选中时 toolbar slot 不显示(通过
v-if条件渲染控制,NodeToolbar 使用:is-visible="true"延迟挂载以避免 viewport 订阅) - [x] AC-03:未选中时 prompt slot 和 NodeActionBar 不显示
- [x] AC-04:未选中且非 hover 时 Handle 不可见不可交互(CSS
opacity:0 + pointer-events:none)
编辑模式(selected 状态)
- [x] AC-05:单选时 toolbar slot 悬浮渲染于节点上方
- [x] AC-06:单选时 prompt slot 和 NodeActionBar 悬浮渲染于节点下方
- [x] AC-07:选中时 Handle 显示
- [x] AC-08:选中时标题可编辑,变更回写到节点数据
- [x] AC-08a:多选时(≥2 节点选中)浮动面板(toolbar/prompt/ActionBar)不渲染
- [x] AC-08b:拖拽中浮动面板不渲染,拖拽结束后恢复
Handle 显示逻辑
- [x] AC-09:hover 节点时 Handle 显示,mouseLeave 后隐藏
- [x] AC-10:画布连线拖拽期间(connect-ready),所有节点 Handle 显示
- [x] AC-11:连线结束后 Handle 恢复到之前的显示状态
交互
- [x] AC-12:单击未选中节点 → 进入 selected 状态
- [x] AC-13:按住拖拽 → 移动节点,不触发选中
- [x] AC-14a:点击画布空白区 → 退出 selected,回到 default(Vue Flow 原生行为)
- [ ] AC-14b:按 Escape → 退出 selected(未显式实现,依赖 Vue Flow 引擎行为,未验证)
视觉
- [x] AC-15:悬浮面板跟随节点位置,不遮挡内容区
通用
- [x] AC-16:TextNode、ImageNode、VideoNode 三种节点均通过 NodeShell 实现双模式
BDD Scenarios
gherkin
Feature: NodeShell 紧凑/编辑双模式
Background:
Given 画布中有一个文本节点 "T1"
And "T1" 处于未选中状态
# --- 紧凑模式 ---
Scenario: 未选中节点展示紧凑模式
Then "T1" 显示标题和内容区
And "T1" 不显示 Toolbar
And "T1" 不显示 Prompt 输入区和 ActionBar
And "T1" 不显示 Handle
# --- 选中进入编辑模式 ---
Scenario: 单击节点进入编辑模式
When 用户单击 "T1"
Then "T1" 变为选中状态
And "T1" 上方悬浮显示 Toolbar
And "T1" 下方悬浮显示 Prompt 输入区和 ActionBar
And "T1" 左右 Handle 显示
And "T1" 标题可编辑
Scenario: 选中态编辑标题并回写
Given "T1" 处于选中状态
When 用户将标题修改为 "新标题"
Then 节点数据中 title 字段更新为 "新标题"
# --- 退出编辑模式 ---
Scenario: 点击空白区退出编辑模式
Given "T1" 处于选中状态
When 用户点击画布空白区域
Then "T1" 回到未选中状态
And Toolbar、Prompt、ActionBar 消失
And Handle 隐藏
Scenario: 按 Escape 退出编辑模式
Given "T1" 处于选中状态
When 用户按下 Escape 键
Then "T1" 回到未选中状态
# --- Handle 显示逻辑 ---
Scenario: hover 显示 Handle
When 用户鼠标悬停在 "T1" 上
Then "T1" 左右 Handle 显示
When 用户鼠标移出 "T1"
Then "T1" Handle 隐藏
Scenario: 连线拖拽期间所有节点显示 Handle
Given 画布中还有一个图片节点 "I1" 且未选中
When 用户从 "T1" 的 source handle 开始拖拽连线
Then "T1" 和 "I1" 的 Handle 均显示
When 用户释放连线(无论是否连接成功)
Then Handle 恢复到拖拽前状态
# --- 拖拽不触发选中 ---
Scenario: 按住拖拽移动节点不触发选中
When 用户按住 "T1" 并拖拽移动
Then "T1" 跟随鼠标移动
And "T1" 不进入选中状态
And 不显示 Toolbar、Prompt、ActionBar
# --- 多选隐藏浮动面板 ---
Scenario: 多选时不显示浮动面板
Given 画布中还有一个图片节点 "I1"
When 用户框选 "T1" 和 "I1"
Then "T1" 和 "I1" 均为选中状态
And 两个节点均不显示 Toolbar、Prompt、ActionBar
And 两个节点的 Handle 均显示
Scenario: 拖拽选中节点时浮动面板隐藏
Given "T1" 处于选中状态且显示浮动面板
When 用户按住 "T1" 开始拖拽
Then Toolbar、Prompt、ActionBar 隐藏
When 用户释放鼠标
Then Toolbar、Prompt、ActionBar 恢复显示
# --- 跨节点类型 ---
Scenario: 三种节点类型均支持双模式
Given 画布中有文本节点、图片节点、视频节点各一个
When 依次单击每个节点
Then 每个节点均进入编辑模式,显示各自的 Toolbar 和 Prompt
When 依次点击空白区
Then 每个节点均回到紧凑模式TDD 单元测试要点
NodeShell 组件测试(Vitest + Vue Test Utils)
| # | 测试要点 | AC | 状态 |
|---|---|---|---|
| T-01 | selected=false 时 toolbar slot 不渲染到 DOM | AC-02 | ✅ |
| T-02 | selected=false 时 prompt slot 和 ActionBar 不渲染到 DOM | AC-03 | ✅ |
| T-03 | selected=false 且 hovered=false 时 Handle 不渲染 | AC-04 | ✅ |
| T-04 | selected=true 时 toolbar slot 渲染 | AC-05 | ✅ |
| T-05 | selected=true 时 prompt slot 和 ActionBar 渲染 | AC-06 | ✅ |
| T-06 | selected=true 时 Handle 渲染 | AC-07 | ✅ |
| T-07 | hover 状态切换 Handle 显示/隐藏 | AC-09 | ✅ |
| T-08 | 多选时浮动面板不渲染 | AC-08a | ✅ |
| T-09 | 拖拽时浮动面板不渲染 | AC-08b | ✅ |
| T-10 | 选中时 border 为 brand-500 色 | 视觉 | ✅ |
| T-11a | hover 时 border 为 50% 品牌色 | 视觉 | ✅ |
| T-12a | 标题编辑触发 update:title 事件 | AC-08 | 未覆盖(NodeHeader 被 mock) |
状态计算逻辑测试(纯函数 / composable)
| # | 测试要点 | AC | 状态 |
|---|---|---|---|
| T-13 | connectStarted=true 时所有节点返回 handle 可见 | AC-10 | ✅ |
| T-14a | connectEnded 后 handle 可见性恢复到之前状态 | AC-11 | ✅ |
| T-15a | 给定 { selected, hovered, connectStarted, dragging, isMultiSelected } 输入,正确计算各区域可见性 | 状态机 | ✅ |
| T-16a | 多选时 mode 为 compact,浮动面板隐藏 | AC-08a | ✅ |
| T-17 | 拖拽时浮动面板隐藏但 handle 保持显示 | AC-08b | ✅ |
composable useNodeShellState(已实现):
- 输入:
{ selected, isHovered, isConnecting, dragging, isMultiSelected } - 输出:
{ mode, showHandles, showFloatingPanels }
集成测试(节点类型验证)
| # | 测试要点 | AC | 状态 |
|---|---|---|---|
| T-18 | TextNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-16 | 未覆盖 |
| T-19 | ImageNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-16 | 未覆盖 |
| T-20 | VideoNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-16 | 未覆盖 |
已知缺口
功能
- Escape 退出编辑:AC-14b 依赖画布引擎原生行为,未验证 Escape 是否自动 deselect
测试
- T-12a:标题编辑
update:title事件未在 nodeShell.test.ts 中覆盖(NodeHeader 被 mock) - T-18~20:三种节点类型的集成测试未编写
- useGenerate:无独立测试文件
AI 生成状态机
用户在任意节点点击生成后,系统调用 AI API 完成生成,结果自动填充节点,全程有明确的状态反馈。
状态转换规则
行为约束
约束 G1:状态转换 — 仅允许上图中的转换路径,其他转换为非法。
约束 G2:generating 期间重复提交 — 取消当前请求,立即发起新请求;旧请求回调被忽略。
约束 G3:generating 期间 loading — 内容区叠加半透明遮罩 + ChatShimmer 微光文字动画("AI 生成中..."),旧内容可见但不可交互。
约束 G4:生成结果填充 — API 成功返回后,结果写入节点数据(TextNode → content,ImageNode → imageUrl/results,VideoNode → videoUrl),状态变为 done。
约束 G5:错误不丢失用户输入 — API 失败后状态变为 error,显示 toast 错误信息;prompt、现有内容、已上传文件均保持不变。
约束 G6:不兼容上游输入 — 全量透传给后端处理,前端不做兼容性过滤。详见 data-flow 约束 6。
约束 G7:组件卸载清理 — onUnmounted 中 abort 当前请求,防止悬挂回调。
Acceptance Criteria
- [x] AC-G01:节点初始状态为
idle - [x] AC-G02:点击生成后立即进入
generating,按钮禁用 - [x] AC-G03a:
generating期间,内容区显示 ChatShimmer 微光文字动画 - [x] AC-G03b:
generating期间,有已有内容时叠加遮罩层,旧内容可见但不可交互 - [x] AC-G03c:
generating期间,TextNode 编辑器不可交互(覆盖层阻断鼠标事件) - [x] AC-G03d:生成完成(done/error)后,loading 和遮罩立即消失,恢复交互
- [x] AC-G03e:三种节点类型均支持上述 loading 行为
- [x] AC-G04:API 成功后进入
done,结果写入节点,loading 消失 - [x] AC-G05:API 失败后进入
error,不丢失用户输入,loading 消失 - [x] AC-G06:连续调用生成时,abort 旧请求,发起新请求
- [x] AC-G07:组件卸载时 abort 进行中的请求
- [x] AC-G08:生成时注入上游节点数据到请求参数 → 详见 data-flow
BDD 场景
gherkin
Feature: AI 生成状态机
Scenario: 正常生成文本
Given 一个文本节点处于 idle 状态,已输入 prompt
When 用户点击生成按钮
Then 节点状态变为 generating,按钮禁用
When AI 生成成功返回
Then 节点状态变为 done,内容区显示生成结果
Scenario: 生成失败保持用户输入
Given 一个文本节点处于 idle 状态,已输入 prompt "测试失败"
When 用户点击生成按钮,API 请求失败
Then 节点状态变为 error
And prompt 输入保持 "测试失败" 不变,显示错误 toast
Scenario: 连续点击 abort 旧请求
Given 一个文本节点处于 generating 状态(第一次请求)
When 用户再次点击生成
Then 第一次请求被 abort,发起新的生成请求
Scenario: 组件卸载时 abort
Given 一个文本节点处于 generating 状态
When 用户导航离开画布页面
Then 进行中的请求被 abort
Scenario: 生成期间 loading 遮罩
Given 一个图片节点已有图片,处于 idle 状态
When 用户点击生成按钮
Then 内容区叠加遮罩层,旧内容可见但不可交互
When 生成完成
Then 遮罩消失,内容区恢复交互TDD 测试要点
生成流程逻辑(useGenerate composable):
- [~] idle → generating 状态转换正确
- [~] 成功后通过 WS Push 更新节点状态和数据
- [~] 失败后 status=error 且 prompt 保持不变
- [~] HTTP 请求失败时回滚节点状态
注:生成流程已改为 HTTP POST 触发 + WS Push 接收结果。当前
useGeneratecomposable 尚未有独立测试文件。
Out of Scope(生成)
- 生成结果的版本历史
- 多变体并行生成的进度展示
- 流式生成(streaming)
Out of Scope
- Toolbar 内部内容 — 各节点类型的 Toolbar 具体按钮和逻辑(文本格式化、图片工具条等),由各节点 spec 定义
- 节点特有的内容区逻辑 — 富文本编辑、图片上传/预览、视频播放等,由各节点 spec 定义
- 节点拖拽/位置管理 — 由画布引擎处理
- 悬浮面板的具体像素尺寸和颜色 — 由 design-system 定义
- 多选框选交互 — 框选本身由画布引擎处理,NodeShell 通过
IS_MULTI_SELECT_KEY注入感知多选状态