item.tsx页面
import React, { FC } from 'react';
// 导入 dnd-kit 的核心 hook,用于处理单个可排序元素的逻辑
import { useSortable } from '@dnd-kit/sortable';
// 导入工具库,用于将 transform 数据转换为 CSS 样式字符串
import { CSS } from '@dnd-kit/utilities';
// 定义组件接收的属性类型
type PropsType = {
id: string; // 列表项的唯一标识符 (必填)
title: string; // 列表项显示的标题 (必填)
};
// 使用函数式组件 (FC) 定义 Item 组件
const Item: FC<PropsType> = (props: PropsType) => {
const { id, title } = props;
// 使用 useSortable Hook 来处理拖拽逻辑
// id 必须是唯一的,用于标识当前拖拽元素
// 返回的对象包含拖拽所需的各种属性和状态:
const {
// attributes: 需要添加到元素上的属性(如 role, aria 等可访问性属性)
attributes,
// listeners: 包含 mousedown 等事件监听器,用于触发拖拽开始
listeners,
// setNodeRef: 必须通过 ref 传递给根元素,用于跟踪拖拽元素的 DOM 节点
setNodeRef,
// transform: 包含 translate(x, y) 等数据,在拖拽时更新元素位置
transform,
// transition: 包含过渡动画数据,在元素排序交换位置时产生平滑动画
} = useSortable({ id });
// 定义样式对象
const style = {
// 将 dnd-kit 计算出的 transform 数据转换为 CSS transform 字符串
// 这样元素才能跟随鼠标移动
transform: CSS.Transform.toString(transform),
// 启用过渡动画,当元素在列表中交换位置时会有滑动效果
transition: undefined,
// 基础样式:边框、外边距、背景色和文字颜色
border: '2px solid #ccc',
margin: '10px 0',
background: '#f1f1f1',
color: 'black',
};
// 渲染 JSX
return (
// 1. ref={setNodeRef}: 让 dnd-kit 能够追踪这个 DOM 元素
// 2. style={style}: 应用包含拖拽位移和动画的样式
// 3. {...attributes}: 添加可访问性属性 (如 draggable="true")
// 4. {...listeners}: 添加鼠标事件监听器 (如 onMouseDown),点击即可拖拽
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
{/* 显示传入的标题内容 */}
Item {title}
</div>
);
};
export default Item;
container.tsx
import React, { FC, useState } from 'react';
// 核心逻辑导入
import {
DndContext, // 核心上下文组件,包裹所有可拖拽元素
closestCenter, // 碰撞检测算法:以元素中心点来判断是否发生碰撞
KeyboardSensor, // 传感器:支持键盘操作(如空格键选中,方向键移动)
PointerSensor, // 传感器:支持鼠标(pc)和触摸(移动端)操作
useSensor, // Hook:用于创建单个传感器实例
useSensors, // Hook:用于组合多个传感器
DragEndEvent // TypeScript类型:拖拽结束事件的类型定义
} from '@dnd-kit/core';
// 排序逻辑导入
import {
arrayMove, // 工具函数:用于根据索引移动数组元素
SortableContext, // 组件:为子元素提供排序上下文
sortableKeyboardCoordinates, // 键盘传感器辅助函数:处理垂直列表的键盘导航坐标
verticalListSortingStrategy // 排序策略:垂直列表的排列方式
} from '@dnd-kit/sortable';
// 导入之前定义的可拖拽子项组件
import Item from './Item';
// 定义列表项的数据结构类型
type ComponentType = {
fe_id: string; // 业务数据中的唯一标识符
title: string; // 显示的标题文本
};
const Container: FC = () => {
// 1. 状态定义:初始化列表数据
// 使用 useState 存储组件列表,数据包含唯一ID和标题
const [items, setItems] = useState<ComponentType[]>([
{ fe_id: 'c1', title: '组件1' },
{ fe_id: 'c2', title: '组件2' },
{ fe_id: 'c3', title: '组件3' }
]);
// 2. 传感器配置:定义用户如何触发拖拽行为
// useSensors 组合了鼠标/触摸 和 键盘 两种触发方式
const sensors = useSensors(
// 鼠标/触摸传感器:监听 mousedown 或 touchstart 事件来启动拖拽
useSensor(PointerSensor),
// 键盘传感器:监听键盘事件进行拖拽
useSensor(KeyboardSensor, {
// coordinateGetter: 告诉键盘传感器如何计算移动坐标
// sortableKeyboardCoordinates 是 dnd-kit 提供的专门用于列表排序的坐标计算方法
coordinateGetter: sortableKeyboardCoordinates,
})
);
// 3. 拖拽结束处理函数
// 当用户松开鼠标或触发放置时调用
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
// 边界情况处理:
// 如果拖拽结束的位置为空(例如拖到了可视区域外),直接返回不做处理
if (over == null) return;
// 只有当拖拽源 (active) 和 放置目标 (over) 不是同一个元素时才执行排序
if (active.id !== over.id) {
// 更新状态
setItems((items) => {
// 查找被拖拽元素的旧索引
const oldIndex = items.findIndex(c => c.fe_id === active.id);
// 查找目标放置位置的新索引
const newIndex = items.findIndex(c => c.fe_id === over.id);
// 使用 dnd-kit 提供的 arrayMove 工具函数
// 它会返回一个新数组,将 oldIndex 位置的元素移动到 newIndex
// 这样可以保证状态的不可变性 (Immutability)
return arrayMove(items, oldIndex, newIndex);
});
}
}
// 4. 数据适配
// dnd-kit 的 SortableContext 要求 items 数组中的每个对象必须包含 'id' 字段
// 我们的数据源使用的是 'fe_id',因此需要做一个映射转换
const itemsWithId = items.map(c => ({
...c, // 保留原有属性
id: c.fe_id // 增加 id 字段以满足 dnd-kit 的要求
}));
// 5. 渲染视图
return (
{/*
DndContext:整个拖拽系统的根容器
sensors:传入传感器配置,决定如何启动拖拽
collisionDetection:碰撞检测策略,closestCenter 表示以元素中心点最近者为准
onDragEnd:拖拽结束时的回调函数
*/}
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
{/*
SortableContext:排序功能的上下文
items:传入包含 id 的数据列表
strategy:排序策略,verticalListSortingStrategy 表示垂直列表排列
*/}
<SortableContext
items={itemsWithId}
strategy={verticalListSortingStrategy}
>
{/*
渲染列表项
遍历 itemsWithId,为每个数据项生成一个 Item 组件
key:React 列表渲染的唯一标识
id:传递给 Item 组件的唯一 id (用于 dnd-kit 内部逻辑)
title:传递给 Item 组件的显示文本
*/}
{itemsWithId.map(c => (
<Item
key={c.id}
id={c.id}
title={c.title}
/>
))}
</SortableContext>
</DndContext>
);
};
export default Container;
588

被折叠的 条评论
为什么被折叠?



