基于React Beautiful DND实现类Clickup泳道的行列拖拽任务管理功能
基于React Beautiful DND实现多行多列任务泳道拖拽(仿Clickup模式)
1. 重构数据结构
先把原有的单列任务结构改成多列泳道结构,每个列对应不同任务状态/分类,为任务新增status、dueDate字段,方便拖拽时更新属性:
// 初始列数据(每列对应一个泳道,包含自身属性和默认规则) const initialColumns = [ { id: 'column-todo', title: '待办', status: 'todo', defaultDueDate: '2024-12-01' // 拖拽到该列时的默认截止日期 }, { id: 'column-in-progress', title: '进行中', status: 'in-progress', defaultDueDate: '2024-11-20' }, { id: 'column-done', title: '已完成', status: 'done', defaultDueDate: '2024-11-15' } ]; // 初始任务数据,关联对应列的status const initialTasks = [ { id: 'task-1', title: '完成组件封装', status: 'todo', dueDate: '2024-12-01' }, { id: 'task-2', title: '编写单元测试', status: 'todo', dueDate: '2024-12-01' }, { id: 'task-3', title: '优化页面性能', status: 'in-progress', dueDate: '2024-11-20' } ];
2. 完整组件实现
以下是扩展后的多列拖拽组件,包含行列布局、跨列拖拽时的属性更新逻辑:
import { useState } from 'react'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import './TaskBoard.css'; const TaskBoard = () => { const [columns, setColumns] = useState(initialColumns); const [tasks, setTasks] = useState(initialTasks); // 按列的status分组任务,方便渲染 const getTasksByColumn = (columnStatus) => { return tasks.filter(task => task.status === columnStatus); }; const onDragEnd = (result) => { const { source, destination, draggableId } = result; // 拖拽未完成(比如拖到组件外)直接返回 if (!destination) return; // 同列内排序 if (source.droppableId === destination.droppableId) { const columnStatus = columns.find(col => col.id === source.droppableId).status; const columnTasks = [...getTasksByColumn(columnStatus)]; const [movedTask] = columnTasks.splice(source.index, 1); columnTasks.splice(destination.index, 0, movedTask); // 更新任务列表(若需持久化可额外添加position字段) setTasks(tasks.map(task => task.id === draggableId ? movedTask : task )); } else { // 跨列拖拽:更新任务的status和dueDate const destColumn = columns.find(col => col.id === destination.droppableId); const movedTask = tasks.find(task => task.id === draggableId); // 更新任务属性:状态改为目标列的status,截止日期用目标列默认值 const updatedTask = { ...movedTask, status: destColumn.status, dueDate: destColumn.defaultDueDate }; setTasks(tasks.map(task => task.id === draggableId ? updatedTask : task )); } }; return ( <div className="task-board"> <DragDropContext onDragEnd={onDragEnd}> {columns.map(column => ( <div key={column.id} className="task-column"> <h3 className="column-title">{column.title}</h3> <Droppable droppableId={column.id}> {(provided) => ( <div ref={provided.innerRef} {...provided.droppableProps} className="task-list" > {getTasksByColumn(column.status).map((task, index) => ( <Draggable key={task.id} draggableId={task.id} index={index} > {(provided) => ( <div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} className="task-card" > <div className="task-title">{task.title}</div> <div className="task-meta"> <span>状态: {task.status}</span> <span>截止日期: {task.dueDate}</span> </div> </div> )} </Draggable> ))} {provided.placeholder} </div> )} </Droppable> </div> ))} </DragDropContext> </div> ); }; export default TaskBoard;
3. 基础样式(TaskBoard.css)
添加简单样式实现多行多列的泳道布局:
.task-board { display: flex; gap: 20px; padding: 20px; flex-wrap: wrap; /* 支持多行自动换行 */ } .task-column { background-color: #f4f5f7; border-radius: 8px; min-width: 280px; max-width: 280px; padding: 12px; } .column-title { margin: 0 0 12px 0; font-size: 16px; color: #333; } .task-list { min-height: 100px; display: flex; flex-direction: column; gap: 8px; } .task-card { background-color: white; border-radius: 6px; padding: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: grab; } .task-card:active { cursor: grabbing; } .task-title { font-weight: 500; margin-bottom: 8px; } .task-meta { font-size: 12px; color: #666; display: flex; flex-direction: column; gap: 4px; }
关键逻辑说明
- 多行多列布局:通过
flex+flex-wrap实现泳道自动换行,适配不同屏幕宽度。 - 跨列属性更新:在
onDragEnd中判断跨列拖拽时,自动将任务的status替换为目标列状态,同时更新dueDate为目标列默认值(若需自定义日期,可在此处添加弹窗选择逻辑)。 - 任务分组渲染:通过
getTasksByColumn方法按任务status关联对应列,确保每个列只渲染属于自身的任务。
内容的提问来源于stack exchange,提问作者RamkumarR
相关产品推荐
相关产品推荐

