Browse Source

feat: 使用react-dnd完成拖拽

xyh 2 years ago
parent
commit
c0e6ac85d3

+ 2 - 0
packages/app/package.json

@@ -32,6 +32,8 @@
     "print-js": "^1.6.0",
     "react": "^18.2.0",
     "react-contexify": "^6.0.0",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
     "react-dom": "^18.2.0",
     "react-error-boundary": "^3.1.4",
     "react-hook-form": "^7.43.0",

+ 54 - 25
packages/app/src/components/table/header-th/index.tsx

@@ -1,41 +1,70 @@
-import {Header, flexRender} from '@tanstack/react-table';
+import {
+  Header,
+  flexRender,
+  Updater,
+  ColumnOrderState,
+  TableState,
+  Column,
+} from '@tanstack/react-table';
 import classNames from 'classnames';
 import {FC} from 'react';
 import css from '../index.module.css';
-import {useSortable} from '@dnd-kit/sortable';
-import {CSS} from '@dnd-kit/utilities';
+import {useDrag, useDrop} from 'react-dnd';
+import {arrayMove} from '@dnd-kit/sortable';
 
-const HeaderTh: FC<{header: Header<Record<string, any>, unknown>}> = function ({
-  header,
-}) {
-  const {setNodeRef, attributes, listeners, transform, transition, isSorting} =
-    useSortable({id: header.id});
+type Props = {
+  update: (updater: Updater<ColumnOrderState>) => void;
+  header: Header<Record<string, any>, unknown>;
+  getState: () => TableState;
+};
+
+const HeaderTh: FC<Props> = function ({header, update, getState}) {
+  const [, dropRef] = useDrop({
+    accept: 'table',
+    drop(item: Column<any>) {
+      const {id: fromId} = item,
+        toId = header.column.id;
+
+      if (fromId === toId) return;
+      const {columnOrder} = getState();
+
+      const fromIdx = columnOrder.findIndex(val => val === fromId),
+        toIdx = columnOrder.findIndex(val => val === toId);
+
+      update(arrayMove(columnOrder, fromIdx, toIdx));
+    },
+  });
+
+  const [_, dragRef] = useDrag({
+    type: 'table',
+    item: () => header.column,
+    collect(monitor) {
+      return {isDragging: monitor.isDragging()};
+    },
+  });
 
   return (
     <th
       key={header.id}
       style={{
         width: header.getSize(),
-        transition,
-        transform: CSS.Transform.toString(transform),
         cursor: 'pointer',
-        zIndex: isSorting ? '2' : '1',
       }}
-      ref={setNodeRef}
-      {...attributes}
-      {...listeners}
+      ref={dropRef}
     >
-      {header.isPlaceholder
-        ? null
-        : flexRender(header.column.columnDef.header, header.getContext())}
-
-      <div
-        onMouseDown={header.getResizeHandler()}
-        onPointerDown={e => e.stopPropagation()}
-        className={classNames(css.resizer, {
-          [css.resizing]: header.column.getIsResizing(),
-        })}
-      />
+      <div ref={dragRef}>
+        {header.isPlaceholder
+          ? null
+          : flexRender(header.column.columnDef.header, header.getContext())}
+
+        <div
+          onMouseDown={header.getResizeHandler()}
+          onClick={e => e.stopPropagation()}
+          className={classNames(css.resizer, {
+            [css.resizing]: header.column.getIsResizing(),
+          })}
+        />
+      </div>
     </th>
   );
 };

+ 3 - 24
packages/app/src/components/table/hooks.ts

@@ -1,4 +1,4 @@
-import {DragEndEvent, DragMoveEvent} from '@dnd-kit/core';
+import {DragMoveEvent} from '@dnd-kit/core';
 import {
   createColumnHelper,
   useReactTable,
@@ -6,8 +6,6 @@ import {
 } from '@tanstack/react-table';
 import {ColumnType} from 'antd/es/table';
 import {useRef, useState} from 'react';
-import {arrayMove} from '@dnd-kit/sortable';
-import {debounce} from 'lodash-es';
 
 function parseColumn<T>(columns: ColumnType<T>[]) {
   const hepler = createColumnHelper<Record<string, any>>();
@@ -75,26 +73,6 @@ export function useTable<T extends Record<string, any>>(
     return ['no', ...nextList];
   });
 
-  const onDrag = debounce(
-    function ({active, over}: DragMoveEvent) {
-      if (!over) return;
-
-      const {id: activeId} = active,
-        {id: overId} = over;
-
-      if (activeId === overId) return;
-
-      setColumnOrder(function (prev) {
-        const formIdx = prev.findIndex(val => val === activeId),
-          toIdx = prev.findIndex(val => val === overId);
-
-        return arrayMove(prev, formIdx, toIdx);
-      });
-    },
-    0,
-    {leading: false, trailing: true},
-  );
-
   const table = useReactTable({
     data,
     columns: columnList.current,
@@ -105,7 +83,8 @@ export function useTable<T extends Record<string, any>>(
     getCoreRowModel: getCoreRowModel(),
     onColumnSizingChange: setColumnSizing,
     columnResizeMode: 'onChange',
+    onColumnOrderChange: setColumnOrder,
   });
 
-  return [{...table}, onDrag] as const;
+  return [{...table}, setColumnOrder] as const;
 }

+ 59 - 81
packages/app/src/components/table/index.tsx

@@ -1,6 +1,5 @@
 import {Spin} from 'antd';
-import 'antd/lib/table/style';
-import {ColumnType, ColumnsType} from 'antd/es/table';
+import {ColumnsType} from 'antd/es/table';
 import {
   createPageContext,
   createSearchContext,
@@ -13,17 +12,6 @@ import {useTable} from './hooks';
 import {flexRender} from '@tanstack/react-table';
 import css from './index.module.css';
 import HeaderTh from './header-th';
-import {
-  DndContext,
-  useSensor,
-  PointerSensor,
-  closestCenter,
-} from '@dnd-kit/core';
-import {
-  SortableContext,
-  horizontalListSortingStrategy,
-} from '@dnd-kit/sortable';
-import {restrictToHorizontalAxis} from '@dnd-kit/modifiers';
 
 type Props<T> = {
   columns: ColumnsType<T>;
@@ -49,76 +37,66 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
   const [{page, pageSize}, {onPageChange}] = usePage(pageContext);
   const [isSearching] = useTableSearchState(searchContext);
 
-  const [{getHeaderGroups, getRowModel, getCenterTotalSize}, onDrag] = useTable(
-    columns,
-    data,
-  );
-  const sensor = useSensor(PointerSensor);
+  const [
+    {
+      getHeaderGroups,
+      getRowModel,
+      getCenterTotalSize,
+      setColumnOrder,
+      getState,
+    },
+  ] = useTable(columns, data);
 
   return (
-    <DndContext
-      sensors={[sensor]}
-      modifiers={[restrictToHorizontalAxis]}
-      collisionDetection={closestCenter}
-      autoScroll
-      onDragEnd={onDrag}
-    >
-      <SortableContext
-        items={(columns as ColumnType<T>[]).map(val =>
-          val.dataIndex!.toString(),
-        )}
-        strategy={horizontalListSortingStrategy}
-      >
-        <Spin spinning={isSearching}>
-          <div className={css.tableWrapper} id='table_wrapper'>
-            <table className={css.table} style={{width: getCenterTotalSize()}}>
-              <thead className={css.tableHead}>
-                {getHeaderGroups().map(function ({id, headers}) {
-                  return (
-                    <tr key={id}>
-                      {headers.map(header => (
-                        <HeaderTh key={header.id} header={header} />
-                      ))}
-                    </tr>
-                  );
-                })}
-              </thead>
-              <tbody className={css.tableBody}>
-                {getRowModel().rows.map(function ({id, getVisibleCells}) {
-                  return (
-                    <tr key={id}>
-                      {getVisibleCells().map(function ({
-                        id,
-                        column,
-                        getContext,
-                      }) {
-                        const align =
-                          (
-                            column.columnDef.meta as {
-                              align: 'left' | 'right' | 'center';
-                            }
-                          )?.align ?? 'left';
-                        return (
-                          <td
-                            key={id}
-                            style={{
-                              width: column.getSize(),
-                              textAlign: align,
-                            }}
-                          >
-                            {flexRender(column.columnDef.cell, getContext())}
-                          </td>
-                        );
-                      })}
-                    </tr>
-                  );
-                })}
-              </tbody>
-            </table>
-          </div>
-        </Spin>
-      </SortableContext>
-    </DndContext>
+    <Spin spinning={isSearching}>
+      <div className={css.tableWrapper} id='table_wrapper'>
+        <table className={css.table} style={{width: getCenterTotalSize()}}>
+          <thead className={css.tableHead}>
+            {getHeaderGroups().map(function ({id, headers}) {
+              return (
+                <tr key={id}>
+                  {headers.map(header => (
+                    <HeaderTh
+                      key={header.id}
+                      header={header}
+                      update={setColumnOrder}
+                      getState={getState}
+                    />
+                  ))}
+                </tr>
+              );
+            })}
+          </thead>
+          <tbody className={css.tableBody}>
+            {getRowModel().rows.map(function ({id, getVisibleCells}) {
+              return (
+                <tr key={id}>
+                  {getVisibleCells().map(function ({id, column, getContext}) {
+                    const align =
+                      (
+                        column.columnDef.meta as {
+                          align: 'left' | 'right' | 'center';
+                        }
+                      )?.align ?? 'left';
+                    return (
+                      <td
+                        key={id}
+                        style={{
+                          width: column.getSize(),
+                          textAlign: align,
+                        }}
+                      >
+                        {flexRender(column.columnDef.cell, getContext())}
+                      </td>
+                    );
+                  })}
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+    </Spin>
   );
 }
 

+ 15 - 11
packages/app/src/index.tsx

@@ -12,6 +12,8 @@ import 'dayjs/locale/zh-cn';
 import zhCN from 'antd/es/locale/zh_CN';
 import {Loading} from '@components';
 import {QUERY_CLIENT} from '@utils';
+import {DndProvider, useDrop} from 'react-dnd';
+import {HTML5Backend} from 'react-dnd-html5-backend';
 
 const root = createRoot(document.getElementById('root')!);
 
@@ -29,16 +31,18 @@ const themeConfig: ThemeConfig = {
 
 root.render(
   <StrictMode>
-    <QueryClientProvider client={QUERY_CLIENT}>
-      <ConfigProvider theme={themeConfig} locale={zhCN}>
-        <Suspense
-          fallback={<Loading tip='正在加载' width='100vw' height='100vh' />}
-        >
-          <BrowserRouter>
-            <RootRoutes />
-          </BrowserRouter>
-        </Suspense>
-      </ConfigProvider>
-    </QueryClientProvider>
+    <DndProvider backend={HTML5Backend}>
+      <QueryClientProvider client={QUERY_CLIENT}>
+        <ConfigProvider theme={themeConfig} locale={zhCN}>
+          <Suspense
+            fallback={<Loading tip='正在加载' width='100vw' height='100vh' />}
+          >
+            <BrowserRouter>
+              <RootRoutes />
+            </BrowserRouter>
+          </Suspense>
+        </ConfigProvider>
+      </QueryClientProvider>
+    </DndProvider>
   </StrictMode>,
 );

+ 69 - 3
pnpm-lock.yaml

@@ -158,6 +158,12 @@ importers:
       react-contexify:
         specifier: ^6.0.0
         version: 6.0.0(react-dom@18.2.0)(react@18.2.0)
+      react-dnd:
+        specifier: ^16.0.1
+        version: 16.0.1(@types/node@18.11.18)(@types/react@18.0.27)(react@18.2.0)
+      react-dnd-html5-backend:
+        specifier: ^16.0.1
+        version: 16.0.1
       react-dom:
         specifier: ^18.2.0
         version: 18.2.0(react@18.2.0)
@@ -2716,6 +2722,18 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
+  /@react-dnd/asap@5.0.2:
+    resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==}
+    dev: false
+
+  /@react-dnd/invariant@4.0.2:
+    resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==}
+    dev: false
+
+  /@react-dnd/shallowequal@4.0.2:
+    resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==}
+    dev: false
+
   /@remix-run/router@1.3.1:
     resolution: {integrity: sha512-+eun1Wtf72RNRSqgU7qM2AMX/oHp+dnx7BHk1qhK5ZHzdHTUU4LA1mGG1vT+jMc8sbhG3orvsfOmryjzx2PzQw==}
     engines: {node: '>=14'}
@@ -3389,7 +3407,6 @@ packages:
 
   /@types/prop-types@15.7.5:
     resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
-    dev: true
 
   /@types/qs@6.9.7:
     resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
@@ -3429,7 +3446,6 @@ packages:
       '@types/prop-types': 15.7.5
       '@types/scheduler': 0.16.2
       csstype: 3.1.1
-    dev: true
 
   /@types/retry@0.12.0:
     resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
@@ -3437,7 +3453,6 @@ packages:
 
   /@types/scheduler@0.16.2:
     resolution: {integrity: sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==}
-    dev: true
 
   /@types/semver@7.3.13:
     resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==}
@@ -5682,6 +5697,14 @@ packages:
       path-type: 4.0.0
     dev: true
 
+  /dnd-core@16.0.1:
+    resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==}
+    dependencies:
+      '@react-dnd/asap': 5.0.2
+      '@react-dnd/invariant': 4.0.2
+      redux: 4.2.1
+    dev: false
+
   /dns-equal@1.0.0:
     resolution: {integrity: sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==}
     dev: true
@@ -7042,6 +7065,12 @@ packages:
     hasBin: true
     dev: true
 
+  /hoist-non-react-statics@3.3.2:
+    resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+    dependencies:
+      react-is: 16.13.1
+    dev: false
+
   /homedir-polyfill@1.0.3:
     resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
     engines: {node: '>=0.10.0'}
@@ -10951,6 +10980,37 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: false
 
+  /react-dnd-html5-backend@16.0.1:
+    resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==}
+    dependencies:
+      dnd-core: 16.0.1
+    dev: false
+
+  /react-dnd@16.0.1(@types/node@18.11.18)(@types/react@18.0.27)(react@18.2.0):
+    resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==}
+    peerDependencies:
+      '@types/hoist-non-react-statics': '>= 3.3.1'
+      '@types/node': '>= 12'
+      '@types/react': '>= 16'
+      react: '>= 16.14'
+    peerDependenciesMeta:
+      '@types/hoist-non-react-statics':
+        optional: true
+      '@types/node':
+        optional: true
+      '@types/react':
+        optional: true
+    dependencies:
+      '@react-dnd/invariant': 4.0.2
+      '@react-dnd/shallowequal': 4.0.2
+      '@types/node': 18.11.18
+      '@types/react': 18.0.27
+      dnd-core: 16.0.1
+      fast-deep-equal: 3.1.3
+      hoist-non-react-statics: 3.3.2
+      react: 18.2.0
+    dev: false
+
   /react-dom@18.2.0(react@18.2.0):
     resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
     peerDependencies:
@@ -11225,6 +11285,12 @@ packages:
       postcss-value-parser: 3.3.1
     dev: false
 
+  /redux@4.2.1:
+    resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
+    dependencies:
+      '@babel/runtime': 7.20.13
+    dev: false
+
   /regenerate-unicode-properties@10.1.0:
     resolution: {integrity: sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==}
     engines: {node: '>=4'}