feat: 优化AdminLayout布局组件,简化代码结构,调整部分元素顺序,并修复HeaderNav组件属性传递方式

This commit is contained in:
FalingCliff 2025-06-15 01:06:57 +08:00
parent b7a012967e
commit 1b9fb6f26d
5 changed files with 118 additions and 117 deletions

View File

@ -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__/*'],

View File

@ -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()

View File

@ -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(() => {

View File

@ -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>

View File

@ -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>