指南
复选框与拖拽
showCheckbox 的父子联动与半选、checkStrictly、勾选方法;draggable 的三分区放置、allow 钩子、防自嵌套与 OKR 跨树规则。
两块交互能力,语义都对齐 el-tree 的使用习惯。
复选框选择
<OkrTree data={data} nodeKey="id" showCheckbox defaultCheckedKeys={[3]} />事件输出(最近 8 条)
尚未触发事件,试试点击节点 / 右键 / 展开按钮。
xxx科技有有限公司
复选框
checkStrictly 切换、事件日志、方法演示。
查看源码
'use client'
import { useMemo, useRef, useState } from 'react'
import { OkrTree, type OkrTreeHandle, type TreeCheckInfo, type TreeNodeData } from 'react-okr-tree'
import 'react-okr-tree/style.css'
import { keyedData } from './data'
import { EventLog, LOG_LIMIT, type LogLine } from './event-log'
/**
* 复选框模式(对应源项目 playground/components/demos/BaseCheckbox.vue)
*
* `showCheckbox` 开勾选框,父子联动带半选态;`checkStrictly` 关掉联动后勾谁只影响谁(也就没有半选)。
* `onCheck(data, info)` 只在**用户点击**勾选框时触发(`setCheckedKeys()` 不触发),
* `onCheckChange(data, checked, indeterminate)` 则每个状态变化的节点各触发一次,含联动与批量设置。
*
* React 与 Vue 的差别只在读结果:`getCheckedKeys()` / `getHalfCheckedKeys()` 是命令式方法,
* 读的是内部 store 而不是 React state,所以只能在事件回调里取——渲染期取不到,
* 也就没法「派生出勾选态」。本例把结果写进日志面板。
* OKR 模式下左右两树勾选独立维护,方法按 key 对两树同时生效。
*/
export function CheckboxDemo() {
const data = useMemo(keyedData, [])
const handle = useRef<OkrTreeHandle>(null)
const [checkStrictly, setCheckStrictly] = useState(false)
const [lines, setLines] = useState<LogLine[]>([])
function push(event: string, text: string) {
setLines(prev => [{ event, text }, ...prev].slice(0, LOG_LIMIT))
}
function handleCheck(data: TreeNodeData, info: TreeCheckInfo) {
push(
'onCheck',
`「${data.label}」→ 已选 ${info.checkedKeys.length} 个,半选 ${info.halfCheckedKeys.length} 个`
)
}
function handleCheckChange(data: TreeNodeData, checked: boolean, indeterminate: boolean) {
push(
'onCheckChange',
`「${data.label}」→ ${checked ? '已选' : indeterminate ? '半选' : '未选'}`
)
}
function setChecked() {
handle.current?.setCheckedKeys([7, 8])
push(
'setCheckedKeys',
checkStrictly
? '勾选 [7, 8](独立模式:父节点 6 不受影响)'
: '勾选 [7, 8](联动模式:父节点 6 自动变半选)'
)
}
function readKeys() {
const h = handle.current
if (!h) return
push(
'getCheckedKeys',
`checked=[${h.getCheckedKeys().join(', ')}] half=[${h.getHalfCheckedKeys().join(', ')}]`
)
}
const btn = 'rounded-lg border border-fd-border px-3 py-1 text-sm'
return (
<>
<div className="mb-3 flex flex-wrap gap-2">
<button
type="button"
onClick={() => setCheckStrictly(v => !v)}
className={`${btn} ${checkStrictly ? 'bg-fd-accent font-medium' : ''}`}
>
checkStrictly(父子不联动):{checkStrictly ? '开' : '关'}
</button>
<button type="button" onClick={setChecked} className={`${btn} hover:bg-fd-accent`}>
setCheckedKeys([7, 8])
</button>
<button type="button" onClick={readKeys} className={`${btn} hover:bg-fd-accent`}>
getCheckedKeys / getHalfCheckedKeys
</button>
</div>
<EventLog lines={lines} onClear={() => setLines([])} />
<OkrTree
ref={handle}
data={data}
nodeKey="id"
direction="horizontal"
showCollapsable
showCheckbox
checkStrictly={checkStrictly}
defaultCheckedKeys={[3, 4]}
onCheck={handleCheck}
onCheckChange={handleCheckChange}
/>
</>
)
}
联动规则:
- 勾选父节点向下联动全部后代(含
disabled节点——disabled只阻止直接点击),祖先按子树重算全选 / 半选。「全部子节点选中 ⇒ 父选中」,所以唯一子链选中会让父节点直接变全选。 checkStrictly关闭联动,勾选只作用于自身;运行时切换后新交互按新模式执行。defaultCheckedKeys需配nodeKey;运行时变更会先清空再按新列表应用,data重建后不恢复。- OKR 模式下左右两树的勾选独立维护(点击只作用于所在树),而
setCheckedKeys/getCheckedKeys等按 key 对两树同时生效 / 合并去重。 - 勾选态在增删子节点、懒加载 resolve、增量重建后自动重算祖先。
回调与方法:onCheck 只在点击复选框时触发(携带全量勾选信息),onCheckChange 在每个状态变化的节点各触发一次(联动、setCheckedKeys 批量、增删级联都覆盖)。方法为 getCheckedKeys / getCheckedNodes / setCheckedKeys / getHalfCheckedKeys / getHalfCheckedNodes / isChecked。键盘 Space 切换勾选、Enter 选中;treeitem 输出 aria-checked,半选为 mixed(见键盘导航)。
样式复用既有变量(边框取 --okr-line-color、填充取 --okr-current-bg),六套主题与自定义主题零配置适配。
拖拽调整层级
<OkrTree data={data} nodeKey="id" draggable allowDrop={allowDrop} onNodeDrop={onDrop} />事件输出(最近 8 条)
尚未触发事件,试试点击节点 / 右键 / 展开按钮。
xxx科技有有限公司
拖拽换父级
prev / inner / next 三分区、allowDrag / allowDrop 钩子、事件日志。
查看源码
'use client'
import { useMemo, useState } from 'react'
import { OkrTree, type DropType, type TreeNode } from 'react-okr-tree'
import 'react-okr-tree/style.css'
import { keyedData } from './data'
import { EventLog, LOG_LIMIT, type LogLine } from './event-log'
/**
* 拖拽调整层级(对应源项目 playground/components/demos/BaseDraggable.vue)
*
* `draggable` 开 HTML5 拖拽,落点按目标卡片的 25% / 50% / 25% 分三区:
* prev(排在目标前)/ inner(成为目标的子节点,目标自动展开)/ next(排在目标后)。
* **分区轴随方向换**:`direction="horizontal"` 时同层是上下排列,按 Y 轴分;
* 默认 vertical 时同层左右排列,按 X 轴分。指示线颜色走 `--okr-drop-color`。
*
* 两条硬性规则不是 `allowDrop` 能改的:不可放进自身或自己的子树;OKR 下跨左右树默认禁止
* (`allowDrop` 明确返回 true 才放开)。移动会同步回写源数据的 children。
*
* 回调参数与 Vue 同形,只是没有第三参 `nodeComponent`(D2):
* `onNodeDrop(draggingNode, dropNode, dropType)` 三个都是内部 TreeNode。
* 程序化入口是 `handle.moveNode(7, 2, 'inner')`,规则一致(本例未挂按钮)。
*/
export function DraggableDemo() {
const data = useMemo(keyedData, [])
const [lines, setLines] = useState<LogLine[]>([])
function push(event: string, text: string) {
setLines(prev => [{ event, text }, ...prev].slice(0, LOG_LIMIT))
}
/** 示例规则:财务部(id 9)不许被拖走 */
function allowDrag(node: TreeNode) {
return node.key !== 9
}
/** 示例规则:财务部也不能当落点;叶子不接受 inner(放成子节点后它就成了父级) */
function allowDrop(_dragging: TreeNode, target: TreeNode, type: DropType) {
if (target.key === 9) return false
return type !== 'inner' || !target.isLeaf
}
return (
<>
<EventLog lines={lines} onClear={() => setLines([])} />
<OkrTree
data={data}
nodeKey="id"
direction="horizontal"
showCollapsable
draggable
allowDrag={allowDrag}
allowDrop={allowDrop}
onNodeDragStart={node => push('onNodeDragStart', `开始拖动「${node.label}」`)}
onNodeDrop={(dragging, target, type) =>
push('onNodeDrop', `「${dragging.label}」→「${target.label}」的 ${type}`)
}
onNodeDragEnd={(dragging, target, type) =>
push(
'onNodeDragEnd',
target && type ? `「${dragging.label}」放置完成` : `「${dragging.label}」未完成放置`
)
}
/>
</>
)
}
- 放置分区为 25% / 50% / 25%,并按布局方向换轴:
horizontal(同级上下排列)按 Y 轴 prev 上 / inner 中 / next 下;vertical(同级左右排列)按 X 轴 prev 左 / inner 中 / next 右。指示线画在节点卡片上(prev / next 指示线、inner 虚线描边),颜色用--okr-drop-color定制。 - 移动同步修改你传入的源数据的
children(与append/remove同一套语义,冻结数据下跳过写入并警告,见数据变更与源数据回写),并修正整棵子树的level与节点注册表;inner 放置时目标自动展开。 - 硬性规则:不可拖到自身或自己的子树内(
allowDrop越不过这条),disabled节点不可拖。 - OKR 模式跨左右树默认禁止;
allowDrop明确返回true才放开,放开后整棵子树的isLeftChild标记与左右注册表随之迁移。 - 六个回调:
onNodeDragStart(node, event)、onNodeDragEnter / leave / over(draggingNode, dropNode, event)、onNodeDragEnd(draggingNode, dropNode | null, dropType | null, event)、onNodeDrop(draggingNode, dropNode, dropType, event)。未完成放置时onNodeDrop不触发、onNodeDragEnd的后两参为null。event是 React 合成事件,需要原生事件时取event.nativeEvent(D11)。 - 不用鼠标也能改层级:
moveNode(data, target, type)与拖拽走同一条路径,data/target均接受 key、data 对象、TreeNode实例三种形态。
相关页面
- 连接线的外观与两种渲染模式:主题与样式定制
- 命令式增删改与回写语义:数据变更与源数据回写