refactor: TagsView Component
This commit is contained in:
parent
481929a502
commit
a10a84de25
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, getCurrentInstance, nextTick, onBeforeMount, reactive, watch } from "vue"
|
import { getCurrentInstance, onMounted, ref, watch } from "vue"
|
||||||
import { useRoute, useRouter } from "vue-router"
|
import { useRoute, useRouter } from "vue-router"
|
||||||
import type { RouteRecordRaw } from "vue-router"
|
import type { RouteRecordRaw } from "vue-router"
|
||||||
import { useTagsViewStore } from "@/store/modules/tags-view"
|
import { useTagsViewStore } from "@/store/modules/tags-view"
|
||||||
@ -15,97 +15,24 @@ const route = useRoute()
|
|||||||
const tagsViewStore = useTagsViewStore()
|
const tagsViewStore = useTagsViewStore()
|
||||||
const permissionStore = usePermissionStore()
|
const permissionStore = usePermissionStore()
|
||||||
|
|
||||||
const toLastView = (visitedViews: ITagView[], view: ITagView) => {
|
const visible = ref(false)
|
||||||
const latestView = visitedViews.slice(-1)[0]
|
const top = ref(0)
|
||||||
if (latestView !== undefined && latestView.fullPath !== undefined) {
|
const left = ref(0)
|
||||||
router.push(latestView.fullPath).catch((err) => {
|
const selectedTag = ref<ITagView>({})
|
||||||
console.warn(err)
|
let affixTags: ITagView[] = []
|
||||||
})
|
|
||||||
} else {
|
const isActive = (tag: ITagView) => {
|
||||||
// 如果没有 TagsView,请默认重定向到主页,如果你需要,可以自行调整它
|
return tag.path === route.path
|
||||||
if (view.name === "Dashboard") {
|
|
||||||
// 重新加载主页
|
|
||||||
router.push({ path: "/redirect" + view.fullPath }).catch((err) => {
|
|
||||||
console.warn(err)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
router.push("/").catch((err) => {
|
|
||||||
console.warn(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = reactive({
|
const isAffix = (tag: ITagView) => {
|
||||||
visible: false,
|
return tag.meta?.affix
|
||||||
top: 0,
|
}
|
||||||
left: 0,
|
|
||||||
selectedTag: {} as ITagView,
|
|
||||||
affixTags: [] as ITagView[],
|
|
||||||
isActive: (tagView: ITagView) => {
|
|
||||||
return tagView.path === route.path
|
|
||||||
},
|
|
||||||
isAffix: (tag: ITagView) => {
|
|
||||||
return tag.meta && tag.meta.affix
|
|
||||||
},
|
|
||||||
refreshSelectedTag: (view: ITagView) => {
|
|
||||||
const { fullPath } = view
|
|
||||||
nextTick(() => {
|
|
||||||
router.replace({ path: "/redirect" + fullPath }).catch((err) => {
|
|
||||||
console.warn(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
closeSelectedTag: (view: ITagView) => {
|
|
||||||
tagsViewStore.delVisitedView(view)
|
|
||||||
if (state.isActive(view)) {
|
|
||||||
toLastView(tagsViewStore.visitedViews, view)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
closeOthersTags: () => {
|
|
||||||
if (state.selectedTag.fullPath !== route.path && state.selectedTag.fullPath !== undefined) {
|
|
||||||
router.push(state.selectedTag.fullPath).catch((err) => {
|
|
||||||
console.warn(err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
tagsViewStore.delOthersVisitedViews(state.selectedTag as ITagView)
|
|
||||||
},
|
|
||||||
closeAllTags: (view: ITagView) => {
|
|
||||||
tagsViewStore.delAllVisitedViews()
|
|
||||||
if (state.affixTags.some((tag) => tag.path === route.path)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toLastView(tagsViewStore.visitedViews, view)
|
|
||||||
},
|
|
||||||
openMenu: (tag: ITagView, e: MouseEvent) => {
|
|
||||||
const menuMinWidth = 105
|
|
||||||
const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left // container margin left
|
|
||||||
const offsetWidth = instance!.proxy!.$el.offsetWidth // container width
|
|
||||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
|
||||||
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
|
||||||
if (left > maxLeft) {
|
|
||||||
state.left = maxLeft
|
|
||||||
} else {
|
|
||||||
state.left = left
|
|
||||||
}
|
|
||||||
state.top = e.clientY
|
|
||||||
state.visible = true
|
|
||||||
state.selectedTag = tag
|
|
||||||
},
|
|
||||||
closeMenu: () => {
|
|
||||||
state.visible = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const visitedViews = computed(() => {
|
|
||||||
return tagsViewStore.visitedViews
|
|
||||||
})
|
|
||||||
const routes = computed(() => permissionStore.routes)
|
|
||||||
|
|
||||||
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
||||||
let tags: ITagView[] = []
|
let tags: ITagView[] = []
|
||||||
routes.forEach((route) => {
|
routes.forEach((route) => {
|
||||||
if (route.meta && route.meta.affix) {
|
if (route.meta?.affix) {
|
||||||
const tagPath = path.resolve(basePath, route.path)
|
const tagPath = path.resolve(basePath, route.path)
|
||||||
tags.push({
|
tags.push({
|
||||||
fullPath: tagPath,
|
fullPath: tagPath,
|
||||||
@ -125,11 +52,11 @@ const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initTags = () => {
|
const initTags = () => {
|
||||||
state.affixTags = filterAffixTags(routes.value)
|
affixTags = filterAffixTags(permissionStore.routes)
|
||||||
for (const tag of state.affixTags) {
|
for (const tag of affixTags) {
|
||||||
// 必须含有 name 属性
|
// 必须含有 name 属性
|
||||||
if (tag.name) {
|
if (tag.name) {
|
||||||
tagsViewStore.addVisitedView(tag as ITagView)
|
tagsViewStore.addVisitedView(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,45 +65,89 @@ const addTags = () => {
|
|||||||
if (route.name) {
|
if (route.name) {
|
||||||
tagsViewStore.addVisitedView(route)
|
tagsViewStore.addVisitedView(route)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveToCurrentTag = () => {
|
const refreshSelectedTag = (view: ITagView) => {
|
||||||
const tags = instance?.refs.tag as any[]
|
router.replace({ path: "/redirect" + view.fullPath })
|
||||||
if (tags === null || tags === undefined || !Array.isArray(tags)) {
|
}
|
||||||
|
|
||||||
|
const closeSelectedTag = (view: ITagView) => {
|
||||||
|
tagsViewStore.delVisitedView(view)
|
||||||
|
if (isActive(view)) {
|
||||||
|
toLastView(tagsViewStore.visitedViews, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeOthersTags = () => {
|
||||||
|
if (selectedTag.value.fullPath !== route.path && selectedTag.value.fullPath !== undefined) {
|
||||||
|
router.push(selectedTag.value.fullPath)
|
||||||
|
}
|
||||||
|
tagsViewStore.delOthersVisitedViews(selectedTag.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeAllTags = (view: ITagView) => {
|
||||||
|
tagsViewStore.delAllVisitedViews()
|
||||||
|
if (affixTags.some((tag) => tag.path === route.path)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (const tag of tags) {
|
toLastView(tagsViewStore.visitedViews, view)
|
||||||
if ((tag.to as ITagView).path === route.path) {
|
}
|
||||||
// When query is different then update
|
|
||||||
if ((tag.to as ITagView).fullPath !== route.fullPath) {
|
const toLastView = (visitedViews: ITagView[], view: ITagView) => {
|
||||||
tagsViewStore.updateVisitedView(route)
|
const latestView = visitedViews.slice(-1)[0]
|
||||||
|
if (latestView !== undefined && latestView.fullPath !== undefined) {
|
||||||
|
router.push(latestView.fullPath)
|
||||||
|
} else {
|
||||||
|
// 如果 TagsView 全部被关闭了,则默认重定向到主页
|
||||||
|
if (view.name === "Dashboard") {
|
||||||
|
// 重新加载主页
|
||||||
|
router.push({ path: "/redirect" + view.fullPath })
|
||||||
|
} else {
|
||||||
|
router.push("/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openMenu = (tag: ITagView, e: MouseEvent) => {
|
||||||
|
const menuMinWidth = 105
|
||||||
|
// container margin left
|
||||||
|
const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left
|
||||||
|
// container width
|
||||||
|
const offsetWidth = instance!.proxy!.$el.offsetWidth
|
||||||
|
// left boundary
|
||||||
|
const maxLeft = offsetWidth - menuMinWidth
|
||||||
|
// 15: margin right
|
||||||
|
const left15 = e.clientX - offsetLeft + 15
|
||||||
|
if (left15 > maxLeft) {
|
||||||
|
left.value = maxLeft
|
||||||
|
} else {
|
||||||
|
left.value = left15
|
||||||
}
|
}
|
||||||
|
top.value = e.clientY
|
||||||
|
visible.value = true
|
||||||
|
selectedTag.value = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeMenu = () => {
|
||||||
|
visible.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.name,
|
() => route.name,
|
||||||
() => {
|
() => {
|
||||||
addTags()
|
addTags()
|
||||||
moveToCurrentTag()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(visible, (value) => {
|
||||||
() => state.visible,
|
|
||||||
(value) => {
|
|
||||||
if (value) {
|
if (value) {
|
||||||
document.body.addEventListener("click", state.closeMenu)
|
document.body.addEventListener("click", closeMenu)
|
||||||
} else {
|
} else {
|
||||||
document.body.removeEventListener("click", state.closeMenu)
|
document.body.removeEventListener("click", closeMenu)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
|
||||||
// life cricle
|
onMounted(() => {
|
||||||
onBeforeMount(() => {
|
|
||||||
initTags()
|
initTags()
|
||||||
addTags()
|
addTags()
|
||||||
})
|
})
|
||||||
@ -186,26 +157,25 @@ onBeforeMount(() => {
|
|||||||
<div class="tags-view-container">
|
<div class="tags-view-container">
|
||||||
<ScrollPane class="tags-view-wrapper">
|
<ScrollPane class="tags-view-wrapper">
|
||||||
<router-link
|
<router-link
|
||||||
v-for="tag in visitedViews"
|
v-for="tag in tagsViewStore.visitedViews"
|
||||||
ref="tag"
|
|
||||||
:key="tag.path"
|
:key="tag.path"
|
||||||
:class="state.isActive(tag) ? 'active' : ''"
|
:class="isActive(tag) ? 'active' : ''"
|
||||||
:to="{ path: tag.path, query: tag.query }"
|
:to="{ path: tag.path, query: tag.query }"
|
||||||
class="tags-view-item"
|
class="tags-view-item"
|
||||||
@click.middle="!state.isAffix(tag) ? state.closeSelectedTag(tag) : ''"
|
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||||
@contextmenu.prevent="state.openMenu(tag, $event)"
|
@contextmenu.prevent="openMenu(tag, $event)"
|
||||||
>
|
>
|
||||||
{{ tag.meta?.title }}
|
{{ tag.meta?.title }}
|
||||||
<el-icon v-if="!state.isAffix(tag)" :size="12" @click.prevent.stop="state.closeSelectedTag(tag)">
|
<el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)">
|
||||||
<Close />
|
<Close />
|
||||||
</el-icon>
|
</el-icon>
|
||||||
</router-link>
|
</router-link>
|
||||||
</ScrollPane>
|
</ScrollPane>
|
||||||
<ul v-show="state.visible" :style="{ left: state.left + 'px', top: state.top + 'px' }" class="contextmenu">
|
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||||
<li @click="state.refreshSelectedTag(state.selectedTag)">刷新</li>
|
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
||||||
<li v-if="!state.isAffix(state.selectedTag)" @click="state.closeSelectedTag(state.selectedTag)">关闭</li>
|
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
||||||
<li @click="state.closeOthersTags">关闭其它</li>
|
<li @click="closeOthersTags">关闭其它</li>
|
||||||
<li @click="state.closeAllTags(state.selectedTag)">关闭所有</li>
|
<li @click="closeAllTags(selectedTag)">关闭所有</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,22 +1,15 @@
|
|||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia"
|
||||||
import type { _RouteLocationBase, RouteLocationNormalized } from "vue-router"
|
import type { RouteLocationNormalized } from "vue-router"
|
||||||
|
|
||||||
export interface ITagView extends Partial<RouteLocationNormalized> {
|
export type ITagView = Partial<RouteLocationNormalized>
|
||||||
title?: string
|
|
||||||
to?: _RouteLocationBase
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTagsViewStore = defineStore("tags-view", () => {
|
export const useTagsViewStore = defineStore("tags-view", () => {
|
||||||
const visitedViews = ref<ITagView[]>([])
|
const visitedViews = ref<ITagView[]>([])
|
||||||
|
|
||||||
const addVisitedView = (view: ITagView) => {
|
const addVisitedView = (view: ITagView) => {
|
||||||
if (visitedViews.value.some((v) => v.path === view.path)) return
|
if (visitedViews.value.some((v) => v.path === view.path)) return
|
||||||
visitedViews.value.push(
|
visitedViews.value.push(Object.assign({}, view))
|
||||||
Object.assign({}, view, {
|
|
||||||
title: view.meta?.title || "no-name"
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
const delVisitedView = (view: ITagView) => {
|
const delVisitedView = (view: ITagView) => {
|
||||||
for (const [i, v] of visitedViews.value.entries()) {
|
for (const [i, v] of visitedViews.value.entries()) {
|
||||||
@ -36,14 +29,6 @@ export const useTagsViewStore = defineStore("tags-view", () => {
|
|||||||
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix)
|
const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix)
|
||||||
visitedViews.value = affixTags
|
visitedViews.value = affixTags
|
||||||
}
|
}
|
||||||
const updateVisitedView = (view: ITagView) => {
|
|
||||||
for (let v of visitedViews.value) {
|
|
||||||
if (v.path === view.path) {
|
|
||||||
v = Object.assign(v, view)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { visitedViews, addVisitedView, delVisitedView, delOthersVisitedViews, delAllVisitedViews, updateVisitedView }
|
return { visitedViews, addVisitedView, delVisitedView, delOthersVisitedViews, delAllVisitedViews }
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user