Kanban
kanban看板 · @dnd-kit headless 多容器 + 跨列/列内拖拽 + 键盘可拖 + 列头统计槽 + 空列占位 · 受控 onMove(消费者改业务字段) · 商机看板/任务流转旗舰
用法
基础用法
受控 items + columns 分桶展示。getId 取稳定主键,getColumnId 取卡片当前所属列。
登录页改版
林晚晴
客户列表筛选
周明远
订单详情抽屉
陈策
商机看板拖拽
高敏
工作台图表
苏晓
tsx
<Kanban
items={cards}
columns={[
{ id: "backlog", title: "待规划" },
{ id: "todo", title: "待开发" },
{ id: "doing", title: "进行中" },
{ id: "done", title: "已完成" },
]}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={() => {}}
renderItem={(c) => (
<div className="rounded-[var(--radius)] border border-border bg-surface p-3 shadow-sm">
<p className="text-sm font-medium text-foreground">{c.title}</p>
<p className="mt-1 text-xs text-muted">{c.owner}</p>
</div>
)}
/>跨列拖拽(受控对账)
组件不直接改业务字段,只回吐 onMove;消费者据此改 status 并按 toIndex 插回目标列。支持指针与键盘(聚焦卡片 Space 抓起 / 方向键移动 / Space 放下)。
待规划1
登录页改版
林晚晴
待开发2
客户列表筛选
周明远
订单详情抽屉
陈策
进行中1
商机看板拖拽
高敏
已完成1
工作台图表
苏晓
tsx
const [cards, setCards] = useState(INITIAL);
<Kanban
items={cards}
columns={columns}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={(e) => setCards((prev) => applyMove(prev, e))}
renderItem={(c) => <Card title={c.title} owner={c.owner} />}
/>自定义列头统计
renderColumnHeader 拿到该列卡片,可做条数 / 进度等聚合。
待规划1
登录页改版
林晚晴
待开发2
客户列表筛选
周明远
订单详情抽屉
陈策
进行中1
商机看板拖拽
高敏
已完成1
工作台图表
苏晓
tsx
<Kanban
items={cards}
columns={columns}
getId={(c) => c.id}
getColumnId={(c) => c.status}
onMove={(e) => setCards((prev) => applyMove(prev, e))}
renderColumnHeader={(col, its) => (
<div className="mb-1 flex items-center justify-between">
<span className="text-sm font-semibold text-foreground">{col.title}</span>
<Tag tone="neutral" size="sm">{its.length}</Tag>
</div>
)}
renderItem={(c) => <Card title={c.title} owner={c.owner} />}
/>何时用
多列拖拽流转(商机看板、任务流、工单状态推进),卡片跨列/列内重排,受控数据 + onMove 回吐落定事件由你改业务字段。单列表内排序用 Sortable;节点连线画布用 Flow。
导入
ts
import { Kanban, resolveKanbanMove } from "@hulianui/ui"Props
KanbanProps<T> 泛型。
| 名称 | 类型 | 默认 | 说明 |
|---|---|---|---|
| columns * | KanbanColumn[] | — | 列定义(顺序即展示顺序)。每列 { id, title, header?, footer? } |
| items * | T[] | — | 受控卡片数组;组件按 getColumnId 分桶,列内顺序 = 数组原始顺序 |
| getId * | (item: T) => string | — | 取卡片稳定 id(全局唯一且稳定) |
| getColumnId * | (item: T) => string | — | 取卡片当前所属列 id |
| className | string | — | 容器类名 |
| columnClassName | string | — | 单列类名 |
Events
| 事件 | 类型 | 说明 |
|---|---|---|
| onMove * | (e: KanbanMoveEvent) => void | 拖拽落定回调;组件不直接改 T,由你据此改状态 |
Slots
| 插槽 | 类型 | 说明 |
|---|---|---|
| renderItem * | (item: T, state: { dragging: boolean }) => ReactNode | 渲染单张卡片的渲染函数;state.dragging 表示正被拖拽 |
| renderColumnHeader | (column: KanbanColumn, items: T[]) => ReactNode | 自定义列头的渲染函数(拿该列卡片做统计);缺省渲染 column.header ?? column.title |
KanbanMoveEvent:{ id, fromColumn, toColumn, toIndex }。toIndex = 目标列内(已剔除被拖卡片后)的插入下标。
禁忌 / 坑
- 受控且组件不替你改卡片:
onMove只回吐事件,你要在回调里改卡片的列字段并按toIndex插回(可用导出的resolveKanbanMove辅助)。这是有意为之——避免组件越界写你的业务字段。 toIndex是「剔除被拖卡片后」目标列的插入下标,落库逻辑要照此剔除再插入,否则跨列同列重排会算偏。getId须全局唯一且稳定(不仅列内唯一);卡片含交互子元素时同样要防拖拽拦截点击([[dnd-kit-draggable-container-guard-interactive-children]])。
相关
Table · Book3D · ProTable · PricingTable · JsonViewer · EditableTable