feat: 优化AdminLayout布局组件,简化代码结构,调整部分元素顺序,并修复HeaderNav组件属性传递方式
This commit is contained in:
parent
b7a012967e
commit
1b9fb6f26d
|
|
@ -20,7 +20,11 @@ export default defineConfigWithVueTs(
|
|||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,14 @@
|
|||
<template>
|
||||
<a-layout class="admin-layout">
|
||||
<!-- 侧边栏 -->
|
||||
<a-layout-sider
|
||||
v-model:collapsed="collapsed"
|
||||
:trigger="null"
|
||||
collapsible
|
||||
class="admin-sider"
|
||||
>
|
||||
<a-layout-sider v-model:collapsed="collapsed" :trigger="null" class="admin-sider" collapsible>
|
||||
<div class="logo">
|
||||
<img src="@/assets/logo.svg" alt="Logo" />
|
||||
<img alt="Logo" src="@/assets/logo.svg" />
|
||||
<h1 v-show="!collapsed">Admin System</h1>
|
||||
</div>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
>
|
||||
|
|
@ -58,18 +53,16 @@
|
|||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
/>
|
||||
<menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
|
||||
<breadcrumb />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-dropdown>
|
||||
<a class="user-dropdown" @click.prevent>
|
||||
<a-avatar>
|
||||
<template #icon><user-outlined /></template>
|
||||
<template #icon>
|
||||
<user-outlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="username">管理员</span>
|
||||
</a>
|
||||
|
|
@ -98,16 +91,16 @@
|
|||
<div class="admin-tabs">
|
||||
<a-tabs
|
||||
v-model:activeKey="activeTab"
|
||||
type="editable-card"
|
||||
:hide-add="true"
|
||||
type="editable-card"
|
||||
@edit="onTabEdit"
|
||||
@tabClick="onTabClick"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="tab in tabs"
|
||||
:key="tab.path"
|
||||
:tab="tab.title"
|
||||
:closable="tabs.length > 1"
|
||||
:tab="tab.title"
|
||||
>
|
||||
<template #closeIcon>
|
||||
<a-dropdown :trigger="['hover']">
|
||||
|
|
@ -130,7 +123,7 @@
|
|||
<!-- 内容 -->
|
||||
<a-layout-content class="admin-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<transition mode="out-in" name="fade">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
|
|
@ -147,19 +140,19 @@
|
|||
</a-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
DashboardOutlined,
|
||||
SettingOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
MenuOutlined,
|
||||
LogoutOutlined,
|
||||
CloseOutlined,
|
||||
DashboardOutlined,
|
||||
LogoutOutlined,
|
||||
MenuFoldOutlined,
|
||||
MenuOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import Breadcrumb from '@/components/common/Breadcrumb.vue'
|
||||
|
|
@ -175,23 +168,23 @@ const loading = ref(false)
|
|||
|
||||
// 标签页相关
|
||||
interface TabItem {
|
||||
title: string;
|
||||
path: string;
|
||||
name: string;
|
||||
query?: Record<string, any>;
|
||||
params?: Record<string, any>;
|
||||
title: string
|
||||
path: string
|
||||
name: string
|
||||
query?: Record<string, any>
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
const tabs = ref<TabItem[]>([]);
|
||||
const activeTab = ref('');
|
||||
const tabs = ref<TabItem[]>([])
|
||||
const activeTab = ref('')
|
||||
|
||||
// 添加标签页
|
||||
const addTab = (route: any) => {
|
||||
const { path, meta, name, query, params } = route;
|
||||
const title = meta?.title || '未命名页面';
|
||||
const { path, meta, name, query, params } = route
|
||||
const title = meta?.title || '未命名页面'
|
||||
|
||||
// 检查标签是否已存在
|
||||
const isExist = tabs.value.some(tab => tab.path === path);
|
||||
const isExist = tabs.value.some((tab) => tab.path === path)
|
||||
|
||||
if (!isExist) {
|
||||
tabs.value.push({
|
||||
|
|
@ -199,171 +192,175 @@ const addTab = (route: any) => {
|
|||
path,
|
||||
name,
|
||||
query,
|
||||
params
|
||||
});
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
activeTab.value = path;
|
||||
};
|
||||
activeTab.value = path
|
||||
}
|
||||
|
||||
// 关闭标签页
|
||||
const closeTab = (targetPath: string) => {
|
||||
const targetIndex = tabs.value.findIndex(tab => tab.path === targetPath);
|
||||
const targetIndex = tabs.value.findIndex((tab) => tab.path === targetPath)
|
||||
|
||||
if (targetIndex === -1) return;
|
||||
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]);
|
||||
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]);
|
||||
activeTab.value = tabs.value[targetIndex - 1].path
|
||||
router.push(tabs.value[targetIndex - 1])
|
||||
}
|
||||
}
|
||||
|
||||
tabs.value.splice(targetIndex, 1);
|
||||
saveTabsToStorage();
|
||||
};
|
||||
tabs.value.splice(targetIndex, 1)
|
||||
saveTabsToStorage()
|
||||
}
|
||||
|
||||
// 处理标签页编辑事件
|
||||
const onTabEdit = (targetKey: string, action: 'add' | 'remove') => {
|
||||
if (action === 'remove') {
|
||||
closeTab(targetKey);
|
||||
closeTab(targetKey)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理标签页点击事件
|
||||
const onTabClick = (key: string) => {
|
||||
// 找到对应的标签页
|
||||
const tab = tabs.value.find(item => item.path === key);
|
||||
const tab = tabs.value.find((item) => item.path === key)
|
||||
if (tab) {
|
||||
// 导航到对应的路由
|
||||
router.push({
|
||||
path: tab.path,
|
||||
query: tab.query,
|
||||
params: tab.params
|
||||
});
|
||||
params: tab.params,
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理标签页操作
|
||||
const handleTabAction = (action: string, tab: TabItem) => {
|
||||
const currentIndex = tabs.value.findIndex(item => item.path === tab.path);
|
||||
const currentIndex = tabs.value.findIndex((item) => item.path === tab.path)
|
||||
|
||||
switch (action) {
|
||||
case 'current':
|
||||
closeTab(tab.path);
|
||||
break;
|
||||
closeTab(tab.path)
|
||||
break
|
||||
case 'others':
|
||||
tabs.value = tabs.value.filter(item => item.path === tab.path);
|
||||
break;
|
||||
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'
|
||||
);
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index >= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break;
|
||||
break
|
||||
case 'right':
|
||||
if (currentIndex < tabs.value.length - 1) {
|
||||
tabs.value = tabs.value.filter((item, index) =>
|
||||
index <= currentIndex || item.path === '/dashboard'
|
||||
);
|
||||
tabs.value = tabs.value.filter(
|
||||
(item, index) => index <= currentIndex || item.path === '/dashboard',
|
||||
)
|
||||
}
|
||||
break;
|
||||
break
|
||||
case 'all':
|
||||
// 保留首页
|
||||
const homePage = tabs.value.find(item => item.path === '/dashboard');
|
||||
tabs.value = homePage ? [homePage] : [];
|
||||
const homePage = tabs.value.find((item) => item.path === '/dashboard')
|
||||
tabs.value = homePage ? [homePage] : []
|
||||
if (homePage) {
|
||||
activeTab.value = homePage.path;
|
||||
router.push(homePage);
|
||||
activeTab.value = homePage.path
|
||||
router.push(homePage)
|
||||
}
|
||||
break
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听路由变化
|
||||
router.beforeEach((to, from, next) => {
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
|
||||
// 如果路由有meta.title,则添加标签页
|
||||
if (to.meta?.title) {
|
||||
addTab(to);
|
||||
addTab(to)
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
// 添加一个小延迟,让加载动画更流畅
|
||||
setTimeout(() => {
|
||||
loading.value = false;
|
||||
}, 300);
|
||||
});
|
||||
loading.value = false
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 保存标签页到本地存储
|
||||
const saveTabsToStorage = () => {
|
||||
localStorage.setItem('admin-tabs', JSON.stringify(tabs.value));
|
||||
localStorage.setItem('admin-active-tab', activeTab.value);
|
||||
};
|
||||
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');
|
||||
const savedTabs = localStorage.getItem('admin-tabs')
|
||||
const savedActiveTab = localStorage.getItem('admin-active-tab')
|
||||
|
||||
if (savedTabs) {
|
||||
try {
|
||||
const parsedTabs = JSON.parse(savedTabs);
|
||||
tabs.value = parsedTabs;
|
||||
const parsedTabs = JSON.parse(savedTabs)
|
||||
tabs.value = parsedTabs
|
||||
|
||||
// 如果有保存的激活标签,则设置它
|
||||
if (savedActiveTab) {
|
||||
activeTab.value = savedActiveTab;
|
||||
activeTab.value = savedActiveTab
|
||||
|
||||
// 导航到激活的标签页
|
||||
const activeTabData = tabs.value.find(tab => tab.path === 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; // 表示已经从存储中恢复了标签页
|
||||
params: activeTabData.params,
|
||||
})
|
||||
return true // 表示已经从存储中恢复了标签页
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse saved tabs:', e);
|
||||
console.error('Failed to parse saved tabs:', e)
|
||||
}
|
||||
}
|
||||
return false; // 表示没有从存储中恢复标签页
|
||||
};
|
||||
return false // 表示没有从存储中恢复标签页
|
||||
}
|
||||
|
||||
// 监听标签页变化,保存到本地存储
|
||||
watch(tabs, () => {
|
||||
saveTabsToStorage();
|
||||
}, { deep: true });
|
||||
watch(
|
||||
tabs,
|
||||
() => {
|
||||
saveTabsToStorage()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
watch(activeTab, () => {
|
||||
localStorage.setItem('admin-active-tab', activeTab.value);
|
||||
});
|
||||
localStorage.setItem('admin-active-tab', activeTab.value)
|
||||
})
|
||||
|
||||
// 初始化标签页
|
||||
onMounted(() => {
|
||||
// 尝试从本地存储加载标签页
|
||||
const restored = loadTabsFromStorage();
|
||||
const restored = loadTabsFromStorage()
|
||||
|
||||
// 如果没有恢复标签页或者标签页为空,则添加当前路由作为初始标签
|
||||
if (!restored || tabs.value.length === 0) {
|
||||
if (route.meta?.title) {
|
||||
addTab(route);
|
||||
addTab(route)
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// 菜单选中状态
|
||||
const selectedKeys = ref<string[]>([])
|
||||
|
|
@ -436,7 +433,7 @@ watch(
|
|||
() => route.path,
|
||||
() => {
|
||||
updateSelectedMenu()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const currentYear = ref(new Date().getFullYear())
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
<menu-unfold-outlined
|
||||
v-if="collapsed"
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
@click="toggleCollapsed"
|
||||
/>
|
||||
<menu-fold-outlined
|
||||
v-else
|
||||
class="trigger"
|
||||
@click="() => (collapsed = !collapsed)"
|
||||
@click="toggleCollapsed"
|
||||
/>
|
||||
<span class="logo">Admin Template</span>
|
||||
</div>
|
||||
|
|
@ -59,11 +59,13 @@ import {
|
|||
LogoutOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
defineProps<{
|
||||
collapsed: boolean
|
||||
}>()
|
||||
const props = defineProps<{ collapsed: boolean }>()
|
||||
|
||||
defineEmits(['update:collapsed'])
|
||||
const emit = defineEmits(['update:collapsed'])
|
||||
|
||||
function toggleCollapsed() {
|
||||
emit('update:collapsed', !props.collapsed)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
Loading…
Reference in New Issue