React DndKit 实现类似slack 类别、频道拖动调整位置功能

一周调试终于实现了类 slack  类别、频道拖动调整位置功能。
历经四个版本迭代。

实现了类似slack 类别、频道拖动调整功能
从vue->react ;更喜欢React的生态及编程风格,新项目用React来重构了。

1.zustand全局状态

2.DndKit  拖动

功能视频:

dndKit 实现类似slack 类别、频道拖动调整位置功能

React DndKit 实现类似slack 类别、频道拖动调整位置功能_哔哩哔哩_bilibili
 

1.ChannelList.tsx

// ChannelList.tsx
import React, { useState } from 'react';
import useChannelsStore from "@/Stores/useChannelListStore";
import { DndContext, closestCenter, DragOverlay, pointerWithin, ragEndEvent, DragOverEvent, DragStartEvent, DragMoveEvent, DragEndEvent } from "@dnd-kit/core";
import { SortableContext, verticalListSortingStrategy, useSortable, arrayMove } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { GripVertical } from "lucide-react";
import { ChevronDown, ChevronRight } from "lucide-react"; // 图标库
import { Channel } from "@/Stores/useChannelListStore"interface ChannelProps {id: string;name: string;selected: boolean
}const ChannelItem: React.FC<ChannelProps> = ({ id, name, selected }) => {const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id, data: { type: 'channel' } });return (<divref={setNodeRef}{...attributes}{...listeners}style={{transform: CSS.Transform.toString(transform),transition,opacity: isDragging ? 0.5 : 1,cursor: "grab",}}><div className={` w-full  rounded-lg pl-1 ${selected ? "bg-gray-300 dark:bg-gray-700 font-bold" : ""} `} ># {name}</div></div>)
}interface CategoryProps {id: string;name: string;channels: Channel[];channelIds: string[];active: string | undefined;
}
const Category: React.FC<CategoryProps> = ({ id, name, channels, channelIds, active }) => {const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id, data: { type: 'category' }, });const [collapsed, setCollapsed] = useState(true); // 控制折叠状态const selectChannel = channels.find(channel => channel.id === active);return (<divref={setNodeRef}{...attributes}style={{transform: CSS.Transform.toString(transform),transition,opacity: isDragging ? 0.5 : 1,cursor: "grab",}}><div className="flex flex-row  text-nowrap   group"><div className=" flex flex-1 flex-row items-center cursor-pointer " onClick={() => setCollapsed(!collapsed)}>{collapsed ? <ChevronDown size={22} /> : <ChevronRight size={22} />}<div className="flex-1 ">{name}</div></div><div{...listeners}  // 绑定拖拽事件到这个点style={{cursor: "grab",}}className="opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-grab"><GripVertical width={18} /></div></div>{collapsed ? (<SortableContext items={channelIds} strategy={verticalListSortingStrategy}>{channels.map((channel) => (<div  key={channel.id}  className='ml-2 m-1  rounded-lg  hover:dark:bg-gray-700   hover:bg-gray-300 cursor-pointer'><ChannelItem                                       id={channel.id}name={channel.name}selected={channel.id === active}/></div>))}</SortableContext>) : (channels.find(channel => channel.id === active) && (<div className="pl-1 ml-2 mr-1 mt-1 font-bold  rounded-lg   dark:bg-gray-700  bg-gray-300 cursor-pointer"># {selectChannel?.name}</div>))}</div>)}const ChannelList: React.FC = () => {const { categories, categoryIds, channelIds, setCategories } = useChannelsStore();const [activeItem, setActiveItem] = useState<{ id: string; name: string; type: string } | undefined>();const [moving, setMoving] = useState(false); // 移动时候渲染const handleDragStart = (event: DragStartEvent) => {//当前选中id ,以便组件中高亮显示const activeData = event.active.data.current as { id: string; name: string } | undefined;if (activeData) {setActiveItem({id: String(event.active.id),name: activeData.name,type: activeData?.type, // 类型});}}const handleDragOver = (event: DragOverEvent) => {setMoving(false)const { active, over } = event;if (!over) return;const activeId = active.id as string;const overId = over.id as string;// 处理类别排序if (activeItem?.type === "category") {setCategories((prevCategories) => {const oldIndex = prevCategories.findIndex((cat) => cat.id === activeId);const newIndex = prevCategories.findIndex((cat) => cat.id === overId);if (oldIndex === -1 || newIndex === -1) return prevCategories;return arrayMove([...prevCategories], oldIndex, newIndex);});return;}if (activeItem?.type === "channel") {setCategories((prevCategories) => {const newCategories = [...prevCategories];const fromCategory = newCategories.find((cat) =>cat.channels.some((ch) => ch.id === activeId));const toCategory = newCategories.find((cat) =>cat.channels.some((ch) => ch.id === overId));if (!fromCategory || !toCategory) return prevCategories;const fromCategoryId = fromCategory.id;const toCategoryId = toCategory.id;if (fromCategory !== toCategory) {const fromCat = newCategories.find((cat) => cat.id === fromCategoryId);const toCat = newCategories.find((cat) => cat.id === toCategoryId);if (!fromCat || !toCat) return prevCategories;const channelIndex = fromCat.channels.findIndex((ch) => ch.id === activeId);if (channelIndex === -1) return prevCategories;const [movedChannel] = fromCat.channels.splice(channelIndex, 1);toCat.channels = [...toCat.channels, movedChannel];return newCategories;} else {const fromCat = newCategories.find((cat) => cat.id === fromCategoryId);if (!fromCat) return prevCategories;const channelIndex = fromCat.channels.findIndex((ch) => ch.id === activeId);const targetIndex = fromCat.channels.findIndex((ch) => ch.id === overId);if (channelIndex !== targetIndex) {fromCat.channels = [...arrayMove([...fromCat.channels], channelIndex, targetIndex)];return newCategories;}return prevCategories;}});}}const handleDragEnd = (event: DragEndEvent) => {setMoving(false)const { active, over } = event;if (!over) return;}const handleDragMove = (event: DragEndEvent) => {setMoving(true)}const renderDragOverlay = (activeItem: { id: string; name: string; type: string }) => {switch (activeItem.type) {case "category":{const category = categories.find(category => category.id === activeItem.id);return (category && (<div><Categorykey={category.id}id={category.id}name={category.name}channels={category.channels}channelIds={[]}active={''} /></div>))}case "channel":{const channel = categories.flatMap(category => category.channels).find(channel => channel.id === activeItem.id);return (channel && (<div><ChannelItem id={channel.id} name={channel.name} selected={true} /></div>))}default:return null;}};return (<DndContextcollisionDetection={pointerWithin}onDragStart={handleDragStart}onDragEnd={handleDragEnd}onDragOver={handleDragOver}onDragMove={handleDragMove}><SortableContext items={categoryIds} strategy={verticalListSortingStrategy}>{categories.map((category) => (<Categorykey={category.id}id={category.id}name={category.name}channels={category.channels}channelIds={channelIds}active={activeItem?.id} />))}</SortableContext><DragOverlay>{moving && activeItem?.type && renderDragOverlay(activeItem)}</DragOverlay></DndContext>);
};export default ChannelList;

