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