diff --git a/assets/styles/_sidebar.css b/assets/styles/_sidebar.css index 48286d9..f3710de 100644 --- a/assets/styles/_sidebar.css +++ b/assets/styles/_sidebar.css @@ -220,12 +220,27 @@ /* 移除顶部 padding,避免标题上方出现缝隙 */ scrollbar-width: none; /* Firefox 隐藏滚动条 */ + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: + opacity 0.18s ease, + visibility 0s linear 0.18s; } .sidebar-submenu-panel:empty { display: none; } +.sidebar.submenu-panel-visible .sidebar-submenu-panel:not(:empty) { + opacity: 1; + visibility: visible; + pointer-events: auto; + transition: + opacity 0.18s ease, + visibility 0s linear 0s; +} + .sidebar-submenu-panel::-webkit-scrollbar { display: none; /* Webkit 浏览器隐藏滚动条 */ diff --git a/src/runtime/app/ui.js b/src/runtime/app/ui.js index ffa4dfb..8203ed3 100644 --- a/src/runtime/app/ui.js +++ b/src/runtime/app/ui.js @@ -13,6 +13,10 @@ module.exports = function initUi(state, dom) { themeIcon, } = dom; + const SUBMENU_PANEL_VISIBLE_CLASS = 'submenu-panel-visible'; + const SIDEBAR_LAYOUT_TRANSITION_MS = 300; + let submenuPanelRevealTimer = null; + // 移除预加载类,允许 CSS 过渡效果 document.documentElement.classList.remove('preload'); @@ -100,6 +104,32 @@ module.exports = function initUi(state, dom) { return window.innerWidth <= 768; } + function clearSubmenuPanelRevealTimer() { + if (submenuPanelRevealTimer) { + window.clearTimeout(submenuPanelRevealTimer); + submenuPanelRevealTimer = null; + } + } + + function hideSubmenuPanelImmediately() { + clearSubmenuPanelRevealTimer(); + sidebar.classList.remove(SUBMENU_PANEL_VISIBLE_CLASS); + } + + function showSubmenuPanelImmediately() { + clearSubmenuPanelRevealTimer(); + sidebar.classList.add(SUBMENU_PANEL_VISIBLE_CLASS); + } + + function revealSubmenuPanelAfterSidebarTransition() { + clearSubmenuPanelRevealTimer(); + submenuPanelRevealTimer = window.setTimeout(() => { + if (!state.isSidebarCollapsed && !sidebar.classList.contains('collapsed')) { + sidebar.classList.add(SUBMENU_PANEL_VISIBLE_CLASS); + } + }, SIDEBAR_LAYOUT_TRANSITION_MS); + } + // 侧边栏折叠功能 function toggleSidebarCollapse() { // 仅在交互时启用布局相关动画,避免首屏闪烁 @@ -107,6 +137,11 @@ module.exports = function initUi(state, dom) { state.isSidebarCollapsed = !state.isSidebarCollapsed; + if (state.isSidebarCollapsed) { + // 收起时立即隐藏目录面板,避免在动画过程中残留。 + hideSubmenuPanelImmediately(); + } + // 使用 requestAnimationFrame 确保平滑过渡 requestAnimationFrame(() => { sidebar.classList.toggle('collapsed', state.isSidebarCollapsed); @@ -114,6 +149,11 @@ module.exports = function initUi(state, dom) { // 保存折叠状态到 localStorage localStorage.setItem('sidebarCollapsed', state.isSidebarCollapsed ? 'true' : 'false'); + + if (!state.isSidebarCollapsed) { + // 展开后再淡入目录面板,避免和宽度动画抢节奏。 + revealSubmenuPanelAfterSidebarTransition(); + } }); } @@ -125,8 +165,10 @@ module.exports = function initUi(state, dom) { // 图标状态与折叠状态保持一致 if (savedState === 'true' && !isMobile()) { state.isSidebarCollapsed = true; + hideSubmenuPanelImmediately(); } else { state.isSidebarCollapsed = false; + showSubmenuPanelImmediately(); } } @@ -238,10 +280,16 @@ module.exports = function initUi(state, dom) { searchContainer.classList.remove('active'); overlay.classList.remove('active'); state.isSidebarOpen = false; + if (state.isSidebarCollapsed) { + hideSubmenuPanelImmediately(); + } else { + showSubmenuPanelImmediately(); + } } else { // 在移动设备下,重置侧边栏折叠状态 sidebar.classList.remove('collapsed'); content.classList.remove('expanded'); + showSubmenuPanelImmediately(); } }); diff --git a/test/sidebar-submenu-panel-and-theme-mode.node-test.js b/test/sidebar-submenu-panel-and-theme-mode.node-test.js index da032b2..a8a5c1b 100644 --- a/test/sidebar-submenu-panel-and-theme-mode.node-test.js +++ b/test/sidebar-submenu-panel-and-theme-mode.node-test.js @@ -44,3 +44,26 @@ test('侧边栏样式:收起时不应在页面按钮下方显示目录子菜 '收起态的 submenu 应明确设置为 display: none' ); }); + +test('侧边栏目录面板:应默认隐藏,并通过独立状态类延后淡入', () => { + const repoRoot = path.resolve(__dirname, '..'); + const sidebarStylePath = path.join(repoRoot, 'assets', 'styles', '_sidebar.css'); + const uiPath = path.join(repoRoot, 'src', 'runtime', 'app', 'ui.js'); + const styleContent = fs.readFileSync(sidebarStylePath, 'utf8'); + const uiContent = fs.readFileSync(uiPath, 'utf8'); + + assert.match( + styleContent, + /\.sidebar-submenu-panel\s*\{[\s\S]*opacity:\s*0;[\s\S]*visibility:\s*hidden;/m, + '目录面板默认应处于隐藏状态' + ); + assert.match( + styleContent, + /\.sidebar\.submenu-panel-visible \.sidebar-submenu-panel:not\(:empty\)\s*\{[\s\S]*opacity:\s*1;[\s\S]*visibility:\s*visible;/m, + '目录面板应通过 submenu-panel-visible 状态类显现' + ); + assert.ok( + uiContent.includes('submenu-panel-visible'), + 'src/runtime/app/ui.js 应控制 submenu-panel-visible 状态类' + ); +});