2.useChannelsStore.ts

//useChannelsStore.ts
import { create } from "zustand";// 频道接口
export interface Channel {id: string;name: string;
}// 频道类型接口
export interface Category {id: string;name: string;channels: Channel[];
}// 初始化频道类型
const initialChannelTypes: Category[] = [{id: "text",name: "文字",channels: [{ id: "1", name: "文字频道1" },{ id: "2", name: "文字频道2" },],},{id: "void",name: "语音",channels: [{ id: "3", name: "语音频道1" },{ id: "4", name: "语音频道2" },],},{id: "prv",name: "私密",channels: [{ id: "5", name: "私密频道1" },{ id: "6", name: "私密频道2" },],},
];interface ChannelsStore {categories: Category[];channelIds: string[];categoryIds: string[];setCategories: (update: Category[] | ((prev: Category[]) => Category[])) => void;}const useChannelsStore = create<ChannelsStore>((set) => ({categories: initialChannelTypes,channelIds: initialChannelTypes.flatMap((channelType) =>channelType.channels.map((channel) => channel.id)),categoryIds: initialChannelTypes.map((category) => category.id),setCategories: (update) => set((state) => ({categories: typeof update === "function" ? update(state.categories) : update,}))
}));export default useChannelsStore;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/75540.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

新浪财经股票每天10点自动爬取

