Commit cedba37e4cf63456c97f7e391761f176137e0165

Authored by vben
1 parent 5cabbac7

feat: add tab drag and drop sort

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