Forráskód Böngészése

update: 增加row 排序功能

xyh 2 éve
szülő
commit
08b6d3c78a

+ 1 - 0
packages/app/package.json

@@ -22,6 +22,7 @@
     "classnames": "^2.3.2",
     "dayjs": "^1.11.8",
     "fast-deep-equal": "^3.1.3",
+    "klona": "^2.0.6",
     "lodash-es": "^4.17.21",
     "react": "^18.2.0",
     "react-contexify": "^6.0.0",

+ 164 - 47
packages/app/src/components/table/Body.tsx

@@ -1,8 +1,30 @@
 import classNames from 'classnames';
-import {forwardRef} from 'react';
+import {
+  Dispatch,
+  MutableRefObject,
+  SetStateAction,
+  forwardRef,
+  useEffect,
+  useState,
+} from 'react';
 import BodyTr from './BodyTr';
 import {Empty} from 'antd';
-import {HeaderGroup, RowModel} from '@tanstack/react-table';
+import {Cell, HeaderGroup, RowModel} from '@tanstack/react-table';
+import {
+  DndContext,
+  DragEndEvent,
+  DragOverlay,
+  DragStartEvent,
+  PointerSensor,
+  closestCenter,
+  useSensor,
+} from '@dnd-kit/core';
+import {
+  SortableContext,
+  arrayMove,
+  verticalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import {debounce} from 'lodash-es';
 
 type Props = {
   scrollProgess: 'start' | 'end' | 'process',
@@ -11,9 +33,19 @@ type Props = {
   highlightValue?: unknown,
   hightlightKey?: any,
   getHeaderGroups: () => HeaderGroup<Record<string, any>>[],
+  enableDnd?: boolean,
+  rawKey?: any,
+  data: any[],
+  setData: Dispatch<SetStateAction<any[]>>,
+  isSecondLevel?: boolean,
 };
 
-export default forwardRef<HTMLTableElement, Props>(function TableBody(
+type ActiveState = {
+  getVisibleCells: () => Cell<Record<string, any>, unknown>[],
+  id: string,
+} | null;
+
+export default forwardRef<HTMLDivElement, Props>(function TableBody(
   {
     scrollProgess,
     getCenterTotalSize,
@@ -21,56 +53,141 @@ export default forwardRef<HTMLTableElement, Props>(function TableBody(
     highlightValue,
     hightlightKey,
     getHeaderGroups,
+    data,
+    enableDnd,
+    rawKey,
+    setData,
+    isSecondLevel,
   },
   ref,
 ) {
+  const sensor = useSensor(PointerSensor);
+
+  const [active, setActive] = useState<ActiveState>(null);
+  function onDragStart(event: DragStartEvent) {
+    setActive(event.active.data.current as ActiveState);
+  }
+  function onDragEnd({over, active}: DragEndEvent) {
+    setActive(null);
+    if (!over)
+      return;
+
+    const {id: activeId} = active,
+          {id: overId} = over;
+
+    if (activeId === overId)
+      return;
+
+    setData(function(prev) {
+      const fromIdx = prev.findIndex(val => val[rawKey ?? 'id'] === activeId),
+            toIdx = prev.findIndex(val => val[rawKey ?? 'id'] === overId);
+
+      return arrayMove(prev, fromIdx, toIdx);
+    });
+  }
+
+  const [tableWidth, setTableWidth] = useState(0);
+
+  useEffect(function() {
+    if (!enableDnd) return;
+
+    const dom = (ref as MutableRefObject<HTMLDivElement>).current;
+    const obsever = new ResizeObserver(debounce(
+      function([entrie]) {
+        setTableWidth(entrie.contentRect.width);
+      },
+      300,
+      {leading: false, trailing: true},
+    ));
+
+    obsever.observe(dom);
+
+    return () => obsever.disconnect();
+  }, [enableDnd, ref]);
+
   return (
-    <div className="ld-table-body-wrapper" ref={ref}>
-      <table
-        className={classNames('ld-table', {
-          'ld-table-enabled-right-fixed-shadow': scrollProgess !== 'end',
-          'ld-table-enabled-left-fixed-shadow': scrollProgess !== 'start',
-        })}
-        style={{width: getCenterTotalSize()}}
+    <DndContext
+      sensors={[sensor]}
+      collisionDetection={closestCenter}
+      onDragStart={onDragStart}
+      onDragEnd={onDragEnd}
+    >
+      <SortableContext
+        items={data.map(val => val[rawKey ?? 'id'])}
+        strategy={verticalListSortingStrategy}
       >
-        <tbody className="ld-table-body">
-          {getRowModel().rows.length > 0 ? (
-            getRowModel().rows.map(function({
-              id,
-              getVisibleCells,
-              original,
-            }) {
-              return (
-                <BodyTr
-                  getVisibleCells={getVisibleCells}
-                  key={id}
-                  highlight={original[hightlightKey as string ?? 'id'] === highlightValue}
-                />
-              );
-            })
-          ) : (
-            <tr>
-              <td
-                colSpan={getHeaderGroups()[0].headers.length}
-                style={{padding: 0}}
+        <div className="ld-table-body-wrapper" ref={ref}>
+          <table
+            className={classNames('ld-table', {
+              'ld-table-enabled-right-fixed-shadow': scrollProgess !== 'end',
+              'ld-table-enabled-left-fixed-shadow': scrollProgess !== 'start',
+            })}
+            style={{width: getCenterTotalSize()}}
+          >
+            <tbody className="ld-table-body">
+              {getRowModel().rows.length > 0 ? (
+                getRowModel().rows.map(function({
+                  id,
+                  getVisibleCells,
+                  original,
+                }) {
+                  return (
+                    <BodyTr
+                      id={id}
+                      getVisibleCells={getVisibleCells}
+                      key={id}
+                      highlight={original[hightlightKey as string ?? 'id'] === highlightValue}
+                      enableDnd={enableDnd}
+                    />
+                  );
+                })
+              ) : (
+                <tr>
+                  <td
+                    colSpan={getHeaderGroups()[0].headers.length}
+                    style={{padding: 0}}
+                  >
+                    <Empty
+                      image={Empty.PRESENTED_IMAGE_SIMPLE}
+                      style={{
+                        width: '400px',
+                        position: 'sticky',
+                        left: '50%',
+                        transform: 'translateX(-50%)',
+                        overflow: 'hidden',
+                        margin: 0,
+                        padding: '50px 10px',
+                      }}
+                    />
+                  </td>
+                </tr>
+              )}
+            </tbody>
+          </table>
+        </div>
+        <DragOverlay>
+          {active && (
+            <div
+              className={classNames({
+                'ld-table-preview-transform': isSecondLevel,
+              })}
+              style={{width: tableWidth, overflow: 'hidden'}}
+            >
+              <table
+                className={classNames('ld-table', {
+                  'ld-table-enabled-right-fixed-shadow': scrollProgess !== 'end',
+                  'ld-table-enabled-left-fixed-shadow': scrollProgess !== 'start',
+                })}
+                style={{width: getCenterTotalSize()}}
               >
-                <Empty
-                  image={Empty.PRESENTED_IMAGE_SIMPLE}
-                  style={{
-                    width: '400px',
-                    position: 'sticky',
-                    left: '50%',
-                    transform: 'translateX(-50%)',
-                    overflow: 'hidden',
-                    margin: 0,
-                    padding: '50px 10px',
-                  }}
-                />
-              </td>
-            </tr>
+                <tbody>
+                  <BodyTr {...active} preview />
+                </tbody>
+              </table>
+            </div>
           )}
-        </tbody>
-      </table>
-    </div>
+        </DragOverlay>
+      </SortableContext>
+    </DndContext>
   );
 });

+ 25 - 2
packages/app/src/components/table/BodyTr.tsx

@@ -1,19 +1,42 @@
+import {useSortable} from '@dnd-kit/sortable';
 import {Cell, flexRender} from '@tanstack/react-table';
 import classNames from 'classnames';
 import {CSSProperties, FC} from 'react';
 
 type Props = {
   getVisibleCells: () => Cell<Record<string, any>, unknown>[],
+  id: string,
   highlight?: boolean,
+  enableDnd?: boolean,
+  preview?: boolean,
 };
 
-const BodyTr: FC<Props> = function({getVisibleCells, highlight}) {
+const BodyTr: FC<Props> = function({
+  getVisibleCells,
+  highlight,
+  id,
+  enableDnd,
+  preview,
+}) {
   const trList = getVisibleCells();
+  const {setNodeRef, listeners, attributes} = useSortable({
+    id,
+    disabled: !enableDnd,
+    data: {
+      getVisibleCells,
+      id,
+    },
+  });
 
   return (
     <tr
-      className={classNames({
+      ref={setNodeRef}
+      {...listeners}
+      {...attributes}
+      style={{cursor: enableDnd || preview ? 'move' : 'auto'}}
+      className={classNames('ld-table-body-tr ', {
         'ld-table-tr-highlight': highlight,
+        'ld-table-tr-preview': preview,
       })}
     >
       {trList.map(function({

+ 5 - 2
packages/app/src/components/table/Header.tsx

@@ -86,13 +86,16 @@ export default forwardRef<HTMLDivElement, Props>(function TableHead(
         </div>
         <DragOverlay>
           {active ? (
-            <table className="width-full">
+            <table
+              className={classNames('width-full', {
+                'ld-table-preview-transform': isSecondLevel,
+              })}
+            >
               <thead className="width-full">
                 <tr className="width-full">
                   <HeaderTh
                     header={active}
                     preview
-                    isSecondLevel={isSecondLevel}
                   />
                 </tr>
               </thead>

+ 0 - 3
packages/app/src/components/table/HeaderTh.tsx

@@ -6,7 +6,6 @@ import {useSortable} from '@dnd-kit/sortable';
 type Props = {
   header: Header<Record<string, any>, unknown>;
   preview?: boolean;
-  isSecondLevel?: boolean;
   disabledSizeChange?: boolean;
   disabledHeadSort?: boolean;
 };
@@ -14,7 +13,6 @@ type Props = {
 const HeaderTh: FC<Props> = function({
   header,
   preview,
-  isSecondLevel,
   disabledSizeChange,
   disabledHeadSort,
 }) {
@@ -60,7 +58,6 @@ const HeaderTh: FC<Props> = function({
       className={classNames('ld-table-head-th', {
         'ld-table-fixed-right ld-table-right-fixed-shadow': fixed === 'right',
         'ld-table-fixed-left ld-table-left-fixed-shadow': fixed === 'left',
-        'ld-table-preview-transform': isSecondLevel && preview,
       })}
     >
       {flexRender(header.column.columnDef.header, header.getContext())}

+ 3 - 0
packages/app/src/components/table/hooks.tsx

@@ -130,12 +130,14 @@ export function useTable<T extends Record<string, any>>(
     settingContext: ReturnType<typeof createTableSettingContext>;
     rowSelection?: RowSelectionState;
     setRowSelection?: Dispatch<SetStateAction<RowSelectionState>>;
+    rawKey?: keyof T;
   },
 ) {
   const {
     settingContext,
     rowSelection,
     setRowSelection,
+    rawKey,
   } = options;
 
   const columnList = useMemo(
@@ -182,6 +184,7 @@ export function useTable<T extends Record<string, any>>(
     columnResizeMode: 'onChange',
     onColumnOrderChange: setColumnOrder,
     onRowSelectionChange: setRowSelection,
+    getRowId: row => row[rawKey as string | undefined ?? 'id'],
   });
 
   type ActiveState = Header<Record<string, any>, unknown> | null;

+ 51 - 54
packages/app/src/components/table/index.css

@@ -22,49 +22,44 @@
   background: var(--layout-background-color);
   border-top: 1px solid var(--border-color-light);
   border-radius: var(--border-radius) var(--border-radius) 0 0;
+}
 
-  & td {
-    min-width: 0;
-    background-color: var(--layout-background-color);
+& .ld-table-right-fixed-shadow,
+& .ld-table-left-fixed-shadow {
+  &::before {
+    position: absolute;
+    top: 0;
+    bottom: -1px;
+    width: 30px;
+    pointer-events: none;
+    content: '';
+    transition: box-shadow var(--animate-duration) linear;
   }
+}
 
-  & .ld-table-right-fixed-shadow,
-  & .ld-table-left-fixed-shadow {
-    &::before {
-      position: absolute;
-      top: 0;
-      bottom: -1px;
-      width: 30px;
-      pointer-events: none;
-      content: '';
-      transition: box-shadow var(--animate-duration) linear;
-    }
+& .ld-table-right-fixed-shadow {
+  &::before {
+    left: 0;
+    transform: translateX(-100%);
   }
+}
 
-  & .ld-table-right-fixed-shadow {
-    &::before {
-      left: 0;
-      transform: translateX(-100%);
-    }
-  }
-
-  & .ld-table-left-fixed-shadow {
-    &::before {
-      right: 0;
-      transform: translateX(100%);
-    }
+& .ld-table-left-fixed-shadow {
+  &::before {
+    right: 0;
+    transform: translateX(100%);
   }
+}
 
-  & .ld-table-right-fixed-shadow ~ .ld-table-right-fixed-shadow {
-    &::before {
-      display: none;
-    }
+& .ld-table-right-fixed-shadow ~ .ld-table-right-fixed-shadow {
+  &::before {
+    display: none;
   }
+}
 
-  & .ld-table-left-fixed-shadow:has(~ .ld-table-left-fixed-shadow) {
-    &::before {
-      display: none;
-    }
+& .ld-table-left-fixed-shadow:has(~ .ld-table-left-fixed-shadow) {
+  &::before {
+    display: none;
   }
 }
 
@@ -88,6 +83,28 @@
   }
 }
 
+.ld-table-body-tr {
+  &:hover {
+    & td {
+      background-color: var(--background-color);
+    }
+  }
+
+  & td {
+    position: relative;
+    min-width: 0;
+    padding: 16px;
+    overflow-wrap: anywhere;
+    background-color: var(--layout-background-color);
+    border-inline-end: 1px solid var(--border-color-light);
+    border-bottom: 1px solid var(--border-color-light);
+    transition: background 0.2s, border-color 0.2s;
+  }
+}
+
+/* .ld-table-tr-preview {
+} */
+
 .ld-table-enabled-right-fixed-shadow {
   & .ld-table-right-fixed-shadow {
     &::before {
@@ -118,26 +135,6 @@
   text-align: center;
 }
 
-.ld-table-body {
-  & td {
-    position: relative;
-    min-width: 0;
-    padding: 16px;
-    overflow-wrap: anywhere;
-    border-inline-end: 1px solid var(--border-color-light);
-    border-bottom: 1px solid var(--border-color-light);
-    transition: background 0.2s, border-color 0.2s;
-  }
-
-  & tr {
-    &:hover {
-      & td {
-        background-color: var(--background-color);
-      }
-    }
-  }
-}
-
 .ld-table-tr-highlight {
   --border-color-light: --primary-color-light;
 

+ 33 - 6
packages/app/src/components/table/index.tsx

@@ -7,13 +7,21 @@ import {
   useTableSearchContext,
 } from '@hooks';
 import {PAGE_SIZE_LIST, TABLE_CELL_WIDTH} from '@utils';
-import {Dispatch, ReactElement, ReactNode, SetStateAction} from 'react';
+import {
+  Dispatch,
+  ReactElement,
+  ReactNode,
+  SetStateAction,
+  useEffect,
+  useState,
+} from 'react';
 import {useTable, useTableShadow} from './hooks';
 import {RowSelectionState} from '@tanstack/react-table';
 import './index.css';
 import {ModifyData} from '@models';
 import TableHead from './Header';
 import TableBody from './Body';
+import {klona} from 'klona/json';
 
 export type LDColumnsType<T extends Record<string, unknown>> = {
   dataIndex: keyof T,
@@ -40,6 +48,8 @@ type Props<T extends Record<string, unknown>> = {
   disabledSizeChange?: boolean;
   highlightValue?: unknown;
   hightlightKey?: keyof T;
+  enableRowDnd?: boolean;
+  rawKey?: keyof T;
 };
 
 function LDTable<T extends Record<string, any>>(props: Props<T>): ReactElement {
@@ -58,18 +68,30 @@ function LDTable<T extends Record<string, any>>(props: Props<T>): ReactElement {
     disabledSizeChange,
     highlightValue,
     hightlightKey,
+    rawKey,
+    enableRowDnd,
   } = props;
+  const [klonaData, setKlonaData] = useState(klona(data));
   const [{page, pageSize}, {setPageContext}] = useTablePageContext(pageContext);
   const [{isSearching}] = useTableSearchContext(searchContext);
 
+  useEffect(function() {
+    enableRowDnd && setKlonaData(klona(data));
+  }, [data, enableRowDnd]);
+
   const [
     {getHeaderGroups, getRowModel, getCenterTotalSize, active},
     {onDragEnd, onDragStart},
-  ] = useTable(columns, data, {
-    settingContext,
-    rowSelection,
-    setRowSelection,
-  });
+  ] = useTable(
+    columns,
+    enableRowDnd ? klonaData : data,
+    {
+      settingContext,
+      rowSelection,
+      setRowSelection,
+      rawKey,
+    },
+  );
 
   const {
     scrollProgess,
@@ -106,6 +128,11 @@ function LDTable<T extends Record<string, any>>(props: Props<T>): ReactElement {
           highlightValue={highlightValue}
           hightlightKey={hightlightKey}
           getHeaderGroups={getHeaderGroups}
+          data={klonaData}
+          rawKey={rawKey}
+          enableDnd={enableRowDnd}
+          setData={setKlonaData}
+          isSecondLevel={isSecondLevel}
         />
       </div>
       <Pagination

+ 3 - 1
pnpm-lock.yaml

@@ -152,6 +152,9 @@ importers:
       fast-deep-equal:
         specifier: ^3.1.3
         version: 3.1.3
+      klona:
+        specifier: ^2.0.6
+        version: 2.0.6
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
@@ -6603,7 +6606,6 @@ packages:
   /klona@2.0.6:
     resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
     engines: {node: '>= 8'}
-    dev: true
 
   /known-css-properties@0.27.0:
     resolution: {integrity: sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg==}