237 lines
7.0 KiB
Vue
237 lines
7.0 KiB
Vue
<template>
|
|
<el-container style="min-height: 100vh; background-color: var(--ios-bg);">
|
|
<el-aside width="240px" class="admin-aside glass">
|
|
<div class="logo-area" @click="router.push('/admin/dashboard')" style="display: flex; align-items: center; justify-content: center; gap: 8px; cursor: pointer;">
|
|
<img src="/logo.png" alt="Logo" style="width: 48px; height: 26px;" />
|
|
<span class="logo-text">Hertz Admin</span>
|
|
</div>
|
|
<el-menu :default-active="active" class="admin-menu" @select="onSelect">
|
|
<template v-for="m in menus" :key="m.id">
|
|
<el-sub-menu v-if="m.children && m.children.length" :index="m.path || String(m.id)">
|
|
<template #title>
|
|
<el-icon v-if="iconComp(m.icon)">
|
|
<component :is="iconComp(m.icon)" />
|
|
</el-icon>
|
|
<span>{{ m.name }}</span>
|
|
</template>
|
|
<el-menu-item
|
|
v-for="c in m.children"
|
|
:key="c.id"
|
|
:index="c.path || ''"
|
|
@click="toMenuPath(c)"
|
|
>
|
|
<el-icon v-if="iconComp(c.icon)">
|
|
<component :is="iconComp(c.icon)" />
|
|
</el-icon>
|
|
<span>{{ c.name }}</span>
|
|
</el-menu-item>
|
|
</el-sub-menu>
|
|
<el-menu-item v-else :index="m.path || ''" @click="toMenuPath(m)">
|
|
<el-icon v-if="iconComp(m.icon)">
|
|
<component :is="iconComp(m.icon)" />
|
|
</el-icon>
|
|
<span>{{ m.name }}</span>
|
|
</el-menu-item>
|
|
</template>
|
|
</el-menu>
|
|
</el-aside>
|
|
|
|
<el-container>
|
|
<el-header height="64px" class="admin-header glass">
|
|
<el-breadcrumb separator="/">
|
|
<el-breadcrumb-item :to="{ path: '/admin' }">首页</el-breadcrumb-item>
|
|
<el-breadcrumb-item v-for="item in breadcrumbs" :key="item.id">{{ item.name }}</el-breadcrumb-item>
|
|
</el-breadcrumb>
|
|
<el-dropdown
|
|
trigger="hover"
|
|
placement="bottom-end"
|
|
popper-class="ios-dropdown-popper"
|
|
:popper-options="{ modifiers: [{ name: 'offset', options: { offset: [8] } }] }"
|
|
>
|
|
<span class="user-dropdown-link">
|
|
<el-avatar :size="36" :src="apiBase + auth.avatarPath" v-if="auth.avatarPath" />
|
|
<el-avatar :size="36" v-else class="default-avatar">{{ auth.nickname?.charAt(0) || auth.username.charAt(0) }}</el-avatar>
|
|
<span class="username">{{ auth.nickname || auth.username }}</span>
|
|
|
|
</span>
|
|
<template #dropdown>
|
|
<el-dropdown-menu class="ios-dropdown">
|
|
<el-dropdown-item disabled class="ios-dropdown-user">
|
|
<div class="ios-user-card">
|
|
<el-avatar :size="44" :src="apiBase + auth.avatarPath" v-if="auth.avatarPath" />
|
|
<el-avatar :size="44" v-else class="default-avatar">{{ auth.nickname?.charAt(0) || auth.username.charAt(0) }}</el-avatar>
|
|
<div class="ios-user-meta">
|
|
<div class="ios-user-name">{{ auth.nickname || auth.username }}</div>
|
|
<div class="ios-user-email">{{ auth.email || '—' }}</div>
|
|
</div>
|
|
</div>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item divided @click="router.push('/admin/profile')">
|
|
<el-icon><component :is="Icons.User" /></el-icon>
|
|
<span>个人资料</span>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item @click="router.push('/portal')">
|
|
<el-icon><component :is="Icons.House" /></el-icon>
|
|
<span>访问前台</span>
|
|
</el-dropdown-item>
|
|
<el-dropdown-item divided class="ios-logout-item" @click="logout">
|
|
<el-icon><component :is="Icons.SwitchButton" /></el-icon>
|
|
<span>退出登录</span>
|
|
</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</el-header>
|
|
<el-main style="padding: 24px; overflow-x: hidden;">
|
|
<router-view />
|
|
</el-main>
|
|
</el-container>
|
|
</el-container>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useAuthStore } from '../stores/auth'
|
|
import { useMenuStore } from '../stores/menu'
|
|
import * as Icons from '@element-plus/icons-vue'
|
|
|
|
const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8080'
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const auth = useAuthStore()
|
|
const menuStore = useMenuStore()
|
|
|
|
const active = computed(() => route.path)
|
|
const menus = computed(() => {
|
|
const root = menuStore.menuTree.length === 1 ? menuStore.menuTree[0] : undefined
|
|
if (root?.children?.length) return root.children
|
|
return menuStore.menuTree
|
|
})
|
|
|
|
const breadcrumbs = computed(() => {
|
|
const findPath = (nodes, targetPath, acc = []) => {
|
|
for (const node of nodes) {
|
|
if (node.path === targetPath) {
|
|
return [...acc, node]
|
|
}
|
|
if (node.children && node.children.length) {
|
|
const found = findPath(node.children, targetPath, [...acc, node])
|
|
if (found) return found
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
return findPath(menuStore.menuTree, route.path) || []
|
|
})
|
|
|
|
function iconComp(name) {
|
|
if (!name) return undefined
|
|
return Icons[name]
|
|
}
|
|
|
|
function toMenuPath(node) {
|
|
if (node.type === 'M' && node.path) router.push(node.path)
|
|
}
|
|
|
|
function onSelect(index) {
|
|
router.push(index)
|
|
}
|
|
|
|
function logout() {
|
|
auth.logout()
|
|
menuStore.reset()
|
|
router.replace('/login')
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.admin-aside {
|
|
border-right: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 20;
|
|
}
|
|
|
|
.logo-area {
|
|
height: 64px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0 24px;
|
|
}
|
|
|
|
.logo-text {
|
|
font-weight: 700;
|
|
font-size: 20px;
|
|
background: linear-gradient(135deg, var(--ios-primary) 0%, #00C6FB 100%);
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.admin-menu {
|
|
border-right: none;
|
|
padding: 16px 12px;
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
:deep(.el-menu-item), :deep(.el-sub-menu__title) {
|
|
border-radius: 12px;
|
|
margin-bottom: 6px;
|
|
height: 48px;
|
|
line-height: 48px;
|
|
color: #555;
|
|
}
|
|
|
|
:deep(.el-menu-item:hover), :deep(.el-sub-menu__title:hover) {
|
|
background-color: rgba(0,0,0,0.03);
|
|
color: #000;
|
|
}
|
|
|
|
:deep(.el-menu-item.is-active) {
|
|
background-color: var(--ios-primary);
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
:deep(.el-sub-menu .el-menu-item) {
|
|
height: 44px;
|
|
line-height: 44px;
|
|
margin-left: 4px;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.admin-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 32px;
|
|
z-index: 10;
|
|
border-bottom: 1px solid var(--ios-border);
|
|
}
|
|
|
|
.user-dropdown-link {
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
transition: background-color 0.2s;
|
|
outline: none;
|
|
}
|
|
|
|
.default-avatar {
|
|
background-color: var(--ios-primary);
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.username {
|
|
font-weight: 500;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|