Skip to content

自由分割布局(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 移除了 freeSplitEnabled prop(渲染改 data-driven,不再受其控)。如果你之前给 Renderer 传过这个 prop,删掉即可(渲染照常)。HyperCardCanvasDesignerfreeSplitEnabled prop 保留(它控编辑)。

进一步阅读