update 优化 页签区增加左右滚动、右侧下拉、单独“刷新”按钮

add 新增 TagsView 全屏功能
add 新增 设置面板增加了“持久化标签页”开关 支持标签页持久化
This commit is contained in:
疯狂的狮子Li
2026-03-24 17:58:07 +08:00
parent 4fb60440f2
commit a86557cc2a
8 changed files with 681 additions and 210 deletions
+8
View File
@@ -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 {
+20 -8
View File
@@ -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);
}; };
+107 -60
View File
@@ -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>
+400 -113
View File
@@ -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;
+5
View File
@@ -28,6 +28,11 @@ const setting: DefaultSettings = {
*/ */
tagsView: true, tagsView: true,
/**
* 持久化标签页
*/
tagsViewPersist: false,
/** /**
* 显示页签图标 * 显示页签图标
*/ */
+3 -1
View File
@@ -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
View File
@@ -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
}; };
}); });
+4
View File
@@ -99,6 +99,10 @@ declare global {
* 是否显示多标签导航 * 是否显示多标签导航
*/ */
tagsView: boolean; tagsView: boolean;
/**
* 是否持久化标签页
*/
tagsViewPersist: boolean;
/** /**
* 显示页签图标 * 显示页签图标
*/ */