feat: 添加标签页管理功能和全局加载动画
This commit is contained in:
parent
87c0e1e130
commit
b7a012967e
|
|
@ -94,6 +94,39 @@
|
|||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<!-- 标签页 -->
|
||||
<div class="admin-tabs">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
type="editable-card"
|
||||
:hide-add="true"
|
||||
@edit="onTabEdit"
|
||||
@tabClick="onTabClick"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
:tab="tab.title"
|
||||
:closable="tabs.length > 1"
|
||||
>
|
||||
<template #closeIcon>
|
||||
<a-dropdown :trigger="['hover']">
|
||||
<CloseOutlined />
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }) => handleTabAction(key, tab)">
|
||||
<a-menu-item key="current">关闭当前标签</a-menu-item>
|
||||
<a-menu-item key="others">关闭其它标签</a-menu-item>
|
||||
<a-menu-item key="left">关闭左侧标签</a-menu-item>
|
||||
<a-menu-item key="right">关闭右侧标签</a-menu-item>
|
||||
<a-menu-item key="all">关闭所有标签</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<a-layout-content class="admin-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
|
|
@ -101,6 +134,9 @@
|
|||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
<a-spin :spinning="loading" class="global-loading" tip="加载中...">
|
||||
<div class="spin-content"></div>
|
||||
</a-spin>
|
||||
</a-layout-content>
|
||||
|
||||
<!-- 页脚 -->
|
||||
|
|
@ -112,7 +148,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
MenuUnfoldOutlined,
|
||||
|
|
@ -123,6 +159,7 @@ import {
|
|||
TeamOutlined,
|
||||
MenuOutlined,
|
||||
LogoutOutlined,
|
||||
CloseOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import Breadcrumb from '@/components/common/Breadcrumb.vue'
|
||||
|
|
@ -133,6 +170,201 @@ const route = useRoute()
|
|||
// 侧边栏折叠状态
|
||||
const collapsed = ref(false)
|
||||
|
||||
// 页面加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 标签页相关
|
||||
interface TabItem {
|
||||
title: string;
|
||||
path: string;
|
||||
name: string;
|
||||
query?: Record<string, any>;
|
||||
params?: Record<string, any>;
|
||||
}
|
||||
|
||||
const tabs = ref<TabItem[]>([]);
|
||||
const activeTab = ref('');
|
||||
|
||||
// 添加标签页
|
||||
const addTab = (route: any) => {
|
||||
const { path, meta, name, query, params } = route;
|
||||
const title = meta?.title || '未命名页面';
|
||||
|
||||
// 检查标签是否已存在
|
||||
const isExist = tabs.value.some(tab => tab.path === path);
|
||||
|
||||
if (!isExist) {
|
||||
tabs.value.push({
|
||||
title,
|
||||
path,
|
||||
name,
|
||||
query,
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
activeTab.value = path;
|
||||
};
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (targetPath: string) => {
|
||||
const targetIndex = tabs.value.findIndex(tab => tab.path === targetPath);
|
||||
|
||||
if (targetIndex === -1) return;
|
||||
|
||||
// 如果关闭的是当前激活的标签,则需要激活其他标签
|
||||
if (activeTab.value === targetPath) {
|
||||
// 优先激活右侧标签,如果没有则激活左侧标签
|
||||
if (targetIndex < tabs.value.length - 1) {
|
||||
activeTab.value = tabs.value[targetIndex + 1].path;
|
||||
router.push(tabs.value[targetIndex + 1]);
|
||||
} else if (targetIndex > 0) {
|
||||
activeTab.value = tabs.value[targetIndex - 1].path;
|
||||
router.push(tabs.value[targetIndex - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
tabs.value.splice(targetIndex, 1);
|
||||
saveTabsToStorage();
|
||||
};
|
||||
|
||||
// 处理标签页编辑事件
|
||||
const onTabEdit = (targetKey: string, action: 'add' | 'remove') => {
|
||||
if (action === 'remove') {
|
||||
closeTab(targetKey);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理标签页点击事件
|
||||
const onTabClick = (key: string) => {
|
||||
// 找到对应的标签页
|
||||
const tab = tabs.value.find(item => item.path === key);
|
||||
if (tab) {
|
||||
// 导航到对应的路由
|
||||
router.push({
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
params: tab.params
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理标签页操作
|
||||
const handleTabAction = (action: string, tab: TabItem) => {
|
||||
const currentIndex = tabs.value.findIndex(item => item.path === tab.path);
|
||||
|
||||
switch (action) {
|
||||
case 'current':
|
||||
closeTab(tab.path);
|
||||
break;
|
||||
case 'others':
|
||||
tabs.value = tabs.value.filter(item => item.path === tab.path);
|
||||
break;
|
||||
case 'left':
|
||||
if (currentIndex > 0) {
|
||||
tabs.value = tabs.value.filter((item, index) =>
|
||||
index >= currentIndex || item.path === '/dashboard'
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'right':
|
||||
if (currentIndex < tabs.value.length - 1) {
|
||||
tabs.value = tabs.value.filter((item, index) =>
|
||||
index <= currentIndex || item.path === '/dashboard'
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'all':
|
||||
// 保留首页
|
||||
const homePage = tabs.value.find(item => item.path === '/dashboard');
|
||||
tabs.value = homePage ? [homePage] : [];
|
||||
if (homePage) {
|
||||
activeTab.value = homePage.path;
|
||||
router.push(homePage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
router.beforeEach((to, from, next) => {
|
||||
loading.value = true;
|
||||
|
||||
// 如果路由有meta.title,则添加标签页
|
||||
if (to.meta?.title) {
|
||||
addTab(to);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
router.afterEach(() => {
|
||||
// 添加一个小延迟,让加载动画更流畅
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 保存标签页到本地存储
|
||||
const saveTabsToStorage = () => {
|
||||
localStorage.setItem('admin-tabs', JSON.stringify(tabs.value));
|
||||
localStorage.setItem('admin-active-tab', activeTab.value);
|
||||
};
|
||||
|
||||
// 从本地存储加载标签页
|
||||
const loadTabsFromStorage = () => {
|
||||
const savedTabs = localStorage.getItem('admin-tabs');
|
||||
const savedActiveTab = localStorage.getItem('admin-active-tab');
|
||||
|
||||
if (savedTabs) {
|
||||
try {
|
||||
const parsedTabs = JSON.parse(savedTabs);
|
||||
tabs.value = parsedTabs;
|
||||
|
||||
// 如果有保存的激活标签,则设置它
|
||||
if (savedActiveTab) {
|
||||
activeTab.value = savedActiveTab;
|
||||
|
||||
// 导航到激活的标签页
|
||||
const activeTabData = tabs.value.find(tab => tab.path === savedActiveTab);
|
||||
if (activeTabData && activeTabData.path !== route.path) {
|
||||
router.push({
|
||||
path: activeTabData.path,
|
||||
query: activeTabData.query,
|
||||
params: activeTabData.params
|
||||
});
|
||||
return true; // 表示已经从存储中恢复了标签页
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved tabs:', e);
|
||||
}
|
||||
}
|
||||
return false; // 表示没有从存储中恢复标签页
|
||||
};
|
||||
|
||||
// 监听标签页变化,保存到本地存储
|
||||
watch(tabs, () => {
|
||||
saveTabsToStorage();
|
||||
}, { deep: true });
|
||||
|
||||
watch(activeTab, () => {
|
||||
localStorage.setItem('admin-active-tab', activeTab.value);
|
||||
});
|
||||
|
||||
// 初始化标签页
|
||||
onMounted(() => {
|
||||
// 尝试从本地存储加载标签页
|
||||
const restored = loadTabsFromStorage();
|
||||
|
||||
// 如果没有恢复标签页或者标签页为空,则添加当前路由作为初始标签
|
||||
if (!restored || tabs.value.length === 0) {
|
||||
if (route.meta?.title) {
|
||||
addTab(route);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 菜单选中状态
|
||||
const selectedKeys = ref<string[]>([])
|
||||
const openKeys = ref<string[]>([])
|
||||
|
|
@ -302,7 +534,7 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
.admin-content {
|
||||
margin: 24px;
|
||||
margin: 8px 16px 16px;
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
flex: 1;
|
||||
|
|
@ -310,6 +542,8 @@ onMounted(() => {
|
|||
overflow-x: hidden;
|
||||
position: relative;
|
||||
height: 0; /* 让flex: 1生效 */
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
|
||||
.admin-footer {
|
||||
|
|
@ -319,6 +553,51 @@ onMounted(() => {
|
|||
background: #fff;
|
||||
}
|
||||
|
||||
.global-loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spin-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-tabs {
|
||||
background: #fff;
|
||||
padding: 6px 16px 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs) {
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
padding: 8px 16px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-tab-remove {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 路由过渡动画 */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
|
|
|
|||
Loading…
Reference in New Issue