feat: add tab drag and drop sort

This commit is contained in:
vben 2020-11-25 22:28:58 +08:00
parent 5cabbac757
commit cedba37e4c
23 changed files with 255 additions and 228 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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