Files
simple-element-plus-template/src/components/common-tabs-view/index.vue
2026-02-01 14:57:19 +08:00

217 lines
5.6 KiB
Vue

<script setup>
import { useTabsViewStore } from '@/stores/TabsViewStore'
import { useRoute, useRouter } from 'vue-router'
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { isString } from 'lodash-es'
import TabsViewItem from '@/components/common-tabs-view/tabs-view-item.vue'
import { toGetParams } from '@/utils'
import { isNestedRoute } from '@/route/RouteUtils'
import { useGetDerivedNamespace } from 'element-plus'
import Sortable from 'sortablejs'
const router = useRouter()
const route = useRoute()
const tabsViewStore = useTabsViewStore()
const tabRef = ref()
let sortable = null
watch(route, () => {
if (route.path) {
tabsViewStore.addHistoryTab(route)
tabsViewStore.currentTab = route.path
}
}, { immediate: true })
onMounted(() => {
if (!tabsViewStore.historyTabs.length) {
tabsViewStore.addHistoryTab(route)
}
tabsViewStore.currentTab = route.path
if (tabRef.value?.tabNavRef) {
const ns = useGetDerivedNamespace().value
const { tabListRef } = tabRef.value.tabNavRef
sortable = new Sortable(tabListRef, {
animation: 150,
draggable: `.${ns}-tabs__item`,
filter: '.is-disabled',
onEnd (event) {
const { oldIndex, newIndex } = event
tabsViewStore.reIndexHistoryTab(oldIndex, newIndex)
}
})
}
})
const selectHistoryTab = path => {
const tab = isString(path) ? tabsViewStore.findHistoryTab(path) : path
if (tab) {
router.push(tab)
if (!isNestedRoute(tab)) {
tabsViewStore.addCachedTab(tab)
}
}
}
const removeHistoryTab = path => {
const lastTab = tabsViewStore.removeHistoryTab({ path })
if (lastTab) {
selectHistoryTab(lastTab)
}
}
const refreshHistoryTab = tab => {
const time = new Date().getTime()
const query = Object.assign({}, tab.query, { _t: time })
router.replace(`${tab.path}?${toGetParams(query)}`)
if (!isNestedRoute(tab)) {
tabsViewStore.addCachedTab(tab)
}
}
const removeOtherHistoryTabs = tab => {
tabsViewStore.removeOtherHistoryTabs(tab)
selectHistoryTab(tab.path)
}
const removeHistoryTabs = (tab, type) => {
tabsViewStore.removeHistoryTabs(tab, type)
selectHistoryTab(tab.path)
}
const tabItems = ref()
const onDropdownVisibleChange = (visible, tab) => {
if (visible) {
tabItems.value.forEach(({ dropdownRef }) => {
if (dropdownRef.id !== tab.path) {
dropdownRef.handleClose()
}
})
}
}
onBeforeUnmount(() => {
sortable?.destroy()
sortable = null
})
</script>
<template>
<el-tabs
ref="tabRef"
v-bind="$attrs"
v-model="tabsViewStore.currentTab"
class="common-tabs"
type="card"
:closable="tabsViewStore.historyTabs.length>1"
@tab-change="selectHistoryTab"
@tab-remove="removeHistoryTab"
>
<tabs-view-item
v-for="item in tabsViewStore.historyTabs"
ref="tabItems"
:key="item.path"
:tab-item="item"
:label-config="item.labelConfig"
@refresh-history-tab="refreshHistoryTab"
@remove-other-history-tabs="removeOtherHistoryTabs"
@remove-history-tabs="removeHistoryTabs"
@on-dropdown-visible-change="onDropdownVisibleChange"
@remove-history-tab="removeHistoryTab"
/>
</el-tabs>
</template>
<style scoped>
:deep(.el-tabs__header) {
margin: 0;
border-bottom: 1px solid var(--el-border-color-light) !important;
position: relative;
}
:deep(.el-tabs__nav) {
border: 0 !important;
}
/* Allow pseudo-elements to overflow and cover the header border */
:deep(.el-tabs__nav-wrap),
:deep(.el-tabs__nav-scroll) {
overflow: visible !important;
}
:deep(.el-tabs__item) {
border: 1px solid transparent !important;
margin: 0 4px 0 0;
border-radius: 4px 4px 0 0;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
/* Default inactive background */
background-color: var(--el-fill-color-light);
}
/* Bottom border for inactive tabs */
:deep(.el-tabs__item:not(.is-active)) {
border-bottom: 1px solid var(--el-border-color-light) !important;
}
/* Specific styling for active tab to look "connected" to content */
:deep(.el-tabs__item.is-active) {
background-color: var(--el-bg-color);
border-left: 1px solid var(--el-border-color-light) !important;
border-right: 1px solid var(--el-border-color-light) !important;
border-top: 1px solid var(--el-border-color-light) !important;
border-bottom: 1px solid transparent !important;
position: relative;
font-weight: 600;
}
/* Cover the header bottom border under active tab */
:deep(.el-tabs__item.is-active)::before {
content: "";
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 4px;
background-color: var(--el-bg-color);
z-index: 10;
}
/* Top highlight line for active tab */
:deep(.el-tabs__item.is-active)::after {
content: "";
position: absolute;
top: -1px;
left: -1px;
width: calc(100% + 2px);
height: 2px;
background-color: var(--el-color-primary);
border-radius: 4px 4px 0 0;
}
:deep(.el-tabs__item:not(.is-active):hover) {
background-color: var(--el-fill-color);
color: var(--el-color-primary);
}
/* Dark mode adjustments */
.dark :deep(.el-tabs__header) {
border-bottom: 1px solid var(--el-border-color-darker) !important;
}
.dark :deep(.el-tabs__item) {
background-color: var(--el-bg-color-overlay);
}
.dark :deep(.el-tabs__item:not(.is-active)) {
border-bottom: 1px solid var(--el-border-color-darker) !important;
}
.dark :deep(.el-tabs__item.is-active) {
background-color: var(--el-bg-color);
border-color: var(--el-border-color-darker) !important;
border-bottom-color: transparent !important;
}
.dark :deep(.el-tabs__item.is-active)::before {
background-color: var(--el-bg-color);
}
</style>