vue3-element-admin实现同一个菜单多标签

原框架代码: 赵志江/huzhushan-vue3-element-admin

目录

TagsBar实现

实现同一个菜单多标签

device/detail/:id,不同参数时页面缓存删不掉的问题


TagsBar实现

在src/layout/components/下新建目录Tagsbar,新建index.vue

html 复制代码
<template>
  <div class="tags-container" :class="{ hide: !isTagsbarShow }">
    <el-scrollbar
      ref="scrollContainer"
      :vertical="false"
      class="scroll-container"
      @wheel.prevent="onScroll"
    >
      <router-link
        v-for="(tag, i) in tagList"
        :key="tag.fullPath"
        :to="tag"
        :ref="el => setItemRef(i, el)"
        custom
        v-slot="{ navigate, isExactActive }"
      >
        <div
          class="tags-item"
          :class="isExactActive? 'active' : ''"
          @click="navigate"
          @click.middle="closeTag(tag)"
          @contextmenu.prevent="openMenu(tag, $event)"
        >
          <span class="title">{{ $t(tag.title) }}</span>

          <el-icon
            v-if="!isAffix(tag)"
            class="el-icon-close"
            @click.prevent.stop="closeTag(tag)"
          >
            <Close />
          </el-icon>
        </div>
      </router-link>
    </el-scrollbar>
  </div>
  <ul
    v-show="visible"
    :style="{ left: left + 'px', top: top + 'px' }"
    class="contextmenu"
  >
    <!-- <li @click="refreshSelectedTag(selectedTag)">{{ $t('tags.refresh') }}</li> -->
    <li v-if="!isAffix(selectedTag)" @click="closeTag(selectedTag)">
      {{ $t('tags.close') }}
    </li>
    <li @click="closeOtherTags">{{ $t('tags.other') }}</li>
    <li @click="closeLeftTags">{{ $t('tags.left') }}</li>
    <li @click="closeRightTags">{{ $t('tags.right') }}</li>
    <li @click="closeAllTags">{{ $t('tags.all') }}</li>
  </ul>
</template>

<script>
import { defineComponent, computed, getCurrentInstance } from 'vue'
import { useTags } from './hooks/useTags'
import { useContextMenu } from './hooks/useContextMenu'
import { useLayoutsettings } from '@/pinia/modules/layoutSettings'

export default defineComponent({
  name: 'Tagsbar',
  mounted() {
    
  },
  setup() {
	const instance = getCurrentInstance()
	instance.appContext.config.globalProperties.$tagsbar = this
	
    const defaultSettings = useLayoutsettings()
    const isTagsbarShow = computed(() => defaultSettings.tagsbar.isShow)

    const tags = useTags()
    const contextMenu = useContextMenu(tags.tagList)

    const onScroll = e => {
      tags.handleScroll(e)
      contextMenu.closeMenu.value()
    }

    return {
      isTagsbarShow,
      onScroll,
      ...tags,
      ...contextMenu
    }
  },
})
</script>

<style lang="scss" scoped>
.tags-container {
  height: 32px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #e0e4ef;
  &.hide {
    display: none;
  }
  .scroll-container {
    white-space: nowrap;
    overflow: hidden;
    ::v-deep(.el-scrollbar__bar) {
      bottom: 0px;
    }
  }

  .tags-item {
    display: inline-block;
    height: 32px;
    line-height: 32px;
    box-sizing: border-box;
    border-left: 1px solid #e6e6e6;
    border-right: 1px solid #e6e6e6;
    color: #5c5c5c;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: -1px;
    vertical-align: bottom;
    cursor: pointer;
    &:first-of-type {
      margin-left: 15px;
    }
    &:last-of-type {
      margin-right: 15px;
    }
    &.active {
      color: #303133;
      background: #f5f5f5;
    }
    .title {
      display: inline-block;
      vertical-align: top;
      max-width: 200px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .el-icon-close {
      color: #5c5c5c;
      margin-left: 8px;
      width: 16px;
      height: 16px;
      vertical-align: -2px;
      border-radius: 50%;
      text-align: center;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(0.8);
        display: inline-block;
        vertical-align: -2px;
      }
      &:hover {
        background-color: #333;
        color: #fff;
      }
    }
  }
}
.contextmenu {
  margin: 0;
  background: #fff;
  z-index: 3000;
  position: fixed;
  list-style-type: none;
  padding: 5px 0;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 400;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
  white-space: nowrap;
  li {
    margin: 0;
    padding: 8px 16px;
    cursor: pointer;
    &:hover {
      background: #eee;
    }
  }
}
</style>

