Browse Source

feat: add tab drag and drop sort

vben 4 years ago
parent
commit
cedba37e4c

+ 2 - 1
CHANGELOG.zh_CN.md

@@ -2,13 +2,14 @@
 
 ### ✨ Refactor
 
-- 重构整体 layout。更改代码实现方式。代码更精简
+- 重构整体 layout。更改代码实现方式。代码更精简,并加入多语言支持
 - 配置项重构
 - 移除 messageSetting 配置
 
 ### ✨ Features
 
 - 缓存可以配置是否加密,默认生产环境开启 Aes 加密
+- 新增标签页拖拽排序
 
 ### 🎫 Chores
 

+ 1 - 0
package.json

@@ -58,6 +58,7 @@
     "@types/nprogress": "^0.2.0",
     "@types/qrcode": "^1.3.5",
     "@types/rollup-plugin-visualizer": "^2.6.0",
+    "@types/sortablejs": "^1.10.6",
     "@types/yargs": "^15.0.10",
     "@types/zxcvbn": "^4.4.0",
     "@typescript-eslint/eslint-plugin": "^4.8.2",

+ 2 - 2
src/hooks/setting/useMenuSetting.ts

@@ -33,7 +33,7 @@ export function useMenuSetting() {
 
   const getMenuBgColor = computed(() => unref(getMenuSetting).bgColor);
 
-  const getHasDrag = computed(() => unref(getMenuSetting).hasDrag);
+  const getCanDrag = computed(() => unref(getMenuSetting).canDrag);
 
   const getAccordion = computed(() => unref(getMenuSetting).accordion);
 
@@ -117,7 +117,7 @@ export function useMenuSetting() {
     getTrigger,
     getSplit,
     getMenuTheme,
-    getHasDrag,
+    getCanDrag,
     getIsHorizontal,
     getShowSearch,
     getCollapsedShowTitle,

+ 4 - 47
src/hooks/web/useTabs.ts

@@ -1,14 +1,6 @@
-import { useTimeoutFn } from '/@/hooks/core/useTimeout';
-import { PageEnum } from '/@/enums/pageEnum';
 import { TabItem, tabStore } from '/@/store/modules/tab';
 import { appStore } from '/@/store/modules/app';
-import router from '/@/router';
-import { ref } from 'vue';
-import { pathToRegexp } from 'path-to-regexp';
 
-const activeKeyRef = ref<string>('');
-
-type Fn = () => void;
 type RouteFn = (tabItem: TabItem) => void;
 
 interface TabFn {
@@ -28,6 +20,7 @@ let closeOther: RouteFn;
 let closeCurrent: RouteFn;
 
 export let isInitUseTab = false;
+
 export function useTabs() {
   function initTabFn({
     refreshPageFn,
@@ -38,6 +31,7 @@ export function useTabs() {
     closeCurrentFn,
   }: TabFn) {
     if (isInitUseTab) return;
+
     refreshPageFn && (refreshPage = refreshPageFn);
     closeAllFn && (closeAll = closeAllFn);
     closeLeftFn && (closeLeft = closeLeftFn);
@@ -58,29 +52,13 @@ export function useTabs() {
   }
 
   function canIUseFn(): boolean {
-    const { getProjectConfig } = appStore;
-    const { multiTabsSetting: { show } = {} } = getProjectConfig;
+    const { multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
     if (!show) {
       throw new Error('当前未开启多标签页,请在设置中打开!');
     }
     return !!show;
   }
-  function getTo(path: string): any {
-    const routes = router.getRoutes();
-    const fn = (p: string): any => {
-      const to = routes.find((item) => {
-        if (item.path === '/:path(.*)*') return;
-        const regexp = pathToRegexp(item.path);
-        return regexp.test(p);
-      });
-      if (!to) return '';
-      if (!to.redirect) return to;
-      if (to.redirect) {
-        return getTo(to.redirect as string);
-      }
-    };
-    return fn(path);
-  }
+
   return {
     initTabFn,
     refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab),
@@ -90,26 +68,5 @@ export function useTabs() {
     closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab),
     closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab),
     resetCache: () => canIUseFn() && resetCache(),
-    addTab: (
-      path: PageEnum | string,
-      goTo = false,
-      opt?: { replace?: boolean; query?: any; params?: any }
-    ) => {
-      const to = getTo(path);
-
-      if (!to) return;
-      useTimeoutFn(() => {
-        tabStore.addTabByPathAction();
-      }, 0);
-      const { replace, query = {}, params = {} } = opt || {};
-      activeKeyRef.value = path;
-      const data = {
-        path,
-        query,
-        params,
-      };
-      goTo && replace ? router.replace(data) : router.push(data);
-    },
-    activeKeyRef,
   };
 }

+ 1 - 0
src/layouts/default/header/index.less

@@ -4,6 +4,7 @@
   display: flex;
   height: @header-height;
   padding: 0 20px 0 0;
+  margin-left: -1px;
   line-height: @header-height;
   color: @white;
   background: @white;

+ 4 - 0
src/layouts/default/index.less

@@ -9,4 +9,8 @@
   > .ant-layout {
     min-height: 100%;
   }
+
+  &__main {
+    margin-left: 2px;
+  }
 }

+ 1 - 1
src/layouts/default/index.tsx

@@ -81,7 +81,7 @@ export default defineComponent({
                 {() => (
                   <>
                     {unref(showSideBarRef) && <LayoutSideBar />}
-                    <Layout>
+                    <Layout class="default-layout__main">
                       {() => (
                         <>
                           <LayoutMultipleHeader />

+ 49 - 62
src/layouts/default/multitabs/TabContent.tsx

@@ -1,16 +1,46 @@
-import { defineComponent, unref, computed } from 'vue';
-
 import type { PropType } from 'vue';
 
+import { defineComponent, unref, computed, FunctionalComponent } from 'vue';
+
 import { TabItem, tabStore } from '/@/store/modules/tab';
-import { getScaleAction, TabContentProps } from './tab.data';
+import { getScaleAction, TabContentProps } from './data';
 
 import { Dropdown } from '/@/components/Dropdown/index';
 import { RightOutlined } from '@ant-design/icons-vue';
-import { appStore } from '/@/store/modules/app';
 
-import { TabContentEnum } from './tab.data';
+import { TabContentEnum } from './data';
 import { useTabDropdown } from './useTabDropdown';
+import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
+import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
+import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
+
+const ExtraContent: FunctionalComponent = () => {
+  return (
+    <span class={`multiple-tabs-content__extra `}>
+      <RightOutlined />
+    </span>
+  );
+};
+
+const TabContent: FunctionalComponent<{ tabItem: TabItem }> = (props) => {
+  const { tabItem: { meta } = {} } = props;
+
+  function handleContextMenu(e: Event) {
+    if (!props.tabItem) return;
+    const tableItem = props.tabItem;
+    e?.preventDefault();
+    const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);
+
+    tabStore.commitCurrentContextMenuIndexState(index);
+    tabStore.commitCurrentContextMenuState(props.tabItem);
+  }
+
+  return (
+    <div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>
+      <span class="ml-1">{meta && meta.title}</span>
+    </div>
+  );
+};
 
 export default defineComponent({
   name: 'TabContent',
@@ -19,82 +49,39 @@ export default defineComponent({
       type: Object as PropType<TabItem>,
       default: null,
     },
+
     type: {
-      type: Number as PropType<number>,
+      type: Number as PropType<TabContentEnum>,
       default: TabContentEnum.TAB_TYPE,
     },
-    trigger: {
-      type: Array as PropType<string[]>,
-      default: () => {
-        return ['contextmenu'];
-      },
-    },
   },
   setup(props) {
-    const getProjectConfigRef = computed(() => {
-      return appStore.getProjectConfig;
-    });
+    const { getShowMenu } = useMenuSetting();
+    const { getShowHeader } = useHeaderSetting();
+    const { getShowQuick } = useMultipleTabSetting();
 
-    const getIsScaleRef = computed(() => {
-      const {
-        menuSetting: { show: showMenu },
-        headerSetting: { show: showHeader },
-      } = unref(getProjectConfigRef);
-      return !showMenu && !showHeader;
+    const getIsScale = computed(() => {
+      return !unref(getShowMenu) && !unref(getShowHeader);
     });
 
-    function handleContextMenu(e: Event) {
-      if (!props.tabItem) return;
-      const tableItem = props.tabItem;
-      e.preventDefault();
-      const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);
-
-      tabStore.commitCurrentContextMenuIndexState(index);
-      tabStore.commitCurrentContextMenuState(props.tabItem);
-    }
-
-    /**
-     * @description: 渲染图标
-     */
-
-    function renderTabContent() {
-      const { tabItem: { meta } = {} } = props;
-      return (
-        <div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>
-          <span class="ml-1">{meta && meta.title}</span>
-        </div>
-      );
-    }
-    function renderExtraContent() {
-      return (
-        <span class={`multiple-tabs-content__extra `}>
-          <RightOutlined />
-        </span>
-      );
-    }
+    const getIsTab = computed(() => {
+      return !unref(getShowQuick) ? true : props.type === TabContentEnum.TAB_TYPE;
+    });
 
     const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps);
 
     return () => {
-      const { trigger, type } = props;
-      const {
-        multiTabsSetting: { showQuick },
-      } = unref(getProjectConfigRef);
-
-      const isTab = !showQuick ? true : type === TabContentEnum.TAB_TYPE;
-      const scaleAction = getScaleAction(
-        unref(getIsScaleRef) ? '缩小' : '放大',
-        unref(getIsScaleRef)
-      );
+      const scaleAction = getScaleAction(unref(getIsScale) ? '收起' : '展开', unref(getIsScale));
       const dropMenuList = unref(getDropMenuList) || [];
 
+      const isTab = unref(getIsTab);
       return (
         <Dropdown
           dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList}
-          trigger={isTab ? trigger : ['hover']}
+          trigger={isTab ? ['contextmenu'] : ['click']}
           onMenuEvent={handleMenuEvent}
         >
-          {() => (isTab ? renderTabContent() : renderExtraContent())}
+          {() => (isTab ? <TabContent tabItem={props.tabItem} /> : <ExtraContent />)}
         </Dropdown>
       );
     };

+ 2 - 0
src/layouts/default/multitabs/tab.data.ts → src/layouts/default/multitabs/data.ts

@@ -6,11 +6,13 @@ export enum TabContentEnum {
   TAB_TYPE,
   EXTRA_TYPE,
 }
+
 export interface TabContentProps {
   tabItem: TabItem | AppRouteRecordRaw;
   type?: TabContentEnum;
   trigger?: Array<'click' | 'hover' | 'contextmenu'>;
 }
+
 /**
  * @description: 右键:下拉菜单文字
  */

+ 36 - 8
src/layouts/default/multitabs/index.less

@@ -2,11 +2,12 @@
 
 .multiple-tabs {
   z-index: 10;
-  height: @multiple-height+2;
+  height: @multiple-height + 2;
   padding: 0 0 2px 0;
-  line-height: @multiple-height+2;
+  margin-left: -1px;
+  line-height: @multiple-height + 2;
   background: @white;
-  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08);
+  box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
 
   .ant-tabs-small {
     height: @multiple-height;
@@ -32,19 +33,25 @@
         color: @text-color-call-out;
         background: @white;
         border: 1px solid darken(@border-color-light, 8%);
-        border-radius: none !important;
         transition: none;
 
+        &:hover {
+          .ant-tabs-close-x {
+            opacity: 1;
+          }
+        }
+
         .ant-tabs-close-x {
-          width: 12px;
+          width: 8px;
           height: 12px;
           font-size: 12px;
           color: inherit;
+          opacity: 0;
           transition: none;
 
           &:hover {
             svg {
-              width: 0.8em;
+              width: 0.75em;
             }
           }
         }
@@ -61,12 +68,26 @@
       }
 
       .ant-tabs-tab-active {
+        position: relative;
+        padding-left: 26px;
         color: @white;
         background: fade(@primary-color, 100%);
         border: 0;
 
         &::before {
-          display: none;
+          position: absolute;
+          top: calc(50% - 3px);
+          left: 8px;
+          width: 6px;
+          height: 6px;
+          background: #fff;
+          border-radius: 50%;
+          content: '';
+          transition: none;
+        }
+
+        .ant-tabs-close-x {
+          opacity: 1;
         }
 
         svg {
@@ -78,6 +99,10 @@
 
     .ant-tabs-nav > div:nth-child(1) {
       padding: 0 10px;
+
+      .ant-tabs-tab {
+        margin-right: 3px !important;
+      }
     }
   }
 
@@ -111,7 +136,10 @@
     text-align: center;
     cursor: pointer;
     border-left: 1px solid #eee;
-    // box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+
+    &:hover {
+      color: @text-color-base;
+    }
 
     span[role='img'] {
       transform: rotate(90deg);

+ 65 - 27
src/layouts/default/multitabs/index.tsx

@@ -1,10 +1,12 @@
 import './index.less';
 
-import type { TabContentProps } from './tab.data';
+import type { TabContentProps } from './data';
 import type { TabItem } from '/@/store/modules/tab';
 import type { AppRouteRecordRaw } from '/@/router/types';
 
-import { defineComponent, watch, computed, unref } from 'vue';
+import { defineComponent, watch, computed, unref, ref, onMounted, nextTick } from 'vue';
+import Sortable from 'sortablejs';
+
 import { useRouter } from 'vue-router';
 
 import { Tabs } from 'ant-design-vue';
@@ -12,24 +14,28 @@ import TabContent from './TabContent';
 
 import { useGo } from '/@/hooks/web/usePage';
 
-import { TabContentEnum } from './tab.data';
+import { TabContentEnum } from './data';
 
 import { tabStore } from '/@/store/modules/tab';
 import { userStore } from '/@/store/modules/user';
 
 import { closeTab } from './useTabDropdown';
-import { useTabs } from '/@/hooks/web/useTabs';
-import { initAffixTabs } from './useAffixTabs';
+import { initAffixTabs } from './useMultipleTabs';
+import { isNullAndUnDef } from '/@/utils/is';
+import { useProjectSetting } from '/@/hooks/setting';
 
 export default defineComponent({
   name: 'MultipleTabs',
   setup() {
-    initAffixTabs();
+    const activeKeyRef = ref('');
+
+    const affixTextList = initAffixTabs();
 
     const go = useGo();
 
+    const { multiTabsSetting } = useProjectSetting();
+
     const { currentRoute } = useRouter();
-    const { activeKeyRef } = useTabs();
 
     const getTabsState = computed(() => tabStore.getTabsState);
 
@@ -41,24 +47,24 @@ export default defineComponent({
 
         if (!lastChangeRoute || !userStore.getTokenState) return;
 
-        const { path, fullPath } = lastChangeRoute;
-        if (activeKeyRef.value !== (fullPath || path)) {
-          activeKeyRef.value = fullPath || path;
+        const { path, fullPath } = lastChangeRoute as AppRouteRecordRaw;
+        const p = fullPath || path;
+        if (activeKeyRef.value !== p) {
+          activeKeyRef.value = p;
         }
-        tabStore.commitAddTab((lastChangeRoute as unknown) as AppRouteRecordRaw);
+        tabStore.commitAddTab(lastChangeRoute);
       },
       {
         immediate: true,
       }
     );
 
-    // tab切换
     function handleChange(activeKey: any) {
       activeKeyRef.value = activeKey;
       go(activeKey, false);
     }
 
-    // 关闭当前tab
+    // Close the current tab
     function handleEdit(targetKey: string) {
       // Added operation to hide, currently only use delete operation
       const index = unref(getTabsState).findIndex(
@@ -71,30 +77,65 @@ export default defineComponent({
       const tabContentProps: TabContentProps = {
         tabItem: (currentRoute as unknown) as AppRouteRecordRaw,
         type: TabContentEnum.EXTRA_TYPE,
-        trigger: ['click', 'contextmenu'],
       };
-      return (
-        <span>
-          <TabContent {...(tabContentProps as any)} />
-        </span>
-      );
+      return <TabContent {...(tabContentProps as any)} />;
     }
 
     function renderTabs() {
       return unref(getTabsState).map((item: TabItem) => {
         const key = item.query ? item.fullPath : item.path;
         const closable = !(item && item.meta && item.meta.affix);
+
+        const slots = {
+          tab: () => <TabContent tabItem={item} />,
+        };
         return (
           <Tabs.TabPane key={key} closable={closable}>
-            {{
-              tab: () => <TabContent tabItem={item} />,
-            }}
+            {slots}
           </Tabs.TabPane>
         );
       });
     }
 
+    function initSortableTabs() {
+      if (!multiTabsSetting.canDrag) return;
+      nextTick(() => {
+        const el = document.querySelectorAll(
+          '.multiple-tabs .ant-tabs-nav > div'
+        )?.[0] as HTMLElement;
+
+        if (!el) return;
+        Sortable.create(el, {
+          animation: 500,
+          delay: 400,
+          delayOnTouchOnly: true,
+          filter: (e: ChangeEvent) => {
+            const text = e?.target?.innerText;
+            if (!text) return false;
+            return affixTextList.includes(text);
+          },
+          onEnd: (evt) => {
+            const { oldIndex, newIndex } = evt;
+
+            if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
+              return;
+            }
+
+            tabStore.commitSortTabs({ oldIndex, newIndex });
+          },
+        });
+      });
+    }
+
+    onMounted(() => {
+      initSortableTabs();
+    });
+
     return () => {
+      const slots = {
+        default: () => renderTabs(),
+        tabBarExtraContent: () => renderQuick(),
+      };
       return (
         <div class="multiple-tabs">
           <Tabs
@@ -102,15 +143,12 @@ export default defineComponent({
             size="small"
             animated={false}
             hideAdd={true}
-            tabBarGutter={4}
+            tabBarGutter={3}
             activeKey={unref(activeKeyRef)}
             onChange={handleChange}
             onEdit={handleEdit}
           >
-            {{
-              default: () => renderTabs(),
-              tabBarExtraContent: () => renderQuick(),
-            }}
+            {slots}
           </Tabs>
         </div>
       );

+ 5 - 1
src/layouts/default/multitabs/useAffixTabs.ts → src/layouts/default/multitabs/useMultipleTabs.ts

@@ -1,9 +1,10 @@
-import { toRaw } from 'vue';
+import { toRaw, ref } from 'vue';
 import router from '/@/router';
 import { AppRouteRecordRaw } from '/@/router/types';
 import { TabItem, tabStore } from '/@/store/modules/tab';
 
 export function initAffixTabs() {
+  const affixList = ref<TabItem[]>([]);
   /**
    * @description: Filter all fixed routes
    */
@@ -23,13 +24,16 @@ export function initAffixTabs() {
    */
   function addAffixTabs(): void {
     const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]);
+    affixList.value = affixTabs;
     for (const tab of affixTabs) {
       tabStore.commitAddTab(tab);
     }
   }
+
   let isAddAffix = false;
   if (!isAddAffix) {
     addAffixTabs();
     isAddAffix = true;
   }
+  return affixList.value.map((item) => item.meta?.title).filter(Boolean);
 }

+ 53 - 55
src/layouts/default/multitabs/useTabDropdown.ts

@@ -1,11 +1,11 @@
 import type { AppRouteRecordRaw } from '/@/router/types';
-import type { TabContentProps } from './tab.data';
+import type { TabContentProps } from './data';
 import type { Ref } from 'vue';
 import type { TabItem } from '/@/store/modules/tab';
 import type { DropMenu } from '/@/components/Dropdown';
 
 import { computed, unref } from 'vue';
-import { TabContentEnum, MenuEventEnum, getActions } from './tab.data';
+import { TabContentEnum, MenuEventEnum, getActions } from './data';
 import { tabStore } from '/@/store/modules/tab';
 import { appStore } from '/@/store/modules/app';
 import { PageEnum } from '/@/enums/pageEnum';
@@ -15,9 +15,7 @@ import { useTabs, isInitUseTab } from '/@/hooks/web/useTabs';
 import { RouteLocationRaw } from 'vue-router';
 
 const { initTabFn } = useTabs();
-/**
- * @description: 右键下拉
- */
+
 export function useTabDropdown(tabContentProps: TabContentProps) {
   const { currentRoute } = router;
   const redo = useRedo();
@@ -30,26 +28,24 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
       : ((unref(currentRoute) as any) as AppRouteRecordRaw);
   });
 
-  // 当前tab列表
-  const getTabsState = computed(() => {
-    return tabStore.getTabsState;
-  });
+  // Current tab list
+  const getTabsState = computed(() => tabStore.getTabsState);
 
   /**
-   * @description: 下拉列表
+   * @description: drop-down list
    */
   const getDropMenuList = computed(() => {
     const dropMenuList = getActions();
-    // 重置为初始状态
+    // Reset to initial state
     for (const item of dropMenuList) {
       item.disabled = false;
     }
 
-    // 没有tab
+    // No tab
     if (!unref(getTabsState) || unref(getTabsState).length <= 0) {
       return dropMenuList;
     } else if (unref(getTabsState).length === 1) {
-      // 只有一个tab
+      // Only one tab
       for (const item of dropMenuList) {
         if (item.event !== MenuEventEnum.REFRESH_PAGE) {
           item.disabled = true;
@@ -57,22 +53,20 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
       }
       return dropMenuList;
     }
-    if (!unref(getCurrentTab)) {
-      return;
-    }
+    if (!unref(getCurrentTab)) return;
     const { meta, path } = unref(getCurrentTab);
-    // console.log(unref(getCurrentTab));
 
-    // 刷新按钮
+    // Refresh button
     const curItem = tabStore.getCurrentContextMenuState;
     const index = tabStore.getCurrentContextMenuIndexState;
     const refreshDisabled = curItem ? curItem.path !== path : true;
-    // 关闭左侧
+    // Close left
     const closeLeftDisabled = index === 0;
 
-    // 关闭右侧
+    // Close right
     const closeRightDisabled = index === unref(getTabsState).length - 1;
-    // 当前为固定tab
+    // Currently fixed tab
+    // TODO PERf
     dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false;
     if (meta && meta.affix) {
       dropMenuList[1].disabled = true;
@@ -84,7 +78,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
   });
 
   /**
-   * @description: 关闭所有页面时,跳转页面
+   * @description: Jump to page when closing all pages
    */
   function gotoPage() {
     const len = unref(getTabsState).length;
@@ -99,14 +93,14 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
         toPath = p;
       }
     }
-    // 跳到当前页面报错
+    // Jump to the current page and report an error
     path !== toPath && go(toPath as PageEnum, true);
   }
 
   function isGotoPage(currentTab?: TabItem) {
     const { path } = unref(currentRoute);
     const currentPath = (currentTab || unref(getCurrentTab)).path;
-    // 不是当前tab,关闭左侧/右侧时,需跳转页面
+    // Not the current tab, when you close the left/right side, you need to jump to the page
     if (path !== currentPath) {
       go(currentPath as PageEnum, true);
     }
@@ -117,25 +111,31 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
     } catch (error) {}
     redo();
   }
+
   function closeAll() {
     tabStore.commitCloseAllTab();
     gotoPage();
   }
+
   function closeLeft(tabItem?: TabItem) {
     tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab));
     isGotoPage(tabItem);
   }
+
   function closeRight(tabItem?: TabItem) {
     tabStore.closeRightTabAction(tabItem || unref(getCurrentTab));
     isGotoPage(tabItem);
   }
+
   function closeOther(tabItem?: TabItem) {
     tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab));
     isGotoPage(tabItem);
   }
+
   function closeCurrent(tabItem?: TabItem) {
     closeTab(unref(tabItem || unref(getCurrentTab)));
   }
+
   function scaleScreen() {
     const {
       headerSetting: { show: showHeader },
@@ -159,7 +159,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
     });
   }
 
-  // 处理右键事件
+  // Handle right click event
   function handleMenuEvent(menu: DropMenu): void {
     const { event } = menu;
 
@@ -168,76 +168,74 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
         scaleScreen();
         break;
       case MenuEventEnum.REFRESH_PAGE:
-        // 刷新页面
+        // refresh page
         refreshPage();
         break;
-      // 关闭当前
+      // Close current
       case MenuEventEnum.CLOSE_CURRENT:
         closeCurrent();
         break;
-      // 关闭左侧
+      // Close left
       case MenuEventEnum.CLOSE_LEFT:
         closeLeft();
         break;
-      // 关闭右侧
+      // Close right
       case MenuEventEnum.CLOSE_RIGHT:
         closeRight();
         break;
-      // 关闭其他
+      // Close other
       case MenuEventEnum.CLOSE_OTHER:
         closeOther();
         break;
-      // 关闭其他
+      // Close all
       case MenuEventEnum.CLOSE_ALL:
         closeAll();
         break;
-      default:
-        break;
     }
   }
   return { getDropMenuList, handleMenuEvent };
 }
+
+export function getObj(tabItem: TabItem) {
+  const { params, path, query } = tabItem;
+  return {
+    params: params || {},
+    path,
+    query: query || {},
+  };
+}
+
 export function closeTab(closedTab: TabItem | AppRouteRecordRaw) {
   const { currentRoute, replace } = router;
-  // 当前tab列表
-  const getTabsState = computed(() => {
-    return tabStore.getTabsState;
-  });
+  // Current tab list
+  const getTabsState = computed(() => tabStore.getTabsState);
 
   const { path } = unref(currentRoute);
   if (path !== closedTab.path) {
-    // 关闭的不是激活tab
+    // Closed is not the activation tab
     tabStore.commitCloseTab(closedTab);
     return;
   }
-  // 关闭的为激活atb
+
+  // Closed is activated atb
   let toObj: RouteLocationRaw = {};
+
   const index = unref(getTabsState).findIndex((item) => item.path === path);
 
-  // 如果当前为最左边tab
+  // If the current is the leftmost tab
   if (index === 0) {
-    // 只有一个tab,则跳转至首页,否则跳转至右tab
+    // There is only one tab, then jump to the homepage, otherwise jump to the right tab
     if (unref(getTabsState).length === 1) {
       toObj = PageEnum.BASE_HOME;
     } else {
-      //  跳转至右边tab
+      //  Jump to the right tab
       const page = unref(getTabsState)[index + 1];
-      const { params, path, query } = page;
-      toObj = {
-        params,
-        path,
-        query,
-      };
+      toObj = getObj(page);
     }
   } else {
-    // 跳转至左边tab
+    // Close the current tab
     const page = unref(getTabsState)[index - 1];
-    const { params, path, query } = page;
-    toObj = {
-      params: params || {},
-      path,
-      query: query || {},
-    };
+    toObj = getObj(page);
   }
   const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw;
   tabStore.commitCloseTab(route);

+ 2 - 2
src/layouts/default/setting/SettingDrawer.tsx

@@ -203,7 +203,7 @@ export default defineComponent({
       getMenuFixed,
       getCollapsed,
       getShowSearch,
-      getHasDrag,
+      getCanDrag,
       getTopMenuAlign,
       getAccordion,
       getMenuWidth,
@@ -267,7 +267,7 @@ export default defineComponent({
           handler: (e) => {
             baseHandler(HandlerEnum.MENU_HAS_DRAG, e);
           },
-          def: unref(getHasDrag),
+          def: unref(getCanDrag),
           disabled: !unref(getShowMenuRef),
         }),
         renderSwitchItem('侧边菜单搜索', {

+ 1 - 1
src/layouts/default/setting/handler.ts

@@ -30,7 +30,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
       };
 
     case HandlerEnum.MENU_HAS_DRAG:
-      return { menuSetting: { hasDrag: value } };
+      return { menuSetting: { canDrag: value } };
 
     case HandlerEnum.MENU_ACCORDION:
       return { menuSetting: { accordion: value } };

+ 2 - 2
src/layouts/default/sider/index.less

@@ -1,7 +1,7 @@
 @import (reference) '../../../design/index.less';
 
 .layout-sidebar {
-  overflow: hidden;
+  // overflow: hidden;
 
   &.fixed {
     position: fixed;
@@ -15,7 +15,7 @@
   }
 
   &:not(.ant-layout-sider-dark) {
-    border-right: 1px solid @border-color-light;
+    // border-right: 1px solid @border-color-light;
     box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
   }
 

+ 2 - 2
src/layouts/default/sider/useLayoutSider.tsx

@@ -82,7 +82,7 @@ export function useTrigger() {
  * @param dragBarRef
  */
 export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
-  const { getMiniWidthNumber, getCollapsed, setMenuSetting, getHasDrag } = useMenuSetting();
+  const { getMiniWidthNumber, getCollapsed, setMenuSetting, getCanDrag } = useMenuSetting();
 
   const getDragBarStyle = computed(() => {
     if (unref(getCollapsed)) {
@@ -101,7 +101,7 @@ export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
   function renderDragLine() {
     return (
       <div
-        class={[`layout-sidebar__darg-bar`, !unref(getHasDrag) ? 'hide' : '']}
+        class={[`layout-sidebar__darg-bar`, { hide: !unref(getCanDrag) }]}
         style={unref(getDragBarStyle)}
         ref={dragBarRef}
       />

+ 3 - 1
src/settings/projectSetting.ts

@@ -83,7 +83,7 @@ const setting: ProjectConfig = {
     collapsedShowTitle: false,
     // Whether it can be dragged
     // Only limited to the opening of the left menu, the mouse has a drag bar on the right side of the menu
-    hasDrag: false,
+    canDrag: false,
     // Whether to show no dom
     show: true,
     // Whether to show dom
@@ -114,6 +114,8 @@ const setting: ProjectConfig = {
   multiTabsSetting: {
     // Turn on
     show: true,
+    // Is it possible to drag and drop sorting tabs
+    canDrag: true,
     // Turn on quick actions
     showQuick: true,
     // Maximum number of tab cache

+ 8 - 0
src/store/modules/tab.ts

@@ -176,6 +176,14 @@ class Tab extends VuexModule {
   }
 
   @Mutation
+  commitSortTabs({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void {
+    const currentTab = this.tabsState[oldIndex];
+
+    this.tabsState.splice(oldIndex, 1);
+    this.tabsState.splice(newIndex, 0, currentTab);
+  }
+
+  @Mutation
   closeMultipleTab({ pathList, nameList }: { pathList: string[]; nameList: string[] }): void {
     this.tabsState = toRaw(this.tabsState).filter((item) => !pathList.includes(item.fullPath));
     if (unref(getOpenKeepAliveRef) && nameList) {

+ 2 - 2
src/types/config.d.ts

@@ -8,7 +8,7 @@ export interface MenuSetting {
   fixed: boolean;
   collapsed: boolean;
   collapsedShowTitle: boolean;
-  hasDrag: boolean;
+  canDrag: boolean;
   showSearch: boolean;
   show: boolean;
   hidden: boolean;
@@ -28,7 +28,7 @@ export interface MultiTabsSetting {
   show: boolean;
   // 开启快速操作
   showQuick: boolean;
-
+  canDrag: boolean;
   // 缓存最大数量
   max: number;
 }

+ 4 - 0
src/utils/is.ts

@@ -24,6 +24,10 @@ export function isNull(val: unknown): val is null {
   return val === null;
 }
 
+export function isNullAndUnDef(val: unknown): val is null | undefined {
+  return isUnDef(val) && isNull(val);
+}
+
 export function isNumber(val: unknown): val is number {
   return is(val, 'Number');
 }

+ 1 - 14
src/views/demo/feat/tabs/index.vue

@@ -11,28 +11,18 @@
       <a-button class="mr-2" @click="closeOther">关闭其他</a-button>
       <a-button class="mr-2" @click="closeCurrent">关闭当前</a-button>
       <a-button class="mr-2" @click="refreshPage">刷新当前</a-button>
-      <a-button class="mr-2" @click="openTab">打开图标界面tab</a-button>
     </CollapseContainer>
   </div>
 </template>
 <script lang="ts">
   import { defineComponent } from 'vue';
   import { CollapseContainer } from '/@/components/Container/index';
-  import { PageEnum } from '/@/enums/pageEnum';
   import { useTabs } from '/@/hooks/web/useTabs';
   export default defineComponent({
     name: 'TabsDemo',
     components: { CollapseContainer },
     setup() {
-      const {
-        closeAll,
-        closeLeft,
-        closeRight,
-        closeOther,
-        closeCurrent,
-        refreshPage,
-        addTab,
-      } = useTabs();
+      const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTabs();
 
       return {
         closeAll,
@@ -41,9 +31,6 @@
         closeOther,
         closeCurrent,
         refreshPage,
-        openTab: () => {
-          addTab('/feat/icon' as PageEnum, true);
-        },
       };
     },
   });

+ 5 - 0
yarn.lock

@@ -1495,6 +1495,11 @@
     "@types/mime" "*"
     "@types/node" "*"
 
+"@types/sortablejs@^1.10.6":
+  version "1.10.6"
+  resolved "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.10.6.tgz#98725ae08f1dfe28b8da0fdf302c417f5ff043c0"
+  integrity sha512-QRz8Z+uw2Y4Gwrtxw8hD782zzuxxugdcq8X/FkPsXUa1kfslhGzy13+4HugO9FXNo+jlWVcE6DYmmegniIQ30A==
+
 "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
   version "2.0.3"
   resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"