Commit cedba37e4cf63456c97f7e391761f176137e0165

Authored by vben
1 parent 5cabbac7

feat: add tab drag and drop sort

CHANGELOG.zh_CN.md
@@ -2,13 +2,14 @@ @@ -2,13 +2,14 @@
2 2
3 ### ✨ Refactor 3 ### ✨ Refactor
4 4
5 -- 重构整体 layout。更改代码实现方式。代码更精简 5 +- 重构整体 layout。更改代码实现方式。代码更精简,并加入多语言支持
6 - 配置项重构 6 - 配置项重构
7 - 移除 messageSetting 配置 7 - 移除 messageSetting 配置
8 8
9 ### ✨ Features 9 ### ✨ Features
10 10
11 - 缓存可以配置是否加密,默认生产环境开启 Aes 加密 11 - 缓存可以配置是否加密,默认生产环境开启 Aes 加密
  12 +- 新增标签页拖拽排序
12 13
13 ### 🎫 Chores 14 ### 🎫 Chores
14 15
package.json
@@ -58,6 +58,7 @@ @@ -58,6 +58,7 @@
58 "@types/nprogress": "^0.2.0", 58 "@types/nprogress": "^0.2.0",
59 "@types/qrcode": "^1.3.5", 59 "@types/qrcode": "^1.3.5",
60 "@types/rollup-plugin-visualizer": "^2.6.0", 60 "@types/rollup-plugin-visualizer": "^2.6.0",
  61 + "@types/sortablejs": "^1.10.6",
