原框架代码: 赵志江/huzhushan-vue3-element-admin
目录
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>