feat: 收口组件样式并调整移动端侧栏

This commit is contained in:
GJ
2026-09-04 17:19:34 +08:00
parent d252d5513f
commit e6e0f08072
68 changed files with 2717 additions and 2775 deletions
+184 -23
View File
@@ -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>