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
@@ -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
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 './TabContent'; | @@ -12,24 +14,28 @@ import TabContent from './TabContent'; | ||
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 '/@/hooks/web/useTabs'; | @@ -15,9 +15,7 @@ import { useTabs, isInitUseTab } from '/@/hooks/web/useTabs'; | ||
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<ProjectConf | @@ -30,7 +30,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<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<any>, dragBarRef: Ref<any>) { | @@ -101,7 +101,7 @@ export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) { | ||
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" |