Commit cedba37e4cf63456c97f7e391761f176137e0165
1 parent
5cabbac7
feat: add tab drag and drop sort
Showing
23 changed files
with
255 additions
and
228 deletions
CHANGELOG.zh_CN.md
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
src/layouts/default/index.less
src/layouts/default/index.tsx
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 './TabContent'; |
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 '/@/hooks/web/useTabs'; |
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<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<any>, dragBarRef: Ref<any>) { |
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" | ... | ... |