| | |
| | | <template> |
| | | <div id="tags-view-container" class="tags-view-container"> |
| | | <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll"> |
| | | <div class="hamburger-container" @click="toggleSidebar"> |
| | | <svg-icon icon-class="drag"/> |
| | | </div> |
| | | <router-link v-for="tag in visitedViews" :key="tag.path" |
| | | :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" custom |
| | | v-slot="{ navigate, isActive, isExactActive }" ref="tag"> |
| | | <span :class="['tags-view-item', isActive && 'router-link-active', isExactActive && 'router-link-exact-active', tag.meta.icon && 'hide-dot']" |
| | | @click="navigate" |
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" |
| | | @contextmenu.prevent="openMenu(tag, $event)"> |
| | | <svg-icon v-if="tag.meta.icon" :icon-class="tag.meta.icon" /> |
| | | {{ tag.title }} |
| | | <icon-close v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> |
| | | </span> |
| | | </router-link> |
| | | </scroll-pane> |
| | | <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> |
| | | <li @click="refreshSelectedTag(selectedTag)">刷新</li> |
| | | <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li> |
| | | <li @click="closeOthersTags">关闭其它</li> |
| | | <li @click="closeAllTags(selectedTag)">关闭全部</li> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick, watch } from "vue"; |
| | | import { useRoute, useRouter } from "vue-router"; |
| | | import { storeToRefs } from "pinia"; |
| | | |
| | | <script setup name="TagsView"> |
| | | import { defineComponent } from 'vue'; |
| | | import ScrollPane from './ScrollPane'; |
| | | import ScrollPane from "./ScrollPane.vue"; |
| | | // import Hamburger from "../Hamburger.vue"; |
| | | // import { useMenuStore } from "@/stores/menu.js"; |
| | | import { useTagsViewStore } from "@/store/tagsView.js"; |
| | | import { usePermissionStore } from '@/store/permission'; |
| | | import path from 'path-browserify'; |
| | | import useAppStore from '@/store/app'; |
| | | import useTagsViewStore from '@/store/tagsView'; |
| | | import usePromissionStore from '@/store/permission'; |
| | | import { storeToRefs } from 'pinia'; |
| | | import { Close as IconClose } from '@element-plus/icons-vue'; |
| | | import SvgIcon from '@/components/SvgIcon/index.vue'; |
| | | import { ref, watch, onMounted, nextTick } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | |
| | | const appStore = useAppStore(); |
| | | const tagsViewStore = useTagsViewStore(); |
| | | const permissionStore = usePromissionStore(); |
| | | const { sidebar } = storeToRefs(appStore); |
| | | const { visitedViews } = storeToRefs(tagsViewStore); |
| | | // import iconNav from "@/components/icons/iconNav.vue"; |
| | | const permissionStore = usePermissionStore(); |
| | | const { routes } = storeToRefs(permissionStore); |
| | | import { useAppStore } from '@/store/app'; |
| | | const appStore = useAppStore(); |
| | | const { toggleSidebar } = appStore; |
| | | const { addVisitedView, addView, updateVisitedView, delCachedView, delView, delOthersViews, delAllViews, } = tagsViewStore; |
| | | |
| | | const tagsViewStore = useTagsViewStore(); |
| | | // const menuStroe = useMenuStore(); |
| | | // const { isOpen } = storeToRefs(menuStroe); |
| | | const wraper = ref(); |
| | | |
| | | const { visitedViews, cachedViews } = storeToRefs(tagsViewStore); |
| | | const { |
| | | addVisitedView, |
| | | addView, |
| | | delCachedView, |
| | | delView, |
| | | delOthersViews, |
| | | delAllViews, |
| | | } = tagsViewStore; |
| | | |
| | | const $router = useRouter(); |
| | | const $route = useRoute(); |
| | | |
| | | const visible = ref(false); |
| | | const top = ref(0); |
| | | const left = ref(0); |
| | | const selectedTag = ref({}); |
| | | const affixTags = ref([]); |
| | | const gData = reactive({ selectedTag: {}, affixTags: [] }); |
| | | const tagRefs = reactive([]); |
| | | const scrollPane = ref(); |
| | | |
| | | const scrollPane = ref(null); |
| | | const tag = ref(null); |
| | | |
| | | watch( |
| | | () => route.path, |
| | | () => { |
| | | addTags(); |
| | | moveToCurrentTag(); |
| | | }, |
| | | { |
| | | immediate: true |
| | | } |
| | | ); |
| | | |
| | | watch( |
| | | () => visible.value, |
| | | value => { |
| | | if (value) { |
| | | document.body.addEventListener('click', closeMenu); |
| | | } else { |
| | | document.body.removeEventListener('click', closeMenu); |
| | | } |
| | | } |
| | | ); |
| | | |
| | | |
| | | onMounted(() => { |
| | | initTags(); |
| | | watch($route, (val) => { |
| | | console.log("val,", $route, "============="); |
| | | addTags(); |
| | | }); |
| | | |
| | | function isCurrentRoute(_route) { |
| | | return route.path === _route.path; |
| | | function isActive(route) { |
| | | return route.path === $route.path; |
| | | } |
| | | |
| | | function isAffix(tag) { |
| | | return tag.meta && tag.meta.affix; |
| | | } |
| | | |
| | | function filterAffixTags(_routes, basePath = '/') { |
| | | function resolvePath(base, ...parts) { |
| | | return base + (base.endsWith("/") ? "" : "/") + parts.join("/"); |
| | | } |
| | | function filterAffixTags(_routes, basePath = "/") { |
| | | let tags = []; |
| | | _routes.forEach(route => { |
| | | if (route.meta && route.meta.affix) { |
| | | const tagPath = path.resolve(basePath, route.path); |
| | | _routes.forEach((_route) => { |
| | | if (_route.meta && _route.meta.affix) { |
| | | // const tagPath = resolvePath(basePath, route.path); |
| | | // const tagPath = resolvePath(basePath, route.path); |
| | | const tagPath = path.resolve(basePath, _route.path); |
| | | // const tagPath = _route.path; |
| | | tags.push({ |
| | | fullPath: tagPath, |
| | | path: tagPath, |
| | | name: route.name, |
| | | meta: { ...route.meta } |
| | | name: _route.name, |
| | | meta: { |
| | | ..._route.meta, |
| | | }, |
| | | }); |
| | | } |
| | | if (route.children) { |
| | | const tempTags = filterAffixTags(route.children, route.path); |
| | | if (_route.children) { |
| | | const tempTags = filterAffixTags(_route.children, _route.path); |
| | | if (tempTags.length >= 1) { |
| | | tags = [...tags, ...tempTags]; |
| | | } |
| | |
| | | }); |
| | | return tags; |
| | | } |
| | | |
| | | function initTags() { |
| | | affixTags.value = filterAffixTags(routes.value); |
| | | for (const tag of affixTags.value) { |
| | | // gData.affixTags.length = 0; |
| | | // gData.affixTags.push(...filterAffixTags(routes.value)); |
| | | gData.affixTags = filterAffixTags(routes.value); |
| | | for (const tag of gData.affixTags) { |
| | | // Must have tag name |
| | | if (tag.name) { |
| | | addVisitedView(tag); |
| | | } |
| | | } |
| | | } |
| | | |
| | | function addTags() { |
| | | const { name } = route; |
| | | const { name } = $route; |
| | | if (name) { |
| | | addView(route); |
| | | addView($route); |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | function moveToCurrentTag() { |
| | | const tags = tag.value; |
| | | nextTick(() => { |
| | | for (const tag of tags) { |
| | | if (tag.to.path === route.path) { |
| | | for (const tag of tagRefs) { |
| | | if (tag.to.path === $route.path) { |
| | | scrollPane.value.moveToTarget(tag); |
| | | // when query is different then update |
| | | if (tag.to.fullPath !== route.fullPath) { |
| | | updateVisitedView(route); |
| | | if (tag.to.fullPath !== $route.fullPath) { |
| | | // $store.dispatch("tagsView/updateVisitedView", $route); |
| | | updateVisitedView($route); |
| | | } |
| | | break; |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | function refreshSelectedTag(view) { |
| | | closeMenu(); |
| | | delCachedView(view); |
| | | const { fullPath } = view; |
| | | const fullPath = view.fullPath; |
| | | nextTick(() => { |
| | | router.replace({ |
| | | path: '/redirect' + fullPath |
| | | $router.replace({ |
| | | path: "/redirect" + fullPath, |
| | | }); |
| | | }) |
| | | }); |
| | | } |
| | | |
| | | function closeSelectedTag(view) { |
| | | closeMenu(); |
| | | delView(view); |
| | | if (isActive(view)) { |
| | | toLastView(visitedViews.value, view); |
| | | } |
| | | if (isActive(view)) { |
| | | toLastView(visitedViews.value, view); |
| | | } |
| | | } |
| | | |
| | | function closeOthersTags() { |
| | | router.push(selectedTag.value); |
| | | delOthersViews(selectedTag.value); |
| | | moveToCurrentTag(); |
| | | closeMenu(); |
| | | $router.push(gData.selectedTag); |
| | | delOthersViews(gData.selectedTag); |
| | | moveToCurrentTag();; |
| | | } |
| | | |
| | | function closeAllTags(view) { |
| | | closeMenu(); |
| | | delAllViews(); |
| | | if (affixTags.value.some(tag => tag.path === view.path)) { |
| | | return; |
| | | } |
| | | toLastView(visitedViews.value, view); |
| | | if (gData.affixTags.some((tag) => tag.path === view.path)) { |
| | | return; |
| | | } |
| | | toLastView(visitedViews.value, view); |
| | | } |
| | | |
| | | function toLastView(_visitedViews, view) { |
| | | const latestView = _visitedViews.slice(-1)[0]; |
| | | function toLastView(visitedViews, view) { |
| | | const latestView = visitedViews.slice(-1)[0]; |
| | | if (latestView) { |
| | | router.push(latestView.fullPath); |
| | | $router.push(latestView.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') { |
| | | if (view.name === "home") { |
| | | // 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 = tag.getBoundingClientRect().left; // container margin left |
| | | const offsetWidth = tag.offsetWidth; // container width |
| | | const offsetLeft = wraper.value.getBoundingClientRect().left; // container margin left |
| | | const offsetWidth = wraper.value.offsetWidth; // container width |
| | | const maxLeft = offsetWidth - menuMinWidth; // left boundary |
| | | const _left = e.clientX - offsetLeft + 15; // 15: margin right |
| | | let _left = e.clientX - offsetLeft + 15; // 15: margin right |
| | | |
| | | if (left > maxLeft) { |
| | | |
| | | if (_left > maxLeft) { |
| | | left.value = maxLeft; |
| | | } else { |
| | | left.value = _left; |
| | | } |
| | | |
| | | top.value = e.clientY; |
| | | top.value = 10; |
| | | visible.value = true; |
| | | selectedTag.value = tag; |
| | | gData.selectedTag = tag; |
| | | } |
| | | |
| | | function closeMenu() { |
| | | visible.value = false; |
| | | } |
| | | |
| | | function handleScroll() { |
| | | closeMenu(); |
| | | } |
| | | |
| | | // function toggle() { |
| | | // isOpen.value = !isOpen.value; |
| | | // } |
| | | |
| | | onMounted(() => { |
| | | initTags(); |
| | | addTags(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div id="tags-view-container" class="tags-view-container" ref="wraper"> |
| | | <div class="el-menu-icon-wrapper" @click="toggleSidebar"> |
| | | <el-icon class="nav" :size="24"> |
| | | <!-- <icon-nav></icon-nav> --> |
| | | <svg-icon icon-class="menu" /> |
| | | </el-icon> |
| | | </div> |
| | | <!-- <hamburger :is-active="menu.opened" @toggleClick="toggleSideBar" /> --> |
| | | <scroll-pane |
| | | ref="scrollPane" |
| | | class="tags-view-wrapper" |
| | | @scroll="handleScroll" |
| | | > |
| | | <!-- :tagRefs="tagRefs" --> |
| | | <!-- tag="span" --> |
| | | <!-- :ref="(el) => (tagRefs[idx] = el)" --> |
| | | <!-- <router-link |
| | | v-for="(tag, idx) in visitedViews" |
| | | :key="tag.path" |
| | | ref="tagRefs" |
| | | :class="isActive(tag) ? 'active' : ''" |
| | | :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" |
| | | class="tags-view-item" |
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" |
| | | @contextmenu.prevent="openMenu(tag, $event)" |
| | | > |
| | | {{ tag.title }} |
| | | <el-icon |
| | | v-if="!isAffix(tag)" |
| | | class="el-icon-close" |
| | | @click.prevent.stop="closeSelectedTag(tag)" |
| | | ><Close |
| | | /></el-icon> |
| | | </router-link> --> |
| | | <router-link |
| | | v-for="tag in visitedViews" |
| | | :key="tag.path" |
| | | :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" |
| | | custom |
| | | v-slot="{ navigate, isActive, isExactActive }" |
| | | :ref="(el) => (tagRefs[idx] = el)" |
| | | > |
| | | <span |
| | | :class="['tags-view-item', isActive && 'router-link-active', isExactActive && 'router-link-exact-active', tag.meta.icon && 'hide-dot']" |
| | | @click="navigate" |
| | | @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" |
| | | @contextmenu.prevent="openMenu(tag, $event)" |
| | | > |
| | | <svg-icon v-if="tag.meta.icon" :icon-class="tag.meta.icon" /> |
| | | {{ tag.title }} |
| | | <el-icon |
| | | v-if="!isAffix(tag)" |
| | | class="el-icon-close" |
| | | @click.prevent.stop="closeSelectedTag(tag)" |
| | | ><Close |
| | | /></el-icon> |
| | | </span> |
| | | </router-link> |
| | | </scroll-pane> |
| | | <ul |
| | | v-show="visible" |
| | | :style="{ left: left + 'px', top: top + 'px' }" |
| | | class="contextmenu" |
| | | > |
| | | <li @click="refreshSelectedTag(gData.selectedTag)">刷新</li> |
| | | <li |
| | | v-if="!isAffix(gData.selectedTag)" |
| | | @click="closeSelectedTag(gData.selectedTag)" |
| | | > |
| | | 关闭当前 |
| | | </li> |
| | | <li @click="closeOthersTags">关闭其他</li> |
| | | <li @click="closeAllTags(gData.selectedTag)">关闭所有</li> |
| | | </ul> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="less" 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, .12), 0 0 3px 0 rgba(0, 0, 0, .04); |
| | | .hamburger-container { |
| | | display: inline-block; |
| | | height: 32px; |
| | | line-height: 32px; |
| | | padding: 0 16px; |
| | | border-left: 1px solid var(--light-color); |
| | | border-right: 1px solid var(--light-color); |
| | | // padding-left: 48px; |
| | | position: relative; |
| | | display: flex; |
| | | border: 1px solid #0ff; |
| | | .el-menu-icon-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 8px; |
| | | color: #FFFFFF; |
| | | &:hover { |
| | | cursor: pointer; |
| | | color: #0ff; |
| | | } |
| | | } |
| | | .nav { |
| | | float: left; |
| | | } |
| | | :deep(.el-scrollbar__view) { |
| | | display: flex; |
| | | align-items: center; |
| | | overflow-y: hidden; |
| | | } |
| | | .tags-view-wrapper { |
| | | // width: calc(100% - 48px); |
| | | // background: gray; |
| | | .tags-view-item { |
| | | user-select: none; |
| | | display: inline-block; |
| | | position: relative; |
| | | cursor: pointer; |
| | | height: 26px; |
| | | line-height: 26px; |
| | | border: 1px solid #d8dce5; |
| | | color: #495060; |
| | | background: #fff; |
| | | padding: 0 8px; |
| | | height: 40px; |
| | | line-height: 40px; |
| | | padding: 0 20px; |
| | | font-size: 12px; |
| | | margin-left: 5px; |
| | | margin-top: 4px; |
| | | |
| | | &:first-of-type { |
| | | margin-left: 15px; |
| | | background: transparent; |
| | | color: #fff; |
| | | &:hover { |
| | | color: #0ff; |
| | | } |
| | | |
| | | &:last-of-type { |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | &.router-link-exact-active { |
| | | background-color: #42b983; |
| | | color: #fff; |
| | | border-color: #42b983; |
| | | |
| | | &::before { |
| | | content: ''; |
| | | background: #fff; |
| | | display: inline-block; |
| | | width: 8px; |
| | | height: 8px; |
| | | border-radius: 50%; |
| | | position: relative; |
| | | margin-right: 2px; |
| | | } |
| | | &.hide-dot { |
| | | &::before { |
| | | width: 0; |
| | | height: 0; |
| | | } |
| | | } |
| | | &.active { |
| | | background-color: #00feff; |
| | | border-color: #00feff; |
| | | color: #041f6c; |
| | | } |
| | | } |
| | | } |
| | |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: #333; |
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); |
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3); |
| | | |
| | | li { |
| | | margin: 0; |
| | |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | <style lang="less"> |
| | | //reset element css of el-icon-close |
| | | .tags-view-wrapper { |
| | | .tags-view-item { |
| | | .link { |
| | | padding: 0 2px; |
| | | } |
| | | .el-icon-close { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 16px; |
| | | height: 16px; |
| | | padding: 4px; |
| | | margin-bottom: -4px; |
| | | border-radius: 50%; |
| | | text-align: center; |
| | | transition: all .3s cubic-bezier(.645, .045, .355, 1); |
| | | transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); |
| | | transform-origin: 100% 50%; |
| | | |
| | | &:before { |
| | | transform: scale(.6); |
| | | display: inline-block; |
| | | vertical-align: -3px; |
| | | } |
| | | |
| | | &:hover { |
| | | background-color: #b4bccc; |