update 优化 页签区增加左右滚动、右侧下拉、单独“刷新”按钮
add 新增 TagsView 全屏功能 add 新增 设置面板增加了“持久化标签页”开关 支持标签页持久化
This commit is contained in:
@@ -52,6 +52,14 @@ function addIframe() {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|
||||||
|
&:fullscreen,
|
||||||
|
&:-webkit-full-screen,
|
||||||
|
&:-moz-full-screen,
|
||||||
|
&:-ms-fullscreen {
|
||||||
|
background: var(--el-bg-color);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main.with-fixed-header {
|
.app-main.with-fixed-header {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<img src="@/assets/images/dark.svg" alt="dark" />
|
<img src="@/assets/images/dark.svg" alt="dark" />
|
||||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false">
|
||||||
<path
|
<path
|
||||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
/>
|
/>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<img src="@/assets/images/light.svg" alt="light" />
|
<img src="@/assets/images/light.svg" alt="light" />
|
||||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||||
<i aria-label="图标: check" class="anticon anticon-check">
|
<i aria-label="图标: check" class="anticon anticon-check">
|
||||||
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
|
<svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false">
|
||||||
<path
|
<path
|
||||||
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"
|
||||||
/>
|
/>
|
||||||
@@ -87,6 +87,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-item">
|
||||||
|
<span>持久化标签页</span>
|
||||||
|
<span class="comp-style">
|
||||||
|
<el-switch v-model="settingsStore.tagsViewPersist" :disabled="!settingsStore.tagsView" class="drawer-switch" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="drawer-item">
|
<div class="drawer-item">
|
||||||
<span>显示页签图标</span>
|
<span>显示页签图标</span>
|
||||||
<span class="comp-style">
|
<span class="comp-style">
|
||||||
@@ -161,19 +168,19 @@ const toggleDark = () => useToggle(isDark);
|
|||||||
|
|
||||||
/** 菜单导航设置 */
|
/** 菜单导航设置 */
|
||||||
watch(
|
watch(
|
||||||
() => navType,
|
navType,
|
||||||
(val: string) => {
|
(val) => {
|
||||||
if (val.value === NavTypeEnum.TOP) {
|
if (val === NavTypeEnum.TOP) {
|
||||||
appStore.toggleSideBarHide(true);
|
appStore.toggleSideBarHide(true);
|
||||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||||
} else if (val.value === NavTypeEnum.LEFT) {
|
} else if (val === NavTypeEnum.LEFT) {
|
||||||
appStore.toggleSideBarHide(false);
|
appStore.toggleSideBarHide(false);
|
||||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||||
} else if (val.value === NavTypeEnum.MIX) {
|
} else if (val === NavTypeEnum.MIX) {
|
||||||
appStore.toggleSideBarHide(false);
|
appStore.toggleSideBarHide(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true, deep: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNavType = (val: NavTypeEnum) => {
|
const handleNavType = (val: NavTypeEnum) => {
|
||||||
@@ -202,7 +209,11 @@ const handleTheme = (val: string) => {
|
|||||||
const saveSetting = () => {
|
const saveSetting = () => {
|
||||||
proxy?.$modal.loading('正在保存到本地,请稍候...');
|
proxy?.$modal.loading('正在保存到本地,请稍候...');
|
||||||
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
|
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
|
||||||
|
if (!storeSettings.value.tagsViewPersist) {
|
||||||
|
localStorage.removeItem('tags-view-visited');
|
||||||
|
}
|
||||||
settings.value.tagsView = storeSettings.value.tagsView;
|
settings.value.tagsView = storeSettings.value.tagsView;
|
||||||
|
settings.value.tagsViewPersist = storeSettings.value.tagsViewPersist;
|
||||||
settings.value.tagsIcon = storeSettings.value.tagsIcon;
|
settings.value.tagsIcon = storeSettings.value.tagsIcon;
|
||||||
settings.value.fixedHeader = storeSettings.value.fixedHeader;
|
settings.value.fixedHeader = storeSettings.value.fixedHeader;
|
||||||
settings.value.sidebarLogo = storeSettings.value.sidebarLogo;
|
settings.value.sidebarLogo = storeSettings.value.sidebarLogo;
|
||||||
@@ -216,6 +227,7 @@ const saveSetting = () => {
|
|||||||
};
|
};
|
||||||
const resetSetting = () => {
|
const resetSetting = () => {
|
||||||
proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
|
proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
|
||||||
|
localStorage.removeItem('tags-view-visited');
|
||||||
useStorage<any>('layout-setting', null).value = null;
|
useStorage<any>('layout-setting', null).value = null;
|
||||||
setTimeout('window.location.reload()', 1000);
|
setTimeout('window.location.reload()', 1000);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,84 +5,127 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouteLocationNormalized } from 'vue-router';
|
import type { RouteLocationNormalized } from 'vue-router';
|
||||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
|
||||||
|
|
||||||
const tagAndTagSpacing = ref(4);
|
const tagAndTagSpacing = 4;
|
||||||
|
|
||||||
|
const emits = defineEmits(['scroll', 'updateArrows']);
|
||||||
const scrollContainerRef = ref<ElScrollbarInstance>();
|
const scrollContainerRef = ref<ElScrollbarInstance>();
|
||||||
const scrollWrapper = computed(() => scrollContainerRef.value?.$refs.wrapRef);
|
|
||||||
|
const getScrollWrapper = (): HTMLElement | null => {
|
||||||
|
const scrollbar = scrollContainerRef.value as any;
|
||||||
|
return scrollbar?.wrapRef || scrollbar?.$refs?.wrapRef || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emitScroll = () => {
|
||||||
|
emits('scroll');
|
||||||
|
emits('updateArrows');
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
scrollWrapper.value?.addEventListener('scroll', emitScroll, true);
|
getScrollWrapper()?.addEventListener('scroll', emitScroll, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
scrollWrapper.value?.removeEventListener('scroll', emitScroll);
|
getScrollWrapper()?.removeEventListener('scroll', emitScroll);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const smoothScrollTo = (target: number) => {
|
||||||
|
const scrollWrapper = getScrollWrapper();
|
||||||
|
if (!scrollWrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollWrapper.scrollTo({ left: target, behavior: 'smooth' });
|
||||||
|
setTimeout(() => {
|
||||||
|
emits('updateArrows');
|
||||||
|
}, 350);
|
||||||
|
};
|
||||||
|
|
||||||
const handleScroll = (e: WheelEvent) => {
|
const handleScroll = (e: WheelEvent) => {
|
||||||
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
const eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||||
const $scrollWrapper = scrollWrapper.value;
|
const scrollWrapper = getScrollWrapper();
|
||||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
|
if (!scrollWrapper) {
|
||||||
};
|
return;
|
||||||
const emits = defineEmits(['scroll']);
|
}
|
||||||
const emitScroll = () => {
|
|
||||||
emits('scroll');
|
|
||||||
};
|
|
||||||
|
|
||||||
const tagsViewStore = useTagsViewStore();
|
scrollWrapper.scrollLeft += eventDelta / 4;
|
||||||
const visitedViews = computed(() => tagsViewStore.visitedViews);
|
emits('updateArrows');
|
||||||
|
};
|
||||||
|
|
||||||
const moveToTarget = (currentTag: RouteLocationNormalized) => {
|
const moveToTarget = (currentTag: RouteLocationNormalized) => {
|
||||||
const $container = scrollContainerRef.value?.$el;
|
const container = scrollContainerRef.value?.$el as HTMLElement | undefined;
|
||||||
const $containerWidth = $container.offsetWidth;
|
const scrollWrapper = getScrollWrapper();
|
||||||
const $scrollWrapper = scrollWrapper.value;
|
if (!container || !scrollWrapper) {
|
||||||
|
return;
|
||||||
let firstTag = null;
|
|
||||||
let lastTag = null;
|
|
||||||
|
|
||||||
// find first tag and last tag
|
|
||||||
if (visitedViews.value.length > 0) {
|
|
||||||
firstTag = visitedViews.value[0];
|
|
||||||
lastTag = visitedViews.value[visitedViews.value.length - 1];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firstTag === currentTag) {
|
const containerWidth = container.offsetWidth;
|
||||||
$scrollWrapper.scrollLeft = 0;
|
const tagKey = currentTag.fullPath || currentTag.path;
|
||||||
} else if (lastTag === currentTag) {
|
const tagListDom = Array.from(document.querySelectorAll('.tags-view-item')) as HTMLElement[];
|
||||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
const currentIndex = tagListDom.findIndex((item) => item.dataset.tagKey === tagKey);
|
||||||
} else {
|
if (currentIndex === -1) {
|
||||||
const tagListDom: any = document.getElementsByClassName('tags-view-item');
|
return;
|
||||||
const currentIndex = visitedViews.value.findIndex((item) => item === currentTag);
|
|
||||||
let prevTag = null;
|
|
||||||
let nextTag = null;
|
|
||||||
|
|
||||||
for (const k in tagListDom) {
|
|
||||||
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
|
|
||||||
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
|
|
||||||
prevTag = tagListDom[k];
|
|
||||||
}
|
|
||||||
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
|
|
||||||
nextTag = tagListDom[k];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// the tag's offsetLeft after of nextTag
|
|
||||||
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value;
|
|
||||||
|
|
||||||
// the tag's offsetLeft before of prevTag
|
|
||||||
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value;
|
|
||||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
|
||||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth;
|
|
||||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
|
||||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentElement = tagListDom[currentIndex];
|
||||||
|
const firstTag = tagListDom[0];
|
||||||
|
const lastTag = tagListDom[tagListDom.length - 1];
|
||||||
|
|
||||||
|
if (currentElement === firstTag) {
|
||||||
|
smoothScrollTo(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentElement === lastTag) {
|
||||||
|
smoothScrollTo(scrollWrapper.scrollWidth - containerWidth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevTag = tagListDom[currentIndex - 1];
|
||||||
|
const nextTag = tagListDom[currentIndex + 1];
|
||||||
|
if (!prevTag || !nextTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing;
|
||||||
|
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing;
|
||||||
|
|
||||||
|
if (afterNextTagOffsetLeft > scrollWrapper.scrollLeft + containerWidth) {
|
||||||
|
smoothScrollTo(afterNextTagOffsetLeft - containerWidth);
|
||||||
|
} else if (beforePrevTagOffsetLeft < scrollWrapper.scrollLeft) {
|
||||||
|
smoothScrollTo(beforePrevTagOffsetLeft);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToStart = () => {
|
||||||
|
smoothScrollTo(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToEnd = () => {
|
||||||
|
const scrollWrapper = getScrollWrapper();
|
||||||
|
if (!scrollWrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
smoothScrollTo(scrollWrapper.scrollWidth - scrollWrapper.clientWidth);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getScrollState = () => {
|
||||||
|
const scrollWrapper = getScrollWrapper();
|
||||||
|
if (!scrollWrapper) {
|
||||||
|
return { canLeft: false, canRight: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canLeft: scrollWrapper.scrollLeft > 0,
|
||||||
|
canRight: scrollWrapper.scrollLeft < scrollWrapper.scrollWidth - scrollWrapper.clientWidth - 1
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
moveToTarget
|
moveToTarget,
|
||||||
|
scrollToStart,
|
||||||
|
scrollToEnd,
|
||||||
|
getScrollState
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -92,11 +135,15 @@ defineExpose({
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
:deep(.el-scrollbar__bar) {
|
:deep(.el-scrollbar__bar) {
|
||||||
bottom: 0px;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-scrollbar__wrap) {
|
:deep(.el-scrollbar__wrap) {
|
||||||
height: 49px;
|
height: 34px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,63 +1,115 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="tags-view-container" class="tags-view-container">
|
<div id="tags-view-container" class="tags-view-container">
|
||||||
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
|
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
|
||||||
|
<el-icon><ArrowLeft /></el-icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll" @update-arrows="updateArrowState">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tag in visitedViews"
|
v-for="tag in visitedViews"
|
||||||
:key="tag.path"
|
:key="tag.fullPath || tag.path"
|
||||||
:data-path="tag.path"
|
:data-tag-key="tag.fullPath || tag.path"
|
||||||
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
|
:class="{ active: isActive(tag), 'has-icon': tagsIcon }"
|
||||||
:to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }"
|
:to="tag.fullPath || tag.path || '/'"
|
||||||
class="tags-view-item"
|
class="tags-view-item"
|
||||||
:style="activeStyle(tag)"
|
:style="activeStyle(tag)"
|
||||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : undefined"
|
||||||
@contextmenu.prevent="openMenu(tag, $event)"
|
@contextmenu.prevent="openMenu(tag, $event)"
|
||||||
>
|
>
|
||||||
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon"/>
|
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
|
||||||
<span class="tags-view-item-title">{{ tag.title }}</span>
|
<span class="tags-view-item-title">{{ tag.title || tag.meta?.title }}</span>
|
||||||
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
||||||
<close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" />
|
<Close class="el-icon-close" style="width: 1em; height: 1em; vertical-align: middle" />
|
||||||
</span>
|
</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</scroll-pane>
|
</scroll-pane>
|
||||||
|
|
||||||
|
<span class="tags-nav-btn tags-nav-btn--right" :class="{ disabled: !canScrollRight }" @click="scrollRight">
|
||||||
|
<el-icon><ArrowRight /></el-icon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
|
||||||
|
<span class="tags-action-btn">
|
||||||
|
<el-icon><ArrowDown /></el-icon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu class="tags-dropdown-menu">
|
||||||
|
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close">关闭当前</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="closeOthers">关闭其他</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="closeLeft" :disabled="isFirstView()">关闭左侧</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="closeRight" :disabled="isLastView()">关闭右侧</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="closeAll">全部关闭</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="fullscreen" divided>全屏显示</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
|
<span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)">
|
||||||
|
<el-icon><RefreshRight /></el-icon>
|
||||||
|
<span>刷新</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||||
<li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> 刷新页面</li>
|
<li @click="refreshSelectedTag(selectedTag)">
|
||||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> 关闭当前</li>
|
<RefreshRight style="width: 1em; height: 1em" /> 刷新页面
|
||||||
<li @click="closeOthersTags"><circle-close style="width: 1em; height: 1em" /> 关闭其他</li>
|
</li>
|
||||||
<li v-if="!isFirstView()" @click="closeLeftTags"><back style="width: 1em; height: 1em" /> 关闭左侧</li>
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||||
<li v-if="!isLastView()" @click="closeRightTags"><right style="width: 1em; height: 1em" /> 关闭右侧</li>
|
<Close style="width: 1em; height: 1em" /> 关闭当前
|
||||||
<li @click="closeAllTags(selectedTag)"><circle-close style="width: 1em; height: 1em" /> 全部关闭</li>
|
</li>
|
||||||
|
<li @click="closeOthersTags">
|
||||||
|
<CircleClose style="width: 1em; height: 1em" /> 关闭其他
|
||||||
|
</li>
|
||||||
|
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||||
|
<Back style="width: 1em; height: 1em" /> 关闭左侧
|
||||||
|
</li>
|
||||||
|
<li v-if="!isLastView()" @click="closeRightTags">
|
||||||
|
<Right style="width: 1em; height: 1em" /> 关闭右侧
|
||||||
|
</li>
|
||||||
|
<li @click="closeAllTags(selectedTag)">
|
||||||
|
<CircleClose style="width: 1em; height: 1em" /> 全部关闭
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ArrowDown, ArrowLeft, ArrowRight, Back, CircleClose, Close, RefreshRight, Right } from '@element-plus/icons-vue';
|
||||||
import ScrollPane from './ScrollPane.vue';
|
import ScrollPane from './ScrollPane.vue';
|
||||||
import { getNormalPath } from '@/utils/ruoyi';
|
import { getNormalPath } from '@/utils/ruoyi';
|
||||||
import { useSettingsStore } from '@/store/modules/settings';
|
import { useSettingsStore } from '@/store/modules/settings';
|
||||||
import { usePermissionStore } from '@/store/modules/permission';
|
import { usePermissionStore } from '@/store/modules/permission';
|
||||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||||
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
|
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const top = ref(0);
|
const top = ref(0);
|
||||||
const left = ref(0);
|
const left = ref(0);
|
||||||
const selectedTag = ref<RouteLocationNormalized>();
|
const selectedTag = ref<RouteLocationNormalized>();
|
||||||
const affixTags = ref<RouteLocationNormalized[]>([]);
|
const affixTags = ref<RouteLocationNormalized[]>([]);
|
||||||
|
const canScrollLeft = ref(false);
|
||||||
|
const canScrollRight = ref(false);
|
||||||
|
const isFullscreen = ref(false);
|
||||||
const scrollPaneRef = ref<InstanceType<typeof ScrollPane>>();
|
const scrollPaneRef = ref<InstanceType<typeof ScrollPane>>();
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
const permissionStore = usePermissionStore();
|
||||||
|
const tagsViewStore = useTagsViewStore();
|
||||||
|
|
||||||
const visitedViews = computed(() => useTagsViewStore().getVisitedViews());
|
const visitedViews = computed(() => tagsViewStore.getVisitedViews());
|
||||||
const routes = computed(() => usePermissionStore().getRoutes());
|
const routes = computed(() => permissionStore.getRoutes());
|
||||||
const theme = computed(() => useSettingsStore().theme);
|
const tagsIcon = computed(() => settingsStore.tagsIcon);
|
||||||
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
|
const selectedDropdownTag = computed<RouteLocationNormalized | undefined>(() => {
|
||||||
|
return visitedViews.value.find((tag) => isActive(tag)) || selectedTag.value;
|
||||||
|
});
|
||||||
|
|
||||||
watch(route, () => {
|
watch(route, () => {
|
||||||
addTags();
|
addTags();
|
||||||
moveToCurrentTag();
|
moveToCurrentTag();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(visible, (value) => {
|
watch(visible, (value) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
document.body.addEventListener('click', closeMenu);
|
document.body.addEventListener('click', closeMenu);
|
||||||
@@ -66,53 +118,80 @@ watch(visible, (value) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const isActive = (r: RouteLocationNormalized): boolean => {
|
watch(
|
||||||
return r.path === route.path;
|
visitedViews,
|
||||||
|
() => {
|
||||||
|
nextTick(() => {
|
||||||
|
updateArrowState();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const isActive = (currentRoute: RouteLocationNormalized): boolean => {
|
||||||
|
return currentRoute.path === route.path;
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeStyle = (tag: RouteLocationNormalized) => {
|
const activeStyle = (tag: RouteLocationNormalized) => {
|
||||||
if (!isActive(tag)) return {};
|
if (!isActive(tag)) return {};
|
||||||
return {
|
return {
|
||||||
'background-color': 'var(--tags-view-active-bg)',
|
backgroundColor: 'var(--tags-view-active-bg)',
|
||||||
'border-color': 'var(--tags-view-active-border-color)'
|
borderColor: 'var(--tags-view-active-border-color)'
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const isAffix = (tag: RouteLocationNormalized) => {
|
|
||||||
return tag?.meta && tag?.meta?.affix;
|
|
||||||
};
|
|
||||||
const isFirstView = () => {
|
|
||||||
try {
|
|
||||||
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const isLastView = () => {
|
|
||||||
try {
|
|
||||||
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath;
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
|
||||||
let tags: RouteLocationNormalized[] = [];
|
|
||||||
|
|
||||||
routes.forEach((route) => {
|
const isAffix = (tag?: RouteLocationNormalized) => {
|
||||||
if (route.meta && route.meta.affix) {
|
return !!tag?.meta?.affix;
|
||||||
const tagPath = getNormalPath(basePath + '/' + route.path);
|
};
|
||||||
|
|
||||||
|
const getOperateTag = () => {
|
||||||
|
return selectedTag.value?.fullPath ? selectedTag.value : selectedDropdownTag.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFirstView = () => {
|
||||||
|
const tag = getOperateTag();
|
||||||
|
if (!tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return tag.fullPath === '/index' || tag.fullPath === visitedViews.value[1]?.fullPath;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLastView = () => {
|
||||||
|
const tag = getOperateTag();
|
||||||
|
if (!tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return tag.fullPath === visitedViews.value[visitedViews.value.length - 1]?.fullPath;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterAffixTags = (routeList: RouteRecordRaw[], basePath = '') => {
|
||||||
|
let tags: RouteLocationNormalized[] = [];
|
||||||
|
routeList.forEach((item) => {
|
||||||
|
if (item.meta?.affix) {
|
||||||
|
const tagPath = getNormalPath(basePath + '/' + item.path);
|
||||||
tags.push({
|
tags.push({
|
||||||
hash: '',
|
hash: '',
|
||||||
matched: [],
|
matched: [],
|
||||||
params: undefined,
|
params: {},
|
||||||
query: undefined,
|
query: {},
|
||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
fullPath: tagPath,
|
fullPath: tagPath,
|
||||||
path: tagPath,
|
path: tagPath,
|
||||||
name: route.name as string,
|
name: item.name as string,
|
||||||
meta: { ...route.meta }
|
meta: { ...item.meta },
|
||||||
});
|
title: item.meta?.title || 'no-name'
|
||||||
|
} as RouteLocationNormalized);
|
||||||
}
|
}
|
||||||
if (route.children) {
|
if (item.children) {
|
||||||
const tempTags = filterAffixTags(route.children, route.path);
|
const tempTags = filterAffixTags(item.children, item.path);
|
||||||
if (tempTags.length >= 1) {
|
if (tempTags.length >= 1) {
|
||||||
tags = [...tags, ...tempTags];
|
tags = [...tags, ...tempTags];
|
||||||
}
|
}
|
||||||
@@ -120,135 +199,283 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
|||||||
});
|
});
|
||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initTags = () => {
|
const initTags = () => {
|
||||||
const res = filterAffixTags(routes.value);
|
if (settingsStore.tagsViewPersist) {
|
||||||
affixTags.value = res;
|
tagsViewStore.loadPersistedViews();
|
||||||
for (const tag of res) {
|
}
|
||||||
// Must have tag name
|
const tags = filterAffixTags(routes.value);
|
||||||
|
affixTags.value = tags;
|
||||||
|
for (const tag of tags) {
|
||||||
if (tag.name) {
|
if (tag.name) {
|
||||||
useTagsViewStore().addVisitedView(tag);
|
tagsViewStore.addAffixView(tag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTags = () => {
|
const addTags = () => {
|
||||||
const { name } = route;
|
if (typeof route.query.title === 'string') {
|
||||||
if (route.query.title) {
|
route.meta.title = route.query.title;
|
||||||
route.meta.title = route.query.title as string;
|
|
||||||
}
|
}
|
||||||
if (name) {
|
if (route.name) {
|
||||||
useTagsViewStore().addView(route as any);
|
tagsViewStore.addView(route as RouteLocationNormalized);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveToCurrentTag = () => {
|
const moveToCurrentTag = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
for (const r of visitedViews.value) {
|
for (const item of visitedViews.value) {
|
||||||
if (r.path === route.path) {
|
if (item.path === route.path) {
|
||||||
scrollPaneRef.value?.moveToTarget(r);
|
scrollPaneRef.value?.moveToTarget(item);
|
||||||
// when query is different then update
|
if (item.fullPath !== route.fullPath) {
|
||||||
if (r.fullPath !== route.fullPath) {
|
tagsViewStore.updateVisitedView(route as RouteLocationNormalized);
|
||||||
useTagsViewStore().updateVisitedView(route);
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const refreshSelectedTag = (view: RouteLocationNormalized) => {
|
|
||||||
|
const refreshSelectedTag = (view?: RouteLocationNormalized) => {
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
proxy?.$tab.refreshPage(view);
|
proxy?.$tab.refreshPage(view);
|
||||||
if (route.meta.link) {
|
if (route.meta.link) {
|
||||||
useTagsViewStore().delIframeView(route);
|
tagsViewStore.delIframeView(route as RouteLocationNormalized);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const closeSelectedTag = (view: RouteLocationNormalized) => {
|
|
||||||
proxy?.$tab.closePage(view).then(({ visitedViews }: any) => {
|
const closeSelectedTag = (view?: RouteLocationNormalized) => {
|
||||||
|
if (!view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proxy?.$tab.closePage(view).then(({ visitedViews }: { visitedViews: RouteLocationNormalized[] }) => {
|
||||||
if (isActive(view)) {
|
if (isActive(view)) {
|
||||||
toLastView(visitedViews, view);
|
toLastView(visitedViews, view);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeRightTags = () => {
|
const closeRightTags = () => {
|
||||||
proxy?.$tab.closeRightPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
|
const tag = getOperateTag();
|
||||||
if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
|
if (!tag) {
|
||||||
toLastView(visitedViews);
|
return;
|
||||||
|
}
|
||||||
|
proxy?.$tab.closeRightPage(tag).then((views: RouteLocationNormalized[]) => {
|
||||||
|
if (!views.find((item) => item.fullPath === route.fullPath)) {
|
||||||
|
toLastView(views);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeLeftTags = () => {
|
const closeLeftTags = () => {
|
||||||
proxy?.$tab.closeLeftPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
|
const tag = getOperateTag();
|
||||||
if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
|
if (!tag) {
|
||||||
toLastView(visitedViews);
|
return;
|
||||||
|
}
|
||||||
|
proxy?.$tab.closeLeftPage(tag).then((views: RouteLocationNormalized[]) => {
|
||||||
|
if (!views.find((item) => item.fullPath === route.fullPath)) {
|
||||||
|
toLastView(views);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeOthersTags = () => {
|
const closeOthersTags = () => {
|
||||||
router.push(selectedTag.value).catch(() => {});
|
const tag = getOperateTag();
|
||||||
proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
|
if (!tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(tag.fullPath || tag.path || '/').catch(() => {});
|
||||||
|
proxy?.$tab.closeOtherPage(tag).then(() => {
|
||||||
moveToCurrentTag();
|
moveToCurrentTag();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const closeAllTags = (view: RouteLocationNormalized) => {
|
|
||||||
proxy?.$tab.closeAllPage().then(({ visitedViews }) => {
|
const closeAllTags = (view?: RouteLocationNormalized) => {
|
||||||
|
proxy?.$tab.closeAllPage().then(({ visitedViews: views }: { visitedViews: RouteLocationNormalized[] }) => {
|
||||||
if (affixTags.value.some((tag) => tag.path === route.path)) {
|
if (affixTags.value.some((tag) => tag.path === route.path)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
toLastView(visitedViews, view);
|
toLastView(views, view);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const toLastView = (visitedViews: RouteLocationNormalized[], view?: RouteLocationNormalized) => {
|
|
||||||
const latestView = visitedViews.slice(-1)[0];
|
const toLastView = (views: RouteLocationNormalized[], view?: RouteLocationNormalized) => {
|
||||||
if (latestView) {
|
const latestView = views.slice(-1)[0];
|
||||||
router.push(latestView.fullPath as string);
|
if (latestView?.fullPath) {
|
||||||
|
router.push(latestView.fullPath);
|
||||||
|
} else if (view?.name === 'Dashboard' && view.fullPath) {
|
||||||
|
router.replace({ path: '/redirect' + view.fullPath });
|
||||||
} else {
|
} else {
|
||||||
// now the default is to redirect to the home page if there is no tags-view,
|
router.push('/');
|
||||||
// you can adjust it according to your needs.
|
|
||||||
if (view?.name === 'Dashboard') {
|
|
||||||
// to reload home page
|
|
||||||
router.replace({ path: '/redirect' + view?.fullPath });
|
|
||||||
} else {
|
|
||||||
router.push('/');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const openMenu = (tag: RouteLocationNormalized, e: MouseEvent) => {
|
|
||||||
const menuMinWidth = 105;
|
|
||||||
const offsetLeft = proxy?.$el.getBoundingClientRect().left; // container margin left
|
|
||||||
const offsetWidth = proxy?.$el.offsetWidth; // container width
|
|
||||||
const maxLeft = offsetWidth - menuMinWidth; // left boundary
|
|
||||||
const l = e.clientX - offsetLeft + 15; // 15: margin right
|
|
||||||
|
|
||||||
if (l > maxLeft) {
|
const scrollLeft = () => {
|
||||||
left.value = maxLeft;
|
if (!canScrollLeft.value) {
|
||||||
} else {
|
return;
|
||||||
left.value = l;
|
|
||||||
}
|
}
|
||||||
|
scrollPaneRef.value?.scrollToStart();
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollRight = () => {
|
||||||
|
if (!canScrollRight.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scrollPaneRef.value?.scrollToEnd();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateArrowState = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
const state = scrollPaneRef.value?.getScrollState();
|
||||||
|
canScrollLeft.value = !!state?.canLeft;
|
||||||
|
canScrollRight.value = !!state?.canRight;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFullscreen = async () => {
|
||||||
|
const appMain = document.querySelector('.app-main') as HTMLElement | null;
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
await appMain?.requestFullscreen?.();
|
||||||
|
} else {
|
||||||
|
await document.exitFullscreen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFullscreenChange = () => {
|
||||||
|
isFullscreen.value = !!document.fullscreenElement;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDropdownCommand = (command: string) => {
|
||||||
|
const tag = selectedDropdownTag.value;
|
||||||
|
if (!tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedTag.value = tag;
|
||||||
|
switch (command) {
|
||||||
|
case 'fullscreen':
|
||||||
|
toggleFullscreen();
|
||||||
|
break;
|
||||||
|
case 'close':
|
||||||
|
closeSelectedTag(tag);
|
||||||
|
break;
|
||||||
|
case 'closeOthers':
|
||||||
|
closeOthersTags();
|
||||||
|
break;
|
||||||
|
case 'closeLeft':
|
||||||
|
closeLeftTags();
|
||||||
|
break;
|
||||||
|
case 'closeRight':
|
||||||
|
closeRightTags();
|
||||||
|
break;
|
||||||
|
case 'closeAll':
|
||||||
|
closeAllTags(tag);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openMenu = (tag: RouteLocationNormalized, e: MouseEvent) => {
|
||||||
|
left.value = e.clientX;
|
||||||
top.value = e.clientY;
|
top.value = e.clientY;
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
selectedTag.value = tag;
|
selectedTag.value = tag;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
visible.value = false;
|
visible.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
|
updateArrowState();
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initTags();
|
initTags();
|
||||||
addTags();
|
addTags();
|
||||||
|
updateArrowState();
|
||||||
|
window.addEventListener('resize', updateArrowState);
|
||||||
|
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('resize', updateArrowState);
|
||||||
|
document.removeEventListener('fullscreenchange', onFullscreenChange);
|
||||||
|
document.body.removeEventListener('click', closeMenu);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.tags-view-container {
|
.tags-view-container {
|
||||||
height: 34px;
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
height: 38px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--el-bg-color);
|
background-color: var(--el-bg-color);
|
||||||
border: 1px solid var(--el-border-color-light);
|
border: 1px solid var(--el-border-color-light);
|
||||||
border-radius: var(--app-radius-md);
|
border-radius: var(--app-radius-md);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
|
||||||
|
$btn-width: 26px;
|
||||||
|
$btn-color: #71717a;
|
||||||
|
$btn-hover-bg: var(--el-fill-color-light);
|
||||||
|
$btn-hover-color: var(--el-text-color-primary);
|
||||||
|
$btn-disabled-color: #c0c4cc;
|
||||||
|
|
||||||
|
.tags-nav-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: $btn-width;
|
||||||
|
height: 26px;
|
||||||
|
margin-top: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $btn-color;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: var(--app-radius-md);
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover:not(.disabled) {
|
||||||
|
background: $btn-hover-bg;
|
||||||
|
color: $btn-hover-color;
|
||||||
|
border-color: var(--el-color-primary-light-5);
|
||||||
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
color: $btn-disabled-color;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-nav-btn--left {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-nav-btn--right {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
@@ -261,23 +488,32 @@ onMounted(() => {
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
border-radius: var(--app-radius-md);
|
border-radius: var(--app-radius-md);
|
||||||
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease, color 0.2s ease;
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
border-color: var(--el-color-primary-light-5);
|
border-color: var(--el-color-primary-light-5);
|
||||||
box-shadow: var(--app-shadow-sm);
|
box-shadow: var(--app-shadow-sm);
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
margin-left: 15px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
margin-right: 15px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background-color: #42b983;
|
background-color: #42b983;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #42b983;
|
border-color: #42b983;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
background: #fff;
|
background: #fff;
|
||||||
@@ -291,28 +527,78 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-view-item.active.has-icon::before {
|
.tags-view-item.active.has-icon::before {
|
||||||
content: none !important;
|
content: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags-view-item-title {
|
.tags-view-item-title {
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-right: 3px;
|
margin-right: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-action-dropdown {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-action-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: $btn-width;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: $btn-color;
|
||||||
|
user-select: none;
|
||||||
|
background-color: var(--el-bg-color);
|
||||||
|
border: 1px solid var(--el-border-color-light);
|
||||||
|
border-radius: var(--app-radius-md);
|
||||||
|
transition:
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $btn-hover-bg;
|
||||||
|
color: $btn-hover-color;
|
||||||
|
border-color: var(--el-color-primary-light-5);
|
||||||
|
box-shadow: var(--app-shadow-sm);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-refresh-btn {
|
||||||
|
width: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.contextmenu {
|
.contextmenu {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: var(--el-bg-color);
|
background: var(--el-bg-color);
|
||||||
z-index: 3000;
|
z-index: 3000;
|
||||||
position: absolute;
|
position: fixed;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
padding: 5px 0;
|
padding: 5px 0;
|
||||||
border-radius: var(--app-radius-md);
|
border-radius: var(--app-radius-md);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
box-shadow: var(--app-shadow-md);
|
box-shadow: var(--app-shadow-md);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 7px 16px;
|
padding: 7px 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
}
|
}
|
||||||
@@ -322,7 +608,6 @@ onMounted(() => {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
//reset element css of el-icon-close
|
|
||||||
.tags-view-wrapper {
|
.tags-view-wrapper {
|
||||||
.tags-view-item {
|
.tags-view-item {
|
||||||
.el-icon-close {
|
.el-icon-close {
|
||||||
@@ -333,11 +618,13 @@ onMounted(() => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
transform-origin: 100% 50%;
|
transform-origin: 100% 50%;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
transform: scale(0.6);
|
transform: scale(0.6);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: -3px;
|
vertical-align: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #b4bccc;
|
background-color: #b4bccc;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ const setting: DefaultSettings = {
|
|||||||
*/
|
*/
|
||||||
tagsView: true,
|
tagsView: true,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持久化标签页
|
||||||
|
*/
|
||||||
|
tagsViewPersist: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示页签图标
|
* 显示页签图标
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
|||||||
|
|
||||||
export const useSettingsStore = defineStore('setting', () => {
|
export const useSettingsStore = defineStore('setting', () => {
|
||||||
const storageSetting = useStorage<LayoutSetting>('layout-setting', {
|
const storageSetting = useStorage<LayoutSetting>('layout-setting', {
|
||||||
topNav: defaultSettings.topNav,
|
|
||||||
tagsView: defaultSettings.tagsView,
|
tagsView: defaultSettings.tagsView,
|
||||||
|
tagsViewPersist: defaultSettings.tagsViewPersist,
|
||||||
tagsIcon: defaultSettings.tagsIcon,
|
tagsIcon: defaultSettings.tagsIcon,
|
||||||
fixedHeader: defaultSettings.fixedHeader,
|
fixedHeader: defaultSettings.fixedHeader,
|
||||||
sidebarLogo: defaultSettings.sidebarLogo,
|
sidebarLogo: defaultSettings.sidebarLogo,
|
||||||
@@ -22,6 +22,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
||||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||||
const tagsView = ref<boolean>(storageSetting.value.tagsView);
|
const tagsView = ref<boolean>(storageSetting.value.tagsView);
|
||||||
|
const tagsViewPersist = ref<boolean>(storageSetting.value.tagsViewPersist);
|
||||||
const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
|
const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
|
||||||
const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
|
const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
|
||||||
const sidebarLogo = ref<boolean>(storageSetting.value.sidebarLogo);
|
const sidebarLogo = ref<boolean>(storageSetting.value.sidebarLogo);
|
||||||
@@ -40,6 +41,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
|||||||
sideTheme,
|
sideTheme,
|
||||||
showSettings,
|
showSettings,
|
||||||
tagsView,
|
tagsView,
|
||||||
|
tagsViewPersist,
|
||||||
tagsIcon,
|
tagsIcon,
|
||||||
fixedHeader,
|
fixedHeader,
|
||||||
sidebarLogo,
|
sidebarLogo,
|
||||||
|
|||||||
+134
-28
@@ -1,17 +1,80 @@
|
|||||||
import { RouteLocationNormalized } from 'vue-router';
|
import type { LocationQuery, RouteLocationNormalized, RouteMeta } from 'vue-router';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useSettingsStore } from './settings';
|
||||||
|
|
||||||
|
const PERSIST_KEY = 'tags-view-visited';
|
||||||
|
|
||||||
|
type PersistedTagView = {
|
||||||
|
path: string;
|
||||||
|
fullPath?: string;
|
||||||
|
name?: string | symbol | null;
|
||||||
|
title?: string;
|
||||||
|
query?: LocationQuery;
|
||||||
|
meta?: RouteMeta;
|
||||||
|
};
|
||||||
|
|
||||||
|
type TagView = RouteLocationNormalized & {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isPersistEnabled = () => {
|
||||||
|
return useSettingsStore().tagsViewPersist;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeVisitedView = (view: RouteLocationNormalized): TagView => {
|
||||||
|
return Object.assign({}, view, {
|
||||||
|
title: view.meta?.title || 'no-name'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveVisitedViews = (views: any[]) => {
|
||||||
|
if (!isPersistEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: PersistedTagView[] = (views as TagView[])
|
||||||
|
.filter((view) => !view.meta?.affix)
|
||||||
|
.map((view) => ({
|
||||||
|
path: view.path,
|
||||||
|
fullPath: view.fullPath,
|
||||||
|
name: view.name,
|
||||||
|
title: (view as any).title || view.meta?.title || 'no-name',
|
||||||
|
query: view.query,
|
||||||
|
meta: view.meta
|
||||||
|
}));
|
||||||
|
|
||||||
|
localStorage.setItem(PERSIST_KEY, JSON.stringify(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadVisitedViews = (): PersistedTagView[] => {
|
||||||
|
const cache = localStorage.getItem(PERSIST_KEY);
|
||||||
|
if (!cache) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(cache) as PersistedTagView[];
|
||||||
|
} catch {
|
||||||
|
localStorage.removeItem(PERSIST_KEY);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearVisitedViews = () => {
|
||||||
|
localStorage.removeItem(PERSIST_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
export const useTagsViewStore = defineStore('tagsView', () => {
|
export const useTagsViewStore = defineStore('tagsView', () => {
|
||||||
const visitedViews = ref<RouteLocationNormalized[]>([]);
|
const visitedViews = ref<TagView[]>([]);
|
||||||
const cachedViews = ref<string[]>([]);
|
const cachedViews = ref<string[]>([]);
|
||||||
const iframeViews = ref<RouteLocationNormalized[]>([]);
|
const iframeViews = ref<TagView[]>([]);
|
||||||
|
|
||||||
const getVisitedViews = (): RouteLocationNormalized[] => {
|
const getVisitedViews = (): TagView[] => {
|
||||||
return visitedViews.value as RouteLocationNormalized[];
|
return visitedViews.value as TagView[];
|
||||||
};
|
};
|
||||||
const getIframeViews = (): RouteLocationNormalized[] => {
|
const getIframeViews = (): TagView[] => {
|
||||||
return iframeViews.value as RouteLocationNormalized[];
|
return iframeViews.value as TagView[];
|
||||||
};
|
};
|
||||||
const getCachedViews = (): string[] => {
|
const getCachedViews = (): string[] => {
|
||||||
return cachedViews.value;
|
return cachedViews.value;
|
||||||
@@ -24,26 +87,54 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
|
|
||||||
const addIframeView = (view: RouteLocationNormalized): void => {
|
const addIframeView = (view: RouteLocationNormalized): void => {
|
||||||
if (iframeViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
if (iframeViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||||
iframeViews.value.push(
|
iframeViews.value.push(normalizeVisitedView(view) as RouteLocationNormalized);
|
||||||
Object.assign({}, view, {
|
|
||||||
title: view.meta?.title || 'no-name'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const delIframeView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delIframeView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
iframeViews.value = iframeViews.value.filter((item: RouteLocationNormalized) => item.path !== view.path);
|
iframeViews.value = iframeViews.value.filter((item: RouteLocationNormalized) => item.path !== view.path);
|
||||||
resolve([...(iframeViews.value as RouteLocationNormalized[])]);
|
resolve(iframeViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addVisitedView = (view: RouteLocationNormalized): void => {
|
const addVisitedView = (view: RouteLocationNormalized): void => {
|
||||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||||
visitedViews.value.push(
|
visitedViews.value.push(normalizeVisitedView(view));
|
||||||
Object.assign({}, view, {
|
saveVisitedViews(visitedViews.value);
|
||||||
title: view.meta?.title || 'no-name'
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addAffixView = (view: RouteLocationNormalized): void => {
|
||||||
|
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||||
|
const insertIndex = visitedViews.value.findIndex((item) => !item.meta?.affix);
|
||||||
|
const normalizedView = normalizeVisitedView(view);
|
||||||
|
if (insertIndex === -1) {
|
||||||
|
visitedViews.value.push(normalizedView);
|
||||||
|
} else {
|
||||||
|
visitedViews.value.splice(insertIndex, 0, normalizedView);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPersistedViews = (): void => {
|
||||||
|
loadVisitedViews().forEach((view) => {
|
||||||
|
if (visitedViews.value.some((item) => item.path === view.path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visitedViews.value.push({
|
||||||
|
hash: '',
|
||||||
|
matched: [],
|
||||||
|
params: {},
|
||||||
|
redirectedFrom: undefined,
|
||||||
|
path: view.path,
|
||||||
|
fullPath: view.fullPath || view.path,
|
||||||
|
query: view.query || {},
|
||||||
|
name: view.name || undefined,
|
||||||
|
meta: view.meta || {},
|
||||||
|
title: view.title || view.meta?.title || 'no-name'
|
||||||
|
} as TagView);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const delView = (
|
const delView = (
|
||||||
view: RouteLocationNormalized
|
view: RouteLocationNormalized
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -56,7 +147,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
delCachedView(view);
|
delCachedView(view);
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -70,9 +161,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
saveVisitedViews(visitedViews.value);
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
|
const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
|
||||||
let viewName = '';
|
let viewName = '';
|
||||||
if (view) {
|
if (view) {
|
||||||
@@ -84,6 +177,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
resolve([...cachedViews.value]);
|
resolve([...cachedViews.value]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delOthersViews = (
|
const delOthersViews = (
|
||||||
view: RouteLocationNormalized
|
view: RouteLocationNormalized
|
||||||
): Promise<{
|
): Promise<{
|
||||||
@@ -94,7 +188,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
delOthersVisitedViews(view);
|
delOthersVisitedViews(view);
|
||||||
delOthersCachedViews(view);
|
delOthersCachedViews(view);
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -105,9 +199,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => {
|
visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => {
|
||||||
return v.meta?.affix || v.path === view.path;
|
return v.meta?.affix || v.path === view.path;
|
||||||
});
|
});
|
||||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
saveVisitedViews(visitedViews.value);
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delOthersCachedViews = (view: RouteLocationNormalized): Promise<string[]> => {
|
const delOthersCachedViews = (view: RouteLocationNormalized): Promise<string[]> => {
|
||||||
const viewName = view.name as string;
|
const viewName = view.name as string;
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@@ -126,15 +222,17 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
delAllVisitedViews();
|
delAllVisitedViews();
|
||||||
delAllCachedViews();
|
delAllCachedViews();
|
||||||
resolve({
|
resolve({
|
||||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||||
cachedViews: [...cachedViews.value]
|
cachedViews: [...cachedViews.value]
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
|
const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
visitedViews.value = visitedViews.value.filter((tag: RouteLocationNormalized) => tag.meta?.affix);
|
visitedViews.value = visitedViews.value.filter((tag: RouteLocationNormalized) => tag.meta?.affix);
|
||||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
clearVisitedViews();
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -152,11 +250,14 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
saveVisitedViews(visitedViews.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const delRightTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delRightTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
||||||
@@ -169,13 +270,16 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
saveVisitedViews(visitedViews.value);
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const delLeftTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
const delLeftTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
||||||
@@ -188,7 +292,8 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
saveVisitedViews(visitedViews.value);
|
||||||
|
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,7 +307,6 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isDynamicRoute = (view: RouteLocationNormalized): boolean => {
|
const isDynamicRoute = (view: RouteLocationNormalized): boolean => {
|
||||||
// 检查匹配的路由记录中是否有动态段
|
|
||||||
return view.matched.some((m) => m.path.includes(':'));
|
return view.matched.some((m) => m.path.includes(':'));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,6 +320,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
getCachedViews,
|
getCachedViews,
|
||||||
|
|
||||||
addVisitedView,
|
addVisitedView,
|
||||||
|
addAffixView,
|
||||||
addCachedView,
|
addCachedView,
|
||||||
delVisitedView,
|
delVisitedView,
|
||||||
delCachedView,
|
delCachedView,
|
||||||
@@ -229,6 +334,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
|||||||
delRightTags,
|
delRightTags,
|
||||||
delLeftTags,
|
delLeftTags,
|
||||||
addIframeView,
|
addIframeView,
|
||||||
delIframeView
|
delIframeView,
|
||||||
|
loadPersistedViews
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+4
@@ -99,6 +99,10 @@ declare global {
|
|||||||
* 是否显示多标签导航
|
* 是否显示多标签导航
|
||||||
*/
|
*/
|
||||||
tagsView: boolean;
|
tagsView: boolean;
|
||||||
|
/**
|
||||||
|
* 是否持久化标签页
|
||||||
|
*/
|
||||||
|
tagsViewPersist: boolean;
|
||||||
/**
|
/**
|
||||||
* 显示页签图标
|
* 显示页签图标
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user