老规矩还是先分好三步&#xff0c;获取数据&#xff0c;解析数据&#xff0c;存储数据 因为股票是实时的&#xff0c;所以要加个cookie值&#xff0c;最好分线程或者爬取数据时等待爬取&#xff0c;不然会封ip 废话不多数&#xff0c;直接上代码 import matplotlib import r…

使用Android 原生LocationManager获取经纬度

一、常用方案 1、使用LocationManager GPS和网络定位 缺点&#xff1a;个别设备,室内或者地下停车场获取不到gps定位,故需要和网络定位相结合使用 2、使用Google Play服务 这种方案需要Android手机中有安装谷歌服务,然后导入谷歌的第三方库&#xff1a; 例如&#xff1a;i…

验证码实现

验证码案例 学了Spring MVC &#xff0c;配置 相关章节&#xff0c; 现可以尝试写一个前后端交互的验证码 文章目录 验证码案例前言一、验证码是什么&#xff1f;二、需求1.引入依赖2.导入前端页面3.约定前后段交互接口 三、代码解析Controllermodelapplication.xml 四丶结果五…

查询当前用户的购物车和清空购物车

业务需求&#xff1a; 在小程序用户端购物车页面能查到当前用户的所有菜品或者套餐 代码实现 controller层 GetMapping("/list")public Result<List<ShoppingCart>> list(){List<ShoppingCart> list shoppingCartService.shopShoppingCart();r…

(多看) CExercise_05_1函数_1.2计算base的exponent次幂

题目&#xff1a; 键盘录入两个整数&#xff1a;底(base)和幂指数(exponent)&#xff0c;计算base的exponent次幂&#xff0c;并打印输出对应的结果。&#xff08;注意底和幂指数都可能是负数&#xff09; 提示&#xff1a;求幂运算时&#xff0c;基础的思路就是先无脑把指数转…

【nacos安装指南】

Nacos安装指南 1.Windows安装 开发阶段采用单机安装即可。 1.1.下载安装包 在Nacos的GitHub页面&#xff0c;提供有下载链接&#xff0c;可以下载编译好的Nacos服务端或者源代码&#xff1a; GitHub主页&#xff1a;https://github.com/alibaba/nacos GitHub的Release下载…

通过发音学英语单词:从音到形的学习方法

&#x1f4cc; 通过发音学英语单词&#xff1a;从音到形的学习方法 英语是一种 表音语言&#xff08;phonetic language&#xff09;&#xff0c;但不像拼音文字&#xff08;如汉语拼音、西班牙语等&#xff09;那么规则&#xff0c;而是 部分表音部分表意。这意味着我们可以通…

列表某个字段由多个值组成,使用id匹配展示

说明&#xff1a;列表中字段A的值由多个值组成&#xff0c;但是后端返回的是这多个值的id字符串&#xff0c;需要前端拿着多个id组成的字符串去另一个接口数据源匹配展示 列表后端返回多个字符串如下&#xff1a; sectorName: "1899292545382895618,1907311191514636289…

MQL5教程 05 指标开发实战:双色线、双线变色MACD、跨时间周期均线

文章目录 一、双色线指标二、双线变色MACD指标三、跨时间周期均线 一、双色线指标 这里的类型中&#xff0c;Color开头的&#xff0c;是可以选择多个颜色的。 #property indicator_chart_window #property indicator_buffers 18 #property indicator_plots 7 //--- plot xian…

Java全栈面试宝典:线程安全机制与Spring Boot核心原理深度解析

目录 一、Java线程安全核心原理 &#x1f525; 问题1&#xff1a;线程安全的三要素与解决方案 线程安全风险模型 线程安全三要素 synchronized解决方案 &#x1f525; 问题2&#xff1a;synchronized底层实现全解析 对象内存布局 Mark Word结构&#xff08;64位系统&…

