mirror of
https://github.com/vbenjs/gf-vben-admin.git
synced 2025-02-02 19:08:40 +08:00
feat: add tab drag and drop sort
This commit is contained in:
parent
5cabbac757
commit
cedba37e4c
@ -2,13 +2,14 @@
|
||||
|
||||
### ✨ Refactor
|
||||
|
||||
- 重构整体 layout。更改代码实现方式。代码更精简
|
||||
- 重构整体 layout。更改代码实现方式。代码更精简,并加入多语言支持
|
||||
- 配置项重构
|
||||
- 移除 messageSetting 配置
|
||||
|
||||
### ✨ Features
|
||||
|
||||
- 缓存可以配置是否加密,默认生产环境开启 Aes 加密
|
||||
- 新增标签页拖拽排序
|
||||
|
||||
### 🎫 Chores
|
||||
|
||||
|
@ -58,6 +58,7 @@
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/rollup-plugin-visualizer": "^2.6.0",
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@types/yargs": "^15.0.10",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.8.2",
|
||||
|
@ -33,7 +33,7 @@ export function useMenuSetting() {
|
||||
|
||||
const getMenuBgColor = computed(() => unref(getMenuSetting).bgColor);
|
||||
|
||||
const getHasDrag = computed(() => unref(getMenuSetting).hasDrag);
|
||||
const getCanDrag = computed(() => unref(getMenuSetting).canDrag);
|
||||
|
||||
const getAccordion = computed(() => unref(getMenuSetting).accordion);
|
||||
|
||||
@ -117,7 +117,7 @@ export function useMenuSetting() {
|
||||
getTrigger,
|
||||
getSplit,
|
||||
getMenuTheme,
|
||||
getHasDrag,
|
||||
getCanDrag,
|
||||
getIsHorizontal,
|
||||
getShowSearch,
|
||||
getCollapsedShowTitle,
|
||||
|
@ -1,14 +1,6 @@
|
||||
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { TabItem, tabStore } from '/@/store/modules/tab';
|
||||
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;
|
||||
|
||||
interface TabFn {
|
||||
@ -28,6 +20,7 @@ let closeOther: RouteFn;
|
||||
let closeCurrent: RouteFn;
|
||||
|
||||
export let isInitUseTab = false;
|
||||
|
||||
export function useTabs() {
|
||||
function initTabFn({
|
||||
refreshPageFn,
|
||||
@ -38,6 +31,7 @@ export function useTabs() {
|
||||
closeCurrentFn,
|
||||
}: TabFn) {
|
||||
if (isInitUseTab) return;
|
||||
|
||||
refreshPageFn && (refreshPage = refreshPageFn);
|
||||
closeAllFn && (closeAll = closeAllFn);
|
||||
closeLeftFn && (closeLeft = closeLeftFn);
|
||||
@ -58,29 +52,13 @@ export function useTabs() {
|
||||
}
|
||||
|
||||
function canIUseFn(): boolean {
|
||||
const { getProjectConfig } = appStore;
|
||||
const { multiTabsSetting: { show } = {} } = getProjectConfig;
|
||||
const { multiTabsSetting: { show } = {} } = appStore.getProjectConfig;
|
||||
if (!show) {
|
||||
throw new Error('当前未开启多标签页,请在设置中打开!');
|
||||
}
|
||||
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 {
|
||||
initTabFn,
|
||||
refreshPage: () => canIUseFn() && refreshPage(tabStore.getCurrentTab),
|
||||
@ -90,26 +68,5 @@ export function useTabs() {
|
||||
closeOther: () => canIUseFn() && closeOther(tabStore.getCurrentTab),
|
||||
closeCurrent: () => canIUseFn() && closeCurrent(tabStore.getCurrentTab),
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
display: flex;
|
||||
height: @header-height;
|
||||
padding: 0 20px 0 0;
|
||||
margin-left: -1px;
|
||||
line-height: @header-height;
|
||||
color: @white;
|
||||
background: @white;
|
||||
|
@ -9,4 +9,8 @@
|
||||
> .ant-layout {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
&__main {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ export default defineComponent({
|
||||
{() => (
|
||||
<>
|
||||
{unref(showSideBarRef) && <LayoutSideBar />}
|
||||
<Layout>
|
||||
<Layout class="default-layout__main">
|
||||
{() => (
|
||||
<>
|
||||
<LayoutMultipleHeader />
|
||||
|
@ -1,16 +1,46 @@
|
||||
import { defineComponent, unref, computed } from 'vue';
|
||||
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { defineComponent, unref, computed, FunctionalComponent } from 'vue';
|
||||
|
||||
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 { 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 { 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({
|
||||
name: 'TabContent',
|
||||
@ -19,82 +49,39 @@ export default defineComponent({
|
||||
type: Object as PropType<TabItem>,
|
||||
default: null,
|
||||
},
|
||||
|
||||
type: {
|
||||
type: Number as PropType<number>,
|
||||
type: Number as PropType<TabContentEnum>,
|
||||
default: TabContentEnum.TAB_TYPE,
|
||||
},
|
||||
trigger: {
|
||||
type: Array as PropType<string[]>,
|
||||
default: () => {
|
||||
return ['contextmenu'];
|
||||
},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const getProjectConfigRef = computed(() => {
|
||||
return appStore.getProjectConfig;
|
||||
const { getShowMenu } = useMenuSetting();
|
||||
const { getShowHeader } = useHeaderSetting();
|
||||
const { getShowQuick } = useMultipleTabSetting();
|
||||
|
||||
const getIsScale = computed(() => {
|
||||
return !unref(getShowMenu) && !unref(getShowHeader);
|
||||
});
|
||||
|
||||
const getIsScaleRef = computed(() => {
|
||||
const {
|
||||
menuSetting: { show: showMenu },
|
||||
headerSetting: { show: showHeader },
|
||||
} = unref(getProjectConfigRef);
|
||||
return !showMenu && !showHeader;
|
||||
const getIsTab = computed(() => {
|
||||
return !unref(getShowQuick) ? true : props.type === TabContentEnum.TAB_TYPE;
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
return () => {
|
||||
const { trigger, type } = props;
|
||||
const {
|
||||
multiTabsSetting: { showQuick },
|
||||
} = unref(getProjectConfigRef);
|
||||
|
||||
const isTab = !showQuick ? true : type === TabContentEnum.TAB_TYPE;
|
||||
const scaleAction = getScaleAction(
|
||||
unref(getIsScaleRef) ? '缩小' : '放大',
|
||||
unref(getIsScaleRef)
|
||||
);
|
||||
const scaleAction = getScaleAction(unref(getIsScale) ? '收起' : '展开', unref(getIsScale));
|
||||
const dropMenuList = unref(getDropMenuList) || [];
|
||||
|
||||
const isTab = unref(getIsTab);
|
||||
return (
|
||||
<Dropdown
|
||||
dropMenuList={!isTab ? [scaleAction, ...dropMenuList] : dropMenuList}
|
||||
trigger={isTab ? trigger : ['hover']}
|
||||
trigger={isTab ? ['contextmenu'] : ['click']}
|
||||
onMenuEvent={handleMenuEvent}
|
||||
>
|
||||
{() => (isTab ? renderTabContent() : renderExtraContent())}
|
||||
{() => (isTab ? <TabContent tabItem={props.tabItem} /> : <ExtraContent />)}
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
@ -6,11 +6,13 @@ export enum TabContentEnum {
|
||||
TAB_TYPE,
|
||||
EXTRA_TYPE,
|
||||
}
|
||||
|
||||
export interface TabContentProps {
|
||||
tabItem: TabItem | AppRouteRecordRaw;
|
||||
type?: TabContentEnum;
|
||||
trigger?: Array<'click' | 'hover' | 'contextmenu'>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 右键:下拉菜单文字
|
||||
*/
|
@ -2,11 +2,12 @@
|
||||
|
||||
.multiple-tabs {
|
||||
z-index: 10;
|
||||
height: @multiple-height+2;
|
||||
height: @multiple-height + 2;
|
||||
padding: 0 0 2px 0;
|
||||
line-height: @multiple-height+2;
|
||||
margin-left: -1px;
|
||||
line-height: @multiple-height + 2;
|
||||
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 {
|
||||
height: @multiple-height;
|
||||
@ -32,19 +33,25 @@
|
||||
color: @text-color-call-out;
|
||||
background: @white;
|
||||
border: 1px solid darken(@border-color-light, 8%);
|
||||
border-radius: none !important;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
.ant-tabs-close-x {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-close-x {
|
||||
width: 12px;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
font-size: 12px;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
svg {
|
||||
width: 0.8em;
|
||||
width: 0.75em;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,12 +68,26 @@
|
||||
}
|
||||
|
||||
.ant-tabs-tab-active {
|
||||
position: relative;
|
||||
padding-left: 26px;
|
||||
color: @white;
|
||||
background: fade(@primary-color, 100%);
|
||||
border: 0;
|
||||
|
||||
&::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 {
|
||||
@ -78,6 +99,10 @@
|
||||
|
||||
.ant-tabs-nav > div:nth-child(1) {
|
||||
padding: 0 10px;
|
||||
|
||||
.ant-tabs-tab {
|
||||
margin-right: 3px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +136,10 @@
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #eee;
|
||||
// box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
&:hover {
|
||||
color: @text-color-base;
|
||||
}
|
||||
|
||||
span[role='img'] {
|
||||
transform: rotate(90deg);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import './index.less';
|
||||
|
||||
import type { TabContentProps } from './tab.data';
|
||||
import type { TabContentProps } from './data';
|
||||
import type { TabItem } from '/@/store/modules/tab';
|
||||
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 { Tabs } from 'ant-design-vue';
|
||||
@ -12,24 +14,28 @@ import TabContent from './TabContent';
|
||||
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
|
||||
import { TabContentEnum } from './tab.data';
|
||||
import { TabContentEnum } from './data';
|
||||
|
||||
import { tabStore } from '/@/store/modules/tab';
|
||||
import { userStore } from '/@/store/modules/user';
|
||||
|
||||
import { closeTab } from './useTabDropdown';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { initAffixTabs } from './useAffixTabs';
|
||||
import { initAffixTabs } from './useMultipleTabs';
|
||||
import { isNullAndUnDef } from '/@/utils/is';
|
||||
import { useProjectSetting } from '/@/hooks/setting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MultipleTabs',
|
||||
setup() {
|
||||
initAffixTabs();
|
||||
const activeKeyRef = ref('');
|
||||
|
||||
const affixTextList = initAffixTabs();
|
||||
|
||||
const go = useGo();
|
||||
|
||||
const { multiTabsSetting } = useProjectSetting();
|
||||
|
||||
const { currentRoute } = useRouter();
|
||||
const { activeKeyRef } = useTabs();
|
||||
|
||||
const getTabsState = computed(() => tabStore.getTabsState);
|
||||
|
||||
@ -41,24 +47,24 @@ export default defineComponent({
|
||||
|
||||
if (!lastChangeRoute || !userStore.getTokenState) return;
|
||||
|
||||
const { path, fullPath } = lastChangeRoute;
|
||||
if (activeKeyRef.value !== (fullPath || path)) {
|
||||
activeKeyRef.value = fullPath || path;
|
||||
const { path, fullPath } = lastChangeRoute as AppRouteRecordRaw;
|
||||
const p = fullPath || path;
|
||||
if (activeKeyRef.value !== p) {
|
||||
activeKeyRef.value = p;
|
||||
}
|
||||
tabStore.commitAddTab((lastChangeRoute as unknown) as AppRouteRecordRaw);
|
||||
tabStore.commitAddTab(lastChangeRoute);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
// tab切换
|
||||
function handleChange(activeKey: any) {
|
||||
activeKeyRef.value = activeKey;
|
||||
go(activeKey, false);
|
||||
}
|
||||
|
||||
// 关闭当前tab
|
||||
// Close the current tab
|
||||
function handleEdit(targetKey: string) {
|
||||
// Added operation to hide, currently only use delete operation
|
||||
const index = unref(getTabsState).findIndex(
|
||||
@ -71,30 +77,65 @@ export default defineComponent({
|
||||
const tabContentProps: TabContentProps = {
|
||||
tabItem: (currentRoute as unknown) as AppRouteRecordRaw,
|
||||
type: TabContentEnum.EXTRA_TYPE,
|
||||
trigger: ['click', 'contextmenu'],
|
||||
};
|
||||
return (
|
||||
<span>
|
||||
<TabContent {...(tabContentProps as any)} />
|
||||
</span>
|
||||
);
|
||||
return <TabContent {...(tabContentProps as any)} />;
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
return unref(getTabsState).map((item: TabItem) => {
|
||||
const key = item.query ? item.fullPath : item.path;
|
||||
const closable = !(item && item.meta && item.meta.affix);
|
||||
|
||||
const slots = {
|
||||
tab: () => <TabContent tabItem={item} />,
|
||||
};
|
||||
return (
|
||||
<Tabs.TabPane key={key} closable={closable}>
|
||||
{{
|
||||
tab: () => <TabContent tabItem={item} />,
|
||||
}}
|
||||
{slots}
|
||||
</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 () => {
|
||||
const slots = {
|
||||
default: () => renderTabs(),
|
||||
tabBarExtraContent: () => renderQuick(),
|
||||
};
|
||||
return (
|
||||
<div class="multiple-tabs">
|
||||
<Tabs
|
||||
@ -102,15 +143,12 @@ export default defineComponent({
|
||||
size="small"
|
||||
animated={false}
|
||||
hideAdd={true}
|
||||
tabBarGutter={4}
|
||||
tabBarGutter={3}
|
||||
activeKey={unref(activeKeyRef)}
|
||||
onChange={handleChange}
|
||||
onEdit={handleEdit}
|
||||
>
|
||||
{{
|
||||
default: () => renderTabs(),
|
||||
tabBarExtraContent: () => renderQuick(),
|
||||
}}
|
||||
{slots}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { toRaw } from 'vue';
|
||||
import { toRaw, ref } from 'vue';
|
||||
import router from '/@/router';
|
||||
import { AppRouteRecordRaw } from '/@/router/types';
|
||||
import { TabItem, tabStore } from '/@/store/modules/tab';
|
||||
|
||||
export function initAffixTabs() {
|
||||
const affixList = ref<TabItem[]>([]);
|
||||
/**
|
||||
* @description: Filter all fixed routes
|
||||
*/
|
||||
@ -23,13 +24,16 @@ export function initAffixTabs() {
|
||||
*/
|
||||
function addAffixTabs(): void {
|
||||
const affixTabs = filterAffixTabs((router.getRoutes() as unknown) as AppRouteRecordRaw[]);
|
||||
affixList.value = affixTabs;
|
||||
for (const tab of affixTabs) {
|
||||
tabStore.commitAddTab(tab);
|
||||
}
|
||||
}
|
||||
|
||||
let isAddAffix = false;
|
||||
if (!isAddAffix) {
|
||||
addAffixTabs();
|
||||
isAddAffix = true;
|
||||
}
|
||||
return affixList.value.map((item) => item.meta?.title).filter(Boolean);
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import type { AppRouteRecordRaw } from '/@/router/types';
|
||||
import type { TabContentProps } from './tab.data';
|
||||
import type { TabContentProps } from './data';
|
||||
import type { Ref } from 'vue';
|
||||
import type { TabItem } from '/@/store/modules/tab';
|
||||
import type { DropMenu } from '/@/components/Dropdown';
|
||||
|
||||
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 { appStore } from '/@/store/modules/app';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
@ -15,9 +15,7 @@ import { useTabs, isInitUseTab } from '/@/hooks/web/useTabs';
|
||||
import { RouteLocationRaw } from 'vue-router';
|
||||
|
||||
const { initTabFn } = useTabs();
|
||||
/**
|
||||
* @description: 右键下拉
|
||||
*/
|
||||
|
||||
export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
const { currentRoute } = router;
|
||||
const redo = useRedo();
|
||||
@ -30,26 +28,24 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
: ((unref(currentRoute) as any) as AppRouteRecordRaw);
|
||||
});
|
||||
|
||||
// 当前tab列表
|
||||
const getTabsState = computed(() => {
|
||||
return tabStore.getTabsState;
|
||||
});
|
||||
// Current tab list
|
||||
const getTabsState = computed(() => tabStore.getTabsState);
|
||||
|
||||
/**
|
||||
* @description: 下拉列表
|
||||
* @description: drop-down list
|
||||
*/
|
||||
const getDropMenuList = computed(() => {
|
||||
const dropMenuList = getActions();
|
||||
// 重置为初始状态
|
||||
// Reset to initial state
|
||||
for (const item of dropMenuList) {
|
||||
item.disabled = false;
|
||||
}
|
||||
|
||||
// 没有tab
|
||||
// No tab
|
||||
if (!unref(getTabsState) || unref(getTabsState).length <= 0) {
|
||||
return dropMenuList;
|
||||
} else if (unref(getTabsState).length === 1) {
|
||||
// 只有一个tab
|
||||
// Only one tab
|
||||
for (const item of dropMenuList) {
|
||||
if (item.event !== MenuEventEnum.REFRESH_PAGE) {
|
||||
item.disabled = true;
|
||||
@ -57,22 +53,20 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
}
|
||||
return dropMenuList;
|
||||
}
|
||||
if (!unref(getCurrentTab)) {
|
||||
return;
|
||||
}
|
||||
if (!unref(getCurrentTab)) return;
|
||||
const { meta, path } = unref(getCurrentTab);
|
||||
// console.log(unref(getCurrentTab));
|
||||
|
||||
// 刷新按钮
|
||||
// Refresh button
|
||||
const curItem = tabStore.getCurrentContextMenuState;
|
||||
const index = tabStore.getCurrentContextMenuIndexState;
|
||||
const refreshDisabled = curItem ? curItem.path !== path : true;
|
||||
// 关闭左侧
|
||||
// Close left
|
||||
const closeLeftDisabled = index === 0;
|
||||
|
||||
// 关闭右侧
|
||||
// Close right
|
||||
const closeRightDisabled = index === unref(getTabsState).length - 1;
|
||||
// 当前为固定tab
|
||||
// Currently fixed tab
|
||||
// TODO PERf
|
||||
dropMenuList[0].disabled = unref(isTabsRef) ? refreshDisabled : false;
|
||||
if (meta && meta.affix) {
|
||||
dropMenuList[1].disabled = true;
|
||||
@ -84,7 +78,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
});
|
||||
|
||||
/**
|
||||
* @description: 关闭所有页面时,跳转页面
|
||||
* @description: Jump to page when closing all pages
|
||||
*/
|
||||
function gotoPage() {
|
||||
const len = unref(getTabsState).length;
|
||||
@ -99,14 +93,14 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
toPath = p;
|
||||
}
|
||||
}
|
||||
// 跳到当前页面报错
|
||||
// Jump to the current page and report an error
|
||||
path !== toPath && go(toPath as PageEnum, true);
|
||||
}
|
||||
|
||||
function isGotoPage(currentTab?: TabItem) {
|
||||
const { path } = unref(currentRoute);
|
||||
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) {
|
||||
go(currentPath as PageEnum, true);
|
||||
}
|
||||
@ -117,25 +111,31 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
} catch (error) {}
|
||||
redo();
|
||||
}
|
||||
|
||||
function closeAll() {
|
||||
tabStore.commitCloseAllTab();
|
||||
gotoPage();
|
||||
}
|
||||
|
||||
function closeLeft(tabItem?: TabItem) {
|
||||
tabStore.closeLeftTabAction(tabItem || unref(getCurrentTab));
|
||||
isGotoPage(tabItem);
|
||||
}
|
||||
|
||||
function closeRight(tabItem?: TabItem) {
|
||||
tabStore.closeRightTabAction(tabItem || unref(getCurrentTab));
|
||||
isGotoPage(tabItem);
|
||||
}
|
||||
|
||||
function closeOther(tabItem?: TabItem) {
|
||||
tabStore.closeOtherTabAction(tabItem || unref(getCurrentTab));
|
||||
isGotoPage(tabItem);
|
||||
}
|
||||
|
||||
function closeCurrent(tabItem?: TabItem) {
|
||||
closeTab(unref(tabItem || unref(getCurrentTab)));
|
||||
}
|
||||
|
||||
function scaleScreen() {
|
||||
const {
|
||||
headerSetting: { show: showHeader },
|
||||
@ -159,7 +159,7 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
});
|
||||
}
|
||||
|
||||
// 处理右键事件
|
||||
// Handle right click event
|
||||
function handleMenuEvent(menu: DropMenu): void {
|
||||
const { event } = menu;
|
||||
|
||||
@ -168,76 +168,74 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
scaleScreen();
|
||||
break;
|
||||
case MenuEventEnum.REFRESH_PAGE:
|
||||
// 刷新页面
|
||||
// refresh page
|
||||
refreshPage();
|
||||
break;
|
||||
// 关闭当前
|
||||
// Close current
|
||||
case MenuEventEnum.CLOSE_CURRENT:
|
||||
closeCurrent();
|
||||
break;
|
||||
// 关闭左侧
|
||||
// Close left
|
||||
case MenuEventEnum.CLOSE_LEFT:
|
||||
closeLeft();
|
||||
break;
|
||||
// 关闭右侧
|
||||
// Close right
|
||||
case MenuEventEnum.CLOSE_RIGHT:
|
||||
closeRight();
|
||||
break;
|
||||
// 关闭其他
|
||||
// Close other
|
||||
case MenuEventEnum.CLOSE_OTHER:
|
||||
closeOther();
|
||||
break;
|
||||
// 关闭其他
|
||||
// Close all
|
||||
case MenuEventEnum.CLOSE_ALL:
|
||||
closeAll();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const { currentRoute, replace } = router;
|
||||
// 当前tab列表
|
||||
const getTabsState = computed(() => {
|
||||
return tabStore.getTabsState;
|
||||
});
|
||||
// Current tab list
|
||||
const getTabsState = computed(() => tabStore.getTabsState);
|
||||
|
||||
const { path } = unref(currentRoute);
|
||||
if (path !== closedTab.path) {
|
||||
// 关闭的不是激活tab
|
||||
// Closed is not the activation tab
|
||||
tabStore.commitCloseTab(closedTab);
|
||||
return;
|
||||
}
|
||||
// 关闭的为激活atb
|
||||
|
||||
// Closed is activated atb
|
||||
let toObj: RouteLocationRaw = {};
|
||||
|
||||
const index = unref(getTabsState).findIndex((item) => item.path === path);
|
||||
|
||||
// 如果当前为最左边tab
|
||||
// If the current is the leftmost tab
|
||||
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) {
|
||||
toObj = PageEnum.BASE_HOME;
|
||||
} else {
|
||||
// 跳转至右边tab
|
||||
// Jump to the right tab
|
||||
const page = unref(getTabsState)[index + 1];
|
||||
const { params, path, query } = page;
|
||||
toObj = {
|
||||
params,
|
||||
path,
|
||||
query,
|
||||
};
|
||||
toObj = getObj(page);
|
||||
}
|
||||
} else {
|
||||
// 跳转至左边tab
|
||||
// Close the current tab
|
||||
const page = unref(getTabsState)[index - 1];
|
||||
const { params, path, query } = page;
|
||||
toObj = {
|
||||
params: params || {},
|
||||
path,
|
||||
query: query || {},
|
||||
};
|
||||
toObj = getObj(page);
|
||||
}
|
||||
const route = (unref(currentRoute) as unknown) as AppRouteRecordRaw;
|
||||
tabStore.commitCloseTab(route);
|
||||
|
@ -203,7 +203,7 @@ export default defineComponent({
|
||||
getMenuFixed,
|
||||
getCollapsed,
|
||||
getShowSearch,
|
||||
getHasDrag,
|
||||
getCanDrag,
|
||||
getTopMenuAlign,
|
||||
getAccordion,
|
||||
getMenuWidth,
|
||||
@ -267,7 +267,7 @@ export default defineComponent({
|
||||
handler: (e) => {
|
||||
baseHandler(HandlerEnum.MENU_HAS_DRAG, e);
|
||||
},
|
||||
def: unref(getHasDrag),
|
||||
def: unref(getCanDrag),
|
||||
disabled: !unref(getShowMenuRef),
|
||||
}),
|
||||
renderSwitchItem('侧边菜单搜索', {
|
||||
|
@ -30,7 +30,7 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
|
||||
};
|
||||
|
||||
case HandlerEnum.MENU_HAS_DRAG:
|
||||
return { menuSetting: { hasDrag: value } };
|
||||
return { menuSetting: { canDrag: value } };
|
||||
|
||||
case HandlerEnum.MENU_ACCORDION:
|
||||
return { menuSetting: { accordion: value } };
|
||||
|
@ -1,7 +1,7 @@
|
||||
@import (reference) '../../../design/index.less';
|
||||
|
||||
.layout-sidebar {
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
|
||||
&.fixed {
|
||||
position: fixed;
|
||||
@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
&: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);
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ export function useTrigger() {
|
||||
* @param dragBarRef
|
||||
*/
|
||||
export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
|
||||
const { getMiniWidthNumber, getCollapsed, setMenuSetting, getHasDrag } = useMenuSetting();
|
||||
const { getMiniWidthNumber, getCollapsed, setMenuSetting, getCanDrag } = useMenuSetting();
|
||||
|
||||
const getDragBarStyle = computed(() => {
|
||||
if (unref(getCollapsed)) {
|
||||
@ -101,7 +101,7 @@ export function useDragLine(siderRef: Ref<any>, dragBarRef: Ref<any>) {
|
||||
function renderDragLine() {
|
||||
return (
|
||||
<div
|
||||
class={[`layout-sidebar__darg-bar`, !unref(getHasDrag) ? 'hide' : '']}
|
||||
class={[`layout-sidebar__darg-bar`, { hide: !unref(getCanDrag) }]}
|
||||
style={unref(getDragBarStyle)}
|
||||
ref={dragBarRef}
|
||||
/>
|
||||
|
@ -83,7 +83,7 @@ const setting: ProjectConfig = {
|
||||
collapsedShowTitle: false,
|
||||
// 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
|
||||
hasDrag: false,
|
||||
canDrag: false,
|
||||
// Whether to show no dom
|
||||
show: true,
|
||||
// Whether to show dom
|
||||
@ -114,6 +114,8 @@ const setting: ProjectConfig = {
|
||||
multiTabsSetting: {
|
||||
// Turn on
|
||||
show: true,
|
||||
// Is it possible to drag and drop sorting tabs
|
||||
canDrag: true,
|
||||
// Turn on quick actions
|
||||
showQuick: true,
|
||||
// Maximum number of tab cache
|
||||
|
@ -175,6 +175,14 @@ class Tab extends VuexModule {
|
||||
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
|
||||
closeMultipleTab({ pathList, nameList }: { pathList: string[]; nameList: string[] }): void {
|
||||
this.tabsState = toRaw(this.tabsState).filter((item) => !pathList.includes(item.fullPath));
|
||||
|
4
src/types/config.d.ts
vendored
4
src/types/config.d.ts
vendored
@ -8,7 +8,7 @@ export interface MenuSetting {
|
||||
fixed: boolean;
|
||||
collapsed: boolean;
|
||||
collapsedShowTitle: boolean;
|
||||
hasDrag: boolean;
|
||||
canDrag: boolean;
|
||||
showSearch: boolean;
|
||||
show: boolean;
|
||||
hidden: boolean;
|
||||
@ -28,7 +28,7 @@ export interface MultiTabsSetting {
|
||||
show: boolean;
|
||||
// 开启快速操作
|
||||
showQuick: boolean;
|
||||
|
||||
canDrag: boolean;
|
||||
// 缓存最大数量
|
||||
max: number;
|
||||
}
|
||||
|
@ -24,6 +24,10 @@ export function isNull(val: unknown): val is 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 {
|
||||
return is(val, 'Number');
|
||||
}
|
||||
|
@ -11,28 +11,18 @@
|
||||
<a-button class="mr-2" @click="closeOther">关闭其他</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="openTab">打开图标界面tab</a-button>
|
||||
</CollapseContainer>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { CollapseContainer } from '/@/components/Container/index';
|
||||
import { PageEnum } from '/@/enums/pageEnum';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
export default defineComponent({
|
||||
name: 'TabsDemo',
|
||||
components: { CollapseContainer },
|
||||
setup() {
|
||||
const {
|
||||
closeAll,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
closeCurrent,
|
||||
refreshPage,
|
||||
addTab,
|
||||
} = useTabs();
|
||||
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTabs();
|
||||
|
||||
return {
|
||||
closeAll,
|
||||
@ -41,9 +31,6 @@
|
||||
closeOther,
|
||||
closeCurrent,
|
||||
refreshPage,
|
||||
openTab: () => {
|
||||
addTab('/feat/icon' as PageEnum, true);
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -1495,6 +1495,11 @@
|
||||
"@types/mime" "*"
|
||||
"@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":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
|
||||
|
Loading…
Reference in New Issue
Block a user