| | |
| | | <template> |
| | | <div id="tags-view-container" class="tags-view-container"> |
| | | <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll"> |
| | | <router-link |
| | | v-for="tag in visitedViews" |
| | | :key="tag.path" |
| | | :data-path="tag.path" |
| | | :class="isActive(tag) ? 'active' : ''" |
| | | :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" |
| | | class="tags-view-item" |
| | | :style="activeStyle(tag)" |
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" |
| | | @contextmenu.prevent="openMenu(tag, $event)" |
| | | > |
| | | {{ tag.title }} |
| | | <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> |
| | | <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" /> |
| | | </span> |
| | | </router-link> |
| | | </scroll-pane> |
| | | <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> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import ScrollPane from './ScrollPane' |
| | | <script setup lang="ts"> |
| | | import ScrollPane from './ScrollPane.vue' |
| | | import { getNormalPath } from '@/utils/ruoyi' |
| | | import useTagsViewStore from '@/store/modules/tagsView' |
| | | import useTagsViewStore from "@/store/modules/tagsView"; |
| | | import useSettingsStore from '@/store/modules/settings' |
| | | import usePermissionStore from '@/store/modules/permission' |
| | | import { ComponentInternalInstance } from "vue"; |
| | | import { RouteOption, TagView, RouteLocationRaw } from "vue-router"; |
| | | |
| | | const visible = ref(false); |
| | | const top = ref(0); |
| | | const left = ref(0); |
| | | const selectedTag = ref({}); |
| | | const affixTags = ref([]); |
| | | const scrollPaneRef = ref(null); |
| | | const selectedTag = ref<TagView>({}); |
| | | const affixTags = ref<TagView[]>([]); |
| | | const scrollPaneRef = ref(ScrollPane); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | |
| | |
| | | const theme = computed(() => useSettingsStore().theme); |
| | | |
| | | watch(route, () => { |
| | | addTags() |
| | | moveToCurrentTag() |
| | | addTags(); |
| | | moveToCurrentTag(); |
| | | }) |
| | | watch(visible, (value) => { |
| | | if (value) { |
| | | document.body.addEventListener('click', closeMenu) |
| | | document.body.addEventListener('click', closeMenu); |
| | | } else { |
| | | document.body.removeEventListener('click', closeMenu) |
| | | document.body.removeEventListener('click', closeMenu); |
| | | } |
| | | }) |
| | | onMounted(() => { |
| | | initTags() |
| | | addTags() |
| | | }) |
| | | |
| | | function isActive(r) { |
| | | return r.path === route.path |
| | | const isActive = (r: TagView): boolean => { |
| | | return r.path === route.path; |
| | | } |
| | | function activeStyle(tag) { |
| | | const activeStyle = (tag: TagView) => { |
| | | if (!isActive(tag)) return {}; |
| | | return { |
| | | "background-color": theme.value, |
| | | "border-color": theme.value |
| | | }; |
| | | } |
| | | function isAffix(tag) { |
| | | return tag.meta && tag.meta.affix |
| | | const isAffix = (tag: TagView) => { |
| | | return tag.meta && tag.meta.affix; |
| | | } |
| | | function isFirstView() { |
| | | const isFirstView = () => { |
| | | try { |
| | | return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath |
| | | return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath; |
| | | } catch (err) { |
| | | return false |
| | | return false; |
| | | } |
| | | } |
| | | function isLastView() { |
| | | const isLastView = () => { |
| | | try { |
| | | return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath |
| | | return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath; |
| | | } catch (err) { |
| | | return false |
| | | return false; |
| | | } |
| | | } |
| | | function filterAffixTags(routes, basePath = '') { |
| | | let tags = [] |
| | | const filterAffixTags = (routes:RouteOption [], basePath = '') => { |
| | | let tags:TagView[] = [] |
| | | routes.forEach(route => { |
| | | if (route.meta && route.meta.affix) { |
| | | const tagPath = getNormalPath(basePath + '/' + route.path) |
| | | const tagPath = getNormalPath(basePath + '/' + route.path); |
| | | tags.push({ |
| | | fullPath: tagPath, |
| | | path: tagPath, |
| | |
| | | }) |
| | | } |
| | | if (route.children) { |
| | | const tempTags = filterAffixTags(route.children, route.path) |
| | | const tempTags = filterAffixTags(route.children, route.path); |
| | | if (tempTags.length >= 1) { |
| | | tags = [...tags, ...tempTags] |
| | | tags = [...tags, ...tempTags]; |
| | | } |
| | | } |
| | | }) |
| | | return tags |
| | | } |
| | | function initTags() { |
| | | const initTags = () => { |
| | | const res = filterAffixTags(routes.value); |
| | | affixTags.value = res; |
| | | for (const tag of res) { |
| | | // Must have tag name |
| | | if (tag.name) { |
| | | useTagsViewStore().addVisitedView(tag) |
| | | useTagsViewStore().addVisitedView(tag); |
| | | } |
| | | } |
| | | } |
| | | function addTags() { |
| | | const { name } = route |
| | | const addTags = () => { |
| | | const { name } = route; |
| | | if (name) { |
| | | useTagsViewStore().addView(route) |
| | | useTagsViewStore().addView(route); |
| | | if (route.meta.link) { |
| | | useTagsViewStore().addIframeView(route); |
| | | } |
| | | } |
| | | return false |
| | | } |
| | | function moveToCurrentTag() { |
| | | 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) |
| | | useTagsViewStore().updateVisitedView(route); |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | function refreshSelectedTag(view) { |
| | | proxy.$tab.refreshPage(view); |
| | | const refreshSelectedTag = (view: TagView) => { |
| | | proxy?.$tab.refreshPage(view); |
| | | if (route.meta.link) { |
| | | useTagsViewStore().delIframeView(route); |
| | | } |
| | | } |
| | | function closeSelectedTag(view) { |
| | | proxy.$tab.closePage(view).then(({ visitedViews }) => { |
| | | const closeSelectedTag = (view: TagView) => { |
| | | proxy?.$tab.closePage(view).then(({ visitedViews }: any) => { |
| | | if (isActive(view)) { |
| | | toLastView(visitedViews, view) |
| | | toLastView(visitedViews, view); |
| | | } |
| | | }) |
| | | } |
| | | function closeRightTags() { |
| | | proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { |
| | | const closeRightTags = () => { |
| | | proxy?.$tab.closeRightPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | toLastView(visitedViews); |
| | | } |
| | | }) |
| | | } |
| | | function closeLeftTags() { |
| | | proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { |
| | | const closeLeftTags = () => { |
| | | proxy?.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | toLastView(visitedViews); |
| | | } |
| | | }) |
| | | } |
| | | function closeOthersTags() { |
| | | router.push(selectedTag.value).catch(() => { }); |
| | | proxy.$tab.closeOtherPage(selectedTag.value).then(() => { |
| | | moveToCurrentTag() |
| | | const closeOthersTags = () => { |
| | | router.push(selectedTag.value as RouteLocationRaw).catch(() => { }); |
| | | proxy?.$tab.closeOtherPage(selectedTag.value).then(() => { |
| | | moveToCurrentTag(); |
| | | }) |
| | | } |
| | | function closeAllTags(view) { |
| | | proxy.$tab.closeAllPage().then(({ visitedViews }) => { |
| | | const closeAllTags = (view: TagView) => { |
| | | proxy?.$tab.closeAllPage().then(({ visitedViews }) => { |
| | | if (affixTags.value.some(tag => tag.path === route.path)) { |
| | | return |
| | | return; |
| | | } |
| | | toLastView(visitedViews, view) |
| | | toLastView(visitedViews, view); |
| | | }) |
| | | } |
| | | function toLastView(visitedViews, view) { |
| | | const latestView = visitedViews.slice(-1)[0] |
| | | const toLastView = (visitedViews:TagView[], view?: TagView) => { |
| | | const latestView = visitedViews.slice(-1)[0]; |
| | | if (latestView) { |
| | | router.push(latestView.fullPath) |
| | | router.push(latestView.fullPath as string); |
| | | } 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') { |
| | | if (view?.name === 'Dashboard') { |
| | | // to reload home page |
| | | router.replace({ path: '/redirect' + view.fullPath }) |
| | | router.replace({ path: '/redirect' + view?.fullPath }); |
| | | } else { |
| | | router.push('/') |
| | | router.push('/'); |
| | | } |
| | | } |
| | | } |
| | | function openMenu(tag, e) { |
| | | 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 |
| | | const openMenu = (tag: TagView, 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 |
| | | left.value = maxLeft; |
| | | } else { |
| | | left.value = l |
| | | left.value = l; |
| | | } |
| | | |
| | | top.value = e.clientY |
| | | visible.value = true |
| | | selectedTag.value = tag |
| | | visible.value = true; |
| | | selectedTag.value = tag; |
| | | } |
| | | function closeMenu() { |
| | | visible.value = false |
| | | const closeMenu = () => { |
| | | visible.value = false; |
| | | } |
| | | function handleScroll() { |
| | | closeMenu() |
| | | const handleScroll = () => { |
| | | closeMenu(); |
| | | } |
| | | |
| | | |
| | | onMounted(() => { |
| | | initTags(); |
| | | addTags(); |
| | | }) |
| | | </script> |
| | | |
| | | <style lang='scss' scoped> |
| | | <template> |
| | | <div id="tags-view-container" class="tags-view-container"> |
| | | <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll"> |
| | | <router-link |
| | | v-for="tag in visitedViews" |
| | | :key="tag.path" |
| | | :data-path="tag.path" |
| | | :class="isActive(tag) ? 'active' : ''" |
| | | :to="{ path: tag.path ? tag.path : '', query: tag.query, fullPath: tag.fullPath ? tag.fullPath : '' }" |
| | | class="tags-view-item" |
| | | :style="activeStyle(tag)" |
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" |
| | | @contextmenu.prevent="openMenu(tag, $event)" |
| | | > |
| | | {{ tag.title }} |
| | | <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> |
| | | <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" /> |
| | | </span> |
| | | </router-link> |
| | | </scroll-pane> |
| | | <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> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .tags-view-container { |
| | | height: 34px; |
| | | width: 100%; |
| | |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | </style> |