【Cursor】设置语言

Ctrl Shift P 搜索 configure display language选择“中文-简体”

【新能源汽车整车动力学模型深度解析:面向MATLAB/Simulink仿真测试工程师的硬核指南】

1. 前言 作为MATLAB/Simulink仿真测试工程师,掌握新能源汽车整车动力学模型的构建方法和实现技巧至关重要。本文将提供一份6000+字的深度技术解析,涵盖从基础理论到Simulink实现的完整流程。内容经过算法优化设计,包含12个核心方程、6大模块实现和3种验证方法,满足SEO流量…

Java 线程池与 Kotlin 协程 高阶学习

以下是Java 线程池与 Kotlin 协程 高阶学习的对比指南&#xff0c;结合具体代码示例&#xff0c;展示两者在异步任务处理中的差异和 Kotlin 的简化优势&#xff1a; 分析&#xff1a; 首先&#xff0c;我们需要回忆Java中线程池的常见用法&#xff0c;比如通过ExecutorService创…

嵌入式EMC设计面试题及参考答案

目录 解释 EMC(电磁兼容性)的定义及其两个核心方面(EMI 和 EMS) 电磁兼容三要素及相互关系 为什么产品必须进行 EMC 设计?列举至少三个实际工程原因 分贝(dB)在 EMC 测试中的作用是什么?为何采用对数单位描述干扰强度? 传导干扰与辐射干扰的本质区别及典型频率范围…

实操(进程状态,R/S/D/T/t/X/Z)Linux

1 R 状态并不直接代表进程在运行&#xff0c;而是该进程在运行队列中进行排队&#xff0c;由操作系统在内存维护的队列 #include <stdio.h> #include <unistd.h>int main() {while(1){printf("我在运行吗\n");sleep(1);}return 0; }查看状态&#xff08…

React 文件上传新玩法:Aliyun OSS 加持的智能上传组件

文件上传是前端开发中的“老朋友”&#xff0c;但如何让它既简单又强大&#xff0c;还能无缝对接云端存储&#xff1f;今天&#xff0c;我要带你认识一个超酷的 React 组件 AliUploader&#xff0c;它不仅支持拖拽上传、批量编辑和文件排序&#xff0c;还直接把文件传到 Aliyun…

LabVIEW多线程

在 LabVIEW 中&#xff0c;多线程编程是提升程序执行效率的关键手段&#xff0c;尤其是在需要并行处理数据采集、控制执行和用户界面交互的场景下。LabVIEW 本身是基于数据流&#xff08;Dataflow&#xff09;的编程语言&#xff0c;天然支持多线程&#xff0c;但要高效利用多线…

图解AUTOSAR_SWS_LINStateManager

AUTOSAR LIN状态管理器(LinSM)详细设计 文档摘要 本文档提供了AUTOSAR LIN状态管理器(LinSM)模块的详细设计解析,包括架构、状态机、睡眠唤醒流程以及配置结构。通过图形化方式展现LinSM在AUTOSAR通信栈中的作用及其与其他模块的交互关系。 目录 AUTOSAR LIN状态管理器(Lin…

python+form+opengl显示动态图形数据

说明&#xff1a; pythonformopengl显示动态图形数据 我希望做一款动态opengl图形数据 1.用python脚本&#xff0c;输入指定参数 2.生成一组数据&#xff0c; 3.将数据保持成本地文件 4.在c#中调用此文件&#xff0c;解析 5.将数据用opengl展示 效果图: step1:添加依赖 C:\U…

Android Gradle、Android Gradle Plugin、BuildTool关系

1. Gradle 的定位&#xff1a;通用构建工具 Gradle 是一个通用的跨平台构建工具&#xff0c;支持多种语言&#xff08;如 Java、Kotlin、C&#xff09;和项目类型 它的核心功能包括&#xff1a; ​任务自动化&#xff1a;通过 Groovy/Kotlin DSL 脚本定义编译、测试、打包等…