|
@@ -0,0 +1,268 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { getCurrentInstance, onMounted, ref, watch } from "vue"
|
|
|
+import { type RouteLocationNormalizedLoaded, type RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router"
|
|
|
+import { type TagView, useTagsViewStore } from "@/store/modules/tags-view"
|
|
|
+import { usePermissionStore } from "@/store/modules/permission"
|
|
|
+import { useRouteListener } from "@/hooks/useRouteListener"
|
|
|
+import path from "path-browserify"
|
|
|
+import ScrollPane from "./ScrollPane.vue"
|
|
|
+import { Close } from "@element-plus/icons-vue"
|
|
|
+
|
|
|
+const instance = getCurrentInstance()
|
|
|
+const router = useRouter()
|
|
|
+const route = useRoute()
|
|
|
+const tagsViewStore = useTagsViewStore()
|
|
|
+const permissionStore = usePermissionStore()
|
|
|
+const { listenerRouteChange } = useRouteListener()
|
|
|
+
|
|
|
+/** 标签页组件元素的引用数组 */
|
|
|
+const tagRefs = ref<InstanceType<typeof RouterLink>[]>([])
|
|
|
+
|
|
|
+/** 右键菜单的状态 */
|
|
|
+const visible = ref(false)
|
|
|
+/** 右键菜单的 top 位置 */
|
|
|
+const top = ref(0)
|
|
|
+/** 右键菜单的 left 位置 */
|
|
|
+const left = ref(0)
|
|
|
+/** 当前正在右键操作的标签页 */
|
|
|
+const selectedTag = ref<TagView>({})
|
|
|
+/** 固定的标签页 */
|
|
|
+let affixTags: TagView[] = []
|
|
|
+
|
|
|
+/** 判断标签页是否激活 */
|
|
|
+const isActive = (tag: TagView) => {
|
|
|
+ return tag.path === route.path
|
|
|
+}
|
|
|
+
|
|
|
+/** 判断标签页是否固定 */
|
|
|
+const isAffix = (tag: TagView) => {
|
|
|
+ return tag.meta?.affix
|
|
|
+}
|
|
|
+
|
|
|
+/** 筛选出固定标签页 */
|
|
|
+const filterAffixTags = (routes: RouteRecordRaw[], basePath = "/") => {
|
|
|
+ const tags: TagView[] = []
|
|
|
+ routes.forEach((route) => {
|
|
|
+ if (isAffix(route)) {
|
|
|
+ const tagPath = path.resolve(basePath, route.path)
|
|
|
+ tags.push({
|
|
|
+ fullPath: tagPath,
|
|
|
+ path: tagPath,
|
|
|
+ name: route.name,
|
|
|
+ meta: { ...route.meta }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (route.children) {
|
|
|
+ const childTags = filterAffixTags(route.children, route.path)
|
|
|
+ tags.push(...childTags)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return tags
|
|
|
+}
|
|
|
+
|
|
|
+/** 初始化标签页 */
|
|
|
+const initTags = () => {
|
|
|
+ affixTags = filterAffixTags(permissionStore.routes)
|
|
|
+ for (const tag of affixTags) {
|
|
|
+ // 必须含有 name 属性
|
|
|
+ tag.name && tagsViewStore.addVisitedView(tag)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 添加标签页 */
|
|
|
+const addTags = (route: RouteLocationNormalizedLoaded) => {
|
|
|
+ if (route.name) {
|
|
|
+ tagsViewStore.addVisitedView(route)
|
|
|
+ tagsViewStore.addCachedView(route)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 刷新当前正在右键操作的标签页 */
|
|
|
+const refreshSelectedTag = (view: TagView) => {
|
|
|
+ tagsViewStore.delCachedView(view)
|
|
|
+ router.replace({ path: "/redirect" + view.path, query: view.query })
|
|
|
+}
|
|
|
+
|
|
|
+/** 关闭当前正在右键操作的标签页 */
|
|
|
+const closeSelectedTag = (view: TagView) => {
|
|
|
+ tagsViewStore.delVisitedView(view)
|
|
|
+ tagsViewStore.delCachedView(view)
|
|
|
+ isActive(view) && toLastView(tagsViewStore.visitedViews, view)
|
|
|
+}
|
|
|
+
|
|
|
+/** 关闭其他标签页 */
|
|
|
+const closeOthersTags = () => {
|
|
|
+ const fullPath = selectedTag.value.fullPath
|
|
|
+ if (fullPath !== route.path && fullPath !== undefined) {
|
|
|
+ router.push(fullPath)
|
|
|
+ }
|
|
|
+ tagsViewStore.delOthersVisitedViews(selectedTag.value)
|
|
|
+ tagsViewStore.delOthersCachedViews(selectedTag.value)
|
|
|
+}
|
|
|
+
|
|
|
+/** 关闭所有标签页 */
|
|
|
+const closeAllTags = (view: TagView) => {
|
|
|
+ tagsViewStore.delAllVisitedViews()
|
|
|
+ tagsViewStore.delAllCachedViews()
|
|
|
+ if (affixTags.some((tag) => tag.path === route.path)) return
|
|
|
+ toLastView(tagsViewStore.visitedViews, view)
|
|
|
+}
|
|
|
+
|
|
|
+/** 跳转到最后一个标签页 */
|
|
|
+const toLastView = (visitedViews: TagView[], view: TagView) => {
|
|
|
+ const latestView = visitedViews.slice(-1)[0]
|
|
|
+ const fullPath = latestView?.fullPath
|
|
|
+ if (fullPath !== undefined) {
|
|
|
+ router.push(fullPath)
|
|
|
+ } else {
|
|
|
+ // 如果 TagsView 全部被关闭了,则默认重定向到主页
|
|
|
+ if (view.name === "Dashboard") {
|
|
|
+ // 重新加载主页
|
|
|
+ router.push({ path: "/redirect" + view.path, query: view.query })
|
|
|
+ } else {
|
|
|
+ router.push("/")
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 打开右键菜单面板 */
|
|
|
+const openMenu = (tag: TagView, e: MouseEvent) => {
|
|
|
+ const menuMinWidth = 105
|
|
|
+ // 当前组件距离浏览器左端的距离
|
|
|
+ const offsetLeft = instance!.proxy!.$el.getBoundingClientRect().left
|
|
|
+ // 当前组件宽度
|
|
|
+ const offsetWidth = instance!.proxy!.$el.offsetWidth
|
|
|
+ // 面板的最大左边距
|
|
|
+ const maxLeft = offsetWidth - menuMinWidth
|
|
|
+ // 面板距离鼠标指针的距离
|
|
|
+ const left15 = e.clientX - offsetLeft + 15
|
|
|
+ left.value = left15 > maxLeft ? maxLeft : left15
|
|
|
+ top.value = e.clientY
|
|
|
+ // 显示面板
|
|
|
+ visible.value = true
|
|
|
+ // 更新当前正在右键操作的标签页
|
|
|
+ selectedTag.value = tag
|
|
|
+}
|
|
|
+
|
|
|
+/** 关闭右键菜单面板 */
|
|
|
+const closeMenu = () => {
|
|
|
+ visible.value = false
|
|
|
+}
|
|
|
+
|
|
|
+watch(visible, (value) => {
|
|
|
+ value ? document.body.addEventListener("click", closeMenu) : document.body.removeEventListener("click", closeMenu)
|
|
|
+})
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ initTags()
|
|
|
+ /** 监听路由变化 */
|
|
|
+ listenerRouteChange(async (route) => {
|
|
|
+ addTags(route)
|
|
|
+ }, true)
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="tags-view-container">
|
|
|
+ <ScrollPane class="tags-view-wrapper" :tag-refs="tagRefs">
|
|
|
+ <router-link
|
|
|
+ ref="tagRefs"
|
|
|
+ v-for="tag in tagsViewStore.visitedViews"
|
|
|
+ :key="tag.path"
|
|
|
+ :class="{ active: isActive(tag) }"
|
|
|
+ class="tags-view-item"
|
|
|
+ :to="{ path: tag.path, query: tag.query }"
|
|
|
+ @click.middle="!isAffix(tag) && closeSelectedTag(tag)"
|
|
|
+ @contextmenu.prevent="openMenu(tag, $event)"
|
|
|
+ >
|
|
|
+ {{ tag.meta?.title }}
|
|
|
+ <el-icon v-if="!isAffix(tag)" :size="12" @click.prevent.stop="closeSelectedTag(tag)">
|
|
|
+ <Close />
|
|
|
+ </el-icon>
|
|
|
+ </router-link>
|
|
|
+ </ScrollPane>
|
|
|
+ <ul v-show="visible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }">
|
|
|
+ <li @click="refreshSelectedTag(selectedTag)">刷新</li>
|
|
|
+ <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">关闭</li>
|
|
|
+ <li @click="closeOthersTags">关闭其它</li>
|
|
|
+ <li @click="closeAllTags(selectedTag)">关闭所有</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.tags-view-container {
|
|
|
+ height: var(--v3-tagsview-height);
|
|
|
+ width: 100%;
|
|
|
+ background-color: var(--v3-header-bg-color);
|
|
|
+ box-shadow: 0 0 3px 0 #00000010;
|
|
|
+ .tags-view-wrapper {
|
|
|
+ .tags-view-item {
|
|
|
+ display: inline-block;
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+ height: 26px;
|
|
|
+ line-height: 26px;
|
|
|
+ border: 1px solid var(--v3-tagsview-tag-border-color);
|
|
|
+ border-radius: var(--v3-tagsview-tag-border-radius);
|
|
|
+ color: var(--v3-tagsview-tag-text-color);
|
|
|
+ background-color: var(--v3-tagsview-tag-bg-color);
|
|
|
+ padding: 0 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ margin-left: 5px;
|
|
|
+ margin-top: 4px;
|
|
|
+ &:first-of-type {
|
|
|
+ margin-left: 5px;
|
|
|
+ }
|
|
|
+ &:last-of-type {
|
|
|
+ margin-right: 5px;
|
|
|
+ }
|
|
|
+ &.active {
|
|
|
+ background-color: var(--v3-tagsview-tag-active-bg-color);
|
|
|
+ color: var(--v3-tagsview-tag-active-text-color);
|
|
|
+ border-color: var(--v3-tagsview-tag-active-border-color);
|
|
|
+ &::before {
|
|
|
+ content: "";
|
|
|
+ background-color: var(--v3-tagsview-tag-active-before-color);
|
|
|
+ display: inline-block;
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ position: relative;
|
|
|
+ margin-right: 2px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .el-icon {
|
|
|
+ margin: 0 2px;
|
|
|
+ vertical-align: middle;
|
|
|
+ border-radius: 50%;
|
|
|
+ &:hover {
|
|
|
+ background-color: var(--v3-tagsview-tag-icon-hover-bg-color);
|
|
|
+ color: var(--v3-tagsview-tag-icon-hover-color);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .contextmenu {
|
|
|
+ margin: 0;
|
|
|
+ background-color: #fff;
|
|
|
+ z-index: 3000;
|
|
|
+ position: absolute;
|
|
|
+ list-style-type: none;
|
|
|
+ padding: 5px 0;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 400;
|
|
|
+ color: #333;
|
|
|
+ box-shadow: 2px 2px 3px 0 #00000030;
|
|
|
+ li {
|
|
|
+ margin: 0;
|
|
|
+ padding: 7px 16px;
|
|
|
+ cursor: pointer;
|
|
|
+ &:hover {
|
|
|
+ background-color: #eee;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|