perf: make mileage pagination explicit

This commit is contained in:
kkfluous
2026-08-07 15:37:07 +08:00
parent c2d42efa6d
commit 616341adc8
+47 -30
View File
@@ -492,6 +492,7 @@ export default function MonitoringView() {
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [pageLoading, setPageLoading] = useState(true); const [pageLoading, setPageLoading] = useState(true);
const [pageError, setPageError] = useState<{ message: string; scope: 'first' | 'more' } | null>(null);
const [manualRefreshing, setManualRefreshing] = useState(false); const [manualRefreshing, setManualRefreshing] = useState(false);
const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null); const [lastRefreshedAt, setLastRefreshedAt] = useState<Date | null>(null);
const [showBackToTop, setShowBackToTop] = useState(false); const [showBackToTop, setShowBackToTop] = useState(false);
@@ -566,6 +567,7 @@ export default function MonitoringView() {
// 加载首页数据 // 加载首页数据
const loadFirstPage = useCallback((showPageLoading = true) => { const loadFirstPage = useCallback((showPageLoading = true) => {
if (showPageLoading) setPageLoading(true); if (showPageLoading) setPageLoading(true);
setPageError(null);
return fetchMonitoring({ return fetchMonitoring({
sortBy, sortBy,
sortOrder, sortOrder,
@@ -597,7 +599,12 @@ export default function MonitoringView() {
setPage(1); setPage(1);
setHasMore(d.page < d.totalPages); setHasMore(d.page < d.totalPages);
setLastRefreshedAt(new Date()); setLastRefreshedAt(new Date());
}).catch(() => {}).finally(() => { }).catch(error => {
setPageError({
message: error instanceof Error ? error.message : '里程数据加载失败,请稍后重试',
scope: 'first',
});
}).finally(() => {
if (showPageLoading) setPageLoading(false); if (showPageLoading) setPageLoading(false);
}); });
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, filterBrands, sourcePriority]);
@@ -617,6 +624,7 @@ export default function MonitoringView() {
if (loadingMore || !hasMore) return; if (loadingMore || !hasMore) return;
const nextPage = page + 1; const nextPage = page + 1;
setLoadingMore(true); setLoadingMore(true);
setPageError(null);
fetchMonitoring({ fetchMonitoring({
sortBy, sortBy,
sortOrder, sortOrder,
@@ -642,7 +650,12 @@ export default function MonitoringView() {
setVehicles(prev => [...prev, ...d.vehicles]); setVehicles(prev => [...prev, ...d.vehicles]);
setPage(nextPage); setPage(nextPage);
setHasMore(nextPage < d.totalPages); setHasMore(nextPage < d.totalPages);
}).catch(() => {}).finally(() => setLoadingMore(false)); }).catch(error => {
setPageError({
message: error instanceof Error ? error.message : '更多车辆加载失败,请稍后重试',
scope: 'more',
});
}).finally(() => setLoadingMore(false));
}, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, page, loadingMore, hasMore, filterBrands, sourcePriority]); }, [sortBy, sortOrder, searchTerm, filterDept, filterCustomer, filterProject, filterEntity, filterRentStatus, filterPlatePrefix, filterTargetNames, filterRegion, filterPlates, appliedMileageRange, rangeStart, rangeEnd, page, loadingMore, hasMore, filterBrands, sourcePriority]);
// 筛选/排序变化时重新加载 // 筛选/排序变化时重新加载
@@ -699,26 +712,6 @@ export default function MonitoringView() {
return () => clearInterval(timer); return () => clearInterval(timer);
}, [loadFirstPage]); }, [loadFirstPage]);
// 触底检测:用 IntersectionObserver 监听哨兵元素
const loadMoreRef = useRef(loadMore);
loadMoreRef.current = loadMore;
const sentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const sentinel = sentinelRef.current;
if (!sentinel) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMoreRef.current();
}
},
{ rootMargin: '200px' }
);
observer.observe(sentinel);
return () => observer.disconnect();
}, []);
// 回到顶部按钮:用 IntersectionObserver 检测顶部哨兵是否离开视口 // 回到顶部按钮:用 IntersectionObserver 检测顶部哨兵是否离开视口
const topSentinelRef = useRef<HTMLDivElement>(null); const topSentinelRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@@ -1500,7 +1493,9 @@ export default function MonitoringView() {
</div> </div>
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest"></span> <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest"></span>
<span className="text-[9px] font-bold text-slate-300">{total} </span> <span className="text-[9px] font-bold text-slate-400">
{filteredVehicles.length} / {total}
</span>
</div> </div>
<div className="hidden grid-cols-[minmax(190px,1.1fr)_minmax(160px,1fr)_130px_130px] items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-2 text-[10px] font-bold text-slate-400 md:grid"> <div className="hidden grid-cols-[minmax(190px,1.1fr)_minmax(160px,1fr)_130px_130px] items-center gap-3 rounded-lg border border-slate-200 bg-white px-4 py-2 text-[10px] font-bold text-slate-400 md:grid">
<span> / </span> <span> / </span>
@@ -1615,16 +1610,40 @@ export default function MonitoringView() {
})} })}
</div> </div>
{filteredVehicles.length === 0 && !loadingMore && ( {filteredVehicles.length === 0 && !loadingMore && !pageError && (
<div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100"> <div className="py-10 text-center bg-white rounded-2xl border border-dashed border-slate-100">
<p className="text-xs font-bold text-slate-300"></p> <p className="text-xs font-bold text-slate-300"></p>
</div> </div>
)} )}
{/* 加载更多提示 */} {pageError && (
{loadingMore && ( <div className="rounded-xl border border-rose-100 bg-rose-50 px-3 py-3 text-center">
<div className="py-4 text-center"> <p className="text-[10px] font-bold text-rose-600">{pageError.message}</p>
<span className="text-[10px] font-bold text-slate-400">...</span> <button
type="button"
onClick={() => pageError.scope === 'more' ? loadMore() : loadFirstPage()}
disabled={pageLoading || loadingMore}
className="mt-2 inline-flex h-8 items-center gap-1.5 rounded-lg border border-rose-200 bg-white px-3 text-[10px] font-black text-rose-700 transition-colors hover:bg-rose-100 disabled:cursor-not-allowed disabled:opacity-60"
>
<RefreshCw size={12} className={pageLoading || loadingMore ? 'animate-spin' : ''} />
</button>
</div>
)}
{hasMore && !pageError && filteredVehicles.length > 0 && (
<div className="flex flex-col items-center gap-1.5 py-4">
<button
type="button"
onClick={loadMore}
disabled={loadingMore}
className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-4 text-[11px] font-black text-slate-700 shadow-sm transition-colors hover:border-blue-200 hover:text-blue-600 disabled:cursor-not-allowed disabled:opacity-60"
>
{loadingMore ? <RefreshCw size={13} className="animate-spin" /> : <ChevronDown size={14} />}
{loadingMore ? '加载中' : '加载更多'}
</button>
<span className="text-[9px] font-bold text-slate-300">
{filteredVehicles.length} / {total}
</span>
</div> </div>
)} )}
{!hasMore && filteredVehicles.length > 0 && ( {!hasMore && filteredVehicles.length > 0 && (
@@ -1632,8 +1651,6 @@ export default function MonitoringView() {
<span className="text-[10px] font-bold text-slate-300"> {total} </span> <span className="text-[10px] font-bold text-slate-300"> {total} </span>
</div> </div>
)} )}
{/* 哨兵元素:进入视口时触发加载更多 */}
<div ref={sentinelRef} className="h-1" />
</div> </div>
<VehicleDetailModal <VehicleDetailModal