feat: 新增主布局(顶部菜单+多标签页)

main
halo 2026-07-08 15:54:12 +08:00
parent cd663a1951
commit 76db7bb8d7
2 changed files with 187 additions and 0 deletions

148
src/layouts/MainLayout.vue Normal file
View File

@ -0,0 +1,148 @@
<template>
<a-layout class="main-layout">
<a-layout-header class="main-header">
<div class="logo">凡荣e链</div>
<a-menu
theme="dark"
mode="horizontal"
:selected-keys="selectedKeys"
class="main-menu"
>
<template v-for="node in menuTree" :key="node.id">
<a-sub-menu v-if="node.children && node.children.length" :key="node.id">
<template #title>{{ node.name }}</template>
<a-menu-item v-for="child in node.children" :key="child.path" @click="onMenuClick(child)">
{{ child.name }}
</a-menu-item>
</a-sub-menu>
<a-menu-item v-else :key="node.path" @click="onMenuClick(node)">
{{ node.name }}
</a-menu-item>
</template>
</a-menu>
<div class="user-area">
<a-dropdown>
<span class="user-trigger">
<UserOutlined />
{{ authStore.userInfo?.realName || authStore.userInfo?.username || '用户' }}
</span>
<template #overlay>
<a-menu @click="onUserMenuClick">
<a-menu-item key="logout">退出登录</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<div class="tabs-bar">
<a-tabs
v-model:activeKey="tabsStore.activeKey"
type="editable-card"
hide-add
size="small"
@tabClick="onTabClick"
@edit="onTabEdit"
>
<a-tab-pane v-for="tab in tabsStore.tabs" :key="tab.key" :tab="tab.title" :closable="tabsStore.tabs.length > 1" />
</a-tabs>
</div>
<a-layout-content class="main-content">
<router-view />
</a-layout-content>
</a-layout>
</template>
<script setup>
import { computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { UserOutlined } from '@ant-design/icons-vue'
import { useAuthStore } from '@/stores/auth'
import { useTabsStore } from '@/stores/tabs'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const tabsStore = useTabsStore()
const menuTree = computed(() => authStore.menuTree)
const selectedKeys = computed(() => [route.path])
function onMenuClick(node) {
if (node.path) router.push(node.path)
}
function onUserMenuClick({ key }) {
if (key === 'logout') {
authStore.logout().finally(() => {
tabsStore.reset()
router.push('/login')
})
}
}
function onTabClick(key) {
const tab = tabsStore.tabs.find((t) => t.key === key)
if (tab) router.push(tab.path)
}
function onTabEdit(targetKey, action) {
if (action === 'remove') {
tabsStore.closeTab(targetKey, router, route.path)
}
}
watch(
() => route.fullPath,
() => {
if (route.meta?.title) tabsStore.openTab(route)
},
{ immediate: true }
)
</script>
<style scoped>
.main-layout {
min-height: 100vh;
}
.main-header {
display: flex;
align-items: center;
padding: 0 16px;
}
.logo {
color: #fff;
font-size: 18px;
font-weight: 600;
margin-right: 24px;
white-space: nowrap;
}
.main-menu {
flex: 1;
min-width: 0;
}
.user-area {
color: #fff;
margin-left: 16px;
white-space: nowrap;
}
.user-trigger {
color: #fff;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
}
.tabs-bar {
background: #fff;
padding: 0 16px;
border-bottom: 1px solid #f0f0f0;
}
.main-content {
margin: 16px;
padding: 24px;
background: #fff;
min-height: calc(100vh - 160px);
}
</style>

39
src/stores/tabs.js Normal file
View File

@ -0,0 +1,39 @@
import { defineStore } from 'pinia'
export const useTabsStore = defineStore('tabs', {
state: () => ({
tabs: [],
activeKey: ''
}),
actions: {
openTab(route) {
const key = route.path
if (!this.tabs.some((t) => t.key === key)) {
this.tabs.push({
key,
title: route.meta?.title || key,
path: route.fullPath
})
}
this.activeKey = key
},
closeTab(key, router, currentPath) {
const index = this.tabs.findIndex((t) => t.key === key)
if (index === -1) return
this.tabs.splice(index, 1)
if (key === currentPath) {
const next = this.tabs[index] || this.tabs[index - 1]
if (next) {
this.activeKey = next.key
router.push(next.path)
} else {
router.push('/')
}
}
},
reset() {
this.tabs = []
this.activeKey = ''
}
}
})