feat: 实时监控改为瀑布流无限滚动+回到顶部
- 移除分页按钮,改为滚动触底自动加载下一页 - 滚动超过600px时显示蓝色回到顶部按钮 - 底部提示加载状态(加载中.../已加载全部 N 条) - 筛选/排序变化时自动重置为首页 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Truck, Search, Filter, ChevronDown,
|
||||
Maximize2, Minimize2, RotateCcw,
|
||||
ArrowUp, ArrowDown, ChevronLeft, ChevronRight,
|
||||
ArrowUp, ArrowDown, ChevronsUp,
|
||||
} from 'lucide-react';
|
||||
import type { MonitoringVehicle, MonitoringStats, MonitoringFilters } from './types';
|
||||
import { fetchMonitoring } from './api';
|
||||
@@ -111,20 +111,24 @@ export default function MonitoringView() {
|
||||
const [filterOptions, setFilterOptions] = useState<MonitoringFilters>({ departments: [], customers: [], plates: [] });
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [updatedAt, setUpdatedAt] = useState('');
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [showBackToTop, setShowBackToTop] = useState(false);
|
||||
const PAGE_SIZE = 50;
|
||||
const listEndRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const departments = filterOptions.departments;
|
||||
const plateNumbers = filterOptions.plates;
|
||||
const projects = filterOptions.customers;
|
||||
|
||||
const loadData = useCallback((p = page) => {
|
||||
// 加载首页数据
|
||||
const loadFirstPage = useCallback(() => {
|
||||
fetchMonitoring({
|
||||
sortBy,
|
||||
sortOrder,
|
||||
limit: PAGE_SIZE,
|
||||
page: p,
|
||||
page: 1,
|
||||
search: searchTerm || undefined,
|
||||
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||
customer: filterProject !== 'All' ? filterProject : undefined,
|
||||
@@ -133,22 +137,54 @@ export default function MonitoringView() {
|
||||
setStats(d.stats);
|
||||
setFilterOptions(d.filters);
|
||||
setTotal(d.total);
|
||||
setPage(d.page);
|
||||
setTotalPages(d.totalPages);
|
||||
setUpdatedAt(d.updatedAt);
|
||||
}).catch(() => {});
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page]);
|
||||
|
||||
// 筛选/排序变化时重置到第1页
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
loadData(1);
|
||||
setHasMore(d.page < d.totalPages);
|
||||
}).catch(() => {});
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject]);
|
||||
|
||||
// 翻页时加载
|
||||
// 加载更多
|
||||
const loadMore = useCallback(() => {
|
||||
if (loadingMore || !hasMore) return;
|
||||
const nextPage = page + 1;
|
||||
setLoadingMore(true);
|
||||
fetchMonitoring({
|
||||
sortBy,
|
||||
sortOrder,
|
||||
limit: PAGE_SIZE,
|
||||
page: nextPage,
|
||||
search: searchTerm || undefined,
|
||||
dept: filterDept !== 'All' ? filterDept : undefined,
|
||||
customer: filterProject !== 'All' ? filterProject : undefined,
|
||||
}).then(d => {
|
||||
setVehicles(prev => [...prev, ...d.vehicles]);
|
||||
setPage(nextPage);
|
||||
setHasMore(nextPage < d.totalPages);
|
||||
}).catch(() => {}).finally(() => setLoadingMore(false));
|
||||
}, [sortBy, sortOrder, searchTerm, filterDept, filterProject, page, loadingMore, hasMore]);
|
||||
|
||||
// 筛选/排序变化时重新加载
|
||||
useEffect(() => {
|
||||
loadData(page);
|
||||
}, [page]);
|
||||
loadFirstPage();
|
||||
}, [loadFirstPage]);
|
||||
|
||||
// 滚动触底检测 + 回到顶部按钮
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
const scrollY = window.scrollY;
|
||||
setShowBackToTop(scrollY > 600);
|
||||
const scrollHeight = document.documentElement.scrollHeight;
|
||||
const clientHeight = window.innerHeight;
|
||||
if (scrollHeight - scrollY - clientHeight < 200) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, [loadMore]);
|
||||
|
||||
const scrollToTop = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const filteredVehicles = vehicles;
|
||||
|
||||
@@ -706,11 +742,11 @@ export default function MonitoringView() {
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between px-2 mb-1">
|
||||
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">车辆详情清单</span>
|
||||
<span className="text-[9px] font-bold text-slate-300">{filteredVehicles.length} 条</span>
|
||||
<span className="text-[9px] font-bold text-slate-300">{total} 条</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-1.5">
|
||||
{filteredVehicles.slice(0, 60).map((v) => (
|
||||
{filteredVehicles.map((v) => (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
@@ -762,35 +798,40 @@ export default function MonitoringView() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{filteredVehicles.length === 0 && (
|
||||
{filteredVehicles.length === 0 && !loadingMore && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 分页控件 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-3 py-3">
|
||||
<button
|
||||
onClick={() => setPage(Math.max(1, page - 1))}
|
||||
disabled={page <= 1}
|
||||
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<ChevronLeft size={14} className="text-slate-600" />
|
||||
</button>
|
||||
<span className="text-[10px] font-bold text-slate-400">
|
||||
{page} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||
disabled={page >= totalPages}
|
||||
className="p-1.5 rounded-lg bg-white border border-slate-100 shadow-sm disabled:opacity-30 disabled:cursor-not-allowed hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<ChevronRight size={14} className="text-slate-600" />
|
||||
</button>
|
||||
{/* 加载更多提示 */}
|
||||
{loadingMore && (
|
||||
<div className="py-4 text-center">
|
||||
<span className="text-[10px] font-bold text-slate-400">加载中...</span>
|
||||
</div>
|
||||
)}
|
||||
{!hasMore && filteredVehicles.length > 0 && (
|
||||
<div className="py-4 text-center">
|
||||
<span className="text-[10px] font-bold text-slate-300">已加载全部 {total} 条</span>
|
||||
</div>
|
||||
)}
|
||||
<div ref={listEndRef} />
|
||||
</div>
|
||||
|
||||
{/* 回到顶部按钮 */}
|
||||
<AnimatePresence>
|
||||
{showBackToTop && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
onClick={scrollToTop}
|
||||
className="fixed bottom-20 right-4 md:bottom-6 md:right-6 z-40 w-10 h-10 bg-blue-600 text-white rounded-full shadow-lg shadow-blue-200 flex items-center justify-center active:scale-95 transition-transform"
|
||||
>
|
||||
<ChevronsUp size={18} />
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user