v2
This commit is contained in:
236
ui/src/layouts/AdminLayout.vue
Normal file
236
ui/src/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<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>
|
||||
173
ui/src/layouts/PortalLayout.vue
Normal file
173
ui/src/layouts/PortalLayout.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<div class="portal-layout">
|
||||
<header class="portal-header glass">
|
||||
<div class="header-content">
|
||||
<div class="logo" @click="go('/portal/home')" style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<img src="/logo.png" alt="Logo" style="width: 48px; height: 26px;" />
|
||||
<span>Hertz Admin</span>
|
||||
</div>
|
||||
<el-menu mode="horizontal" :default-active="active()" class="portal-menu" :ellipsis="false">
|
||||
<el-menu-item index="/portal/home" @click="go('/portal/home')">首页</el-menu-item>
|
||||
<el-menu-item index="/portal/monitor" @click="go('/portal/monitor')">监控</el-menu-item>
|
||||
<el-menu-item index="/portal/chat" @click="go('/portal/chat')">聊天</el-menu-item>
|
||||
<el-menu-item index="/portal/about" @click="go('/portal/about')">关于</el-menu-item>
|
||||
</el-menu>
|
||||
<div class="auth-actions">
|
||||
<template v-if="!auth.token">
|
||||
<el-button type="primary" @click="goLogin">登录</el-button>
|
||||
<el-button @click="goRegister" style="border: 1px solid var(--ios-primary); color: var(--ios-primary);">注册</el-button>
|
||||
</template>
|
||||
<el-dropdown v-else trigger="hover" placement="bottom-end" popper-class="ios-dropdown-popper">
|
||||
<span class="user-link">
|
||||
<el-avatar :size="32" :src="apiBase + auth.avatarPath" v-if="auth.avatarPath" />
|
||||
<el-avatar :size="32" 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="go('/portal/profile')">
|
||||
<el-icon><component :is="Icons.User" /></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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main class="portal-main">
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
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 active = () => route.path
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function goLogin() {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
function goRegister() {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout()
|
||||
router.push('/portal/home')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.portal-layout {
|
||||
min-height: 100vh;
|
||||
background-color: var(--ios-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.portal-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: 64px;
|
||||
border-bottom: 1px solid var(--ios-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 24px;
|
||||
}
|
||||
.logo {
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
margin-right: 40px;
|
||||
background: linear-gradient(135deg, var(--ios-primary) 0%, #00C6FB 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.portal-menu {
|
||||
flex: 1;
|
||||
border-bottom: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
:deep(.el-menu-item) {
|
||||
background: transparent !important;
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
border-bottom: 2px solid transparent !important;
|
||||
color: #555 !important;
|
||||
}
|
||||
:deep(.el-menu-item.is-active) {
|
||||
color: var(--ios-primary) !important;
|
||||
border-bottom-color: var(--ios-primary) !important;
|
||||
}
|
||||
:deep(.el-menu-item:hover) {
|
||||
color: var(--ios-primary) !important;
|
||||
}
|
||||
.auth-actions {
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.user-link {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
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;
|
||||
}
|
||||
.portal-main {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user