完善UI
This commit is contained in:
+159
-45
@@ -2,12 +2,15 @@
|
||||
<n-layout has-sider class="app-root app-backdrop">
|
||||
<n-layout-sider
|
||||
collapse-mode="width"
|
||||
:collapsed-width="72"
|
||||
:width="248"
|
||||
:collapsed="authStore.layoutCollapsed"
|
||||
:collapsed-width="isFullscreen ? 0 : 72"
|
||||
:width="siderWidth"
|
||||
:collapsed="isFullscreen || authStore.layoutCollapsed"
|
||||
class="layout-sider"
|
||||
:class="{ 'layout-sider-collapsed': authStore.layoutCollapsed }"
|
||||
show-trigger
|
||||
:class="{
|
||||
'layout-sider-collapsed': isSiderCollapsed,
|
||||
'layout-sider-hidden': isFullscreen
|
||||
}"
|
||||
:show-trigger="!isFullscreen"
|
||||
@collapse="authStore.layoutCollapsed = true"
|
||||
@expand="authStore.layoutCollapsed = false"
|
||||
>
|
||||
@@ -18,28 +21,49 @@
|
||||
<div class="menu-wrap">
|
||||
<AppMenu />
|
||||
</div>
|
||||
<div
|
||||
v-if="!isSiderCollapsed"
|
||||
class="sider-resizer"
|
||||
title="拖动调整菜单宽度"
|
||||
@pointerdown="startSiderResize"
|
||||
></div>
|
||||
</n-layout-sider>
|
||||
|
||||
<n-layout class="app-main">
|
||||
<header class="app-header">
|
||||
<n-layout class="app-main" :class="{ 'app-main-fullscreen': isFullscreen }">
|
||||
<header v-if="!isFullscreen" class="app-header">
|
||||
<div class="header-main">
|
||||
<div class="title-group">
|
||||
<h1>{{ currentTitle }}</h1>
|
||||
<n-button
|
||||
tertiary
|
||||
circle
|
||||
size="small"
|
||||
class="header-refresh"
|
||||
title="刷新当前页面"
|
||||
@click="authStore.refreshTab(route.path)"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><RefreshCw /></n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
tertiary
|
||||
circle
|
||||
size="small"
|
||||
class="header-fullscreen"
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<Minimize2 v-if="isFullscreen" />
|
||||
<Maximize2 v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
<div class="breadcrumb-group">
|
||||
<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-tag type="info" size="small">{{ userDisplayName }}</n-tag>
|
||||
<n-button tertiary size="small" @click="handleLogout">退出</n-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,8 +89,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue'
|
||||
import { RefreshCw } from 'lucide-vue-next'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { Maximize2, Minimize2, RefreshCw } from 'lucide-vue-next'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import AppMenu from '@/components/AppMenu.vue'
|
||||
@@ -79,8 +103,18 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const appTitle = appEnv.appTitle
|
||||
|
||||
const currentTitle = computed(() => (route.meta.title as string | undefined) ?? '工作台')
|
||||
const isFullscreen = ref(false)
|
||||
const SIDER_WIDTH_KEY = 'platform.layout.siderWidth'
|
||||
const MIN_SIDER_WIDTH = 200
|
||||
const MAX_SIDER_WIDTH = 360
|
||||
const DEFAULT_SIDER_WIDTH = 248
|
||||
const storedSiderWidth = Number(localStorage.getItem(SIDER_WIDTH_KEY))
|
||||
const siderWidth = ref(
|
||||
Number.isFinite(storedSiderWidth)
|
||||
? Math.min(MAX_SIDER_WIDTH, Math.max(MIN_SIDER_WIDTH, storedSiderWidth))
|
||||
: DEFAULT_SIDER_WIDTH
|
||||
)
|
||||
const isSiderCollapsed = computed(() => isFullscreen.value || authStore.layoutCollapsed)
|
||||
|
||||
const cachedRouteNames = computed(() =>
|
||||
router
|
||||
@@ -89,6 +123,11 @@ const cachedRouteNames = computed(() =>
|
||||
.map((item) => item.name as string)
|
||||
)
|
||||
|
||||
const userDisplayName = computed(() => {
|
||||
const nickname = authStore.user?.nickname?.trim()
|
||||
return nickname || authStore.user?.username || '访客'
|
||||
})
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
@@ -104,6 +143,47 @@ watch(
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
function syncFullscreenState() {
|
||||
isFullscreen.value = Boolean(document.fullscreenElement)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncFullscreenState()
|
||||
document.addEventListener('fullscreenchange', syncFullscreenState)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('fullscreenchange', syncFullscreenState)
|
||||
stopSiderResize()
|
||||
})
|
||||
|
||||
async function toggleFullscreen() {
|
||||
if (document.fullscreenElement) {
|
||||
await document.exitFullscreen()
|
||||
return
|
||||
}
|
||||
await document.documentElement.requestFullscreen()
|
||||
}
|
||||
|
||||
function startSiderResize(event: PointerEvent) {
|
||||
event.preventDefault()
|
||||
document.body.classList.add('sider-resizing')
|
||||
window.addEventListener('pointermove', resizeSider)
|
||||
window.addEventListener('pointerup', stopSiderResize)
|
||||
}
|
||||
|
||||
function resizeSider(event: PointerEvent) {
|
||||
const nextWidth = Math.min(MAX_SIDER_WIDTH, Math.max(MIN_SIDER_WIDTH, event.clientX))
|
||||
siderWidth.value = nextWidth
|
||||
localStorage.setItem(SIDER_WIDTH_KEY, String(nextWidth))
|
||||
}
|
||||
|
||||
function stopSiderResize() {
|
||||
document.body.classList.remove('sider-resizing')
|
||||
window.removeEventListener('pointermove', resizeSider)
|
||||
window.removeEventListener('pointerup', stopSiderResize)
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
await authStore.logout()
|
||||
resetDynamicRoutes()
|
||||
@@ -134,40 +214,37 @@ async function handleLogout() {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 18px 18px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.app-main-fullscreen .layout-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.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;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
background: #ffffff;
|
||||
padding: 10px 18px 8px;
|
||||
}
|
||||
|
||||
.header-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.title-group {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
.header-refresh,
|
||||
.header-fullscreen {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.title-group h1 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
.breadcrumb-group {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-actions {
|
||||
@@ -181,7 +258,7 @@ async function handleLogout() {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 14px;
|
||||
padding: 16px 18px 18px;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
@@ -195,10 +272,15 @@ async function handleLogout() {
|
||||
}
|
||||
|
||||
.layout-sider {
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.layout-sider-hidden {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -231,6 +313,38 @@ async function handleLogout() {
|
||||
padding: 12px 10px;
|
||||
}
|
||||
|
||||
.sider-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -3px;
|
||||
z-index: 3;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sider-resizer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 2px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
transition: background 0.16s ease;
|
||||
}
|
||||
|
||||
.sider-resizer:hover::after,
|
||||
:global(.sider-resizing) .sider-resizer::after {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
:global(.sider-resizing) {
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.layout-sider-collapsed .brand-mark {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
|
||||
Reference in New Issue
Block a user