perf: make mileage pagination explicit
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user