浏览代码

feat: 添加AppTabs

wanggaokun 1 月之前
父节点
当前提交
4ff793ffd7

+ 31 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <svg :style="iconStyle" aria-hidden="true">
+    <use :xlink:href="symbolId" />
+  </svg>
+</template>
+
+<script setup lang="ts" name="SvgIcon">
+import type { CSSProperties } from 'vue'
+
+interface SvgProps {
+  name: string // 图标的名称 ==> 必传
+  prefix?: string // 图标的前缀 ==> 非必传(默认为"icon")
+  iconStyle?: CSSProperties // 图标的样式 ==> 非必传
+  width?: number
+  height?: number
+}
+
+const props = withDefaults(defineProps<SvgProps>(), {
+  prefix: 'icon',
+  width: 16,
+  height: 16
+})
+
+const symbolId = computed(() => `#${props.prefix}-${props.name}`)
+
+const iconStyle = computed(() => ({
+  width: `${props.width}px`,
+  height: `${props.height}px`,
+  fill: 'var(--color)'
+}))
+</script>

+ 1 - 1
src/hooks/interface/index.ts

@@ -18,7 +18,7 @@ export type StateProps = {
   icon?: {
     [key: string]: any
   }
-  loading?: boolean
+  loading: boolean
 }
 
 export type HandleDataMessageType = '' | 'success' | 'warning' | 'info' | 'error'

+ 16 - 8
src/layouts/components/AppMain/index.vue

@@ -1,10 +1,18 @@
 <template>
-  <router-view v-slot="{ Component, route }">
-    <transition appear name="fade-transform" mode="out-in">
-      <keep-alive>
-        <component :is="Component" :key="route.name" />
-      </keep-alive>
-    </transition>
-  </router-view>
+  <AppTabs />
+  <el-main>
+    <router-view v-slot="{ Component, route }">
+      <transition appear name="fade-transform" mode="out-in">
+        <keep-alive>
+          <component :is="Component" :key="route.name" />
+        </keep-alive>
+      </transition>
+    </router-view>
+  </el-main>
 </template>
-<script lang="ts" name="AppMain" setup></script>
+<script lang="ts" name="AppMain" setup>
+// 注入刷新页面方法
+const isRouterShow = ref(true)
+const refreshCurrentPage = (val: boolean) => (isRouterShow.value = val)
+provide('refresh', refreshCurrentPage)
+</script>

+ 96 - 0
src/layouts/components/AppTabs/MoreButton.vue

@@ -0,0 +1,96 @@
+<template>
+  <el-dropdown trigger="click" :teleported="false">
+    <div class="more-button">
+      <i :class="'iconfont icon-xiala'" />
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item @click="refresh">
+          <el-icon>
+            <Refresh />
+          </el-icon>
+          刷新
+        </el-dropdown-item>
+        <el-dropdown-item @click="closeCurrentTab">
+          <el-icon>
+            <Remove />
+          </el-icon>
+          关闭当前
+        </el-dropdown-item>
+        <el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'left')">
+          <el-icon>
+            <DArrowLeft />
+          </el-icon>
+          关闭左侧
+        </el-dropdown-item>
+        <el-dropdown-item @click="tabStore.closeTabsOnSide(route.fullPath, 'right')">
+          <el-icon>
+            <DArrowRight />
+          </el-icon>
+          关闭右侧
+        </el-dropdown-item>
+        <el-dropdown-item divided @click="closeOtherTab">
+          <el-icon>
+            <CircleClose />
+          </el-icon>
+          关闭其他
+        </el-dropdown-item>
+        <el-dropdown-item @click="closeAllTab">
+          <el-icon>
+            <FolderDelete />
+          </el-icon>
+          关闭所有
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>
+
+<script setup lang="ts">
+import { inject, nextTick } from 'vue'
+import { HOME_URL } from '@/constants'
+import { useTabsStore } from '@/stores/modules/tabs'
+import { useKeepAliveStore } from '@/stores/modules/keepAlive'
+import { useRoute, useRouter } from 'vue-router'
+import { CircleClose, DArrowLeft, DArrowRight, FolderDelete, Remove, Refresh } from '@element-plus/icons-vue'
+type refreshFunction = (val: boolean) => void
+const route = useRoute()
+const router = useRouter()
+const tabStore = useTabsStore()
+const keepAliveStore = useKeepAliveStore()
+
+// refresh current page
+const refreshCurrentPage = inject('refresh') as refreshFunction
+const refresh = () => {
+  setTimeout(() => {
+    keepAliveStore.removeKeepAliveName(route.name as string)
+    refreshCurrentPage(false)
+    nextTick(() => {
+      keepAliveStore.addKeepAliveName(route.name as string)
+      refreshCurrentPage(true)
+    })
+  }, 0)
+}
+
+// Close Current
+const closeCurrentTab = () => {
+  if (route.meta.isAffix === 'T') return
+  tabStore.removeTabs(route.fullPath)
+  keepAliveStore.removeKeepAliveName(route.name as string)
+}
+
+// Close Other
+const closeOtherTab = () => {
+  tabStore.closeMultipleTab(route.fullPath)
+}
+
+// Close All
+const closeAllTab = () => {
+  tabStore.closeMultipleTab()
+  router.push(HOME_URL)
+}
+</script>
+
+<style scoped lang="scss">
+@use './index';
+</style>

