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