J'Blog
← 返回文章列表

React AntD后台系统组件开发实践:拖拽排序与穿梭框实现

React AntD后台系统组件开发实践:拖拽排序与穿梭框实现

React AntD组件开发最佳实践

在后台系统开发中,合理设计组件架构至关重要。基础组件应尽量保持无状态,通过props接收数据;对于层级嵌套较深的业务组件,推荐使用数据流而非props传递数据;对于不需要重新渲染的组件,可通过ref获取数据以提高性能。

拖拽排序组件实现

基于react-beautiful-dnd插件实现的拖拽排序列表组件,支持自定义样式和拖拽回调。

// 基于react-beautiful-dnd插件
/**
 * 拖拽排序列表组件
 * @param dropId 拖拽id,用于区分不同列表
 * @param list 列表数据
 * @param children 列表子数据ReactNode,自定义样式
 * @param changeHandle 拖拽回调函数
 * @param showDragIcon 是否展示拖拽图标
 * @returns 排序后的列表
 */

// index.tsx
import React, { useEffect, useState } from "react"
import { DragDropContext, Draggable } from "react-beautiful-dnd"
// react18存在兼容性问题,所以需要StrictModeDroppable
import { StrictModeDroppable } from "./StrictModeDroppable"
import Item from "./Item"
import "./index.less"

interface DragSortListProps {
  dropId?: string  // 设置拖拽区域唯一id
  list: any[] // 包含唯一id,用于区分每个item,作为key
  children: any  // 每个可拖拽的子元素,自定义样式
  changeHandle?: (list: any[]) => void // 拖拽后callback
  showDragIcon?: boolean // 是否显示拖拽图标
}

