xyh пре 2 година
родитељ
комит
f6556b6f04

+ 4 - 0
packages/app/package.json

@@ -11,6 +11,10 @@
   },
   "dependencies": {
     "@ant-design/icons": "^5.0.1",
+    "@dnd-kit/core": "^6.0.8",
+    "@dnd-kit/modifiers": "^6.0.1",
+    "@dnd-kit/sortable": "^7.0.2",
+    "@dnd-kit/utilities": "^3.2.1",
     "@hookform/resolvers": "^2.9.10",
     "@icon-park/react": "^1.4.2",
     "@tanstack/react-query": "^4.23.0",

+ 4 - 4
packages/app/src/index.tsx

@@ -28,8 +28,8 @@ const themeConfig: ThemeConfig = {
 };
 
 root.render(
-  <QueryClientProvider client={QUERY_CLIENT}>
-    <StrictMode>
+  <StrictMode>
+    <QueryClientProvider client={QUERY_CLIENT}>
       <ConfigProvider theme={themeConfig} locale={zhCN}>
         <Suspense
           fallback={<Loading tip='正在加载' width='100vw' height='100vh' />}
@@ -39,6 +39,6 @@ root.render(
           </BrowserRouter>
         </Suspense>
       </ConfigProvider>
-    </StrictMode>
-  </QueryClientProvider>,
+    </QueryClientProvider>
+  </StrictMode>,
 );

+ 4 - 1
packages/app/src/pages/home/context.ts

@@ -13,7 +13,8 @@ type Action =
   | {type: 'REMOVE'; payload: string}
   | {type: 'REMOVE_OTHER'; payload: string}
   | {type: 'REMOVE_RIGHT'; payload: string}
-  | {type: 'CLEAR'};
+  | {type: 'CLEAR'}
+  | {type: 'SORT'; payload: State};
 
 const defaultTab: State[0] = {key: '-1', url: '/main', label: '首页'};
 
@@ -80,6 +81,8 @@ function reducer(state: State, action: Action): State {
 
       return [{...defaultTab}];
     }
+    case 'SORT':
+      return action.payload;
     default:
       return state;
   }

+ 50 - 0
packages/app/src/pages/home/main/draggable-item/index.tsx

@@ -0,0 +1,50 @@
+import {useSortable} from '@dnd-kit/sortable';
+import {
+  CSSProperties,
+  FC,
+  HTMLAttributes,
+  ReactElement,
+  cloneElement,
+} from 'react';
+import {ShowContextMenuParams} from 'react-contexify';
+
+type MakeOptional<Type, Key extends keyof Type> = Omit<Type, Key> &
+  Partial<Pick<Type, Key>>;
+
+type Props = {
+  id: string;
+  showContext: (
+    params: MakeOptional<ShowContextMenuParams<unknown>, 'id'>,
+  ) => void;
+  children: ReactElement;
+} & HTMLAttributes<HTMLDivElement>;
+
+const DraggableItem: FC<Props> = function ({
+  id,
+  children,
+  showContext,
+  ...props
+}) {
+  const {setNodeRef, attributes, listeners, transition, transform} =
+    useSortable({id});
+
+  const style: CSSProperties = {
+    ...props.style,
+    transition,
+    transform: `translate3d(${transform?.x ?? 0}px, ${transform?.y ?? 0}px, 0)`,
+  };
+
+  return cloneElement(children, {
+    onContextMenu(e: MouseEvent) {
+      id === '-1'
+        ? e.preventDefault()
+        : showContext({event: e, props: {key: id}});
+    },
+    style,
+    ref: setNodeRef,
+    ...attributes,
+    ...listeners,
+  });
+};
+
+export default DraggableItem;

+ 22 - 2
packages/app/src/pages/home/main/hooks.tsx

@@ -1,4 +1,4 @@
-import {useContextSection} from '@hooks';
+import {useContext, useContextSection} from '@hooks';
 import {context} from '../context';
 import {Modal, TabPaneProps, TabsProps} from 'antd';
 import {ReactNode} from 'react';
@@ -6,6 +6,8 @@ import css from './index.module.css';
 import {useStore} from 'zustand';
 import {tabStore} from '@stores';
 import {useContextMenu, ItemParams} from 'react-contexify';
+import {DragEndEvent} from '@dnd-kit/core';
+import {arrayMove} from '@dnd-kit/sortable';
 
 export type Tab = {
   key: string;
@@ -15,10 +17,11 @@ export type Tab = {
 export function useTabItems() {
   const {host} = location;
 
-  return useContextSection(context, function ([tabs]) {
+  const tabs = useContextSection(context, function ([tabs]) {
     return tabs.map<Tab>(function (tab) {
       return {
         ...tab,
+        originalTab: tab,
         forceRender: true,
         closable: tab.key !== '-1',
         animated: true,
@@ -32,6 +35,23 @@ export function useTabItems() {
       };
     });
   });
+  const [originalTab, dispatch] = useContext(context);
+
+  function onDragEnd({active, over}: DragEndEvent) {
+    if (!over) return;
+
+    const {id: fromId} = active,
+      {id: toId} = over;
+    const fromIdx = originalTab.findIndex(val => val.key === fromId),
+      toIdx = originalTab.findIndex(val => val.key === toId);
+
+    dispatch({
+      type: 'SORT',
+      payload: arrayMove(originalTab, fromIdx, toIdx),
+    });
+  }
+
+  return [tabs, onDragEnd] as const;
 }
 
 export function useTabActive() {

+ 45 - 17
packages/app/src/pages/home/main/index.tsx

@@ -3,41 +3,69 @@ import {FC} from 'react';
 import css from './index.module.css';
 import {useMenu, useTabActive, useTabItems} from './hooks';
 import {Menu, Item} from 'react-contexify';
+import {
+  DndContext,
+  useSensor,
+  PointerSensor,
+  closestCenter,
+} from '@dnd-kit/core';
+import {restrictToHorizontalAxis} from '@dnd-kit/modifiers';
+import {
+  SortableContext,
+  horizontalListSortingStrategy,
+} from '@dnd-kit/sortable';
+import DraggableItem from './draggable-item';
 
 const Main: FC = function () {
-  const tabItems = useTabItems();
+  const [tabs, onDragEnd] = useTabItems();
   const [activeKey, {onChange, onEdit}] = useTabActive();
   const {show, onMenuitemClick} = useMenu();
+  const sensor = useSensor(PointerSensor, {
+    activationConstraint: {distance: 10},
+  });
 
   return (
     <>
       <Layout.Content className='layout-content-custom'>
         <Tabs
+          defaultActiveKey='-1'
           data-testid='tab_list'
           size='small'
           hideAdd
           activeKey={activeKey}
           onChange={onChange}
-          items={tabItems}
+          items={tabs}
           className={css.tabs}
           onEdit={onEdit}
           renderTabBar={function (props, DefaultTab) {
             return (
-              <DefaultTab {...props}>
-                {function (node) {
-                  return (
-                    <span
-                      onContextMenu={e =>
-                        node.key === '-1'
-                          ? e.preventDefault()
-                          : show({event: e, props: {key: node.key}})
-                      }
-                    >
-                      {node}
-                    </span>
-                  );
-                }}
-              </DefaultTab>
+              <DndContext
+                modifiers={[restrictToHorizontalAxis]}
+                collisionDetection={closestCenter}
+                onDragEnd={onDragEnd}
+                sensors={[sensor]}
+                autoScroll
+              >
+                <SortableContext
+                  items={tabs.map(val => val.key)}
+                  strategy={horizontalListSortingStrategy}
+                >
+                  <DefaultTab {...props}>
+                    {function (node) {
+                      return (
+                        <DraggableItem
+                          {...node.props}
+                          id={node.key!.toString()}
+                          showContext={show}
+                          key={node.key}
+                        >
+                          {node}
+                        </DraggableItem>
+                      );
+                    }}
+                  </DefaultTab>
+                </SortableContext>
+              </DndContext>
             );
           }}
         />

+ 67 - 0
pnpm-lock.yaml

@@ -95,6 +95,18 @@ importers:
       '@ant-design/icons':
         specifier: ^5.0.1
         version: 5.0.1(react-dom@18.2.0)(react@18.2.0)
+      '@dnd-kit/core':
+        specifier: ^6.0.8
+        version: 6.0.8(react-dom@18.2.0)(react@18.2.0)
+      '@dnd-kit/modifiers':
+        specifier: ^6.0.1
+        version: 6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0)
+      '@dnd-kit/sortable':
+        specifier: ^7.0.2
+        version: 7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0)
+      '@dnd-kit/utilities':
+        specifier: ^3.2.1
+        version: 3.2.1(react@18.2.0)
       '@hookform/resolvers':
         specifier: ^2.9.10
         version: 2.9.10(react-hook-form@7.43.0)
@@ -2142,6 +2154,61 @@ packages:
     engines: {node: '>=10.0.0'}
     dev: true
 
+  /@dnd-kit/accessibility@3.0.1(react@18.2.0):
+    resolution: {integrity: sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+      tslib: 2.4.1
+    dev: false
+
+  /@dnd-kit/core@6.0.8(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-lYaoP8yHTQSLlZe6Rr9qogouGUz9oRUj4AHhDQGQzq/hqaJRpFo65X+JKsdHf8oUFBzx5A+SJPUvxAwTF2OabA==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+    dependencies:
+      '@dnd-kit/accessibility': 3.0.1(react@18.2.0)
+      '@dnd-kit/utilities': 3.2.1(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      tslib: 2.4.1
+    dev: false
+
+  /@dnd-kit/modifiers@6.0.1(@dnd-kit/core@6.0.8)(react@18.2.0):
+    resolution: {integrity: sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==}
+    peerDependencies:
+      '@dnd-kit/core': ^6.0.6
+      react: '>=16.8.0'
+    dependencies:
+      '@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0)
+      '@dnd-kit/utilities': 3.2.1(react@18.2.0)
+      react: 18.2.0
+      tslib: 2.4.1
+    dev: false
+
+  /@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.0.8)(react@18.2.0):
+    resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==}
+    peerDependencies:
+      '@dnd-kit/core': ^6.0.7
+      react: '>=16.8.0'
+    dependencies:
+      '@dnd-kit/core': 6.0.8(react-dom@18.2.0)(react@18.2.0)
+      '@dnd-kit/utilities': 3.2.1(react@18.2.0)
+      react: 18.2.0
+      tslib: 2.4.1
+    dev: false
+
+  /@dnd-kit/utilities@3.2.1(react@18.2.0):
+    resolution: {integrity: sha512-OOXqISfvBw/1REtkSK2N3Fi2EQiLMlWUlqnOK/UpOISqBZPWpE6TqL+jcPtMOkE8TqYGiURvRdPSI9hltNUjEA==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+      tslib: 2.4.1
+    dev: false
+
   /@emotion/hash@0.8.0:
     resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
     dev: false