refactor: ♻️ (layouts)重构标签页组件 TagsView

- 使用 el-tag 组件替换自定义标签样式

- 改进右键菜单功能,使用 Teleport 组件优化显示位置,修复在混合布局下的位置显示错误

- 移除未使用的 layout 计算属性和冗余代码
This commit is contained in:
zimo493
2025-08-14 16:40:51 +08:00
parent b77ea4ebf7
commit 9b8299e895

View File

@@ -1,56 +1,69 @@
<template> <template>
<div class="tags-container"> <div class="tags-container">
<!-- 水平滚动容器 --> <!-- 水平滚动容器 -->
<el-scrollbar ref="scrollbarRef" class="scroll-container" @wheel="handleScroll"> <el-scrollbar
<router-link ref="scrollbarRef"
v-for="tag in visitedViews" class="scroll-container"
ref="tagRef" :view-style="{ height: '100%' }"
:key="tag.fullPath" @wheel="handleScroll"
:class="['tags-item', { active: tagsViewStore.isActive(tag) }]" >
:to="{ path: tag.path, query: tag.query }" <div h-full flex-y-center gap-8px>
@click.middle="handleMiddleClick(tag)" <el-tag
@contextmenu.prevent="(event: MouseEvent) => openContextMenu(tag, event)" v-for="tag in visitedViews"
> :key="tag.fullPath"
<!-- 标签文本 --> h-26px
<span class="tag-text">{{ translateRouteTitle(tag.title) }}</span> cursor-pointer
<!-- 关闭按钮固定标签不显示 --> :closable="!tag.affix"
<span v-if="!tag.affix" class="tag-close-btn" @click.prevent.stop="closeSelectedTag(tag)"> :effect="tagsViewStore.isActive(tag) ? 'dark' : 'light'"
× :type="tagsViewStore.isActive(tag) ? 'primary' : 'info'"
</span> @click.middle="handleMiddleClick(tag)"
</router-link> @contextmenu.prevent="(event: MouseEvent) => openContextMenu(tag, event)"
@close="closeSelectedTag(tag)"
@click="
router.push({
path: tag.fullPath,
query: tag.query,
})
"
>
{{ translateRouteTitle(tag.title) }}
</el-tag>
</div>
</el-scrollbar> </el-scrollbar>
<!-- 标签右键菜单 --> <!-- 标签右键菜单 -->
<ul <Teleport to="body">
v-show="contextMenu.visible" <ul
class="contextmenu" v-show="contextMenu.visible"
:style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }" class="contextmenu"
> :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
<li @click="refreshSelectedTag(selectedTag)"> >
<div class="i-svg:refresh" /> <li @click="refreshSelectedTag(selectedTag)">
刷新 <div class="i-svg:refresh" />
</li> 刷新
<li v-if="!selectedTag?.affix" @click="closeSelectedTag(selectedTag)"> </li>
<div class="i-svg:close" /> <li v-if="!selectedTag?.affix" @click="closeSelectedTag(selectedTag)">
关闭 <div class="i-svg:close" />
</li> 关闭
<li @click="closeOtherTags"> </li>
<div class="i-svg:close_other" /> <li @click="closeOtherTags">
关闭其它 <div class="i-svg:close_other" />
</li> 关闭其它
<li v-if="!isFirstView" @click="closeLeftTags"> </li>
<div class="i-svg:close_left" /> <li v-if="!isFirstView" @click="closeLeftTags">
关闭左侧 <div class="i-svg:close_left" />
</li> 关闭左侧
<li v-if="!isLastView" @click="closeRightTags"> </li>
<div class="i-svg:close_right" /> <li v-if="!isLastView" @click="closeRightTags">
关闭右侧 <div class="i-svg:close_right" />
</li> 关闭右侧
<li @click="closeAllTags(selectedTag)"> </li>
<div class="i-svg:close_all" /> <li @click="closeAllTags(selectedTag)">
关闭所有 <div class="i-svg:close_all" />
</li> 关闭所有
</ul> </li>
</ul>
</Teleport>
</div> </div>
</template> </template>
@@ -58,8 +71,7 @@
import { useRoute, useRouter, type RouteRecordRaw } from "vue-router"; import { useRoute, useRouter, type RouteRecordRaw } from "vue-router";
import { resolve } from "path-browserify"; import { resolve } from "path-browserify";
import { translateRouteTitle } from "@/utils/i18n"; import { translateRouteTitle } from "@/utils/i18n";
import { usePermissionStore, useTagsViewStore, useSettingsStore } from "@/store"; import { usePermissionStore, useTagsViewStore } from "@/store";
import { LayoutMode } from "@/enums";
interface ContextMenu { interface ContextMenu {
visible: boolean; visible: boolean;
@@ -67,17 +79,12 @@ interface ContextMenu {
y: number; y: number;
} }
const instance = getCurrentInstance();
const proxy = instance?.proxy;
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
// 状态管理 // 状态管理
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const settingsStore = useSettingsStore();
// const { visitedViews } = storeToRefs(tagsViewStore);
const visitedViews = ref<TagView[]>([]); const visitedViews = ref<TagView[]>([]);
@@ -88,8 +95,6 @@ watchEffect(() => {
tagsViewStore.setCacheRoutes(names, permissionStore.allCacheRoutes); tagsViewStore.setCacheRoutes(names, permissionStore.allCacheRoutes);
}); });
const layout = computed(() => settingsStore.layout);
// 当前选中的标签 // 当前选中的标签
const selectedTag = ref<TagView | null>(null); const selectedTag = ref<TagView | null>(null);
@@ -224,19 +229,8 @@ const handleMiddleClick = (tag: TagView) => {
* 打开右键菜单 * 打开右键菜单
*/ */
const openContextMenu = (tag: TagView, event: MouseEvent) => { const openContextMenu = (tag: TagView, event: MouseEvent) => {
const MENU_MIN_WIDTH = 105; contextMenu.x = event.clientX;
const MENU_MARGIN = 15; contextMenu.y = event.clientY;
const containerRect = proxy?.$el.getBoundingClientRect();
const offsetLeft = containerRect?.left || 0;
const containerWidth = proxy?.$el.offsetWidth || 0;
const maxLeft = containerWidth - MENU_MIN_WIDTH;
const leftPosition = event.clientX - offsetLeft + MENU_MARGIN;
contextMenu.x = Math.min(leftPosition, maxLeft);
// 混合模式下,需要减去顶部菜单(fixed)的高度
contextMenu.y = layout.value === LayoutMode.MIX ? event.clientY - 50 : event.clientY;
contextMenu.visible = true; contextMenu.visible = true;
selectedTag.value = tag; selectedTag.value = tag;
@@ -386,113 +380,37 @@ useContextMenuManager();
.tags-container { .tags-container {
width: 100%; width: 100%;
height: $tags-view-height; height: $tags-view-height;
background-color: var(--el-bg-color); padding: 0 15px;
border: 1px solid var(--el-border-color-light); border-top: 1px solid var(--el-border-color-light);
box-shadow: 0 1px 1px var(--el-box-shadow-light);
.scroll-container { .scroll-container {
white-space: nowrap; white-space: nowrap;
} }
}
.contextmenu {
position: absolute;
z-index: 3000;
padding: 5px 0;
margin: 0;
font-size: 12px;
font-weight: 400;
color: var(--el-text-color-primary);
list-style-type: none;
background: var(--el-bg-color);
border-radius: 4px;
box-shadow: var(--el-box-shadow-light);
.tags-item { li {
position: relative; display: flex;
display: inline-flex; gap: 8px;
align-items: center; align-items: center;
height: 26px; padding: 7px 16px;
padding: 0 8px;
margin-top: 4px;
margin-left: 5px;
font-size: 12px;
line-height: 26px;
color: var(--el-text-color-primary);
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
transition: all 0.2s ease;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
.tag-text {
display: inline-block;
vertical-align: middle;
}
.tag-close-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 5px;
font-size: 12px;
font-weight: bold;
color: var(--el-text-color-secondary);
cursor: pointer;
border-radius: 50%;
transition: all 0.2s ease;
&:hover {
color: var(--el-color-white);
background-color: var(--el-text-color-placeholder);
}
}
&.active {
color: var(--el-color-white);
background-color: var(--el-color-primary);
border-color: var(--el-color-primary);
&::before {
position: relative;
display: inline-block;
width: 8px;
height: 8px;
margin-right: 2px;
content: "";
background: var(--el-color-white);
border-radius: 50%;
}
.tag-close-btn {
color: var(--el-color-white);
&:hover {
color: var(--el-color-white);
background-color: rgba(255, 255, 255, 0.3);
}
}
}
}
.contextmenu {
position: absolute;
z-index: 3000;
padding: 5px 0;
margin: 0; margin: 0;
font-size: 12px; cursor: pointer;
font-weight: 400; transition: background-color 0.2s;
color: var(--el-text-color-primary);
list-style-type: none;
background: var(--el-bg-color);
border-radius: 4px;
box-shadow: var(--el-box-shadow-light);
li { &:hover {
display: flex; background: var(--el-fill-color-light);
gap: 8px;
align-items: center;
padding: 7px 16px;
margin: 0;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background: var(--el-fill-color-light);
}
} }
} }
} }