Browse Source

feat: 拖拽完成-1

xyh 2 years ago
parent
commit
cce17a79e7

+ 3 - 0
packages/app/package.json

@@ -37,6 +37,8 @@
     "react-hook-form": "^7.43.0",
     "react-modal": "^3.16.1",
     "react-router-dom": "^6.8.0",
+    "react-sticky-box": "^2.0.4",
+    "react-stickynode": "^4.1.0",
     "react-virtualized-auto-sizer": "^1.0.15",
     "react-window": "^1.8.9",
     "recharts": "^2.4.3",
@@ -54,6 +56,7 @@
     "@types/react": "^18.0.27",
     "@types/react-dom": "^18.0.10",
     "@types/react-modal": "^3.13.1",
+    "@types/react-stickynode": "^4.0.0",
     "@types/react-window": "^1.8.5",
     "jest-canvas-mock": "^2.4.0"
   }

+ 43 - 0
packages/app/src/components/table/header-th/index.tsx

@@ -0,0 +1,43 @@
+import {Header, flexRender} 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';
+
+const HeaderTh: FC<{header: Header<Record<string, any>, unknown>}> = function ({
+  header,
+}) {
+  const {setNodeRef, attributes, listeners, transform, transition, isSorting} =
+    useSortable({id: header.id});
+
+  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}
+    >
+      {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(),
+        })}
+      />
+    </th>
+  );
+};
+
+export default HeaderTh;

+ 32 - 1
packages/app/src/components/table/hooks.ts

@@ -1,3 +1,4 @@
+import {DragEndEvent, DragMoveEvent} from '@dnd-kit/core';
 import {
   createColumnHelper,
   useReactTable,
@@ -5,6 +6,8 @@ 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>>();
@@ -41,6 +44,7 @@ function parseColumn<T>(columns: ColumnType<T>[]) {
       cell({row}) {
         return row.index;
       },
+      meta: {align: 'center'},
     }),
   );
 
@@ -65,16 +69,43 @@ export function useTable<T extends Record<string, any>>(
     return obj;
   });
 
+  const [columnOrder, setColumnOrder] = useState(function () {
+    const nextList = columns.map(val => val.dataIndex!.toString());
+
+    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,
     state: {
       columnSizing,
+      columnOrder,
     },
     getCoreRowModel: getCoreRowModel(),
     onColumnSizingChange: setColumnSizing,
     columnResizeMode: 'onChange',
   });
 
-  return {...table};
+  return [{...table}, onDrag] as const;
 }

+ 7 - 0
packages/app/src/components/table/index.module.css

@@ -10,6 +10,7 @@
   min-width: 100%;
   padding: 0;
   margin: 0;
+  overflow: unset;
   font-size: 14px;
   line-height: 1.5714;
   color: rgb(0 0 0 / 88%);
