likes
comments
collection
share

前后端合作实现基于Antd的列表拖拽功能

作者站长头像
站长
· 阅读数 4

要通过 Sequelize 的 Schema 和 Ant Design 的 DraggableTable 组件实现拖拽排序功能,需要在后端设计一个用于存储排序顺序的字段,并在前端实现拖拽排序的逻辑。本文介绍一个实现此功能的步骤指南以及对应的代码示例:

1. 后端 (Sequelize 模型设计)

首先,在 Sequelize 模型中添加一个字段,比如 sortOrder,用于存储每个记录的排序顺序:

const Item = sequelize.define('item', {
  // ...其他字段
  sortOrder: {
    type: Sequelize.INTEGER,
    allowNull: false,
    defaultValue: 0 // 默认排序值
  }
  // ...其他配置
});

2. 后端 (处理排序更新请求)

接着,创建一个接口来处理排序更新的请求:

app.post('/update-sort-order', async (req, res) => {
  const { sortedItems } = req.body; // 前端发送的已排序的数组

  try {
    await sequelize.transaction(async (transaction) => {
      for (const [index, item] of sortedItems.entries()) {
        await Item.update({ sortOrder: index }, { where: { id: item.id }, transaction });
      }
    });

    return res.status(200).json({ message: 'Sort order updated successfully' });
  } catch (error) {
    return res.status(500).json({ message: error.message });
  }
});

3. 前端 (Ant Design DraggableTable)

在前端需要配置 Ant Design 的 DraggableTable 组件来使行可拖拽,并在拖拽结束时发送更新请求到后端:

import React, { useState, useCallback } from 'react';
import { Table } from 'antd';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import update from 'immutability-helper';

const type = 'DraggableRow';

const DraggableRow = ({ index, moveRow, ...restProps }) => {
  const ref = React.useRef();
  const [, drop] = useDrop({
    accept: type,
    hover(item, monitor) {
      if (!ref.current) {
        return;
      }
      const dragIndex = item.index;
      const hoverIndex = index;
      // Don't replace items with themselves
      if (dragIndex === hoverIndex) {
        return;
      }
      // Determine rectangle on screen
      const hoverBoundingRect = ref.current.getBoundingClientRect();
      // Get vertical middle
      const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      // Determine mouse position
      const clientOffset = monitor.getClientOffset();
      // Get pixels to the top
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;
      // Only perform the move when the mouse has crossed half of the items height
      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }
      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return;
      }
      // Time to actually perform the action
      moveRow(dragIndex, hoverIndex);
      // Note: we're mutating the monitor item here!
      // Generally, it's better to avoid mutations,
      // but it's good here for the sake of performance
      // to avoid expensive index searches.
      item.index = hoverIndex;
    },
  });
  const [{ isDragging }, drag] = useDrag({
    type,
    item: { type, index },
    collect: (monitor) => ({
      isDragging: monitor.isDragging(),
    }),
  });
  drag(drop(ref));

  return (
    <tr
      ref={ref}
      style={{ cursor: 'move', opacity: isDragging ? 0 : 1 }}
      {...restProps}
    />
  );
};

const DraggableTable = ({ columns, dataSource, onSortEnd }) => {
  const [data, setData] = useState(dataSource);
  const moveRow = useCallback(
    (dragIndex, hoverIndex) => {
      const dragRow = data[dragIndex];
      setData(update(data, {
        $splice: [
          [dragIndex, 1],
          [hoverIndex, 0, dragRow],
        ],
      }));
      onSortEnd(data);
    },
    [data, onSortEnd],
  );

  return (
    <DndProvider backend={HTML5Backend}>
      <Table
        columns={columns}
        dataSource={data}
        components={{
          body: {
            row: DraggableRow,
          },
        }}
        onRow={(record, index) => ({
          index,
          moveRow,
        })}
      />
    </DndProvider>
  );
};

export default DraggableTable;

在拖拽结束后,onSortEnd 回调会被触发,这时可以将新的排序发送到后端进行更新。

4. 前端 (发送排序更新请求)

当用户完成拖拽操作时,触发一个函数来发送新的排序顺序给后端:

const handleSortEnd = async (sortedData) => {
  const sortedItems = sortedData.map((item, index) => ({ id: item.id, sortOrder: index }));
  // 发送请求到后端更新排序
  await fetch('/update-sort-order', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ sortedItems }),
  });
  // ...处理响应
};

上述代码实现了前端的拖拽排序功能,并在每次拖拽结束时更新后端数据库中的排序字段。这样,无论何时从数据库中检索数据,它们都将按照用户定义的顺序进行排序。


如果前端提交的数据包括分页信息(currentpageSize),并且开发者希望在分页的环境中实现拖拽排序,这个情况会更复杂。因为排序操作可能会影响多个页面上的数据。为了处理这种情况,可以采用下述策略:

1. 后端处理

当后端接收到排序更新请求时,它需要考虑当前页面和页面大小,以便正确计算全局排序顺序。具体实现可能会根据业务逻辑而有所不同,但基本思路是:

  • 计算当前页面第一项在全局数据中的排序位置。
  • 更新当前页面项的全局排序位置。
  • 考虑到排序变更可能会影响到其他页面的数据,可能需要重新计算并更新其他页面数据的排序位置。

2. Sequelize 更新逻辑

假设前端发送的数据包括排序项的 ID、当前页数 current 和每页大小 pageSize

app.post('/update-sort-order', async (req, res) => {
  const { sortedItems, current, pageSize } = req.body;

  try {
    await sequelize.transaction(async (transaction) => {
      const globalStartIndex = (current - 1) * pageSize;

      for (const [index, item] of sortedItems.entries()) {
        await Item.update(
          { sortOrder: globalStartIndex + index }, 
          { where: { id: item.id }, transaction }
        );
      }

      // 如果需要,更新其他页面数据的 sortOrder

    });

    return res.status(200).json({ message: 'Sort order updated successfully' });
  } catch (error) {
    return res.status(500).json({ message: error.message });
  }
});

3. 前端逻辑

前端在发送排序请求时,应包括当前页面和页面大小信息:

const handleSortEnd = async (sortedData, current, pageSize) => {
  const sortedItems = sortedData.map((item, index) => ({ id: item.id, sortOrder: index }));
  await fetch('/update-sort-order', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ sortedItems, current, pageSize }),
  });
  // ...处理响应
};

注意点

  • 需要考虑到用户可能将项目从一个页面拖到另一个页面的情况。这种情况下可能需要重新加载并重新排序相关页面上的数据。
  • 也可能需要在数据库查询中采用一些优化措施,特别是在处理大量数据时,以保持系统性能和响应速度。

这种分页情况下的拖拽排序实现相对复杂,需要仔细考虑不同情况下的数据处理和用户体验。