ci/woodpecker/push/woodpecker Pipeline was successful
Refactor asset drilldowns with combined dimension filters, parent summaries and progressive loading. Add the hidden /who read-only permission preview with scoped authorization, identity mapping and audit logging. Refine inventory counts and AI问数 presentation.
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState, type RefObject } from 'react';
|
|
|
|
interface ProgressiveRowsOptions {
|
|
total: number;
|
|
rootRef: RefObject<HTMLElement | null>;
|
|
resetKey: unknown;
|
|
enabled?: boolean;
|
|
batchSize?: number;
|
|
}
|
|
|
|
/** Render a cumulative prefix, loading another batch near the actual scroll body's bottom. */
|
|
export function useProgressiveRows({ total, rootRef, resetKey, enabled = true, batchSize = 40 }: ProgressiveRowsOptions) {
|
|
const context = useMemo(() => ({ resetKey, enabled, batchSize }), [resetKey, enabled, batchSize]);
|
|
const [progress, setProgress] = useState(() => ({ context, limit: batchSize }));
|
|
const [sentinel, setSentinel] = useState<HTMLDivElement | null>(null);
|
|
const visibleCount = Math.min(total, progress.context === context ? progress.limit : batchSize);
|
|
const hasMore = visibleCount < total;
|
|
const loadMore = useCallback(() => {
|
|
if (!enabled) return;
|
|
setProgress(previous => ({ context, limit: Math.min(total, (previous.context === context ? previous.limit : batchSize) + batchSize) }));
|
|
}, [enabled, context, total, batchSize]);
|
|
|
|
useEffect(() => {
|
|
if (enabled && rootRef.current) rootRef.current.scrollTop = 0;
|
|
}, [context, enabled, rootRef]);
|
|
|
|
useEffect(() => {
|
|
const root = rootRef.current;
|
|
if (!enabled || !hasMore || !root || !sentinel || typeof IntersectionObserver === 'undefined') return;
|
|
let active = true;
|
|
let queued = false;
|
|
let frame: number | undefined;
|
|
const observer = new IntersectionObserver(entries => {
|
|
if (!active || queued || !entries.some(entry => entry.isIntersecting)) return;
|
|
queued = true;
|
|
// One append per observer: let layout settle before deciding whether more rows fit.
|
|
frame = requestAnimationFrame(loadMore);
|
|
}, { root, rootMargin: '0px 0px 240px 0px', threshold: 0 });
|
|
observer.observe(sentinel);
|
|
return () => {
|
|
active = false;
|
|
observer.disconnect();
|
|
if (frame !== undefined) cancelAnimationFrame(frame);
|
|
};
|
|
}, [enabled, hasMore, rootRef, sentinel, visibleCount, loadMore]);
|
|
|
|
return { visibleCount, total, hasMore, loadMore, sentinelRef: setSentinel };
|
|
}
|