fix(ui): replace full-page loading overlay with grid-scoped loader to eliminate flicker

- Add .grid-loading-overlay CSS: position:absolute inside card grid,
  semi-transparent dark background, z-index 100, pointer-events:none
- Add showGridLoading() / hideGridLoading() to VirtualScroller:
  creates/removes the scoped overlay inside the card grid only
- Modify loadMoreWithVirtualScroll(): replace full-page
  state.loadingManager overlay with grid-scoped loading, defer
  hide via requestAnimationFrame to eliminate blank-frame gap
- Clean up gridLoadingOverlay in dispose() to prevent DOM leak
This commit is contained in:
Will Miao
2026-06-24 21:11:13 +08:00
parent ea14d211be
commit 93ad81ed87
3 changed files with 56 additions and 2 deletions

View File

@@ -734,6 +734,21 @@ body.hide-card-version .hl-badge {
}
}
/* Grid-scoped loading overlay (replaces full-page overlay for VirtualScroller refreshes) */
.grid-loading-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--lora-bg-transparent, oklch(0% 0 0 / 0.3));
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
pointer-events: none;
}
/* Add after the existing .model-card:hover styles */
@keyframes update-pulse {

View File

@@ -115,7 +115,10 @@ export class BaseModelApiClient {
const pageState = this.getPageState();
try {
state.loadingManager.showSimpleLoading(`Loading more ${this.apiConfig.config.displayName}s...`);
// Use grid-scoped loading instead of full-page overlay
if (state.virtualScroller?.showGridLoading) {
state.virtualScroller.showGridLoading();
}
pageState.isLoading = true;
if (resetPage) {
@@ -154,7 +157,14 @@ export class BaseModelApiClient {
throw error;
} finally {
pageState.isLoading = false;
state.loadingManager.hide();
// Wait for the next rAF so refreshWithData's scheduleRender has
// completed rendering new cards before hiding the grid loading overlay.
// This eliminates the ~6.7ms blank-frame gap that caused the flicker.
if (state.virtualScroller?.hideGridLoading) {
requestAnimationFrame(() => {
state.virtualScroller.hideGridLoading();
});
}
}
}

View File

@@ -657,6 +657,9 @@ export class VirtualScroller {
this.resizeObserver.disconnect();
}
// Remove any active grid loading overlay
this.hideGridLoading();
// Remove rendered elements
this.clearRenderedItems();
@@ -1130,4 +1133,30 @@ export class VirtualScroller {
index: targetIndex
};
}
/**
* Show a grid-scoped loading indicator (replaces full-page overlay)
* Only covers the card grid area, leaving header/sidebar unaffected.
*/
showGridLoading() {
// Remove any stale overlay from a prior deferred hide (e.g. from final rAF)
this.hideGridLoading();
const overlay = document.createElement('div');
overlay.className = 'grid-loading-overlay';
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
overlay.appendChild(spinner);
this.gridElement.appendChild(overlay);
this.gridLoadingOverlay = overlay;
}
/**
* Hide the grid-scoped loading indicator.
*/
hideGridLoading() {
if (this.gridLoadingOverlay) {
this.gridLoadingOverlay.remove();
this.gridLoadingOverlay = null;
}
}
}