新建hooks目录,新建useTags.js

javascript 复制代码
import { storeToRefs } from 'pinia'
import { useTags as useTagsbar } from '@/pinia/modules/tags'
import { useScrollbar } from './useScrollbar'
import { watch, computed, ref, nextTick, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'

export const isAffix = tag => {
  return !!tag.meta && !!tag.meta.affix
}

export const useTags = () => {
  const tagStore = useTagsbar()
  const { tagList } = storeToRefs(tagStore)
  const { addTag, delTag, saveActivePosition, updateTagList } = tagStore
  const router = useRouter()
  const route = router.currentRoute
  const routes = computed(() => router.getRoutes())

  const tagsItem = ref([])

  const setItemRef = (i, el) => {
    tagsItem.value[i] = el
  }

  const scrollbar = useScrollbar(tagsItem)

  watch(
    () => tagList.value.length,
    () => {
      tagsItem.value = []
    }
  )

  const filterAffixTags = routes => {
    return routes.filter(route => isAffix(route))
  }

  const initTags = () => {
    const affixTags = filterAffixTags(routes.value)

    for (const tag of affixTags) {
      if (tag.name) {
        addTag(tag)
      }
    }
    // 不在路由中的所有标签,需要删除
    const noUseTags = tagList.value.filter(tag =>
      routes.value.every(route => route.name !== tag.name)
    )
    noUseTags.forEach(tag => {
      delTag(tag)
    })
  }

  const addTagList = () => {
    const tag = route.value
    if (!!tag.name && tag.matched[0].components.default.name === 'layout') {
      addTag(tag)
    }
  }

  const saveTagPosition = tag => {
    const index = tagList.value.findIndex(
      item => item.fullPath === tag.fullPath
    )

    saveActivePosition(Math.max(0, index))
  }

  const moveToCurrentTag = () => {
    nextTick(() => {
      for (const tag of tagsItem.value) {
        if (!!tag && tag.to.path === route.value.path) {
          scrollbar.moveToTarget(tag)

          if (tag.to.fullPath !== route.value.fullPath) {
            updateTagList(route.value)
          }
          break
        }
      }
    })
  }

  onBeforeMount(() => {
    initTags()
    addTagList()
    moveToCurrentTag()
  })

  watch(route, (newRoute, oldRoute) => {
    saveTagPosition(oldRoute) // 保存标签的位置
    addTagList()
    moveToCurrentTag()
  })

  return {
    tagList,
    setItemRef,
    isAffix,
    ...scrollbar,
  }
}

useScrollbar.js

javascript 复制代码
import { ref } from 'vue'

export const useScrollbar = tagsItem => {
  const scrollContainer = ref(null)
  const scrollLeft = ref(0)

  const doScroll = val => {
    scrollLeft.value = val
    scrollContainer.value.setScrollLeft(scrollLeft.value)
  }

  const handleScroll = e => {
    const $wrap = scrollContainer.value.wrapRef
    if ($wrap.offsetWidth + scrollLeft.value > $wrap.children[0].scrollWidth) {
      doScroll($wrap.children[0].scrollWidth - $wrap.offsetWidth)
      return
    } else if (scrollLeft.value < 0) {
      doScroll(0)
      return
    }
    const eventDelta = e.wheelDelta || -e.deltaY
    doScroll(scrollLeft.value - eventDelta / 4)
  }

  const moveToTarget = currentTag => {
    const $wrap = scrollContainer.value.wrapRef
    const tagList = tagsItem.value

    let firstTag = null
    let lastTag = null

    if (tagList.length > 0) {
      firstTag = tagList[0]
      lastTag = tagList[tagList.length - 1]
    }
    if (firstTag === currentTag) {
      doScroll(0)
    } else if (lastTag === currentTag) {
      doScroll($wrap.children[0].scrollWidth - $wrap.offsetWidth)
    } else {
      const el = currentTag.$el.nextElementSibling

      el.offsetLeft + el.offsetWidth > $wrap.offsetWidth
        ? doScroll(el.offsetLeft - el.offsetWidth)
        : doScroll(0)
    }
  }

  return {
    scrollContainer,
    handleScroll,
    moveToTarget,
  }
}

useContextMenu.js

javascript 复制代码
import { useTags } from '@/pinia/modules/tags'
import { onMounted, onBeforeUnmount, reactive, toRefs, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { isAffix } from './useTags'

export const useContextMenu = tagList => {
  const router = useRouter()
  const route = useRoute()

  const tagsStore = useTags()

  const state = reactive({
    visible: false,
    top: 0,
    left: 0,
    selectedTag: {},
    openMenu(tag, e) {
      state.visible = true
      state.left = e.clientX
      state.top = e.clientY
      state.selectedTag = tag
    },
    closeMenu() {
      state.visible = false
    },
    refreshSelectedTag(tag) {
      tagsStore.deCacheList(tag)
      const { fullPath } = tag
      nextTick(() => {
        router.replace({
          path: '/redirect' + fullPath,
        })
      })
    },
    closeTag(tag) {
      if (isAffix(tag)) return

      const closedTagIndex = tagList.value.findIndex(
        item => {
			return item.path === tag.path
		}
      )
	  console.log(closedTagIndex)
      tagsStore.delTag(tag)
      if (isActive(tag)) {
        toLastTag(closedTagIndex - 1)
      }
    },
    closeOtherTags() {
      tagsStore.delOtherTags(state.selectedTag)
      router.push(state.selectedTag)
    },
    closeLeftTags() {
      state.closeSomeTags('left')
    },
    closeRightTags() {
      state.closeSomeTags('right')
    },
    closeSomeTags(direction) {
      const index = tagList.value.findIndex(
        item => item.fullPath === state.selectedTag.fullPath
      )

      if (
        (direction === 'left' && index <= 0) ||
        (direction === 'right' && index >= tagList.value.length - 1)
      ) {
        return
      }

      const needToClose =
        direction === 'left'
          ? tagList.value.slice(0, index)
          : tagList.value.slice(index + 1)
      tagsStore.delSomeTags(needToClose)
      router.push(state.selectedTag)
    },
    closeAllTags() {
      tagsStore.delAllTags()
      router.push('/')
    },
  })

  const isActive = tag => {
    return tag.fullPath === route.fullPath
  }

  const toLastTag = lastTagIndex => {
    const lastTag = tagList.value[lastTagIndex]
    if (lastTag) {
      router.push(lastTag.fullPath)
    } else {
      router.push('/')
    }
  }

  onMounted(() => {
    document.addEventListener('click', state.closeMenu)
  })

  onBeforeUnmount(() => {
    document.removeEventListener('click', state.closeMenu)
  })

  return toRefs(state)
}

在src/pinia/modules下新建tags.js

javascript 复制代码
import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: meta.title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      if (this.cacheList.includes(name)) return
      if (!meta.noCache) {
        this.cacheList.push(name)
      }
    },
    deTagList(tag) {
      // 删除tagList
      this.tagList = this.tagList.filter(v => v.path !== tag.path)
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)
    },
    deCacheList(tag) {
      // 删除cacheList
      this.cacheList = this.cacheList.filter(v => v !== tag.name)
    },
    delTag(tag) {
      // 删除tagList
      this.deTagList(tag)

      // 删除cacheList
      this.deCacheList(tag)
    },
    delOtherTags(tag) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || v.path === tag.path
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v => v === tag.name)
    },
    delSomeTags(tags) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || tags.every(tag => tag.path !== v.path)
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v =>
        tags.every(tag => tag.name !== v)
      )
    },
    delAllTags() {
      this.tagList = this.tagList.filter(v => !!v.meta.affix)
      // 保存到localStorage
      removeItem(TAGLIST)
      this.cacheList = []
    },
    updateTagList(tag) {
      const index = this.tagList.findIndex(v => v.path === tag.path)
      if (index > -1) {
        this.tagList[index] = Object.assign({}, this.tagList[index], tag)
        // 保存到localStorage
        setItem(TAGLIST, this.tagList)
      }
    },
    clearAllTags() {
      this.cacheList = []
      this.tagList = []
      // 保存到localStorage
      removeItem(TAGLIST)
    },
  },
})

