Files
datalive-design/docs/设计器选中与分组交互规范.md
skycurtain c3846da8ae docs: 新增 DatAlive 设计器核心架构设计文档
- 新增设计器静态依赖分析方案,阐述基于 AST 的代码片段解析与依赖图谱构建
- 新增实体生命周期引擎实现规范,定义引擎驱动的事件分发与交互触发策略
- 新增悬浮组件架构设计规范,明确底层数据统一与上层视图分离的核心原则
- 新增应用运行模式架构规范,严格区分设计态、预览态与运行态的边界
- 新增设计器选中与分组交互规范,定义选中态、激活态及下钻交互的行为矩阵
2026-04-10 23:03:45 +08:00

185 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DatAlive 设计器选中与分组交互规范 (Editor Selection & Drill-down Strategy)
> 本文档定义了 DatAlive 设计器中核心的“选中(Selection)”与“下钻(Drill-down)”交互逻辑,旨在解决多层级组件嵌套下的精准选中难题。该规范参考了 Figma 等主流设计工具的交互模式,并严格适配 DatAlive 的底层 Schema 架构。
## 1. 核心概念 (Core Concepts)
为了实现自然流畅的深层嵌套组件操作,我们必须在编辑器**运行时的内存状态**中区分两个维度的“选中”概念:
### 1.1 选中态 (Selection State)
* **含义**:当前用户正在操作的目标组件。
* **表现**:显示实线边框、8个缩放控制点(Resize Handles)、右侧属性面板显示该组件属性。
* **操作**:此时按 Delete 删除该组件,按方向键移动该组件,或拖拽控制点改变其 `layout` 属性。
* **数据结构**`selectedIds: string[]`(支持多选)。
### 1.2 激活态 / 下钻态 (Activation State / Drill-down Context)
* **含义**:当前用户“进入”了某个容器组件(如 Group, Card, Modal)的内部上下文。
* **表现**:该容器本身不再被选中(无控制点),但显示虚线边框(或高亮背景),提示用户“你正在编辑这个容器内部的内容”。该容器外部的元素通常会变暗或不可响应点击(Focus Mode 专注模式)。
* **操作**:此时点击该容器内的任何子元素,将直接选中子元素,而不再选中该容器本身。
* **数据结构**`activeContainerId: string | null`
* `null`: 默认状态,处在当前 `Page` 的画布根层级。
* `string`: 处在某个具体容器组件的内部。
---
## 2. 状态定义 (Store Definition)
前端状态管理(如 Zustand/Redux)在设计态(`AppMode === 'design'`)应维护以下瞬时字段(这些字段**绝对不存入**持久化的 Schema 数据库中):
```typescript
interface EditorSelectionState {
// 当前选中的组件 ID 列表
selectedIds: string[];
// 当前激活的容器组件 ID(下钻上下文)
// null 表示未激活任何深层容器,处于 Root Page Context
activeContainerId: string | null;
// 【架构约束】:为实现 O(1) 的层级查找,
// 引擎在加载 Schema 树 (Page.components) 时,必须在内存中构建一个扁平化的辅助字典,
// 动态推导出每个组件的 parentId(持久化 Schema 中坚决不存 parentId)。
// componentNodeMap: Record<string, { id: string, parentId: string | null, type: string }>;
}
```
---
## 3. 交互行为矩阵 (Interaction Matrix)
### 3.1 单击行为 (Click)
**核心规则**:始终在 `activeContainerId` 定义的上下文中,寻找最高层级的子节点(冒泡拦截)。
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
| :--- | :--- | :--- | :--- | :--- |
| **基础选中** | `active: null` | 点击 Button A (根节点) | `sel: ['Button A']`<br>`active: null` | 最简单的选中。 |
| **选中容器** | `active: null` | 点击 Button B (属于 Group 1) | `sel: ['Group 1']`<br>`active: null` | 点击容器内元素,自动冒泡选中其最外层容器。 |
| **组内选中** | `active: 'Group 1'` | 点击 Button B (属于 Group 1) | `sel: ['Button B']`<br>`active: 'Group 1'` | 因为已激活 Group 1,点击其子元素直接选中子元素。 |
| **深层冒泡** | `active: 'Group 1'` | 点击 Button C (属于 Group 2,且 Group 2 在 Group 1 内) | `sel: ['Group 2']`<br>`active: 'Group 1'` | 冒泡到当前激活层级(Group 1)的直接子节点(Group 2)。 |
| **退出下钻** | `active: 'Group 1'` | 点击 Button A (根节点,在 Group 1 外) | `sel: ['Button A']`<br>`active: null` | 点击目标不在当前激活容器内,自动重置 `activeContainerId` 为 null,并选中目标。 |
| **空白重置** | 任意 | 点击画布空白处 | `sel: []`<br>`active: null` | 清空所有状态。 |
### 3.2 双击行为 (Double Click)
**核心规则**:进入(Drill-down)当前选中的容器组件内部。
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
| :--- | :--- | :--- | :--- | :--- |
| **进入容器** | `sel: ['Group 1']` | 双击 Group 1 | `sel: []` <br>`active: 'Group 1'` | **关键交互**:进入 Group 1 内部。此时 Group 1 变为容器上下文。 |
| **连续下钻** | `sel: ['Group 2']`<br>`active: 'Group 1'` | 双击 Group 2 | `sel: []`<br>`active: 'Group 2'` | 在 Group 1 内部继续下钻进入 Group 2。 |
| **文本编辑** | `sel: ['Button']` | 双击 Button | `active: 不变` | 【架构约束】:只有 `ComponentRegistry[type].isContainer === true` 的组件才允许双击下钻。原子组件双击通常触发“文本行内编辑”或忽略。 |
### 3.3 修饰键直选 (Cmd/Ctrl + Click)
**核心规则**:穿透所有嵌套容器,直接选中鼠标下方最深层的原子组件。
| 场景 | 当前状态 | 动作 | 结果状态 | 逻辑说明 |
| :--- | :--- | :--- | :--- | :--- |
| **穿透选中** | `active: null` | Cmd + 点击 Button C (在 Group 2 中) | `sel: ['Button C']`<br>`active: 'Group 2'` | 直接选中 Button C。为了方便后续操作,建议自动将 `activeContainerId` 设置为 Button C 的动态 `parentId`(即 Group 2)。 |
---
## 4. 算法实现逻辑 (Implementation Logic)
以下伪代码描述了画布引擎在接收到底层 DOM 点击事件时的核心处理流程,完美适配了 DatAlive 的无 `parentId` 纯净 Schema 树架构:
```typescript
/**
* 处理画布上的组件点击事件
* @param targetId - 鼠标直接点击到的最深层 DOM 对应的组件 ID
* @param isDoubleClick - 是否为双击事件
* @param isCmdPressed - 是否按住 Cmd/Ctrl 键
*/
function handleComponentClick(targetId: string, isDoubleClick: boolean, isCmdPressed: boolean) {
const {
componentNodeMap, // 引擎在内存中扁平化生成的辅助字典 (包含动态 parentId)
activeContainerId,
setActiveContainerId,
setSelectedIds
} = useEditorStore.getState();
const targetNode = componentNodeMap[targetId];
if (!targetNode) return;
// 1. 处理 Cmd/Ctrl 直选(穿透所有层级)
if (isCmdPressed) {
setSelectedIds([targetId]);
// 体验优化:直选后,自动激活其直接父级容器,方便后续点击旁边的兄弟元素
setActiveContainerId(targetNode.parentId || null);
return;
}
// 2. 处理双击下钻 (Drill-down)
if (isDoubleClick) {
// 【架构防呆】:必须去物料元数据注册表中查询该组件是否为合法的容器!
// 坚决不能写死 targetNode.type === 'Group'
const meta = ComponentRegistry[targetNode.type];
if (meta?.isContainer) {
setActiveContainerId(targetId);
setSelectedIds([]); // 清空选中,等待用户在组内进行下一步点击
}
return;
}
// 3. 处理普通单击(向上冒泡拦截逻辑)
let currentId: string | null = targetId;
let candidateId = targetId;
// 沿着辅助字典的 parentId 向上遍历,寻找在当前激活上下文中的正确选中目标
while (currentId) {
const node = componentNodeMap[currentId];
const parentId = node.parentId;
// 关键判断 A:如果当前节点的父级就是 activeContainerId
// 说明当前节点就是我们要选中的目标 (因为它刚好在当前“激活结界”的第一层)
if (parentId === activeContainerId) {
candidateId = currentId;
break;
}
// 关键判断 B:如果父级是 null (说明遍历到了 Root Page)
// 且当前没有任何容器被激活,目标就是这个最外层的根组件
if (parentId === null) {
candidateId = currentId;
break;
}
// 继续向上冒泡
currentId = parentId;
}
// 4. 跨层级逃逸检测(用户点击了 activeContainer 外部的其他组件)
// 引擎利用预计算的 nodePath 或向上追溯,判断算出来的 candidateId 是否根本不是 activeContainer 的后代
if (activeContainerId && !isDescendant(componentNodeMap, activeContainerId, candidateId)) {
// 强制退出当前的下钻状态,重置为画布根层级
setActiveContainerId(null);
// 在根层级的上下文中,重新计算一次单击冒泡逻辑
return handleComponentClick(targetId, false, false);
}
// 5. 最终应用选中状态,触发属性面板和控制点 UI 的重绘
setSelectedIds([candidateId]);
}
```
---
## 5. 视觉反馈规范 (Visual Feedback)
为了让实施人员清晰感知当前的层级上下文,画布引擎必须提供极其明确的视觉反馈:
| 状态 | 选中组件表现 | 激活容器表现 (Active Container) | 其他非相关元素表现 |
| :--- | :--- | :--- | :--- |
| **普通选中** | 实线蓝色边框<br>8个白色方块控制点<br>可拖拽移动 | N/A | 正常显示 |
| **下钻编辑中** | 实线蓝色边框<br>8个白色方块控制点<br>可拖拽移动 | **粗虚线灰色边框**<br>无控制点(不可拖拽)<br>左上角显示层级面包屑 (e.g. "Group A > Card B") | **半透明 (Opacity 0.6)**<br>屏蔽鼠标事件 (Focus Mode) |
| **悬停 (Hover)** | 蓝色细边框 (Outline)<br>不显示控制点 | N/A | 正常显示 |
---
## 6. 键盘快捷键辅助 (Shortcuts)
在复杂的层级树中,提供高效的键盘导航是专业设计器的标配:
* **`Esc` (逃逸键)**:
* 场景 1:如果 `selectedIds` 有值,清空选中(`selectedIds = []`)。
* 场景 2:如果 `selectedIds` 为空,但 `activeContainerId` 不为空(处于下钻态),则**向上跳出一级**(将 `activeContainerId` 设为其 `parentId`)。
* **`Enter` (深入键)**:
* 如果当前仅选中了一个 `isContainer: true` 的组件,直接进入该容器(等同于鼠标双击)。