feat: stage sidebar submenu panel visibility

This commit is contained in:
rbetree
2026-03-28 15:29:33 +08:00
parent 0bd2090d74
commit f270bbd685
3 changed files with 86 additions and 0 deletions

View File

@@ -220,12 +220,27 @@
/* 移除顶部 padding避免标题上方出现缝隙 */ /* 移除顶部 padding避免标题上方出现缝隙 */
scrollbar-width: none; scrollbar-width: none;
/* Firefox 隐藏滚动条 */ /* Firefox 隐藏滚动条 */
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.18s ease,
visibility 0s linear 0.18s;
} }
.sidebar-submenu-panel:empty { .sidebar-submenu-panel:empty {
display: none; 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 { .sidebar-submenu-panel::-webkit-scrollbar {
display: none; display: none;
/* Webkit 浏览器隐藏滚动条 */ /* Webkit 浏览器隐藏滚动条 */

View File

@@ -13,6 +13,10 @@ module.exports = function initUi(state, dom) {
themeIcon, themeIcon,
} = dom; } = dom;
const SUBMENU_PANEL_VISIBLE_CLASS = 'submenu-panel-visible';
const SIDEBAR_LAYOUT_TRANSITION_MS = 300;
let submenuPanelRevealTimer = null;
// 移除预加载类,允许 CSS 过渡效果 // 移除预加载类,允许 CSS 过渡效果
document.documentElement.classList.remove('preload'); document.documentElement.classList.remove('preload');
@@ -100,6 +104,32 @@ module.exports = function initUi(state, dom) {
return window.innerWidth <= 768; 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() { function toggleSidebarCollapse() {
// 仅在交互时启用布局相关动画,避免首屏闪烁 // 仅在交互时启用布局相关动画,避免首屏闪烁
@@ -107,6 +137,11 @@ module.exports = function initUi(state, dom) {
state.isSidebarCollapsed = !state.isSidebarCollapsed; state.isSidebarCollapsed = !state.isSidebarCollapsed;
if (state.isSidebarCollapsed) {
// 收起时立即隐藏目录面板,避免在动画过程中残留。
hideSubmenuPanelImmediately();
}
// 使用 requestAnimationFrame 确保平滑过渡 // 使用 requestAnimationFrame 确保平滑过渡
requestAnimationFrame(() => { requestAnimationFrame(() => {
sidebar.classList.toggle('collapsed', state.isSidebarCollapsed); sidebar.classList.toggle('collapsed', state.isSidebarCollapsed);
@@ -114,6 +149,11 @@ module.exports = function initUi(state, dom) {
// 保存折叠状态到 localStorage // 保存折叠状态到 localStorage
localStorage.setItem('sidebarCollapsed', state.isSidebarCollapsed ? 'true' : 'false'); 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()) { if (savedState === 'true' && !isMobile()) {
state.isSidebarCollapsed = true; state.isSidebarCollapsed = true;
hideSubmenuPanelImmediately();
} else { } else {
state.isSidebarCollapsed = false; state.isSidebarCollapsed = false;
showSubmenuPanelImmediately();
} }
} }
@@ -238,10 +280,16 @@ module.exports = function initUi(state, dom) {
searchContainer.classList.remove('active'); searchContainer.classList.remove('active');
overlay.classList.remove('active'); overlay.classList.remove('active');
state.isSidebarOpen = false; state.isSidebarOpen = false;
if (state.isSidebarCollapsed) {
hideSubmenuPanelImmediately();
} else {
showSubmenuPanelImmediately();
}
} else { } else {
// 在移动设备下,重置侧边栏折叠状态 // 在移动设备下,重置侧边栏折叠状态
sidebar.classList.remove('collapsed'); sidebar.classList.remove('collapsed');
content.classList.remove('expanded'); content.classList.remove('expanded');
showSubmenuPanelImmediately();
} }
}); });

View File

@@ -44,3 +44,26 @@ test('侧边栏样式:收起时不应在页面按钮下方显示目录子菜
'收起态的 submenu 应明确设置为 display: none' '收起态的 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 状态类'
);
});