利用@dnd-kit 实现拖拽Table的一行进行排序

949 阅读2分钟

这是一个基于@dnd-kit 实现拖拽Table的一行进行排序,点击单行可点开弹框进行更换数据,@dnd-kit其中的一些原理不多赘述,我们直接上案例 下载安装包:@mantine/core @dnd-kit/core

1.首先写一个展示列表

import { useEffect, useState } from "react";

import { Button, Group, Stack, Title } from "@mantine/core";

import SortableList from "./SortableList";

const data = [
    {
        id: "1",
        name: "黑森林蛋糕"
    },
    {
        id: "2",
        name: "青提树莓桃桃"
    },
    {
        id: "3",
        name: "半熟芝士"
    }
];

export default function App() {

const [isEdit, setIsEdit] = useState(false);
const [items, setItems] = useState<any[]>(data || []);

useEffect(() => {
    setItems(data || []);
}, []);

return (
<Stack>
    <Group position="apart">
        <Title order={3} color="blue">
            今日贩卖
        </Title>
        {!isEdit ? (
            <Button size="sm" onClick={() => setIsEdit(true)}>
                编辑
            </Button>
            ) : (
        <Group>
            <Button
                variant="outline"
                size="sm"
                onClick={() => {
                    setIsEdit(false);
                    setItems(data || []);
                }}
                >
                取消
            </Button>
            <Button size="sm" onClick={() => setIsEdit(false)}>
                保存
            </Button>
        </Group>
        )}
    </Group>

    <SortableList items={items || []} onChange={setItems} isEdit={isEdit} />
</Stack>
)}

export const getDefaultData = () => {
    return {
            id: "",
            name: ""
           };
    };

2.接下来写可拖拽时的列表

import { Active, DndContext, useSensors, KeyboardSensor, PointerSensor, useSensor, Over } from "@dnd-kit/core";

import { SortableContext, arrayMove, sortableKeyboardCoordinates, horizontalListSortingStrategy } from "@dnd-kit/sortable";
import { useMemo, useState } from "react";
import SortableOverlay from "./SortableOverlay";
import SortableItem, { DragHandle } from "./SortableItem";
import { Table } from "@mantine/core";

type SortableListProps = {
    items: any[];
    onChange: (items: any[]) => void;
    isEdit: boolean;
};
export default function SortableList(props: SortableListProps) {
    const { items, onChange, isEdit } = props;
    const [active, setActive] = useState<Active | null>(null);
    
    const activeItem = useMemo(
        () => items.find((item) => item.id === active?.id),
        [active, items]
    );

    const sensors = useSensors(
        useSensor(PointerSensor),
        useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
    );

    const handleDragStart = (active: Active) => {
        setActive(active);
    };

    const handleDragEnd = (active: Active, over: Over | null) => {
        if (over && active.id !== over?.id) {
            const activeIndex = items.findIndex(({ id }) => id === active.id);
            const overIndex = items.findIndex(({ id }) => id === over.id);
            onChange(arrayMove(items, activeIndex, overIndex));
        }
        setActive(null);
    };

    const handleDragCancel = () => {
        setActive(null);
    };

return (
    <DndContext
        sensors={sensors}
        onDragStart={({ active }) => handleDragStart(active)}
        onDragEnd={({ active, over }) => handleDragEnd(active, over)}
        onDragCancel={handleDragCancel}>
        <Table withBorder>
            <thead>
                <tr>
                {isEdit ? <th>数据排序</th> : <th>展示序号</th>}
                <th>名称</th>
                </tr>
            </thead>
            <tbody>
            {isEdit ? (
                <SortableContext
                    items={items}
                    strategy={horizontalListSortingStrategy}>
                        {items.map((item, index) => {
                            return (
                                <SortableItem
                                    key={index}
                                    item={item}
                                    items={items}
                                    onChange={onChange}
                                    index={index}
                                />
                            );
                        })}
                 </SortableContext>
                        ) : (
                    <>
                        {items.map((t, i) => {
                            return (
                                <tr key={i}>
                                    <td>{i + 1}</td>
                                    <td>{t.name}</td>
                                </tr>
                            );
                        })}
                    </>
                        )}
           </tbody>
          </Table>
                <SortableOverlay>
                    {activeItem ? (
                        <Table>
                            <tbody>
                                <SortableItem item={activeItem} />
                            </tbody>
                        </Table>
                    ) : null}
                </SortableOverlay>
        </DndContext>
                    );
                    }

SortableList.DragHandle = DragHandle;

3.列表项

