From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目
---
src/layout/components/TagsView/src/TagsView.vue | 661 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 661 insertions(+), 0 deletions(-)
diff --git a/src/layout/components/TagsView/src/TagsView.vue b/src/layout/components/TagsView/src/TagsView.vue
new file mode 100644
index 0000000..69f94bf
--- /dev/null
+++ b/src/layout/components/TagsView/src/TagsView.vue
@@ -0,0 +1,661 @@
+<script lang="ts" setup>
+import { computed, nextTick, onMounted, ref, unref, watch } from 'vue'
+import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
+import { useRouter } from 'vue-router'
+import { usePermissionStore } from '@/store/modules/permission'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { useAppStore } from '@/store/modules/app'
+import { useI18n } from '@/hooks/web/useI18n'
+import { filterAffixTags } from './helper'
+import { ContextMenu, ContextMenuExpose } from '@/layout/components/ContextMenu'
+import { useDesign } from '@/hooks/web/useDesign'
+import { useTemplateRefsList } from '@vueuse/core'
+import { ElScrollbar } from 'element-plus'
+import { useScrollTo } from '@/hooks/event/useScrollTo'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'TagsView' })
+
+const { getPrefixCls } = useDesign()
+
+const prefixCls = getPrefixCls('tags-view')
+
+const { t } = useI18n()
+
+const { currentRoute, push } = useRouter()
+
+const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage } = useTagsView()
+
+const permissionStore = usePermissionStore()
+
+const routers = computed(() => permissionStore.getRouters)
+
+const tagsViewStore = useTagsViewStore()
+
+const visitedViews = computed(() => tagsViewStore.getVisitedViews)
+
+const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([])
+
+const selectedTag = computed(() => tagsViewStore.getSelectedTag)
+
+const setSelectTag = tagsViewStore.setSelectedTag
+
+const appStore = useAppStore()
+
+const tagsViewImmerse = computed(() => appStore.getTagsViewImmerse)
+
+const tagsViewIcon = computed(() => appStore.getTagsViewIcon)
+
+const isDark = computed(() => appStore.getIsDark)
+
+// 鍒濆鍖杢ag
+const initTags = () => {
+ affixTagArr.value = filterAffixTags(unref(routers))
+ for (const tag of unref(affixTagArr)) {
+ // Must have tag name
+ if (tag.name) {
+ tagsViewStore.addVisitedView(cloneDeep(tag))
+ }
+ }
+}
+
+// 鏂板tag
+const addTags = () => {
+ const { name } = unref(currentRoute)
+ if (name) {
+ setSelectTag(unref(currentRoute))
+ tagsViewStore.addView(unref(currentRoute))
+ }
+}
+
+// 鍏抽棴閫変腑鐨則ag
+const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => {
+ closeCurrent(view, () => {
+ if (isActive(view)) {
+ toLastView()
+ }
+ })
+}
+
+// 鍘绘渶鍚庝竴涓�
+const toLastView = () => {
+ const visitedViews = tagsViewStore.getVisitedViews
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ push(latestView)
+ } else {
+ if (
+ unref(currentRoute).path === permissionStore.getAddRouters[0].path ||
+ unref(currentRoute).path === permissionStore.getAddRouters[0].redirect
+ ) {
+ addTags()
+ return
+ }
+ // You can set another route
+ push(permissionStore.getAddRouters[0].path)
+ }
+}
+
+// 鍏抽棴鍏ㄩ儴
+const closeAllTags = () => {
+ closeAll(() => {
+ toLastView()
+ })
+}
+
+// 鍏抽棴鍏跺畠
+const closeOthersTags = () => {
+ closeOther()
+}
+
+// 閲嶆柊鍔犺浇
+const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => {
+ refreshPage(view)
+}
+
+// 鍏抽棴宸︿晶
+const closeLeftTags = () => {
+ closeLeft()
+}
+
+// 鍏抽棴鍙充晶
+const closeRightTags = () => {
+ closeRight()
+}
+
+// 婊氬姩鍒伴�変腑鐨則ag
+const moveToCurrentTag = async () => {
+ await nextTick()
+ for (const v of unref(visitedViews)) {
+ if (v.fullPath === unref(currentRoute).fullPath) {
+ moveToTarget(v)
+ break
+ }
+ }
+}
+
+const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
+
+const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
+ const wrap$ = unref(scrollbarRef)?.wrapRef
+ let firstTag: Nullable<RouterLinkProps> = null
+ let lastTag: Nullable<RouterLinkProps> = null
+
+ const tagList = unref(tagLinksRefs)
+ // find first tag and last tag
+ if (tagList.length > 0) {
+ firstTag = tagList[0]
+ lastTag = tagList[tagList.length - 1]
+ }
+ if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
+ // 鐩存帴婊氬姩鍒�0鐨勪綅缃�
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: 0,
+ duration: 500
+ })
+ start()
+ } else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
+ // 婊氬姩鍒版渶鍚庣殑浣嶇疆
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: wrap$!.scrollWidth - wrap$!.offsetWidth,
+ duration: 500
+ })
+ start()
+ } else {
+ // find preTag and nextTag
+ const currentIndex: number = tagList.findIndex(
+ (item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
+ )
+ const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
+
+ const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
+ const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
+
+ // the tag's offsetLeft after of nextTag
+ const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
+
+ // the tag's offsetLeft before of prevTag
+ const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
+
+ if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
+ duration: 500
+ })
+ start()
+ } else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: beforePrevTagOffsetLeft,
+ duration: 500
+ })
+ start()
+ }
+ }
+}
+
+// 鏄惁鏄綋鍓峵ag
+const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
+ return route.fullPath === unref(currentRoute).fullPath
+}
+
+// 鎵�鏈夊彸閿彍鍗曠粍浠剁殑鍏冪礌
+const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
+
+// 鍙抽敭鑿滃崟鐘舵�佹敼鍙樼殑鏃跺��
+const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => {
+ if (visible) {
+ for (const v of unref(itemRefs)) {
+ const elDropdownMenuRef = v.elDropdownMenuRef
+ if (tagItem.fullPath !== v.tagItem.fullPath) {
+ elDropdownMenuRef?.handleClose()
+ setSelectTag(tagItem)
+ }
+ }
+ }
+}
+
+// elscroll 瀹炰緥
+const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
+
+// 淇濆瓨婊氬姩浣嶇疆
+const scrollLeftNumber = ref(0)
+
+const scroll = ({ scrollLeft }) => {
+ scrollLeftNumber.value = scrollLeft as number
+}
+
+// 绉诲姩鍒版煇涓綅缃�
+const move = (to: number) => {
+ const wrap$ = unref(scrollbarRef)?.wrapRef
+ const { start } = useScrollTo({
+ el: wrap$!,
+ position: 'scrollLeft',
+ to: unref(scrollLeftNumber) + to,
+ duration: 500
+ })
+ start()
+}
+
+const canShowIcon = (item: RouteLocationNormalizedLoaded) => {
+ if (
+ (item?.matched?.[1]?.meta?.icon && unref(tagsViewIcon)) ||
+ (item?.meta?.affix && unref(tagsViewIcon) && item?.meta?.icon)
+ ) {
+ return true
+ }
+ return false
+}
+
+const closeTabOnMouseMidClick = (e: MouseEvent, item) => {
+ // 涓敭锛歜utton === 1
+ if (e.button === 1) {
+ e.preventDefault()
+ e.stopPropagation()
+ closeSelectedTag(item)
+ }
+}
+
+onBeforeMount(() => {
+ initTags()
+ addTags()
+})
+
+watch(
+ () => currentRoute.value,
+ () => {
+ addTags()
+ moveToCurrentTag()
+ }
+)
+</script>
+
+<template>
+ <div
+ :id="prefixCls"
+ :class="prefixCls"
+ class="relative w-full flex bg-[#fff] dark:bg-[var(--el-bg-color)]"
+ >
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool ${prefixCls}__tool--first`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="move(-200)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:d-arrow-left"
+ />
+ </span>
+ <div class="flex-1 overflow-hidden">
+ <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
+ <div class="h-[var(--tags-view-height)] flex">
+ <ContextMenu
+ v-for="item in visitedViews"
+ :key="item.fullPath"
+ :ref="itemRefs.set"
+ @auxclick="closeTabOnMouseMidClick($event, item)"
+ :class="[
+ `${prefixCls}__item`,
+ tagsViewImmerse ? `${prefixCls}__item--immerse` : '',
+ tagsViewIcon ? `${prefixCls}__item--icon` : '',
+ tagsViewImmerse && tagsViewIcon ? `${prefixCls}__item--immerse--icon` : '',
+ item?.meta?.affix ? `${prefixCls}__item--affix` : '',
+ {
+ 'is-active': isActive(item)
+ }
+ ]"
+ :schema="[
+ {
+ icon: 'ep:refresh',
+ label: t('common.reload'),
+ disabled: selectedTag?.fullPath !== item.fullPath,
+ command: () => {
+ refreshSelectedTag(item)
+ }
+ },
+ {
+ icon: 'ep:close',
+ label: t('common.closeTab'),
+ disabled: !!visitedViews?.length && selectedTag?.meta.affix,
+ command: () => {
+ closeSelectedTag(item)
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:d-arrow-left',
+ label: t('common.closeTheLeftTab'),
+ disabled:
+ !!visitedViews?.length &&
+ (item.fullPath === visitedViews[0].fullPath ||
+ selectedTag?.fullPath !== item.fullPath),
+ command: () => {
+ closeLeftTags()
+ }
+ },
+ {
+ icon: 'ep:d-arrow-right',
+ label: t('common.closeTheRightTab'),
+ disabled:
+ !!visitedViews?.length &&
+ (item.fullPath === visitedViews[visitedViews.length - 1].fullPath ||
+ selectedTag?.fullPath !== item.fullPath),
+ command: () => {
+ closeRightTags()
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:discount',
+ label: t('common.closeOther'),
+ disabled: selectedTag?.fullPath !== item.fullPath,
+ command: () => {
+ closeOthersTags()
+ }
+ },
+ {
+ icon: 'ep:minus',
+ label: t('common.closeAll'),
+ command: () => {
+ closeAllTags()
+ }
+ }
+ ]"
+ :tag-item="item"
+ @visible-change="visibleChange"
+ >
+ <div>
+ <router-link :ref="tagLinksRefs.set" v-slot="{ navigate }" :to="{ ...item }" custom>
+ <div
+ :class="`h-full flex items-center justify-center whitespace-nowrap pl-15px ${prefixCls}__item--label`"
+ @click="navigate"
+ >
+ <Icon
+ v-if="
+ tagsViewIcon &&
+ (item?.meta?.icon ||
+ (item?.matched &&
+ item.matched[0] &&
+ item.matched[item.matched.length - 1].meta?.icon))
+ "
+ :icon="item?.meta?.icon || item.matched[item.matched.length - 1].meta.icon"
+ :size="12"
+ class="mr-5px"
+ />
+ {{
+ t(item?.meta?.title as string) +
+ (item?.meta?.titleSuffix ? ` (${item?.meta?.titleSuffix})` : '')
+ }}
+ <Icon
+ :class="`${prefixCls}__item--close`"
+ :size="12"
+ color="#333"
+ icon="ep:close"
+ @click.prevent.stop="closeSelectedTag(item)"
+ />
+ </div>
+ </router-link>
+ </div>
+ </ContextMenu>
+ </div>
+ </ElScrollbar>
+ </div>
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="move(200)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:d-arrow-right"
+ />
+ </span>
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ @click="refreshSelectedTag(selectedTag)"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:refresh-right"
+ />
+ </span>
+ <ContextMenu
+ :schema="[
+ {
+ icon: 'ep:refresh',
+ label: t('common.reload'),
+ command: () => {
+ refreshSelectedTag(selectedTag)
+ }
+ },
+ {
+ icon: 'ep:close',
+ label: t('common.closeTab'),
+ disabled: !!visitedViews?.length && selectedTag?.meta.affix,
+ command: () => {
+ closeSelectedTag(selectedTag!)
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:d-arrow-left',
+ label: t('common.closeTheLeftTab'),
+ disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath,
+ command: () => {
+ closeLeftTags()
+ }
+ },
+ {
+ icon: 'ep:d-arrow-right',
+ label: t('common.closeTheRightTab'),
+ disabled:
+ !!visitedViews?.length &&
+ selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath,
+ command: () => {
+ closeRightTags()
+ }
+ },
+ {
+ divided: true,
+ icon: 'ep:discount',
+ label: t('common.closeOther'),
+ command: () => {
+ closeOthersTags()
+ }
+ },
+ {
+ icon: 'ep:minus',
+ label: t('common.closeAll'),
+ command: () => {
+ closeAllTags()
+ }
+ }
+ ]"
+ trigger="click"
+ >
+ <span
+ :class="tagsViewImmerse ? '' : `${prefixCls}__tool`"
+ class="block h-[var(--tags-view-height)] w-[var(--tags-view-height)] flex cursor-pointer items-center justify-center"
+ >
+ <Icon
+ :hover-color="isDark ? '#fff' : 'var(--el-color-black)'"
+ color="var(--el-text-color-placeholder)"
+ icon="ep:menu"
+ />
+ </span>
+ </ContextMenu>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-tags-view;
+
+.#{$prefix-cls} {
+ :deep(.#{$elNamespace}-scrollbar__view) {
+ height: 100%;
+ }
+
+ &__tool {
+ position: relative;
+
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-left: 1px solid var(--el-border-color);
+ content: '';
+ }
+
+ &--first {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ border-right: 1px solid var(--el-border-color);
+ border-left: none;
+ content: '';
+ }
+ }
+ }
+
+ &__item {
+ position: relative;
+ top: 3px;
+ height: calc(100% - 6px);
+ padding-right: 15px;
+ margin-left: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ border: 1px solid #d9d9d9;
+ border-radius: 2px;
+ box-sizing: border-box;
+
+ &--close {
+ position: absolute;
+ top: 50%;
+ right: 5px;
+ display: none;
+ transform: translate(0, -50%);
+ }
+
+ &:not(.#{$prefix-cls}__item--affix):hover {
+ .#{$prefix-cls}__item--close {
+ display: block;
+ }
+ }
+ }
+
+ &__item--icon {
+ padding-right: 20px;
+ }
+
+ &__item:not(.is-active) {
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+
+ &__item.is-active {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+ border: 1px solid var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+
+ &__item--immerse {
+ top: 2px;
+ height: calc(100% - 3px);
+ padding-right: 35px;
+ margin: 0 -10px;
+ border: none !important;
+ -webkit-mask-box-image: url("data:image/svg+xml,%3Csvg width='68' height='34' viewBox='0 0 68 34' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='m27,0c-7.99582,0 -11.95105,0.00205 -12,12l0,6c0,8.284 -0.48549,16.49691 -8.76949,16.49691l54.37857,-0.11145c-8.284,0 -8.60908,-8.10146 -8.60908,-16.38546l0,-6c0.11145,-12.08445 -4.38441,-12 -12,-12l-13,0z' fill='%23409eff'/%3E%3C/svg%3E")
+ 12 27 15;
+
+ .#{$prefix-cls}__item--label {
+ padding-left: 35px;
+ }
+
+ .#{$prefix-cls}__item--close {
+ right: 20px;
+ }
+ }
+
+ &__item--immerse--icon {
+ padding-right: 35px;
+ }
+
+ &__item--immerse:not(.is-active) {
+ &:hover {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+ }
+}
+
+.dark {
+ .#{$prefix-cls} {
+ &__tool {
+ &--first {
+ &::after {
+ display: none;
+ }
+ }
+ }
+
+ &__item {
+ border: 1px solid var(--el-border-color);
+ }
+
+ &__item:not(.is-active) {
+ &:hover {
+ color: var(--el-color-primary);
+ }
+ }
+
+ &__item.is-active {
+ color: var(--el-color-white);
+ background-color: var(--el-color-primary);
+ border: 1px solid var(--el-color-primary);
+
+ .#{$prefix-cls}__item--close {
+ :deep(span) {
+ color: var(--el-color-white) !important;
+ }
+ }
+ }
+
+ &__item--immerse:not(.is-active) {
+ &:hover {
+ color: var(--el-color-white);
+ }
+ }
+ }
+}
+</style>
--
Gitblit v1.8.0