src/layout/components/Content下新建index.vue,keep-alive组件会根据Component的name来跟include进行匹配,来缓存页面,同样的页面重新进入时不会触发onMounted,只会触发onActivated。

html 复制代码
<template>
  <router-view v-slot="{ Component }">
    <keep-alive :include="cacheList.join(',')">
      <component :is="Component" :key="key" />
    </keep-alive>
  </router-view>
</template>
<script>
import { storeToRefs } from 'pinia'
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useTags } from '@/pinia/modules/tags'

export default defineComponent({
  setup() {
    const route = useRoute()
    const { cacheList } = storeToRefs(useTags())
    const key = computed(() => route.fullPath)

    return {
      cacheList,
      key,
    }
  },
})
</script>

实现同一个菜单多标签

框架通过vue-router来实现页面跳转和菜单展示,下面介绍对一个菜单,如果实现参数不同,显示多个tag。

按如下定义menu

javascript 复制代码
{
    path: 'detail/:id',
    name: 'device_detail',
    component: () => import('@/views/device/detail.vue'),
    meta: { title: '设备详情', icon: 'el-icon-s-platform' },
    hidden: true,
 }

device/detail.vue中动态修改Component的name:

javascript 复制代码
onMounted(() => {
	  ctx.deviceId = parseInt(ctx.$route.params.id)
	  ctx.$options.name = 'device_detail' + ctx.deviceId
})
onActivated(() => {
	ctx.$options.name = 'device_detail' + ctx.deviceId
})

