XuqmGroup-Web/tenant-platform/src/views/layout/MainLayout.vue

278 行
6.1 KiB
Vue

2026-04-21 22:07:29 +08:00
<template>
<el-container class="layout-container">
<el-aside v-if="!isMobile" width="220px" class="sidebar">
2026-04-21 22:07:29 +08:00
<div class="logo">
<span>XuqmGroup</span>
</div>
<el-menu
:default-active="$route.path"
router
background-color="#1d2129"
text-color="#c9d1d9"
active-text-color="#409eff"
class="nav-menu"
2026-04-21 22:07:29 +08:00
>
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
2026-04-21 22:07:29 +08:00
</el-menu-item>
</el-menu>
</el-aside>
<el-drawer
v-model="drawerVisible"
direction="ltr"
:with-header="false"
size="250px"
class="mobile-drawer"
>
<div class="drawer-brand">
<span>XuqmGroup</span>
</div>
<el-menu
:default-active="$route.path"
router
background-color="#1d2129"
text-color="#c9d1d9"
active-text-color="#409eff"
class="nav-menu"
@select="handleNavSelect"
>
<el-menu-item v-for="item in navItems" :key="item.path" :index="item.path">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</el-menu-item>
</el-menu>
</el-drawer>
<el-container class="content-shell">
2026-04-21 22:07:29 +08:00
<el-header class="header">
<div v-if="isMobile" class="mobile-header-left">
<el-button text circle class="menu-button" @click="drawerVisible = true">
<el-icon><Menu /></el-icon>
</el-button>
<span class="mobile-title">XuqmGroup</span>
</div>
<div v-else class="header-spacer" />
2026-04-21 22:07:29 +08:00
<div class="header-right">
<el-tooltip content="开发者文档" placement="bottom">
<a href="/docs/" target="_blank" class="docs-link">
<el-icon :size="18"><Document /></el-icon>
<span>文档</span>
</a>
</el-tooltip>
2026-04-21 22:07:29 +08:00
<el-dropdown @command="handleCommand">
<span class="user-info">
<el-avatar :size="32" style="background:#409eff">
{{ auth.user?.nickname?.charAt(0) ?? 'U' }}
</el-avatar>
<span class="nickname">{{ auth.user?.nickname }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-content">
2026-04-21 22:07:29 +08:00
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
2026-04-21 22:07:29 +08:00
import { useAuthStore } from '@/stores/auth'
import { useRoute, useRouter } from 'vue-router'
import { Document, Grid, Menu, Odometer, User } from '@element-plus/icons-vue'
2026-04-21 22:07:29 +08:00
const auth = useAuthStore()
const route = useRoute()
2026-04-21 22:07:29 +08:00
const router = useRouter()
const isMobile = ref(false)
const drawerVisible = ref(false)
const navItems = computed(() => {
const items = [
{ path: '/dashboard', label: '控制台', icon: Odometer },
{ path: '/apps', label: '我的应用', icon: Grid },
{ path: '/operation-logs', label: '操作日志', icon: Document },
]
if (auth.user?.type === 'MAIN') {
items.push({ path: '/accounts', label: '子账号管理', icon: User })
}
return items
})
function updateViewport() {
isMobile.value = window.innerWidth < 768
if (!isMobile.value) {
drawerVisible.value = false
}
}
function handleNavSelect() {
drawerVisible.value = false
}
2026-04-21 22:07:29 +08:00
function handleCommand(cmd: string) {
if (cmd === 'logout') {
auth.logout()
router.push('/login')
}
}
watch(
() => route.path,
() => {
drawerVisible.value = false
},
)
onMounted(() => {
updateViewport()
window.addEventListener('resize', updateViewport)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', updateViewport)
})
2026-04-21 22:07:29 +08:00
</script>
<style scoped>
.layout-container {
height: 100vh;
overflow: hidden;
background: linear-gradient(180deg, #f7f9fc 0%, #eef2f7 100%);
2026-04-21 22:07:29 +08:00
}
.sidebar {
background: #1d2129;
flex: 0 0 220px;
box-shadow: 8px 0 24px rgba(15, 23, 42, 0.08);
2026-04-21 22:07:29 +08:00
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #2d3142;
}
.nav-menu {
border-right: none;
}
.content-shell {
min-width: 0;
}
2026-04-21 22:07:29 +08:00
.header {
background: #fff;
border-bottom: 1px solid #e8e8e8;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
gap: 16px;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.03);
position: sticky;
top: 0;
z-index: 9;
2026-04-21 22:07:29 +08:00
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.header-spacer {
flex: 1;
}
.mobile-header-left {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.mobile-title {
font-size: 16px;
font-weight: 700;
color: #111827;
}
.menu-button {
color: #111827;
}
.docs-link {
display: flex;
align-items: center;
gap: 4px;
color: #555;
font-size: 13px;
text-decoration: none;
padding: 4px 8px;
border-radius: 6px;
transition: background 0.15s;
}
.docs-link:hover {
background: #f5f5f5;
color: #409eff;
2026-04-21 22:07:29 +08:00
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.nickname {
font-size: 14px;
color: #333;
}
.main-content {
padding: 16px;
overflow: auto;
}
.drawer-brand {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: bold;
border-bottom: 1px solid #2d3142;
background: #1d2129;
}
.mobile-drawer :deep(.el-drawer__body) {
padding: 0;
background: #1d2129;
}
@media (max-width: 767px) {
.layout-container {
display: block;
height: auto;
min-height: 100vh;
}
.header {
padding: 0 12px;
}
.header-right {
gap: 10px;
}
.docs-link span,
.nickname {
display: none;
}
.main-content {
padding: 12px;
}
}
2026-04-21 22:07:29 +08:00
</style>