feat: 收口组件样式并调整移动端侧栏
This commit is contained in:
+184
-23
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, h, provide, ref, shallowRef } from 'vue'
|
||||
import { computed, h, onScopeDispose, provide, ref, shallowRef, watch } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import {
|
||||
NButton,
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
NGlobalStyle,
|
||||
NLayout,
|
||||
NLayoutHeader,
|
||||
NLayoutSider,
|
||||
NMenu,
|
||||
NScrollbar,
|
||||
NTooltip,
|
||||
@@ -33,10 +32,61 @@ import { useTheme } from './composables/useTheme'
|
||||
import ThemeToggle from './components/ui/ThemeToggle.vue'
|
||||
import { projectAccessKey, type ProjectAccess } from './features/projects/access'
|
||||
|
||||
/** 窄屏与桌面共用同一侧栏;窄屏展开时主栏右移,宽度保持折叠时的尺寸。 */
|
||||
const NARROW_NAV = '(max-width: 800px)'
|
||||
/** 折叠侧栏宽度,与菜单 collapsed-width 对齐。 */
|
||||
const SIDER_COLLAPSED_WIDTH = 64
|
||||
/** 展开侧栏宽度,窄屏裁切动画以内层保持该宽度。 */
|
||||
const SIDER_EXPANDED_WIDTH = 208
|
||||
/** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */
|
||||
const NAV_ICON_SIZE = 20
|
||||
/** 折叠与展开共用的图标左偏移,避免切换时图标跳动。 */
|
||||
const NAV_ICON_INDENT = SIDER_COLLAPSED_WIDTH / 2 - NAV_ICON_SIZE / 2
|
||||
|
||||
/** 应用外壳固定在视口内,业务数据仍由各工作区自行加载。 */
|
||||
const route = useRoute()
|
||||
const collapsed = ref(window.matchMedia('(max-width: 800px)').matches)
|
||||
const narrowMedia = window.matchMedia(NARROW_NAV)
|
||||
const narrow = ref(narrowMedia.matches)
|
||||
const collapsed = ref(narrow.value)
|
||||
const settingsOpen = ref(false)
|
||||
/** 窄屏折叠只裁切文字,不把菜单收成图标,避免推挤动画时错位。 */
|
||||
const menuCollapsed = computed(() => !narrow.value && collapsed.value)
|
||||
|
||||
/** 视口跨过断点时:进入窄屏收回侧栏。 */
|
||||
function syncNarrow(event: MediaQueryListEvent) {
|
||||
narrow.value = event.matches
|
||||
if (event.matches) collapsed.value = true
|
||||
}
|
||||
narrowMedia.addEventListener('change', syncNarrow)
|
||||
onScopeDispose(() => narrowMedia.removeEventListener('change', syncNarrow))
|
||||
|
||||
/** 宽屏与窄屏都只切换这一份侧栏的展开态。 */
|
||||
function toggleNav() {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
/** 打开后端连接说明;窄屏先收回侧栏给弹窗腾出宽度。 */
|
||||
function openSettings() {
|
||||
if (narrow.value) collapsed.value = true
|
||||
settingsOpen.value = true
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
() => {
|
||||
if (narrow.value) collapsed.value = true
|
||||
}
|
||||
)
|
||||
|
||||
/** Esc 收回窄屏展开的侧栏。 */
|
||||
function onKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && narrow.value && !collapsed.value) collapsed.value = true
|
||||
}
|
||||
window.addEventListener('keydown', onKeydown)
|
||||
onScopeDispose(() => window.removeEventListener('keydown', onKeydown))
|
||||
|
||||
/** 顶栏切换按钮的可访问名称。 */
|
||||
const navToggleLabel = computed(() => (collapsed.value ? '展开侧栏' : '折叠侧栏'))
|
||||
const { preference, theme, overrides } = useTheme()
|
||||
const projectId = computed(() => String(route.params.projectId || ''))
|
||||
const projectAccess = shallowRef<ProjectAccess | null>(null)
|
||||
@@ -91,18 +141,22 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
>
|
||||
<NGlobalStyle />
|
||||
<a href="#main-content" class="skip-link">跳到主要内容</a>
|
||||
<NLayout has-sider class="admin-layout">
|
||||
<NLayoutSider
|
||||
collapse-mode="width"
|
||||
:collapsed="collapsed"
|
||||
:width="208"
|
||||
:collapsed-width="64"
|
||||
:native-scrollbar="false"
|
||||
<div
|
||||
class="admin-layout"
|
||||
:style="{
|
||||
'--admin-sider-collapsed': SIDER_COLLAPSED_WIDTH + 'px',
|
||||
'--admin-sider-expanded': SIDER_EXPANDED_WIDTH + 'px'
|
||||
}"
|
||||
>
|
||||
<!-- 以下是主导航侧栏:宽屏与窄屏共用同一份 -->
|
||||
<aside
|
||||
class="admin-sider"
|
||||
content-class="admin-sider-content"
|
||||
:class="{ 'is-collapsed': collapsed }"
|
||||
:style="{ width: (collapsed ? SIDER_COLLAPSED_WIDTH : SIDER_EXPANDED_WIDTH) + 'px' }"
|
||||
aria-label="主导航"
|
||||
>
|
||||
<RouterLink to="/projects" class="admin-brand" aria-label="短剧工作台首页">
|
||||
<Clapperboard :size="24" /><span v-if="!collapsed"
|
||||
<Clapperboard :size="24" /><span v-if="!menuCollapsed"
|
||||
>短剧工作台<small>SHORT DRAMA STUDIO</small></span
|
||||
>
|
||||
</RouterLink>
|
||||
@@ -110,9 +164,12 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
<NMenu
|
||||
:value="route.path"
|
||||
:options="menuOptions"
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="19"
|
||||
:collapsed="menuCollapsed"
|
||||
:collapsed-width="SIDER_COLLAPSED_WIDTH"
|
||||
:icon-size="NAV_ICON_SIZE"
|
||||
:collapsed-icon-size="NAV_ICON_SIZE"
|
||||
:indent="NAV_ICON_INDENT"
|
||||
:root-indent="NAV_ICON_INDENT"
|
||||
/>
|
||||
</NScrollbar>
|
||||
<footer class="admin-sider-footer">
|
||||
@@ -122,27 +179,29 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
quaternary
|
||||
block
|
||||
class="admin-settings-button"
|
||||
:class="{ 'is-collapsed': collapsed, 'icon-button': collapsed }"
|
||||
:class="{ 'is-collapsed': menuCollapsed }"
|
||||
aria-label="后端连接"
|
||||
@click="settingsOpen = true"
|
||||
@click="openSettings"
|
||||
>
|
||||
<template #icon><Settings2 :size="18" /></template>
|
||||
<span v-if="!collapsed">后端连接</span>
|
||||
<span v-if="!menuCollapsed">后端连接</span>
|
||||
</NButton>
|
||||
</template>
|
||||
后端连接
|
||||
</NTooltip>
|
||||
</footer>
|
||||
</NLayoutSider>
|
||||
</aside>
|
||||
<!-- 以下是顶栏与主内容 -->
|
||||
<NLayout class="admin-main">
|
||||
<NLayoutHeader class="admin-topbar">
|
||||
<div class="flex min-w-0 items-center gap-3">
|
||||
<NButton
|
||||
quaternary
|
||||
class="icon-button"
|
||||
:aria-label="collapsed ? '展开侧栏' : '折叠侧栏'"
|
||||
:title="collapsed ? '展开侧栏' : '折叠侧栏'"
|
||||
@click="collapsed = !collapsed"
|
||||
:aria-label="navToggleLabel"
|
||||
:title="navToggleLabel"
|
||||
:aria-expanded="!collapsed"
|
||||
@click="toggleNav"
|
||||
>
|
||||
<template #icon
|
||||
><PanelLeftOpen v-if="collapsed" :size="18" /><PanelLeftClose v-else :size="18"
|
||||
@@ -154,7 +213,7 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
</NLayoutHeader>
|
||||
<main id="main-content" tabindex="-1" class="admin-content"><RouterView /></main>
|
||||
</NLayout>
|
||||
</NLayout>
|
||||
</div>
|
||||
<AppDialog
|
||||
v-model:open="settingsOpen"
|
||||
title="后端连接"
|
||||
@@ -175,3 +234,105 @@ const menuOptions = computed<MenuOption[]>(() => [
|
||||
</AppDialog>
|
||||
</NConfigProvider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 本组件专属布局与 Naive 内部结构覆盖。 */
|
||||
@reference "styles/styles.css";
|
||||
.skip-link {
|
||||
@apply fixed z-[100] -top-[80px] left-[15px] py-2.5 px-[15px] bg-(--app-surface) border-[1px_solid_var(--color-accent)];
|
||||
}
|
||||
.skip-link:focus {
|
||||
@apply top-2.5 z-[3000];
|
||||
}
|
||||
.admin-layout {
|
||||
--admin-sider-collapsed: 64px;
|
||||
--admin-sider-expanded: 208px;
|
||||
@apply flex h-dvh overflow-hidden;
|
||||
}
|
||||
.admin-main {
|
||||
@apply min-w-0 h-full flex-1;
|
||||
}
|
||||
.admin-main > .n-layout-scroll-container {
|
||||
@apply h-full flex flex-col overflow-hidden;
|
||||
}
|
||||
.admin-sider {
|
||||
@apply flex flex-col h-full w-[208px] shrink-0 overflow-hidden bg-(--app-subtle);
|
||||
}
|
||||
.admin-sider.is-collapsed {
|
||||
@apply w-16;
|
||||
}
|
||||
.admin-nav-scroll.n-scrollbar {
|
||||
@apply flex-1 min-h-0;
|
||||
}
|
||||
.admin-sider-footer {
|
||||
@apply shrink-0;
|
||||
padding-block: 8px max(8px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.n-button.admin-settings-button {
|
||||
@apply justify-start h-11 px-5;
|
||||
}
|
||||
.admin-sider .n-menu .n-menu-item-content::before {
|
||||
@apply left-0 right-0 rounded-none;
|
||||
}
|
||||
.admin-brand {
|
||||
@apply flex shrink-0 items-center gap-3 h-[74px] py-0 px-5 text-ink whitespace-nowrap;
|
||||
}
|
||||
.admin-brand > svg {
|
||||
@apply shrink-0;
|
||||
}
|
||||
.admin-brand span {
|
||||
@apply text-[15px] font-semibold;
|
||||
}
|
||||
.admin-brand small {
|
||||
@apply block text-[8px] tracking-[1px] font-normal text-muted mt-[3px];
|
||||
}
|
||||
.admin-topbar {
|
||||
@apply relative z-10 flex items-center justify-between shrink-0 h-11 py-0 px-5 gap-3 bg-(--app-surface);
|
||||
}
|
||||
.admin-sider .n-menu-divider {
|
||||
@apply h-3 bg-transparent;
|
||||
}
|
||||
.admin-content {
|
||||
@apply flex-1 min-h-0 min-w-0 overflow-hidden outline-none;
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.admin-sider {
|
||||
overflow: hidden;
|
||||
transition: width 0.32s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
/* 主栏宽度锁在折叠态剩余空间,侧栏变宽时把主栏推到右侧而不是压窄。 */
|
||||
.admin-layout > .admin-main {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - var(--admin-sider-collapsed));
|
||||
min-width: calc(100% - var(--admin-sider-collapsed));
|
||||
max-width: calc(100% - var(--admin-sider-collapsed));
|
||||
}
|
||||
.admin-brand,
|
||||
.admin-nav-scroll.n-scrollbar,
|
||||
.admin-sider-footer {
|
||||
min-width: var(--admin-sider-expanded);
|
||||
}
|
||||
.admin-brand span,
|
||||
.admin-sider .n-menu-item-content-header,
|
||||
.admin-settings-button span {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.admin-sider.is-collapsed .admin-brand span,
|
||||
.admin-sider.is-collapsed .n-menu-item-content-header,
|
||||
.admin-sider.is-collapsed .admin-settings-button span {
|
||||
opacity: 0;
|
||||
}
|
||||
.admin-topbar {
|
||||
@apply py-0 px-2.5;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.admin-sider,
|
||||
.admin-brand span,
|
||||
.admin-sider .n-menu-item-content-header,
|
||||
.admin-settings-button span {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user