Skip to content

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                │
       └─────────────────────────────────────┘

状态定义

状态HandleToolbarPrompt + ActionBar内容区
default隐藏隐藏隐藏只读展示
hovered显示隐藏隐藏只读展示
selected显示悬浮显示悬浮显示可交互
connect-ready显示隐藏隐藏只读展示

转换规则

事件备注
defaultmouseEnterhovered
defaultclickselected
defaultconnectStart (全局)connect-ready画布开始连线拖拽
hoveredmouseLeavedefault
hoveredclickselected
hoveredconnectStart (全局)connect-ready
selectedclickAway / Escapedefault点击画布空白或按 Esc
selectedconnectStart (全局)connect-ready连线期间临时退出编辑态
connect-readyconnectEnd (全局)default恢复到连线前的状态

约束

  • selectedhovered 不叠加 — 选中态已包含 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-01selected=false 时 toolbar slot 不渲染到 DOMAC-02
T-02selected=false 时 prompt slot 和 ActionBar 不渲染到 DOMAC-03
T-03selected=falsehovered=false 时 Handle 不渲染AC-04
T-04selected=true 时 toolbar slot 渲染AC-05
T-05selected=true 时 prompt slot 和 ActionBar 渲染AC-06
T-06selected=true 时 Handle 渲染AC-07
T-07hover 状态切换 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-11connectStarted=true 时所有节点返回 handle 可见AC-10
T-12connectEnded 后 handle 可见性恢复到之前状态AC-11
T-13给定 { selected, hovered, connectStarted } 输入,正确计算各区域可见性状态机

建议新建 composable useNodeShellState

  • 输入:{ selected, hovered, connectStarted }
  • 输出:{ showToolbar, showPrompt, showActionBar, showHandles, shadowLevel }

集成测试(节点类型验证)

#测试要点AC状态
T-14TextNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确AC-19未覆盖
T-15ImageNode 使用 NodeShell,selected 切换时 toolbar/prompt 显隐正确AC-19未覆盖
T-16VideoNode 使用 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 只响应 selected prop 变化