| | |
| | | :key="tag.path" |
| | | :data-path="tag.path" |
| | | :class="isActive(tag) ? 'active' : ''" |
| | | :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" |
| | | :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) : ''" |
| | |
| | | > |
| | | {{ 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;" /> |
| | | <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> |
| | | <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' |
| | | import { getNormalPath } from '@/utils/ruoyi' |
| | | import useTagsViewStore from '@/store/modules/tagsView' |
| | | import useSettingsStore from '@/store/modules/settings' |
| | | import usePermissionStore from '@/store/modules/permission' |
| | | <script setup lang="ts"> |
| | | 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'; |
| | | |
| | | 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<RouteLocationNormalized>(); |
| | | const affixTags = ref<RouteLocationNormalized[]>([]); |
| | | const scrollPaneRef = ref<InstanceType<typeof ScrollPane>>(); |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const { proxy } = getCurrentInstance() as ComponentInternalInstance; |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | |
| | | const visitedViews = computed(() => useTagsViewStore().visitedViews); |
| | | const routes = computed(() => usePermissionStore().routes); |
| | | const visitedViews = computed(() => useTagsViewStore().getVisitedViews()); |
| | | const routes = computed(() => usePermissionStore().getRoutes()); |
| | | 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 |
| | | } |
| | | function activeStyle(tag) { |
| | | const isActive = (r: RouteLocationNormalized): boolean => { |
| | | return r.path === route.path; |
| | | }; |
| | | const activeStyle = (tag: RouteLocationNormalized) => { |
| | | if (!isActive(tag)) return {}; |
| | | return { |
| | | "background-color": theme.value, |
| | | "border-color": theme.value |
| | | 'background-color': 'var(--tags-view-active-bg)', |
| | | 'border-color': 'var(--tags-view-active-border-color)' |
| | | }; |
| | | } |
| | | function isAffix(tag) { |
| | | return tag.meta && tag.meta.affix |
| | | } |
| | | function isFirstView() { |
| | | }; |
| | | 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 |
| | | 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 = [] |
| | | routes.forEach(route => { |
| | | }; |
| | | const filterAffixTags = (routes: RouteRecordRaw[], basePath = '') => { |
| | | let tags: RouteLocationNormalized[] = []; |
| | | |
| | | routes.forEach((route) => { |
| | | if (route.meta && route.meta.affix) { |
| | | const tagPath = getNormalPath(basePath + '/' + route.path) |
| | | const tagPath = getNormalPath(basePath + '/' + route.path); |
| | | tags.push({ |
| | | hash: '', |
| | | matched: [], |
| | | params: undefined, |
| | | query: undefined, |
| | | redirectedFrom: undefined, |
| | | fullPath: tagPath, |
| | | path: tagPath, |
| | | name: route.name, |
| | | name: route.name as string, |
| | | meta: { ...route.meta } |
| | | }) |
| | | }); |
| | | } |
| | | 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() { |
| | | }); |
| | | return tags; |
| | | }; |
| | | 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 (route.query.title) { |
| | | route.meta.title = route.query.title as string; |
| | | } |
| | | if (name) { |
| | | useTagsViewStore().addView(route) |
| | | if (route.meta.link) { |
| | | useTagsViewStore().addIframeView(route); |
| | | } |
| | | useTagsViewStore().addView(route as any); |
| | | } |
| | | return false |
| | | } |
| | | function moveToCurrentTag() { |
| | | }; |
| | | const moveToCurrentTag = () => { |
| | | nextTick(() => { |
| | | for (const r of visitedViews.value) { |
| | | if (r.path === route.path) { |
| | | scrollPaneRef.value.moveToTarget(r); |
| | | 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: RouteLocationNormalized) => { |
| | | proxy?.$tab.refreshPage(view); |
| | | if (route.meta.link) { |
| | | useTagsViewStore().delIframeView(route); |
| | | } |
| | | } |
| | | function closeSelectedTag(view) { |
| | | proxy.$tab.closePage(view).then(({ visitedViews }) => { |
| | | }; |
| | | const closeSelectedTag = (view: RouteLocationNormalized) => { |
| | | 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 => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | }); |
| | | }; |
| | | const closeRightTags = () => { |
| | | proxy?.$tab.closeRightPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => { |
| | | if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews); |
| | | } |
| | | }) |
| | | } |
| | | function closeLeftTags() { |
| | | proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | }); |
| | | }; |
| | | const closeLeftTags = () => { |
| | | proxy?.$tab.closeLeftPage(selectedTag.value).then((visitedViews: RouteLocationNormalized[]) => { |
| | | if (!visitedViews.find((i: RouteLocationNormalized) => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews); |
| | | } |
| | | }) |
| | | } |
| | | function closeOthersTags() { |
| | | router.push(selectedTag.value).catch(() => { }); |
| | | proxy.$tab.closeOtherPage(selectedTag.value).then(() => { |
| | | moveToCurrentTag() |
| | | }) |
| | | } |
| | | function closeAllTags(view) { |
| | | proxy.$tab.closeAllPage().then(({ visitedViews }) => { |
| | | if (affixTags.value.some(tag => tag.path === route.path)) { |
| | | return |
| | | }); |
| | | }; |
| | | const closeOthersTags = () => { |
| | | router.push(selectedTag.value).catch(() => {}); |
| | | proxy?.$tab.closeOtherPage(selectedTag.value).then(() => { |
| | | moveToCurrentTag(); |
| | | }); |
| | | }; |
| | | const closeAllTags = (view: RouteLocationNormalized) => { |
| | | proxy?.$tab.closeAllPage().then(({ visitedViews }) => { |
| | | if (affixTags.value.some((tag) => tag.path === route.path)) { |
| | | return; |
| | | } |
| | | toLastView(visitedViews, view) |
| | | }) |
| | | } |
| | | function toLastView(visitedViews, view) { |
| | | const latestView = visitedViews.slice(-1)[0] |
| | | toLastView(visitedViews, view); |
| | | }); |
| | | }; |
| | | const toLastView = (visitedViews: RouteLocationNormalized[], view?: RouteLocationNormalized) => { |
| | | 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: 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 |
| | | left.value = maxLeft; |
| | | } else { |
| | | left.value = l |
| | | left.value = l; |
| | | } |
| | | |
| | | top.value = e.clientY |
| | | visible.value = true |
| | | selectedTag.value = tag |
| | | } |
| | | function closeMenu() { |
| | | visible.value = false |
| | | } |
| | | function handleScroll() { |
| | | closeMenu() |
| | | } |
| | | top.value = e.clientY; |
| | | visible.value = true; |
| | | selectedTag.value = tag; |
| | | }; |
| | | const closeMenu = () => { |
| | | visible.value = false; |
| | | }; |
| | | const handleScroll = () => { |
| | | closeMenu(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | initTags(); |
| | | addTags(); |
| | | }); |
| | | </script> |
| | | |
| | | <style lang='scss' scoped> |
| | | <style lang="scss" scoped> |
| | | .tags-view-container { |
| | | height: 34px; |
| | | width: 100%; |
| | | background: #fff; |
| | | border-bottom: 1px solid #d8dce5; |
| | | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04); |
| | | background-color: var(--el-bg-color); |
| | | border: 1px solid var(--el-border-color-light); |
| | | box-shadow: |
| | | 0 1px 3px 0 rgba(0, 0, 0, 0.12), |
| | | 0 0 3px 0 rgba(0, 0, 0, 0.04); |
| | | .tags-view-wrapper { |
| | | .tags-view-item { |
| | | display: inline-block; |
| | | position: relative; |
| | | cursor: pointer; |
| | | height: 26px; |
| | | line-height: 26px; |
| | | border: 1px solid #d8dce5; |
| | | line-height: 23px; |
| | | background-color: var(--el-bg-color); |
| | | border: 1px solid var(--el-border-color-light); |
| | | color: #495060; |
| | | background: #fff; |
| | | padding: 0 8px; |
| | | font-size: 12px; |
| | | margin-left: 5px; |
| | | margin-top: 4px; |
| | | &:hover { |
| | | color: var(--el-color-primary); |
| | | } |
| | | &:first-of-type { |
| | | margin-left: 15px; |
| | | } |
| | |
| | | color: #fff; |
| | | border-color: #42b983; |
| | | &::before { |
| | | content: ""; |
| | | content: ''; |
| | | background: #fff; |
| | | display: inline-block; |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | position: relative; |
| | | margin-right: 2px; |
| | | margin-right: 5px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | .contextmenu { |
| | | margin: 0; |
| | | background: #fff; |
| | | background: var(--el-bg-color); |
| | | z-index: 3000; |
| | | position: absolute; |
| | | list-style-type: none; |
| | |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: #333; |
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); |
| | | li { |
| | | margin: 0; |
| | |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | </style> |