自由分割布局(free-split)
free-split 是 0.5.0 新增的容器布局模式:把一个容器内部切成一棵递归分割树,每个格子(leaf)放一个组件。支持在画布上拖分隔条调比例、切分、合并、删除、移动/交换,以及把组件拖进格子。
一句话区分:
split是一层左右/上下按比例切;free-split是可任意嵌套的分割树(左切完再上下切),每格一个组件、可视化增删改。
数据模型
ts
interface FreeSplitContainerLayout {
mode: 'free-split'
root: SplitNode // 分割树根(可以是单个 leaf,也可以是 branch)
structureLocked?: boolean // 锁结构:只能填换内容,不能切/合/删
gap?: number // 格子间距 px,默认 0
}
type SplitNode = SplitBranch | SplitLeaf
interface SplitBranch { // 内部节点:沿 dir 把空间切成 children
type: 'split'
nodeId: string // 容器内唯一
dir: 'row' | 'col' // row=左右切,col=上下切
children: SplitNode[] // ≥2
sizes: NodeSize[] // 跟 children 一一对应
}
interface SplitLeaf { // 叶子 = 一格
type: 'leaf'
leafId: string // 容器内唯一
instanceId: string | null // 指向容器的一个子实例;null=空格
locked?: boolean // 该格结构锁
}
interface NodeSize {
mode: 'ratio' | 'px' // ratio=按权重分剩余;px=固定像素
value: number // ratio>0;px≥0
min?: number; max?: number
}约束(SDK 与后端同口径校验):leafId / nodeId 容器内唯一(共用命名空间)、instanceId 一个实例只在一个叶、NodeSize 合法、树深 ≤ 8。非法树渲染时降级平铺(不崩)。
在工作台里用
选中一个容器 → 「容器内布局」面板 mode 选 free-split(空容器才可切;容器已有子组件时该项置灰,先清空)。切完是一个空格,然后:
| 操作 | 手势 |
|---|---|
| 切分 | 格子右上角 ⬌(竖切=左右)/ ⬍(横切=上下),或从格子边缘往里拖 |
| 调比例(resize) | 拖两格之间的分隔条 |
| 合并 | 双击分隔条(相邻一侧为空格时),或 hover 分隔条出现的合并按钮 |
| 删除分区 | 格子右上角 ✕(内容随之清除,相邻格吸收空间) |
| 移动 / 交换 | 拖格子右上角 ⠿ 手柄到另一格:目标空=移动过去,目标有内容=两格交换 |
| 填内容 | 从组件面板把组件拖进空格 |
每个操作都是一次 undo;锁结构(structureLocked / leaf.locked)时,切/合/删/move 入口自动隐藏,但填换内容仍可用。
宿主接入(重点)
free-split 的渲染和编辑是分开的两件事,接入时分别处理:
1. 渲染 —— data-driven,零 opt-in
只要页面文档里有合法的 free-split 容器,HyperCardPageRenderer 就会自动按分割树渲染,宿主不需要任何开关:
vue
<HyperCardPageRenderer :payload="payload" />没有 free-split 数据的页面渲染完全不变(老宿主升级到 0.5.0 零行为变化)。
2. 编辑 —— 受 per-canvas gate 控
要让用户在你的设计器里创建/编辑 free-split(出分隔条/热区 overlay、切分/合并/resize/拖填),给 HyperCardCanvasDesigner 传 :free-split-enabled="true":
vue
<HyperCardCanvasDesigner
v-model="doc"
:component-source-map="sourceMap"
:free-split-enabled="true"
mode="design"
/>不传(或传 false)→ 仍然渲染已有 free-split,但不暴露编辑能力(等价只读)。
3. 后端 —— 存储校验必须放行 free-split
宿主自己的页面保存接口要让 containerLayout.mode='free-split' 通过,并校验分割树(与 SDK 同口径:id 唯一、NodeSize、深度 ≤ 8)。否则保存会被拒(常见表现:/versions 返回 400 mode must be one of 'none'|'free'|...)。宿主后端需实现等价的分割树 / NodeSize 校验(id 唯一、ratio/px 合法、深度 ≤ 8)。
⚠️ 这一步最容易漏:业务宿主用各自的后端,SDK 这边放行不代表你的后端放行。
4. drop 灌内容 —— 必须走 handle.freeSplit.fill
free-split 容器的子实例不能用普通 addInstance 直插(会被守门拒绝)。自定义 drop 流程时:命中坑用 handle.getFreeSplitDropTarget(point),空叶用 handle.freeSplit.fill 填:
ts
const target = handle.getFreeSplitDropTarget({ x, y })
if (target) {
// 只填空叶;满叶按需走「叶内容自身的嵌套 outlet」普通 slot-add,或忽略
handle.freeSplit.fill(target.containerId, target.leafId, newInstance)
}handle API
ts
handle.freeSplit.fill(containerId, leafId, instance) // 填实例进空叶(ctx 纠正归属)
handle.freeSplit.clear(containerId, leafId) // 清空叶(级联删子树)
handle.freeSplit.swap(containerId, leafA, leafB) // 对调两叶(移动/交换)
handle.freeSplit.resize(containerId, nodeId, sizes) // 改某 branch 比例
handle.freeSplit.split(containerId, leafId, edge, share) // 切叶 → {leafId, nodeId}
handle.freeSplit.merge(containerId, leafId) // 合并(删空叶,相邻吸收)
handle.getFreeSplitDropTarget(point, root?) // DOM 命中坑 → {containerId, leafId, leafEl}选中走并行通道(leaf/divider 不混进普通 selectedIds):handle.selectFreeSplitLeaf / selectFreeSplitDivider / clearFreeSplitSelection / handle.freeSplitSelection。
从 0.4.x 升级到 0.5.0
- 不用 free-split 的页面/宿主:零改动,升依赖即可(渲染 data-driven 保证非 free-split 页零变化)。
- 要用 free-split:① 设计器传
:free-split-enabled="true"② 后端放行 free-split 校验。 - BREAKING:
HyperCardPageRenderer移除了freeSplitEnabledprop(渲染改 data-driven,不再受其控)。如果你之前给 Renderer 传过这个 prop,删掉即可(渲染照常)。HyperCardCanvasDesigner的freeSplitEnabledprop 保留(它控编辑)。
进一步阅读
- LayoutBox 布局模型 — containerLayout 各模式总览
- 设计器接入