mirror of
https://gitee.com/fantastic-admin/basic.git
synced 2024-11-29 18:48:31 +08:00
feat: 增加标签页支持
This commit is contained in:
parent
52b25d1eae
commit
a55f3016ce
@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.17",
|
||||
"@imengyu/vue3-context-menu": "^1.3.6",
|
||||
"@vueuse/core": "^10.7.1",
|
||||
"@vueuse/integrations": "^10.7.1",
|
||||
"axios": "^1.6.5",
|
||||
|
@ -8,6 +8,9 @@ dependencies:
|
||||
'@headlessui/vue':
|
||||
specifier: ^1.7.17
|
||||
version: 1.7.17(vue@3.4.8)
|
||||
'@imengyu/vue3-context-menu':
|
||||
specifier: ^1.3.6
|
||||
version: 1.3.6
|
||||
'@vueuse/core':
|
||||
specifier: ^10.7.1
|
||||
version: 10.7.1(vue@3.4.8)
|
||||
@ -2689,6 +2692,10 @@ packages:
|
||||
vue: 3.4.8(typescript@5.3.3)
|
||||
dev: true
|
||||
|
||||
/@imengyu/vue3-context-menu@1.3.6:
|
||||
resolution: {integrity: sha512-xjA37derX5pVKyWjMe0mzbTEIBG6YJRYEMdLBjN3thoSVUuBTRObJ2Uc0sejU6QqM31pSr3PbITBgnXV6P1heg==}
|
||||
dev: false
|
||||
|
||||
/@isaacs/cliui@8.0.2:
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
@ -8,6 +8,8 @@
|
||||
--g-sub-sidebar-collapse-width: 64px;
|
||||
// 侧边栏 Logo 区域高度
|
||||
--g-sidebar-logo-height: 50px;
|
||||
// 标签栏高度
|
||||
--g-tabbar-height: 50px;
|
||||
// 工具栏高度
|
||||
--g-toolbar-height: 50px;
|
||||
}
|
||||
|
@ -173,6 +173,29 @@ function handleCopy() {
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="divider">
|
||||
标签栏
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用
|
||||
</div>
|
||||
<HToggle v-model="settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否显示图标
|
||||
</div>
|
||||
<HToggle v-model="settingsStore.settings.tabbar.enableIcon" :disabled="!settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="label">
|
||||
是否启用快捷键
|
||||
</div>
|
||||
<HToggle v-model="settingsStore.settings.tabbar.enableHotkeys" :disabled="!settingsStore.settings.tabbar.enable" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider">
|
||||
工具栏
|
||||
</div>
|
||||
|
@ -50,6 +50,38 @@ onMounted(() => {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys">
|
||||
<h2 class="m-0 text-lg font-bold">
|
||||
标签栏
|
||||
</h2>
|
||||
<ul class="list-none pl-4 text-sm">
|
||||
<li class="py-1">
|
||||
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
|
||||
<HKbd>←</HKbd>
|
||||
切换到上一个标签页
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
|
||||
<HKbd>→</HKbd>
|
||||
切换到下一个标签页
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
|
||||
<HKbd>W</HKbd>
|
||||
关闭当前标签页
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
|
||||
<HKbd>1~9</HKbd>
|
||||
切换到第 n 个标签页
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<HKbd>{{ settingsStore.os === 'mac' ? '⌥' : 'Alt' }}</HKbd>
|
||||
<HKbd>0</HKbd>
|
||||
切换到最后一个标签页
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HDialog>
|
||||
|
464
src/layouts/components/Topbar/Tabbar/index.vue
Normal file
464
src/layouts/components/Topbar/Tabbar/index.vue
Normal file
@ -0,0 +1,464 @@
|
||||
<script setup lang="ts">
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import '@imengyu/vue3-context-menu/lib/vue3-context-menu.css'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import Message from 'vue-m-message'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import useTabbarStore from '@/store/modules/tabbar'
|
||||
import type { Tabbar } from '#/global'
|
||||
|
||||
defineOptions({
|
||||
name: 'Tabbar',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
|
||||
const tabbar = useTabbar()
|
||||
const mainPage = useMainPage()
|
||||
|
||||
const keys = useMagicKeys({ reactive: true })
|
||||
|
||||
const activedTabId = computed(() => tabbar.getId())
|
||||
|
||||
const tabsRef = ref()
|
||||
const tabContainerRef = ref()
|
||||
const tabRef = shallowRef<HTMLElement[]>([])
|
||||
onBeforeUpdate(() => {
|
||||
tabRef.value = []
|
||||
})
|
||||
|
||||
watch(() => route, (val) => {
|
||||
if (settingsStore.settings.tabbar.enable) {
|
||||
tabbarStore.add(val).then(() => {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
|
||||
if (index !== -1) {
|
||||
scrollTo(tabRef.value[index].offsetLeft)
|
||||
tabbarScrollTip()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
})
|
||||
function tabbarScrollTip() {
|
||||
if (tabContainerRef.value.$el.clientWidth > tabsRef.value.clientWidth && localStorage.getItem('tabbarScrollTip') === undefined) {
|
||||
localStorage.setItem('tabbarScrollTip', '')
|
||||
Message.info('标签栏数量超过展示区域范围,可以将鼠标移到标签栏上,通过鼠标滚轮滑动浏览', {
|
||||
title: '温馨提示',
|
||||
duration: 5000,
|
||||
closable: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
function handlerMouserScroll(event: WheelEvent) {
|
||||
if (event.deltaY || event.detail !== 0) {
|
||||
tabsRef.value.scrollBy({
|
||||
left: (event.deltaY || event.detail) > 0 ? 50 : -50,
|
||||
})
|
||||
}
|
||||
}
|
||||
function scrollTo(offsetLeft: number) {
|
||||
tabsRef.value.scrollTo({
|
||||
left: offsetLeft - 50,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
function onTabbarContextmenu(event: MouseEvent, routeItem: Tabbar.recordRaw) {
|
||||
event.preventDefault()
|
||||
ContextMenu.showContextMenu({
|
||||
x: event.x,
|
||||
y: event.y,
|
||||
zIndex: 1000,
|
||||
iconFontClass: '',
|
||||
customClass: 'contextmenu-custom',
|
||||
items: [
|
||||
{
|
||||
label: '重新加载',
|
||||
icon: 'i-ri:refresh-line',
|
||||
disabled: routeItem.tabId !== activedTabId.value,
|
||||
onClick: () => mainPage.reload(),
|
||||
},
|
||||
{
|
||||
label: '关闭标签页',
|
||||
icon: 'i-ri:close-line',
|
||||
disabled: tabbarStore.list.length <= 1,
|
||||
divided: true,
|
||||
onClick: () => {
|
||||
tabbar.closeById(routeItem.tabId)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '关闭其他标签页',
|
||||
disabled: !tabbar.checkCloseOtherSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeOtherSide(routeItem.tabId)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '关闭左侧标签页',
|
||||
disabled: !tabbar.checkCloseLeftSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeLeftSide(routeItem.tabId)
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '关闭右侧标签页',
|
||||
disabled: !tabbar.checkCloseRightSide(routeItem.tabId),
|
||||
onClick: () => {
|
||||
tabbar.closeRightSide(routeItem.tabId)
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
hotkeys('alt+left,alt+right,alt+w,alt+1,alt+2,alt+3,alt+4,alt+5,alt+6,alt+7,alt+8,alt+9,alt+0', (e, handle) => {
|
||||
if (settingsStore.settings.tabbar.enable && settingsStore.settings.tabbar.enableHotkeys) {
|
||||
e.preventDefault()
|
||||
switch (handle.key) {
|
||||
// 切换到当前标签页紧邻的上一个标签页
|
||||
case 'alt+left':
|
||||
if (tabbarStore.list[0].tabId !== activedTabId.value) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
|
||||
router.push(tabbarStore.list[index - 1].fullPath)
|
||||
}
|
||||
break
|
||||
// 切换到当前标签页紧邻的下一个标签页
|
||||
case 'alt+right':
|
||||
if (tabbarStore.list.at(-1)?.tabId !== activedTabId.value) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === activedTabId.value)
|
||||
router.push(tabbarStore.list[index + 1].fullPath)
|
||||
}
|
||||
break
|
||||
// 关闭当前标签页
|
||||
case 'alt+w':
|
||||
tabbar.closeById(activedTabId.value)
|
||||
break
|
||||
// 切换到第 n 个标签页
|
||||
case 'alt+1':
|
||||
case 'alt+2':
|
||||
case 'alt+3':
|
||||
case 'alt+4':
|
||||
case 'alt+5':
|
||||
case 'alt+6':
|
||||
case 'alt+7':
|
||||
case 'alt+8':
|
||||
case 'alt+9':
|
||||
{
|
||||
const number = Number(handle.key.split('+')[1])
|
||||
tabbarStore.list[number - 1]?.fullPath && router.push(tabbarStore.list[number - 1].fullPath)
|
||||
break
|
||||
}
|
||||
// 切换到最后一个标签页
|
||||
case 'alt+0':
|
||||
router.push(tabbarStore.list[tabbarStore.list.length - 1].fullPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
onUnmounted(() => {
|
||||
hotkeys.unbind('alt+q,alt+e,alt+w,alt+1,alt+2,alt+3,alt+4,alt+5,alt+6,alt+7,alt+8,alt+9,alt+0')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tabbar-container">
|
||||
<div ref="tabsRef" class="tabs" @wheel.prevent="handlerMouserScroll">
|
||||
<TransitionGroup ref="tabContainerRef" name="tabbar" tag="div" class="tab-container">
|
||||
<div
|
||||
v-for="(element, index) in tabbarStore.list" :key="element.tabId"
|
||||
ref="tabRef" :data-index="index" class="tab" :class="{
|
||||
actived: element.tabId === activedTabId,
|
||||
}" :title="typeof element?.title === 'function' ? element.title() : element.title" @click="router.push(element.fullPath)" @contextmenu="onTabbarContextmenu($event, element)"
|
||||
>
|
||||
<div class="tab-dividers" />
|
||||
<div class="tab-background" />
|
||||
<div class="tab-content">
|
||||
<div :key="element.tabId" class="title">
|
||||
<SvgIcon v-if="settingsStore.settings.tabbar.enableIcon && element.icon" :name="element.icon" class="icon" />
|
||||
{{ typeof element?.title === 'function' ? element.title() : element.title }}
|
||||
</div>
|
||||
<div v-if="tabbarStore.list.length > 1" class="action-icon">
|
||||
<SvgIcon name="i-ri:close-fill" @click.stop="tabbar.closeById(element.tabId)" />
|
||||
</div>
|
||||
<div v-show="keys.alt && index < 9" class="hotkey-number">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.mx-menu-ghost-host {
|
||||
z-index: 1000;
|
||||
|
||||
.mx-context-menu {
|
||||
--at-apply: fixed ring-1 ring-stone-2 dark:ring-stone-7 shadow-2xl;
|
||||
|
||||
background-color: var(--g-container-bg);
|
||||
|
||||
.mx-context-menu-items .mx-context-menu-item {
|
||||
--at-apply: transition-background-color;
|
||||
|
||||
&:not(.disabled):hover {
|
||||
--at-apply: cursor-pointer bg-stone-1 dark:bg-stone-9;
|
||||
}
|
||||
|
||||
span {
|
||||
color: initial;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 1em;
|
||||
margin-right: 10px;
|
||||
color: initial;
|
||||
}
|
||||
|
||||
&.disabled span,
|
||||
&.disabled .icon {
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.mx-context-menu-item-sperator {
|
||||
background-color: var(--g-container-bg);
|
||||
|
||||
&::after {
|
||||
--at-apply: bg-stone-2 dark:bg-stone-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tabbar-container {
|
||||
position: relative;
|
||||
height: var(--g-tabbar-height);
|
||||
background-color: var(--g-bg);
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.tabs {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
// firefox隐藏滚动条
|
||||
scrollbar-width: none;
|
||||
|
||||
// chrome隐藏滚动条
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: inline-block;
|
||||
|
||||
.tab {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
height: var(--g-tabbar-height);
|
||||
font-size: 14px;
|
||||
line-height: calc(var(--g-tabbar-height) - 2px);
|
||||
vertical-align: bottom;
|
||||
pointer-events: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.actived):hover {
|
||||
z-index: 3;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-tabbar-tab-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&.actived {
|
||||
z-index: 5;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
& + .tab .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.title,
|
||||
.action-icon {
|
||||
color: var(--g-tabbar-tab-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
background-color: var(--g-container-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-dividers {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: -1px;
|
||||
left: -1px;
|
||||
z-index: 0;
|
||||
height: 14px;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1px;
|
||||
display: block;
|
||||
width: 1px;
|
||||
content: "";
|
||||
background-color: var(--g-tabbar-dividers-bg);
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease, background-color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child .tab-dividers::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s, background-color 0.3s;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 5px;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
margin-right: 10px;
|
||||
overflow: hidden;
|
||||
color: var(--g-tabbar-tab-color);
|
||||
white-space: nowrap;
|
||||
mask-image: linear-gradient(to right, #000 calc(100% - 20px), transparent);
|
||||
transition: margin-right 0.3s;
|
||||
|
||||
&:has(+ .action-icon) {
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.5em;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
font-size: 12px;
|
||||
color: var(--g-tabbar-tab-color);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
&:hover {
|
||||
--at-apply: ring-1 ring-stone-3 dark:ring-stone-7;
|
||||
|
||||
background-color: var(--g-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.hotkey-number {
|
||||
--at-apply: ring-1 ring-stone-3 dark:ring-stone-7;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.5em;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
font-size: 12px;
|
||||
color: var(--g-tabbar-tab-color);
|
||||
background-color: var(--g-bg);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签栏动画
|
||||
.tabs {
|
||||
.tabbar-move,
|
||||
.tabbar-enter-active,
|
||||
.tabbar-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tabbar-enter-from,
|
||||
.tabbar-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
|
||||
.tabbar-leave-active {
|
||||
position: absolute !important;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import Tabbar from './Tabbar/index.vue'
|
||||
import Toolbar from './Toolbar/index.vue'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
@ -19,7 +20,9 @@ const enableToolbar = computed(() => {
|
||||
const scrollTop = ref(0)
|
||||
const scrollOnHide = ref(false)
|
||||
const topbarHeight = computed(() => {
|
||||
return enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
|
||||
const tabbarHeight = settingsStore.settings.tabbar.enable ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-tabbar-height')) : 0
|
||||
const toolbarHeight = enableToolbar.value ? Number.parseInt(getComputedStyle(document.documentElement || document.body).getPropertyValue('--g-toolbar-height')) : 0
|
||||
return tabbarHeight + toolbarHeight
|
||||
})
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', onScroll)
|
||||
@ -38,12 +41,14 @@ watch(scrollTop, (val, oldVal) => {
|
||||
<template>
|
||||
<div
|
||||
class="topbar-container" :class="{
|
||||
'has-tabbar': settingsStore.settings.tabbar.enable,
|
||||
'has-toolbar': enableToolbar,
|
||||
[`topbar-${settingsStore.settings.topbar.mode}`]: true,
|
||||
'shadow': scrollTop,
|
||||
'hide': scrollOnHide,
|
||||
}" data-fixed-calc-width
|
||||
>
|
||||
<Tabbar v-if="settingsStore.settings.tabbar.enable" />
|
||||
<Toolbar v-if="enableToolbar" />
|
||||
</div>
|
||||
</template>
|
||||
@ -68,7 +73,7 @@ watch(scrollTop, (val, oldVal) => {
|
||||
}
|
||||
|
||||
&.topbar-sticky.hide {
|
||||
top: calc(var(--g-toolbar-height) * -1) !important;
|
||||
top: calc((var(--g-tabbar-height) + var(--g-toolbar-height)) * -1) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -202,9 +202,17 @@ const enableAppSetting = import.meta.env.VITE_APP_SETTING === 'true'
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.topbar-container.has-tabbar + .main {
|
||||
margin: var(--g-tabbar-height) 0 0;
|
||||
}
|
||||
|
||||
.topbar-container.has-toolbar + .main {
|
||||
margin: var(--g-toolbar-height) 0 0;
|
||||
}
|
||||
|
||||
.topbar-container.has-tabbar.has-toolbar + .main {
|
||||
margin: calc(var(--g-tabbar-height) + var(--g-toolbar-height)) 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,6 +156,21 @@ router.afterEach((to, from) => {
|
||||
}
|
||||
break
|
||||
}
|
||||
// 通过 meta.noCache 判断针对哪些页面不需要进行缓存
|
||||
if (from.meta.noCache) {
|
||||
switch (typeof from.meta.noCache) {
|
||||
case 'string':
|
||||
if (from.meta.noCache === to.name) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
case 'object':
|
||||
if (from.meta.noCache.includes(to.name as string)) {
|
||||
keepAliveStore.remove(componentName)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
// 如果进入的是 reload 页面,则也将离开页面的缓存清空
|
||||
if (to.name === 'reload') {
|
||||
keepAliveStore.remove(componentName)
|
||||
|
@ -30,6 +30,11 @@ const globalSettingsDefault: RecursiveRequired<Settings.all> = {
|
||||
topbar: {
|
||||
mode: 'static',
|
||||
},
|
||||
tabbar: {
|
||||
enable: false,
|
||||
enableIcon: false,
|
||||
enableHotkeys: false,
|
||||
},
|
||||
toolbar: {
|
||||
breadcrumb: true,
|
||||
navSearch: true,
|
||||
|
169
src/store/modules/tabbar.ts
Executable file
169
src/store/modules/tabbar.ts
Executable file
@ -0,0 +1,169 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import useKeepAliveStore from './keepAlive'
|
||||
import type { Tabbar } from '#/global'
|
||||
|
||||
const useTabbarStore = defineStore(
|
||||
// 唯一ID
|
||||
'tabbar',
|
||||
() => {
|
||||
const keepAliveStore = useKeepAliveStore()
|
||||
|
||||
const list = ref<Tabbar.recordRaw[]>([])
|
||||
const leaveIndex = ref(-1)
|
||||
|
||||
// 添加标签页
|
||||
async function add(route: RouteLocationNormalized) {
|
||||
const names: string[] = []
|
||||
route.matched.forEach((v, i) => {
|
||||
if (i > 0) {
|
||||
v.components?.default.name && names.push(v.components.default.name)
|
||||
}
|
||||
})
|
||||
const meta = route.matched.at(-1)?.meta
|
||||
const tabId = route.fullPath
|
||||
if (route.name !== 'reload') {
|
||||
// 记录查找到的标签页
|
||||
const findTab = list.value.find((item) => {
|
||||
if (item.routeName) {
|
||||
return item.routeName === route.name
|
||||
}
|
||||
else {
|
||||
return item.tabId === tabId
|
||||
}
|
||||
})
|
||||
// 新增标签页
|
||||
if (!findTab) {
|
||||
const listItem = {
|
||||
tabId,
|
||||
fullPath: route.fullPath,
|
||||
routeName: route.name,
|
||||
title: typeof meta?.title === 'function' ? meta.title() : meta?.title,
|
||||
icon: meta?.icon ?? meta?.breadcrumbNeste?.findLast(item => item.icon)?.icon,
|
||||
name: names,
|
||||
}
|
||||
if (leaveIndex.value >= 0) {
|
||||
list.value.splice(leaveIndex.value + 1, 0, listItem)
|
||||
leaveIndex.value = -1
|
||||
}
|
||||
else {
|
||||
list.value.push(listItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 删除指定标签页
|
||||
function remove(tabId: Tabbar.recordRaw['tabId']) {
|
||||
const keepName: string[] = []
|
||||
const removeName: string[] = []
|
||||
list.value.forEach((v) => {
|
||||
if (v.tabId === tabId) {
|
||||
removeName.push(...v.name)
|
||||
}
|
||||
else {
|
||||
keepName.push(...v.name)
|
||||
}
|
||||
})
|
||||
const name: string[] = []
|
||||
removeName.forEach((v) => {
|
||||
if (!keepName.includes(v)) {
|
||||
name.push(v)
|
||||
}
|
||||
})
|
||||
// 如果是手动点击关闭 tab 标签页,则删除页面缓存
|
||||
keepAliveStore.remove(name)
|
||||
list.value = list.value.filter((item) => {
|
||||
return item.tabId !== tabId
|
||||
})
|
||||
}
|
||||
// 删除两侧标签页
|
||||
function removeOtherSide(tabId: Tabbar.recordRaw['tabId']) {
|
||||
const keepName: string[] = []
|
||||
const removeName: string[] = []
|
||||
list.value.forEach((v) => {
|
||||
if (v.tabId !== tabId) {
|
||||
removeName.push(...v.name)
|
||||
}
|
||||
else {
|
||||
keepName.push(...v.name)
|
||||
}
|
||||
})
|
||||
const name: string[] = []
|
||||
removeName.forEach((v) => {
|
||||
if (!keepName.includes(v)) {
|
||||
name.push(v)
|
||||
}
|
||||
})
|
||||
keepAliveStore.remove(name)
|
||||
list.value = list.value.filter((item) => {
|
||||
return item.tabId === tabId
|
||||
})
|
||||
}
|
||||
// 删除左侧标签页
|
||||
function removeLeftSide(tabId: Tabbar.recordRaw['tabId']) {
|
||||
// 查找指定路由对应在标签页列表里的下标
|
||||
const index = list.value.findIndex(item => item.tabId === tabId)
|
||||
const keepName: string[] = []
|
||||
const removeName: string[] = []
|
||||
list.value.forEach((v, i) => {
|
||||
if (i < index) {
|
||||
removeName.push(...v.name)
|
||||
}
|
||||
else {
|
||||
keepName.push(...v.name)
|
||||
}
|
||||
})
|
||||
const name: string[] = []
|
||||
removeName.forEach((v) => {
|
||||
if (!keepName.includes(v)) {
|
||||
name.push(v)
|
||||
}
|
||||
})
|
||||
keepAliveStore.remove(name)
|
||||
list.value = list.value.filter((item, i) => {
|
||||
return i >= index
|
||||
})
|
||||
}
|
||||
// 删除右侧标签页
|
||||
function removeRightSide(tabId: Tabbar.recordRaw['tabId']) {
|
||||
// 查找指定路由对应在标签页列表里的下标
|
||||
const index = list.value.findIndex(item => item.tabId === tabId)
|
||||
const keepName: string[] = []
|
||||
const removeName: string[] = []
|
||||
list.value.forEach((v, i) => {
|
||||
if (i > index) {
|
||||
removeName.push(...v.name)
|
||||
}
|
||||
else {
|
||||
keepName.push(...v.name)
|
||||
}
|
||||
})
|
||||
const name: string[] = []
|
||||
removeName.forEach((v) => {
|
||||
if (!keepName.includes(v)) {
|
||||
name.push(v)
|
||||
}
|
||||
})
|
||||
keepAliveStore.remove(name)
|
||||
list.value = list.value.filter((item, i) => {
|
||||
return i <= index
|
||||
})
|
||||
}
|
||||
// 清空所有标签页,登出的时候需要清空
|
||||
function clean() {
|
||||
list.value = []
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
leaveIndex,
|
||||
add,
|
||||
remove,
|
||||
removeOtherSide,
|
||||
removeLeftSide,
|
||||
removeRightSide,
|
||||
clean,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default useTabbarStore
|
1
src/types/auto-imports.d.ts
vendored
1
src/types/auto-imports.d.ts
vendored
@ -74,6 +74,7 @@ declare global {
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTabbar: typeof import('../utils/composables/useTabbar')['default']
|
||||
const useViewTransition: typeof import('../utils/composables/useViewTransition')['default']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
|
31
src/types/global.d.ts
vendored
31
src/types/global.d.ts
vendored
@ -112,6 +112,23 @@ declare namespace Settings {
|
||||
*/
|
||||
mode?: 'static' | 'fixed' | 'sticky'
|
||||
}
|
||||
interface tabbar {
|
||||
/**
|
||||
* 是否开启标签栏
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enable?: boolean
|
||||
/**
|
||||
* 是否开启标签栏图标显示
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableIcon?: boolean
|
||||
/**
|
||||
* 是否开启标签栏快捷键
|
||||
* @默认值 `false`
|
||||
*/
|
||||
enableHotkeys?: boolean
|
||||
}
|
||||
interface toolbar {
|
||||
/**
|
||||
* 是否开启面包屑导航
|
||||
@ -191,6 +208,8 @@ declare namespace Settings {
|
||||
menu?: menu
|
||||
/** 顶栏设置 */
|
||||
topbar?: topbar
|
||||
/** 标签栏设置 */
|
||||
tabbar?: tabbar
|
||||
/** 工具栏设置 */
|
||||
toolbar?: toolbar
|
||||
/** 页面设置 */
|
||||
@ -212,6 +231,7 @@ declare module 'vue-router' {
|
||||
breadcrumb?: boolean
|
||||
activeMenu?: string
|
||||
cache?: boolean | string | string[]
|
||||
noCache?: string | string[]
|
||||
link?: string
|
||||
breadcrumbNeste?: Route.breadcrumb[]
|
||||
}
|
||||
@ -258,3 +278,14 @@ declare namespace Menu {
|
||||
children: recordRaw[]
|
||||
}
|
||||
}
|
||||
|
||||
declare namespace Tabbar {
|
||||
interface recordRaw {
|
||||
tabId: string
|
||||
fullPath: string
|
||||
routeName?: RouteRecordRaw.name
|
||||
title?: string | (() => string)
|
||||
icon?: string
|
||||
name: string[]
|
||||
}
|
||||
}
|
||||
|
164
src/utils/composables/useTabbar.ts
Executable file
164
src/utils/composables/useTabbar.ts
Executable file
@ -0,0 +1,164 @@
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
import Message from 'vue-m-message'
|
||||
import useTabbarStore from '@/store/modules/tabbar'
|
||||
|
||||
export default function useTabbar() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
|
||||
function getId() {
|
||||
return route.fullPath
|
||||
}
|
||||
|
||||
function open(to: RouteLocationRaw) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === getId())
|
||||
tabbarStore.$patch({
|
||||
leaveIndex: index,
|
||||
})
|
||||
router.push(to)
|
||||
}
|
||||
|
||||
function go(delta: number) {
|
||||
const tabId = getId()
|
||||
router.go(delta)
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
|
||||
function close(to: RouteLocationRaw) {
|
||||
const tabId = getId()
|
||||
router.push(to).then(() => {
|
||||
tabbarStore.remove(tabId)
|
||||
})
|
||||
}
|
||||
|
||||
function closeById(tabId = getId()) {
|
||||
const activedTabId = getId()
|
||||
if (tabbarStore.list.some(item => item.tabId === tabId)) {
|
||||
if (tabbarStore.list.length > 1) {
|
||||
// 如果关闭的标签正好是当前路由
|
||||
if (tabId === activedTabId) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
if (index < tabbarStore.list.length - 1) {
|
||||
close(tabbarStore.list[index + 1].fullPath)
|
||||
}
|
||||
else {
|
||||
close(tabbarStore.list[index - 1].fullPath)
|
||||
}
|
||||
}
|
||||
else {
|
||||
tabbarStore.remove(tabId)
|
||||
}
|
||||
}
|
||||
else {
|
||||
Message.error('当前只有一个标签页,已阻止关闭')
|
||||
}
|
||||
}
|
||||
else {
|
||||
Message.error('关闭的页面不存在')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭两侧标签页
|
||||
*/
|
||||
function closeOtherSide(tabId = getId()) {
|
||||
const activedTabId = getId()
|
||||
// 如果操作的是非当前路由标签页,则先跳转到指定路由标签页
|
||||
if (tabId !== activedTabId) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
router.push(tabbarStore.list[index].fullPath)
|
||||
}
|
||||
tabbarStore.removeOtherSide(tabId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧标签页
|
||||
*/
|
||||
function closeLeftSide(tabId = getId()) {
|
||||
const activedTabId = getId()
|
||||
// 如果操作的是非当前路由标签页,需要判断当前标签页是否在指定标签页左侧,如果是则先跳转到指定路由标签页
|
||||
if (tabId !== activedTabId) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
const activedIndex = tabbarStore.list.findIndex(item => item.tabId === activedTabId)
|
||||
if (activedIndex < index) {
|
||||
router.push(tabbarStore.list[index].fullPath)
|
||||
}
|
||||
}
|
||||
tabbarStore.removeLeftSide(tabId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧标签页
|
||||
*/
|
||||
function closeRightSide(tabId = getId()) {
|
||||
const activedTabId = getId()
|
||||
// 如果操作的是非当前路由标签页,需要判断当前标签页是否在指定标签页右侧,如果是则先跳转到指定路由标签页
|
||||
if (tabId !== activedTabId) {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
const activedIndex = tabbarStore.list.findIndex(item => item.tabId === activedTabId)
|
||||
if (activedIndex > index) {
|
||||
router.push(tabbarStore.list[index].fullPath)
|
||||
}
|
||||
}
|
||||
tabbarStore.removeRightSide(tabId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定标签两侧是否有可关闭的标签
|
||||
*/
|
||||
function checkCloseOtherSide(tabId = getId()) {
|
||||
return tabbarStore.list.some((item) => {
|
||||
return item.tabId !== tabId
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定标签左侧是否有可关闭的标签
|
||||
*/
|
||||
function checkCloseLeftSide(tabId = getId()) {
|
||||
let flag = true
|
||||
if (tabId === tabbarStore.list[0].tabId) {
|
||||
flag = false
|
||||
}
|
||||
else {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
flag = tabbarStore.list.some((item, i) => {
|
||||
return i < index && item.tabId !== tabId
|
||||
})
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验指定标签右侧是否有可关闭的标签
|
||||
*/
|
||||
function checkCloseRightSide(tabId = getId()) {
|
||||
let flag = true
|
||||
if (tabId === tabbarStore.list.at(-1)?.tabId) {
|
||||
flag = false
|
||||
}
|
||||
else {
|
||||
const index = tabbarStore.list.findIndex(item => item.tabId === tabId)
|
||||
flag = tabbarStore.list.some((item, i) => {
|
||||
return i >= index && item.tabId !== tabId
|
||||
})
|
||||
}
|
||||
return flag
|
||||
}
|
||||
|
||||
return {
|
||||
getId,
|
||||
open,
|
||||
go,
|
||||
close,
|
||||
closeById,
|
||||
closeOtherSide,
|
||||
closeLeftSide,
|
||||
closeRightSide,
|
||||
checkCloseOtherSide,
|
||||
checkCloseLeftSide,
|
||||
checkCloseRightSide,
|
||||
}
|
||||
}
|
@ -33,6 +33,12 @@ export const lightTheme = {
|
||||
'--g-sub-sidebar-menu-hover-color': '#0f0f0f',
|
||||
'--g-sub-sidebar-menu-active-bg': '#0f0f0f',
|
||||
'--g-sub-sidebar-menu-active-color': '#fff',
|
||||
// 标签栏
|
||||
'--g-tabbar-dividers-bg': '#a3a3a3',
|
||||
'--g-tabbar-tab-color': '#a3a3a3',
|
||||
'--g-tabbar-tab-hover-bg': '#e5e5e5',
|
||||
'--g-tabbar-tab-hover-color': '#0f0f0f',
|
||||
'--g-tabbar-tab-active-color': '#0f0f0f',
|
||||
}
|
||||
|
||||
export const darkTheme = {
|
||||
@ -68,4 +74,10 @@ export const darkTheme = {
|
||||
'--g-sub-sidebar-menu-hover-color': '#e5e5e5',
|
||||
'--g-sub-sidebar-menu-active-bg': '#e5e5e5',
|
||||
'--g-sub-sidebar-menu-active-color': '#0a0a0a',
|
||||
// 标签栏
|
||||
'--g-tabbar-dividers-bg': '#a8a29e',
|
||||
'--g-tabbar-tab-color': '#a8a29e',
|
||||
'--g-tabbar-tab-hover-bg': '#1b1b1b',
|
||||
'--g-tabbar-tab-hover-color': '#e5e5e5',
|
||||
'--g-tabbar-tab-active-color': '#e5e5e5',
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user