通用中后台框架第一版
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
<template>
|
||||
<n-layout has-sider class="app-root app-backdrop">
|
||||
<n-layout-sider
|
||||
collapse-mode="width"
|
||||
:collapsed-width="72"
|
||||
:width="248"
|
||||
:collapsed="authStore.layoutCollapsed"
|
||||
class="layout-sider"
|
||||
:class="{ 'layout-sider-collapsed': authStore.layoutCollapsed }"
|
||||
show-trigger
|
||||
@collapse="authStore.layoutCollapsed = true"
|
||||
@expand="authStore.layoutCollapsed = false"
|
||||
>
|
||||
<div class="brand-mark">
|
||||
<span class="brand-dot"></span>
|
||||
<span class="brand-name" :title="appTitle">{{ appTitle }}</span>
|
||||
</div>
|
||||
<div class="menu-wrap">
|
||||
<AppMenu />
|
||||
</div>
|
||||
</n-layout-sider>
|
||||
|
||||
<n-layout class="app-main">
|
||||
<header class="app-header">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<h1>{{ currentTitle }}</h1>
|
||||
<AppBreadcrumb />
|
||||
</div>
|
||||
<div class="summary-actions">
|
||||
<n-button
|
||||
tertiary
|
||||
circle
|
||||
size="small"
|
||||
title="刷新当前页面"
|
||||
@click="authStore.refreshTab(route.path)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><RefreshCw /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-tag type="info" size="small">{{ authStore.user?.username ?? '访客' }}</n-tag>
|
||||
<n-button tertiary size="small" @click="handleLogout">退出</n-button>
|
||||
</div>
|
||||
</div>
|
||||
<AppTabs />
|
||||
</header>
|
||||
|
||||
<n-layout-content embedded class="layout-content bg-transparent">
|
||||
<div class="content-scroll">
|
||||
<router-view v-slot="{ Component, route: currentRoute }">
|
||||
<transition name="fade-slide" mode="out-in">
|
||||
<keep-alive :include="cachedRouteNames">
|
||||
<component
|
||||
:is="Component"
|
||||
:key="`${currentRoute.fullPath}-${authStore.tabRefreshMarks[currentRoute.path === '/workbench' ? '/dashboard' : currentRoute.path] ?? 0}`"
|
||||
/>
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</n-layout-content>
|
||||
</n-layout>
|
||||
</n-layout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import AppMenu from '@/components/AppMenu.vue'
|
||||
import AppTabs from '@/components/AppTabs.vue'
|
||||
import AppBreadcrumb from '@/components/AppBreadcrumb.vue'
|
||||
import { appEnv } from '@/config/env'
|
||||
import { resetDynamicRoutes } from '@/router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const appTitle = appEnv.appTitle
|
||||
|
||||
const currentTitle = computed(() => (route.meta.title as string | undefined) ?? '工作台')
|
||||
|
||||
const cachedRouteNames = computed(() =>
|
||||
router
|
||||
.getRoutes()
|
||||
.filter((item) => item.meta.keepAlive && typeof item.name === 'string')
|
||||
.map((item) => item.name as string)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (!route.meta.requiresAuth || route.path === '/403' || route.path === '/404') return
|
||||
const name = (route.name as string | undefined) ?? route.path
|
||||
authStore.addTab({
|
||||
name,
|
||||
title: (route.meta.title as string | undefined) ?? '未命名页面',
|
||||
path: route.path,
|
||||
closable: route.path !== '/dashboard' && route.path !== '/workbench'
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout()
|
||||
resetDynamicRoutes()
|
||||
await router.replace('/login')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-slide-enter-active,
|
||||
.fade-slide-leave-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-slide-enter-from,
|
||||
.fade-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
.app-root {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
height: 100vh;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 18px 18px;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
flex: 0 0 auto;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.06);
|
||||
padding: 12px 14px 8px;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.title-group {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.title-group h1 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.summary-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-backdrop {
|
||||
background: #f5f7fb;
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
height: 64px;
|
||||
padding: 0 18px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.brand-dot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #111827, #2563eb);
|
||||
box-shadow: 0 10px 22px rgba(37, 99, 235, 0.18);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.layout-sider-collapsed .brand-mark {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-sider-collapsed .brand-name {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user