377 lines
8.1 KiB
Vue
377 lines
8.1 KiB
Vue
<script lang="js" setup>
|
|
import { useTabStore } from "@/store/tab";
|
|
import { useRoute } from "vue-router";
|
|
import emitter from "@/utils/emitter";
|
|
|
|
const { refs, setRefs } = useRefs();
|
|
const showScroll = ref(false);
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const tabStore = useTabStore();
|
|
|
|
const fullScreen = ref(false);
|
|
|
|
const emits = defineEmits(["fullScreen-change"]);
|
|
|
|
watch(
|
|
() => tabStore.activeTab,
|
|
(newVal) => {
|
|
nextTick(() => {
|
|
updateScroll();
|
|
|
|
const tabIndex = tabStore.findTabIndex(newVal);
|
|
moveScroll(tabIndex);
|
|
});
|
|
},
|
|
);
|
|
|
|
function useRefs() {
|
|
const refs = reactive({});
|
|
|
|
function setRefs(name) {
|
|
return (el) => {
|
|
refs[name] = el;
|
|
return () => refs[name];
|
|
};
|
|
}
|
|
|
|
return { refs, setRefs };
|
|
}
|
|
|
|
function updateScroll() {
|
|
const wrap = refs.scroller;
|
|
if (!wrap) return;
|
|
showScroll.value = wrap.scrollWidth > wrap.clientWidth;
|
|
}
|
|
|
|
function moveScroll(index) {
|
|
nextTick(() => {
|
|
const el = refs[`item-${index}`];
|
|
const wrap = refs.scroller;
|
|
if (!el || !wrap) return;
|
|
|
|
let attempts = 0;
|
|
const maxAttempts = 5;
|
|
|
|
const scrollToActiveTab = () => {
|
|
const scrollLeft =
|
|
el.getBoundingClientRect().left -
|
|
wrap.getBoundingClientRect().left +
|
|
wrap.scrollLeft -
|
|
wrap.clientWidth / 2 +
|
|
el.clientWidth / 2;
|
|
|
|
wrap.scrollLeft = scrollLeft;
|
|
|
|
if (
|
|
Math.abs(wrap.scrollLeft - scrollLeft) > 1 &&
|
|
attempts < maxAttempts
|
|
) {
|
|
attempts++;
|
|
requestAnimationFrame(scrollToActiveTab);
|
|
}
|
|
};
|
|
|
|
scrollToActiveTab();
|
|
});
|
|
}
|
|
|
|
// 切换标签
|
|
function switchTab(tab, index) {
|
|
router.push(tab.fullPath);
|
|
moveScroll(index);
|
|
}
|
|
|
|
const HOME_PATH = "/";
|
|
|
|
// 关闭标签
|
|
async function closeTab(tab) {
|
|
if (tab.fullPath === HOME_PATH) return; // ❗首页不能关
|
|
|
|
const activePath = await tabStore.closeTab(tab.fullPath);
|
|
|
|
if (activePath && route.fullPath !== activePath) {
|
|
router.push(activePath);
|
|
} else if (!activePath) {
|
|
router.push(HOME_PATH);
|
|
}
|
|
}
|
|
|
|
// 关闭左侧
|
|
function closeLeft(data) {
|
|
const currentIndex = tabStore.tabs.findIndex(
|
|
(item) => item.fullPath === data.fullPath,
|
|
);
|
|
if (currentIndex === -1) return;
|
|
|
|
tabStore.tabs
|
|
.slice(0, currentIndex)
|
|
.filter((item) => item.fullPath !== HOME_PATH) // ❗过滤首页
|
|
.forEach((item) => {
|
|
const index = tabStore.tabs.findIndex(
|
|
(t) => t.fullPath === item.fullPath,
|
|
);
|
|
if (index !== -1) tabStore.tabs.splice(index, 1);
|
|
|
|
if (item.fullPath === route.fullPath) {
|
|
router.push(data.fullPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 关闭右侧
|
|
function closeRight(data) {
|
|
const currentIndex = tabStore.tabs.findIndex(
|
|
(item) => item.fullPath === data.fullPath,
|
|
);
|
|
if (currentIndex === -1) return;
|
|
|
|
tabStore.tabs
|
|
.slice(currentIndex + 1)
|
|
.filter((item) => item.fullPath !== HOME_PATH) // ❗过滤首页
|
|
.forEach((item) => {
|
|
const index = tabStore.tabs.findIndex(
|
|
(t) => t.fullPath === item.fullPath,
|
|
);
|
|
if (index !== -1) tabStore.tabs.splice(index, 1);
|
|
|
|
if (item.fullPath === route.fullPath) {
|
|
router.push(data.fullPath);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 关闭其他
|
|
function closeOther(tab) {
|
|
tabStore.tabs = tabStore.tabs.filter(
|
|
(t) => t.fullPath === tab.fullPath || t.fullPath === HOME_PATH, // ❗保留首页
|
|
);
|
|
|
|
router.push(tab.fullPath);
|
|
}
|
|
|
|
// 关闭全部
|
|
function closeAll() {
|
|
tabStore.tabs = tabStore.tabs.filter(
|
|
(t) => t.fullPath === HOME_PATH, // ❗只保留首页
|
|
);
|
|
|
|
router.push(HOME_PATH);
|
|
}
|
|
function toHome() {
|
|
router.push("/");
|
|
}
|
|
|
|
function refreshPage() {
|
|
emitter.emit("view.refresh");
|
|
}
|
|
|
|
function fullScreenPage() {
|
|
fullScreen.value = !fullScreen.value;
|
|
|
|
emits("fullScreen-change", fullScreen.value);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="g3-tabs">
|
|
<div class="g3-tab-op">
|
|
<el-tooltip content="刷新">
|
|
<div class="g3-action-icon" @click="refreshPage">
|
|
<G3Icon icon="lucide:refresh-ccw" />
|
|
</div>
|
|
</el-tooltip>
|
|
|
|
<el-tooltip content="首页">
|
|
<div class="g3-action-icon" @click="toHome">
|
|
<G3Icon icon="lucide:house" />
|
|
</div>
|
|
</el-tooltip>
|
|
|
|
<el-tooltip :content="fullScreen ? '恢复' : '全屏'">
|
|
<div class="g3-action-icon" @click="fullScreenPage">
|
|
<G3Icon :icon="fullScreen ? 'lucide:minimize' : 'lucide:maximize'" />
|
|
</div>
|
|
</el-tooltip>
|
|
</div>
|
|
|
|
<div class="g3-divider"></div>
|
|
|
|
<div v-if="showScroll" class="g3-action-icon g3-scroll-more">
|
|
<G3Icon icon="lucide:chevrons-left" />
|
|
</div>
|
|
|
|
<div class="g3-tab-container">
|
|
<div :ref="setRefs('scroller')" class="app-tab-scroller">
|
|
<el-dropdown
|
|
v-for="(item, index) in tabStore.tabs"
|
|
:key="item.fullPath"
|
|
trigger="contextmenu"
|
|
:persistent="false"
|
|
>
|
|
<template #dropdown>
|
|
<el-dropdown-menu v-if="item.fullPath !== '/'">
|
|
<el-dropdown-item @click="closeTab(item)">
|
|
关闭
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="closeOther(item)">
|
|
关闭其他
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="closeLeft(item)">
|
|
关闭左侧
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="closeRight(item)">
|
|
关闭右侧
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="closeAll"> 关闭所有 </el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
<div
|
|
:ref="setRefs(`item-${index}`)"
|
|
class="app-tab-item"
|
|
:class="{ active: item.fullPath === tabStore.activeTab }"
|
|
@click="switchTab(item)"
|
|
>
|
|
<span class="label">{{ item.title }}</span>
|
|
<G3Icon
|
|
v-if="item.fullPath !== '/'"
|
|
icon="lucide:x"
|
|
size="14"
|
|
class="icon"
|
|
@click.stop="closeTab(item)"
|
|
/>
|
|
</div>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="showScroll"
|
|
class="g3-action-icon g3-scroll-more"
|
|
style="margin: 0px 10px"
|
|
>
|
|
<G3Icon icon="lucide:chevrons-right" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.g3-tabs {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 38px;
|
|
padding-left: 10px;
|
|
user-select: none;
|
|
background-color: #fff;
|
|
border-bottom: 1px solid #e4e4e7;
|
|
}
|
|
|
|
.g3-tab-op {
|
|
display: flex;
|
|
align-items: center;
|
|
list-style: none;
|
|
}
|
|
|
|
.g3-action-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 28px;
|
|
width: 28px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
transition: all 0.2s ease-in-out;
|
|
outline: none;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
margin-right: 8px;
|
|
&:hover {
|
|
border-color: var(--border-color-hover);
|
|
}
|
|
|
|
&:last-child {
|
|
margin-right: 0px;
|
|
}
|
|
}
|
|
|
|
.g3-divider {
|
|
width: 1px;
|
|
height: 18px;
|
|
background-color: #ddd;
|
|
margin: 0px 12px;
|
|
}
|
|
|
|
.g3-tab-container {
|
|
height: 30px;
|
|
flex: 1;
|
|
position: relative;
|
|
|
|
overflow: hidden;
|
|
|
|
:deep(.el-dropdown) {
|
|
margin-right: 12px;
|
|
&:last-child {
|
|
margin-right: 0px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.app-tab-scroller {
|
|
height: 100%;
|
|
width: 100%;
|
|
white-space: nowrap;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
scrollbar-width: none;
|
|
overflow: hidden;
|
|
scroll-behavior: smooth;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.app-tab-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
height: 28px;
|
|
padding: 0px 8px;
|
|
cursor: pointer;
|
|
color: #18181b;
|
|
border-radius: 5px;
|
|
border: 1px solid var(--border-color);
|
|
background-color: #fff;
|
|
|
|
.label {
|
|
font-size: 14px;
|
|
line-height: 1;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.icon {
|
|
margin-left: 5px;
|
|
}
|
|
|
|
&:hover {
|
|
background-color: var(--el-color-primary-light-8);
|
|
}
|
|
|
|
&.active {
|
|
background-color: var(--el-color-primary-light-8);
|
|
.label {
|
|
color: var(--el-color-primary);
|
|
}
|
|
.icon {
|
|
color: var(--el-color-primary);
|
|
}
|
|
&:hover {
|
|
background-color: var(--el-color-primary-light-8);
|
|
}
|
|
}
|
|
}
|
|
|
|
.g3-scroll-more {
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
</style>
|