feat: 增加标签页支持

This commit is contained in:
Hooray Hu 2024-01-25 09:32:00 +08:00
parent 52b25d1eae
commit a55f3016ce
15 changed files with 941 additions and 2 deletions

View File

@ -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",

View File

@ -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'}

View File

@ -8,6 +8,8 @@
--g-sub-sidebar-collapse-width: 64px;
// 侧边栏 Logo 区域高度
--g-sidebar-logo-height: 50px;
// 标签栏高度
--g-tabbar-height: 50px;
// 工具栏高度
--g-toolbar-height: 50px;
}

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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
View 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

View File

@ -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
View File

@ -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[]
}
}

View 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,
}
}

View File

@ -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',
}