修改src/pinia/modules/tags.js,修改地方:tag.name 改为 this.getFinalName(tag),即根据参数不同name也不同,name放入cacheList,用于唯一标识一个Component。

javascript 复制代码
import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      var title = meta.title
	  if (name == 'device_detail') {
		title = title + ' ' + query.name
	  }
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      const finalName = this.getFinalName(target)
      if (this.cacheList.includes(finalName)) return
      if (!meta.noCache) {
        this.cacheList.push(finalName)
      }
    },
    getFinalName(tag) {
		if (tag.name == 'device_detail') {
			return tag.name + tag.params.id
		}
		return tag.name
	},
    deTagList(tag) {
      // 删除tagList
      this.tagList = this.tagList.filter(v => v.path !== tag.path)
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)
    },
    deCacheList(tag) {
      const name = this.getFinalName(tag)
      // 删除cacheList
      this.cacheList = this.cacheList.filter(v => v !== name)
    },
    delTag(tag) {
      // 删除tagList
      this.deTagList(tag)

      // 删除cacheList
      this.deCacheList(tag)
    },
    delOtherTags(tag) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || v.path === tag.path
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      const name = this.getFinalName(tag)
      this.cacheList = this.cacheList.filter(v => v === name)
    },
    delSomeTags(tags) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || tags.every(tag => tag.path !== v.path)
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v =>
        tags.every(tag => tag.name !== v)
      )
    },
    delAllTags() {
      this.tagList = this.tagList.filter(v => !!v.meta.affix)
      // 保存到localStorage
      removeItem(TAGLIST)
      this.cacheList = []
    },
    updateTagList(tag) {
      const index = this.tagList.findIndex(v => v.path === tag.path)
      if (index > -1) {
        this.tagList[index] = Object.assign({}, this.tagList[index], tag)
        // 保存到localStorage
        setItem(TAGLIST, this.tagList)
      }
    },
    clearAllTags() {
      this.cacheList = []
      this.tagList = []
      // 保存到localStorage
      removeItem(TAGLIST)
    },
  },
})

