update 优化 页签区增加左右滚动、右侧下拉、单独“刷新”按钮
add 新增 TagsView 全屏功能 add 新增 设置面板增加了“持久化标签页”开关 支持标签页持久化
This commit is contained in:
@@ -52,6 +52,14 @@ function addIframe() {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
|
||||
&:fullscreen,
|
||||
&:-webkit-full-screen,
|
||||
&:-moz-full-screen,
|
||||
&:-ms-fullscreen {
|
||||
background: var(--el-bg-color);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.app-main.with-fixed-header {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<img src="@/assets/images/dark.svg" alt="dark" />
|
||||
<div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||
<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
|
||||
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" />
|
||||
<div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block">
|
||||
<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
|
||||
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>
|
||||
</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">
|
||||
<span>显示页签图标</span>
|
||||
<span class="comp-style">
|
||||
@@ -161,19 +168,19 @@ const toggleDark = () => useToggle(isDark);
|
||||
|
||||
/** 菜单导航设置 */
|
||||
watch(
|
||||
() => navType,
|
||||
(val: string) => {
|
||||
if (val.value === NavTypeEnum.TOP) {
|
||||
navType,
|
||||
(val) => {
|
||||
if (val === NavTypeEnum.TOP) {
|
||||
appStore.toggleSideBarHide(true);
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||
} else if (val.value === NavTypeEnum.LEFT) {
|
||||
} else if (val === NavTypeEnum.LEFT) {
|
||||
appStore.toggleSideBarHide(false);
|
||||
permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
|
||||
} else if (val.value === NavTypeEnum.MIX) {
|
||||
} else if (val === NavTypeEnum.MIX) {
|
||||
appStore.toggleSideBarHide(false);
|
||||
}
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleNavType = (val: NavTypeEnum) => {
|
||||
@@ -202,7 +209,11 @@ const handleTheme = (val: string) => {
|
||||
const saveSetting = () => {
|
||||
proxy?.$modal.loading('正在保存到本地,请稍候...');
|
||||
const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
|
||||
if (!storeSettings.value.tagsViewPersist) {
|
||||
localStorage.removeItem('tags-view-visited');
|
||||
}
|
||||
settings.value.tagsView = storeSettings.value.tagsView;
|
||||
settings.value.tagsViewPersist = storeSettings.value.tagsViewPersist;
|
||||
settings.value.tagsIcon = storeSettings.value.tagsIcon;
|
||||
settings.value.fixedHeader = storeSettings.value.fixedHeader;
|
||||
settings.value.sidebarLogo = storeSettings.value.sidebarLogo;
|
||||
@@ -216,6 +227,7 @@ const saveSetting = () => {
|
||||
};
|
||||
const resetSetting = () => {
|
||||
proxy?.$modal.loading('正在清除设置缓存并刷新,请稍候...');
|
||||
localStorage.removeItem('tags-view-visited');
|
||||
useStorage<any>('layout-setting', null).value = null;
|
||||
setTimeout('window.location.reload()', 1000);
|
||||
};
|
||||
|
||||
@@ -5,84 +5,127 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouteLocationNormalized } from 'vue-router';
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import type { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const tagAndTagSpacing = 4;
|
||||
|
||||
const emits = defineEmits(['scroll', 'updateArrows']);
|
||||
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(() => {
|
||||
scrollWrapper.value?.addEventListener('scroll', emitScroll, true);
|
||||
getScrollWrapper()?.addEventListener('scroll', emitScroll, true);
|
||||
});
|
||||
|
||||
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 eventDelta = (e as any).wheelDelta || -e.deltaY * 40;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4;
|
||||
};
|
||||
const emits = defineEmits(['scroll']);
|
||||
const emitScroll = () => {
|
||||
emits('scroll');
|
||||
};
|
||||
const scrollWrapper = getScrollWrapper();
|
||||
if (!scrollWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const visitedViews = computed(() => tagsViewStore.visitedViews);
|
||||
scrollWrapper.scrollLeft += eventDelta / 4;
|
||||
emits('updateArrows');
|
||||
};
|
||||
|
||||
const moveToTarget = (currentTag: RouteLocationNormalized) => {
|
||||
const $container = scrollContainerRef.value?.$el;
|
||||
const $containerWidth = $container.offsetWidth;
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
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];
|
||||
const container = scrollContainerRef.value?.$el as HTMLElement | undefined;
|
||||
const scrollWrapper = getScrollWrapper();
|
||||
if (!container || !scrollWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0;
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth;
|
||||
} else {
|
||||
const tagListDom: any = document.getElementsByClassName('tags-view-item');
|
||||
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 containerWidth = container.offsetWidth;
|
||||
const tagKey = currentTag.fullPath || currentTag.path;
|
||||
const tagListDom = Array.from(document.querySelectorAll('.tags-view-item')) as HTMLElement[];
|
||||
const currentIndex = tagListDom.findIndex((item) => item.dataset.tagKey === tagKey);
|
||||
if (currentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
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({
|
||||
moveToTarget
|
||||
moveToTarget,
|
||||
scrollToStart,
|
||||
scrollToEnd,
|
||||
getScrollState
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -92,11 +135,15 @@ defineExpose({
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-scrollbar__bar) {
|
||||
bottom: 0px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
height: 49px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,63 +1,115 @@
|
||||
<template>
|
||||
<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
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
|
||||
:to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }"
|
||||
:key="tag.fullPath || tag.path"
|
||||
:data-tag-key="tag.fullPath || tag.path"
|
||||
:class="{ active: isActive(tag), 'has-icon': tagsIcon }"
|
||||
:to="tag.fullPath || tag.path || '/'"
|
||||
class="tags-view-item"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : undefined"
|
||||
@contextmenu.prevent="openMenu(tag, $event)"
|
||||
>
|
||||
<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>
|
||||
<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 || tag.meta?.title }}</span>
|
||||
<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>
|
||||
</router-link>
|
||||
</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">
|
||||
<li @click="refreshSelectedTag(selectedTag)"><refresh-right style="width: 1em; height: 1em" /> 刷新页面</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><close style="width: 1em; height: 1em" /> 关闭当前</li>
|
||||
<li @click="closeOthersTags"><circle-close 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)"><circle-close style="width: 1em; height: 1em" /> 全部关闭</li>
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<RefreshRight style="width: 1em; height: 1em" /> 刷新页面
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<Close style="width: 1em; height: 1em" /> 关闭当前
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowDown, ArrowLeft, ArrowRight, Back, CircleClose, Close, RefreshRight, Right } from '@element-plus/icons-vue';
|
||||
import ScrollPane from './ScrollPane.vue';
|
||||
import { getNormalPath } from '@/utils/ruoyi';
|
||||
import { useSettingsStore } from '@/store/modules/settings';
|
||||
import { usePermissionStore } from '@/store/modules/permission';
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView';
|
||||
import { RouteRecordRaw, RouteLocationNormalized } from 'vue-router';
|
||||
import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const visible = ref(false);
|
||||
const top = ref(0);
|
||||
const left = ref(0);
|
||||
const selectedTag = 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 { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const settingsStore = useSettingsStore();
|
||||
const permissionStore = usePermissionStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
|
||||
const visitedViews = computed(() => useTagsViewStore().getVisitedViews());
|
||||
const routes = computed(() => usePermissionStore().getRoutes());
|
||||
const theme = computed(() => useSettingsStore().theme);
|
||||
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
|
||||
const visitedViews = computed(() => tagsViewStore.getVisitedViews());
|
||||
const routes = computed(() => permissionStore.getRoutes());
|
||||
const tagsIcon = computed(() => settingsStore.tagsIcon);
|
||||
const selectedDropdownTag = computed<RouteLocationNormalized | undefined>(() => {
|
||||
return visitedViews.value.find((tag) => isActive(tag)) || selectedTag.value;
|
||||
});
|
||||
|
||||
watch(route, () => {
|
||||
addTags();
|
||||
moveToCurrentTag();
|
||||
});
|
||||
|
||||
watch(visible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', closeMenu);
|
||||
@@ -66,53 +118,80 @@ watch(visible, (value) => {
|
||||
}
|
||||
});
|
||||
|
||||
const isActive = (r: RouteLocationNormalized): boolean => {
|
||||
return r.path === route.path;
|
||||
watch(
|
||||
visitedViews,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
updateArrowState();
|
||||
});
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
const isActive = (currentRoute: RouteLocationNormalized): boolean => {
|
||||
return currentRoute.path === route.path;
|
||||
};
|
||||
|
||||
const activeStyle = (tag: RouteLocationNormalized) => {
|
||||
if (!isActive(tag)) return {};
|
||||
return {
|
||||
'background-color': 'var(--tags-view-active-bg)',
|
||||
'border-color': 'var(--tags-view-active-border-color)'
|
||||
backgroundColor: 'var(--tags-view-active-bg)',
|
||||
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) => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = getNormalPath(basePath + '/' + route.path);
|
||||
const isAffix = (tag?: RouteLocationNormalized) => {
|
||||
return !!tag?.meta?.affix;
|
||||
};
|
||||
|
||||
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({
|
||||
hash: '',
|
||||
matched: [],
|
||||
params: undefined,
|
||||
query: undefined,
|
||||
params: {},
|
||||
query: {},
|
||||
redirectedFrom: undefined,
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name as string,
|
||||
meta: { ...route.meta }
|
||||
});
|
||||
name: item.name as string,
|
||||
meta: { ...item.meta },
|
||||
title: item.meta?.title || 'no-name'
|
||||
} as RouteLocationNormalized);
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = filterAffixTags(route.children, route.path);
|
||||
if (item.children) {
|
||||
const tempTags = filterAffixTags(item.children, item.path);
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags];
|
||||
}
|
||||
@@ -120,135 +199,283 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => {
|
||||
});
|
||||
return tags;
|
||||
};
|
||||
|
||||
const initTags = () => {
|
||||
const res = filterAffixTags(routes.value);
|
||||
affixTags.value = res;
|
||||
for (const tag of res) {
|
||||
// Must have tag name
|
||||
if (settingsStore.tagsViewPersist) {
|
||||
tagsViewStore.loadPersistedViews();
|
||||
}
|
||||
const tags = filterAffixTags(routes.value);
|
||||
affixTags.value = tags;
|
||||
for (const tag of tags) {
|
||||
if (tag.name) {
|
||||
useTagsViewStore().addVisitedView(tag);
|
||||
tagsViewStore.addAffixView(tag);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addTags = () => {
|
||||
const { name } = route;
|
||||
if (route.query.title) {
|
||||
route.meta.title = route.query.title as string;
|
||||
if (typeof route.query.title === 'string') {
|
||||
route.meta.title = route.query.title;
|
||||
}
|
||||
if (name) {
|
||||
useTagsViewStore().addView(route as any);
|
||||
if (route.name) {
|
||||
tagsViewStore.addView(route as RouteLocationNormalized);
|
||||
}
|
||||
};
|
||||
|
||||
const moveToCurrentTag = () => {
|
||||
nextTick(() => {
|
||||
for (const r of visitedViews.value) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value?.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
useTagsViewStore().updateVisitedView(route);
|
||||
for (const item of visitedViews.value) {
|
||||
if (item.path === route.path) {
|
||||
scrollPaneRef.value?.moveToTarget(item);
|
||||
if (item.fullPath !== route.fullPath) {
|
||||
tagsViewStore.updateVisitedView(route as RouteLocationNormalized);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
const refreshSelectedTag = (view: RouteLocationNormalized) => {
|
||||
|
||||
const refreshSelectedTag = (view?: RouteLocationNormalized) => {
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
proxy?.$tab.refreshPage(view);
|
||||
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)) {
|
||||
toLastView(visitedViews, view);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeRightTags = () => {
|
||||
proxy?.$tab.closeRightPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
|
||||
if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
|
||||
toLastView(visitedViews);
|
||||
const tag = getOperateTag();
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
proxy?.$tab.closeRightPage(tag).then((views: RouteLocationNormalized[]) => {
|
||||
if (!views.find((item) => item.fullPath === route.fullPath)) {
|
||||
toLastView(views);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeLeftTags = () => {
|
||||
proxy?.$tab.closeLeftPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => {
|
||||
if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) {
|
||||
toLastView(visitedViews);
|
||||
const tag = getOperateTag();
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
proxy?.$tab.closeLeftPage(tag).then((views: RouteLocationNormalized[]) => {
|
||||
if (!views.find((item) => item.fullPath === route.fullPath)) {
|
||||
toLastView(views);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeOthersTags = () => {
|
||||
router.push(selectedTag.value).catch(() => {});
|
||||
proxy?.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||
const tag = getOperateTag();
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
router.push(tag.fullPath || tag.path || '/').catch(() => {});
|
||||
proxy?.$tab.closeOtherPage(tag).then(() => {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
toLastView(visitedViews, view);
|
||||
toLastView(views, view);
|
||||
});
|
||||
};
|
||||
const toLastView = (visitedViews: RouteLocationNormalized[], view?: RouteLocationNormalized) => {
|
||||
const latestView = visitedViews.slice(-1)[0];
|
||||
if (latestView) {
|
||||
router.push(latestView.fullPath as string);
|
||||
|
||||
const toLastView = (views: RouteLocationNormalized[], view?: RouteLocationNormalized) => {
|
||||
const latestView = views.slice(-1)[0];
|
||||
if (latestView?.fullPath) {
|
||||
router.push(latestView.fullPath);
|
||||
} else if (view?.name === 'Dashboard' && view.fullPath) {
|
||||
router.replace({ path: '/redirect' + view.fullPath });
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// 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('/');
|
||||
}
|
||||
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) {
|
||||
left.value = maxLeft;
|
||||
} else {
|
||||
left.value = l;
|
||||
const scrollLeft = () => {
|
||||
if (!canScrollLeft.value) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
visible.value = true;
|
||||
selectedTag.value = tag;
|
||||
};
|
||||
|
||||
const closeMenu = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
closeMenu();
|
||||
updateArrowState();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initTags();
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
height: 38px;
|
||||
width: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: var(--app-radius-md);
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
@@ -261,23 +488,32 @@ onMounted(() => {
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
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 {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
box-shadow: var(--app-shadow-sm);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
background: #fff;
|
||||
@@ -291,28 +527,78 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tags-view-item.active.has-icon::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.tags-view-item-title {
|
||||
margin-left: 4px;
|
||||
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 {
|
||||
margin: 0;
|
||||
background: var(--el-bg-color);
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: var(--app-radius-md);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
box-shadow: var(--app-shadow-md);
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
@@ -322,7 +608,6 @@ onMounted(() => {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
@@ -333,11 +618,13 @@ onMounted(() => {
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transform-origin: 100% 50%;
|
||||
|
||||
&:before {
|
||||
transform: scale(0.6);
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #b4bccc;
|
||||
color: #fff;
|
||||
|
||||
@@ -28,6 +28,11 @@ const setting: DefaultSettings = {
|
||||
*/
|
||||
tagsView: true,
|
||||
|
||||
/**
|
||||
* 持久化标签页
|
||||
*/
|
||||
tagsViewPersist: false,
|
||||
|
||||
/**
|
||||
* 显示页签图标
|
||||
*/
|
||||
|
||||
@@ -7,8 +7,8 @@ import { NavTypeEnum } from '@/enums/NavTypeEnum';
|
||||
|
||||
export const useSettingsStore = defineStore('setting', () => {
|
||||
const storageSetting = useStorage<LayoutSetting>('layout-setting', {
|
||||
topNav: defaultSettings.topNav,
|
||||
tagsView: defaultSettings.tagsView,
|
||||
tagsViewPersist: defaultSettings.tagsViewPersist,
|
||||
tagsIcon: defaultSettings.tagsIcon,
|
||||
fixedHeader: defaultSettings.fixedHeader,
|
||||
sidebarLogo: defaultSettings.sidebarLogo,
|
||||
@@ -22,6 +22,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
const sideTheme = ref<string>(storageSetting.value.sideTheme);
|
||||
const showSettings = ref<boolean>(defaultSettings.showSettings);
|
||||
const tagsView = ref<boolean>(storageSetting.value.tagsView);
|
||||
const tagsViewPersist = ref<boolean>(storageSetting.value.tagsViewPersist);
|
||||
const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
|
||||
const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
|
||||
const sidebarLogo = ref<boolean>(storageSetting.value.sidebarLogo);
|
||||
@@ -40,6 +41,7 @@ export const useSettingsStore = defineStore('setting', () => {
|
||||
sideTheme,
|
||||
showSettings,
|
||||
tagsView,
|
||||
tagsViewPersist,
|
||||
tagsIcon,
|
||||
fixedHeader,
|
||||
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 { 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', () => {
|
||||
const visitedViews = ref<RouteLocationNormalized[]>([]);
|
||||
const visitedViews = ref<TagView[]>([]);
|
||||
const cachedViews = ref<string[]>([]);
|
||||
const iframeViews = ref<RouteLocationNormalized[]>([]);
|
||||
const iframeViews = ref<TagView[]>([]);
|
||||
|
||||
const getVisitedViews = (): RouteLocationNormalized[] => {
|
||||
return visitedViews.value as RouteLocationNormalized[];
|
||||
const getVisitedViews = (): TagView[] => {
|
||||
return visitedViews.value as TagView[];
|
||||
};
|
||||
const getIframeViews = (): RouteLocationNormalized[] => {
|
||||
return iframeViews.value as RouteLocationNormalized[];
|
||||
const getIframeViews = (): TagView[] => {
|
||||
return iframeViews.value as TagView[];
|
||||
};
|
||||
const getCachedViews = (): string[] => {
|
||||
return cachedViews.value;
|
||||
@@ -24,26 +87,54 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
|
||||
const addIframeView = (view: RouteLocationNormalized): void => {
|
||||
if (iframeViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||
iframeViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || 'no-name'
|
||||
})
|
||||
);
|
||||
iframeViews.value.push(normalizeVisitedView(view) as RouteLocationNormalized);
|
||||
};
|
||||
|
||||
const delIframeView = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||
return new Promise((resolve) => {
|
||||
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 => {
|
||||
if (visitedViews.value.some((v: RouteLocationNormalized) => v.path === view.path)) return;
|
||||
visitedViews.value.push(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta?.title || 'no-name'
|
||||
})
|
||||
);
|
||||
visitedViews.value.push(normalizeVisitedView(view));
|
||||
saveVisitedViews(visitedViews.value);
|
||||
};
|
||||
|
||||
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 = (
|
||||
view: RouteLocationNormalized
|
||||
): Promise<{
|
||||
@@ -56,7 +147,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
delCachedView(view);
|
||||
}
|
||||
resolve({
|
||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||
cachedViews: [...cachedViews.value]
|
||||
});
|
||||
});
|
||||
@@ -70,9 +161,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
||||
saveVisitedViews(visitedViews.value);
|
||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||
});
|
||||
};
|
||||
|
||||
const delCachedView = (view?: RouteLocationNormalized): Promise<string[]> => {
|
||||
let viewName = '';
|
||||
if (view) {
|
||||
@@ -84,6 +177,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
resolve([...cachedViews.value]);
|
||||
});
|
||||
};
|
||||
|
||||
const delOthersViews = (
|
||||
view: RouteLocationNormalized
|
||||
): Promise<{
|
||||
@@ -94,7 +188,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
delOthersVisitedViews(view);
|
||||
delOthersCachedViews(view);
|
||||
resolve({
|
||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||
cachedViews: [...cachedViews.value]
|
||||
});
|
||||
});
|
||||
@@ -105,9 +199,11 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
visitedViews.value = visitedViews.value.filter((v: RouteLocationNormalized) => {
|
||||
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 viewName = view.name as string;
|
||||
return new Promise((resolve) => {
|
||||
@@ -126,15 +222,17 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
delAllVisitedViews();
|
||||
delAllCachedViews();
|
||||
resolve({
|
||||
visitedViews: [...(visitedViews.value as RouteLocationNormalized[])],
|
||||
visitedViews: visitedViews.value.slice() as RouteLocationNormalized[],
|
||||
cachedViews: [...cachedViews.value]
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const delAllVisitedViews = (): Promise<RouteLocationNormalized[]> => {
|
||||
return new Promise((resolve) => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
saveVisitedViews(visitedViews.value);
|
||||
};
|
||||
|
||||
const delRightTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||
return new Promise((resolve) => {
|
||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
||||
if (index === -1) {
|
||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
||||
@@ -169,13 +270,16 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
resolve([...(visitedViews.value as RouteLocationNormalized[])]);
|
||||
saveVisitedViews(visitedViews.value);
|
||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||
});
|
||||
};
|
||||
|
||||
const delLeftTags = (view: RouteLocationNormalized): Promise<RouteLocationNormalized[]> => {
|
||||
return new Promise((resolve) => {
|
||||
const index = visitedViews.value.findIndex((v: RouteLocationNormalized) => v.path === view.path);
|
||||
if (index === -1) {
|
||||
resolve(visitedViews.value.slice() as RouteLocationNormalized[]);
|
||||
return;
|
||||
}
|
||||
visitedViews.value = visitedViews.value.filter((item: RouteLocationNormalized, idx: number) => {
|
||||
@@ -188,7 +292,8 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
}
|
||||
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 => {
|
||||
// 检查匹配的路由记录中是否有动态段
|
||||
return view.matched.some((m) => m.path.includes(':'));
|
||||
};
|
||||
|
||||
@@ -216,6 +320,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
getCachedViews,
|
||||
|
||||
addVisitedView,
|
||||
addAffixView,
|
||||
addCachedView,
|
||||
delVisitedView,
|
||||
delCachedView,
|
||||
@@ -229,6 +334,7 @@ export const useTagsViewStore = defineStore('tagsView', () => {
|
||||
delRightTags,
|
||||
delLeftTags,
|
||||
addIframeView,
|
||||
delIframeView
|
||||
delIframeView,
|
||||
loadPersistedViews
|
||||
};
|
||||
});
|
||||
|
||||
Vendored
+4
@@ -99,6 +99,10 @@ declare global {
|
||||
* 是否显示多标签导航
|
||||
*/
|
||||
tagsView: boolean;
|
||||
/**
|
||||
* 是否持久化标签页
|
||||
*/
|
||||
tagsViewPersist: boolean;
|
||||
/**
|
||||
* 显示页签图标
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user