import { DraggableSyntheticListeners } from "@dnd-kit/core";
import { createContext, CSSProperties, useContext, useMemo, useState } from "react";
import { useSortable } from "@dnd-kit/sortable";
import { Anchor } from "@mantine/core";
import { CSS } from "@dnd-kit/utilities";
import SortableList from "./SortableList";
import AnnouncementPublishListDialog from "./AnnouncementPublishListDialog";

type SortableItemProps = {
    item: any;
    items?: any[];
    index?: number;
    onChange?: (items: any[]) => void;
};

interface Context {
    attributes: Record<string, any>;
    listeners: DraggableSyntheticListeners;
    ref(node: HTMLElement | null): void;
}

const SortableItemContext = createContext<Context>({
    attributes: {},
    listeners: undefined,
    ref: () => {}
});

export default function SortableItem({
    item,
    items,
    index,
    onChange
}: SortableItemProps) {
    const {
        attributes,
        isDragging,
        listeners,
        setNodeRef,
        transform,
        transition,
        setActivatorNodeRef
    } = useSortable({
    id: item.id
    });
  
const context = useMemo(
    () => ({ attributes, listeners, ref: setActivatorNodeRef }),
   [attributes, listeners, setActivatorNodeRef]
);

const style: CSSProperties = {
    opacity: isDragging ? 0.4 : undefined,
    transform: CSS.Translate.toString(transform),
    transition
};
const [opened, setOpened] = useState(false);
const newData = Array.from(items || []);

const onSelect = (item: any) => {
    if (items?.some((i) => i.id === item.id)) {
    const clonedData = [...newData];
    const idx = items.findIndex((i) => i.id === item.id);
    clonedData[idx] = {};
    clonedData[index || 0] = item;
    onChange?.(clonedData);
} else {
    const clonedData = [...newData];
    clonedData[index || 0] = item;
    onChange?.(clonedData);
}
};

return (
    <SortableItemContext.Provider value={context}>
        <AnnouncementPublishListDialog
            opened={opened}
            onClose={() => setOpened(false)}
            onSelect={onSelect}
        />
        <tr ref={setNodeRef} style={style} onClick={() => setOpened(true)}>
            <td>
                <SortableList.DragHandle color="black" />
            </td>
            <td>{item.name}</td>
        </tr>
    </SortableItemContext.Provider>
    );
}

export function DragHandle({ color }: { color: string }) {
const { attributes, listeners, ref } = useContext(SortableItemContext);

return (
    <Anchor className="DragHandle" {...attributes} {...listeners} ref={ref}>
        <svg viewBox="0 0 20 20" width="12">
        <path
        d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"
        fill={color}
        />
        </svg>
    </Anchor>
);
}

4.拖拽的元素

import { DragOverlay, DropAnimation, defaultDropAnimationSideEffects } from "@dnd-kit/core";

export default function SortableOverlay({ children }: { children: any }) {
    const dropAnimationConfig: DropAnimation = {
        sideEffects: defaultDropAnimationSideEffects({
            styles: {
                active: {
                opacity: "0.4"
            }
        })
    };

return (
    <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>
   );
}

5.弹窗里面的内容

import { Modal, Stack, Table, Button } from "@mantine/core";

type ProductListDialogProps = {
    opened: boolean;
    onClose: () => void;
    onSelect: (o: any) => void;

};

const elements = [
    { id: 65, name: "冰可乐" },
    { id: 75, name: "杨梅冰冰桶" },
    { id: 39, name: "芝士桃桃" },
    { id: 56, name: "榴莲千层" },
    { id: 58, name: "覆盆子千层" }
];

export default function AnnouncementPublishListDialog({
    opened,
    onClose,
    onSelect
}: ProductListDialogProps) {
    const rows = elements.map((element, index) => (
        <tr key={element.name}>
            <td>{index + 1}</td>
            <td>{element.name}</td>
            <td>
            <Button
                size="xs"
                variant="outline"
                onClick={() => {
                onSelect(element);
                onClose();
                }}
            >
                选择
            </Button>
            </td>
         </tr>
            ));
    return (
        <Modal
            opened={opened}
            onClose={onClose}
            centered
            size="900px">
            <Stack>
                <Table>
                    <thead>
                        <tr>
                            <th>序号</th>
                            <th>名称</th>
                            <th>操作</th>
                        </tr>
                    </thead>
                    <tbody>{rows}</tbody>
                </Table>
            </Stack>
        </Modal>
);
}

实现结果:

image.png

image.png

image.png

最后附上源码:codesandbox.io/s/gifted-mo…