const DragSortList = ({
  dropId,
  list,
  children,
  changeHandle,
  showDragIcon,
}: DragSortListProps) => {
  const [data, setData] = useState(list)

  // 拖拽后执行函数
  const dragEnded = (param: any) => {
    const { source, destination } = param
    const _arr = [...data]
    const _item = _arr.splice(source.index, 1)[0]
    _arr.splice(destination.index, 0, _item)
    setData(_arr)
    changeHandle && changeHandle(_arr)
  }

  useEffect(() => {
    setData(list)
  }, [list])
  
  return (
    
      
        
          {(provided, snapshot) => (
            
    {data && data.map((item, index) => { return ( {(_provided, _snapshot) => ( )} ) })} {provided.placeholder}
)} ) } export default DragSortList // StrictModeDroppable.tsx import { useEffect, useState } from "react"; import { Droppable, DroppableProps } from "react-beautiful-dnd"; export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => { const [enabled, setEnabled] = useState(false); useEffect(() => { const animation = requestAnimationFrame(() => setEnabled(true)); return () => { cancelAnimationFrame(animation); setEnabled(false); }; }, []); if (!enabled) { return null; } return {children}; }; // Item.tsx import React, { forwardRef, ReactNode } from "react" import { DragOutlined } from "@ant-design/icons" interface IProps { dataItem: any itemContent: (data: any, dragHandleProps?: any) => ReactNode dragHandleProps: any snapshot: any showDragIcon?: boolean } const Item = forwardRef( ( { dataItem, itemContent, dragHandleProps, snapshot, showDragIcon = true, ...props }: IProps, ref: any, ) => { return (
  • {showDragIcon && } {itemContent(dataItem, dragHandleProps)}
  • ) }, ) export default Item // 使用示例 import { useEffect, useState } from "react" import { Input } from "antd" import { DeleteOutlined, PlusCircleOutlined } from "@ant-design/icons" import "./index.less" import { useDebounceFn } from "ahooks" import DragSortList from "@/components/ui/DragSortList" import ObjectID from "bson-objectid" interface IProps { defaultValue?: string[] // 初始值 changeCb?: (list: any[]) => void // 值发生变化时回调 } const PersonalizedTags = ({ defaultValue, changeCb }: IProps) => { const [tags, setTags] = useState([]) const Item = (data: any) => { const { tag, id } = data return ( removeHandle(id)} /> ) } const addHandle = () => { const newTags = [...tags, { id: ObjectID(Date.now()).id, tag: "" }] setTags([...newTags]) changeCb && changeCb([...newTags]) } const removeHandle = (id: any) => { const newTags = tags.filter((item: any) => item.id !== id) setTags(newTags) changeCb && changeCb(newTags) } const changeValue = () => { changeCb && changeCb([...tags]) } const changeValueHandle = useDebounceFn( (e: any) => { const { dataset: { id }, } = e.currentTarget const curVal = e.target.value const curTag = tags.find((item: any) => item.id === id) curTag["tag"] = curVal }, { wait: 500 }, ) const changeDragCheckedList = (checkedList: any) => { setTags(checkedList) changeCb && changeCb(checkedList) } useEffect(() => { setTags( defaultValue?.map((item: any) => ({ id: ObjectID(Date.now()).id, tag: item, })) || [], ) }, [defaultValue]) return ( {Item} 添加 ) } export default PersonalizedTags

    自定义穿梭框组件实现

    支持左侧滚动刷新、右侧拖拽排序的穿梭框组件,满足复杂业务场景需求。

    /**
     * 穿梭框组件
     * @param leftData 左侧数据源
     * @param rightData 右侧数据源
     * @param showMore 左侧加载更多回调函数
     * @param goLeftHandle 移动到左侧回调函数
     * @param goRightHandle 移动到右侧回调函数
     * @param getDragCheckedList 获取右侧拖拽后列表的回调函数
     * @returns 穿梭框组件
     */
    import { LeftOutlined, RightOutlined } from "@ant-design/icons"
    import Item from "./item"
    import "./index.less"
    import { useState } from "react"
    
    interface IProps {
      leftData: any[]
      rightData: any[]
      showMore?: () => any
      goLeftHandle: (checkedList: string[]) => void
      goRightHandle: (checkedList: string[]) => void
      getDragCheckedList?: (list: any[]) => void
    }
    
    const Transfer = ({ 
      leftData, 
      rightData, 
      showMore, 
      goLeftHandle, 
      goRightHandle, 
      getDragCheckedList 
    }: IProps) => {
      const [leftCheckedList, setLeftCheckedList] = useState([])
      const [rightCheckedList, setRightCheckedList] = useState([])
    
      // 移动到左侧
      const goLeft = () => {
        goLeftHandle(rightCheckedList)
      }
    
      // 移动到右侧
      const goRight = () => {
        goRightHandle(leftCheckedList)
      }
    
      // 获取选中的数据
      const getCheckedList = (checkedList: string[], type: "left" | "right") => {
        if (type === "left") {
          setLeftCheckedList(checkedList)
        } else {
          setRightCheckedList(checkedList)
        }
      }
    
      return (
        
          
          
            
              
            
            
              
            
          
          
        
      )
    }
    
    export default Transfer
    
    // Item.tsx
    import { useInViewport } from "ahooks"
    import { Checkbox } from "antd"
    import { useEffect, useRef, useState } from "react"
    import { CheckboxChangeEvent } from "antd/lib/checkbox"
    import DragSortList from "../DragSortList"
    
    const CheckboxGroup = Checkbox.Group
    
    interface IProps {
      type: "left" | "right"
      data: any[]
      showMore?: () => any
      getCheckedList: (list: string[], type: IProps["type"]) => void
      getDragCheckedList?: any
    }
    
    const Item = ({ 
      type, 
      data, 
      showMore, 
      getCheckedList, 
      getDragCheckedList 
    }: IProps) => {
      const [checkedList, setCheckedList] = useState([])
      const [loadMore, setLoadMore] = useState(type === "left" ? true : false)
      const checkAll = data.length === checkedList.length && data?.length ? true : false
      const indeterminate = checkedList.length > 0 && checkedList.length < data.length
      const title = type === "left" ? "未关联" : "已关联"
      
      const loadMoreRef = useRef(null)
      const rootRef = useRef(null)
    
      const [inViewport] = useInViewport(loadMoreRef, {
        root: rootRef,
      })
    
      const dataOps = data.map((item) => {
        return {
          id: item.id,
          label: item.name,
          value: item.id,
        }
      })
    
      const ItemContent = (data: any, dragHandleProps?: any) => {
        const { value, label } = data
    
        return (
          
            {label}
          
        )
      }
    
      const onChange = (list: CheckboxChangeEvent[]) => {
        const checkedValues = list.map(item => item.target.value)
        setCheckedList(checkedValues)
        getCheckedList(checkedValues, type)
      }
    
      const onCheckAllChange = (e: CheckboxChangeEvent) => {
        const checked = e.target.checked
        setCheckedList(checked ? dataOps.map(item => item.value) : [])
        getCheckedList(checked ? dataOps.map(item => item.value) : [], type)
      }
    
      useEffect(() => {
        if (inViewport && loadMore && showMore) {
          showMore()
        }
      }, [inViewport, loadMore, showMore])
    
      return (
        
          
            
              {title}
            
          
          
            
            {type === "left" && loadMore && (
              
                加载更多
              
            )}
          
        
      )
    }
    
    export default Item
    

    组件使用最佳实践

    1. 拖拽排序组件适用于需要用户自定义排序的场景,如标签管理、菜单排序等。使用时应确保数据项有唯一id,并在拖拽回调中处理数据更新逻辑。

    2. 穿梭框组件适用于数据关联场景,如权限分配、标签关联等。左侧支持无限滚动加载,右侧支持拖拽排序,满足复杂业务需求。

    3. 在组件设计上,应遵循单一职责原则,将UI展示与业务逻辑分离,提高组件复用性和可维护性。