fix: reveal collapsed desktop sidebar labels together on hover
This commit is contained in:
+106
-71
@@ -8,7 +8,6 @@ import {
|
|||||||
NLayout,
|
NLayout,
|
||||||
NLayoutHeader,
|
NLayoutHeader,
|
||||||
NMenu,
|
NMenu,
|
||||||
NPopover,
|
|
||||||
NScrollbar,
|
NScrollbar,
|
||||||
zhCN,
|
zhCN,
|
||||||
dateZhCN,
|
dateZhCN,
|
||||||
@@ -34,8 +33,8 @@ import { projectAccessKey, type ProjectAccess } from './features/projects/access
|
|||||||
|
|
||||||
/** 窄屏将侧栏改为全屏覆盖层,关闭后不再占用正文宽度。 */
|
/** 窄屏将侧栏改为全屏覆盖层,关闭后不再占用正文宽度。 */
|
||||||
const NARROW_NAV = '(max-width: 800px)'
|
const NARROW_NAV = '(max-width: 800px)'
|
||||||
/** 紧凑侧栏为图标与常驻菜单名称保留空间。 */
|
/** 折叠侧栏只占图标宽度,悬停文字覆盖正文而不挤动布局。 */
|
||||||
const SIDER_COLLAPSED_WIDTH = 144
|
const SIDER_COLLAPSED_WIDTH = 64
|
||||||
/** 桌面端展开侧栏宽度。 */
|
/** 桌面端展开侧栏宽度。 */
|
||||||
const SIDER_EXPANDED_WIDTH = 208
|
const SIDER_EXPANDED_WIDTH = 208
|
||||||
/** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */
|
/** 菜单图标尺寸,折叠/展开共用,避免切换时缩放。 */
|
||||||
@@ -47,12 +46,8 @@ const narrowMedia = window.matchMedia(NARROW_NAV)
|
|||||||
const narrow = ref(narrowMedia.matches)
|
const narrow = ref(narrowMedia.matches)
|
||||||
const collapsed = ref(narrow.value)
|
const collapsed = ref(narrow.value)
|
||||||
const settingsOpen = ref(false)
|
const settingsOpen = ref(false)
|
||||||
/** 桌面紧凑态保留所有菜单名称;移动端打开时仍展示完整菜单。 */
|
/** 始终保留菜单文字节点,用 CSS 同步控制整列文字的进出动画。 */
|
||||||
const menuCollapsed = false
|
const menuCollapsed = false
|
||||||
/** 菜单内部会缓存图标 VNode,切换显示模式时重建一次以同步 Popover 状态。 */
|
|
||||||
const menuRenderKey = computed(
|
|
||||||
() => `${narrow.value ? 'mobile' : 'desktop'}-${collapsed.value ? 'collapsed' : 'expanded'}`
|
|
||||||
)
|
|
||||||
/** Naive Menu 的桌面折叠宽度;移动端覆盖层不参与正文宽度计算。 */
|
/** Naive Menu 的桌面折叠宽度;移动端覆盖层不参与正文宽度计算。 */
|
||||||
const siderCollapsedWidth = SIDER_COLLAPSED_WIDTH
|
const siderCollapsedWidth = SIDER_COLLAPSED_WIDTH
|
||||||
/** 侧栏展开宽度仅用于桌面布局。 */
|
/** 侧栏展开宽度仅用于桌面布局。 */
|
||||||
@@ -116,41 +111,18 @@ const workflowItems = [
|
|||||||
['production', '镜头生产', Clapperboard]
|
['production', '镜头生产', Clapperboard]
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
/** PC 折叠侧栏只显示图标时渲染 Popover;可用图标同时保留路由跳转能力。 */
|
/** 图标始终留在原位,名称由菜单本身显示,不再创建逐项浮层。 */
|
||||||
function createMenuIcon(icon: typeof FolderOpen, label: string, popoverEnabled: boolean, target?: string) {
|
function createMenuIcon(icon: typeof FolderOpen) {
|
||||||
return () => {
|
return () => h('span', { class: 'admin-menu-icon' }, [h(icon, { size: NAV_ICON_SIZE })])
|
||||||
const trigger = () =>
|
|
||||||
popoverEnabled && target
|
|
||||||
? h(RouterLink, { to: target, class: 'admin-menu-popover-trigger' }, () => h(icon, { size: 18 }))
|
|
||||||
: h('span', { class: 'admin-menu-popover-trigger' }, [h(icon, { size: 18 })])
|
|
||||||
if (!popoverEnabled) return trigger()
|
|
||||||
|
|
||||||
return h(
|
|
||||||
NPopover,
|
|
||||||
{
|
|
||||||
trigger: 'hover',
|
|
||||||
placement: 'right',
|
|
||||||
to: 'body',
|
|
||||||
class: 'admin-nav-popover',
|
|
||||||
zIndex: 3000
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trigger,
|
|
||||||
default: () => label
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 菜单链接使用 RouterLink,保留新标签打开和浏览器导航行为。 */
|
/** 菜单链接使用 RouterLink,保留新标签打开和浏览器导航行为。 */
|
||||||
const menuOptions = computed<MenuOption[]>(() => {
|
const menuOptions = computed<MenuOption[]>(() => {
|
||||||
// 菜单名称在紧凑态也常驻显示,不再重复弹出逐项提示。
|
|
||||||
const popoverEnabled = false
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: '/projects',
|
key: '/projects',
|
||||||
label: () => h(RouterLink, { to: '/projects' }, () => '我的剧本'),
|
label: () => h(RouterLink, { to: '/projects' }, () => h('span', { class: 'admin-menu-label' }, '我的剧本')),
|
||||||
icon: createMenuIcon(FolderOpen, '我的剧本', popoverEnabled, '/projects')
|
icon: createMenuIcon(FolderOpen)
|
||||||
},
|
},
|
||||||
...(projectId.value
|
...(projectId.value
|
||||||
? [
|
? [
|
||||||
@@ -163,14 +135,15 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
disabled,
|
disabled,
|
||||||
label: () =>
|
label: () =>
|
||||||
disabled
|
disabled
|
||||||
? h('span', { title: '剧本完成后可用', 'aria-disabled': 'true' }, label)
|
? h(
|
||||||
: h(RouterLink, { to: target }, () => label),
|
'span',
|
||||||
icon: createMenuIcon(
|
{ class: 'admin-menu-label', title: '剧本完成后可用', 'aria-disabled': 'true' },
|
||||||
icon,
|
label
|
||||||
disabled ? `${label}(剧本完成后可用)` : label,
|
|
||||||
popoverEnabled,
|
|
||||||
disabled ? undefined : target
|
|
||||||
)
|
)
|
||||||
|
: h(RouterLink, { to: target }, () =>
|
||||||
|
h('span', { class: 'admin-menu-label' }, label)
|
||||||
|
),
|
||||||
|
icon: createMenuIcon(icon)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
@@ -232,7 +205,6 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
</header>
|
</header>
|
||||||
<NScrollbar class="admin-nav-scroll">
|
<NScrollbar class="admin-nav-scroll">
|
||||||
<NMenu
|
<NMenu
|
||||||
:key="menuRenderKey"
|
|
||||||
:value="route.path"
|
:value="route.path"
|
||||||
:options="menuOptions"
|
:options="menuOptions"
|
||||||
:collapsed="menuCollapsed"
|
:collapsed="menuCollapsed"
|
||||||
@@ -243,17 +215,8 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
:root-indent="navIconIndent"
|
:root-indent="navIconIndent"
|
||||||
/>
|
/>
|
||||||
</NScrollbar>
|
</NScrollbar>
|
||||||
<!-- 以下是侧栏底部后端连接:折叠裁切文字,图标位置保持不变 -->
|
<!-- 底部入口与菜单名称一起显示,图标始终可点击。 -->
|
||||||
<footer class="admin-sider-footer">
|
<footer class="admin-sider-footer">
|
||||||
<NPopover
|
|
||||||
trigger="hover"
|
|
||||||
placement="right"
|
|
||||||
to="body"
|
|
||||||
class="admin-nav-popover"
|
|
||||||
:z-index="3000"
|
|
||||||
:disabled="true"
|
|
||||||
>
|
|
||||||
<template #trigger>
|
|
||||||
<NButton
|
<NButton
|
||||||
quaternary
|
quaternary
|
||||||
block
|
block
|
||||||
@@ -265,9 +228,6 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
<template #icon><Settings2 :size="18" /></template>
|
<template #icon><Settings2 :size="18" /></template>
|
||||||
<span class="admin-settings-label">后端连接</span>
|
<span class="admin-settings-label">后端连接</span>
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
|
||||||
后端连接
|
|
||||||
</NPopover>
|
|
||||||
</footer>
|
</footer>
|
||||||
</aside>
|
</aside>
|
||||||
<!-- 以下是顶栏与主内容 -->
|
<!-- 以下是顶栏与主内容 -->
|
||||||
@@ -319,7 +279,7 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
@apply top-2.5 z-[3000];
|
@apply top-2.5 z-[3000];
|
||||||
}
|
}
|
||||||
.admin-layout {
|
.admin-layout {
|
||||||
--admin-sider-collapsed: 144px;
|
--admin-sider-collapsed: 64px;
|
||||||
--admin-sider-expanded: 208px;
|
--admin-sider-expanded: 208px;
|
||||||
@apply flex h-dvh overflow-hidden;
|
@apply flex h-dvh overflow-hidden;
|
||||||
}
|
}
|
||||||
@@ -340,7 +300,7 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
@apply hidden shrink-0;
|
@apply hidden shrink-0;
|
||||||
}
|
}
|
||||||
.admin-sider.is-collapsed {
|
.admin-sider.is-collapsed {
|
||||||
@apply w-16;
|
width: var(--admin-sider-collapsed);
|
||||||
}
|
}
|
||||||
.admin-nav-scroll.n-scrollbar {
|
.admin-nav-scroll.n-scrollbar {
|
||||||
@apply flex-1 min-h-0;
|
@apply flex-1 min-h-0;
|
||||||
@@ -348,14 +308,16 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
}
|
}
|
||||||
.admin-sider .n-menu-item-content-header {
|
.admin-sider .n-menu-item-content-header {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s ease;
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.admin-sider.is-collapsed .n-menu-item-content-header {
|
.admin-sider.is-collapsed .n-menu-item-content-header {
|
||||||
opacity: 1;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
/* 折叠态将图标抬到透明路由链接上方,使点击与悬停都命中图标触发区。 */
|
/* 点击图标时命中菜单链接覆盖层,隐藏名称也不会阻断跳转。 */
|
||||||
.admin-sider.is-collapsed .n-menu-item-content__icon {
|
.admin-sider.is-collapsed .n-menu-item-content__icon {
|
||||||
@apply relative z-[2];
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.admin-sider-footer {
|
.admin-sider-footer {
|
||||||
@apply w-full shrink-0 overflow-hidden;
|
@apply w-full shrink-0 overflow-hidden;
|
||||||
@@ -370,20 +332,22 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
.admin-settings-label {
|
.admin-settings-label {
|
||||||
@apply whitespace-nowrap;
|
@apply whitespace-nowrap;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s ease;
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.admin-sider.is-collapsed .admin-settings-label {
|
.admin-sider.is-collapsed .admin-settings-label {
|
||||||
opacity: 1;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.admin-sider .n-menu .n-menu-item-content::before {
|
.admin-sider .n-menu .n-menu-item-content::before {
|
||||||
@apply left-0 right-0 rounded-none;
|
@apply left-0 right-0 rounded-none;
|
||||||
}
|
}
|
||||||
.admin-menu-popover-trigger {
|
.admin-menu-icon {
|
||||||
@apply relative z-[1] inline-flex items-center justify-center text-inherit;
|
@apply inline-flex items-center justify-center text-inherit;
|
||||||
}
|
}
|
||||||
/* 折叠侧栏提示完整落在 64px 边界之外,并覆盖业务内容层。 */
|
.admin-menu-label {
|
||||||
.n-popover.admin-nav-popover {
|
display: inline-block;
|
||||||
margin-inline-start: 24px !important;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.admin-brand {
|
.admin-brand {
|
||||||
@apply flex shrink-0 items-center gap-3 h-[74px] overflow-hidden py-0 px-5 text-ink whitespace-nowrap;
|
@apply flex shrink-0 items-center gap-3 h-[74px] overflow-hidden py-0 px-5 text-ink whitespace-nowrap;
|
||||||
@@ -395,7 +359,9 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
.admin-brand span {
|
.admin-brand span {
|
||||||
@apply text-[15px] font-semibold;
|
@apply text-[15px] font-semibold;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 0.2s ease;
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
}
|
}
|
||||||
.admin-sider.is-collapsed .admin-brand span {
|
.admin-sider.is-collapsed .admin-brand span {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -412,6 +378,70 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
.admin-content {
|
.admin-content {
|
||||||
@apply flex-1 min-h-0 min-w-0 overflow-hidden outline-none;
|
@apply flex-1 min-h-0 min-w-0 overflow-hidden outline-none;
|
||||||
}
|
}
|
||||||
|
/* 桌面折叠态:64px 图标轨道固定,整列名称在右侧浮现。 */
|
||||||
|
@media (min-width: 801px) {
|
||||||
|
.admin-sider.is-collapsed {
|
||||||
|
--admin-nav-reveal-width: 160px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 30;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
/* 浮层使用当前主题底色,保证文字不会与正文混在一起。 */
|
||||||
|
.admin-sider.is-collapsed::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 auto 0 var(--admin-sider-collapsed);
|
||||||
|
width: calc(var(--admin-nav-reveal-width) - var(--admin-sider-collapsed));
|
||||||
|
background: var(--app-surface);
|
||||||
|
border-right: 1px solid var(--app-border);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .admin-nav-scroll.n-scrollbar,
|
||||||
|
.admin-sider.is-collapsed .admin-sider-footer {
|
||||||
|
position: relative;
|
||||||
|
width: var(--admin-nav-reveal-width);
|
||||||
|
min-width: var(--admin-nav-reveal-width);
|
||||||
|
/* 隐藏的名称不能阻挡正文点击,淡出结束后再收回命中区域。 */
|
||||||
|
clip-path: inset(0 calc(var(--admin-nav-reveal-width) - var(--admin-sider-collapsed)) 0 0);
|
||||||
|
transition: clip-path 0s 0.2s;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible))::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .admin-nav-scroll.n-scrollbar,
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .admin-sider-footer {
|
||||||
|
clip-path: inset(0);
|
||||||
|
transition-delay: 0s;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .n-menu-item-content__icon {
|
||||||
|
margin-right: 34px !important;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .admin-settings-label {
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .admin-menu-label,
|
||||||
|
.admin-sider.is-collapsed .admin-settings-label {
|
||||||
|
transform: translateX(-6px);
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .n-menu-item-content-header,
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .admin-settings-label {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .admin-menu-label,
|
||||||
|
.admin-sider.is-collapsed:is(:hover, :has(:focus-visible)) .admin-settings-label {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
/* 选中、悬停背景仅落在图标轨道内。 */
|
||||||
|
.admin-sider.is-collapsed .n-menu .n-menu-item-content::before {
|
||||||
|
right: calc(var(--admin-nav-reveal-width) - var(--admin-sider-collapsed));
|
||||||
|
}
|
||||||
|
.admin-sider.is-collapsed .admin-settings-button .n-button__state-border,
|
||||||
|
.admin-sider.is-collapsed .admin-settings-button .n-button__border {
|
||||||
|
right: calc(var(--admin-nav-reveal-width) - var(--admin-sider-collapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.admin-sider {
|
.admin-sider {
|
||||||
@apply fixed inset-0 z-50 overflow-hidden;
|
@apply fixed inset-0 z-50 overflow-hidden;
|
||||||
@@ -457,7 +487,12 @@ const menuOptions = computed<MenuOption[]>(() => {
|
|||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.admin-sider,
|
.admin-sider,
|
||||||
|
.admin-sider.is-collapsed,
|
||||||
|
.admin-sider.is-collapsed::before,
|
||||||
|
.admin-sider.is-collapsed .admin-nav-scroll.n-scrollbar,
|
||||||
|
.admin-sider.is-collapsed .admin-sider-footer,
|
||||||
.admin-sider .n-menu-item-content-header,
|
.admin-sider .n-menu-item-content-header,
|
||||||
|
.admin-menu-label,
|
||||||
.admin-brand span,
|
.admin-brand span,
|
||||||
.admin-settings-label {
|
.admin-settings-label {
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user