2024-02-27 17:22:27 +08:00
|
|
|
|
<template>
|
|
|
|
|
<div class="tabs-view">
|
|
|
|
|
<Tabs
|
|
|
|
|
:active-key="activeKey"
|
|
|
|
|
hide-add
|
|
|
|
|
type="editable-card"
|
|
|
|
|
class="tabs"
|
|
|
|
|
@change="changePage"
|
|
|
|
|
@edit="editTabItem"
|
|
|
|
|
>
|
|
|
|
|
<Tabs.TabPane v-for="pageItem in tabsList" :key="pageItem.fullPath">
|
|
|
|
|
<template #tab>
|
|
|
|
|
<Dropdown :trigger="['contextmenu']">
|
|
|
|
|
<div style="display: inline-block">
|
|
|
|
|
<TitleI18n :title="pageItem.meta?.title" />
|
|
|
|
|
</div>
|
|
|
|
|
<template #overlay>
|
|
|
|
|
<Menu style="user-select: none">
|
|
|
|
|
<Menu.Item key="1" :disabled="activeKey !== pageItem.fullPath" @click="reloadPage">
|
|
|
|
|
<reload-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.reload') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Item key="2" @click="removeTab(pageItem)">
|
|
|
|
|
<close-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.close') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Divider />
|
|
|
|
|
<Menu.Item key="3" @click="closeLeft(pageItem)">
|
|
|
|
|
<vertical-right-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeLeft') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Item key="4" @click="closeRight(pageItem)">
|
|
|
|
|
<vertical-left-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeRight') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Divider />
|
|
|
|
|
<Menu.Item key="5" @click="closeOther(pageItem)">
|
|
|
|
|
<column-width-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeOther') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Item key="6" @click="closeAll">
|
|
|
|
|
<minus-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeAll') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<template v-if="isDevMode">
|
|
|
|
|
<Menu.Divider />
|
|
|
|
|
<Menu.Item key="7" @click="openPageFile(pageItem)">
|
|
|
|
|
<column-width-outlined />
|
|
|
|
|
打开页面文件
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
</template>
|
|
|
|
|
</Menu>
|
|
|
|
|
</template>
|
|
|
|
|
</Dropdown>
|
|
|
|
|
</template>
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
|
|
|
|
|
<template #rightExtra>
|
|
|
|
|
<Dropdown :trigger="['click']" placement="bottomRight">
|
|
|
|
|
<a class="ant-dropdown-link" @click.prevent>
|
|
|
|
|
<down-outlined :style="{ fontSize: '20px' }" />
|
|
|
|
|
</a>
|
|
|
|
|
<template #overlay>
|
|
|
|
|
<Menu style="user-select: none">
|
|
|
|
|
<Menu.Item key="1" :disabled="activeKey !== route.fullPath" @click="reloadPage">
|
|
|
|
|
<reload-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.reload') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Item key="2" @click="removeTab(route)">
|
|
|
|
|
<close-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.close') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Divider />
|
|
|
|
|
<Menu.Item key="5" @click="closeOther(route)">
|
|
|
|
|
<column-width-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeOther') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Item key="6" @click="closeAll">
|
|
|
|
|
<minus-outlined />
|
|
|
|
|
{{ $t('layout.multipleTab.closeAll') }}
|
|
|
|
|
</Menu.Item>
|
|
|
|
|
<Menu.Divider />
|
|
|
|
|
</Menu>
|
|
|
|
|
</template>
|
|
|
|
|
</Dropdown>
|
|
|
|
|
</template>
|
|
|
|
|
</Tabs>
|
|
|
|
|
<router-view v-slot="{ Component }" class="tabs-view-content">
|
|
|
|
|
<template v-if="Component">
|
|
|
|
|
<transition
|
|
|
|
|
:name="Object.is(route.meta?.transitionName, false) ? '' : 'fade-transform'"
|
|
|
|
|
mode="out-in"
|
|
|
|
|
appear
|
2024-02-28 15:04:31 +08:00
|
|
|
|
:duration="300"
|
2024-02-27 17:22:27 +08:00
|
|
|
|
>
|
|
|
|
|
<keep-alive :include="keepAliveComponents">
|
|
|
|
|
<component :is="Component" :key="route.fullPath" />
|
|
|
|
|
</keep-alive>
|
|
|
|
|
</transition>
|
|
|
|
|
</template>
|
|
|
|
|
</router-view>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, unref, watch } from 'vue';
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
|
|
|
import {
|
|
|
|
|
DownOutlined,
|
|
|
|
|
ReloadOutlined,
|
|
|
|
|
CloseOutlined,
|
|
|
|
|
VerticalRightOutlined,
|
|
|
|
|
VerticalLeftOutlined,
|
|
|
|
|
ColumnWidthOutlined,
|
|
|
|
|
MinusOutlined,
|
|
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
import { isFunction } from 'lodash-es';
|
|
|
|
|
import { Dropdown, Tabs, message, Menu } from 'ant-design-vue';
|
|
|
|
|
import type { RouteLocation } from 'vue-router';
|
|
|
|
|
import { Storage } from '@/utils/Storage';
|
|
|
|
|
import { TABS_ROUTES } from '@/enums/cacheEnum';
|
|
|
|
|
import { useTabsViewStore, blackList } from '@/store/modules/tabsView';
|
|
|
|
|
import { useKeepAliveStore } from '@/store/modules/keepAlive';
|
|
|
|
|
import { REDIRECT_NAME } from '@/router/constant';
|
|
|
|
|
import { TitleI18n } from '@/components/basic/title-i18n';
|
|
|
|
|
import { isDevMode } from '@/constants/env';
|
|
|
|
|
|
|
|
|
|
type RouteItem = Omit<RouteLocation, 'matched' | 'redirectedFrom'>;
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const tabsViewStore = useTabsViewStore();
|
|
|
|
|
const keepAliveStore = useKeepAliveStore();
|
|
|
|
|
|
|
|
|
|
const activeKey = computed(() => tabsViewStore.getCurrentTab?.fullPath);
|
|
|
|
|
|
|
|
|
|
// 标签页列表
|
|
|
|
|
const tabsList = computed(() => tabsViewStore.getTabsList);
|
|
|
|
|
|
|
|
|
|
// 缓存的路由组件列表
|
|
|
|
|
const keepAliveComponents = computed(() => keepAliveStore.list);
|
|
|
|
|
|
|
|
|
|
// 获取简易的路由对象
|
|
|
|
|
const getSimpleRoute = (route): RouteItem => {
|
|
|
|
|
const { fullPath, hash, meta, name, params, path, query } = route;
|
|
|
|
|
return { fullPath, hash, meta, name, params, path, query };
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let routes: RouteItem[] = [];
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const routesStr = Storage.get(TABS_ROUTES) as string | null | undefined;
|
|
|
|
|
routes = routesStr ? JSON.parse(routesStr) : [getSimpleRoute(route)];
|
|
|
|
|
} catch (e) {
|
|
|
|
|
routes = [getSimpleRoute(route)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 初始化标签页
|
|
|
|
|
tabsViewStore.initTabs(routes);
|
|
|
|
|
|
|
|
|
|
// tabsViewMutations.initTabs(routes)
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => route.fullPath,
|
|
|
|
|
() => {
|
|
|
|
|
if (blackList.some((n) => n === route.name)) return;
|
|
|
|
|
// tabsViewMutations.addTabs(getSimpleRoute(route))
|
|
|
|
|
tabsViewStore.addTabs(getSimpleRoute(route));
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 在页面关闭或刷新之前,保存数据
|
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
|
Storage.set(TABS_ROUTES, JSON.stringify([tabsViewStore.getCurrentTab]));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 目标路由是否等于当前路由
|
|
|
|
|
const isCurrentRoute = (route) => {
|
|
|
|
|
return router.currentRoute.value.matched.some((item) => item.name === route.name);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭当前页面
|
|
|
|
|
const removeTab = (route) => {
|
|
|
|
|
if (tabsList.value.length === 1) {
|
|
|
|
|
return message.warning('这已经是最后一页,不能再关闭了!');
|
|
|
|
|
}
|
|
|
|
|
// tabsViewMutations.closeCurrentTabs(route)
|
|
|
|
|
tabsViewStore.closeCurrentTab(route);
|
|
|
|
|
};
|
|
|
|
|
// tabs 编辑(remove || add)
|
|
|
|
|
const editTabItem = (targetKey, action: string) => {
|
|
|
|
|
if (action == 'remove') {
|
|
|
|
|
removeTab(tabsList.value.find((item) => item.fullPath == targetKey));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
// 切换页面
|
|
|
|
|
const changePage = (key) => {
|
|
|
|
|
Object.is(route.fullPath, key) || router.push(key);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 刷新页面
|
|
|
|
|
const reloadPage = () => {
|
|
|
|
|
router.replace({
|
|
|
|
|
name: REDIRECT_NAME,
|
|
|
|
|
params: {
|
|
|
|
|
path: unref(route).fullPath,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭左侧
|
|
|
|
|
const closeLeft = (route) => {
|
|
|
|
|
// tabsViewMutations.closeLeftTabs(route)
|
|
|
|
|
tabsViewStore.closeLeftTabs(route);
|
|
|
|
|
!isCurrentRoute(route) && router.replace(route.fullPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭右侧
|
|
|
|
|
const closeRight = (route) => {
|
|
|
|
|
// tabsViewMutations.closeRightTabs(route)
|
|
|
|
|
tabsViewStore.closeRightTabs(route);
|
|
|
|
|
!isCurrentRoute(route) && router.replace(route.fullPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭其他
|
|
|
|
|
const closeOther = (route) => {
|
|
|
|
|
// tabsViewMutations.closeOtherTabs(route)
|
|
|
|
|
tabsViewStore.closeOtherTabs(route);
|
|
|
|
|
!isCurrentRoute(route) && router.replace(route.fullPath);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 关闭全部
|
|
|
|
|
const closeAll = () => {
|
|
|
|
|
localStorage.removeItem('routes');
|
|
|
|
|
// tabsViewMutations.closeAllTabs()
|
|
|
|
|
tabsViewStore.closeAllTabs();
|
|
|
|
|
router.replace('/');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 打开页面所在的文件(仅在开发环境有效) */
|
|
|
|
|
const openPageFile = async (pageItem) => {
|
|
|
|
|
if (!isDevMode) {
|
|
|
|
|
console.warn('仅在开发环境有效');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const routes = router.getRoutes();
|
|
|
|
|
const target = routes.find((n) => n.name === pageItem.name);
|
|
|
|
|
if (target) {
|
|
|
|
|
const comp = target.components?.default;
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
let __file = comp?.__file as string;
|
|
|
|
|
if (isFunction(comp)) {
|
|
|
|
|
try {
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
const res = await comp();
|
|
|
|
|
__file = res?.default?.__file;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (__file) {
|
|
|
|
|
const filePath = `/__open-in-editor?file=${__file}`;
|
|
|
|
|
fetch(filePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="less" scoped>
|
|
|
|
|
.dark .tabs-view {
|
|
|
|
|
border-top: 1px solid black;
|
|
|
|
|
}
|
2024-04-07 09:29:40 +08:00
|
|
|
|
.ant-dropdown-link{
|
|
|
|
|
color:@primary-color;
|
|
|
|
|
}
|
2024-02-27 17:22:27 +08:00
|
|
|
|
.tabs-view {
|
|
|
|
|
border-top: 1px solid #eee;
|
|
|
|
|
|
|
|
|
|
:deep(.tabs) {
|
|
|
|
|
.ant-tabs-nav {
|
|
|
|
|
@apply bg-white dark:bg-black;
|
|
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 4px 20px 0 10px;
|
|
|
|
|
user-select: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-tabs-tabpane {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-tabs-tab-remove {
|
|
|
|
|
display: flex;
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
|
|
.anticon-close {
|
|
|
|
|
padding-left: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-tabs-tab:not(.ant-tabs-tab-active) {
|
|
|
|
|
.ant-tabs-tab-remove {
|
|
|
|
|
width: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.anticon-close {
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
width: 0;
|
|
|
|
|
transition: width 0.3s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
.anticon-close {
|
|
|
|
|
visibility: visible;
|
|
|
|
|
width: 16px;
|
|
|
|
|
padding-left: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.ant-tabs-tab-remove {
|
|
|
|
|
width: unset;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-view-content {
|
|
|
|
|
/* height: calc(100vh - #{$header-height}); */
|
|
|
|
|
height: calc(100vh - 110px);
|
|
|
|
|
padding: 20px 14px 0;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|