Appearance
Feature: 通用节点框架(NodeShell)
一句话目标
为三种节点类型提供统一的容器,通过选中/未选中两种模式切换紧凑展示与完整编辑体验。
卡片结构
节点有两种视觉模式:
未选中态(紧凑模式):
┌─────────────────────────┐
│ 标题 │
│ │
│ [内容区] │
│ │
└─────────────────────────┘选中态(编辑模式):
┌─ Toolbar(悬浮)──────┐ ← 浮在节点上方
│ 格式工具 / 内容操作 │
└───────────────────────┘
○ ┌─────────────────────────┐ ○ ← Handle 在选中/hover 时显示
│ 标题 │
│ │
│ [内容区] │
│ │
└─────────────────────────┘
┌─ Prompt + GenerateBar ─┐ ← 浮在节点下方
│ [Prompt 输入] │
│ 模型 | 变体 | 生成按钮 │
└───────────────────────┘行为约束
约束 1:未选中态 — 紧凑展示
前置条件: 节点未被选中且非 hover 状态 行为: 节点只渲染 NodeHeader(标题 + 类型图标)和内容区(default slot)。Toolbar slot、Prompt slot、NodeActionBar 均不渲染。Handle 隐藏。 后置条件: 节点呈紧凑卡片形态
约束 2:选中态 — 完整编辑
前置条件: 节点被选中(单选或多选中的一个) 行为: 上方悬浮渲染 toolbar slot(内容操作工具),下方依次渲染 prompt slot 和 NodeActionBar。Handle 显示。内容区可交互(编辑文本、查看图片等)。 后置条件: 节点进入编辑模式,用户可输入 prompt 并触发生成
约束 3:连接点显示
前置条件: 节点在画布中 行为: Handle 在以下情况显示:节点被选中、鼠标 hover 节点、画布中正在进行连线拖拽。其余时候隐藏。 后置条件: 左侧 target handle、右侧 source handle 可见
约束 4:点击与拖拽分离
前置条件: 节点未选中 行为: 单击 → 选中节点;按住拖拽 → 移动节点,不改变选中状态 后置条件: 两种交互互不干扰
约束 5:右键上下文菜单
前置条件: 鼠标在节点上 行为: 右键弹出上下文菜单,包含:复制、粘贴、拷贝、删除 后置条件: 选择操作后执行对应行为
约束 6:阴影状态层级
前置条件: 节点在画布中 行为: 阴影优先级 selected > dragging > default(具体样式见 design-system) 后置条件: 视觉层级清晰
约束 7:标题编辑
前置条件: 节点被选中 行为: 标题区可编辑,变更通过事件回写 后置条件: 标题持久化到节点数据
约束 8:悬浮面板定位
前置条件: 节点被选中 行为: Toolbar 悬浮于节点卡片上方,Prompt + ActionBar 悬浮于节点卡片下方,定位相对于节点边界 后置条件: 悬浮面板不遮挡节点内容,跟随节点移动/缩放
约束 9:模式切换过渡
前置条件: 节点选中状态发生变化 行为: 悬浮面板的出现/消失有过渡动画(fade + slide),避免突兀 后置条件: 视觉平滑过渡
状态机
┌─────────────┐
│ default │ 紧凑模式:仅标题+内容
└──────┬──────┘
│
┌────────────┼────────────┐
│ hover │ click │ connectStart
▼ ▼ ▼
┌───────────┐ ┌──────────┐ ┌───────────────┐
│ hovered │ │ selected │ │ connect-ready │
│ Handle 显示│ │ 编辑模式 │ │ Handle 显示 │
└───────────┘ └──────────┘ └───────────────┘
│ │ │
│ mouseLeave │ clickAway │ connectEnd
▼ ▼ ▼
┌─────────────────────────────────────┐
│ default │
└─────────────────────────────────────┘状态定义
| 状态 | Handle | Toolbar | Prompt + ActionBar | 内容区 |
|---|---|---|---|---|
| default | 隐藏 | 隐藏 | 隐藏 | 只读展示 |
| hovered | 显示 | 隐藏 | 隐藏 | 只读展示 |
| selected | 显示 | 悬浮显示 | 悬浮显示 | 可交互 |
| 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是全局事件驱动,与节点自身交互无关- 多选时每个被选中的节点独立进入
selected状态
Slots
| Slot 名 | 显示时机 | 用途 |
|---|---|---|
| default | 始终 | 节点主内容区(编辑器/图片/视频) |
| toolbar | 选中时 | 上方悬浮的内容工具栏 |
| prompt | 选中时 | 下方悬浮的 Prompt 输入区域 |
Acceptance Criteria
紧凑模式(default 状态)
- [x] AC-01:未选中且非 hover 时,仅渲染 NodeHeader 和内容区(default slot)
- [x] AC-02:未选中时 toolbar slot 不渲染
- [x] AC-03:未选中时 prompt slot 和 NodeActionBar 不渲染
- [x] AC-04:未选中且非 hover 时 Handle 不渲染
编辑模式(selected 状态)
- [x] AC-05:选中时 toolbar slot 悬浮渲染于节点上方
- [x] AC-06:选中时 prompt slot 和 NodeActionBar 悬浮渲染于节点下方
- [x] AC-07:选中时 Handle 显示
- [x] AC-08:选中时标题可编辑,变更回写到节点数据
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-14:点击画布空白区或按 Escape → 退出 selected,回到 default
- [x] AC-15:右键弹出上下文菜单,包含复制/粘贴/拷贝/删除
视觉
- [x] AC-16:阴影优先级 selected > dragging > default
- [x] AC-17:悬浮面板出现/消失有过渡动画(fade + slide)
- [x] AC-18:悬浮面板跟随节点位置,不遮挡内容区
通用
- [x] AC-19: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: 右键打开上下文菜单
When 用户在 "T1" 上右键
Then 弹出上下文菜单
And 菜单包含复制、粘贴、拷贝、删除选项
Scenario: 上下文菜单执行删除
When 用户在 "T1" 上右键
And 用户点击 "删除"
Then "T1" 从画布中移除
# --- 视觉过渡 ---
Scenario: 模式切换过渡动画
When 用户单击 "T1"
Then 悬浮面板以 fade+slide 动画出现
When 用户点击画布空白区域
Then 悬浮面板以 fade+slide 动画消失
Scenario: 阴影状态切换
When 用户单击 "T1"
Then "T1" 显示 selected 阴影
When 用户按住 "T1" 拖拽
Then "T1" 显示 dragging 阴影
When 用户释放并点击空白区
Then "T1" 显示 default 阴影
# --- 跨节点类型 ---
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 | 阴影样式:selected > dragging > default 三种状态正确切换 | AC-16 | ✅ |
| T-09 | 标题编辑触发 update:title 事件 | AC-08 | 未覆盖(NodeHeader 被 mock) |
| T-10 | 右键事件触发上下文菜单渲染,包含四个操作项 | AC-15 | 未覆盖(UContextMenu 被 mock) |
状态计算逻辑测试(纯函数 / composable)
| # | 测试要点 | AC | 状态 |
|---|---|---|---|
| T-11 | connectStarted=true 时所有节点返回 handle 可见 | AC-10 | ✅ |
| T-12 | connectEnded 后 handle 可见性恢复到之前状态 | AC-11 | ✅ |
| T-13 | 给定 { selected, hovered, connectStarted } 输入,正确计算各区域可见性 | 状态机 | ✅ |
建议新建 composable useNodeShellState:
- 输入:
{ selected, hovered, connectStarted } - 输出:
{ showToolbar, showPrompt, showActionBar, showHandles, shadowLevel }
集成测试(节点类型验证)
| # | 测试要点 | AC | 状态 |
|---|---|---|---|
| T-14 | TextNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-19 | 未覆盖 |
| T-15 | ImageNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-19 | 未覆盖 |
| T-16 | VideoNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确 | AC-19 | 未覆盖 |
已知缺口
功能
- 复制节点 / 复制为副本:右键菜单 UI 已就绪(emit
copyNode/duplicate),但flowStore缺少对应方法,事件被静默忽略 - 粘贴节点:AC-15 提到"粘贴",菜单中未包含(需要剪贴板序列化方案)
- Escape 退出编辑:AC-14 依赖画布引擎原生行为,未验证 Escape 是否自动 deselect
测试
- E2E BDD:spec 中 13 个 Gherkin 场景无 Playwright 覆盖(
@nuxt/test-utils/playwright与 SSG 模式不兼容) - T-09:标题编辑
update:title事件未在 nodeShell.test.ts 中覆盖(NodeHeader 被 mock) - T-10:右键菜单渲染未在 nodeShell.test.ts 中覆盖(UContextMenu 被 mock)
- T-14~16:三种节点类型的集成测试未编写
Out of Scope
- Toolbar 内部内容 — 各节点类型的 Toolbar 具体按钮和逻辑(文本格式化、图片工具条等),由各节点 spec 定义
- GenerateBar 内部行为 — 模型选择、上游数据预览、生成按钮逻辑,由独立 spec 定义(待创建)
- 节点特有的内容区逻辑 — 富文本编辑、图片上传/预览、视频播放等,由各节点 spec 定义
- 节点拖拽/位置管理 — 由画布引擎处理
- 上下文菜单各操作的具体实现 — 复制/粘贴/删除的业务逻辑不在本 spec 范围,仅定义菜单触发和选项列表
- 悬浮面板的具体像素尺寸和颜色 — 由 design-system 定义
- 多选框选交互 — 框选本身由画布引擎处理,NodeShell 只响应
selectedprop 变化