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展示与业务逻辑分离,提高组件复用性和可维护性。