61 "@types/yargs": "^15.0.10", 62 "@types/yargs": "^15.0.10",
62 "@types/zxcvbn": "^4.4.0", 63 "@types/zxcvbn": "^4.4.0",
63 "@typescript-eslint/eslint-plugin": "^4.8.2", 64 "@typescript-eslint/eslint-plugin": "^4.8.2",
src/hooks/setting/useMenuSetting.ts
@@ -33,7 +33,7 @@ export function useMenuSetting() { @@ -33,7 +33,7 @@ export function useMenuSetting() {
33 33
34 const getMenuBgColor = computed(() => unref(getMenuSetting).bgColor); 34 const getMenuBgColor = computed(() => unref(getMenuSetting).bgColor);
35 35
36 - const getHasDrag = computed(() => unref(getMenuSetting).hasDrag); 36 + const getCanDrag = computed(() => unref(getMenuSetting).canDrag);
37 37
38 const getAccordion = computed(() => unref(getMenuSetting).accordion); 38 const getAccordion = computed(() => unref(getMenuSetting).accordion);
39 39
@@ -117,7 +117,7 @@ export function useMenuSetting() { @@ -117,7 +117,7 @@ export function useMenuSetting() {
117 getTrigger, 117 getTrigger,
118 getSplit, 118 getSplit,
119 getMenuTheme, 119 getMenuTheme,
120 - getHasDrag, 120 + getCanDrag,
121 getIsHorizontal, 121 getIsHorizontal,
122 getShowSearch, 122 getShowSearch,
123 getCollapsedShowTitle, 123 getCollapsedShowTitle,
src/hooks/web/useTabs.ts
1 -import { useTimeoutFn } from '/@/hooks/core/useTimeout';  
2 -import { PageEnum } from '/@/enums/pageEnum';  
3 import { TabItem, tabStore } from '/@/store/modules/tab'; 1 import { TabItem, tabStore } from '/@/store/modules/tab';
4 import { appStore } from '/@/store/modules/app'; 2 import { appStore } from '/@/store/modules/app';
5 -import router from '/@/router';  
6 -import { ref } from 'vue';  
7 -import { pathToRegexp } from 'path-to-regexp';  
8 3
9 -const activeKeyRef = ref<string>('');  
10 -  
11 -type Fn = () => void;  
12 type RouteFn = (tabItem: TabItem) => void; 4 type RouteFn = (tabItem: TabItem) => void;
13 5
14 interface TabFn { 6 interface TabFn {
@@ -28,6 +20,7 @@ let closeOther: RouteFn; @@ -28,6 +20,7 @@ let closeOther: RouteFn;
28 let closeCurrent: RouteFn; 20 let closeCurrent: RouteFn;
29 21
30 export let isInitUseTab = false; 22 export let isInitUseTab = false;
  23 +
31 export function useTabs() { 24 export function useTabs() {
32 function initTabFn({ 25 function initTabFn({
33 refreshPageFn, 26 refreshPageFn,
@@ -38,6 +31,7 @@ export function useTabs() { @@ -38,6 +31,7 @@ export function useTabs() {
38 closeCurrentFn, 31 closeCurrentFn,
39 }: TabFn) { 32 }: TabFn) {
40 if (isInitUseTab) return; 33 if (isInitUseTab) return;
  34 +
41 refreshPageFn && (refreshPage = refreshPageFn); 35 refreshPageFn && (refreshPage = refreshPageFn);
42 closeAllFn && (closeAll = closeAllFn); 36 closeAllFn && (closeAll = closeAllFn);
43 closeLeftFn && (closeLeft = closeLeftFn); 37 closeLeftFn && (closeLeft = closeLeftFn);
@@ -58,29 +52,13 @@ export function useTabs() { @@ -58,29 +52,13 @@ export function useTabs() {
58 } 52 }
59 53
60 function canIUseFn(): boolean { 54 function canIUseFn(): boolean {
61 - const { getProjectConfig } = appStore;  
62 - const { multiTabsSetting: { show } = {} } = getProjectConfig; 55 + const { multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
63 if (!show) { 56 if (!show) {
64 throw new Error('当前未开启多标签页,请在设置中打开!'); 57 throw new Error('当前未开启多标签页,请在设置中打开!');
65 } 58 }
66 return !!show; 59 return !!show;
67 } 60 }
68 - function getTo(path: string): any {  
69 - const routes = router.getRoutes();  
70 - const fn = (p: string): any => {  
71 - const to = routes.find((item) => {  
72 - if (item.path === '/:path(.*)*') return;  
73 - const regexp = pathToRegexp(item.path);  
74 - return regexp.test(p);  
75 - });  
76 - if (!to) return '';  
77 - if (!to.redirect) return to;  
78 - if (to.redirect) {  
79 - return getTo(to.redirect as string);  
80 - }  
81 - };  
82 - return fn(path);  
83 - } 61 +
84 return { 62 return {
85 initTabFn, 63 initTabFn,
86 refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab), 64 refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab),
@@ -90,26 +68,5 @@ export function useTabs() { @@ -90,26 +68,5 @@ export function useTabs() {
90 closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab), 68 closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab),
91 closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab), 69 closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab),
92 resetCache: () => canIUseFn() && resetCache(), 70 resetCache: () => canIUseFn() && resetCache(),
93 - addTab: (  
94 - path: PageEnum | string,  
95 - goTo = false,  
96 - opt?: { replace?: boolean; query?: any; params?: any }  
97 - ) => {  
98 - const to = getTo(path);  
99 -  
100 - if (!to) return;  
101 - useTimeoutFn(() => {  
102 - tabStore.addTabByPathAction();  
103 - }, 0);  
104 - const { replace, query = {}, params = {} } = opt || {};  
105 - activeKeyRef.value = path;  
106 - const data = {  
107 - path,  
108 - query,  
109 - params,  
110 - };  
111 - goTo && replace ? router.replace(data) : router.push(data);  
112 - },  
113 - activeKeyRef,  
114 }; 71 };
115 } 72 }
src/layouts/default/header/index.less
@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 display: flex; 4 display: flex;
5 height: @header-height; 5 height: @header-height;
6 padding: 0 20px 0 0; 6 padding: 0 20px 0 0;
  7 + margin-left: -1px;
7 line-height: @header-height; 8 line-height: @header-height;
8 color: @white; 9 color: @white;
9 background: @white; 10 background: @white;
src/layouts/default/index.less
@@ -9,4 +9,8 @@ @@ -9,4 +9,8 @@
9 > .ant-layout { 9 > .ant-layout {
10 min-height: 100%; 10 min-height: 100%;
11 } 11 }
  12 +
  13 + &__main {
  14 + margin-left: 2px;
  15 + }
12 } 16 }
src/layouts/default/index.tsx
@@ -81,7 +81,7 @@ export default defineComponent({ @@ -81,7 +81,7 @@ export default defineComponent({
81 {() => ( 81 {() => (
82 <> 82 <>
83 {unref(showSideBarRef) && <LayoutSideBar />} 83 {unref(showSideBarRef) && <LayoutSideBar />}
84 - <Layout> 84 + <Layout class="default-layout__main">
85 {() => ( 85 {() => (
86 <> 86 <>
87 <LayoutMultipleHeader /> 87 <LayoutMultipleHeader />
src/layouts/default/multitabs/TabContent.tsx
1 -import { defineComponent, unref, computed } from 'vue';  
2 -  
3 import type { PropType } from 'vue'; 1 import type { PropType } from 'vue';
4 2
  3 +import { defineComponent, unref, computed, FunctionalComponent } from 'vue';
  4 +
5 import { TabItem, tabStore } from '/@/store/modules/tab'; 5 import { TabItem, tabStore } from '/@/store/modules/tab';
6 -import { getScaleAction, TabContentProps } from './tab.data'; 6 +import { getScaleAction, TabContentProps } from './data';
7 7
8 import { Dropdown } from '/@/components/Dropdown/index'; 8 import { Dropdown } from '/@/components/Dropdown/index';
9 import { RightOutlined } from '@ant-design/icons-vue'; 9 import { RightOutlined } from '@ant-design/icons-vue';
10 -import { appStore } from '/@/store/modules/app';  
11 10
12 -import { TabContentEnum } from './tab.data'; 11 +import { TabContentEnum } from './data';
13 import { useTabDropdown } from './useTabDropdown'; 12 import { useTabDropdown } from './useTabDropdown';
  13 +import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  14 +import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
  15 +import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
  16 +
  17 +const ExtraContent: FunctionalComponent = () => {
  18 + return (
  19 + <span class={`multiple-tabs-content__extra `}>
  20 + <RightOutlined />
  21 + </span>
  22 + );
  23 +};
  24 +
  25 +const TabContent: FunctionalComponent<{ tabItem: TabItem }> = (props) => {
  26 + const { tabItem: { meta } = {} } = props;
  27 +
  28 + function handleContextMenu(e: Event) {
  29 + if (!props.tabItem) return;
  30 + const tableItem = props.tabItem;
  31 + e?.preventDefault();
  32 + const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);
  33 +
  34 + tabStore.commitCurrentContextMenuIndexState(index);
  35 + tabStore.commitCurrentContextMenuState(props.tabItem);
  36 + }
  37 +
  38 + return (
  39 + <div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>
  40 + <span class="ml-1">{meta && meta.title}</span>
  41 + </div>
  42 + );
  43 +};
14 44
15 export default defineComponent({ 45 export default defineComponent({
16 name: 'TabContent', 46 name: 'TabContent',
@@ -19,82 +49,39 @@ export default defineComponent({ @@ -19,82 +49,39 @@ export default defineComponent({
19 type: Object as PropType<TabItem>, 49 type: Object as PropType<TabItem>,
20 default: null, 50 default: null,
21 }, 51 },
  52 +
22 type: { 53 type: {
23 - type: Number as PropType<number>, 54 + type: Number as PropType<TabContentEnum>,
24 default: TabContentEnum.TAB_TYPE, 55 default: TabContentEnum.TAB_TYPE,
25 }, 56 },
26 - trigger: {  
27 - type: Array as PropType<string[]>,  
28 - default: () => {  
29 - return ['contextmenu'];  
30 - },  
31 - },  
32 }, 57 },
33 setup(props) { 58 setup(props) {
34 - const getProjectConfigRef = computed(() => {  
35 - return appStore.getProjectConfig;  
36 - }); 59 + const { getShowMenu } = useMenuSetting();
  60 + const { getShowHeader } = useHeaderSetting();
  61 + const { getShowQuick } = useMultipleTabSetting();
37 62
38 - const getIsScaleRef = computed(() => {  
39 - const {  
40 - menuSetting: { show: showMenu },  
41 - headerSetting: { show: showHeader },  
42 - } = unref(getProjectConfigRef);  
43 - return !showMenu && !showHeader; 63 + const getIsScale = computed(() => {
  64 + return !unref(getShowMenu) && !unref(getShowHeader);
44 }); 65 });
45 66
46 - function handleContextMenu(e: Event) {  
47 - if (!props.tabItem) return;  
48 - const tableItem = props.tabItem;  
49 - e.preventDefault();  
50 - const index = unref(tabStore.getTabsState).findIndex((tab) => tab.path === tableItem.path);  
51 -  
52 - tabStore.commitCurrentContextMenuIndexState(index);  
53 - tabStore.commitCurrentContextMenuState(props.tabItem);  
54 - }  
55 -  
56 - /**  
57 - * @description: 渲染图标  
58 - */  
59 -  
60 - function renderTabContent() {  
61 - const { tabItem: { meta } = {} } = props;  
62 - return (  
63 - <div class={`multiple-tabs-content__content `} onContextmenu={handleContextMenu}>  
64 - <span class="ml-1">{meta && meta.title}</span>  
65 - </div>  
66 - );  
67 - }  
68 - function renderExtraContent() {  
69 - return (  
70 - <span class={`multiple-tabs-content__extra `}>  
71 - <RightOutlined />  
72 - </span>  
73 - );  
74 - } 67 + const getIsTab = computed(() => {
  68 + return !unref(getShowQuick) ? true : props.type === TabContentEnum.TAB_TYPE;
  69 + });
75 70
76 const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps); 71 const { getDropMenuList, handleMenuEvent } = useTabDropdown(props as TabContentProps);
77 72
78 return () => { 73 return () => {
79 - const { trigger, type } = props;  
80 - const {  
81 - multiTabsSetting: { showQuick },  
82 - } = unref(getProjectConfigRef);  
83 -  
84 - const isTab = !showQuick ? true : type === TabContentEnum.TAB_TYPE;  
85 - const scaleAction = getScaleAction(  
86 - unref(getIsScaleRef) ? '缩小' : '放大',  
87 - unref(getIsScaleRef)  
88 - ); 74 + const scaleAction = getScaleAction(unref(getIsScale) ? '收起' : '展开', unref(getIsScale));
89 const dropMenuList = unref(getDropMenuList) || []; 75 const dropMenuList = unref(getDropMenuList) || [];
90 76
  77 + const isTab = unref(getIsTab);
91 return ( 78 return (
92 <Dropdown 79 <Dropdown
93 dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList} 80 dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList}
94 - trigger={isTab ? trigger : ['hover']} 81 + trigger={isTab ? ['contextmenu'] : ['click']}
95 onMenuEvent={handleMenuEvent} 82 onMenuEvent={handleMenuEvent}
96 > 83 >
97 - {() => (isTab ? renderTabContent() : renderExtraContent())} 84 + {() => (isTab ? <TabContent tabItem={props.tabItem} /> : <ExtraContent />)}
98 </Dropdown> 85 </Dropdown>
99 ); 86 );
100 }; 87 };
src/layouts/default/multitabs/tab.data.ts renamed to src/layouts/default/multitabs/data.ts
@@ -6,11 +6,13 @@ export enum TabContentEnum { @@ -6,11 +6,13 @@ export enum TabContentEnum {
6 TAB_TYPE, 6 TAB_TYPE,
7 EXTRA_TYPE, 7 EXTRA_TYPE,
8 } 8 }
  9 +
9 export interface TabContentProps { 10 export interface TabContentProps {
10 tabItem: TabItem | AppRouteRecordRaw; 11 tabItem: TabItem | AppRouteRecordRaw;
11 type?: TabContentEnum; 12 type?: TabContentEnum;
12 trigger?: Array<'click' | 'hover' | 'contextmenu'>; 13 trigger?: Array<'click' | 'hover' | 'contextmenu'>;
13 } 14 }
  15 +
14 /** 16 /**
15 * @description: 右键:下拉菜单文字 17 * @description: 右键:下拉菜单文字
16 */ 18 */
src/layouts/default/multitabs/index.less
@@ -2,11 +2,12 @@ @@ -2,11 +2,12 @@
2 2
3 .multiple-tabs { 3 .multiple-tabs {
4 z-index: 10; 4 z-index: 10;
5 - height: @multiple-height+2; 5 + height: @multiple-height + 2;
6 padding: 0 0 2px 0; 6 padding: 0 0 2px 0;
7 - line-height: @multiple-height+2; 7 + margin-left: -1px;
  8 + line-height: @multiple-height + 2;
8 background: @white; 9 background: @white;
9 - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.08); 10 + box-shadow: 0 1px 2px 0 rgba(29, 35, 41, 0.05);
10 11
11 .ant-tabs-small { 12 .ant-tabs-small {
12 height: @multiple-height; 13 height: @multiple-height;
@@ -32,19 +33,25 @@ @@ -32,19 +33,25 @@
32 color: @text-color-call-out; 33 color: @text-color-call-out;
33 background: @white; 34 background: @white;
34 border: 1px solid darken(@border-color-light, 8%); 35 border: 1px solid darken(@border-color-light, 8%);
35 - border-radius: none !important;  
36 transition: none; 36 transition: none;
37 37
  38 + &:hover {
  39 + .ant-tabs-close-x {
  40 + opacity: 1;
  41 + }
  42 + }
  43 +
38 .ant-tabs-close-x { 44 .ant-tabs-close-x {
39 - width: 12px; 45 + width: 8px;
40 height: 12px; 46 height: 12px;
41 font-size: 12px; 47 font-size: 12px;
42 color: inherit; 48 color: inherit;
  49 + opacity: 0;
43 transition: none; 50 transition: none;
44 51
45 &:hover { 52 &:hover {
46 svg { 53 svg {
47 - width: 0.8em; 54 + width: 0.75em;
48 } 55 }
49 } 56 }
50 } 57 }
@@ -61,12 +68,26 @@ @@ -61,12 +68,26 @@
61 } 68 }
62 69
63 .ant-tabs-tab-active { 70 .ant-tabs-tab-active {
  71 + position: relative;
  72 + padding-left: 26px;
64 color: @white; 73 color: @white;
65 background: fade(@primary-color, 100%); 74 background: fade(@primary-color, 100%);
66 border: 0; 75 border: 0;
67 76
68 &::before { 77 &::before {
69 - display: none; 78 + position: absolute;
  79 + top: calc(50% - 3px);
  80 + left: 8px;
  81 + width: 6px;
  82 + height: 6px;
  83 + background: #fff;
  84 + border-radius: 50%;
  85 + content: '';
  86 + transition: none;
  87 + }
  88 +
  89 + .ant-tabs-close-x {
  90 + opacity: 1;
70 } 91 }
71 92
72 svg { 93 svg {
@@ -78,6 +99,10 @@ @@ -78,6 +99,10 @@
78 99
79 .ant-tabs-nav > div:nth-child(1) { 100 .ant-tabs-nav > div:nth-child(1) {
80 padding: 0 10px; 101 padding: 0 10px;
  102 +
  103 + .ant-tabs-tab {
  104 + margin-right: 3px !important;
  105 + }
81 } 106 }
82 } 107 }
83 108
@@ -111,7 +136,10 @@ @@ -111,7 +136,10 @@
111 text-align: center; 136 text-align: center;
112 cursor: pointer; 137 cursor: pointer;
113 border-left: 1px solid #eee; 138 border-left: 1px solid #eee;
114 - // box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); 139 +
  140 + &:hover {
  141 + color: @text-color-base;
  142 + }
115 143
116 span[role='img'] { 144 span[role='img'] {
117 transform: rotate(90deg); 145 transform: rotate(90deg);
src/layouts/default/multitabs/index.tsx
1 import './index.less'; 1 import './index.less';
2 2
3 -import type { TabContentProps } from './tab.data'; 3 +import type { TabContentProps } from './data';
4 import type { TabItem } from '/@/store/modules/tab'; 4 import type { TabItem } from '/@/store/modules/tab';
5 import type { AppRouteRecordRaw } from '/@/router/types'; 5 import type { AppRouteRecordRaw } from '/@/router/types';
6 6
7 -import { defineComponent, watch, computed, unref } from 'vue'; 7 +import { defineComponent, watch, computed, unref, ref, onMounted, nextTick } from 'vue';
  8 +import Sortable from 'sortablejs';
  9 +
8 import { useRouter } from 'vue-router'; 10 import { useRouter } from 'vue-router';
9 11
10 import { Tabs } from 'ant-design-vue'; 12 import { Tabs } from 'ant-design-vue';
@@ -12,24 +14,28 @@ import TabContent from &#39;./TabContent&#39;; @@ -12,24 +14,28 @@ import TabContent from &#39;./TabContent&#39;;
12 14
13 import { useGo } from '/@/hooks/web/usePage'; 15 import { useGo } from '/@/hooks/web/usePage';
14 16
15 -import { TabContentEnum } from './tab.data'; 17 +import { TabContentEnum } from './data';
16 18
17 import { tabStore } from '/@/store/modules/tab'; 19 import { tabStore } from '/@/store/modules/tab';
18 import { userStore } from '/@/store/modules/user'; 20 import { userStore } from '/@/store/modules/user';
19 21
20 import { closeTab } from './useTabDropdown'; 22 import { closeTab } from './useTabDropdown';
21 -import { useTabs } from '/@/hooks/web/useTabs';  
22 -import { initAffixTabs } from './useAffixTabs'; 23 +import { initAffixTabs } from './useMultipleTabs';
  24 +import { isNullAndUnDef } from '/@/utils/is';
  25 +import { useProjectSetting } from '/@/hooks/setting';
23 26
24 export default defineComponent({ 27 export default defineComponent({
25 name: 'MultipleTabs', 28 name: 'MultipleTabs',
26 setup() { 29 setup() {
27 - initAffixTabs(); 30 + const activeKeyRef = ref('');
  31 +
  32 + const affixTextList = initAffixTabs();
28 33
29 const go = useGo(); 34 const go = useGo();
30 35
  36 + const { multiTabsSetting } = useProjectSetting();
  37 +
31 const { currentRoute } = useRouter(); 38 const { currentRoute } = useRouter();
32 - const { activeKeyRef } = useTabs();  
33 39
34 const getTabsState = computed(() => tabStore.getTabsState); 40 const getTabsState = computed(() => tabStore.getTabsState);
35 41
@@ -41,24 +47,24 @@ export default defineComponent({ @@ -41,24 +47,24 @@ export default defineComponent({
41 47
42 if (!lastChangeRoute || !userStore.getTokenState) return; 48 if (!lastChangeRoute || !userStore.getTokenState) return;
43 49
44 - const { path, fullPath } = lastChangeRoute;  
45 - if (activeKeyRef.value !== (fullPath || path)) {  
46 - activeKeyRef.value = fullPath || path; 50 + const { path, fullPath } = lastChangeRoute as AppRouteRecordRaw;
  51 + const p = fullPath || path;
  52 + if (activeKeyRef.value !== p) {
  53 + activeKeyRef.value = p;
47 } 54 }
48 - tabStore.commitAddTab((lastChangeRoute as unknown) as AppRouteRecordRaw); 55 + tabStore.commitAddTab(lastChangeRoute);
49 }, 56 },
50 { 57 {
51 immediate: true, 58 immediate: true,
52 } 59 }
53 ); 60 );
54 61
55 - // tab切换  
56 function handleChange(activeKey: any) { 62 function handleChange(activeKey: any) {
57 activeKeyRef.value = activeKey; 63 activeKeyRef.value = activeKey;
58 go(activeKey, false); 64 go(activeKey, false);
59 } 65 }
60 66
61 - // 关闭当前tab 67 + // Close the current tab
62 function handleEdit(targetKey: string) { 68 function handleEdit(targetKey: string) {
63 // Added operation to hide, currently only use delete operation 69 // Added operation to hide, currently only use delete operation
64 const index = unref(getTabsState).findIndex( 70 const index = unref(getTabsState).findIndex(
@@ -71,30 +77,65 @@ export default defineComponent({ @@ -71,30 +77,65 @@ export default defineComponent({
71 const tabContentProps: TabContentProps = { 77 const tabContentProps: TabContentProps = {
72 tabItem: (currentRoute as unknown) as AppRouteRecordRaw, 78 tabItem: (currentRoute as unknown) as AppRouteRecordRaw,
73 type: TabContentEnum.EXTRA_TYPE, 79 type: TabContentEnum.EXTRA_TYPE,
74 - trigger: ['click', 'contextmenu'],  
75 }; 80 };
76 - return (  
77 - <span>  
78 - <TabContent {...(tabContentProps as any)} />  
79 - </span>  
80 - ); 81 + return <TabContent {...(tabContentProps as any)} />;
81 } 82 }
82 83
83 function renderTabs() { 84 function renderTabs() {
84 return unref(getTabsState).map((item: TabItem) => { 85 return unref(getTabsState).map((item: TabItem) => {
85 const key = item.query ? item.fullPath : item.path; 86 const key = item.query ? item.fullPath : item.path;
86 const closable = !(item && item.meta && item.meta.affix); 87 const closable = !(item && item.meta && item.meta.affix);
  88 +
  89 + const slots = {
  90 + tab: () => <TabContent tabItem={item} />,
  91 + };
87 return ( 92 return (
88 <Tabs.TabPane key={key} closable={closable}> 93 <Tabs.TabPane key={key} closable={closable}>
89 - {{  
90 - tab: () => <TabContent tabItem={item} />,  
91 - }} 94 + {slots}
92 </Tabs.TabPane> 95 </Tabs.TabPane>
93 ); 96 );
94 }); 97 });
95 } 98 }
96 99
  100 + function initSortableTabs() {
  101 + if (!multiTabsSetting.canDrag) return;
  102 + nextTick(() => {
  103 + const el = document.querySelectorAll(
  104 + '.multiple-tabs .ant-tabs-nav > div'
  105 + )?.[0] as HTMLElement;
  106 +
  107 + if (!el) return;
  108 + Sortable.create(el, {
  109 + animation: 500,
  110 + delay: 400,
  111 + delayOnTouchOnly: true,
  112 + filter: (e: ChangeEvent) => {
  113 + const text = e?.target?.innerText;
  114 + if (!text) return false;
  115 + return affixTextList.includes(text);
  116 + },
  117 + onEnd: (evt) => {
  118 + const { oldIndex, newIndex } = evt;
  119 +
  120 + if (isNullAndUnDef(oldIndex) || isNullAndUnDef(newIndex) || oldIndex === newIndex) {
  121 + return;
  122 + }
  123 +
  124 + tabStore.commitSortTabs({ oldIndex, newIndex });
  125 + },
  126 + });
  127 + });
  128 + }
  129 +
  130 + onMounted(() => {
  131 + initSortableTabs();
  132 + });
  133 +
97 return () => { 134 return () => {
  135 + const slots = {
  136 + default: () => renderTabs(),
  137 + tabBarExtraContent: () => renderQuick(),
  138 + };
98 return ( 139 return (
99 <div class="multiple-tabs"> 140 <div class="multiple-tabs">
100 <Tabs 141 <Tabs
@@ -102,15 +143,12 @@ export default defineComponent({ @@ -102,15 +143,12 @@ export default defineComponent({
102 size="small" 143 size="small"
103 animated={false} 144 animated={false}
104 hideAdd={true} 145 hideAdd={true}
105 - tabBarGutter={4} 146 + tabBarGutter={3}
106 activeKey={unref(activeKeyRef)} 147 activeKey={unref(activeKeyRef)}
107 onChange={handleChange} 148 onChange={handleChange}
108 onEdit={handleEdit} 149 onEdit={handleEdit}
109 > 150 >
110 - {{  
111 - default: () => renderTabs(),  
112 - tabBarExtraContent: () => renderQuick(),  
113 - }} 151 + {slots}
114 </Tabs> 152 </Tabs>
115 </div> 153 </div>
116 ); 154 );
src/layouts/default/multitabs/useAffixTabs.ts renamed to src/layouts/default/multitabs/useMultipleTabs.ts
1 -import { toRaw } from 'vue'; 1 +import { toRaw, ref } from 'vue';
2 import router from '/@/router'; 2 import router from '/@/router';
3 import { AppRouteRecordRaw } from '/@/router/types'; 3 import { AppRouteRecordRaw } from '/@/router/types';
4 import { TabItem, tabStore } from '/@/store/modules/tab'; 4 import { TabItem, tabStore } from '/@/store/modules/tab';
5 5
6 export function initAffixTabs() { 6 export function initAffixTabs() {
  7 + const affixList = ref<TabItem[]>([]);
7 /** 8 /**
8 * @description: Filter all fixed routes 9 * @description: Filter all fixed routes
9 */ 10 */
@@ -23,13 +24,16 @@ export function initAffixTabs() { @@ -23,13 +24,16 @@ export function initAffixTabs() {
23 */ 24 */
24 function addAffixTabs(): void { 25 function addAffixTabs(): void {
25 const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]); 26 const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]);
  27 + affixList.value = affixTabs;
26 for (const tab of affixTabs) { 28 for (const tab of affixTabs) {
27 tabStore.commitAddTab(tab); 29 tabStore.commitAddTab(tab);
28 } 30 }
29 } 31 }
  32 +
30 let isAddAffix = false; 33 let isAddAffix = false;
31 if (!isAddAffix) { 34 if (!isAddAffix) {
32 addAffixTabs(); 35 addAffixTabs();
33 isAddAffix = true; 36 isAddAffix = true;
34 } 37 }
  38 + return affixList.value.map((item) => item.meta?.title).filter(Boolean);
35 } 39 }
src/layouts/default/multitabs/useTabDropdown.ts
1 import type { AppRouteRecordRaw } from '/@/router/types'; 1 import type { AppRouteRecordRaw } from '/@/router/types';
2 -import type { TabContentProps } from './tab.data'; 2 +import type { TabContentProps } from './data';
3 import type { Ref } from 'vue'; 3 import type { Ref } from 'vue';
4 import type { TabItem } from '/@/store/modules/tab'; 4 import type { TabItem } from '/@/store/modules/tab';
5 import type { DropMenu } from '/@/components/Dropdown'; 5 import type { DropMenu } from '/@/components/Dropdown';
6 6
7 import { computed, unref } from 'vue'; 7 import { computed, unref } from 'vue';
8 -import { TabContentEnum, MenuEventEnum, getActions } from './tab.data'; 8 +import { TabContentEnum, MenuEventEnum, getActions } from './data';
9 import { tabStore } from '/@/store/modules/tab'; 9 import { tabStore } from '/@/store/modules/tab';
10 import { appStore } from '/@/store/modules/app'; 10 import { appStore } from '/@/store/modules/app';
11 import { PageEnum } from '/@/enums/pageEnum'; 11 import { PageEnum } from '/@/enums/pageEnum';
@@ -15,9 +15,7 @@ import { useTabs, isInitUseTab } from &#39;/@/hooks/web/useTabs&#39;; @@ -15,9 +15,7 @@ import { useTabs, isInitUseTab } from &#39;/@/hooks/web/useTabs&#39;;
15 import { RouteLocationRaw } from 'vue-router'; 15 import { RouteLocationRaw } from 'vue-router';
16 16
17 const { initTabFn } = useTabs(); 17 const { initTabFn } = useTabs();
18 -/**  
19 - * @description: 右键下拉  
20 - */ 18 +
21 export function useTabDropdown(tabContentProps: TabContentProps) { 19 export function useTabDropdown(tabContentProps: TabContentProps) {
22 const { currentRoute } = router; 20 const { currentRoute } = router;
23 const redo = useRedo(); 21 const redo = useRedo();
@@ -30,26 +28,24 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -30,26 +28,24 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
30 : ((unref(currentRoute) as any) as AppRouteRecordRaw); 28 : ((unref(currentRoute) as any) as AppRouteRecordRaw);
31 }); 29 });
32 30
33 - // 当前tab列表  
34 - const getTabsState = computed(() => {  
35 - return tabStore.getTabsState;  
36 - }); 31 + // Current tab list
  32 + const getTabsState = computed(() => tabStore.getTabsState);
37 33
38 /** 34 /**
39 - * @description: 下拉列表 35 + * @description: drop-down list
40 */ 36 */
41 const getDropMenuList = computed(() => { 37 const getDropMenuList = computed(() => {
42 const dropMenuList = getActions(); 38 const dropMenuList = getActions();
43 - // 重置为初始状态 39 + // Reset to initial state
44 for (const item of dropMenuList) { 40 for (const item of dropMenuList) {
45 item.disabled = false; 41 item.disabled = false;
46 } 42 }
47 43
48 - // 没有tab 44 + // No tab
49 if (!unref(getTabsState) || unref(getTabsState).length <= 0) { 45 if (!unref(getTabsState) || unref(getTabsState).length <= 0) {
50 return dropMenuList; 46 return dropMenuList;
51 } else if (unref(getTabsState).length === 1) { 47 } else if (unref(getTabsState).length === 1) {
52 - // 只有一个tab 48 + // Only one tab
53 for (const item of dropMenuList) { 49 for (const item of dropMenuList) {
54 if (item.event !== MenuEventEnum.REFRESH_PAGE) { 50 if (item.event !== MenuEventEnum.REFRESH_PAGE) {
55 item.disabled = true; 51 item.disabled = true;
@@ -57,22 +53,20 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -57,22 +53,20 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
57 } 53 }
58 return dropMenuList; 54 return dropMenuList;
59 } 55 }
60 - if (!unref(getCurrentTab)) {  
61 - return;  
62 - } 56 + if (!unref(getCurrentTab)) return;
63 const { meta, path } = unref(getCurrentTab); 57 const { meta, path } = unref(getCurrentTab);
64 - // console.log(unref(getCurrentTab));  
65 58
66 - // 刷新按钮 59 + // Refresh button
67 const curItem = tabStore.getCurrentContextMenuState; 60 const curItem = tabStore.getCurrentContextMenuState;
68 const index = tabStore.getCurrentContextMenuIndexState; 61 const index = tabStore.getCurrentContextMenuIndexState;
69 const refreshDisabled = curItem ? curItem.path !== path : true; 62 const refreshDisabled = curItem ? curItem.path !== path : true;
70 - // 关闭左侧 63 + // Close left
71 const closeLeftDisabled = index === 0; 64 const closeLeftDisabled = index === 0;
72 65
73 - // 关闭右侧 66 + // Close right
74 const closeRightDisabled = index === unref(getTabsState).length - 1; 67 const closeRightDisabled = index === unref(getTabsState).length - 1;
75 - // 当前为固定tab 68 + // Currently fixed tab
  69 + // TODO PERf
76 dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false; 70 dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false;
77 if (meta && meta.affix) { 71 if (meta && meta.affix) {
78 dropMenuList[1].disabled = true; 72 dropMenuList[1].disabled = true;
@@ -84,7 +78,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -84,7 +78,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
84 }); 78 });
85 79
86 /** 80 /**
87 - * @description: 关闭所有页面时,跳转页面 81 + * @description: Jump to page when closing all pages
88 */ 82 */
89 function gotoPage() { 83 function gotoPage() {
90 const len = unref(getTabsState).length; 84 const len = unref(getTabsState).length;
@@ -99,14 +93,14 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -99,14 +93,14 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
99 toPath = p; 93 toPath = p;
100 } 94 }
101 } 95 }
102 - // 跳到当前页面报错 96 + // Jump to the current page and report an error
103 path !== toPath && go(toPath as PageEnum, true); 97 path !== toPath && go(toPath as PageEnum, true);
104 } 98 }
105 99
106 function isGotoPage(currentTab?: TabItem) { 100 function isGotoPage(currentTab?: TabItem) {
107 const { path } = unref(currentRoute); 101 const { path } = unref(currentRoute);
108 const currentPath = (currentTab || unref(getCurrentTab)).path; 102 const currentPath = (currentTab || unref(getCurrentTab)).path;
109 - // 不是当前tab,关闭左侧/右侧时,需跳转页面 103 + // Not the current tab, when you close the left/right side, you need to jump to the page
110 if (path !== currentPath) { 104 if (path !== currentPath) {
111 go(currentPath as PageEnum, true); 105 go(currentPath as PageEnum, true);
112 } 106 }
@@ -117,25 +111,31 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -117,25 +111,31 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
117 } catch (error) {} 111 } catch (error) {}
118 redo(); 112 redo();
119 } 113 }
  114 +
120 function closeAll() { 115 function closeAll() {
121 tabStore.commitCloseAllTab(); 116 tabStore.commitCloseAllTab();
122 gotoPage(); 117 gotoPage();
123 } 118 }
  119 +
124 function closeLeft(tabItem?: TabItem) { 120 function closeLeft(tabItem?: TabItem) {
125 tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab)); 121 tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab));
126 isGotoPage(tabItem); 122 isGotoPage(tabItem);
127 } 123 }
  124 +
128 function closeRight(tabItem?: TabItem) { 125 function closeRight(tabItem?: TabItem) {
129 tabStore.closeRightTabAction(tabItem || unref(getCurrentTab)); 126 tabStore.closeRightTabAction(tabItem || unref(getCurrentTab));
130 isGotoPage(tabItem); 127 isGotoPage(tabItem);
131 } 128 }
  129 +
132 function closeOther(tabItem?: TabItem) { 130 function closeOther(tabItem?: TabItem) {
133 tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab)); 131 tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab));
134 isGotoPage(tabItem); 132 isGotoPage(tabItem);
135 } 133 }
  134 +
136 function closeCurrent(tabItem?: TabItem) { 135 function closeCurrent(tabItem?: TabItem) {
137 closeTab(unref(tabItem || unref(getCurrentTab))); 136 closeTab(unref(tabItem || unref(getCurrentTab)));
138 } 137 }
  138 +
139 function scaleScreen() { 139 function scaleScreen() {
140 const { 140 const {
141 headerSetting: { show: showHeader }, 141 headerSetting: { show: showHeader },
@@ -159,7 +159,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -159,7 +159,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
159 }); 159 });
160 } 160 }
161 161
162 - // 处理右键事件 162 + // Handle right click event
163 function handleMenuEvent(menu: DropMenu): void { 163 function handleMenuEvent(menu: DropMenu): void {
164 const { event } = menu; 164 const { event } = menu;
165 165
@@ -168,76 +168,74 @@ export function useTabDropdown(tabContentProps: TabContentProps) { @@ -168,76 +168,74 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
168 scaleScreen(); 168 scaleScreen();
169 break; 169 break;
170 case MenuEventEnum.REFRESH_PAGE: 170 case MenuEventEnum.REFRESH_PAGE:
171 - // 刷新页面 171 + // refresh page
172 refreshPage(); 172 refreshPage();
173 break; 173 break;
174 - // 关闭当前 174 + // Close current
175 case MenuEventEnum.CLOSE_CURRENT: 175 case MenuEventEnum.CLOSE_CURRENT:
176 closeCurrent(); 176 closeCurrent();
177 break; 177 break;
178 - // 关闭左侧 178 + // Close left
179 case MenuEventEnum.CLOSE_LEFT: 179 case MenuEventEnum.CLOSE_LEFT:
180 closeLeft(); 180 closeLeft();
181 break; 181 break;
182 - // 关闭右侧 182 + // Close right
183 case MenuEventEnum.CLOSE_RIGHT: 183 case MenuEventEnum.CLOSE_RIGHT:
184 closeRight(); 184 closeRight();
185 break; 185 break;
186 - // 关闭其他 186 + // Close other
187 case MenuEventEnum.CLOSE_OTHER: 187 case MenuEventEnum.CLOSE_OTHER:
188 closeOther(); 188 closeOther();
189 break; 189 break;
190 - // 关闭其他 190 + // Close all
191 case MenuEventEnum.CLOSE_ALL: 191 case MenuEventEnum.CLOSE_ALL:
192 closeAll(); 192 closeAll();
193 break; 193 break;
194 - default:  
195 - break;  
196 } 194 }
197 } 195 }
198 return { getDropMenuList, handleMenuEvent }; 196 return { getDropMenuList, handleMenuEvent };
199 } 197 }
  198 +
  199 +export function getObj(tabItem: TabItem) {
  200 + const { params, path, query } = tabItem;
  201 + return {
  202 + params: params || {},
  203 + path,
  204 + query: query || {},
  205 + };
  206 +}
  207 +
200 export function closeTab(closedTab: TabItem | AppRouteRecordRaw) { 208 export function closeTab(closedTab: TabItem | AppRouteRecordRaw) {
201 const { currentRoute, replace } = router; 209 const { currentRoute, replace } = router;
202 - // 当前tab列表  
203 - const getTabsState = computed(() => {  
204 - return tabStore.getTabsState;  
205 - }); 210 + // Current tab list
  211 + const getTabsState = computed(() => tabStore.getTabsState);
206 212
207 const { path } = unref(currentRoute); 213 const { path } = unref(currentRoute);
208 if (path !== closedTab.path) { 214 if (path !== closedTab.path) {
209 - // 关闭的不是激活tab 215 + // Closed is not the activation tab
210 tabStore.commitCloseTab(closedTab); 216 tabStore.commitCloseTab(closedTab);
211 return; 217 return;
212 } 218 }
213 - // 关闭的为激活atb 219 +
  220 + // Closed is activated atb
214 let toObj: RouteLocationRaw = {}; 221 let toObj: RouteLocationRaw = {};
  222 +
215 const index = unref(getTabsState).findIndex((item) => item.path === path); 223 const index = unref(getTabsState).findIndex((item) => item.path === path);
216 224
217 - // 如果当前为最左边tab 225 + // If the current is the leftmost tab
218 if (index === 0) { 226 if (index === 0) {
219 - // 只有一个tab,则跳转至首页,否则跳转至右tab 227 + // There is only one tab, then jump to the homepage, otherwise jump to the right tab
220 if (unref(getTabsState).length === 1) { 228 if (unref(getTabsState).length === 1) {
221 toObj = PageEnum.BASE_HOME; 229 toObj = PageEnum.BASE_HOME;
222 } else { 230 } else {
223 - // 跳转至右边tab 231 + // Jump to the right tab
224 const page = unref(getTabsState)[index + 1]; 232 const page = unref(getTabsState)[index + 1];
225 - const { params, path, query } = page;  
226 - toObj = {  
227 - params,  
228 - path,  
229 - query,  
230 - }; 233 + toObj = getObj(page);
231 } 234 }
232 } else { 235 } else {
233 - // 跳转至左边tab 236 + // Close the current tab
234 const page = unref(getTabsState)[index - 1]; 237 const page = unref(getTabsState)[index - 1];
235 - const { params, path, query } = page;  
236 - toObj = {  
237 - params: params || {},  
238 - path,  
239 - query: query || {},  
240 - }; 238 + toObj = getObj(page);
241 } 239 }
242 const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw; 240 const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw;
243 tabStore.commitCloseTab(route); 241 tabStore.commitCloseTab(route);
src/layouts/default/setting/SettingDrawer.tsx
@@ -203,7 +203,7 @@ export default defineComponent({ @@ -203,7 +203,7 @@ export default defineComponent({
203 getMenuFixed, 203 getMenuFixed,
204 getCollapsed, 204 getCollapsed,
205 getShowSearch, 205 getShowSearch,
206 - getHasDrag, 206 + getCanDrag,
207 getTopMenuAlign, 207 getTopMenuAlign,
208 getAccordion, 208 getAccordion,
209 getMenuWidth, 209 getMenuWidth,
@@ -267,7 +267,7 @@ export default defineComponent({ @@ -267,7 +267,7 @@ export default defineComponent({
267 handler: (e) => { 267 handler: (e) => {
268 baseHandler(HandlerEnum.MENU_HAS_DRAG, e); 268 baseHandler(HandlerEnum.MENU_HAS_DRAG, e);
269 }, 269 },
270 - def: unref(getHasDrag), 270 + def: unref(getCanDrag),
271 disabled: !unref(getShowMenuRef), 271 disabled: !unref(getShowMenuRef),
272 }), 272 }),
273 renderSwitchItem('侧边菜单搜索', { 273 renderSwitchItem('侧边菜单搜索', {
src/layouts/default/setting/handler.ts
@@ -30,7 +30,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial&lt;ProjectConf @@ -30,7 +30,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial&lt;ProjectConf
30 }; 30 };
31 31
32 case HandlerEnum.MENU_HAS_DRAG: 32 case HandlerEnum.MENU_HAS_DRAG:
33 - return { menuSetting: { hasDrag: value } }; 33 + return { menuSetting: { canDrag: value } };
34 34
35 case HandlerEnum.MENU_ACCORDION: 35 case HandlerEnum.MENU_ACCORDION:
36 return { menuSetting: { accordion: value } }; 36 return { menuSetting: { accordion: value } };
src/layouts/default/sider/index.less
1 @import (reference) '../../../design/index.less'; 1 @import (reference) '../../../design/index.less';
2 2
3 .layout-sidebar { 3 .layout-sidebar {
4 - overflow: hidden; 4 + // overflow: hidden;
5 5
6 &.fixed { 6 &.fixed {
7 position: fixed; 7 position: fixed;
@@ -15,7 +15,7 @@ @@ -15,7 +15,7 @@
15 } 15 }
16 16
17 &:not(.ant-layout-sider-dark) { 17 &:not(.ant-layout-sider-dark) {
18 - border-right: 1px solid @border-color-light; 18 + // border-right: 1px solid @border-color-light;
19 box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05); 19 box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
20 } 20 }
21 21
src/layouts/default/sider/useLayoutSider.tsx
@@ -82,7 +82,7 @@ export function useTrigger() { @@ -82,7 +82,7 @@ export function useTrigger() {
82 * @param dragBarRef 82 * @param dragBarRef
83 */ 83 */
84 export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) { 84 export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
85 - const { getMiniWidthNumber, getCollapsed, setMenuSetting, getHasDrag } = useMenuSetting(); 85 + const { getMiniWidthNumber, getCollapsed, setMenuSetting, getCanDrag } = useMenuSetting();
86 86
87 const getDragBarStyle = computed(() => { 87 const getDragBarStyle = computed(() => {
88 if (unref(getCollapsed)) { 88 if (unref(getCollapsed)) {
@@ -101,7 +101,7 @@ export function useDragLine(siderRef: Ref&lt;any&gt;, dragBarRef: Ref&lt;any&gt;) { @@ -101,7 +101,7 @@ export function useDragLine(siderRef: Ref&lt;any&gt;, dragBarRef: Ref&lt;any&gt;) {
101 function renderDragLine() { 101 function renderDragLine() {
102 return ( 102 return (
103 <div 103 <div
104 - class={[`layout-sidebar__darg-bar`, !unref(getHasDrag) ? 'hide' : '']} 104 + class={[`layout-sidebar__darg-bar`, { hide: !unref(getCanDrag) }]}
105 style={unref(getDragBarStyle)} 105 style={unref(getDragBarStyle)}
106 ref={dragBarRef} 106 ref={dragBarRef}
107 /> 107 />
src/settings/projectSetting.ts
@@ -83,7 +83,7 @@ const setting: ProjectConfig = { @@ -83,7 +83,7 @@ const setting: ProjectConfig = {
83 collapsedShowTitle: false, 83 collapsedShowTitle: false,
84 // Whether it can be dragged 84 // Whether it can be dragged
85 // Only limited to the opening of the left menu, the mouse has a drag bar on the right side of the menu 85 // Only limited to the opening of the left menu, the mouse has a drag bar on the right side of the menu
86 - hasDrag: false, 86 + canDrag: false,
87 // Whether to show no dom 87 // Whether to show no dom
88 show: true, 88 show: true,
89 // Whether to show dom 89 // Whether to show dom
@@ -114,6 +114,8 @@ const setting: ProjectConfig = { @@ -114,6 +114,8 @@ const setting: ProjectConfig = {
114 multiTabsSetting: { 114 multiTabsSetting: {
115 // Turn on 115 // Turn on
116 show: true, 116 show: true,
  117 + // Is it possible to drag and drop sorting tabs
  118 + canDrag: true,
117 // Turn on quick actions 119 // Turn on quick actions
118 showQuick: true, 120 showQuick: true,
119 // Maximum number of tab cache 121 // Maximum number of tab cache
src/store/modules/tab.ts
@@ -176,6 +176,14 @@ class Tab extends VuexModule { @@ -176,6 +176,14 @@ class Tab extends VuexModule {
176 } 176 }
177 177
178 @Mutation 178 @Mutation
  179 + commitSortTabs({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }): void {
  180 + const currentTab = this.tabsState[oldIndex];
  181 +
  182 + this.tabsState.splice(oldIndex, 1);
  183 + this.tabsState.splice(newIndex, 0, currentTab);
  184 + }
  185 +
  186 + @Mutation
179 closeMultipleTab({ pathList, nameList }: { pathList: string[]; nameList: string[] }): void { 187 closeMultipleTab({ pathList, nameList }: { pathList: string[]; nameList: string[] }): void {
180 this.tabsState = toRaw(this.tabsState).filter((item) => !pathList.includes(item.fullPath)); 188 this.tabsState = toRaw(this.tabsState).filter((item) => !pathList.includes(item.fullPath));
181 if (unref(getOpenKeepAliveRef) && nameList) { 189 if (unref(getOpenKeepAliveRef) && nameList) {
src/types/config.d.ts
@@ -8,7 +8,7 @@ export interface MenuSetting { @@ -8,7 +8,7 @@ export interface MenuSetting {
8 fixed: boolean; 8 fixed: boolean;
9 collapsed: boolean; 9 collapsed: boolean;
10 collapsedShowTitle: boolean; 10 collapsedShowTitle: boolean;
11 - hasDrag: boolean; 11 + canDrag: boolean;
12 showSearch: boolean; 12 showSearch: boolean;
13 show: boolean; 13 show: boolean;
14 hidden: boolean; 14 hidden: boolean;
@@ -28,7 +28,7 @@ export interface MultiTabsSetting { @@ -28,7 +28,7 @@ export interface MultiTabsSetting {
28 show: boolean; 28 show: boolean;
29 // 开启快速操作 29 // 开启快速操作
30 showQuick: boolean; 30 showQuick: boolean;
31 - 31 + canDrag: boolean;
32 // 缓存最大数量 32 // 缓存最大数量
33 max: number; 33 max: number;
34 } 34 }
src/utils/is.ts
@@ -24,6 +24,10 @@ export function isNull(val: unknown): val is null { @@ -24,6 +24,10 @@ export function isNull(val: unknown): val is null {
24 return val === null; 24 return val === null;
25 } 25 }
26 26
  27 +export function isNullAndUnDef(val: unknown): val is null | undefined {
  28 + return isUnDef(val) && isNull(val);
  29 +}
  30 +
27 export function isNumber(val: unknown): val is number { 31 export function isNumber(val: unknown): val is number {
28 return is(val, 'Number'); 32 return is(val, 'Number');
29 } 33 }
src/views/demo/feat/tabs/index.vue
@@ -11,28 +11,18 @@ @@ -11,28 +11,18 @@
11 <a-button class="mr-2" @click="closeOther">关闭其他</a-button> 11 <a-button class="mr-2" @click="closeOther">关闭其他</a-button>
12 <a-button class="mr-2" @click="closeCurrent">关闭当前</a-button> 12 <a-button class="mr-2" @click="closeCurrent">关闭当前</a-button>
13 <a-button class="mr-2" @click="refreshPage">刷新当前</a-button> 13 <a-button class="mr-2" @click="refreshPage">刷新当前</a-button>
14 - <a-button class="mr-2" @click="openTab">打开图标界面tab</a-button>  
15 </CollapseContainer> 14 </CollapseContainer>
16 </div> 15 </div>
17 </template> 16 </template>
18 <script lang="ts"> 17 <script lang="ts">
19 import { defineComponent } from 'vue'; 18 import { defineComponent } from 'vue';
20 import { CollapseContainer } from '/@/components/Container/index'; 19 import { CollapseContainer } from '/@/components/Container/index';
21 - import { PageEnum } from '/@/enums/pageEnum';  
22 import { useTabs } from '/@/hooks/web/useTabs'; 20 import { useTabs } from '/@/hooks/web/useTabs';
23 export default defineComponent({ 21 export default defineComponent({
24 name: 'TabsDemo', 22 name: 'TabsDemo',
25 components: { CollapseContainer }, 23 components: { CollapseContainer },
26 setup() { 24 setup() {
27 - const {  
28 - closeAll,  
29 - closeLeft,  
30 - closeRight,  
31 - closeOther,  
32 - closeCurrent,  
33 - refreshPage,  
34 - addTab,  
35 - } = useTabs(); 25 + const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTabs();
36 26
37 return { 27 return {
38 closeAll, 28 closeAll,
@@ -41,9 +31,6 @@ @@ -41,9 +31,6 @@
41 closeOther, 31 closeOther,
42 closeCurrent, 32 closeCurrent,
43 refreshPage, 33 refreshPage,
44 - openTab: () => {  
45 - addTab('/feat/icon' as PageEnum, true);  
46 - },  
47 }; 34 };
48 }, 35 },
49 }); 36 });
yarn.lock
@@ -1495,6 +1495,11 @@ @@ -1495,6 +1495,11 @@
1495 "@types/mime" "*" 1495 "@types/mime" "*"
1496 "@types/node" "*" 1496 "@types/node" "*"
1497 1497
  1498 +"@types/sortablejs@^1.10.6":
  1499 + version "1.10.6"
  1500 + resolved "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.10.6.tgz#98725ae08f1dfe28b8da0fdf302c417f5ff043c0"
  1501 + integrity sha512-QRz8Z+uw2Y4Gwrtxw8hD782zzuxxugdcq8X/FkPsXUa1kfslhGzy13+4HugO9FXNo+jlWVcE6DYmmegniIQ30A==
  1502 +
1498 "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": 1503 "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2":
1499 version "2.0.3" 1504 version "2.0.3"
1500 resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" 1505 resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"