@@ -27,6 +28,8 @@
 }
 
 .table-head {
+  position: sticky;
+  top: 0;
   text-align: center;
 
   & th {
@@ -83,3 +86,7 @@
 .resizing {
   opacity: 1;
 }
+
+.sticky {
+  width: 100%;
+}

+ 80 - 64
packages/app/src/components/table/index.tsx

@@ -1,18 +1,29 @@
 import {Spin} from 'antd';
 import 'antd/lib/table/style';
-import {ColumnsType} from 'antd/es/table';
+import {ColumnType, ColumnsType} from 'antd/es/table';
 import {
   createPageContext,
   createSearchContext,
   usePage,
   useTableSearchState,
 } from '@hooks';
-import {PAGE_SIZE_LIST, calcColumnsWidth} from '@utils';
-import {ReactElement, useMemo} from 'react';
+import {PAGE_SIZE_LIST} from '@utils';
+import {ReactElement} from 'react';
 import {useTable} from './hooks';
 import {flexRender} from '@tanstack/react-table';
 import css from './index.module.css';
-import classNames from 'classnames';
+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>;
@@ -37,72 +48,77 @@ function Table<T extends Record<string, any>>(props: Props<T>): ReactElement {
   } = props;
   const [{page, pageSize}, {onPageChange}] = usePage(pageContext);
   const [isSearching] = useTableSearchState(searchContext);
-  const colWidth = calcColumnsWidth(columns);
 
-  const {getHeaderGroups, getRowModel, getCenterTotalSize} = useTable(
+  const [{getHeaderGroups, getRowModel, getCenterTotalSize}, onDrag] = useTable(
     columns,
     data,
   );
+  const sensor = useSensor(PointerSensor);
 
   return (
-    <Spin spinning={isSearching}>
-      <div className={css.tableWrapper}>
-        <table className={css.table} style={{width: getCenterTotalSize()}}>
-          <thead className={css.tableHead}>
-            {getHeaderGroups().map(function ({id, headers}) {
-              return (
-                <tr key={id}>
-                  {headers.map(header => (
-                    <th key={header.id} style={{width: header.getSize()}}>
-                      {header.isPlaceholder
-                        ? null
-                        : flexRender(
-                            header.column.columnDef.header,
-                            header.getContext(),
-                          )}
-
-                      <div
-                        onMouseDown={header.getResizeHandler()}
-                        className={classNames(css.resizer, {
-                          [css.resizing]: header.column.getIsResizing(),
-                        })}
-                      />
-                    </th>
-                  ))}
-                </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>
+    <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>
   );
 }
 

+ 54 - 2
pnpm-lock.yaml

@@ -173,6 +173,12 @@ importers:
       react-router-dom:
         specifier: ^6.8.0
         version: 6.8.0(react-dom@18.2.0)(react@18.2.0)
+      react-sticky-box:
+        specifier: ^2.0.4
+        version: 2.0.4(react@18.2.0)
+      react-stickynode:
+        specifier: ^4.1.0
+        version: 4.1.0(react-dom@18.2.0)(react@18.2.0)
       react-virtualized-auto-sizer:
         specifier: ^1.0.15
         version: 1.0.15(react-dom@18.2.0)(react@18.2.0)
@@ -219,6 +225,9 @@ importers:
       '@types/react-modal':
         specifier: ^3.13.1
         version: 3.13.1
+      '@types/react-stickynode':
+        specifier: ^4.0.0
+        version: 4.0.0
       '@types/react-window':
         specifier: ^1.8.5
         version: 1.8.5
@@ -3402,6 +3411,12 @@ packages:
       '@types/react': 18.0.27
     dev: true
 
+  /@types/react-stickynode@4.0.0:
+    resolution: {integrity: sha512-PKkmOzF6WCNuyIKrvhidGeUPLfe8htPwfEljKnQBF4bA5v74ADvXtwkjavOH8i6aCSw9J14AyDDl1Ul0VNQJUg==}
+    dependencies:
+      '@types/react': 18.0.27
+    dev: true
+
   /@types/react-window@1.8.5:
     resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==}
     dependencies:
@@ -4996,7 +5011,6 @@ packages:
   /core-js@3.26.1:
     resolution: {integrity: sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==}
     requiresBuild: true
-    dev: true
 
   /core-util-is@1.0.2:
     resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
@@ -6320,6 +6334,10 @@ packages:
     resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==}
     dev: false
 
+  /eventemitter3@3.1.2:
+    resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==}
+    dev: false
+
   /eventemitter3@4.0.7:
     resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
 
@@ -10354,7 +10372,6 @@ packages:
     resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==}
     dependencies:
       performance-now: 2.1.0
-    dev: true
 
   /randombytes@2.1.0:
     resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
@@ -11045,6 +11062,29 @@ packages:
       react-transition-group: 2.9.0(react-dom@18.2.0)(react@18.2.0)
     dev: false
 
+  /react-sticky-box@2.0.4(react@18.2.0):
+    resolution: {integrity: sha512-EcnT4fpyyrBwyy5tsWViBTFBMsJZijDC78s2D/GlqrKuU0vsmW+jNc/Ubs1C4/HF/YmTZ90Y5GIGLQMzScQQbA==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+    dev: false
+
+  /react-stickynode@4.1.0(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-zylWgfad75jLfh/gYIayDcDWIDwO4weZrsZqDpjZ/axhF06zRjdCWFBgUr33Pvv2+htKWqPSFksWTyB6aMQ1ZQ==}
+    peerDependencies:
+      react: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+      react-dom: ^0.14.2 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
+    dependencies:
+      classnames: 2.3.2
+      core-js: 3.26.1
+      prop-types: 15.8.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      shallowequal: 1.1.0
+      subscribe-ui-event: 2.0.7
+    dev: false
+
   /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==}
     peerDependencies:
@@ -11539,6 +11579,10 @@ packages:
       kind-of: 6.0.3
     dev: true
 
+  /shallowequal@1.1.0:
+    resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+    dev: false
+
   /shebang-command@2.0.0:
     resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
     engines: {node: '>=8'}
@@ -11997,6 +12041,14 @@ packages:
     resolution: {integrity: sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==}
     dev: false
 
+  /subscribe-ui-event@2.0.7:
+    resolution: {integrity: sha512-Acrtf9XXl6lpyHAWYeRD1xTPUQHDERfL4GHeNuYAtZMc4Z8Us2iDBP0Fn3xiRvkQ1FO+hx+qRLmPEwiZxp7FDQ==}
+    dependencies:
+      eventemitter3: 3.1.2
+      lodash: 4.17.21
+      raf: 3.4.1
+    dev: false
+
   /superjson@1.12.0:
     resolution: {integrity: sha512-B4tefmFqj8KDShHi2br2rz0kBlUJuQHtxMCydEuHvooL+6EscROTNWRfOLMDxW1dS/daK2zZr3C3N9DU+jXATQ==}
     engines: {node: '>=10'}