+ 69 - 0
src/layouts/components/AppTabs/index.scss

@@ -0,0 +1,69 @@
+.tabs-box {
+  background-color: var(--bg-color);
+  .tabs-menu {
+    position: relative;
+    width: 100%;
+    .el-dropdown {
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      .more-button {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        width: 43px;
+        cursor: pointer;
+        border-left: 1px solid var(--border-color);
+        transition: all 0.3s;
+        &:hover {
+          background-color: var(--el-color-info-light-9);
+        }
+        .iconfont {
+          font-size: 12.5px;
+        }
+      }
+    }
+    :deep(.el-tabs) {
+      .el-tabs__header {
+        box-sizing: border-box;
+        height: 40px;
+        padding: 0 10px;
+        margin: 0;
+        .el-tabs__nav-wrap {
+          position: absolute;
+          width: calc(100% - 70px);
+          .el-tabs__nav {
+            display: flex;
+            border: none;
+            .el-tabs__item {
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              color: #afafaf;
+              border: none;
+              .tabs-icon {
+                margin: 1.5px 4px 0 0;
+                font-size: 15px;
+              }
+              .is-icon-close {
+                margin-top: 1px;
+              }
+              &.is-active {
+                color: var(--el-color-primary);
+                &::before {
+                  position: absolute;
+                  bottom: 0;
+                  width: 100%;
+                  height: 0;
+                  content: '';
+                  border-bottom: 2px solid var(--el-color-primary) !important;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 106 - 0
src/layouts/components/AppTabs/index.vue

@@ -0,0 +1,106 @@
+<template>
+  <div class="tabs-box">
+    <div class="tabs-menu">
+      <el-tabs v-model="tabsMenuValue" type="card" @tab-click="tabClick" @tab-remove="tabRemove">
+        <el-tab-pane v-for="item in tabsMenuList" :key="item.path" :label="item.title" :name="item.path" :closable="item.close">
+          <template #label>
+            <!-- <el-icon v-show="item.icon" class="tabs-icon">
+              <SvgIcon v-if="item.icon.startsWith('svg-')" :name="item.icon.substring(4)" />
+              <component v-else :is="item.icon" />
+            </el-icon> -->
+            {{ item.title }}
+          </template>
+        </el-tab-pane>
+      </el-tabs>
+      <MoreButton />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="AppTabs">
+import Sortable from 'sortablejs'
+import type { TabsMenuProps } from '@/stores/interface'
+import type { TabPaneName, TabsPaneContext } from 'element-plus'
+import { useAuthStore, useKeepAliveStore, useTabsStore } from '@/stores'
+
+const route = useRoute()
+const router = useRouter()
+const tabStore = useTabsStore()
+const authStore = useAuthStore()
+const keepAliveStore = useKeepAliveStore()
+
+const tabsMenuValue = ref(route.fullPath)
+const tabsMenuList = computed(() => tabStore.getTabsMenuList)
+
+onMounted(() => {
+  tabsDrop()
+  initTabs()
+})
+
+// 监听路由的变化(防止浏览器后退/前进不变化 tabsMenuValue)
+watch(
+  () => route.fullPath,
+  () => {
+    if (route.meta.isFull === true) return
+    tabsMenuValue.value = route.fullPath
+    const tabsParams: TabsMenuProps = {
+      icon: route.meta.icon as string,
+      title: route.meta.title as string,
+      path: route.fullPath,
+      name: route.name as string,
+      close: route.meta.isAffix !== true,
+      isKeepAlive: route.meta.isKeepAlive === true
+    }
+    tabStore.addTabs(tabsParams)
+  },
+  { immediate: true }
+)
+
+// tabs 拖拽排序
+const tabsDrop = () => {
+  Sortable.create(document.querySelector('.el-tabs__nav') as HTMLElement, {
+    draggable: '.el-tabs__item',
+    animation: 300,
+    onEnd({ newIndex, oldIndex }) {
+      const tabsList = [...tabStore.tabsMenuList]
+      const currRow = tabsList.splice(oldIndex as number, 1)[0]
+      tabsList.splice(newIndex as number, 0, currRow)
+      tabStore.setTabs(tabsList)
+    }
+  })
+}
+
+// 初始化需要固定的 tabs
+const initTabs = () => {
+  authStore.flatMenuListGet.forEach(item => {
+    if (item.meta.isAffix && item.meta.isHidden && item.meta.isFull) {
+      const tabsParams = {
+        icon: item.meta.icon,
+        title: item.meta.title,
+        path: item.path,
+        name: item.name,
+        close: !item.meta.isAffix,
+        isKeepAlive: item.meta.isKeepAlive === 'true'
+      }
+      tabStore.addTabs(tabsParams)
+    }
+  })
+}
+
+// Tab Click
+const tabClick = (tabItem: TabsPaneContext) => {
+  const fullPath = tabItem.props.name as string
+  router.push(fullPath)
+}
+
+// Remove Tab
+const tabRemove = (fullPath: TabPaneName) => {
+  const name = tabStore.tabsMenuList.filter(item => item.path === fullPath)[0].name || ''
+  keepAliveStore.removeKeepAliveName(name)
+  tabStore.removeTabs(fullPath as string, fullPath === route.fullPath)
+}
+</script>
+
+<style scoped lang="scss">
+@use './index';
+</style>

+ 1 - 3
src/layouts/container/AppTransverse/index.vue

@@ -5,9 +5,7 @@
       <AppMenu />
       <AppTools />
     </el-header>
-    <el-main>
-      <AppMain />
-    </el-main>
+    <AppMain />
     <el-footer>
       <AppFooter />
     </el-footer>

+ 5 - 2
src/stores/modules/tabs.ts

@@ -1,16 +1,17 @@
 import { pinia, useKeepAliveStore } from '../index'
 import { TabsMenuProps } from '../interface'
 import router from '@/router'
-const keepAliveStore = useKeepAliveStore()
+
 export const useTabsStore = defineStore('eco-tabs', {
   state: () => ({
     tabsMenuList: [] as TabsMenuProps[]
   }),
   getters: {
-    tabsMenuList: state => state.tabsMenuList
+    getTabsMenuList: state => state.tabsMenuList
   },
   actions: {
     addTabs(tabItem: TabsMenuProps) {
+      const keepAliveStore = useKeepAliveStore()
       const index = this.tabsMenuList.findIndex(item => item.path === tabItem.path)
       if (index === -1) {
         this.tabsMenuList.push(tabItem)
@@ -34,6 +35,7 @@ export const useTabsStore = defineStore('eco-tabs', {
       this.tabsMenuList = tabsMenus.filter(item => item.path !== tabPath)
     },
     closeTabsOnSide(path: string, type: string) {
+      const keepAliveStore = useKeepAliveStore()
       const currentIndex = this.tabsMenuList.findIndex(item => item.path === path)
       if (currentIndex !== -1) {
         const range = type === 'left' ? [0, currentIndex] : [currentIndex + 1, this.tabsMenuList.length]
@@ -44,6 +46,7 @@ export const useTabsStore = defineStore('eco-tabs', {
       keepAliveStore.setKeepAliveName(this.tabsMenuList.map(item => item.name))
     },
     closeMultipleTab(tabsMenuValue?: string) {
+      const keepAliveStore = useKeepAliveStore()
       this.tabsMenuList = this.tabsMenuList.filter(item => {
         return item.path === tabsMenuValue || !item.close
       })

+ 7 - 0
src/types/auto-components.d.ts

@@ -22,6 +22,9 @@ declare module 'vue' {
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
     ElDrawer: typeof import('element-plus/es')['ElDrawer']
+    ElDropdown: typeof import('element-plus/es')['ElDropdown']
+    ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
+    ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
     ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -39,6 +42,8 @@ declare module 'vue' {
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTable: typeof import('element-plus/es')['ElTable']
     ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTabPane: typeof import('element-plus/es')['ElTabPane']
+    ElTabs: typeof import('element-plus/es')['ElTabs']
     ElTag: typeof import('element-plus/es')['ElTag']
     ElTooltip: typeof import('element-plus/es')['ElTooltip']
     ElTree: typeof import('element-plus/es')['ElTree']
@@ -46,12 +51,14 @@ declare module 'vue' {
     GridItem: typeof import('./../components/Grid/GridItem.vue')['default']
     HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
     LoginForm: typeof import('./../views/login/components/LoginForm.vue')['default']
+    MoreButton: typeof import('./../layouts/components/AppTabs/MoreButton.vue')['default']
     Pagination: typeof import('./../components/ProTable/Pagination.vue')['default']
     ProTable: typeof import('./../components/ProTable/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     SearchForm: typeof import('./../components/SearchForm/index.vue')['default']
     SearchFormItem: typeof import('./../components/SearchForm/SearchFormItem.vue')['default']
+    SvgIcon: typeof import('./../components/SvgIcon/index.vue')['default']
     TableColumn: typeof import('./../components/ProTable/TableColumn.vue')['default']
     TreeFilter: typeof import('./../components/TreeFilter/index.vue')['default']
   }