device/detail/:id,不同参数时页面缓存删不掉的问题

现象如下:进入/device/detail/1,再打开/device/detail/2,点击其他标签,删掉/device/detail/2标签,再打开/device/detail/2,此时发现只触发了onActivated方法,没有触发onMounted方法,页面没有重新渲染,keepalive这里的缓存机制不清楚,但是可以知道框架误以为/device/detail/2还在缓存中,直接把缓存中的页面拿过来显示了。

解决方法

对于这种动态菜单的情况,Compnent的key属性增加自增的标识,每次打开标识加1。

修改src/pinia/modules/tags.js,增加detailIndex,在addTag时增加detailIndex的修改

javascript 复制代码
import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
    detailIndex: {}
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      var title = meta.title
	  if (name == 'device_detail') {
		title = title + ' ' + query.name
	  }
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      const finalName = this.getFinalName(target)
      if (this.cacheList.includes(finalName)) return
      if (!meta.noCache) {
        if (finalName.startsWith('device_detail')) {
            if (!this.detailIndex[target.path]) {
                this.detailIndex[target.path] = 1
            } else {
                this.detailIndex[target.path]++
            }
        } else {
            this.detailIndex[target.path] = ''
        }
        this.cacheList.push(finalName)
      }
    },

修改src/layout/components/Content/index.vue中的key未route.path + detailIndex.value[route.path]

html 复制代码
<template>
  <router-view v-slot="{ Component }">
    <keep-alive :include="cacheList.join(',')">
      <component :is="Component" :key="key" />
    </keep-alive>
  </router-view>
</template>
<script>
import { storeToRefs } from 'pinia'
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useTags } from '@/pinia/modules/tags'

export default defineComponent({
  setup() {
    const route = useRoute()
    const { cacheList, detailIndex } = storeToRefs(useTags())
    const key = computed(() => route.path + detailIndex[route.path])

    return {
      cacheList,
      key,
    }
  },
})
</script>
相关推荐
西凉河的葛三叔7 小时前
vue3+elementui-plus el-dialog全局配置点击空白处不关闭弹窗
前端·vue3·elementui-plus
xiaohuatu7 小时前
CSRF保护--laravel进阶篇
vue3·laravel·csrf
半度℃温热5 天前
ERROR TypeError: AutoImport is not a function
webpack·vue3·element-plus·插件版本·autoimport
朝阳396 天前
vue3 中直接使用 JSX ( lang=“tsx“ 的用法)
vue3·tsx·jsx
xcLeigh7 天前
VUE3实现简洁的特色美食网站源码
前端·源码·vue3
陈逸子风10 天前
(系列十一)Vue3框架中路由守卫及请求拦截(实现前后端交互)
vue3·webapi·权限·流程·表单
爱分享的Hayes小朋友10 天前
如何用post请求调用Server-Sent Events服务
前端·vue3·sse·post
Coisini_甜柚か13 天前
打字机效果显示
前端·vue3·antv
Modify_QmQ13 天前
初识Electron & 进程通信
electron·vue3·桌面端
Bessie23413 天前
实现 Nuxt3 预览PDF文件
pdf·vue3·nuxt3