feat: 新增主布局(顶部菜单+多标签页)
parent
cd663a1951
commit
76db7bb8d7
|
|
@ -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>
|
||||
|
|
@ -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 = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue