123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- <template>
- <div class="tags-container">
- <el-scrollbar class="scroll-container" :vertical="false" @wheel.prevent="handleScroll">
- <router-link
- ref="tagRef"
- v-for="tag in visitedViews"
- :key="tag.fullPath"
- :class="'tags-item ' + (isActive(tag) ? 'active' : '')"
- :to="{ path: tag.path, query: tag.query }"
- @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
- @contextmenu.prevent="openContentMenu(tag, $event)"
- >
- {{ translateRouteTitle(tag.title) }}
- <Close class="close-icon" v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)" />
- </router-link>
- </el-scrollbar>
- <!-- tag标签操作菜单 -->
- <ul v-show="contentMenuVisible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }">
- <li @click="refreshSelectedTag(selectedTag)">
- <svg-icon icon-class="refresh" />
- 刷新
- </li>
- <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
- <svg-icon icon-class="close" />
- 关闭
- </li>
- <li @click="closeOtherTags">
- <svg-icon icon-class="close_other" />
- 关闭其它
- </li>
- <li v-if="!isFirstView()" @click="closeLeftTags">
- <svg-icon icon-class="close_left" />
- 关闭左侧
- </li>
- <li v-if="!isLastView()" @click="closeRightTags">
- <svg-icon icon-class="close_right" />
- 关闭右侧
- </li>
- <li @click="closeAllTags(selectedTag)">
- <svg-icon icon-class="close_all" />
- 关闭所有
- </li>
- </ul>
- </div>
- </template>
- <script setup>
- import { ref, watch, onMounted, getCurrentInstance, computed } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import { storeToRefs } from 'pinia'
- import { resolve } from 'path-browserify'
- import { translateRouteTitle } from '@/utils/i18n'
- import { usePermissionStore, useTagsViewStore, useSettingsStore, useAppStore } from '@/store'
- const { proxy } = getCurrentInstance()
- const router = useRouter()
- const route = useRoute()
- const permissionStore = usePermissionStore()
- const tagsViewStore = useTagsViewStore()
- const appStore = useAppStore()
- const { visitedViews } = storeToRefs(tagsViewStore)
- const settingsStore = useSettingsStore()
- const layout = computed(() => settingsStore.layout)
- const selectedTag = ref({
- path: '',
- fullPath: '',
- name: '',
- title: '',
- affix: false,
- keepAlive: false
- })
- const affixTags = ref([])
- const left = ref(0)
- const top = ref(0)
- watch(
- route,
- () => {
- addTags()
- moveToCurrentTag()
- },
- {
- immediate: true //初始化立即执行
- }
- )
- const contentMenuVisible = ref(false) // 右键菜单是否显示
- watch(contentMenuVisible, (value) => {
- if (value) {
- document.body.addEventListener('click', closeContentMenu)
- } else {
- document.body.removeEventListener('click', closeContentMenu)
- }
- })
- /**
- * 过滤出需要固定的标签
- */
- function filterAffixTags(routes, basePath = '/') {
- let tags = []
- routes.forEach((route) => {
- const tagPath = resolve(basePath, route.path)
- if (route.meta?.affix) {
- tags.push({
- path: tagPath,
- fullPath: tagPath,
- name: String(route.name),
- title: route.meta?.title || 'no-name',
- affix: route.meta?.affix,
- keepAlive: route.meta?.keepAlive
- })
- }
- if (route.children) {
- const tempTags = filterAffixTags(route.children, basePath + route.path)
- if (tempTags.length >= 1) {
- tags = [...tags, ...tempTags]
- }
- }
- })
- return tags
- }
- function initTags() {
- const tags = filterAffixTags(permissionStore.routes)
- affixTags.value = tags
- for (const tag of tags) {
- // Must have tag name
- if (tag.name) {
- tagsViewStore.addVisitedView(tag)
- }
- }
- }
- function addTags() {
- if (route.meta.title) {
- tagsViewStore.addView({
- name: route.name,
- title: route.meta.title,
- path: route.path,
- fullPath: route.fullPath,
- affix: route.meta?.affix,
- keepAlive: route.meta?.keepAlive
- })
- }
- }
- function moveToCurrentTag() {
- // 使用 nextTick() 的目的是确保在更新 tagsView 组件之前,scrollPaneRef 对象已经滚动到了正确的位置。
- nextTick(() => {
- for (const tag of visitedViews.value) {
- if (tag.path === route.path) {
- if (tag.fullPath !== route.fullPath) {
- tagsViewStore.updateVisitedView({
- name: route.name,
- title: route.meta.title || '',
- path: route.path,
- fullPath: route.fullPath,
- affix: route.meta?.affix,
- keepAlive: route.meta?.keepAlive
- })
- }
- }
- }
- })
- }
- function isActive(tag) {
- return tag.path === route.path
- }
- function isAffix(tag) {
- return tag?.affix
- }
- function isFirstView() {
- try {
- return selectedTag.value.path === '/dashboard' || selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
- } catch (err) {
- return false
- }
- }
- function isLastView() {
- try {
- return selectedTag.value.fullPath === tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
- } catch (err) {
- return false
- }
- }
- function refreshSelectedTag(view) {
- tagsViewStore.delCachedView(view)
- const { fullPath } = view
- nextTick(() => {
- router.replace({ path: '/redirect' + fullPath })
- })
- }
- function toLastView(visitedViews, view) {
- const latestView = visitedViews.slice(-1)[0]
- if (latestView && latestView.fullPath) {
- router.push(latestView.fullPath)
- } else {
- if (view?.name === 'Dashboard') {
- router.replace({ path: '/redirect' + view.fullPath })
- } else {
- router.push('/')
- }
- }
- }
- function closeSelectedTag(view) {
- tagsViewStore.delView(view).then((res) => {
- if (isActive(view)) {
- toLastView(res.visitedViews, view)
- }
- })
- }
- function closeLeftTags() {
- tagsViewStore.delLeftViews(selectedTag.value).then((res) => {
- if (!res.visitedViews.find((item) => item.path === route.path)) {
- toLastView(res.visitedViews)
- }
- })
- }
- function closeRightTags() {
- tagsViewStore.delRightViews(selectedTag.value).then((res) => {
- if (!res.visitedViews.find((item) => item.path === route.path)) {
- toLastView(res.visitedViews)
- }
- })
- }
- function closeOtherTags() {
- router.push(selectedTag.value)
- tagsViewStore.delOtherViews(selectedTag.value).then(() => {
- moveToCurrentTag()
- })
- }
- function closeAllTags(view) {
- tagsViewStore.delAllViews().then((res) => {
- toLastView(res.visitedViews, view)
- })
- }
- /**
- * 打开右键菜单
- */
- function openContentMenu(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
- }
- // 混合模式下,需要减去顶部菜单(fixed)的高度
- if (layout.value === 'mix') {
- top.value = e.clientY - 50
- } else {
- top.value = e.clientY
- }
- contentMenuVisible.value = true
- selectedTag.value = tag
- }
- /**
- * 关闭右键菜单
- */
- function closeContentMenu() {
- contentMenuVisible.value = false
- }
- /**
- * 滚动事件
- */
- function handleScroll() {
- closeContentMenu()
- }
- function findOutermostParent(tree, findName) {
- let parentMap = {}
- function buildParentMap(node, parent) {
- parentMap[node.name] = parent
- if (node.children) {
- for (let i = 0; i < node.children.length; i++) {
- buildParentMap(node.children[i], node)
- }
- }
- }
- for (let i = 0; i < tree.length; i++) {
- buildParentMap(tree[i], null)
- }
- let currentNode = parentMap[findName]
- while (currentNode) {
- if (!parentMap[currentNode.name]) {
- return currentNode
- }
- currentNode = parentMap[currentNode.name]
- }
- return null
- }
- const againActiveTop = (newVal) => {
- if (layout.value !== 'mix') return
- const parent = findOutermostParent(permissionStore.routes, newVal)
- if (appStore.activeTopMenu !== parent.path) {
- appStore.activeTopMenu(parent.path)
- }
- }
- // 如果是混合模式,更改selectedTag,需要对应高亮的activeTop
- watch(
- () => route.name,
- (newVal) => {
- if (newVal) {
- againActiveTop(newVal)
- }
- },
- {
- deep: true
- }
- )
- onMounted(() => {
- initTags()
- })
- </script>
- <style lang="scss" scoped>
- .tags-container {
- height: 35px;
- background-color: var(--el-bg-color);
- border: 1px solid var(--el-border-color-light);
- box-shadow: 0 1px 1px var(--el-box-shadow-light);
- .tags-item {
- display: inline-block;
- padding: 3px 8px;
- margin: 4px 0 0 5px;
- font-size: 12px;
- cursor: pointer;
- border: 1px solid var(--el-border-color-light);
- text-decoration: none;
- &:hover {
- color: var(--el-color-primary);
- }
- &:first-of-type {
- margin-left: 15px;
- }
- &:last-of-type {
- margin-right: 15px;
- }
- .close-icon {
- width: 1em;
- height: 1em;
- color: #000;
- border-radius: 50%;
- &:hover {
- color: #fff;
- background-color: var(--el-color-primary);
- }
- }
- &.active {
- color: #fff;
- background-color: var(--el-color-primary);
- &::before {
- display: inline-block;
- width: 8px;
- height: 8px;
- margin-right: 5px;
- content: "";
- background: #fff;
- border-radius: 50%;
- }
- .close-icon {
- color: #fff;
- }
- .close-icon:hover {
- color: var(--el-color-primary);
- background-color: var(--el-fill-color-light);
- }
- }
- }
- }
- .contextmenu {
- position: absolute;
- z-index: 99;
- font-size: 12px;
- background: var(--el-bg-color-overlay);
- border-radius: 4px;
- box-shadow: var(--el-box-shadow-light);
- li {
- padding: 8px 16px;
- cursor: pointer;
- &:hover {
- background: var(--el-fill-color-light);
- }
- }
- }
- .scroll-container {
- position: relative;
- width: 100%;
- overflow: hidden;
- white-space: nowrap;
- .el-scrollbar__bar {
- bottom: 0;
- }
- .el-scrollbar__wrap {
- height: 49px;
- }
- }
- </style>
|