react-okr-tree Logoreact-okr-tree
指南

复选框与拖拽

showCheckbox 的父子联动与半选、checkStrictly、勾选方法;draggable 的三分区放置、allow 钩子、防自嵌套与 OKR 跨树规则。

两块交互能力,语义都对齐 el-tree 的使用习惯。

复选框选择

<OkrTree data={data} nodeKey="id" showCheckbox defaultCheckedKeys={[3]} />
事件输出(最近 8 条)

尚未触发事件,试试点击节点 / 右键 / 展开按钮。

复选框

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 条)

尚未触发事件,试试点击节点 / 右键 / 展开按钮。

拖拽换父级

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 的后两参为 nullevent 是 React 合成事件,需要原生事件时取 event.nativeEvent(D11)。
  • 不用鼠标也能改层级:moveNode(data, target, type) 与拖拽走同一条路径,data / target 均接受 key、data 对象、TreeNode 实例三种形态。

相关页面

本页目录