ToolJet Kanban 组件详解:看板数据结构、事件机制与组件专属操作(CSA)实战指南
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
本文基于 ToolJet 官方组件文档与前端源码,系统讲解 Kanban(看板)组件的完整用法:如何用columnData/cardData绑定动态数据、卡片内cardData模板变量、6 类事件与 7 个暴露变量、4 个可脚本调用的组件专属操作(CSA),以及卡片详情弹窗、删除区、样式与设备适配等全部配置项。读完后你可以把一个静态任务板改造成由查询驱动、事件可监听、卡片可增删移改的完整工作流看板,并理解其底层基于@dnd-kit的拖拽实现原理。
一、组件定位与默认结构
Kanban组件用于以可视化看板方式组织与排期任务,提供透明的任务流转工作流:可配置显示列数、启用/禁用“+Add Card”按钮,并把外部数据绑定到卡片上(参见 官方文档)。
在 组件配置文件 中可以看到,新添加的 Kanban 组件默认宽度 40、高度 490,并自带两个默认的Text子组件,直接绑定到当前卡片数据上:
// 默认子组件 1:卡片标题(top: 20, left: 4, 加粗 16px) text: '{{cardData.title}}' // 默认子组件 2:卡片描述(top: 50, left: 4, 14px) text: '{{cardData.description}}'默认看板数据为 3 列(r1To Do /r2In Progress /r3Done)与 10 张卡片,卡片默认宽度302、高度100,删除区默认文案Drop here to delete——即开箱即可运行一个完整的“待办-进行中-完成”看板。
卡片与弹窗内的受限组件
:::info 受限组件 Kanban 的Card与Popout(卡片详情弹窗)中不允许放置部分组件:
- Card:Calendar、Kanban、Form、Tabs、Modal、ListView、Container
- Popout:Calendar、Kanban :::
这一限制从源码结构看可以避免嵌套拖拽容器(DndContext)与嵌套弹窗造成状态冲突。
二、数据绑定:Column data 与 Card data
这是看板数据模型的骨架,两项均为code类型属性,支持直接写数组字面量或绑定查询结果:
| 属性 | 说明 | 期望值 |
|---|---|---|
| Column data | 以对象数组(或返回对象数组的查询)提供列的id与title | {{[{ "id": "c1", "title": "to do" },{ "id": "c2", "title": "in progress" },{ "id": "c3", "title": "Completed" }]}}或{{queries.xyz.data}} |
| Card data | 以对象数组(或查询)提供卡片的id、title、columnId(可含description等自定义字段) | {{[{ id: "r1", title: "Title 1", description: "Description 1", columnId: "c1" },{ id: "r2", title: "Title 2", description: "Description 2", columnId: "c2" },{ id: "r3", title: "Title 3", description: "Description 3", columnId: "c3" }]}}或{{queries.abc.data}} |
两条强制约束(缺失时看板无法正确渲染):
column data中每列必须提供id,id可为string或number;Card data中每张卡片必须提供id和columnId,两者类型同为string或number。
从 helpers/utils.js 的实现可以印证这一点:convertArrayToObj直接以d.id作为键构建containers[d.id] = d,getColumnData只做columnData.map(container => container.id);getCardData则以card.columnId分组、card.id入列。因此id缺失或重复都会破坏“列→卡片 ID 列表”的映射结构。非数组输入会被normalizeCardData归一为[],表现为空看板而不是报错。
三、在卡片内使用cardData变量
卡片内部组件需要动态展示当前卡片的数据时,使用cardData键。例如把卡片上一个 Text 组件的Data属性设为:
{{cardData.title}} // 将 title 替换为你数据中的字段名,如 cardData.description底层机制在 KanbanBoard.jsx 中实现:组件通过updateCardDataInCustomResolvables(id, flatCardData.map(d => ({ cardData: d })), 'cardData', moduleId)为每张卡片注册一条独立的cardData可解析值。也就是说,同一张卡片内的所有子组件共享“这张卡片”这一上下文的cardData,而不同卡片各自的cardData互不干扰。因此除了内置的title/description默认占位,你的查询数据中的任意字段(如assignee、dueDate)都可以在卡片内以{{cardData.字段名}}直接引用。
四、板面配置:Card width / Card height / 添加与删除区
| 属性 | 说明 | 期望值 |
|---|---|---|
| Card width | 设置卡片宽度 | 数值,默认{{302}} |
| Card height | 设置卡片高度 | 数值,默认{{100}} |
| Enable add card | 显示/隐藏看板上的+Add Cards按钮 | 默认启用;点击属性旁的fx可绑定{{true}}/{{false}}动态控制 |
| Show delete button | 显示/隐藏看板底部的Drop here to delete cards删除区 | 默认启用;同样可通过fx动态控制 |
几个源码级细节:
- 列容器宽度由
cardWidth推导:KanbanBoard.jsx 中width: \${(Number(cardWidth) || 300) + 48}px``,即卡片宽度加 48px 内边距; - 删除区在代码中是
id为常量'void'(TRASH_ID)的Trash组件:拖拽卡片悬停其上并松开即触发删除; + Add Card按钮点击时触发onAddCardClick事件,且受enableAddCard控制(禁用时按钮被设为invisible类);- 配置中还存在Delete zone label属性(默认文案
Drop here to delete),可自定义删除区提示文字。
五、卡片详情弹窗(Popout)配置
文档的 Events 一节提到“点击卡片打开弹窗”,其对应配置在 kanban.js 的Card details modal小节:
| 属性 | 说明 | 默认值 |
|---|---|---|
| Open modal on card click | 点击卡片是否打开详情弹窗 | {{true}} |
| Modal size | 弹窗尺寸,可选small/medium/large/fullscreen | lg(medium) |
| Height | 弹窗高度 | 400 |
弹窗内的 Popout 区域可像 Card 一样放置组件(受前文所列受限组件约束),组件内同样可读取cardData。
六、Events:6 个事件的触发时机
在右侧属性面板的Events区点击Add handler即可为下列事件添加处理器,与 ToolJet 其他组件一样支持同一事件绑定多个 handler:
| 事件 | 触发时机 |
|---|---|
| On update | 卡片数据(id、title、description 或 columnID)通过组件专属操作(CSA)被更新时触发 |
| On add card click | 点击看板上的Add card按钮时触发 |
| Card removed | 卡片被删除时触发(拖入底部删除区,或通过 CSA 删除) |
| Card added | 通过 CSA 向看板添加卡片时触发 |
| Card moved | 卡片在看板上的位置改变时触发(拖拽或通过 CSA 移动) |
| Card selected | 点击卡片打开弹窗(选中)时触发 |
与事件对应的内部实现:每个 CSA 或拖拽落点在执行完状态变更后调用fireEvent('onCardAdded' | 'onCardRemoved' | 'onCardMoved' | 'onUpdate')(见 KanbanBoard.jsx)。需要注意一个细节:删除卡片若通过拖入删除区(overId === TRASH_ID分支),只会设置lastRemovedCard并触发onCardRemoved;而通过 CSAdeleteCard删除时同理。事件更多说明可参考 ToolJet 文档站中的Action Reference分类。
七、Exposed Variables:7 个暴露变量全解
| 变量 | 说明 | 访问方式 |
|---|---|---|
| updatedCardData | 看板中所有卡片的最新值集合。只有当对任意卡片执行过移动、添加、删除或更新操作后才有值 | 直接读取{{components.kanban1.updatedCardData}} |
| lastAddedCard | 最后添加的卡片,含id、title、description、columnId | {{components.kanban1.lastAddedCard.title}} |
| lastRemovedCard | 最近被删除的卡片,含id、title、description、columnId | {{components.kanbanboard1.lastRemovedCard.title}} |
| lastCardMovement | 最近移动的卡片:originColumnId、destinationColumnId、originCardIndex、destinationCardIndex,以及cardDetails对象(含id、title、description、columnId) | {{components.kanbanboard1.lastCardMovement.cardDetails.title}}或{{components.kanbanboard1.lastCardMovement.destinationCardIndex}} |
| lastSelectedCard | 最后选中(点击查看)卡片的id、title、columnId、description | {{components.kanban1.lastSelectedCard.columnId}} |
| lastUpdatedCard | 最后通过 CSA 更新的卡片,含id、title、description、columnId | {{components.kanban1.lastUpdatedCard.columnId}} |
| lastCardUpdate | 记录本次 CSA 更新中“被修改属性”的旧值与新值(数组) | {{components.kanban1.lastCardUpdate[0].title.oldValue}} |
这 7 个变量与 kanban.js 中exposedVariables的声明一一对应。lastCardUpdate的“新旧值对比”结构由deep-object-diff计算得出——updateCardData实现中先对更新前后的卡片做diff,再把变更键映射为{ [key]: { oldValue, newValue } }数组,这解释了为什么访问写法是lastCardUpdate[0].title.oldValue(数组下标 + 属性名 + oldValue/newValue)。lastCardMovement在拖拽与 CSA 两条路径下都会被写入:拖拽路径记录originCardIndex/destinationCardIndex(数组内索引),CSAmoveCard路径则把卡片插入目标列首位、destinationIndex为0。
八、Component Specific Actions(CSA):脚本化操作看板
以下 4 个操作可在任意事件 handler 的RunJS查询中调用,实现对看板的编程式控制(参数签名以 kanban.js 中actions定义为准):
| 操作 | 说明 | 调用示例 |
|---|---|---|
| updateCardData | 更新卡片数据 | components.kanban1.updateCardData('c1', { title: 'New Title' }) |
| moveCard | 把卡片移动到另一列 | await components.kanban1.moveCard('c1', 'r2')(第一个参数为卡片 id,第二个为目标列 id) |
| addCard | 向看板添加卡片 | await components.kanban1.addCard('c1', { title: 'New Title' }) |
| deleteCard | 删除卡片 | await components.kanban1.deleteCard('c2')(参数为卡片 id) |
CSA 的防御性校验在 KanbanBoard.jsx 中可见:
updateCardData/moveCard/deleteCard:目标卡片不存在时弹出Card not found提示并中止;moveCard:若卡片已处于目标列(columnId相同)则直接返回,不触发事件;addCard:卡片 id 已存在时提示Card already exists;columnId缺失或指向不存在的列时提示Column Id not found。
每个 CSA 成功后都会同步刷新updatedCardData及对应的lastXxx变量,并触发对应事件,因此“CSA 改状态 → 暴露变量更新 → 事件 handler 执行后续查询/保存”构成了完整的可编程闭环。
一个典型的“点击按钮落库”场景:
// 事件 handler 中的 RunJS 示例 // 1. 把最近选中的卡片移动到其他列 await components.kanban1.moveCard(components.kanban1.lastSelectedCard.id, 'r3'); // 2. 更新卡片标题 components.kanban1.updateCardData(components.kanban1.lastSelectedCard.id, { title: 'Done!' }); // 3. 把最新全量卡片数据提交到后端查询 await queries.saveCards.run({ cards: components.kanban1.updatedCardData });九、拖拽实现原理:基于 @dnd-kit 的多列排序
从源码结构看,看板拖拽由@dnd-kit驱动(KanbanBoard.jsx):
DndContext注册了MouseSensor、TouchSensor与KeyboardSensor(键盘坐标由 multipleContainersKeyboardCoordinates.js 的coordinateGetter提供,支持跨列键盘移动),碰撞检测使用rectIntersection;- 每个列是一个
tj-kanban-container-{columnId}可放置容器,列内是SortableContext+verticalListSortingStrategy的垂直排序上下文; onDragOver负责跨列实时重排并记录lastCardMovement中间态;onDragEnd处理落点:落点在删除区('void')走删除分支,落点位置变化走arrayMove重排分支,并在此统一写入lastCardMovement、触发onCardMoved;- 拖拽中的卡片由
DragOverlay渲染,松手时有半透明落位动画(dropAnimation,opacity: 0.5)。
这也解释了文档约束的由来:findContainer通过tj-kanban-container-前缀识别列容器,跨列拖拽判定、卡片归属都依赖这套 ID 约定,因此列/卡片的id必须是字符串或数字且保持稳定。
十、General、Devices 与 Styles 配置
Tooltip(General 折叠区)
在General区设置字符串后,鼠标悬停组件即显示该文案作为提示。当前实现还支持Tooltip format切换(plainText/markdown/html,默认plainText),可让提示文本支持 Markdown 或 HTML 渲染。
Devices
| 属性 | 说明 |
|---|---|
| Show on desktop | 桌面视图下是否可见。默认{{true}};可用开关设置,或点击fx填入逻辑表达式动态控制 |
| Show on mobile | 移动视图下是否可见。默认{{false}};同样支持 fx 动态绑定 |
Styles
| 样式 | 说明 |
|---|---|
| Disable | 禁用后组件被锁定且不可交互。默认值{{false}}(文档表格中“默认禁用”的表述与源码默认值{{false}}不一致,以源码definition.styles.disabledState为准,即默认未禁用) |
| Visibility | 控制组件可见性,{{false}}时应用部署后组件不显示。默认{{true}} |
| Accent color | 列标题底色/强调色,可输入 Hex 值或用取色器选择;默认值为 CSS 变量var(--cc-primary-brand),+ Add Card按钮与列标题共用该色(源码中colAccentColor = { color: '#fff', backgroundColor: accentColor ?? '#4d72fa' }) |
从 Kanban.jsx 可见,disabledState会通过data-disabled属性与useDisableInert钩子将看板标记为inert,使其按钮与内嵌组件同时退出 Tab 键盘焦点顺序;visibility为false时外层容器直接display: none。
十一、兼容旧版 KanbanBoard 组件
仓库中另有一份 kanbanBoard.js 配置,文件首行注释明确写道:KanbanBoard 组件已弃用(deprecated),该配置仅为兼容存量应用中的旧组件而保留。旧版属性名为columns,事件多一个onCardUpdated(新版并入onUpdate),且暴露变量中没有lastSelectedCard/lastCardUpdate。如果你在新建应用中看到 Kanban Board 选项,应优先使用新版Kanban组件;旧应用数据可沿用旧组件继续工作,但不再获得新特性。
十二、小结
- 数据模型:列只需
id+title,卡片必须带id+columnId,两者均支持字面量数组或{{queries.xxx.data}}查询绑定;卡片内组件统一通过{{cardData.字段}}读取当前卡片数据。 - 交互能力:6 个事件覆盖更新、加卡点击、删除、添加、移动、选中;7 个暴露变量(
updatedCardData及 6 个lastXxx)让 handler 能精确感知每次操作前后的状态。 - 编程控制:
addCard/deleteCard/moveCard/updateCardData四个 CSA 可在 RunJS 中 await 调用,与事件、暴露变量组合即可实现“拖拽→事件→落库”的完整闭环。 - 底层实现:基于
@dnd-kit的DndContext+ 多容器SortableContext,支持鼠标、触摸与键盘三套 sensor,删除区是一个 id 为'void'的特殊放置目标。
【免费下载链接】ToolJetOpen-source foundation of ToolJet AI - the enterprise app generation platform for internal tools, dashboards, business applications, workflows and AI agents. Build visually, from a prompt, or from Claude Code, Codex and Cursor over MCP 🚀项目地址: https://gitcode.com/GitHub_Trending/to/ToolJet
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考