| | |
| | | <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)"> |
| | | <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> |
| | | </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> |
| | |
| | | const theme = computed(() => useSettingsStore().theme) |
| | | |
| | | watch(route, () => { |
| | | addTags() |
| | | moveToCurrentTag() |
| | | addTags() |
| | | moveToCurrentTag() |
| | | }) |
| | | |
| | | watch(visible, (value) => { |
| | | if (value) { |
| | | document.body.addEventListener('click', closeMenu) |
| | | } else { |
| | | document.body.removeEventListener('click', closeMenu) |
| | | } |
| | | if (value) { |
| | | document.body.addEventListener('click', closeMenu) |
| | | } else { |
| | | document.body.removeEventListener('click', closeMenu) |
| | | } |
| | | }) |
| | | |
| | | onMounted(() => { |
| | | initTags() |
| | | addTags() |
| | | initTags() |
| | | addTags() |
| | | }) |
| | | |
| | | function isActive(r) { |
| | | return r.path === route.path |
| | | return r.path === route.path |
| | | } |
| | | |
| | | function activeStyle(tag) { |
| | | if (!isActive(tag)) return {} |
| | | return { |
| | | "background-color": theme.value, |
| | | "border-color": theme.value |
| | | } |
| | | if (!isActive(tag)) return {} |
| | | return { |
| | | "background-color": theme.value, |
| | | "border-color": theme.value |
| | | } |
| | | } |
| | | |
| | | function isAffix(tag) { |
| | | return tag.meta && tag.meta.affix |
| | | return tag.meta && tag.meta.affix |
| | | } |
| | | |
| | | function isFirstView() { |
| | | try { |
| | | return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath |
| | | } catch (err) { |
| | | return false |
| | | } |
| | | try { |
| | | return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath |
| | | } catch (err) { |
| | | return false |
| | | } |
| | | } |
| | | |
| | | function isLastView() { |
| | | try { |
| | | return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath |
| | | } catch (err) { |
| | | return false |
| | | } |
| | | try { |
| | | return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath |
| | | } catch (err) { |
| | | return false |
| | | } |
| | | } |
| | | |
| | | function filterAffixTags(routes, basePath = '') { |
| | | let tags = [] |
| | | routes.forEach(route => { |
| | | if (route.meta && route.meta.affix) { |
| | | const tagPath = getNormalPath(basePath + '/' + route.path) |
| | | tags.push({ |
| | | fullPath: tagPath, |
| | | path: tagPath, |
| | | name: route.name, |
| | | meta: { ...route.meta } |
| | | }) |
| | | } |
| | | if (route.children) { |
| | | const tempTags = filterAffixTags(route.children, route.path) |
| | | if (tempTags.length >= 1) { |
| | | tags = [...tags, ...tempTags] |
| | | } |
| | | } |
| | | }) |
| | | return tags |
| | | let tags = [] |
| | | routes.forEach(route => { |
| | | if (route.meta && route.meta.affix) { |
| | | const tagPath = getNormalPath(basePath + '/' + route.path) |
| | | tags.push({ |
| | | fullPath: tagPath, |
| | | path: tagPath, |
| | | name: route.name, |
| | | meta: { ...route.meta } |
| | | }) |
| | | } |
| | | if (route.children) { |
| | | const tempTags = filterAffixTags(route.children, route.path) |
| | | if (tempTags.length >= 1) { |
| | | tags = [...tags, ...tempTags] |
| | | } |
| | | } |
| | | }) |
| | | return tags |
| | | } |
| | | |
| | | function initTags() { |
| | | const res = filterAffixTags(routes.value) |
| | | affixTags.value = res |
| | | for (const tag of res) { |
| | | // Must have tag name |
| | | if (tag.name) { |
| | | useTagsViewStore().addVisitedView(tag) |
| | | } |
| | | } |
| | | const res = filterAffixTags(routes.value) |
| | | affixTags.value = res |
| | | for (const tag of res) { |
| | | // Must have tag name |
| | | if (tag.name) { |
| | | useTagsViewStore().addVisitedView(tag) |
| | | } |
| | | } |
| | | } |
| | | |
| | | function addTags() { |
| | | const { name } = route |
| | | if (name) { |
| | | useTagsViewStore().addView(route) |
| | | } |
| | | const { name } = route |
| | | if (name) { |
| | | useTagsViewStore().addView(route) |
| | | } |
| | | } |
| | | |
| | | function 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) |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | 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) |
| | | } |
| | | } |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function refreshSelectedTag(view) { |
| | | proxy.$tab.refreshPage(view) |
| | | if (route.meta.link) { |
| | | useTagsViewStore().delIframeView(route) |
| | | } |
| | | proxy.$tab.refreshPage(view) |
| | | if (route.meta.link) { |
| | | useTagsViewStore().delIframeView(route) |
| | | } |
| | | } |
| | | |
| | | function closeSelectedTag(view) { |
| | | proxy.$tab.closePage(view).then(({ visitedViews }) => { |
| | | if (isActive(view)) { |
| | | toLastView(visitedViews, view) |
| | | } |
| | | }) |
| | | proxy.$tab.closePage(view).then(({ visitedViews }) => { |
| | | if (isActive(view)) { |
| | | toLastView(visitedViews, view) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function closeRightTags() { |
| | | proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | } |
| | | }) |
| | | proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => 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) |
| | | } |
| | | }) |
| | | proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { |
| | | if (!visitedViews.find(i => i.fullPath === route.fullPath)) { |
| | | toLastView(visitedViews) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | function closeOthersTags() { |
| | | router.push(selectedTag.value).catch(() => { }) |
| | | proxy.$tab.closeOtherPage(selectedTag.value).then(() => { |
| | | moveToCurrentTag() |
| | | }) |
| | | 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 |
| | | } |
| | | toLastView(visitedViews, view) |
| | | }) |
| | | 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] |
| | | if (latestView) { |
| | | 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') { |
| | | // to reload home page |
| | | router.replace({ path: '/redirect' + view.fullPath }) |
| | | } else { |
| | | router.push('/') |
| | | } |
| | | } |
| | | const latestView = visitedViews.slice(-1)[0] |
| | | if (latestView) { |
| | | 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') { |
| | | // to reload home page |
| | | router.replace({ path: '/redirect' + view.fullPath }) |
| | | } else { |
| | | 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 |
| | | |
| | | if (l > maxLeft) { |
| | | left.value = maxLeft |
| | | } else { |
| | | left.value = l |
| | | } |
| | | |
| | | top.value = e.clientY |
| | | visible.value = true |
| | | selectedTag.value = tag |
| | | 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 |
| | | } |
| | | |
| | | top.value = e.clientY |
| | | visible.value = true |
| | | selectedTag.value = tag |
| | | } |
| | | |
| | | function closeMenu() { |
| | | visible.value = false |
| | | visible.value = false |
| | | } |
| | | |
| | | function handleScroll() { |
| | | closeMenu() |
| | | closeMenu() |
| | | } |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | .tags-view-container { |
| | | height: 34px; |
| | | width: 100%; |
| | | position: fixed; /* 将头部固定 */ |
| | | top: 50px; /* 在顶部固定 */ |
| | | z-index: 1000; /* 确保头部在其他内容之上 */ |
| | | background: #fff; |
| | | box-shadow: none; |
| | | |
| | | .tags-view-wrapper { |
| | | .tags-view-item { |
| | | display: inline-block; |
| | | position: relative; |
| | | cursor: pointer; |
| | | height: 30px; |
| | | line-height: 26px; |
| | | //border: 1px solid var(--tags-item-border, #d8dce5); |
| | | color: var(--tags-item-text, #495060); |
| | | background: var(--tags-item-bg, #fff); |
| | | padding: 2px 16px; |
| | | font-size: 12px; |
| | | //margin-left: 5px; |
| | | margin-top: 4px; |
| | | |
| | | &:first-of-type { |
| | | margin-left: 15px; |
| | | } |
| | | |
| | | &:last-of-type { |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | &.active { |
| | | border-radius: 10px 10px 0px 0px; |
| | | background-color: #F7F7F7 !important; |
| | | color: #165DFF; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .contextmenu { |
| | | margin: 0; |
| | | background: var(--el-bg-color-overlay, #fff); |
| | | z-index: 3000; |
| | | position: absolute; |
| | | list-style-type: none; |
| | | padding: 5px 0; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: var(--tags-item-text, #333); |
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); |
| | | border: 1px solid var(--el-border-color-light, #e4e7ed); |
| | | |
| | | li { |
| | | margin: 0; |
| | | padding: 7px 16px; |
| | | cursor: pointer; |
| | | |
| | | &:hover { |
| | | background: var(--tags-item-hover, #eee); |
| | | } |
| | | } |
| | | } |
| | | height: 34px; |
| | | width: 100%; |
| | | background: var(--tags-bg, #fff); |
| | | border-bottom: 1px solid var(--tags-item-border, #d8dce5); |
| | | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(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 var(--tags-item-border, #d8dce5); |
| | | color: var(--tags-item-text, #495060); |
| | | background: var(--tags-item-bg, #fff); |
| | | padding: 0 8px; |
| | | font-size: 12px; |
| | | margin-left: 5px; |
| | | margin-top: 4px; |
| | | |
| | | &:first-of-type { |
| | | margin-left: 15px; |
| | | } |
| | | |
| | | &:last-of-type { |
| | | margin-right: 15px; |
| | | } |
| | | |
| | | &.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: 5px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | .contextmenu { |
| | | margin: 0; |
| | | background: var(--el-bg-color-overlay, #fff); |
| | | z-index: 3000; |
| | | position: absolute; |
| | | list-style-type: none; |
| | | padding: 5px 0; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: 400; |
| | | color: var(--tags-item-text, #333); |
| | | box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); |
| | | border: 1px solid var(--el-border-color-light, #e4e7ed); |
| | | |
| | | li { |
| | | margin: 0; |
| | | padding: 7px 16px; |
| | | cursor: pointer; |
| | | |
| | | &:hover { |
| | | background: var(--tags-item-hover, #eee); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss"> |
| | | //reset element css of el-icon-close |
| | | .tags-view-wrapper { |
| | | .tags-view-item { |
| | | .el-icon-close { |
| | | width: 16px; |
| | | height: 16px; |
| | | vertical-align: 2px; |
| | | border-radius: 50%; |
| | | text-align: center; |
| | | transition: all .3s cubic-bezier(.645, .045, .355, 1); |
| | | transform-origin: 100% 50%; |
| | | |
| | | &:before { |
| | | transform: scale(.6); |
| | | display: inline-block; |
| | | vertical-align: -3px; |
| | | } |
| | | |
| | | &:hover { |
| | | background-color: var(--tags-close-hover, #b4bccc); |
| | | color: #fff; |
| | | width: 12px !important; |
| | | height: 12px !important; |
| | | } |
| | | } |
| | | } |
| | | .tags-view-item { |
| | | .el-icon-close { |
| | | width: 16px; |
| | | height: 16px; |
| | | vertical-align: 2px; |
| | | border-radius: 50%; |
| | | text-align: center; |
| | | transition: all .3s cubic-bezier(.645, .045, .355, 1); |
| | | transform-origin: 100% 50%; |
| | | |
| | | &:before { |
| | | transform: scale(.6); |
| | | display: inline-block; |
| | | vertical-align: -3px; |
| | | } |
| | | |
| | | &:hover { |
| | | background-color: var(--tags-close-hover, #b4bccc); |
| | | color: #fff; |
| | | width: 12px !important; |
| | | height: 12px !important; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |