️ perf(sidebar): 优化大型资源树折叠动画

- 延迟隐藏资源树内容,使侧栏宽度收起动画保持连续
- 稳定 Sidebar 属性并保留已加载节点,避免大量对象触发重复渲染
- 覆盖 V2、旧版与减少动态效果场景,补充折叠回归测试
- 关联问题:#697
This commit is contained in:
Syngnat
2026-07-24 15:16:15 +08:00
parent 1e4ece74a7
commit d520c9b806
5 changed files with 65 additions and 22 deletions

View File

@@ -1005,6 +1005,10 @@ body[data-ui-version="legacy"] .ant-layout-sider {
max-width: min(960px, calc(100vw - 360px)) !important;
}
body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'] {
--gonavi-sidebar-collapse-duration: 200ms;
}
body[data-ui-version] .ant-layout-sider[data-sidebar-collapsed='true'] {
min-width: var(--gonavi-sidebar-collapsed-width, 0px) !important;
max-width: var(--gonavi-sidebar-collapsed-width, 0px) !important;
@@ -1013,6 +1017,29 @@ body[data-ui-version] .ant-layout-sider[data-sidebar-collapsed='true'] {
border-right: 0 !important;
}
body[data-ui-version='legacy'] .ant-layout-sider[data-sidebar-collapsed='true'] [data-sidebar-content='true'],
body[data-ui-version='v2'] .ant-layout-sider[data-sidebar-collapsed='true'] [data-sidebar-tree-panel='true'] {
visibility: hidden;
pointer-events: none;
transition: visibility 0s linear var(--gonavi-sidebar-collapse-duration);
}
body[data-ui-version='legacy'] .ant-layout-sider[data-sidebar-collapsed='false'] [data-sidebar-content='true'],
body[data-ui-version='v2'] .ant-layout-sider[data-sidebar-collapsed='false'] [data-sidebar-tree-panel='true'] {
visibility: visible;
transition: none;
}
body[data-ui-version='v2'] .ant-layout-sider[data-sidebar-collapsed='false'] .gn-v2-rail-sidebar-toggle-slot {
display: none;
}
@media (prefers-reduced-motion: reduce) {
body[data-ui-version] .ant-layout-sider[data-sidebar-panel='true'] {
--gonavi-sidebar-collapse-duration: 0ms;
}
}
body[data-ui-version] .gonavi-sidebar-collapse-trigger.ant-btn {
display: inline-flex;
width: 26px;

View File

@@ -19,14 +19,31 @@ describe('app sidebar tree panel collapse', () => {
expect(appSource).not.toContain('setSidebarWidth(0)');
});
it('keeps loaded tree nodes mounted and Sidebar props stable during the width transition', () => {
expect(appSource).toContain("const sidebarPanelCollapseLabel = t('app.sidebar.collapse');");
expect(appSource).toContain("const sidebarPanelExpandLabel = t('app.sidebar.expand');");
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
expect(sidebarSource).not.toContain('isTreePanelCollapsed?: boolean;');
expect(sidebarSource).not.toContain("display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex'");
expect(appSource).toContain('data-sidebar-content="true"');
expect(appCssSource).toContain('--gonavi-sidebar-collapse-duration: 200ms;');
expect(appCssSource).toMatch(
/\.ant-layout-sider\[data-sidebar-collapsed='true'\]\s+\[data-sidebar-tree-panel='true'\][^{]*\{[^}]*visibility:\s*hidden;[^}]*transition:\s*visibility 0s linear var\(--gonavi-sidebar-collapse-duration\);/s,
);
expect(appCssSource).toMatch(
/\.ant-layout-sider\[data-sidebar-collapsed='false'\]\s+\.gn-v2-rail-sidebar-toggle-slot\s*\{[^}]*display:\s*none;/s,
);
});
it('keeps the fixed rail visible and hides only the v2 explorer tree panel', () => {
expect(sidebarSource).toContain("id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}");
expect(sidebarSource).toContain("data-sidebar-tree-panel={isV2Ui ? 'true' : undefined}");
expect(sidebarSource).toContain('aria-hidden={isV2Ui ? isTreePanelCollapsed : undefined}');
expect(sidebarSource).toContain("display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex'");
expect(sidebarSource).toContain("style={{ display: 'flex', flexDirection: 'column'");
expect(connectionRailSource).toContain('data-sidebar-fixed-rail="true"');
expect(appSource).toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
expect(appSource).toContain("visibility: !isV2Ui && isSidebarCollapsed ? 'hidden' : 'visible'");
expect(appSource).not.toContain('isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}');
expect(appSource).not.toContain("visibility: !isV2Ui && isSidebarCollapsed ? 'hidden' : 'visible'");
expect(appSource).toContain('data-sidebar-collapse-trigger="true"');
expect(appSource).toContain('data-titlebar-brand-region="true"');
expect(appSource).toContain('data-sidebar-toggle-placement="titlebar"');
@@ -42,8 +59,8 @@ describe('app sidebar tree panel collapse', () => {
expect(appSource).toContain('handleFocusSidebarSearch();');
expect(appSource).toContain('<TabManager onFocusSidebarSearch={handleFocusSidebarSearch} />');
expect(appSource).toContain('{!isV2Ui && (');
expect(appSource).toContain('onCollapseSidebar={isV2Ui && !isSidebarCollapsed ? handleCollapseSidebarPanel : undefined}');
expect(appSource).toContain('onExpandSidebar={isV2Ui && isSidebarCollapsed ? handleExpandSidebarPanel : undefined}');
expect(appSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(appSource).toContain('onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}');
expect(appSource).toContain('collapseSidebarButtonRef={sidebarExplorerToggleRef}');
expect(appSource).toContain('expandSidebarButtonRef={sidebarCollapsedToggleRef}');
expect(appSource).toContain('ref={sidebarCollapsedToggleRef}');
@@ -76,7 +93,7 @@ describe('app sidebar tree panel collapse', () => {
expect(triggerSource).toContain('type="text"');
expect(triggerSource).toContain("WebkitAppRegion: 'no-drag'");
expect(triggerSource).toContain("'--wails-draggable': 'no-drag'");
expect(siderSource).toContain('onCollapseSidebar={isV2Ui && !isSidebarCollapsed ? handleCollapseSidebarPanel : undefined}');
expect(siderSource).toContain('onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}');
expect(fixedRailIndex).toBeGreaterThan(-1);
expect(explorerPanelIndex).toBeGreaterThan(fixedRailIndex);
expect(explorerToggleIndex).toBeGreaterThan(connectionMenuIndex);
@@ -100,7 +117,9 @@ describe('app sidebar tree panel collapse', () => {
expect(appCssSource).toMatch(
/body\[data-ui-version='v2'\]\s+\.gonavi-sidebar-collapse-trigger\.ant-btn\[data-sidebar-toggle-placement='explorer-header'\]\s*\{[^}]*width:\s*24px;[^}]*height:\s*24px(?:\s*!important)?;[^}]*border:\s*1px solid var\(--gn-br-2\)\s*!important;[^}]*border-radius:\s*7px\s*!important;/s,
);
expect(appCssSource).not.toContain(".ant-layout-sider[data-sidebar-panel='true']");
expect(appCssSource).not.toMatch(
/\.ant-layout-sider\[data-sidebar-panel='true'\]\s*\{[^}]*(?:min-width|max-width|\bwidth\s*:|\bflex\s*:)/s,
);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*position:\s*absolute/s);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*right:\s*-\d+px/s);
expect(appCssSource).not.toMatch(/\.gonavi-sidebar-collapse-trigger[^}]*translateY/s);

View File

@@ -7454,7 +7454,9 @@ function App() {
return null;
};
const sidebarPanelToggleLabel = t(isSidebarCollapsed ? 'app.sidebar.expand' : 'app.sidebar.collapse');
const sidebarPanelCollapseLabel = t('app.sidebar.collapse');
const sidebarPanelExpandLabel = t('app.sidebar.expand');
const sidebarPanelToggleLabel = isSidebarCollapsed ? sidebarPanelExpandLabel : sidebarPanelCollapseLabel;
return (
<ConfigProvider
@@ -7622,13 +7624,13 @@ function App() {
>
<div
id={isV2Ui ? undefined : 'gonavi-sidebar-tree-panel'}
data-sidebar-content="true"
aria-hidden={!isV2Ui ? isSidebarCollapsed : undefined}
style={{
height: '100%',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
visibility: !isV2Ui && isSidebarCollapsed ? 'hidden' : 'visible',
}}
>
{!isV2Ui && (
@@ -7665,13 +7667,12 @@ function App() {
onToggleLogPanel={handleToggleLogPanel}
uiVersion={appearance.uiVersion}
onFocusCommandSearch={handleFocusSidebarSearch}
onCollapseSidebar={isV2Ui && !isSidebarCollapsed ? handleCollapseSidebarPanel : undefined}
onExpandSidebar={isV2Ui && isSidebarCollapsed ? handleExpandSidebarPanel : undefined}
collapseSidebarLabel={sidebarPanelToggleLabel}
onCollapseSidebar={isV2Ui ? handleCollapseSidebarPanel : undefined}
onExpandSidebar={isV2Ui ? handleExpandSidebarPanel : undefined}
collapseSidebarLabel={isV2Ui ? sidebarPanelCollapseLabel : undefined}
collapseSidebarButtonRef={sidebarExplorerToggleRef}
expandSidebarLabel={sidebarPanelToggleLabel}
expandSidebarLabel={isV2Ui ? sidebarPanelExpandLabel : undefined}
expandSidebarButtonRef={sidebarCollapsedToggleRef}
isTreePanelCollapsed={isV2Ui && isSidebarCollapsed}
/>
</div>
{!connectionWorkbenchState.ready && (

View File

@@ -1000,7 +1000,6 @@ describe('Sidebar locate toolbar', () => {
it('renders the fixed v2 rail, explorer filters and workbench actions', () => {
const markup = renderSidebarMarkup({ uiVersion: 'v2', onCreateConnection: mocks.noop });
const collapsedMarkup = renderSidebarMarkup({ uiVersion: 'v2', isTreePanelCollapsed: true });
const source = readSidebarSource();
expect(markup).toContain('gn-v2-sidebar-redesign');
@@ -1008,8 +1007,8 @@ describe('Sidebar locate toolbar', () => {
expect(markup).toContain('data-sidebar-fixed-rail="true"');
expect(markup).toContain('gn-v2-object-explorer');
expect(markup.indexOf('data-sidebar-fixed-rail="true"')).toBeLessThan(markup.indexOf('data-sidebar-tree-panel="true"'));
expect(collapsedMarkup).toContain('data-sidebar-fixed-rail="true"');
expect(collapsedMarkup).toContain('data-sidebar-tree-panel="true" aria-hidden="true" style="display:none');
expect(markup).toContain('data-sidebar-tree-panel="true" style="display:flex');
expect(markup).not.toContain('data-sidebar-tree-panel="true" aria-hidden="true" style="display:none');
expect(markup).toContain('gn-v2-active-connection-header');
expect(markup).toContain('gn-v2-explorer-search');
expect(markup).toContain('data-v2-sidebar-search-mode="command"');

View File

@@ -516,7 +516,6 @@ const Sidebar: React.FC<{
collapseSidebarButtonRef?: React.Ref<HTMLButtonElement>;
expandSidebarLabel?: string;
expandSidebarButtonRef?: React.Ref<HTMLButtonElement>;
isTreePanelCollapsed?: boolean;
}> = React.memo(({
onCreateConnection,
onEditConnection,
@@ -531,7 +530,6 @@ const Sidebar: React.FC<{
collapseSidebarButtonRef,
expandSidebarLabel,
expandSidebarButtonRef,
isTreePanelCollapsed = false,
}) => {
const connections = useStore(state => state.connections);
const savedQueries = useStore(state => state.savedQueries);
@@ -3044,8 +3042,7 @@ const Sidebar: React.FC<{
id={isV2Ui ? 'gonavi-sidebar-tree-panel' : undefined}
className={isV2Ui ? 'gn-v2-object-explorer' : undefined}
data-sidebar-tree-panel={isV2Ui ? 'true' : undefined}
aria-hidden={isV2Ui ? isTreePanelCollapsed : undefined}
style={{ display: isV2Ui && isTreePanelCollapsed ? 'none' : 'flex', flexDirection: 'column', height: '100%', minWidth: 0, flex: 1 }}
style={{ display: 'flex', flexDirection: 'column', height: '100%', minWidth: 0, flex: 1 }}
>
{isV2Ui && (
<div className="gn-v2-active-connection-header" data-object-count={activeConnectionObjectCount}>